diff --git a/index.d.ts b/index.d.ts index a2ae75a..39d094d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -18,7 +18,7 @@ declare namespace KoaProxies { } } - type IKoaProxiesOptionsFunc = (params: { [key: string]: string }, ctx: Koa.Context) => IBaseKoaProxiesOptions; + type IKoaProxiesOptionsFunc = (params: { [key: string]: string }, ctx: Koa.Context) => IBaseKoaProxiesOptions | false; type IKoaProxiesOptions = string | IBaseKoaProxiesOptions | IKoaProxiesOptionsFunc; } diff --git a/index.js b/index.js index 804af3d..f884618 100644 --- a/index.js +++ b/index.js @@ -3,8 +3,10 @@ */ const { URL } = require('url') const HttpProxy = require('http-proxy') +const httpProxyCommonUtil = require('http-proxy/lib/http-proxy/common') const pathMatch = require('path-match') const { v4: uuidv4 } = require('uuid') +const url = require('url') /** * Constants @@ -55,19 +57,14 @@ module.exports = (path, options) => { let opts if (typeof options === 'function') { opts = options.call(options, params, ctx) + if (opts === false) { + return next() + } } else { opts = Object.assign({}, options) } - // object-rest-spread is still in stage-3 - // https://github.com/tc39/proposal-object-rest-spread - const { logs, rewrite, events } = opts - const httpProxyOpts = Object.keys(opts) - .filter(n => ['logs', 'rewrite', 'events'].indexOf(n) < 0) - .reduce((prev, cur) => { - prev[cur] = opts[cur] - return prev - }, {}) + const { logs, rewrite, events, ...httpProxyOpts } = opts return new Promise((resolve, reject) => { ctx.req.oldPath = ctx.req.url @@ -77,7 +74,7 @@ module.exports = (path, options) => { } if (logs) { - typeof logs === 'function' ? logs(ctx, opts.target) : logger(ctx, opts.target) + typeof logs === 'function' ? logs(ctx, opts.target) : logger(ctx, httpProxyOpts) } if (events && typeof events === 'object') { @@ -126,6 +123,18 @@ module.exports = (path, options) => { module.exports.proxy = proxy -function logger (ctx, target) { - console.log('%s - %s %s proxy to -> %s', new Date().toISOString(), ctx.req.method, ctx.req.oldPath, new URL(ctx.req.url, target)) +function logger (ctx, httpProxyOpts) { + // Because the proxying is done by http-proxy, Getting correct proxied url needs imitating the behavior of http-proxy. **BE CAUTION** that here we rely on inner implementation of http-proxy. + // See https://github.com/http-party/node-http-proxy/blob/master/lib/http-proxy/common.js#L33 + const outgoing = {} + let dest + try { + // eslint-disable-next-line node/no-deprecated-api + const httpProxyOpts2 = { ...httpProxyOpts, target: url.parse(httpProxyOpts.target || '') } + httpProxyCommonUtil.setupOutgoing(outgoing, httpProxyOpts2, ctx.req) + dest = new URL('', `${httpProxyOpts2.target.protocol}//${outgoing.host}${outgoing.path}`) + } catch (err) { + console.error('Error occurs when logging. Please check if target is a valid URL.') + } + console.log('%s - %s %s proxy to -> %s', new Date().toISOString(), ctx.req.method, ctx.req.oldPath, `${dest || httpProxyOpts.target}`) } diff --git a/readme.md b/readme.md index 5a4fc83..cf0653a 100644 --- a/readme.md +++ b/readme.md @@ -37,7 +37,7 @@ options.logs = true; // or false // custom log function options.logs = (ctx, target) { console.log('%s - %s %s proxy to -> %s', new Date().toISOString(), ctx.req.method, ctx.req.oldPath, new URL(ctx.req.url, target)) -} +} ``` ## Usage @@ -52,13 +52,34 @@ const app = new Koa() // middleware app.use(proxy('/octocat', { - target: 'https://api.github.com/users', + target: 'https://api.github.com/users/', changeOrigin: true, agent: new httpsProxyAgent('http://1.2.3.4:88'), // if you need or just delete this line rewrite: path => path.replace(/^\/octocat(\/|\/\w+)?$/, '/vagusx'), logs: true })) ``` +The 2nd parameter `options` can be a function. It will be called with the path matching result (see [path-match](https://www.npmjs.com/package/path-match) for details) and Koa `ctx` object. You can leverage this feature to dynamically set proxy. Here is an example: + +```js +// dependencies +const Koa = require('koa') +const proxy = require('koa-proxies') + +const app = new Koa() + +// middleware +app.use(proxy('/octocat/:name', (params, ctx) => { + return { + target: 'https://api.github.com/', + changeOrigin: true, + rewrite: () => `/users/${params.name}`, + logs: true + }}) +) +``` +Moreover, if the `options` function return `false`, then the proxy will be bypassed. This allows the middleware to bail out even if path matching succeeds, which could be helpful if you need complex logic to determine whether to proxy or not. + ### Attention @@ -71,7 +92,7 @@ const proxy = require('koa-proxies') const bodyParser = require('koa-bodyparser') app.use(proxy('/user', { - target: 'http://example.com', + target: 'http://example.com', changeOrigin: true })) diff --git a/test/test.js b/test/test.js index ecce57b..7a66710 100644 --- a/test/test.js +++ b/test/test.js @@ -80,7 +80,7 @@ describe('tests for koa proxies', () => { expect(ret2).to.have.status(404) }) - it('test for options function', async () => { + it('test for options as function', async () => { // supports https://github.com/vagusX/koa-proxies/issues/17 const pathRegex = /^\/octocat(\/|\/\w+)?$/ const proxyMiddleware = proxy('/octocat', (params, ctx) => { @@ -134,6 +134,94 @@ describe('tests for koa proxies', () => { expect(ret3).to.have.status(500) }) + it('can leverage path matching params', async () => { + const proxyMiddleware = proxy('/octocat/:status', (params, ctx) => { + return { + target: 'http://127.0.0.1:12306', + changeOrigin: true, + rewrite: () => `/${params.status}`, + logs: true + } + }) + + server = startServer(3000, proxyMiddleware) + const requester = chai.request(server).keepOpen() + + const ret = await requester.get('/octocat/204') + expect(ret).to.have.status(204) + expect(ret).to.have.header('x-special-header', 'you see') + expect(ret.body).to.eqls({}) + + const ret1 = await requester.get('/octocat/200') + expect(ret1).to.have.status(200) + expect(ret1.body).to.eqls({ data: 'foo' }) + + const ret2 = await requester.get('/notfound') + expect(ret2).to.have.status(404) + + const ret3 = await requester.get('/octocat/500') + expect(ret3).to.have.status(500) + }) + + it('test for options as function which can return `false` value and get bypassed', async () => { + const pathRegex = /^\/octocat(\/|\/\w+)?$/ + const proxyMiddleware = proxy( + { + path: '/octocat' + }, + (params, ctx) => { + // require header matching + if (ctx.headers['x-custom-header'] !== 'custom header value') { + return false + } + return { + target: 'http://127.0.0.1:12306', + changeOrigin: true, + rewrite: path => { + if (pathRegex.test(path)) { + const [, subpath] = pathRegex.exec(path) + if (subpath && subpath.startsWith('/bar')) { + return '/200' + } + return path.replace(pathRegex, '/204') + } else { + return path + } + }, + logs: true + } + } + ) + server = startServer(3000, proxyMiddleware, async (ctx) => { + if (ctx.url.endsWith('baz')) { + ctx.body = { data: 'Hello test' } + } + }) + + // Match both path and headers + const requester = chai.request(server).keepOpen() + const ret = await requester.get('/octocat').set('x-custom-header', 'custom header value') + expect(ret).to.have.status(204) + expect(ret).to.have.header('x-special-header', 'you see') + expect(ret.body).to.eqls({}) + + // Match both path and headers + const ret2 = await requester.get('/octocat/bar').set('x-custom-header', 'custom header value') + expect(ret2).to.have.status(200) + expect(ret2.body).to.eqls({ data: 'foo' }) + + // If request only match path, it should not be proxied + const ret3 = await requester.get('/octocat').set('x-custom-header', 'custom header value not matched') + expect(ret3).to.have.status(404) + + const ret4 = await requester.get('/octocat/bar') // no header at all + expect(ret4).to.have.status(404) + + const ret5 = await requester.get('/octocat/bar/baz') // no header at all, but match other middleware + expect(ret5).to.have.status(200) + expect(ret5.body).to.eqls({ data: 'Hello test' }) + }) + it('should bypass when path not matched', async () => { const proxyMiddleware = proxy('/octocat', { target: 'http://127.0.0.1:12306', @@ -328,6 +416,25 @@ describe('tests for koa proxies', () => { console.log.restore() }) + it('log with correct outgoing url', async () => { + // spies + const logSpy = sinon.spy(console, 'log') + + const proxyMiddleware = proxy('/baz', { + target: 'http://127.0.0.1:12306/foo/bar', + changeOrigin: true, + prependPath: false, + logs: true + }) + + server = startServer(3000, proxyMiddleware) + + await chai.request(server).get('/baz') + const logSpyCall = logSpy.getCall(0) + chai.expect(logSpyCall.args).to.contains('http://127.0.0.1:12306/baz') + console.log.restore() + }) + it('log function', async () => { // spies const logSpy = sinon.spy() @@ -344,17 +451,59 @@ describe('tests for koa proxies', () => { sinon.assert.calledOnce(logSpy) }) - it.skip('test using github API', async () => { + it('log while error occurs', async () => { + // spies + const logSpy = sinon.spy(console, 'error') + + const proxyMiddleware = proxy('/200', { + target: 'abc.com', // should be prepended with http(s):// + changeOrigin: true, + logs: true + }) + + server = startServer(3000, proxyMiddleware) + + await chai.request(server).get('/200') + sinon.assert.called(logSpy) + console.error.restore() + }) + + it('test using github API', async () => { + const proxyReqSpy = sinon.spy() + const proxyMiddleware = proxy('/octocat', { - target: 'https://api.github.com/users', + target: 'https://api.github.com/users/', changeOrigin: true, rewrite: path => path.replace(/^\/octocat(\/|\/\w+)?$/, '/vagusx'), - logs: true + logs: true, + events: { + proxyReq: proxyReqSpy + } }) server = startServer(3000, proxyMiddleware) - const ret = await chai.request(server).get('/octocat') + const ret = await chai.request(server).get('/octocat').set('user-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.69') + expect(ret).to.have.status(200) + sinon.assert.called(proxyReqSpy) + const proxyReq = proxyReqSpy.args[0][0] + // notice that `proxyReq.protocol` would be undefined in node10 + expect(new URL('', `${proxyReq.protocol || 'https:'}//${proxyReq.getHeaders().host}${proxyReq.path}`).toString()).to.equal('https://api.github.com/users/vagusx') + }) + + it('test using github API with another configuration', async () => { + const proxyMiddleware = proxy('/octocat/:name', (params) => { + return { + target: 'https://api.github.com/', + changeOrigin: true, + rewrite: () => `/users/${params.name}`, + logs: true + } + }) + + server = startServer(3000, proxyMiddleware) + + const ret = await chai.request(server).get('/octocat/vagusx').set('user-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 Edg/118.0.2088.69') expect(ret).to.have.status(200) }) })