diff --git a/.gitignore b/.gitignore index f0aeaf5d..4c6d8505 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ typings/ .dynamodb/ # End of https://www.gitignore.io/api/node + +dist diff --git a/dist/config-factory.js b/dist/config-factory.js deleted file mode 100644 index a4c47092..00000000 --- a/dist/config-factory.js +++ /dev/null @@ -1,79 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const _ = require("lodash"); -const url = require("url"); -const errors_1 = require("./errors"); -const logger_1 = require("./logger"); -const logger = logger_1.getInstance(); -function createConfig(context, opts) { - // structure of config object to be returned - const config = { - context: undefined, - options: {} - }; - // app.use('/api', proxy({target:'http://localhost:9000'})); - if (isContextless(context, opts)) { - config.context = '/'; - config.options = _.assign(config.options, context); - // app.use('/api', proxy('http://localhost:9000')); - // app.use(proxy('http://localhost:9000/api')); - } - else if (isStringShortHand(context)) { - const oUrl = url.parse(context); - const target = [oUrl.protocol, '//', oUrl.host].join(''); - config.context = oUrl.pathname || '/'; - config.options = _.assign(config.options, { target }, opts); - if (oUrl.protocol === 'ws:' || oUrl.protocol === 'wss:') { - config.options.ws = true; - } - // app.use('/api', proxy({target:'http://localhost:9000'})); - } - else { - config.context = context; - config.options = _.assign(config.options, opts); - } - configureLogger(config.options); - if (!config.options.target) { - throw new Error(errors_1.ERRORS.ERR_CONFIG_FACTORY_TARGET_MISSING); - } - return config; -} -exports.createConfig = createConfig; -/** - * Checks if a String only target/config is provided. - * This can be just the host or with the optional path. - * - * @example - * app.use('/api', proxy('http://localhost:9000')); - * app.use(proxy('http://localhost:9000/api')); - * - * @param {String} context [description] - * @return {Boolean} [description] - */ -function isStringShortHand(context) { - if (_.isString(context)) { - return !!url.parse(context).host; - } -} -/** - * Checks if a Object only config is provided, without a context. - * In this case the all paths will be proxied. - * - * @example - * app.use('/api', proxy({target:'http://localhost:9000'})); - * - * @param {Object} context [description] - * @param {*} opts [description] - * @return {Boolean} [description] - */ -function isContextless(context, opts) { - return _.isPlainObject(context) && _.isEmpty(opts); -} -function configureLogger(options) { - if (options.logLevel) { - logger.setLevel(options.logLevel); - } - if (options.logProvider) { - logger.setProvider(options.logProvider); - } -} diff --git a/dist/context-matcher.js b/dist/context-matcher.js deleted file mode 100644 index 91144bce..00000000 --- a/dist/context-matcher.js +++ /dev/null @@ -1,81 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const isGlob = require("is-glob"); -const _ = require("lodash"); -const micromatch = require("micromatch"); -const url = require("url"); -const errors_1 = require("./errors"); -function match(context, uri, req) { - // single path - if (isStringPath(context)) { - return matchSingleStringPath(context, uri); - } - // single glob path - if (isGlobPath(context)) { - return matchSingleGlobPath(context, uri); - } - // multi path - if (Array.isArray(context)) { - if (context.every(isStringPath)) { - return matchMultiPath(context, uri); - } - if (context.every(isGlobPath)) { - return matchMultiGlobPath(context, uri); - } - throw new Error(errors_1.ERRORS.ERR_CONTEXT_MATCHER_INVALID_ARRAY); - } - // custom matching - if (_.isFunction(context)) { - const pathname = getUrlPathName(uri); - return context(pathname, req); - } - throw new Error(errors_1.ERRORS.ERR_CONTEXT_MATCHER_GENERIC); -} -exports.match = match; -/** - * @param {String} context '/api' - * @param {String} uri 'http://example.org/api/b/c/d.html' - * @return {Boolean} - */ -function matchSingleStringPath(context, uri) { - const pathname = getUrlPathName(uri); - return pathname.indexOf(context) === 0; -} -function matchSingleGlobPath(pattern, uri) { - const pathname = getUrlPathName(uri); - const matches = micromatch([pathname], pattern); - return matches && matches.length > 0; -} -function matchMultiGlobPath(patternList, uri) { - return matchSingleGlobPath(patternList, uri); -} -/** - * @param {String} contextList ['/api', '/ajax'] - * @param {String} uri 'http://example.org/api/b/c/d.html' - * @return {Boolean} - */ -function matchMultiPath(contextList, uri) { - let isMultiPath = false; - for (const context of contextList) { - if (matchSingleStringPath(context, uri)) { - isMultiPath = true; - break; - } - } - return isMultiPath; -} -/** - * Parses URI and returns RFC 3986 path - * - * @param {String} uri from req.url - * @return {String} RFC 3986 path - */ -function getUrlPathName(uri) { - return uri && url.parse(uri).pathname; -} -function isStringPath(context) { - return _.isString(context) && !isGlob(context); -} -function isGlobPath(context) { - return isGlob(context); -} diff --git a/dist/errors.js b/dist/errors.js deleted file mode 100644 index f00211d6..00000000 --- a/dist/errors.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -var ERRORS; -(function (ERRORS) { - ERRORS["ERR_CONFIG_FACTORY_TARGET_MISSING"] = "[HPM] Missing \"target\" option. Example: {target: \"http://www.example.org\"}"; - ERRORS["ERR_CONTEXT_MATCHER_GENERIC"] = "[HPM] Invalid context. Expecting something like: \"/api\" or [\"/api\", \"/ajax\"]"; - ERRORS["ERR_CONTEXT_MATCHER_INVALID_ARRAY"] = "[HPM] Invalid context. Expecting something like: [\"/api\", \"/ajax\"] or [\"/api/**\", \"!**.html\"]"; - ERRORS["ERR_PATH_REWRITER_CONFIG"] = "[HPM] Invalid pathRewrite config. Expecting object with pathRewrite config or a rewrite function"; -})(ERRORS = exports.ERRORS || (exports.ERRORS = {})); diff --git a/dist/handlers.js b/dist/handlers.js deleted file mode 100644 index 5e42902a..00000000 --- a/dist/handlers.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const _ = require("lodash"); -const logger_1 = require("./logger"); -const logger = logger_1.getInstance(); -function init(proxy, option) { - const handlers = getHandlers(option); - for (const eventName of Object.keys(handlers)) { - proxy.on(eventName, handlers[eventName]); - } - logger.debug('[HPM] Subscribed to http-proxy events:', Object.keys(handlers)); -} -exports.init = init; -function getHandlers(options) { - // https://github.com/nodejitsu/node-http-proxy#listening-for-proxy-events - const proxyEvents = [ - 'error', - 'proxyReq', - 'proxyReqWs', - 'proxyRes', - 'open', - 'close' - ]; - const handlers = {}; - for (const event of proxyEvents) { - // all handlers for the http-proxy events are prefixed with 'on'. - // loop through options and try to find these handlers - // and add them to the handlers object for subscription in init(). - const eventName = _.camelCase('on ' + event); - const fnHandler = _.get(options, eventName); - if (_.isFunction(fnHandler)) { - handlers[event] = fnHandler; - } - } - // add default error handler in absence of error handler - if (!_.isFunction(handlers.error)) { - handlers.error = defaultErrorHandler; - } - // add default close handler in absence of close handler - if (!_.isFunction(handlers.close)) { - handlers.close = logClose; - } - return handlers; -} -exports.getHandlers = getHandlers; -function defaultErrorHandler(err, req, res) { - const host = req.headers && req.headers.host; - const code = err.code; - if (res.writeHead && !res.headersSent) { - if (/HPE_INVALID/.test(code)) { - res.writeHead(502); - } - else { - switch (code) { - case 'ECONNRESET': - case 'ENOTFOUND': - case 'ECONNREFUSED': - res.writeHead(504); - break; - default: - res.writeHead(500); - } - } - } - res.end('Error occured while trying to proxy to: ' + host + req.url); -} -function logClose(req, socket, head) { - // view disconnected websocket connections - logger.info('[HPM] Client disconnected'); -} diff --git a/dist/http-proxy-middleware.js b/dist/http-proxy-middleware.js deleted file mode 100644 index 5f942029..00000000 --- a/dist/http-proxy-middleware.js +++ /dev/null @@ -1,138 +0,0 @@ -"use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const httpProxy = require("http-proxy"); -const _ = require("lodash"); -const config_factory_1 = require("./config-factory"); -const contextMatcher = require("./context-matcher"); -const handlers = require("./handlers"); -const logger_1 = require("./logger"); -const PathRewriter = require("./path-rewriter"); -const Router = require("./router"); -class HttpProxyMiddleware { - constructor(context, opts) { - this.logger = logger_1.getInstance(); - this.wsInternalSubscribed = false; - // https://github.com/Microsoft/TypeScript/wiki/'this'-in-TypeScript#red-flags-for-this - this.middleware = (req, res, next) => __awaiter(this, void 0, void 0, function* () { - if (this.shouldProxy(this.config.context, req)) { - const activeProxyOptions = this.prepareProxyRequest(req); - this.proxy.web(req, res, activeProxyOptions); - } - else { - next(); - } - if (this.proxyOptions.ws === true) { - // use initial request to access the server object to subscribe to http upgrade event - this.catchUpgradeRequest(req.connection.server); - } - }); - this.catchUpgradeRequest = server => { - server.on('upgrade', this.handleUpgrade); - // prevent duplicate upgrade handling; - // in case external upgrade is also configured - this.wsInternalSubscribed = true; - }; - this.handleUpgrade = (req, socket, head) => { - if (this.shouldProxy(this.config.context, req)) { - const activeProxyOptions = this.prepareProxyRequest(req); - this.proxy.ws(req, socket, head, activeProxyOptions); - this.logger.info('[HPM] Upgrading to WebSocket'); - } - }; - /** - * Determine whether request should be proxied. - * - * @private - * @param {String} context [description] - * @param {Object} req [description] - * @return {Boolean} - */ - this.shouldProxy = (context, req) => { - const path = req.originalUrl || req.url; - return contextMatcher.match(context, path, req); - }; - /** - * Apply option.router and option.pathRewrite - * Order matters: - * Router uses original path for routing; - * NOT the modified path, after it has been rewritten by pathRewrite - * @param {Object} req - * @return {Object} proxy options - */ - this.prepareProxyRequest = req => { - // https://github.com/chimurai/http-proxy-middleware/issues/17 - // https://github.com/chimurai/http-proxy-middleware/issues/94 - req.url = req.originalUrl || req.url; - // store uri before it gets rewritten for logging - const originalPath = req.url; - const newProxyOptions = _.assign({}, this.proxyOptions); - // Apply in order: - // 1. option.router - // 2. option.pathRewrite - this.applyRouter(req, newProxyOptions); - this.applyPathRewrite(req, this.pathRewriter); - // debug logging for both http(s) and websockets - if (this.proxyOptions.logLevel === 'debug') { - const arrow = logger_1.getArrow(originalPath, req.url, this.proxyOptions.target, newProxyOptions.target); - this.logger.debug('[HPM] %s %s %s %s', req.method, originalPath, arrow, newProxyOptions.target); - } - return newProxyOptions; - }; - // Modify option.target when router present. - this.applyRouter = (req, options) => { - let newTarget; - if (options.router) { - newTarget = Router.getTarget(req, options); - if (newTarget) { - this.logger.debug('[HPM] Router new target: %s -> "%s"', options.target, newTarget); - options.target = newTarget; - } - } - }; - // rewrite path - this.applyPathRewrite = (req, pathRewriter) => { - if (pathRewriter) { - const path = pathRewriter(req.url, req); - if (typeof path === 'string') { - req.url = path; - } - else { - this.logger.info('[HPM] pathRewrite: No rewritten path found. (%s)', req.url); - } - } - }; - this.logError = (err, req, res) => { - const hostname = (req.headers && req.headers.host) || (req.hostname || req.host); // (websocket) || (node0.10 || node 4/5) - const target = this.proxyOptions.target.host || this.proxyOptions.target; - const errorMessage = '[HPM] Error occurred while trying to proxy request %s from %s to %s (%s) (%s)'; - const errReference = 'https://nodejs.org/api/errors.html#errors_common_system_errors'; // link to Node Common Systems Errors page - this.logger.error(errorMessage, req.url, hostname, target, err.code || err, errReference); - }; - this.config = config_factory_1.createConfig(context, opts); - this.proxyOptions = this.config.options; - // create proxy - this.proxy = httpProxy.createProxyServer({}); - this.logger.info(`[HPM] Proxy created: ${this.config.context} -> ${this.proxyOptions.target}`); - this.pathRewriter = PathRewriter.createPathRewriter(this.proxyOptions.pathRewrite); // returns undefined when "pathRewrite" is not provided - // attach handler to http-proxy events - handlers.init(this.proxy, this.proxyOptions); - // log errors for debug purpose - this.proxy.on('error', this.logError); - // https://github.com/chimurai/http-proxy-middleware/issues/19 - // expose function to upgrade externally - this.middleware.upgrade = (req, socket, head) => { - if (!this.wsInternalSubscribed) { - this.handleUpgrade(req, socket, head); - } - }; - } -} -exports.HttpProxyMiddleware = HttpProxyMiddleware; diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 033d50d0..00000000 --- a/dist/index.js +++ /dev/null @@ -1,7 +0,0 @@ -"use strict"; -const http_proxy_middleware_1 = require("./http-proxy-middleware"); -function proxy(context, opts) { - const { middleware } = new http_proxy_middleware_1.HttpProxyMiddleware(context, opts); - return middleware; -} -module.exports = proxy; diff --git a/dist/logger.js b/dist/logger.js deleted file mode 100644 index 0a5ccb2b..00000000 --- a/dist/logger.js +++ /dev/null @@ -1,135 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const _ = require("lodash"); -const util = require("util"); -let loggerInstance; -const defaultProvider = { - // tslint:disable: no-console - log: console.log, - debug: console.log, - info: console.info, - warn: console.warn, - error: console.error -}; -// log level 'weight' -var LEVELS; -(function (LEVELS) { - LEVELS[LEVELS["debug"] = 10] = "debug"; - LEVELS[LEVELS["info"] = 20] = "info"; - LEVELS[LEVELS["warn"] = 30] = "warn"; - LEVELS[LEVELS["error"] = 50] = "error"; - LEVELS[LEVELS["silent"] = 80] = "silent"; -})(LEVELS || (LEVELS = {})); -function getInstance() { - if (!loggerInstance) { - loggerInstance = new Logger(); - } - return loggerInstance; -} -exports.getInstance = getInstance; -class Logger { - constructor() { - this.setLevel('info'); - this.setProvider(() => defaultProvider); - } - // log will log messages, regardless of logLevels - log() { - this.provider.log(this._interpolate.apply(null, arguments)); - } - debug() { - if (this._showLevel('debug')) { - this.provider.debug(this._interpolate.apply(null, arguments)); - } - } - info() { - if (this._showLevel('info')) { - this.provider.info(this._interpolate.apply(null, arguments)); - } - } - warn() { - if (this._showLevel('warn')) { - this.provider.warn(this._interpolate.apply(null, arguments)); - } - } - error() { - if (this._showLevel('error')) { - this.provider.error(this._interpolate.apply(null, arguments)); - } - } - setLevel(v) { - if (this.isValidLevel(v)) { - this.logLevel = v; - } - } - setProvider(fn) { - if (fn && this.isValidProvider(fn)) { - this.provider = fn(defaultProvider); - } - } - isValidProvider(fnProvider) { - const result = true; - if (fnProvider && !_.isFunction(fnProvider)) { - throw new Error('[HPM] Log provider config error. Expecting a function.'); - } - return result; - } - isValidLevel(levelName) { - const validLevels = Object.keys(LEVELS); - const isValid = validLevels.includes(levelName); - if (!isValid) { - throw new Error('[HPM] Log level error. Invalid logLevel.'); - } - return isValid; - } - /** - * Decide to log or not to log, based on the log levels 'weight' - * @param {String} showLevel [debug, info, warn, error, silent] - * @return {Boolean} - */ - _showLevel(showLevel) { - let result = false; - const currentLogLevel = LEVELS[this.logLevel]; - if (currentLogLevel && currentLogLevel <= LEVELS[showLevel]) { - result = true; - } - return result; - } - // make sure logged messages and its data are return interpolated - // make it possible for additional log data, such date/time or custom prefix. - _interpolate() { - const fn = _.spread(util.format); - const result = fn(_.slice(arguments)); - return result; - } -} -/** - * -> normal proxy - * => router - * ~> pathRewrite - * ≈> router + pathRewrite - * - * @param {String} originalPath - * @param {String} newPath - * @param {String} originalTarget - * @param {String} newTarget - * @return {String} - */ -function getArrow(originalPath, newPath, originalTarget, newTarget) { - const arrow = ['>']; - const isNewTarget = originalTarget !== newTarget; // router - const isNewPath = originalPath !== newPath; // pathRewrite - if (isNewPath && !isNewTarget) { - arrow.unshift('~'); - } - else if (!isNewPath && isNewTarget) { - arrow.unshift('='); - } - else if (isNewPath && isNewTarget) { - arrow.unshift('≈'); - } - else { - arrow.unshift('-'); - } - return arrow.join(''); -} -exports.getArrow = getArrow; diff --git a/dist/path-rewriter.js b/dist/path-rewriter.js deleted file mode 100644 index 5f1ae7ea..00000000 --- a/dist/path-rewriter.js +++ /dev/null @@ -1,67 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const _ = require("lodash"); -const errors_1 = require("./errors"); -const logger_1 = require("./logger"); -const logger = logger_1.getInstance(); -/** - * Create rewrite function, to cache parsed rewrite rules. - * - * @param {Object} rewriteConfig - * @return {Function} Function to rewrite paths; This function should accept `path` (request.url) as parameter - */ -function createPathRewriter(rewriteConfig) { - let rulesCache; - if (!isValidRewriteConfig(rewriteConfig)) { - return; - } - if (_.isFunction(rewriteConfig)) { - const customRewriteFn = rewriteConfig; - return customRewriteFn; - } - else { - rulesCache = parsePathRewriteRules(rewriteConfig); - return rewritePath; - } - function rewritePath(path) { - let result = path; - _.forEach(rulesCache, rule => { - if (rule.regex.test(path)) { - result = result.replace(rule.regex, rule.value); - logger.debug('[HPM] Rewriting path from "%s" to "%s"', path, result); - return false; - } - }); - return result; - } -} -exports.createPathRewriter = createPathRewriter; -function isValidRewriteConfig(rewriteConfig) { - if (_.isFunction(rewriteConfig)) { - return true; - } - else if (!_.isEmpty(rewriteConfig) && _.isPlainObject(rewriteConfig)) { - return true; - } - else if (_.isUndefined(rewriteConfig) || - _.isNull(rewriteConfig) || - _.isEqual(rewriteConfig, {})) { - return false; - } - else { - throw new Error(errors_1.ERRORS.ERR_PATH_REWRITER_CONFIG); - } -} -function parsePathRewriteRules(rewriteConfig) { - const rules = []; - if (_.isPlainObject(rewriteConfig)) { - _.forIn(rewriteConfig, (value, key) => { - rules.push({ - regex: new RegExp(key), - value: rewriteConfig[key] - }); - logger.info('[HPM] Proxy rewrite rule created: "%s" ~> "%s"', key, rewriteConfig[key]); - }); - } - return rules; -} diff --git a/dist/router.js b/dist/router.js deleted file mode 100644 index 36b0f246..00000000 --- a/dist/router.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -const _ = require("lodash"); -const logger_1 = require("./logger"); -const logger = logger_1.getInstance(); -function getTarget(req, config) { - let newTarget; - const router = config.router; - if (_.isPlainObject(router)) { - newTarget = getTargetFromProxyTable(req, router); - } - else if (_.isFunction(router)) { - newTarget = router(req); - } - return newTarget; -} -exports.getTarget = getTarget; -function getTargetFromProxyTable(req, table) { - let result; - const host = req.headers.host; - const path = req.url; - const hostAndPath = host + path; - _.forIn(table, (value, key) => { - if (containsPath(key)) { - if (hostAndPath.indexOf(key) > -1) { - // match 'localhost:3000/api' - result = table[key]; - logger.debug('[HPM] Router table match: "%s"', key); - return false; - } - } - else { - if (key === host) { - // match 'localhost:3000' - result = table[key]; - logger.debug('[HPM] Router table match: "%s"', host); - return false; - } - } - }); - return result; -} -function containsPath(v) { - return v.indexOf('/') > -1; -} diff --git a/package.json b/package.json index 0ce52fb8..2c96dfbc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dist" ], "scripts": { - "clean": "rm -rf coverage", + "clean": "rm -rf dist && rm -rf coverage", "lint": "yarn lint:prettier && yarn lint:tslint", "lint:prettier": "prettier --check \"**/*.{js,ts,md}\"", "lint:tslint": "yarn tslint -c tslint.json '{lib,test}/**/*.ts'", @@ -20,7 +20,7 @@ "precoveralls": "yarn clean && yarn build", "coveralls": "jest --runInBand --coverage --coverageReporters=text-lcov | coveralls", "postcoveralls": "yarn clean", - "prepublish": "yarn build" + "prepare": "yarn clean && yarn build" }, "repository": { "type": "git", diff --git a/src/http-proxy-middleware.ts b/src/http-proxy-middleware.ts index b8757757..e1a0d6fe 100644 --- a/src/http-proxy-middleware.ts +++ b/src/http-proxy-middleware.ts @@ -22,9 +22,7 @@ export class HttpProxyMiddleware { // create proxy this.proxy = httpProxy.createProxyServer({}); this.logger.info( - `[HPM] Proxy created: ${this.config.context} -> ${ - this.proxyOptions.target - }` + `[HPM] Proxy created: ${this.config.context} -> ${this.proxyOptions.target}` ); this.pathRewriter = PathRewriter.createPathRewriter(