Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: options function return false, fix default logger #74

Merged
merged 2 commits into from
Nov 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
159 changes: 154 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,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 () => {
vagusX marked this conversation as resolved.
Show resolved Hide resolved
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)
})
})
Loading