Skip to content

Commit

Permalink
feat: options function return false, fix loggger
Browse files Browse the repository at this point in the history
  • Loading branch information
xc1427 committed Nov 3, 2023
1 parent 15d3f1c commit 68c6943
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 21 deletions.
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
33 changes: 21 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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') {
Expand Down Expand Up @@ -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}`)
}
27 changes: 24 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
}))

Expand Down
155 changes: 150 additions & 5 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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()
Expand All @@ -344,17 +451,55 @@ 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 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: (proxyReq) => {
expect(new URL('', `${proxyReq.protocol}//${proxyReq.getHeaders().host}${proxyReq.path}`).toString()).to.equal('https://api.github.com/users/vagusx')
}
}
})

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)
})

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)
})
})

0 comments on commit 68c6943

Please sign in to comment.