├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md └── workflows │ └── tests.yaml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── benchmark ├── gateway-cluster.js ├── gateway.js ├── service.js └── websocket │ ├── artillery-perf1.yml │ └── echo.js ├── demos ├── basic-express.js ├── basic-hostnames.js ├── basic.js ├── before-end-middleware.js ├── circuitbreaker.js ├── consistent-hashing.js ├── hooks-ts-example.ts ├── hooks.js ├── https-example.js ├── load-balancer-multiple-proxies.js ├── multiple-hooks.js ├── post-processing.js ├── rate-limit.js ├── ssl-termination.js ├── target-load-balancer.js ├── url-rewrite.js └── ws-proxy.js ├── docs ├── .nojekyll ├── CNAME ├── README.md ├── fast-gateway-logo.svg └── index.html ├── index.d.ts ├── index.js ├── lib ├── default-hooks.js ├── hostnames-hook.js ├── proxy-factory.js └── ws-proxy.js ├── package.json └── test ├── config.js ├── hostnames-hook.test.js ├── services-endpoint.test.js ├── smoke.test.js └── ws-proxy.test.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: https://www.paypal.me/kyberneees 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Have you contributed with at least a little donation to support the development of this project?** 11 | - Paypal: https://www.paypal.me/kyberneees 12 | - [TRON](https://www.binance.com/en/buy-TRON) Wallet: `TJ5Bbf9v4kpptnRsePXYDvnYcYrS5Tyxus` 13 | 14 | **Is your feature request related to a problem? Please describe.** 15 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 16 | 17 | **Describe the solution you'd like** 18 | A clear and concise description of what you want to happen. 19 | 20 | **Describe alternatives you've considered** 21 | A clear and concise description of any alternative solutions or features you've considered. 22 | 23 | **Additional context** 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | testing: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v1 9 | - name: Setup Environment (Using NodeJS 22.x) 10 | uses: actions/setup-node@v1 11 | with: 12 | node-version: 22.x 13 | 14 | - name: Install dependencies 15 | run: npm install 16 | 17 | - name: Linting 18 | run: npx standard 19 | 20 | - name: Run tests 21 | run: npm run test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .DS_Store 64 | 65 | bun.lockb -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | test/ 3 | demos/ 4 | .nyc_output 5 | .github/ 6 | .travis.yml 7 | .benchmark/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.yaml 2 | *.yml 3 | *.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "semi": false 5 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kyberneees@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rolando Santamaria Maso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | [![tests](https://github.com/BackendStack21/fast-gateway/actions/workflows/tests.yaml/badge.svg)](https://github.com/BackendStack21/fast-gateway/actions/workflows/tests.yaml) 3 | [![NPM version](https://badgen.net/npm/v/fast-gateway)](https://www.npmjs.com/package/fast-gateway) 4 | [![NPM Total Downloads](https://badgen.net/npm/dt/fast-gateway)](https://www.npmjs.com/package/fast-gateway) 5 | [![License](https://badgen.net/npm/license/fast-gateway)](https://www.npmjs.com/package/fast-gateway) 6 | [![TypeScript support](https://badgen.net/npm/types/fast-gateway)](https://www.npmjs.com/package/fast-gateway) 7 | [![Github stars](https://badgen.net/github/stars/jkyberneees/fast-gateway?icon=github)](https://github.com/jkyberneees/fast-gateway) 8 | 9 | 10 | 11 | A super fast, framework agnostic Node.js API Gateway for the masses ❤️ 12 | *Docker images: https://hub.docker.com/repository/docker/kyberneees/rproxy* 13 | > Since v2.3.0, [AWS Lambda](https://www.youtube.com/watch?v=EBSdyoO3goc) proxying integration is supported via [`http-lambda-proxy`](https://www.npmjs.com/package/http-lambda-proxy) 🔥 14 | > Since v3.1.0, WebSockets proxying is supported via [`faye-websocket`](https://www.npmjs.com/package/faye-websocket) 🔥 15 | 16 | Read more online: 17 | - A “.js” API Gateway for the masses: https://itnext.io/a-js-api-gateway-for-the-masses-a12fdb9e961c 18 | 19 | ## Install 20 | ```js 21 | npm i fast-gateway 22 | ``` 23 | 24 | ## Usage 25 | ### Gateway 26 | ```js 27 | const gateway = require('fast-gateway') 28 | const server = gateway({ 29 | routes: [{ 30 | prefix: '/service', 31 | target: 'http://127.0.0.1:3000' 32 | }] 33 | }) 34 | 35 | server.start(8080) 36 | ``` 37 | ### Remote Service 38 | ```js 39 | const service = require('restana')() 40 | service.get('/get', (req, res) => res.send('Hello World!')) 41 | 42 | service.start(3000) 43 | ``` 44 | ### Testing 45 | ```bash 46 | curl -v http://127.0.0.1:8080/service/get 47 | ``` 48 | ## More 49 | - Website and documentation: https://fgw.21no.de -------------------------------------------------------------------------------- /benchmark/gateway-cluster.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const cluster = require('cluster') 4 | const os = require('os') 5 | 6 | if (cluster.isMaster) { 7 | const cpuCount = os.cpus().length 8 | for (let i = 0; i < cpuCount; i++) { 9 | cluster.fork() 10 | } 11 | } else { 12 | const gateway = require('../index') 13 | 14 | const server = gateway({ 15 | routes: [{ 16 | prefix: '/service', 17 | target: 'http://127.0.0.1:3000' 18 | }] 19 | }) 20 | 21 | server.start(8080) 22 | } 23 | 24 | cluster.on('exit', (worker) => { 25 | cluster.fork() 26 | }) 27 | -------------------------------------------------------------------------------- /benchmark/gateway.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('../index') 4 | 5 | const server = gateway({ 6 | routes: [{ 7 | prefix: '/service', 8 | target: 'http://127.0.0.1:3000' 9 | }] 10 | }) 11 | 12 | server.start(8080) 13 | -------------------------------------------------------------------------------- /benchmark/service.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const service = require('restana')() 4 | service.get('/get', (req, res) => res.send('Hello World!')) 5 | 6 | service.start(3000) 7 | -------------------------------------------------------------------------------- /benchmark/websocket/artillery-perf1.yml: -------------------------------------------------------------------------------- 1 | config: 2 | target: "ws://localhost:8080/echo" 3 | phases: 4 | - duration: 15 5 | arrivalRate: 20 6 | rampTo: 500 7 | name: "Ramping up the load" 8 | scenarios: 9 | - engine: "ws" 10 | flow: 11 | - send: 'echo me' -------------------------------------------------------------------------------- /benchmark/websocket/echo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('../../index') 4 | const WebSocket = require('faye-websocket') 5 | const http = require('http') 6 | 7 | gateway({ 8 | routes: [{ 9 | proxyType: 'websocket', 10 | prefix: '/echo', 11 | target: 'ws://127.0.0.1:3000' 12 | }] 13 | }).start(8080) 14 | 15 | const service = http.createServer() 16 | service.on('upgrade', (request, socket, body) => { 17 | if (WebSocket.isWebSocket(request)) { 18 | const ws = new WebSocket(request, socket, body) 19 | 20 | ws.on('message', (event) => { 21 | ws.send(event.data) 22 | }) 23 | } 24 | }) 25 | service.listen(3000) 26 | -------------------------------------------------------------------------------- /demos/basic-express.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-regex-literals */ 2 | 3 | 'use strict' 4 | 5 | const gateway = require('../index') 6 | const express = require('express') 7 | const PORT = process.env.PORT || 8080 8 | 9 | gateway({ 10 | server: express(), 11 | 12 | middlewares: [require('cors')(), require('helmet')()], 13 | 14 | routes: [ 15 | { 16 | prefix: new RegExp('/public/.*'), // Express.js v5 requires a RegExp object 17 | // prefix: '/public', // Compatible with Express.js v4, 18 | 19 | urlRewrite: (req) => req.url.replace('/public', ''), 20 | target: 'http://localhost:3000', 21 | docs: { 22 | name: 'Public Service', 23 | endpoint: 'swagger.json', 24 | type: 'swagger' 25 | } 26 | }, 27 | { 28 | prefix: new RegExp('/admin/.*'), // Express.js v5 requires a RegExp object 29 | // prefix: '/admin', // Compatible with Express.js v4, 30 | target: 'http://localhost:3001', 31 | middlewares: [ 32 | /* 33 | require('express-jwt').expressjwt({ 34 | secret: 'shhhhhhared-secret', 35 | algorithms: ['HS256'], 36 | }), 37 | */ 38 | ] 39 | } 40 | ] 41 | }).listen(PORT, () => { 42 | console.log(`API Gateway listening on ${PORT} port!`) 43 | }) 44 | 45 | const service1 = require('restana')({}) 46 | service1 47 | .get('/hi', (req, res) => res.send('Hello World!')) 48 | .start(3000) 49 | .then(() => console.log('Public service listening on 3000 port!')) 50 | 51 | const service2 = require('restana')({}) 52 | service2 53 | .get('/admin/users', (req, res) => res.send([])) 54 | .start(3001) 55 | .then(() => console.log('Admin service listening on 3001 port!')) 56 | -------------------------------------------------------------------------------- /demos/basic-hostnames.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('../index') 4 | const PORT = process.env.PORT || 8080 5 | const http = require('http') 6 | const restana = require('restana') 7 | 8 | // binding hostnames to prefixes 9 | const hostnames2prefix = [{ 10 | prefix: '/api', 11 | hostname: 'api.company.tld' 12 | }] 13 | // instantiate hostnames hook, this will prefix request urls according to data in hostnames2prefix 14 | const hostnamesHook = require('./../lib/hostnames-hook')(hostnames2prefix) 15 | 16 | // separately instantiate and configure restana application 17 | const app = restana() 18 | const server = http.createServer((req, res) => { 19 | hostnamesHook(req, res, () => { 20 | return app(req, res) 21 | }) 22 | }) 23 | 24 | // gateway configuration 25 | gateway({ 26 | server: app, // injecting existing restana application 27 | middlewares: [ 28 | ], 29 | 30 | routes: [{ 31 | prefix: '/api', 32 | target: 'http://localhost:3000' 33 | }] 34 | }) 35 | 36 | server.listen(PORT) 37 | 38 | const origin = require('restana')({}) 39 | origin 40 | .get('/hi', (req, res) => res.send('Hello World!')) 41 | .start(3000) 42 | -------------------------------------------------------------------------------- /demos/basic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('./../index') 4 | const PORT = process.env.PORT || 8080 5 | 6 | const { expressjwt: jwt } = require('express-jwt') 7 | 8 | gateway({ 9 | middlewares: [ 10 | require('cors')(), 11 | require('helmet')() 12 | ], 13 | 14 | routes: [{ 15 | prefix: '/public', 16 | target: 'http://localhost:3000' 17 | /* 18 | // applicable in case Swagger Definitions would be supported on downstream service 19 | docs: { 20 | name: 'Public Service', 21 | endpoint: 'swagger.json', 22 | type: 'swagger' 23 | } 24 | */ 25 | }, { 26 | prefix: '/admin', 27 | target: 'http://localhost:3001', 28 | middlewares: [ 29 | jwt({ 30 | secret: 'shhhhhhared-secret', 31 | algorithms: ['HS256'] 32 | }) 33 | ] 34 | }, { 35 | // this route definition makes http://localhost:3000 (/public) a default service if other routes prefixes are omitted 36 | prefix: '/*', 37 | pathRegex: '', 38 | target: 'http://localhost:3000' 39 | }] 40 | }).start(PORT).then(server => { 41 | console.log(`API Gateway listening on ${PORT} port!`) 42 | }) 43 | 44 | const service1 = require('restana')({}) 45 | service1 46 | .get('/hi', (req, res) => res.send('Hello World!')) 47 | .get('/hi-chunked', (req, res) => { 48 | res.write('Hello ') 49 | res.write('World!') 50 | res.end() 51 | }) 52 | .start(3000).then(() => console.log('Public service listening on 3000 port!')) 53 | 54 | const service2 = require('restana')({}) 55 | service2 56 | .get('/users', (req, res) => res.send([])) 57 | .start(3001).then(() => console.log('Admin service listening on 3001 port!')) 58 | -------------------------------------------------------------------------------- /demos/before-end-middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('./../index') 4 | const PORT = process.env.PORT || 8080 5 | 6 | const middleware503to404 = (req, res, next) => { 7 | const end = res.end 8 | res.end = function (...args) { 9 | if (res.statusCode === 503) { 10 | res.statusCode = 404 11 | } 12 | return end.apply(res, args) 13 | } 14 | 15 | return next() 16 | } 17 | 18 | gateway({ 19 | routes: [{ 20 | prefix: '/service', 21 | target: 'http://127.0.0.1:3000', 22 | middlewares: [ 23 | middleware503to404 24 | ] 25 | }] 26 | }).start(PORT).then(server => { 27 | console.log(`API Gateway listening on ${PORT} port!`) 28 | }) 29 | -------------------------------------------------------------------------------- /demos/circuitbreaker.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('../index') 4 | const onEnd = require('on-http-end') 5 | const CircuitBreaker = require('opossum') 6 | 7 | const options = { 8 | timeout: 1500, // If our function takes longer than "timeout", trigger a failure 9 | errorThresholdPercentage: 50, // When 50% of requests fail, trip the circuit 10 | resetTimeout: 30 * 1000 // After 30 seconds, try again. 11 | } 12 | const breaker = new CircuitBreaker(([req, res, url, proxy, proxyOpts]) => { 13 | return new Promise((resolve, reject) => { 14 | proxy(req, res, url, proxyOpts) 15 | onEnd(res, () => resolve()) // you can optionally evaluate response codes here... 16 | }) 17 | }, options) 18 | 19 | breaker.fallback(([req, res], err) => { 20 | if (err.code === 'EOPENBREAKER') { 21 | res.send({ 22 | message: 'Upps, looks like we are under heavy load. Please try again in 30 seconds!' 23 | }, 503) 24 | } 25 | }) 26 | 27 | gateway({ 28 | routes: [{ 29 | proxyHandler: (...params) => breaker.fire(params), 30 | prefix: '/public', 31 | target: 'http://localhost:3000' 32 | }] 33 | }).start(8080).then(() => console.log('API Gateway listening on 8080 port!')) 34 | 35 | const service = require('restana')({}) 36 | service 37 | .get('/longop', (req, res) => setTimeout(() => res.send('This operation will trigger the breaker failure counter...'), 2000)) 38 | .get('/hi', (req, res) => res.send('Hello World!')) 39 | .start(3000).then(() => console.log('Public service listening on 3000 port!')) 40 | -------------------------------------------------------------------------------- /demos/consistent-hashing.js: -------------------------------------------------------------------------------- 1 | // Gateway implementation 2 | 3 | 'use strict' 4 | 5 | const gateway = require('../index') 6 | const ConsistentHash = require('consistent-hash') 7 | 8 | const targets = [ 9 | 'http://localhost:3000', 10 | 'http://localhost:3001', 11 | 'http://localhost:3002' 12 | ] 13 | 14 | const consistentHash = new ConsistentHash() 15 | targets.forEach((target) => consistentHash.add(target)) 16 | 17 | gateway({ 18 | routes: [ 19 | { 20 | proxyHandler: (req, res, url, proxy, proxyOpts) => { 21 | const target = consistentHash.get(req.path) 22 | proxyOpts.base = target 23 | 24 | return proxy(req, res, url, proxyOpts) 25 | }, 26 | prefix: '/api' 27 | } 28 | ] 29 | }) 30 | .start(8080) 31 | .then(() => console.log('API Gateway listening on 8080 port!')) 32 | 33 | // Below is the services implementation, commonly located on separated projects 34 | const express = require('express') 35 | 36 | // service1.js 37 | const service1 = express() 38 | service1.get('/orders/:orderId', (req, res) => { 39 | res.header('Service-Id', 'service1') 40 | res.send('Order from service 1!') 41 | }) 42 | service1.listen(3000, () => { 43 | console.log('Service 1 running!') 44 | }) 45 | 46 | // service2.js 47 | const service2 = express() 48 | 49 | service2.get('/orders/:orderId', (req, res) => { 50 | res.header('Service-Id', 'service2') 51 | res.send('Order from service 2!') 52 | }) 53 | 54 | service2.listen(3001, () => { 55 | console.log('Service 2 running!') 56 | }) 57 | 58 | // service3.js 59 | const service3 = express() 60 | 61 | service3.get('/orders/:orderId', (req, res) => { 62 | res.header('Service-Id', 'service3') 63 | res.send('Order from service 3!') 64 | }) 65 | 66 | service3.listen(3002, () => { 67 | console.log('Service 3 running!') 68 | }) 69 | -------------------------------------------------------------------------------- /demos/hooks-ts-example.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { IncomingMessage } from 'http' 4 | import gateway from '../index' 5 | import { http } from '../lib/default-hooks' 6 | import { ServerResponse } from 'http' 7 | 8 | gateway({ 9 | routes: [ 10 | { 11 | prefix: '/httpbin', 12 | target: 'https://httpbin.org', 13 | hooks: { 14 | onRequest: (req: IncomingMessage, res: ServerResponse) => { 15 | console.log('Request to httpbin.org') 16 | 17 | return false 18 | }, 19 | onResponse: ( 20 | req: IncomingMessage, 21 | res: ServerResponse, 22 | proxyRequest: IncomingMessage, 23 | ) => { 24 | console.log('POST request to httpbin.org') 25 | 26 | // continue forwarding the response 27 | http.onResponse(req, res, proxyRequest) 28 | }, 29 | }, 30 | }, 31 | ], 32 | }).start(8080) 33 | -------------------------------------------------------------------------------- /demos/hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('./../index') 4 | const PORT = process.env.PORT || 8080 5 | 6 | gateway({ 7 | routes: [{ 8 | prefix: '/service', 9 | target: 'http://127.0.0.1:3000', 10 | hooks: { 11 | async onRequest (req, res) { 12 | // you can alter the request object here 13 | // adding headers: 14 | req.headers['x-header'] = 'value' 15 | }, 16 | rewriteHeaders (headers) { 17 | // you can alter response headers here 18 | return headers 19 | }, 20 | onResponse (req, res, stream) { 21 | // you can alter the origin response and remote response here 22 | // default implementation explained here: 23 | // https://www.npmjs.com/package/fast-gateway#onresponse-hook-default-implementation 24 | } 25 | } 26 | }] 27 | }).start(PORT).then(server => { 28 | console.log(`API Gateway listening on ${PORT} port!`) 29 | }) 30 | -------------------------------------------------------------------------------- /demos/https-example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('./../index') 4 | 5 | gateway({ 6 | routes: [{ 7 | prefix: '/httpbin', 8 | target: 'https://httpbin.org' 9 | }] 10 | }).start(8080) 11 | -------------------------------------------------------------------------------- /demos/load-balancer-multiple-proxies.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('../index') 4 | const { P2cBalancer } = require('load-balancers') 5 | const lambdaProxy = require('http-lambda-proxy') 6 | const { onRequestNoOp, onResponse } = require('./../lib/default-hooks').lambda 7 | 8 | // @TODO: update the list of target origins or proxy instances 9 | const targets = [ 10 | 'http://localhost:3000', 11 | lambdaProxy({ 12 | target: process.env.FUNCTION_NAME, 13 | region: process.env.AWS_REGION 14 | }) 15 | ] 16 | const balancer = new P2cBalancer(targets.length) 17 | 18 | gateway({ 19 | routes: [{ 20 | proxyHandler: (req, res, url, proxy, proxyOpts) => { 21 | const target = targets[balancer.pick()] 22 | if (typeof target === 'string') { 23 | proxyOpts.base = target 24 | } else { 25 | proxyOpts.onResponse = onResponse 26 | proxyOpts.onRequest = onRequestNoOp 27 | proxy = target 28 | } 29 | 30 | return proxy(req, res, url, proxyOpts) 31 | }, 32 | prefix: '/balanced' 33 | }] 34 | }).start(8080).then(() => console.log('API Gateway listening on 8080 port!')) 35 | 36 | const service = require('restana')({}) 37 | service 38 | .get('/get', (req, res) => res.send({ msg: 'Hello from service 1!' })) 39 | .start(3000).then(() => console.log('Public service listening on 3000 port!')) 40 | 41 | // Usage: curl 'http://localhost:8080/balanced/get' 42 | -------------------------------------------------------------------------------- /demos/multiple-hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('./../index') 4 | const PORT = process.env.PORT || 8080 5 | 6 | const { multipleHooks } = require('fg-multiple-hooks') 7 | 8 | const hook1 = async (req, res) => { 9 | console.log('hook1 with logic 1 called') 10 | // res.send('hook failed here'); 11 | return false // do not abort the request 12 | } 13 | 14 | const hook2 = async (req, res) => { 15 | console.log('hook2 with logic 2 called') 16 | const shouldAbort = true 17 | if (shouldAbort) { 18 | res.send('handle a rejected request here') 19 | } 20 | return shouldAbort 21 | } 22 | 23 | gateway({ 24 | routes: [{ 25 | prefix: '/service', 26 | target: 'http://127.0.0.1:3000', 27 | hooks: { 28 | onRequest: (req, res) => multipleHooks(req, res, hook1, hook2), // you can add as many hooks as you please 29 | onResponse (req, res, stream) { 30 | // you can alter the origin response and remote response here 31 | // default implementation explained here: 32 | // https://www.npmjs.com/package/fast-gateway#onresponse-hook-default-implementation 33 | } 34 | } 35 | }] 36 | }).start(PORT).then(server => { 37 | console.log(`API Gateway listening on ${PORT} port!`) 38 | }) 39 | -------------------------------------------------------------------------------- /demos/post-processing.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const toArray = require('stream-to-array') 4 | const gateway = require('../index') 5 | 6 | gateway({ 7 | routes: [{ 8 | prefix: '/httpbin', 9 | target: 'https://httpbin.org', 10 | hooks: { 11 | async onResponse (req, res, stream) { 12 | // collect all streams parts 13 | const resBuffer = Buffer.concat(await toArray(stream)) 14 | 15 | // parse response body, for example: JSON 16 | const payload = JSON.parse(resBuffer) 17 | // modify the response, for example adding new properties 18 | payload.newProperty = 'post-processing' 19 | 20 | // stringify response object again 21 | const newResponseBody = JSON.stringify(payload) 22 | 23 | // set new content-length header 24 | res.setHeader('content-length', '' + Buffer.byteLength(newResponseBody)) 25 | // set response statusCode 26 | res.statusCode = stream.statusCode 27 | 28 | // send new response payload 29 | res.end(newResponseBody) 30 | } 31 | } 32 | }] 33 | }).start(8080) 34 | -------------------------------------------------------------------------------- /demos/rate-limit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('../index') 4 | const rateLimit = require('express-rate-limit') 5 | const requestIp = require('request-ip') 6 | 7 | gateway({ 8 | middlewares: [ 9 | // acquire request IP 10 | (req, res, next) => { 11 | req.ip = requestIp.getClientIp(req) 12 | return next() 13 | }, 14 | // rate limiter 15 | rateLimit({ 16 | windowMs: 1 * 60 * 1000, // 1 minutes 17 | max: 60, // limit each IP to 60 requests per windowMs 18 | handler: (req, res) => { 19 | res.send('Too many requests, please try again later.', 429) 20 | } 21 | }) 22 | ], 23 | routes: [{ 24 | prefix: '/public', 25 | target: 'http://localhost:3000' 26 | }] 27 | }).start(8080).then(() => console.log('API Gateway listening on 8080 port!')) 28 | 29 | const service = require('restana')({}) 30 | service 31 | .get('/hi', (req, res) => res.send('Hello World!')) 32 | .start(3000).then(() => console.log('Public service listening on 3000 port!')) 33 | -------------------------------------------------------------------------------- /demos/ssl-termination.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('./../index') 4 | const PORT = process.env.PORT || 4443 5 | const https = require('https') 6 | const pem = require('pem') 7 | 8 | pem.createCertificate({ 9 | days: 1, 10 | selfSigned: true 11 | }, (err, keys) => { 12 | if (err) console.error(err) 13 | 14 | gateway({ 15 | restana: { 16 | server: https.createServer({ 17 | key: keys.serviceKey, 18 | cert: keys.certificate 19 | }) 20 | }, 21 | middlewares: [ 22 | ], 23 | 24 | routes: [{ 25 | prefix: '/api', 26 | target: 'http://localhost:3000' 27 | }] 28 | }).start(PORT).then(server => { 29 | console.log(`API Gateway listening on ${PORT} port!`) 30 | }) 31 | 32 | const api = require('restana')({}) 33 | api.get('/ssl-protected', (req, res) => { 34 | res.send('SSL Terminated!') 35 | }) 36 | 37 | api.start(3000).then(() => console.log('API service listening on 3000 port!')) 38 | }) 39 | -------------------------------------------------------------------------------- /demos/target-load-balancer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('../index') 4 | const { P2cBalancer } = require('load-balancers') 5 | 6 | // @TODO: update the list of target origins 7 | const targets = [ 8 | 'http://localhost:3000', 9 | 'https://httpbin.org' 10 | ] 11 | const balancer = new P2cBalancer(targets.length) 12 | 13 | gateway({ 14 | routes: [{ 15 | proxyHandler: (req, res, url, proxy, proxyOpts) => { 16 | proxyOpts.base = targets[balancer.pick()] 17 | 18 | return proxy(req, res, url, proxyOpts) 19 | }, 20 | prefix: '/balanced' 21 | }] 22 | }).start(8080).then(() => console.log('API Gateway listening on 8080 port!')) 23 | 24 | const service = require('restana')({}) 25 | service 26 | .get('/get', (req, res) => res.send({ msg: 'Hello from service 1!' })) 27 | .start(3000).then(() => console.log('Public service listening on 3000 port!')) 28 | 29 | // Usage: curl 'http://localhost:8080/balanced/get' 30 | -------------------------------------------------------------------------------- /demos/url-rewrite.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('../index') 4 | const PORT = process.env.PORT || 8080 5 | 6 | gateway({ 7 | routes: [{ 8 | pathRegex: '', 9 | prefix: '/customers/:customerId', 10 | target: 'http://localhost:3000', 11 | urlRewrite: ({ params: { customerId } }) => `/users/${customerId}` 12 | }] 13 | }).start(PORT).then(server => { 14 | console.log(`API Gateway listening on ${PORT} port!`) 15 | }) 16 | 17 | const service = require('restana')({}) 18 | service 19 | .get('/users/:id', (req, res) => res.send('Hello ' + req.params.id)) 20 | .start(3000).then(() => console.log('Service listening on 3000 port!')) 21 | -------------------------------------------------------------------------------- /demos/ws-proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const gateway = require('./../index') 4 | const PORT = process.env.PORT || 8080 5 | 6 | gateway({ 7 | routes: [{ 8 | // ... other HTTP or WebSocket routes 9 | }, { 10 | proxyType: 'websocket', 11 | prefix: '/echo', 12 | target: 'ws://ws.ifelse.io', 13 | hooks: { 14 | onOpen: (ws, searchParams) => { 15 | 16 | } 17 | } 18 | }] 19 | }).start(PORT).then(server => { 20 | console.log(`API Gateway listening on ${PORT} port!`) 21 | }) 22 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BackendStack21/fast-gateway/944c17e5fba73a280674e0fcd03b030b9cd8f939/docs/.nojekyll -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | fgw.21no.de -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [![tests](https://github.com/BackendStack21/fast-gateway/actions/workflows/tests.yaml/badge.svg)](https://github.com/BackendStack21/fast-gateway/actions/workflows/tests.yaml) 4 | [![NPM version](https://badgen.net/npm/v/fast-gateway)](https://www.npmjs.com/package/fast-gateway) 5 | [![NPM Total Downloads](https://badgen.net/npm/dt/fast-gateway)](https://www.npmjs.com/package/fast-gateway) 6 | [![License](https://badgen.net/npm/license/fast-gateway)](https://www.npmjs.com/package/fast-gateway) 7 | [![TypeScript support](https://badgen.net/npm/types/fast-gateway)](https://www.npmjs.com/package/fast-gateway) 8 | [![Github stars](https://badgen.net/github/stars/jkyberneees/fast-gateway?icon=github)](https://github.com/jkyberneees/fast-gateway) 9 | 10 | 11 | 12 | A super fast, framework agnostic Node.js API Gateway for the masses ❤️ 13 | _Docker images: https://hub.docker.com/repository/docker/kyberneees/rproxy_ 14 | 15 | > Since v2.3.0, [AWS Lambda](https://www.youtube.com/watch?v=EBSdyoO3goc) proxying integration is supported via [`http-lambda-proxy`](https://www.npmjs.com/package/http-lambda-proxy) 🔥 16 | > Since v3.1.0, WebSockets proxying is supported via [`faye-websocket`](https://www.npmjs.com/package/faye-websocket) 🔥 17 | 18 | Read more online: 19 | 20 | - A “.js” API Gateway for the masses: https://itnext.io/a-js-api-gateway-for-the-masses-a12fdb9e961c 21 | 22 | # Install 23 | 24 | ```js 25 | npm i fast-gateway 26 | ``` 27 | 28 | # Usage 29 | 30 | Next we describe two examples proxying HTTP and Lambda downstream services. 31 | 32 | > For simplicity of reading, both examples are separated, however a single gateway configuration supports all routes configurations. 33 | 34 | ## HTTP proxying 35 | 36 | ### Gateway 37 | 38 | ```js 39 | const gateway = require('fast-gateway') 40 | const server = gateway({ 41 | routes: [ 42 | { 43 | prefix: '/service', 44 | target: 'http://127.0.0.1:3000', 45 | }, 46 | ], 47 | }) 48 | 49 | server.start(8080) 50 | ``` 51 | 52 | ### Remote Service 53 | 54 | ```js 55 | const service = require('restana')() 56 | service.get('/get', (req, res) => res.send('Hello World!')) 57 | 58 | service.start(3000) 59 | ``` 60 | 61 | ## AWS Lambda proxying 62 | 63 | ### Gateway 64 | 65 | ```bash 66 | npm i http-lambda-proxy 67 | ``` 68 | 69 | ```js 70 | const gateway = require('fast-gateway') 71 | const server = gateway({ 72 | routes: [ 73 | { 74 | prefix: '/service', 75 | target: 'my-lambda-serverless-api', 76 | proxyType: 'lambda', 77 | proxyConfig: { 78 | region: 'eu-central-1', 79 | }, 80 | }, 81 | ], 82 | }) 83 | 84 | server.start(8080) 85 | ``` 86 | 87 | > You might also want to read: [Setting AWS Credentials in Node.js](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html) 88 | 89 | ### Function implementation 90 | 91 | ```js 92 | const serverless = require('serverless-http') 93 | const json = require('serverless-json-parser') 94 | const restana = require('restana') 95 | 96 | const service = restana() 97 | service.use(json()) 98 | 99 | // routes 100 | service.get('/get', (req, res) => { 101 | res.send({msg: 'Go Serverless!'}) 102 | }) 103 | service.post('/post', (req, res) => { 104 | res.send(req.body) 105 | }) 106 | 107 | // export handler 108 | module.exports.handler = serverless(service) 109 | ``` 110 | 111 | # Configuration options explained 112 | 113 | ```js 114 | { 115 | // Optional server instance. Any HTTP framework that supports the following signature is compatible: 116 | // - server[HTTP_METHOD](pattern, [middleware1, middleware2,], handler) 117 | // 118 | // Known compatible frameworks: Restana, Express.js 119 | // If omitted, restana is used as default HTTP framework 120 | server, 121 | // Optional restana library configuration (https://www.npmjs.com/package/restana#configuration) 122 | // Please note that if "server" is provided, this settings are ignored. 123 | restana: {}, 124 | // Optional global middlewares in the format: (req, res, next) => next() 125 | // Default value: [] 126 | middlewares: [], 127 | // Optional global value for routes "pathRegex". Default value: '/*' 128 | pathRegex: '/*', 129 | // Optional global requests timeout value (given in milliseconds). Default value: '0' (DISABLED) 130 | // Ignored if proxyType = 'lambda' 131 | timeout: 0, 132 | // Optional "target" value that overrides the routes "target" config value. Feature intended for testing purposes. 133 | targetOverride: "https://yourdev.api-gateway.com", 134 | // Optional "Proxy Factory" implementation, allows integration of custom proxying strategies. 135 | // Behavior: 136 | // - If it returns any value (e.g. a custom proxy), that value will be used directly. 137 | // - If it returns `undefined` (or does not return anything), the default factory from `fast-gateway` will be used as a fallback. 138 | // - If it returns `null`, no proxy will be used and the default factory will be skipped entirely. 139 | // Default: the built-in proxy factory from `fast-gateway` 140 | proxyFactory: ({ proxyType, opts, route }) => {...} 141 | // Optional toggle for exposing minimal documentation of registered services at `GET /services.json` 142 | // Default value: true 143 | enableServicesEndpoint: true 144 | 145 | // HTTP proxy 146 | routes: [{ 147 | // Optional proxy type definition. Supported values: http, http-legacy, lambda 148 | // Modules: 149 | // - http: fast-proxy-lite 150 | // - http-legacy: fast-proxy 151 | // - lambda: http-lambda-proxy 152 | // Default value: http 153 | proxyType: 'http' 154 | // Optional proxy library configuration: 155 | // - fast-proxy-lite: https://www.npmjs.com/package/fast-proxy-lite#options 156 | // - fast-proxy: https://www.npmjs.com/package/fast-proxy#options 157 | // - http-lambda-proxy: https://www.npmjs.com/package/http-lambda-proxy#options 158 | // Default value: {} 159 | proxyConfig: {}, 160 | // Optional proxy handler function. Default value: (req, res, url, proxy, proxyOpts) => proxy(req, res, url, proxyOpts) 161 | proxyHandler: () => {}, 162 | // Optional path matching regex. Default value: '/*' 163 | // In order to disable the 'pathRegex' at all, you can use an empty string: ''. Please note, when prefix is instance of RegExp, this setting is ignored. 164 | pathRegex: '/*', 165 | // Optional service requests timeout value (given in milliseconds). Default value: '0' (DISABLED) 166 | // This setting apply only when proxyType = 'http' 167 | timeout: 0, 168 | // Route prefix, defined as string or as a RegExp instance. 169 | prefix: '/public', 170 | // Uses the raw request query string value instead of req.query. Default value: false 171 | disableQsOverwrite: true, 172 | // Optional documentation configuration (unrestricted schema) 173 | docs: { 174 | name: 'Public Service', 175 | endpoint: '/api-docs', 176 | type: 'swagger' 177 | }, 178 | // Optional "prefix rewrite" before request is forwarded. Default value: '' 179 | prefixRewrite: '', 180 | // Optional "url rewrite" hook. If defined, the prefixRewrite setting is ignored. 181 | urlRewrite: (req) => req.url, 182 | // Remote HTTP server URL to forward the request. 183 | // If proxyType = 'lambda', the value is the name of the Lambda function, version, or alias. 184 | target: 'http://localhost:3000', 185 | // Optional HTTP methods to limit the requests proxy to certain verbs only 186 | // Supported HTTP methods: ['GET', 'DELETE', 'PATCH', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'TRACE'] 187 | methods: ['GET', 'POST', ...], 188 | // Optional route level middlewares. Default value: [] 189 | middlewares: [], 190 | // Optional proxy lifecycle hooks. Default value: {} 191 | hooks: { 192 | async onRequest (req, res) { 193 | // // we can optionally reply from here if required 194 | // res.end('Hello World!') 195 | // 196 | // // we can optionally update the request query params from here if required 197 | // req.query.category = 'js' 198 | // 199 | // return true // truthy value returned will abort the request forwarding 200 | }, 201 | onResponse (req, res, stream) { 202 | // do some post-processing here 203 | // ... 204 | } 205 | 206 | // if proxyType= 'http', other options allowed https://www.npmjs.com/package/fast-proxy-lite#opts 207 | } 208 | }] 209 | } 210 | ``` 211 | 212 | ## Default hooks 213 | 214 | For developers reference, default hooks implementation are located in `lib/default-hooks.js` file. 215 | 216 | # The "_GET /services.json_" endpoint 217 | 218 | Since version `1.3.5` the gateway exposes minimal documentation about registered services at: `GET /services.json` 219 | 220 | Since version `4.2.0`, the `/services.json` route can be disabled by setting `enableServicesEndpoint: false` in the gateway options. 221 | 222 | Example output: 223 | 224 | ```json 225 | [ 226 | { 227 | "prefix": "/public", 228 | "docs": { 229 | "name": "Public Service", 230 | "endpoint": "/swagger.json", 231 | "type": "swagger" 232 | } 233 | }, 234 | { 235 | "prefix": "/admin" 236 | } 237 | ] 238 | ``` 239 | 240 | > NOTE: Please see `docs` configuration entry explained above. 241 | 242 | # WebSockets 243 | 244 | WebSockets proxying is supported since `v3.1.0`. Main considerations: 245 | 246 | - The `faye-websocket` module dependency require to be installed: 247 | ```bash 248 | npm i faye-websocket 249 | ``` 250 | - WebSockets middlewares are not supported. 251 | - WebSocketRoute configuration definition: 252 | 253 | ```ts 254 | interface WebSocketRoute { 255 | proxyType: 'websocket' 256 | // https://github.com/faye/faye-websocket-node#initialization-options 257 | proxyConfig?: {} 258 | // used as micromatch matcher pattern: https://www.npmjs.com/package/micromatch 259 | // prefix examples: '/graphql', '/ws-all/*', ['/rtp', '/rtp/*.flv'], '!/media/*.avi' 260 | prefix: string 261 | target: string 262 | // https://github.com/faye/faye-websocket-node#subprotocol-negotiation 263 | subProtocols?: [] 264 | hooks?: WebSocketHooks 265 | } 266 | 267 | interface WebSocketHooks { 268 | onOpen?: (ws: any, searchParams: URLSearchParams) => Promise 269 | } 270 | ``` 271 | 272 | - The `/` route prefix is considered the default route. 273 | 274 | ## Configuration example 275 | 276 | ```js 277 | gateway({ 278 | routes: [ 279 | { 280 | // ... other HTTP or WebSocket routes 281 | }, 282 | { 283 | proxyType: 'websocket', 284 | prefix: '/echo', 285 | target: 'ws://ws.ifelse.io', 286 | }, 287 | ], 288 | }).start(PORT) 289 | ``` 290 | 291 | # Traffic Management 292 | 293 | ## Timeouts and Unavailability 294 | 295 | We can restrict requests timeouts globally or at service level using the `timeout` configuration. 296 | 297 | You can also define endpoints specific timeout using the property `timeout` of the request object, normally inside a middleware: 298 | 299 | ```js 300 | req.timeout = 500 // define a 500ms timeout on a custom request. 301 | ``` 302 | 303 | > NOTE: You might want to also check https://www.npmjs.com/package/middleware-if-unless 304 | 305 | ## Circuit Breakers 306 | 307 | By using the `proxyHandler` hook, developers can optionally intercept and modify the default gateway routing behavior right before the origin request is proxied to the remote service. Therefore, connecting advanced monitoring mechanisms like [Circuit Breakers](https://martinfowler.com/bliki/CircuitBreaker.html) is rather simple. 308 | 309 | Please see the `demos/circuitbreaker.js` example for more details using the `opossum` library. 310 | 311 | ## Rate Limiting 312 | 313 | [Rate limiting](https://en.wikipedia.org/wiki/Rate_limiting), as well many other gateway level features can be easily implemented using `fast-gateway`: 314 | 315 | ```js 316 | const rateLimit = require('express-rate-limit') 317 | const requestIp = require('request-ip') 318 | 319 | gateway({ 320 | middlewares: [ 321 | // first acquire request IP 322 | (req, res, next) => { 323 | req.ip = requestIp.getClientIp(req) 324 | return next() 325 | }, 326 | // second enable rate limiter 327 | rateLimit({ 328 | windowMs: 1 * 60 * 1000, // 1 minutes 329 | max: 60, // limit each IP to 60 requests per windowMs 330 | handler: (req, res) => 331 | res.send('Too many requests, please try again later.', 429), 332 | }), 333 | ], 334 | 335 | // your downstream services 336 | routes: [ 337 | { 338 | prefix: '/public', 339 | target: 'http://localhost:3000', 340 | }, 341 | { 342 | // ... 343 | }, 344 | ], 345 | }) 346 | ``` 347 | 348 | > In this example we have used the [express-rate-limit](https://www.npmjs.com/package/express-rate-limit) module. 349 | 350 | # Hostnames support 351 | 352 | We can also implement hostnames support with fast-gateway, basically we translate hostnames to prefixes: 353 | 354 | ```js 355 | ... 356 | 357 | // binding hostnames to prefixes 358 | const hostnames2prefix = [{ 359 | prefix: '/api', 360 | hostname: 'api.company.tld' 361 | }] 362 | // instantiate hostnames hook, this will prefix request urls according to data in hostnames2prefix 363 | const hostnamesHook = require('fast-gateway/lib/hostnames-hook')(hostnames2prefix) 364 | 365 | // separately instantiate and configure restana application 366 | const app = restana() 367 | const server = http.createServer((req, res) => { 368 | hostnamesHook(req, res, () => { 369 | return app(req, res) 370 | }) 371 | }) 372 | 373 | // gateway configuration 374 | gateway({ 375 | server: app, // injecting existing restana application 376 | routes: [{ 377 | prefix: '/api', 378 | target: 'http://localhost:3000' 379 | }] 380 | }) 381 | 382 | ... 383 | ``` 384 | 385 | > Afterwards: 386 | > `curl --header "Host: api.company.tld:8080" http://127.0.0.1:8080/api-service-endpoint` 387 | 388 | Using micromatch patterns as hostname value: 389 | 390 | ```js 391 | const hostnames2prefix = [ 392 | { 393 | prefix: '/admin', 394 | hostname: '*.admin.company.tld', 395 | }, 396 | { 397 | prefix: '/services', 398 | hostname: ['services.company.tld', '*.services.company.tld'], 399 | }, 400 | ] 401 | ``` 402 | 403 | For more details, please checkout the `basic-hostnames.js` demo. 404 | 405 | # Caching 406 | 407 | Caching support is provided by the `http-cache-middleware` module. https://www.npmjs.com/package/http-cache-middleware 408 | 409 | ## Introduction 410 | 411 | Enabling proper caching strategies at gateway level will drastically reduce the latency of your system, 412 | as it reduces network round-trips and remote services processing. 413 | We are talking here about improvements in response times from `X ms` to `~2ms`, as an example. 414 | 415 | > We use the `http-cache-middleware` module to support gateway level caching. Read more about it: https://github.com/jkyberneees/http-cache-middleware 416 | 417 | ## Caching responses from all services 418 | 419 | ### Single node cache (memory) 420 | 421 | ```js 422 | // cache middleware 423 | const cache = require('http-cache-middleware')() 424 | // enable http cache middleware 425 | const gateway = require('fast-gateway') 426 | const server = gateway({ 427 | middlewares: [cache], 428 | routes: [...] 429 | }) 430 | ``` 431 | 432 | > Memory storage is recommended if there is only one gateway instance and you are not afraid of losing cache data. 433 | 434 | ### Multi nodes cache (redis) 435 | 436 | ```js 437 | // redis setup 438 | const CacheManager = require('cache-manager') 439 | const redisStore = require('cache-manager-ioredis') 440 | const redisCache = CacheManager.caching({ 441 | store: redisStore, 442 | db: 0, 443 | host: 'localhost', 444 | port: 6379, 445 | ttl: 30 446 | }) 447 | 448 | // cache middleware 449 | const cache = require('http-cache-middleware')({ 450 | stores: [redisCache] 451 | }) 452 | 453 | // enable http cache middleware 454 | const gateway = require('fast-gateway') 455 | const server = gateway({ 456 | middlewares: [cache], 457 | routes: [...] 458 | }) 459 | ``` 460 | 461 | > Required if there are more than one gateway instances 462 | 463 | ## Enabling caching on remote services 464 | 465 | https://github.com/jkyberneees/http-cache-middleware#enabling-cache-for-service-endpoints 466 | 467 | ## Cache invalidation 468 | 469 | https://github.com/jkyberneees/http-cache-middleware#invalidating-caches 470 | 471 | ## Custom cache keys 472 | 473 | Cache keys are generated using: `req.method + req.url`, however, for indexing/segmenting requirements it makes sense to allow cache keys extensions. 474 | Unfortunately, this feature can't be implemented at remote service level, because the gateway needs to know the entire lookup key when a request 475 | reaches the gateway. 476 | 477 | For doing this, we simply recommend using middlewares on the service configuration: 478 | 479 | ```js 480 | routes: [ 481 | { 482 | prefix: '/users', 483 | target: 'http://localhost:3000', 484 | middlewares: [ 485 | (req, res, next) => { 486 | req.cacheAppendKey = (req) => req.user.id // here cache key will be: req.method + req.url + req.user.id 487 | return next() 488 | }, 489 | ], 490 | }, 491 | ] 492 | ``` 493 | 494 | > In this example we also distinguish cache entries by `user.id`, very common case! 495 | 496 | ## Disabling caching programmatically 497 | 498 | You can also disable cache checks for certain requests programmatically: 499 | 500 | ```js 501 | routes: [ 502 | { 503 | prefix: '/users', 504 | target: 'http://localhost:3000', 505 | middlewares: [ 506 | (req, res, next) => { 507 | req.cacheDisabled = true 508 | return next() 509 | }, 510 | ], 511 | }, 512 | ] 513 | ``` 514 | 515 | # Related projects 516 | 517 | - middleware-if-unless (https://www.npmjs.com/package/middleware-if-unless) 518 | - fast-proxy-lite (https://www.npmjs.com/package/fast-proxy-lite) 519 | - http-lambda-proxy (https://www.npmjs.com/package/http-lambda-proxy) 520 | - restana (https://www.npmjs.com/package/restana) 521 | 522 | # Benchmarks 523 | 524 | https://github.com/jkyberneees/nodejs-proxy-benchmarks 525 | 526 | # Sponsors 527 | 528 | - (INACTIVE) Kindly sponsored by [ShareNow](https://www.share-now.com/), a company that promotes innovation! 529 | 530 | # Support / Donate 💚 531 | 532 | You can support the maintenance of this project: 533 | 534 | - PayPal: https://www.paypal.me/kyberneees 535 | - [TRON](https://www.binance.com/en/buy-TRON) Wallet: `TJ5Bbf9v4kpptnRsePXYDvnYcYrS5Tyxus` 536 | 537 | # Breaking Changes 538 | 539 | ## v3.x 540 | 541 | - The `fast-proxy-lite` module is used by default to support `http` proxy type 🔥. This means, no `undici` or `http2` are supported by default. 542 | - The old `fast-proxy` module is available under the `http-legacy` proxy type, but the module is not installed by default. 543 | - Proxy configuration is now generalized under the `proxyConfig` property. 544 | 545 | # Express.js v5 compatibility 546 | 547 | Since `v5.0.0`, Express.js has introduced a breaking change that affects the compatibility with `fast-gateway`. Changes in [Path route matching syntax](https://expressjs.com/en/guide/migrating-5.html#path-syntax) require a minor adjustment in the gateway configuration. 548 | 549 | Before: 550 | 551 | ```js 552 | { 553 | prefix: '/public', 554 | // ... 555 | } 556 | ``` 557 | 558 | After: 559 | 560 | ```js 561 | { 562 | prefix: new RegExp('/public/.*'), 563 | urlRewrite: (req) => req.url.replace('/public', ''), // optional if you want to rewrite the URL 564 | // ... 565 | } 566 | ``` 567 | 568 | Full example: 569 | 570 | ```js 571 | 'use strict' 572 | 573 | const gateway = require('fast-gateway') 574 | const express = require('express') 575 | const PORT = process.env.PORT || 8080 576 | 577 | gateway({ 578 | server: express(), 579 | 580 | middlewares: [require('cors')(), require('helmet')()], 581 | 582 | routes: [ 583 | { 584 | prefix: new RegExp('/public/.*'), // Express.js v5 requires a RegExp object 585 | //prefix: '/public', // Compatible with Express.js v4, 586 | 587 | urlRewrite: (req) => req.url.replace('/public', ''), 588 | target: 'http://localhost:3000', 589 | docs: { 590 | name: 'Public Service', 591 | endpoint: 'swagger.json', 592 | type: 'swagger', 593 | }, 594 | }, 595 | { 596 | prefix: new RegExp('/admin/.*'), // Express.js v5 requires a RegExp object 597 | //prefix: '/admin', // Compatible with Express.js v4, 598 | target: 'http://localhost:3001', 599 | middlewares: [ 600 | /* 601 | require('express-jwt').expressjwt({ 602 | secret: 'shhhhhhared-secret', 603 | algorithms: ['HS256'], 604 | }), 605 | */ 606 | ], 607 | }, 608 | ], 609 | }).listen(PORT, () => { 610 | console.log(`API Gateway listening on ${PORT} port!`) 611 | }) 612 | 613 | const service1 = require('restana')({}) 614 | service1 615 | .get('/hi', (req, res) => res.send('Hello World!')) 616 | .start(3000) 617 | .then(() => console.log('Public service listening on 3000 port!')) 618 | 619 | const service2 = require('restana')({}) 620 | service2 621 | .get('/admin/users', (req, res) => res.send([])) 622 | .start(3001) 623 | .then(() => console.log('Admin service listening on 3001 port!')) 624 | ``` 625 | -------------------------------------------------------------------------------- /docs/fast-gateway-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | fast-gateway 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | 42 | . 43 | 44 | 46 | 47 | 48 | 49 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as restana from 'restana' 2 | 3 | declare namespace fastgateway { 4 | type ProxyType = 'http' | 'lambda' | (string & {}) 5 | 6 | type Method = 7 | | 'GET' 8 | | 'DELETE' 9 | | 'PATCH' 10 | | 'POST' 11 | | 'PUT' 12 | | 'HEAD' 13 | | 'OPTIONS' 14 | | 'TRACE' 15 | 16 | interface Docs { 17 | name: string 18 | endpoint: string 19 | type: string 20 | } 21 | 22 | interface ProxyFactoryOpts { 23 | proxyType: ProxyType 24 | opts: {} 25 | route: Route 26 | } 27 | 28 | interface Route { 29 | proxyType?: ProxyType 30 | proxyConfig?: {} 31 | proxyHandler?: Function 32 | pathRegex?: string 33 | timeout?: number 34 | prefix: string | RegExp 35 | docs?: Docs 36 | prefixRewrite?: string 37 | target: string 38 | methods?: Method[] 39 | middlewares?: Function[] 40 | urlRewrite?: Function 41 | hooks?: Hooks 42 | disableQsOverwrite?: boolean 43 | } 44 | 45 | interface WebSocketRoute { 46 | proxyType: 'websocket' 47 | proxyConfig?: {} // https://github.com/faye/faye-websocket-node#initialization-options 48 | prefix: string 49 | target: string 50 | subProtocols?: [] // https://github.com/faye/faye-websocket-node#subprotocol-negotiation 51 | hooks?: WebSocketHooks 52 | } 53 | 54 | interface WebSocketHooks { 55 | onOpen?: (ws: any, searchParams: URLSearchParams) => Promise 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 | --------------------------------------------------------------------------------