56 | }
57 |
58 | interface Hooks {
59 | onRequest?: Function
60 | rewriteHeaders?: Function
61 | onResponse?: Function
62 | rewriteRequestHeaders?: Function
63 | request?: {
64 | timeout?: number
65 | [x: string]: any
66 | }
67 | queryString?: string
68 | [x: string]: any
69 | }
70 |
71 | interface Options {
72 | server?: Object | restana.Service
| Express.Application
73 | proxyFactory?: (opts: ProxyFactoryOpts) => Function | null | undefined
74 | restana?: {}
75 | middlewares?: Function[]
76 | pathRegex?: string
77 | timeout?: number
78 | targetOverride?: string
79 | enableServicesEndpoint?: boolean
80 | routes: (Route | WebSocketRoute)[]
81 | }
82 | }
83 |
84 | declare function fastgateway<
85 | P extends restana.Protocol = restana.Protocol.HTTP,
86 | >(opts?: fastgateway.Options
): restana.Service
87 |
88 | export = fastgateway
89 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* eslint-disable no-useless-call */
4 |
5 | const defaultProxyFactory = require('./lib/proxy-factory')
6 | const restana = require('restana')
7 | const defaultProxyHandler = (req, res, url, proxy, proxyOpts) =>
8 | proxy(req, res, url, proxyOpts)
9 | const DEFAULT_METHODS = require('restana/libs/methods').filter(
10 | (method) => method !== 'all'
11 | )
12 | const NOOP = (req, res) => {}
13 | const PROXY_TYPES = ['http', 'lambda']
14 | const registerWebSocketRoutes = require('./lib/ws-proxy')
15 |
16 | const gateway = (opts) => {
17 | let proxyFactory
18 |
19 | if (opts.proxyFactory) {
20 | proxyFactory = (...args) => {
21 | const result = opts.proxyFactory(...args)
22 | return result === undefined ? defaultProxyFactory(...args) : result
23 | }
24 | } else {
25 | proxyFactory = defaultProxyFactory
26 | }
27 |
28 | opts = Object.assign(
29 | {
30 | middlewares: [],
31 | pathRegex: '/*',
32 | enableServicesEndpoint: true
33 | },
34 | opts
35 | )
36 |
37 | const router = opts.server || restana(opts.restana)
38 |
39 | // registering global middlewares
40 | opts.middlewares.forEach((middleware) => {
41 | router.use(middleware)
42 | })
43 |
44 | // registering services.json
45 | const services = opts.routes.map((route) => ({
46 | prefix: route.prefix,
47 | docs: route.docs
48 | }))
49 | if (opts.enableServicesEndpoint) {
50 | router.get('/services.json', (req, res) => {
51 | res.statusCode = 200
52 | res.setHeader('Content-Type', 'application/json')
53 | res.end(JSON.stringify(services))
54 | })
55 | }
56 |
57 | // processing websocket routes
58 | const wsRoutes = opts.routes.filter(
59 | (route) => route.proxyType === 'websocket'
60 | )
61 | if (wsRoutes.length) {
62 | if (typeof router.getServer !== 'function') {
63 | throw new Error(
64 | 'Unable to retrieve the HTTP server instance. ' +
65 | 'If you are not using restana, make sure to provide an "app.getServer()" alternative method!'
66 | )
67 | }
68 | registerWebSocketRoutes({
69 | routes: wsRoutes,
70 | server: router.getServer()
71 | })
72 | }
73 |
74 | // processing non-websocket routes
75 | opts.routes
76 | .filter((route) => route.proxyType !== 'websocket')
77 | .forEach((route) => {
78 | if (undefined === route.prefixRewrite) {
79 | route.prefixRewrite = ''
80 | }
81 |
82 | // retrieve proxy type
83 | const { proxyType = 'http' } = route
84 | const isDefaultProxyType = PROXY_TYPES.includes(proxyType)
85 | if (!opts.proxyFactory && !isDefaultProxyType) {
86 | throw new Error(
87 | 'Unsupported proxy type, expecting one of ' + PROXY_TYPES.toString()
88 | )
89 | }
90 |
91 | // retrieve default hooks for proxy
92 | const hooksForDefaultType = isDefaultProxyType
93 | ? require('./lib/default-hooks')[proxyType]
94 | : {}
95 | const { onRequestNoOp = NOOP, onResponse = NOOP } = hooksForDefaultType
96 |
97 | // populating required NOOPS
98 | route.hooks = route.hooks || {}
99 | route.hooks.onRequest = route.hooks.onRequest || onRequestNoOp
100 | route.hooks.onResponse = route.hooks.onResponse || onResponse
101 |
102 | // populating route middlewares
103 | route.middlewares = route.middlewares || []
104 |
105 | // populating pathRegex if missing
106 | route.pathRegex =
107 | route.pathRegex === undefined ? opts.pathRegex : route.pathRegex
108 |
109 | // instantiate route proxy
110 | const proxy = proxyFactory({ opts, route, proxyType })
111 |
112 | // route proxy handler function
113 | const proxyHandler = route.proxyHandler || defaultProxyHandler
114 |
115 | // populating timeout config
116 | route.timeout = route.timeout || opts.timeout
117 |
118 | // registering route handlers
119 | const methods = route.methods || DEFAULT_METHODS
120 |
121 | const args = [
122 | // path
123 | route.prefix instanceof RegExp
124 | ? route.prefix
125 | : route.prefix + route.pathRegex,
126 | // route middlewares
127 | ...route.middlewares,
128 | // route handler
129 | handler(route, proxy, proxyHandler)
130 | ]
131 |
132 | methods.forEach((method) => {
133 | method = method.toLowerCase()
134 | if (router[method]) {
135 | router[method].apply(router, args)
136 | }
137 | })
138 | })
139 |
140 | return router
141 | }
142 |
143 | const handler = (route, proxy, proxyHandler) => async (req, res, next) => {
144 | const {
145 | urlRewrite,
146 | prefix,
147 | prefixRewrite,
148 | hooks,
149 | timeout,
150 | disableQsOverwrite
151 | } = route
152 | const { onRequest } = hooks
153 |
154 | try {
155 | if (typeof urlRewrite === 'function') {
156 | req.url = urlRewrite(req)
157 | } else if (typeof prefix === 'string') {
158 | req.url = req.url.replace(prefix, prefixRewrite)
159 | }
160 |
161 | const shouldAbortProxy = await onRequest(req, res)
162 | if (!shouldAbortProxy) {
163 | const proxyOpts = Object.assign(
164 | {
165 | request: {
166 | timeout: req.timeout || timeout
167 | },
168 | queryString: disableQsOverwrite ? null : req.query
169 | },
170 | route.hooks
171 | )
172 |
173 | proxyHandler(req, res, req.url, proxy, proxyOpts)
174 | }
175 | } catch (err) {
176 | return next(err)
177 | }
178 | }
179 |
180 | module.exports = gateway
181 |
--------------------------------------------------------------------------------
/lib/default-hooks.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const pump = require('pump')
4 | const toArray = require('stream-to-array')
5 | const TRANSFER_ENCODING_HEADER_NAME = 'transfer-encoding'
6 |
7 | module.exports = {
8 | websocket: {
9 | onOpenNoOp (ws, searchParams) {}
10 | },
11 | lambda: {
12 | onRequestNoOp (req, res) {},
13 | onResponse (req, res, response) {
14 | const { statusCode, body } = JSON.parse(response.Payload)
15 |
16 | res.statusCode = statusCode
17 | res.end(body)
18 | }
19 | },
20 | http: {
21 | onRequestNoOp (req, res) {},
22 | async onResponse (req, res, stream) {
23 | const chunked = stream.headers[TRANSFER_ENCODING_HEADER_NAME]
24 | ? stream.headers[TRANSFER_ENCODING_HEADER_NAME].endsWith('chunked')
25 | : false
26 |
27 | if (req.headers.connection === 'close' && chunked) {
28 | try {
29 | // remove transfer-encoding header
30 | const transferEncoding = stream.headers[
31 | TRANSFER_ENCODING_HEADER_NAME
32 | ].replace(/(,( )?)?chunked/, '')
33 | if (transferEncoding) {
34 | // header format includes many encodings, example: gzip, chunked
35 | res.setHeader(TRANSFER_ENCODING_HEADER_NAME, transferEncoding)
36 | } else {
37 | res.removeHeader(TRANSFER_ENCODING_HEADER_NAME)
38 | }
39 |
40 | if (!stream.headers['content-length']) {
41 | // pack all pieces into 1 buffer to calculate content length
42 | const resBuffer = Buffer.concat(await toArray(stream))
43 |
44 | // add content-length header and send the merged response buffer
45 | res.setHeader('content-length', '' + Buffer.byteLength(resBuffer))
46 | res.end(resBuffer)
47 | }
48 | } catch (err) {
49 | res.statusCode = 500
50 | res.end(err.message)
51 | }
52 | } else {
53 | res.statusCode = stream.statusCode
54 | pump(stream, res)
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/hostnames-hook.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | let micromatch
4 | try {
5 | micromatch = require('micromatch')
6 | } catch (e) {
7 | micromatch = {
8 | isMatch (value, pattern) {
9 | return value === pattern
10 | }
11 | }
12 | }
13 |
14 | module.exports = (hostname2prefix) => {
15 | const matches = {}
16 |
17 | return (req, res, cb) => {
18 | if (req.headers.host) {
19 | const hostHeader = req.headers.host.split(':')[0]
20 | let prefix = matches[hostHeader]
21 |
22 | if (!prefix) {
23 | for (const e of hostname2prefix) {
24 | if (micromatch.isMatch(hostHeader, e.hostname)) {
25 | prefix = e.prefix
26 | matches[hostHeader] = prefix
27 |
28 | break
29 | }
30 | }
31 | }
32 |
33 | if (prefix) {
34 | req.url = prefix + req.url
35 | }
36 | }
37 |
38 | return cb()
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/proxy-factory.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = (() => {
4 | let fastProxyLite, httpLambdaProxy, fastProxyLegacy
5 |
6 | return ({ proxyType, opts, route }) => {
7 | const base = opts.targetOverride || route.target
8 | const config = route.proxyConfig || {}
9 |
10 | switch (proxyType) {
11 | case 'http':
12 | fastProxyLite = fastProxyLite || require('fast-proxy-lite')
13 | return fastProxyLite({
14 | base,
15 | ...config
16 | }).proxy
17 |
18 | case 'lambda':
19 | httpLambdaProxy = httpLambdaProxy || require('http-lambda-proxy')
20 | return httpLambdaProxy({
21 | target: base,
22 | region: 'eu-central-1',
23 | ...config
24 | })
25 |
26 | case 'http-legacy':
27 | fastProxyLegacy = fastProxyLegacy || require('fast-proxy')
28 | return fastProxyLegacy({
29 | base,
30 | ...config
31 | }).proxy
32 |
33 | default:
34 | throw new Error(`Unsupported proxy type: ${proxyType}!`)
35 | }
36 | }
37 | })()
38 |
--------------------------------------------------------------------------------
/lib/ws-proxy.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const micromatch = require('micromatch')
4 | const { onOpenNoOp } = require('./default-hooks').websocket
5 |
6 | module.exports = (config) => {
7 | const WebSocket = require('faye-websocket')
8 |
9 | const { routes, server } = config
10 |
11 | routes.forEach((route) => {
12 | route._isMatch = micromatch.matcher(route.prefix)
13 | })
14 |
15 | server.on('upgrade', async (req, socket, body) => {
16 | if (WebSocket.isWebSocket(req)) {
17 | const url = new URL('http://fw' + req.url)
18 | const prefix = url.pathname || '/'
19 |
20 | const route = routes.find((route) => route._isMatch(prefix))
21 | if (route) {
22 | const subProtocols = route.subProtocols || []
23 | route.hooks = route.hooks || {}
24 | const onOpen = route.hooks.onOpen || onOpenNoOp
25 |
26 | const client = new WebSocket(req, socket, body, subProtocols)
27 |
28 | try {
29 | await onOpen(client, url.searchParams)
30 |
31 | const target =
32 | route.target + url.pathname + '?' + url.searchParams.toString()
33 | const remote = new WebSocket.Client(
34 | target,
35 | subProtocols,
36 | route.proxyConfig
37 | )
38 |
39 | client.pipe(remote)
40 | remote.pipe(client)
41 | } catch (err) {
42 | client.close(err.closeEventCode || 4500, err.message)
43 | }
44 | } else {
45 | socket.end()
46 | }
47 | }
48 | })
49 | }
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fast-gateway",
3 | "version": "4.2.0",
4 | "description": "A Node.js API Gateway for the masses!",
5 | "main": "index.js",
6 | "types": "index.d.ts",
7 | "scripts": {
8 | "test": "nyc mocha test/*.test.js",
9 | "format": "npx standard --fix",
10 | "lint": "npx standard",
11 | "ws-bench": "npx artillery run benchmark/websocket/artillery-perf1.yml"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/jkyberneees/fast-gateway.git"
16 | },
17 | "keywords": [
18 | "fast",
19 | "http",
20 | "proxy",
21 | "api",
22 | "gateway"
23 | ],
24 | "author": "Rolando Santamaria Maso ",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/jkyberneees/fast-gateway/issues"
28 | },
29 | "homepage": "https://github.com/jkyberneees/fast-gateway#readme",
30 | "dependencies": {
31 | "fast-proxy-lite": "^1.1.2",
32 | "http-cache-middleware": "^1.4.1",
33 | "micromatch": "^4.0.8",
34 | "restana": "^5.0.0",
35 | "stream-to-array": "^2.3.0"
36 | },
37 | "files": [
38 | "lib/",
39 | "index.js",
40 | "index.d.ts",
41 | "README.md",
42 | "LICENSE"
43 | ],
44 | "devDependencies": {
45 | "@types/node": "^22.13.11",
46 | "@types/express": "^5.0.0",
47 | "artillery": "^2.0.21",
48 | "aws-sdk": "^2.1691.0",
49 | "chai": "^4.5.0",
50 | "consistent-hash": "^1.2.2",
51 | "cors": "^2.8.5",
52 | "express": "^5.0.1",
53 | "express-jwt": "^7.7.8",
54 | "express-rate-limit": "^6.11.2",
55 | "faye-websocket": "^0.11.4",
56 | "fg-multiple-hooks": "^1.3.0",
57 | "helmet": "^7.2.0",
58 | "http-lambda-proxy": "^1.1.4",
59 | "load-balancers": "^1.3.52",
60 | "mocha": "^10.8.2",
61 | "nyc": "^17.1.0",
62 | "pem": "^1.14.8",
63 | "request-ip": "^3.3.0",
64 | "response-time": "^2.3.3",
65 | "supertest": "^7.0.0"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/test/config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-regex-literals */
2 |
3 | 'use strict'
4 |
5 | const pump = require('pump')
6 |
7 | module.exports = async () => {
8 | return {
9 | timeout: 1.5 * 1000,
10 |
11 | middlewares: [require('cors')(), require('http-cache-middleware')()],
12 |
13 | routes: [
14 | {
15 | pathRegex: '',
16 | prefix: '/endpoint-proxy',
17 | prefixRewrite: '/endpoint-proxy',
18 | target: 'http://localhost:3000',
19 | middlewares: [
20 | (req, res, next) => {
21 | req.cacheDisabled = true
22 |
23 | return next()
24 | }
25 | ],
26 | hooks: {
27 | async onRequest (req, res) {},
28 | onResponse (req, res, stream) {
29 | pump(stream, res)
30 | }
31 | }
32 | },
33 | {
34 | prefix: '/users/response-time',
35 | prefixRewrite: '',
36 | target: 'http://localhost:3000',
37 | middlewares: [require('response-time')()],
38 | hooks: {
39 | rewriteHeaders (headers) {
40 | headers['post-processed'] = true
41 |
42 | return headers
43 | }
44 | }
45 | },
46 | {
47 | prefix: new RegExp('/regex/.*'),
48 | target: 'http://localhost:5000',
49 | hooks: {
50 | async onRequest (req, res) {
51 | res.statusCode = 200
52 | res.end('Matched via Regular Expression!')
53 |
54 | return true
55 | }
56 | }
57 | },
58 | {
59 | prefix: '/users/proxy-aborted',
60 | target: 'http://localhost:5000',
61 | hooks: {
62 | async onRequest (req, res) {
63 | res.setHeader('x-cache-timeout', '1 second')
64 | res.statusCode = 200
65 | res.end('Hello World!')
66 |
67 | return true
68 | }
69 | }
70 | },
71 | {
72 | prefix: '/users/on-request-error',
73 | target: 'http://localhost:3000',
74 | hooks: {
75 | async onRequest (req, res) {
76 | throw new Error('ups, pre-processing error...')
77 | }
78 | }
79 | },
80 | {
81 | prefix: '/users',
82 | target: 'http://localhost:3000',
83 | docs: {
84 | name: 'Users Service',
85 | endpoint: 'swagger.json',
86 | type: 'swagger'
87 | }
88 | },
89 | {
90 | prefix: new RegExp('/users-regex/.*'),
91 | urlRewrite: (req) => req.url.replace('/users-regex', ''),
92 | target: 'http://localhost:3000',
93 | docs: {
94 | name: 'Users Service',
95 | endpoint: 'swagger.json',
96 | type: 'swagger'
97 | }
98 | },
99 | {
100 | pathRegex: '',
101 | prefix: '/endpoint-proxy-methods',
102 | urlRewrite: (req) => '/endpoint-proxy-methods',
103 | target: 'http://localhost:3000',
104 | methods: ['GET', 'POST']
105 | },
106 | {
107 | pathRegex: '',
108 | prefix: '/qs',
109 | prefixRewrite: '/qs',
110 | target: 'http://localhost:3000',
111 | methods: ['GET'],
112 | hooks: {
113 | onRequest: (req) => {
114 | req.query.name = 'fast-gateway'
115 | }
116 | }
117 | },
118 | {
119 | pathRegex: '',
120 | prefix: '/qs-no-overwrite',
121 | disableQsOverwrite: true,
122 | prefixRewrite: '/qs-no-overwrite',
123 | target: 'http://localhost:3000',
124 | methods: ['GET'],
125 | hooks: {
126 | onRequest: (req) => {
127 | req.query.name = 'fast-gateway'
128 | }
129 | }
130 | },
131 | {
132 | pathRegex: '',
133 | prefix: '/qs2',
134 | prefixRewrite: '/qs',
135 | target: 'http://localhost:3000',
136 | methods: ['GET'],
137 | hooks: {
138 | onRequest: (req) => {
139 | req.query.name = 'fast-gateway'
140 | },
141 | queryString: {
142 | name: 'qs-overwrite'
143 | }
144 | }
145 | },
146 | {
147 | pathRegex: '',
148 | prefix: '/endpoint-proxy-methods-put',
149 | prefixRewrite: '/endpoint-proxy-methods-put',
150 | target: 'http://localhost:3000',
151 | methods: ['PUT']
152 | },
153 | {
154 | prefix: '/lambda',
155 | proxyType: 'lambda',
156 | target: 'a-lambda-function-name',
157 | hooks: {
158 | async onRequest (req, res) {
159 | res.end('Go Serverless!')
160 |
161 | return true
162 | }
163 | }
164 | }
165 | ]
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/test/hostnames-hook.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* global describe, it */
4 | const expect = require('chai').expect
5 |
6 | describe('hostnames-hook', () => {
7 | let hostnamesHook = null
8 |
9 | it('initialize', async () => {
10 | hostnamesHook = require('./../lib/hostnames-hook')([
11 | {
12 | prefix: '/nodejs',
13 | hostname: 'nodejs.org'
14 | },
15 | {
16 | prefix: '/github',
17 | hostname: 'github.com'
18 | },
19 | {
20 | prefix: '/users',
21 | hostname: '*.company.tld'
22 | }
23 | ])
24 | })
25 |
26 | it('is match - nodejs.org', (cb) => {
27 | const req = {
28 | headers: {
29 | host: 'nodejs.org:443'
30 | },
31 | url: '/about'
32 | }
33 |
34 | hostnamesHook(req, null, () => {
35 | expect(req.url).to.equal('/nodejs/about')
36 | cb()
37 | })
38 | })
39 |
40 | it('is match - github.com', (cb) => {
41 | const req = {
42 | headers: {
43 | host: 'github.com:443'
44 | },
45 | url: '/about'
46 | }
47 |
48 | hostnamesHook(req, null, () => {
49 | expect(req.url).to.equal('/github/about')
50 | cb()
51 | })
52 | })
53 |
54 | it('is match - wildcard', (cb) => {
55 | const req = {
56 | headers: {
57 | host: 'kyberneees.company.tld:443'
58 | },
59 | url: '/about'
60 | }
61 |
62 | hostnamesHook(req, null, () => {
63 | expect(req.url).to.equal('/users/about')
64 | cb()
65 | })
66 | })
67 |
68 | it('is not match - 404', (cb) => {
69 | const req = {
70 | headers: {
71 | host: 'facebook.com:443'
72 | },
73 | url: '/about'
74 | }
75 |
76 | hostnamesHook(req, null, () => {
77 | expect(req.url).to.equal('/about')
78 | cb()
79 | })
80 | })
81 | })
82 |
--------------------------------------------------------------------------------
/test/services-endpoint.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* global describe, it, afterEach */
4 | const expect = require('chai').expect
5 | const request = require('supertest')
6 | const fastGateway = require('../index')
7 | const config = require('./config')
8 |
9 | describe('enableServicesEndpoint option', () => {
10 | let gateway
11 |
12 | afterEach(async () => {
13 | if (gateway && gateway.close) await gateway.close()
14 | gateway = null
15 | })
16 |
17 | it('should enable services endpoint by default', async () => {
18 | const customConfig = await config()
19 | gateway = await fastGateway(customConfig).start(8090)
20 | await request(gateway)
21 | .get('/services.json')
22 | .expect(200)
23 | .then((response) => {
24 | expect(response.body).to.be.an('array')
25 | /* eslint-disable-next-line no-unused-expressions */
26 | expect(response.body.find((service) => service.prefix === '/users') !== null).to.be.ok
27 | })
28 | })
29 |
30 | it('should enable services endpoint when enableServicesEndpoint=true', async () => {
31 | const customConfig = await config()
32 | gateway = await fastGateway(Object.assign({}, customConfig, { enableServicesEndpoint: true })).start(8091)
33 | await request(gateway)
34 | .get('/services.json')
35 | .expect(200)
36 | .then((response) => {
37 | expect(response.body).to.be.an('array')
38 | /* eslint-disable-next-line no-unused-expressions */
39 | expect(response.body.find((service) => service.prefix === '/users') !== null).to.be.ok
40 | })
41 | })
42 |
43 | it('should disable services endpoint when enableServicesEndpoint=false', async () => {
44 | const customConfig = await config()
45 | gateway = await fastGateway(Object.assign({}, customConfig, { enableServicesEndpoint: false })).start(8092)
46 | await request(gateway)
47 | .get('/services.json')
48 | .expect(404)
49 | })
50 | })
51 |
--------------------------------------------------------------------------------
/test/smoke.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* global describe, it */
4 | const expect = require('chai').expect
5 | const request = require('supertest')
6 | const fastGateway = require('../index')
7 | const config = require('./config')
8 |
9 | let remote, gateway
10 |
11 | describe('API Gateway', () => {
12 | it('initialize', async () => {
13 | // init gateway
14 | gateway = await fastGateway(await config()).start(8080)
15 |
16 | // init remote service
17 | remote = require('restana')({})
18 | remote.get('/endpoint-proxy', (req, res) =>
19 | res.send({
20 | name: 'endpoint-proxy'
21 | })
22 | )
23 | remote.get('/info', (req, res) =>
24 | res.send({
25 | name: 'fast-gateway'
26 | })
27 | )
28 | remote.get('/chunked', (req, res) => {
29 | res.write('user')
30 | res.write('1')
31 | res.end()
32 | })
33 | remote.get('/cache', (req, res) => {
34 | res.setHeader('x-cache-timeout', '1 second')
35 | res.send({
36 | time: new Date().getTime()
37 | })
38 | })
39 | remote.get('/cache-expire', (req, res) => {
40 | res.setHeader('x-cache-expire', 'GET/users/cache')
41 | res.send({})
42 | })
43 | remote.get('/cache-expire-pattern', (req, res) => {
44 | res.setHeader('x-cache-expire', 'GET/users/*')
45 | res.send({})
46 | })
47 | remote.get('/longop', (req, res) => {
48 | setTimeout(() => {
49 | res.send({})
50 | }, 2000)
51 | })
52 | remote.post('/204', (req, res) => res.send(204))
53 | remote.get('/endpoint-proxy-methods', (req, res) =>
54 | res.send({
55 | name: 'endpoint-proxy-methods'
56 | })
57 | )
58 | remote.put('/endpoint-proxy-methods-put', (req, res) =>
59 | res.send({
60 | name: 'endpoint-proxy-methods-put'
61 | })
62 | )
63 | remote.post('/endpoint-proxy-methods', (req, res) =>
64 | res.send({
65 | name: 'endpoint-proxy-methods'
66 | })
67 | )
68 | remote.get(['/qs-no-overwrite', '/qs'], (req, res) => {
69 | res.send(req.query)
70 | })
71 |
72 | await remote.start(3000)
73 | })
74 |
75 | it('services.json contains registered services', async () => {
76 | await request(gateway)
77 | .get('/services.json')
78 | .expect(200)
79 | .then((response) => {
80 | expect(
81 | response.body.find((service) => service.prefix === '/users')
82 | ).to.deep.equal({
83 | prefix: '/users',
84 | docs: {
85 | name: 'Users Service',
86 | endpoint: 'swagger.json',
87 | type: 'swagger'
88 | }
89 | })
90 | })
91 | })
92 |
93 | it('remote is proxied /users/response-time/204 - 204', async () => {
94 | await request(gateway).post('/users/response-time/204').expect(204)
95 | })
96 |
97 | it('(cors present) OPTIONS /users/response-time/info - 204', async () => {
98 | await request(gateway)
99 | .options('/users/response-time/info')
100 | .expect(204)
101 | .then((response) => {
102 | expect(response.header['access-control-allow-origin']).to.equal('*')
103 | })
104 | })
105 |
106 | it('(cors present) OPTIONS /users/info - 204', async () => {
107 | await request(gateway)
108 | .options('/users/info')
109 | .expect(204)
110 | .then((response) => {
111 | expect(response.header['access-control-allow-origin']).to.equal('*')
112 | })
113 | })
114 |
115 | it('(response-time not present) OPTIONS /users/info - 204', async () => {
116 | await request(gateway)
117 | .options('/users/info')
118 | .expect(204)
119 | .then((response) => {
120 | expect(response.header['x-response-time']).to.equal(undefined)
121 | })
122 | })
123 |
124 | it('(response-time present) GET /users/response-time/info - 200', async () => {
125 | await request(gateway)
126 | .get('/users/response-time/info')
127 | .expect(200)
128 | .then((response) => {
129 | expect(typeof response.header['x-response-time']).to.equal('string')
130 | })
131 | })
132 |
133 | it('(cache created 1) GET /users/cache - 200', async () => {
134 | await request(gateway)
135 | .get('/users/cache')
136 | .expect(200)
137 | .then((response) => {
138 | expect(response.headers['x-cache-hit']).to.equal(undefined)
139 | expect(typeof response.body.time).to.equal('number')
140 | })
141 | })
142 |
143 | it('(cache hit) GET /users/cache - 200', async () => {
144 | await request(gateway)
145 | .get('/users/cache')
146 | .expect(200)
147 | .then((response) => {
148 | expect(response.headers['x-cache-hit']).to.equal('1')
149 | expect(typeof response.body.time).to.equal('number')
150 | })
151 | })
152 |
153 | it('(cache expire) GET /users/cache-expire - 200', async () => {
154 | await request(gateway).get('/users/cache-expire').expect(200)
155 | })
156 |
157 | it('(cache created 2) GET /users/cache - 200', async () => {
158 | return request(gateway)
159 | .get('/users/cache')
160 | .expect(200)
161 | .then((response) => {
162 | expect(response.headers['x-cache-hit']).to.equal(undefined)
163 | })
164 | })
165 |
166 | it('(cache expire pattern) GET /users/cache-expire-pattern - 200', async () => {
167 | await request(gateway).get('/users/cache-expire-pattern').expect(200)
168 | })
169 |
170 | it('(cache created 3) GET /users/cache - 200', async () => {
171 | return request(gateway)
172 | .get('/users/cache')
173 | .expect(200)
174 | .then((response) => {
175 | expect(response.headers['x-cache-hit']).to.equal(undefined)
176 | })
177 | })
178 |
179 | it('Should timeout on GET /longop - 504', async () => {
180 | return request(gateway).get('/users/longop').expect(504)
181 | })
182 |
183 | it('GET /users/info - 200', async () => {
184 | await request(gateway)
185 | .get('/users/info')
186 | .expect(200)
187 | .then((response) => {
188 | expect(response.body.name).to.equal('fast-gateway')
189 | })
190 | })
191 |
192 | it('GET /users-regex/info - 200', async () => {
193 | await request(gateway)
194 | .get('/users-regex/info')
195 | .expect(200)
196 | .then((response) => {
197 | expect(response.body.name).to.equal('fast-gateway')
198 | })
199 | })
200 |
201 | it('GET /endpoint-proxy - 200', async () => {
202 | await request(gateway)
203 | .get('/endpoint-proxy')
204 | .expect(200)
205 | .then((response) => {
206 | expect(response.body.name).to.equal('endpoint-proxy')
207 | })
208 | })
209 |
210 | it('GET /endpoint-proxy-methods - 200', async () => {
211 | await request(gateway)
212 | .get('/endpoint-proxy-methods')
213 | .expect(200)
214 | .then((response) => {
215 | expect(response.body.name).to.equal('endpoint-proxy-methods')
216 | })
217 | })
218 |
219 | it('POST /endpoint-proxy-methods - 200', async () => {
220 | await request(gateway)
221 | .post('/endpoint-proxy-methods')
222 | .expect(200)
223 | .then((response) => {
224 | expect(response.body.name).to.equal('endpoint-proxy-methods')
225 | })
226 | })
227 |
228 | it('PUT /endpoint-proxy-methods - 404', async () => {
229 | await request(gateway).put('/endpoint-proxy-methods').expect(404)
230 | })
231 |
232 | it('PUT /endpoint-proxy-methods-put - 200', async () => {
233 | await request(gateway)
234 | .put('/endpoint-proxy-methods-put')
235 | .expect(200)
236 | .then((response) => {
237 | expect(response.body.name).to.equal('endpoint-proxy-methods-put')
238 | })
239 | })
240 |
241 | it('GET /endpoint-proxy-sdfsfsfsf - should fail with 404 because pathRegex=""', async () => {
242 | await request(gateway).get('/endpoint-proxy-sdfsfsfsf').expect(404)
243 | })
244 |
245 | it('(aggregation cache created) GET /users/proxy-aborted/info - 200', async () => {
246 | await request(gateway)
247 | .get('/users/proxy-aborted/info')
248 | .expect(200)
249 | .then((response) => {
250 | expect(response.text).to.equal('Hello World!')
251 | })
252 | })
253 |
254 | it('(aggregation) GET /regex/match - 200', async () => {
255 | await request(gateway)
256 | .get('/regex/match')
257 | .expect(200)
258 | .then((response) => {
259 | expect(response.text).to.equal('Matched via Regular Expression!')
260 | })
261 | })
262 |
263 | it('(aggregation) GET /regex/match/match/match - 200', async () => {
264 | await request(gateway)
265 | .get('/regex/match/match/match')
266 | .expect(200)
267 | .then((response) => {
268 | expect(response.text).to.equal('Matched via Regular Expression!')
269 | })
270 | })
271 |
272 | it('(aggregation cache created after expire) GET /users/proxy-aborted/info - 200', (done) => {
273 | setTimeout(() => {
274 | request(gateway)
275 | .get('/users/proxy-aborted/info')
276 | .expect(200)
277 | .then((response) => {
278 | expect(response.text).to.equal('Hello World!')
279 | expect(response.headers['x-cache-hit']).to.equal(undefined)
280 | done()
281 | })
282 | }, 1100)
283 | })
284 |
285 | it('POST /users/info - 404', async () => {
286 | await request(gateway).post('/users/info').expect(404)
287 | })
288 |
289 | it('(hooks) GET /users/response-time/info - 200', async () => {
290 | await request(gateway)
291 | .get('/users/response-time/info')
292 | .expect(200)
293 | .then((response) => {
294 | expect(response.header['post-processed']).to.equal('true')
295 | })
296 | })
297 |
298 | it('(hooks) GET /users/on-request-error/info - 500', async () => {
299 | await request(gateway)
300 | .get('/users/on-request-error/info')
301 | .expect(500)
302 | .then((response) => {
303 | expect(response.body.message).to.equal('ups, pre-processing error...')
304 | })
305 | })
306 |
307 | it('(Connection: close) chunked transfer-encoding support', async () => {
308 | await request(gateway)
309 | .get('/users/chunked')
310 | .set({ Connection: 'close' })
311 | .expect(200)
312 | .then((response) => {
313 | expect(response.text).to.equal('user1')
314 | })
315 | })
316 |
317 | it('(Connection: keep-alive) chunked transfer-encoding support', async () => {
318 | await request(gateway)
319 | .get('/users/chunked')
320 | .set('Connection', 'keep-alive')
321 | .then((res) => {
322 | expect(res.text).to.equal('user1')
323 | })
324 | })
325 |
326 | it('(Should overwrite query string using req.query) GET /qs - 200', async () => {
327 | await request(gateway)
328 | .get('/qs?name=nodejs&category=js')
329 | .expect(200)
330 | .then((response) => {
331 | expect(response.body.name).to.equal('fast-gateway')
332 | expect(response.body.category).to.equal('js')
333 | })
334 | })
335 |
336 | it('(Should NOT overwrite query string using req.query) GET /qs-no-overwrite - 200', async () => {
337 | await request(gateway)
338 | .get('/qs-no-overwrite?name=nodejs&category=js')
339 | .expect(200)
340 | .then((response) => {
341 | expect(response.body.name).to.equal('nodejs')
342 | expect(response.body.category).to.equal('js')
343 | })
344 | })
345 |
346 | it('(Should overwrite query string using queryString option) GET /qs2 - 200', async () => {
347 | await request(gateway)
348 | .get('/qs2?name=fast-gateway')
349 | .expect(200)
350 | .then((response) => {
351 | expect(response.body.name).to.equal('qs-overwrite')
352 | })
353 | })
354 |
355 | it('GET /lambda/hi', async () => {
356 | await request(gateway)
357 | .get('/lambda/hi')
358 | .then((res) => {
359 | expect(res.text).to.equal('Go Serverless!')
360 | })
361 | })
362 |
363 | it('close', async function () {
364 | this.timeout(10 * 1000)
365 |
366 | await remote.close()
367 | await gateway.close()
368 | })
369 | })
370 |
--------------------------------------------------------------------------------
/test/ws-proxy.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const gateway = require('../index')
4 | const WebSocket = require('faye-websocket')
5 | const http = require('http')
6 |
7 | /* global describe, it */
8 | const expect = require('chai').expect
9 |
10 | describe('ws-proxy', () => {
11 | let gw, echoServer
12 |
13 | it('initialize', async () => {
14 | echoServer = http.createServer()
15 | echoServer.on('upgrade', (request, socket, body) => {
16 | if (WebSocket.isWebSocket(request)) {
17 | const ws = new WebSocket(request, socket, body)
18 |
19 | ws.on('message', (event) => {
20 | ws.send(JSON.stringify({
21 | data: event.data,
22 | url: request.url
23 | }))
24 | })
25 | }
26 | })
27 | echoServer.listen(3000)
28 |
29 | gw = gateway({
30 | routes: [{
31 | proxyType: 'websocket',
32 | prefix: '/',
33 | target: 'ws://127.0.0.1:3000'
34 | }, {
35 | proxyType: 'websocket',
36 | prefix: '/echo',
37 | target: 'ws://127.0.0.1:3000'
38 | }, {
39 | proxyType: 'websocket',
40 | prefix: '/*-auth',
41 | target: 'ws://127.0.0.1:3000',
42 | hooks: {
43 | onOpen (ws, searchParams) {
44 | if (searchParams.get('accessToken') !== '12345') {
45 | const err = new Error('Unauthorized')
46 | err.closeEventCode = 4401
47 |
48 | throw err
49 | }
50 | }
51 | }
52 | }, {
53 | proxyType: 'websocket',
54 | prefix: '/echo-params',
55 | target: 'ws://127.0.0.1:3000',
56 | hooks: {
57 | onOpen (ws, searchParams) {
58 | searchParams.set('x-token', 'abc')
59 | }
60 | }
61 | }]
62 | })
63 |
64 | await gw.start(8080)
65 | })
66 |
67 | it('should echo using default prefix', (done) => {
68 | const ws = new WebSocket.Client('ws://127.0.0.1:8080')
69 | const msg = 'hello'
70 |
71 | ws.on('message', (event) => {
72 | const { data } = JSON.parse(event.data)
73 | expect(data).equals('hello')
74 |
75 | ws.close()
76 | done()
77 | })
78 |
79 | ws.send(msg)
80 | })
81 |
82 | it('should echo', (done) => {
83 | const ws = new WebSocket.Client('ws://127.0.0.1:8080/echo')
84 | const msg = 'hello'
85 |
86 | ws.on('message', (event) => {
87 | const { data } = JSON.parse(event.data)
88 | expect(data).equals('hello')
89 |
90 | ws.close()
91 | done()
92 | })
93 |
94 | ws.send(msg)
95 | })
96 |
97 | it('should fail auth', (done) => {
98 | const ws = new WebSocket.Client('ws://127.0.0.1:8080/echo-auth?accessToken=2')
99 | ws.on('close', (event) => {
100 | done()
101 | })
102 | })
103 |
104 | it('should pass auth', (done) => {
105 | const ws = new WebSocket.Client('ws://127.0.0.1:8080/echo-auth?accessToken=12345')
106 | const msg = 'hello'
107 |
108 | ws.on('message', (event) => {
109 | const { data } = JSON.parse(event.data)
110 | expect(data).equals('hello')
111 |
112 | ws.close()
113 | done()
114 | })
115 |
116 | ws.send(msg)
117 | })
118 |
119 | it('should rewrite search params', (done) => {
120 | const ws = new WebSocket.Client('ws://127.0.0.1:8080/echo-params')
121 | const msg = 'hello'
122 |
123 | ws.on('message', (event) => {
124 | const { url } = JSON.parse(event.data)
125 | expect(url).contains('?x-token=abc')
126 |
127 | ws.close()
128 | done()
129 | })
130 |
131 | ws.send(msg)
132 | })
133 |
134 | it('shutdown', async () => {
135 | await gw.close()
136 | echoServer.close()
137 | })
138 | })
139 |
--------------------------------------------------------------------------------