From 0a41072f07a2d5bc44ffb7f5ecee0d78e69c1797 Mon Sep 17 00:00:00 2001 From: Yusipov Asan Date: Wed, 14 Nov 2018 16:57:24 +0300 Subject: [PATCH] feat(middleware): Add customAuthorization middleware --- npm-shrinkwrap.json | 42 +++++++++++ package.json | 1 + src/base/index.js | 6 +- src/config/schemas.js | 19 ++++- src/servlet/corsproxy/index.js | 3 +- .../middlewares/customAuthorization.js | 45 ++++++++++++ src/servlet/corsproxy/middlewares/index.js | 4 +- .../middleware/customAuthorization.js | 72 +++++++++++++++++++ 8 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 src/servlet/corsproxy/middlewares/customAuthorization.js create mode 100644 test/servlet/corsproxy/middleware/customAuthorization.js diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 2d353ff..804e10b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1856,6 +1856,15 @@ "moment": "^2.11.2" } }, + "fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", + "requires": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -2604,6 +2613,11 @@ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=" + }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", @@ -3303,6 +3317,11 @@ "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true }, + "module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=" + }, "moment": { "version": "2.22.2", "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", @@ -5748,6 +5767,11 @@ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -5820,6 +5844,16 @@ "ipaddr.js": "1.6.0" } }, + "proxyquire": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.0.tgz", + "integrity": "sha512-kptdFArCfGRtQFv3Qwjr10lwbEV0TBJYvfqzhwucyfEXqVgmnAkyEw/S3FYzR5HI9i5QOq4rcqQjZ6AlknlCDQ==", + "requires": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.0", + "resolve": "~1.8.1" + } + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -6060,6 +6094,14 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "requires": { + "path-parse": "^1.0.5" + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", diff --git a/package.json b/package.json index 8d0dc2e..64c33d3 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "mocha-lcov-reporter": "^1.3.0", "mocha-sinon": "^2.1.0", "nyc": "^12.0.2", + "proxyquire": "^2.1.0", "reqresnext": "^1.5.1", "semantic-release": "^15.8.1", "sinon": "^6.1.4", diff --git a/src/base/index.js b/src/base/index.js index 2a515e9..d0ab59d 100644 --- a/src/base/index.js +++ b/src/base/index.js @@ -11,7 +11,8 @@ import { find, mapValues, assign, - isMatch + isMatch, + pick } from 'lodash-es'; export { @@ -26,5 +27,6 @@ export { find, mapValues, assign, - isMatch + isMatch, + pick }; \ No newline at end of file diff --git a/src/config/schemas.js b/src/config/schemas.js index 7215eb7..d6f51d5 100644 --- a/src/config/schemas.js +++ b/src/config/schemas.js @@ -11,6 +11,20 @@ export const HOST_ARRAY = {type: 'array', items: HOST}; export const STRING_NON_EMPTY = {type: 'string', minLength: 1}; export const INTEGER = {type: 'integer'}; + +const URL = { type: "string", pattern: "^https?://.+" }; +const STRING_ARRAY = { type: "array", items: STRING_NON_EMPTY }; +const CUSTOM_AUTHORIZATION = { + type: "object", + properties: { + targetUrl: URL, + authorizationUrl: URL, + headers: STRING_ARRAY, + authPath: STRING_NON_EMPTY + }, + required: ["targetUrl", "authorizationUrl", "headers", "authPath"] +}; + // TODO refactor export const SCHEMA = { type: 'object', @@ -34,7 +48,10 @@ export const SCHEMA = { type: 'object', patternProperties: { '.+': { - type: 'object' + type: 'object', + properties: { + customAuthorization: CUSTOM_AUTHORIZATION + }, } } } diff --git a/src/servlet/corsproxy/index.js b/src/servlet/corsproxy/index.js index aa5013a..26fec8b 100644 --- a/src/servlet/corsproxy/index.js +++ b/src/servlet/corsproxy/index.js @@ -5,7 +5,7 @@ import basicAuth from 'basic-auth'; import url from './url'; import Stats from './stats'; import Rules from './rules'; -import { pipe, cors, error, gatekeeper, from, to, end, intercept, logger, crumbs } from './middlewares'; +import { pipe, cors, error, gatekeeper, from, to, end, intercept, logger, crumbs, customAuthorization } from './middlewares'; export default class Server { constructor() { @@ -32,6 +32,7 @@ export default class Server { .use(cors) // NOTE Handles res.piped.headers .use(statsRes) .use(crumbs) + .use(customAuthorization) .use(end) .use(error) .disable('x-powered-by'); diff --git a/src/servlet/corsproxy/middlewares/customAuthorization.js b/src/servlet/corsproxy/middlewares/customAuthorization.js new file mode 100644 index 0000000..8d64b31 --- /dev/null +++ b/src/servlet/corsproxy/middlewares/customAuthorization.js @@ -0,0 +1,45 @@ +import request from 'request'; +import { get, pick } from '../../../base'; + +const normalizeUrl = url => url.replace(/^\//, ''); + +const checkUrl = (url, targetUrl) => normalizeUrl(url).indexOf(targetUrl) === 0; + +const extractAuthorization = (body, path) => { + try { + const json = JSON.parse(body); + return get(json, path); + } catch (error) { + return false; + } +}; + +const processRequest = (req, res, next, config) => { + const options = { + url: config.authorizationUrl, + headers: pick(req.headers, config.headers) + }; + request.get(options, (error, response, body) => { + const authorization = extractAuthorization(body, config.authPath); + if (authorization) { + const options = { + uri: normalizeUrl(req.url), + method: req.method, + headers: { + Authorization: authorization + } + }; + request(options).pipe(res); + } else { + next(error); + } + }); +}; + +export default (req, res, next) => { + const config = get(req, 'proxy.rule.customAuthorization'); + if (config && checkUrl(req.url, config.targetUrl)) { + return processRequest(req, res, next, config); + } + return next(); +}; diff --git a/src/servlet/corsproxy/middlewares/index.js b/src/servlet/corsproxy/middlewares/index.js index 0dc1a83..418cd02 100644 --- a/src/servlet/corsproxy/middlewares/index.js +++ b/src/servlet/corsproxy/middlewares/index.js @@ -8,6 +8,7 @@ import end from './end'; import intercept from './intercept'; import crumbs from './crumbs'; import logger from './logger'; +import customAuthorization from './customAuthorization'; export { cors, @@ -20,5 +21,6 @@ export { end, intercept, crumbs, - logger + logger, + customAuthorization }; \ No newline at end of file diff --git a/test/servlet/corsproxy/middleware/customAuthorization.js b/test/servlet/corsproxy/middleware/customAuthorization.js new file mode 100644 index 0000000..0bd40d5 --- /dev/null +++ b/test/servlet/corsproxy/middleware/customAuthorization.js @@ -0,0 +1,72 @@ +import proxyquire from 'proxyquire'; +import reqres from 'reqresnext'; + +const stubRequest = request => + proxyquire( + '../../../../src/servlet/corsproxy/middlewares/customAuthorization', + { request: request } + ).default; + +describe('corsproxy.middleware.customAuthorization', () => { + const authBody = '{"key1":{"key2":"SuchSecretMuchSecurity"}}'; + const rule = { + customAuthorization: { + targetUrl: 'http://target', + authorizationUrl: 'http://authorization', + headers: ['authorization', 'additionalHeader'], + authPath: 'key1.key2' + } + }; + const proxy = { rule }; + const headers = { + authorization: '1', + additionalHeader: '2', + badHeader: '3' + }; + const expectedHeaders = { + authorization: '1', + additionalHeader: '2' + }; + const targetUrl = '/http://target'; + const otherUrl = '/http://other'; + + it('exchanges headers to Authorization', () => { + let authReqOpts = {}; + let proxyedReqOpts = {}; + const request = sinon.stub().callsFake((opts, cb) => { + proxyedReqOpts = opts; + return { pipe: sinon.stub() }; + }); + sinon.stub(request, 'get').callsFake((opts, cb) => { + authReqOpts = opts; + cb(null, {}, authBody); + }); + const customAuthorization = stubRequest(request); + + const { req, res, next } = reqres( + { proxy, url: targetUrl, headers }, + {} + ); + customAuthorization(req, res, next); + + expect(authReqOpts.headers).to.deep.equal(expectedHeaders); + expect(proxyedReqOpts.headers.Authorization).to.be.equal( + 'SuchSecretMuchSecurity' + ); + }); + + it('does nothing to urls not from config', () => { + const request = sinon.stub(); + const customAuthorization = stubRequest(request); + const pass = sinon.stub(); + + const { req, res, next } = reqres( + { proxy, url: otherUrl, headers }, + {} + ); + customAuthorization(req, res, pass); + + expect(pass.called).to.be.true; + expect(request.called).to.be.false; + }); +});