├── .npmignore ├── .dockerignore ├── bun.lockb ├── hl-tests ├── 64 │ └── proxy.js ├── letsencrypt │ ├── certs │ │ ├── dash │ │ │ ├── .well-known │ │ │ │ └── acme-challenge │ │ │ │ │ └── abc │ │ │ └── privkey.pem │ │ ├── accounts │ │ │ ├── acme-v01.api.letsencrypt.org │ │ │ │ └── directory │ │ │ │ │ └── 49881ab35d6ac7bb51f05ca3a220fbac │ │ │ │ │ ├── meta.json │ │ │ │ │ ├── regr.json │ │ │ │ │ └── private_key.json │ │ │ └── acme-staging.api.letsencrypt.org │ │ │ │ └── directory │ │ │ │ └── 367e0270a5d31ab031561f9f284ca350 │ │ │ │ ├── meta.json │ │ │ │ ├── regr.json │ │ │ │ └── private_key.json │ │ ├── dev-csr.pem │ │ ├── dev-key.pem │ │ ├── dev-cert.pem │ │ ├── api │ │ │ └── privkey.pem │ │ ├── dash_ │ │ │ └── privkey.pem │ │ ├── api.com │ │ │ └── privkey.pem │ │ ├── dash.com │ │ │ └── privkey.pem │ │ ├── example.com │ │ │ └── privkey.pem │ │ ├── localhost │ │ │ └── privkey.pem │ │ ├── caturra.exactbytes.com │ │ │ ├── privkey.pem │ │ │ ├── privkey.pem.bak │ │ │ ├── chain.pem │ │ │ ├── cert.pem │ │ │ └── fullchain.pem │ │ ├── archive │ │ │ └── caturra.exactbytes.com │ │ │ │ ├── privkey0.pem │ │ │ │ ├── chain0.pem │ │ │ │ ├── cert0.pem │ │ │ │ └── fullchain0.pem │ │ └── renewal │ │ │ ├── caturra.exactbytes.com.conf.bak │ │ │ └── caturra.exactbytes.com.conf │ ├── a.js │ └── proxy.js └── paths.js ├── test ├── fixtures │ ├── index.ts │ ├── test_crt.ts │ └── test_key.ts ├── tes_utils.ts ├── onrequest.spec.ts ├── pathnames.spec.ts ├── hostheader.spec.ts ├── custom_resolver_certificates.spec.ts ├── custom_resolver.spec.ts ├── letsencrypt_certificates.spec.ts └── register.spec.ts ├── lib ├── interfaces │ ├── index.ts │ ├── proxy-target-url.ts │ ├── proxy-route.ts │ ├── route-options.ts │ ├── resolver.ts │ └── proxy-options.ts ├── index.ts ├── declarations.d.ts ├── redis-backend.js ├── etcd-backend.ts ├── third-party │ └── le-challenge-fs.ts ├── letsencrypt.ts ├── docker.ts └── proxy.ts ├── Dockerfile ├── vitest.config.ts ├── .vscode └── launch.json ├── .github └── workflows │ ├── test.yml │ └── release.yml ├── .gitignore ├── tsconfig.json ├── .eslintrc ├── samples ├── sample1.mjs ├── lazy-wildcards.mjs └── bench.mjs ├── gulpfile.js ├── LICENSE ├── docs └── CHANGELOG.MD ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OptimalBits/redbird/HEAD/bun.lockb -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/dash/.well-known/acme-challenge/abc: -------------------------------------------------------------------------------- 1 | hello masda! 2 | -------------------------------------------------------------------------------- /test/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | export * from './test_crt.js'; 2 | export * from './test_key.js'; 3 | -------------------------------------------------------------------------------- /lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './proxy-options.js'; 2 | export * from './proxy-route.js'; 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:4.5 2 | 3 | ADD . /proxy 4 | RUN cd /proxy; npm install --production 5 | EXPOSE 8080 6 | 7 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/accounts/acme-v01.api.letsencrypt.org/directory/49881ab35d6ac7bb51f05ca3a220fbac/meta.json: -------------------------------------------------------------------------------- 1 | {"creation_host":"Manuels-MBP","creation_dt":"2016-09-18T14:25:30.785Z"} -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | /*eslint-env node */ 2 | 'use strict'; 3 | export * from './docker.js'; 4 | export * from './etcd-backend.js'; 5 | export * from './proxy.js'; 6 | export * from './interfaces/index.js'; 7 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/accounts/acme-staging.api.letsencrypt.org/directory/367e0270a5d31ab031561f9f284ca350/meta.json: -------------------------------------------------------------------------------- 1 | {"creation_host":"Manuels-MacBook-Pro.local","creation_dt":"2016-08-30T15:22:20.371Z"} -------------------------------------------------------------------------------- /lib/interfaces/proxy-target-url.ts: -------------------------------------------------------------------------------- 1 | export interface ProxyTargetUrl { 2 | host: string; 3 | hostname: string; 4 | port: number; 5 | pathname: string; 6 | useTargetHostHeader: boolean; 7 | href: string; 8 | } 9 | -------------------------------------------------------------------------------- /lib/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'greenlock' { 2 | export function create(options: any): any; 3 | } 4 | declare module 'le-store-certbot' { 5 | export function create(options: any): any; 6 | } 7 | declare module 'le-challenge-fs' { 8 | export function create(options: any): any; 9 | } 10 | -------------------------------------------------------------------------------- /lib/interfaces/proxy-route.ts: -------------------------------------------------------------------------------- 1 | import { ProxyTargetUrl } from './proxy-target-url.js'; 2 | import { RouteOptions } from './route-options.js'; 3 | 4 | /** 5 | * ProxyRoute interface 6 | * @description 7 | * Interface for ProxyRoute 8 | */ 9 | export interface ProxyRoute { 10 | urls?: ProxyTargetUrl[]; 11 | path?: string; 12 | rr?: number; 13 | isResolved?: boolean; 14 | opts?: RouteOptions; 15 | } 16 | -------------------------------------------------------------------------------- /lib/interfaces/route-options.ts: -------------------------------------------------------------------------------- 1 | import { ProxyTargetUrl } from './proxy-target-url.js'; 2 | 3 | export interface RouteOptions { 4 | useTargetHostHeader?: boolean; 5 | ssl?: { 6 | key?: string; 7 | cert?: string; 8 | ca?: string; 9 | letsencrypt?: { email: string; production: boolean; lazy?: boolean }; 10 | }; 11 | onRequest?: (req: any, res: any, target: ProxyTargetUrl) => void; 12 | } 13 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | // Specify the environment, can be 'node' or 'jsdom' based on your needs 6 | environment: 'node', 7 | 8 | // Specify the file extensions and the files to include 9 | include: ['**/*.spec.ts'], 10 | }, 11 | resolve: { 12 | extensions: ['.ts', '.js'], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /test/tes_utils.ts: -------------------------------------------------------------------------------- 1 | import { createServer, IncomingMessage } from 'http'; 2 | 3 | export function testServer(port: number) { 4 | return new Promise(function (resolve, reject) { 5 | const server = createServer(function (req, res) { 6 | res.write(''); 7 | res.end(); 8 | server.close((err) => { 9 | if (err) { 10 | return reject(err); 11 | } 12 | resolve(req); 13 | }); 14 | }); 15 | 16 | server.listen(port); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /lib/interfaces/resolver.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'http'; 2 | import { RouteOptions } from './route-options.js'; 3 | 4 | export type ResolverFnResult = 5 | | string 6 | | { path?: string; url: string; opts?: RouteOptions } 7 | | null 8 | | undefined; 9 | 10 | export type ResolverFn = ( 11 | host: string, 12 | url: string, 13 | req?: IncomingMessage 14 | ) => ResolverFnResult | Promise; 15 | 16 | export interface Resolver { 17 | fn: ResolverFn; 18 | priority: number; 19 | } 20 | -------------------------------------------------------------------------------- /hl-tests/paths.js: -------------------------------------------------------------------------------- 1 | var proxy = require('../index')({ port: 8080 }); 2 | 3 | proxy.register('http://127.0.0.1/a', 'http://127.0.0.1:3000'); 4 | proxy.register('http://127.0.0.1/b', 'http://127.0.0.1:4000'); 5 | 6 | startServer(3000); 7 | startServer(4000); 8 | 9 | function startServer(port) { 10 | var http = require('http'); 11 | function handleRequest(request, response) { 12 | response.end('Path Hit: ' + request.url); 13 | } 14 | var server = http.createServer(handleRequest); 15 | 16 | server.listen(port, function () { 17 | console.log('Server listening on: http://localhost:%s', port); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "hl-tests/paths.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": ".", 12 | "runtimeExecutable": null, 13 | "runtimeArgs": [ 14 | "--nolazy" 15 | ], 16 | "env": { 17 | "NODE_ENV": "development" 18 | }, 19 | "externalConsole": false, 20 | "sourceMaps": false, 21 | "outDir": null 22 | }, 23 | { 24 | "name": "Attach", 25 | "type": "node", 26 | "request": "attach", 27 | "port": 5858 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [16.x, 18.x, 20.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: 'npm' 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Run Vitest 28 | run: npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | dist 28 | .letsencrypt 29 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/accounts/acme-staging.api.letsencrypt.org/directory/367e0270a5d31ab031561f9f284ca350/regr.json: -------------------------------------------------------------------------------- 1 | {"body":{"id":304846,"key":{"kty":"RSA","n":"v7Q7VtM_s1obhDJrSef8oSZtT9-v91cHnqzeTyMf0-Sz3CjYXamzEz_v7ASMS4JsGoG1SeaDlk_GYMO4OyAUNTB5nUJSR5ImNPwNz0m6dmr42tqOQalGG20lbm0-ZA7UYrJes1WylLyeNE_sgMVkpI50f8GHfAbZlEsJz54Vt8jqv-DSbXXoK-PyuhvP0y-uMujJQs6cWvztgun_8xvAeR9EykxAhw-7n9h84P0j3zprARVI0JTMjKeZyJ14aupWLEZ350Hd_ryWz36D6dDQJgjWqHh6lvv36Md90KcR-CDB3I2qVHQT5kFJhUNVGK7-eJQs8C0APZvyHS9jynuX4Q","e":"AQAB"},"contact":["mailto:manuel@optimalbits.com"],"agreement":"https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf","initialIp":"85.235.1.31","createdAt":"2016-08-30T15:22:19Z"}} -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/accounts/acme-v01.api.letsencrypt.org/directory/49881ab35d6ac7bb51f05ca3a220fbac/regr.json: -------------------------------------------------------------------------------- 1 | {"body":{"id":4407503,"key":{"kty":"RSA","n":"1sfAvmeOBdJelhXoG0HUrSiiY2PORPkEz9cDnfEVW8-G0w_584kpJHsKm2ixhtmZnjR0lismnaaE9wsqGXCZQ0Gn5dTUkcVKqRyntLKUh1emMY2SjEfTorEZqh6mq-7fUr6NtJU24QEJHWkDFxPp6PsQA8FREcrDIr5gt50xgaK65FxJ4YWCHH8kfCBY9XG5lD8NKya6S9upJFWJtldj7Qplx4JTf-se2YXCqvhVNXGkU_f23ZO9zOt9aPWtEekcZLTHjwTMUhq64vOFV7YlIuUPvQJhLG9AXVszYKjrWak-B_Eq6E74onOyI7W3aT7YjRIZl1RmBnnKmBYgysr2ww","e":"AQAB"},"contact":["mailto:manuel@optimalbits.com"],"agreement":"https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf","initialIp":"217.72.54.127","createdAt":"2016-09-18T14:25:30Z"}} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "types": ["node"], 5 | "target": "ES2017", 6 | "module": "NodeNext", 7 | "incremental": true, 8 | "declaration": true, 9 | "outDir": "dist", 10 | "sourceMap": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "strict": true, 14 | "jsx": "preserve", 15 | "importHelpers": true, 16 | "esModuleInterop": false, 17 | "strictNullChecks": false, 18 | "baseUrl": ".", 19 | "paths": { 20 | "@src/*": ["lib/*"] 21 | }, 22 | "lib": ["esnext"] 23 | }, 24 | "include": ["lib"], 25 | "exclude": ["node_modules", "dist", "test/*"] 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | rules: 2 | space-before-blocks: [2, "never"] 3 | space-after-keywords: [2, "never"] 4 | new-cap: 0 5 | no-underscore-dangle: 0 6 | indent: [2, 2] 7 | brace-style: [2, "1tbs"] 8 | comma-style: [2, "last"] 9 | default-case: 2 10 | func-style: [2, "declaration"] 11 | guard-for-in: 2 12 | no-floating-decimal: 2 13 | no-nested-ternary: 2 14 | no-undefined: 2 15 | radix: 2 16 | space-after-function-name: [2, "never"] 17 | space-after-keywords: [2, "always"] 18 | space-before-blocks: [2, "never"] 19 | spaced-line-comment: [2, "always", { exceptions: ["-"]}] 20 | valid-jsdoc: [2, { prefer: { "return": "returns"}}] 21 | wrap-iife: 2 22 | quotes: [2, "single"] 23 | -------------------------------------------------------------------------------- /samples/sample1.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { createServer } from 'http'; 4 | import { Redbird } from '../dist/index.js'; 5 | import { isPrimary } from 'cluster'; 6 | 7 | async function sample1() { 8 | const proxy = new Redbird({ 9 | port: 8080, 10 | cluster: 4, 11 | keepAlive: true, 12 | }); 13 | 14 | proxy.register({ 15 | src: 'http://localhost', 16 | target: 'localhost:3000/test', 17 | onRequest: (req, res, target) => { 18 | req.headers.foo = 'bar'; 19 | delete req.headers.blah; 20 | }, 21 | }); 22 | } 23 | 24 | sample1(); 25 | 26 | if (!isPrimary) { 27 | createServer(function (req, res) { 28 | res.writeHead(200); 29 | res.write('hello world'); 30 | res.end(); 31 | }).listen(3000); 32 | } 33 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/dev-csr.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIB3jCCAUcCAQAwgZ0xCzAJBgNVBAYTAlNFMQ8wDQYDVQQIEwZTY2FuaWExDTAL 3 | BgNVBAcTBEx1bmQxHzAdBgNVBAoTFk9wdGltYWwgQml0cyBTd2VkZW4gQUIxFDAS 4 | BgNVBAsTC09wdGltYWxCaXRzMRIwEAYDVQQDEwlsb2NhbGhvc3QxIzAhBgkqhkiG 5 | 9w0BCQEWFGluZm9Ab3B0aW1hbGJpdHMuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GN 6 | ADCBiQKBgQDM1n2CE193KOgNnO7EkhkpvN/xa2hPRYFPU7SDqz3OAEIDVu3UjMPK 7 | xlaNwmfQXLrQfhsQXzn7YSHKa5T/EDVZDoXyNuoUmo4KmoE969KwF/eRTz40GVXE 8 | RzXB7BzukORNDa1w/yItm60ODM05iIAwCpPufC8EAk01hpx3VHtixQIDAQABoAAw 9 | DQYJKoZIhvcNAQEFBQADgYEAVOF8HV93E8PvVqqDVG30dZ6PvzT7okZqBNeqh511 10 | DdowLC07r5qIyF/sBtGa9ESBW4H+/Cz58tTea7UzoJZrpCos7J090bM9el1Lzp+J 11 | /VOh9qd0smin7icssVQAlF5wzMsOpR4bL85RPDewx2wALShEYuJfKAOqhlVhh3qj 12 | C8U= 13 | -----END CERTIFICATE REQUEST----- 14 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/dev-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQDM1n2CE193KOgNnO7EkhkpvN/xa2hPRYFPU7SDqz3OAEIDVu3U 3 | jMPKxlaNwmfQXLrQfhsQXzn7YSHKa5T/EDVZDoXyNuoUmo4KmoE969KwF/eRTz40 4 | GVXERzXB7BzukORNDa1w/yItm60ODM05iIAwCpPufC8EAk01hpx3VHtixQIDAQAB 5 | AoGAcKwXE4K2g2Qj6MEG8Wdvoe67vB8ZnGkeDNV9OOPrtjGcHhwl7EGVvSdGGunx 6 | ksI/HEoRdvr6eNTf8mkk5vwyaxND9aJ6Y6Iq9SlLFofgF3lJ3SkoCbbCdvXod33k 7 | o9wu09NtdxaqFQVaP7MrlwSKTNtiFWME14c0npTzZvBHkOECQQDpNGUcGOofBMUx 8 | 6BhEb2gM2tW4DEFQ2rA/0F821mASrRNYldndcwTR0raOARIHgiKVrkvipWzjMVE2 9 | OZpmZATdAkEA4NxB4EP1w7t/T96FuPPOu9uS5jCva5XjX85VwnZJhJhUnOqw/K1o 10 | UnCvpH866USLuPqfHovAm5p0j8YXoDEjCQJAU0QJ9gZPUdP6NN+SCp1coXphZN27 11 | VItA8wgLdyEEHKb/iVm3+IHg7qo11G49acDlaFxbbAl034n0XVAj+PstYQJATpIo 12 | Iqkck0xM7CehKkNnFZVf+zc/1KQHU07SAKU8gyyHRF1tgp1FOqlNdnlOqHvfJr/M 13 | IexLXRPXbvWVA9CnoQJBAOUrDiGmCWbA9+AqkD10nmj6I4BeEhX0e3IHf+SzyXBu 14 | Xkrbej3pJr7XCNkAFq1usvGppKzl52zZCfDtWJlehgs= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | env: 8 | HUSKY: 0 9 | 10 | jobs: 11 | release: 12 | permissions: 13 | contents: write # for release publishing 14 | 15 | name: Release 16 | env: 17 | commitmsg: ${{ github.event.head_commit.message }} 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: lts/* 28 | cache: 'yarn' 29 | - name: Install dependencies Node 30 | run: yarn install --frozen-lockfile --non-interactive 31 | - run: yarn build 32 | - name: Release Node 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | run: npx semantic-release 37 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node */ 2 | 'use strict'; 3 | 4 | var gulp = require('gulp'); 5 | var eslint = require('gulp-eslint'); 6 | 7 | gulp.task('lint', function () { 8 | // Note: To have the process exit with an error code (1) on 9 | // lint error, return the stream and pipe to failOnError last. 10 | return gulp 11 | .src(['./**/*.js', '!./test/**', '!./node_modules/**']) 12 | .pipe( 13 | eslint({ 14 | rules: { 15 | 'space-after-keywords': [2, 'never'], 16 | indent: [2, 2], 17 | 'valid-jsdoc': 0, 18 | 'func-style': 0, 19 | 'no-use-before-define': 0, 20 | camelcase: 1, 21 | 'no-unused-vars': 1, 22 | 'no-alert': 1, 23 | 'no-console': 1, 24 | 'no-unused-expressions': 0, 25 | 'consistent-return': 0, 26 | }, 27 | globals: { 28 | define: true, 29 | }, 30 | }) 31 | ) 32 | .pipe(eslint.format()) 33 | .pipe(eslint.failAfterError()); 34 | }); 35 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/dev-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICszCCAhwCCQDz5KVzUsFJSzANBgkqhkiG9w0BAQUFADCBnTELMAkGA1UEBhMC 3 | U0UxDzANBgNVBAgTBlNjYW5pYTENMAsGA1UEBxMETHVuZDEfMB0GA1UEChMWT3B0 4 | aW1hbCBCaXRzIFN3ZWRlbiBBQjEUMBIGA1UECxMLT3B0aW1hbEJpdHMxEjAQBgNV 5 | BAMTCWxvY2FsaG9zdDEjMCEGCSqGSIb3DQEJARYUaW5mb0BvcHRpbWFsYml0cy5j 6 | b20wHhcNMTQwODI2MTIxMTIwWhcNMTQwOTI1MTIxMTIwWjCBnTELMAkGA1UEBhMC 7 | U0UxDzANBgNVBAgTBlNjYW5pYTENMAsGA1UEBxMETHVuZDEfMB0GA1UEChMWT3B0 8 | aW1hbCBCaXRzIFN3ZWRlbiBBQjEUMBIGA1UECxMLT3B0aW1hbEJpdHMxEjAQBgNV 9 | BAMTCWxvY2FsaG9zdDEjMCEGCSqGSIb3DQEJARYUaW5mb0BvcHRpbWFsYml0cy5j 10 | b20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMzWfYITX3co6A2c7sSSGSm8 11 | 3/FraE9FgU9TtIOrPc4AQgNW7dSMw8rGVo3CZ9BcutB+GxBfOfthIcprlP8QNVkO 12 | hfI26hSajgqagT3r0rAX95FPPjQZVcRHNcHsHO6Q5E0NrXD/Ii2brQ4MzTmIgDAK 13 | k+58LwQCTTWGnHdUe2LFAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEACPsKiw83ouKT 14 | wZg4lt2kUhLS/Zrly4jNKuFyeQaUU1RHTTVagpGFmzLpH0vOyMGtiinjfHK0oHa9 15 | 18Aomq6t1V1WTyn46gL10mFWBuAbCVKypbGx4QmQ/uFEmz/A9x3lMytzGt3Ww5WK 16 | gybUbmCM9mi5IB+wco8miTpOWwOWRIw= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/a.js: -------------------------------------------------------------------------------- 1 | var spdy = require('spdy'), 2 | fs = require('fs'), 3 | path = require('path'); 4 | 5 | var options = { 6 | // Private key 7 | //key: fs.readFileSync(path.join(__dirname, "certs/dev-key.pem")), 8 | //cert: fs.readFileSync(path.join(__dirname, "certs/dev-cert.pem")), 9 | 10 | // **optional** SPDY-specific options 11 | spdy: { 12 | protocols: ['h2', 'spdy/3.1', 'http/1.1'], 13 | plain: false, 14 | 15 | // **optional** 16 | // Parse first incoming X_FORWARDED_FOR frame and put it to the 17 | // headers of every request. 18 | // NOTE: Use with care! This should not be used without some proxy that 19 | // will *always* send X_FORWARDED_FOR 20 | 'x-forwarded-for': true, 21 | 22 | connection: { 23 | windowSize: 1024 * 1024, // Server's window size 24 | 25 | // **optional** if true - server will send 3.1 frames on 3.0 *plain* spdy 26 | autoSpdy31: false, 27 | }, 28 | }, 29 | }; 30 | 31 | var server = spdy.createServer(options, function (req, res) { 32 | res.writeHead(200); 33 | res.end('hello world!'); 34 | }); 35 | 36 | server.listen(3000); 37 | -------------------------------------------------------------------------------- /test/fixtures/test_crt.ts: -------------------------------------------------------------------------------- 1 | export const certificate = `-----BEGIN CERTIFICATE----- 2 | MIIDCTCCAfGgAwIBAgIUAu5McVFExA+FNcHq3unnP7d24SEwDQYJKoZIhvcNAQEL 3 | BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDkxNDA5NTA1NVoXDTI0MTAx 4 | NDA5NTA1NVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF 5 | AAOCAQ8AMIIBCgKCAQEA2vPEL6vNbihGhLH0H0IkkIbBXJsH5AeGFX07LAc6gIym 6 | W0244D0ZkOStxbRfUSgiH9Vf0Z+Qp1ufDlFjB98vbwgy68WFU7mESovuDbTKbNRm 7 | eURgex9VleHIBddqzpHkv8bhbdxrHkgrgbPl14Waa+AxZ1FMcDeJUwkujKKBFFv2 8 | SF8ScogRBTOznA2ITIbFHs/n6d8F3FF/Vx6tZzCX3ziccoD2JROs4RAZqZ1eTdar 9 | 4KZPJ3VyX6WSDvmB5xEE5JeKlCHxNV2qBflXNPIccwVYhNzKVfogwh3XsVElL0GX 10 | GCXhDtSkaWn/BNOo9HQpAy3flBn33mQWbmdnVpqeYwIDAQABo1MwUTAdBgNVHQ4E 11 | FgQU6KMWMiUa2ECYY4VE06h/1f+IObYwHwYDVR0jBBgwFoAU6KMWMiUa2ECYY4VE 12 | 06h/1f+IObYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAA8jp 13 | cX6CeiFm46SyD/w1bFIFEBAI6f+vDOUIh8B0mU2cuZJ8axhA6jw1kztJmDVEUd8+ 14 | /NnSUlVwpi6/pOoIe35gZzK1nNKvvkHp6PT03cf+rV9iJaZseVENs0AQAisq544o 15 | drTgZVmyFlvKr/J6lxRvB8fxRD3EQIVEbGFa45UnEwjOl8+E0gWpI3TR6GkbsHIe 16 | iR6NeyjlWue6icpVfK8lsbRV/ynZI2KfcEB10bmhVLE8Ihq4fUFECn+p8lwO4xkV 17 | pUyfES3f5G7PbujBQyVJcgXZlZZoeBdaHOgSMi8dUNx3Rj6iiMxEGf/L8WGy6C5C 18 | ghp2U+x6nMeXLm2C2w== 19 | -----END CERTIFICATE----- 20 | `; 21 | -------------------------------------------------------------------------------- /hl-tests/64/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // If URL has/.well-known/, send request to upstream API service 4 | var customResolver1 = function (host, url) { 5 | if (/^\/.well-known\//.test(url)) { 6 | return 'http://localhost:3000'; 7 | } 8 | }; 9 | 10 | // assign high priority 11 | customResolver1.priority = 100; 12 | 13 | var proxy = new require('../../index.js')({ 14 | port: 8080, 15 | resolvers: [customResolver1], 16 | secure: true, 17 | ssl: { port: 443 }, 18 | }); 19 | 20 | proxy.register('www', 'http://www.planetex.press:3000', { 21 | /* 22 | ssl: { 23 | key: "/home/planetex/ssl.key", 24 | cert: "/home/planetex/ssl.cert", 25 | } 26 | */ 27 | }); 28 | proxy.register('api', 'http://api.planetex.press:3002', { 29 | /* 30 | ssl: { 31 | key: "/home/planetex/domains/api.planetex.press/ssl.key", 32 | cert: "/home/planetex/domains/api.planetex.press/ssl.cert", 33 | } 34 | */ 35 | }); 36 | proxy.register('dash', 'http://dash.planetex.press:3001', { 37 | /* 38 | ssl: { 39 | key: "/home/planetex/domains/dash.planetex.press/ssl.key", 40 | cert: "/home/planetex/domains/dash.planetex.press/ssl.cert", 41 | } 42 | */ 43 | }); 44 | 45 | var http = require('http'); 46 | 47 | http 48 | .createServer(function (req, res) { 49 | res.writeHead(200); 50 | res.write(req.url); 51 | res.end(); 52 | }) 53 | .listen(3000); 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, OptimalBits 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /samples/lazy-wildcards.mjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { createServer } from 'http'; 4 | import { Redbird } from '../dist/index.js'; 5 | import { isPrimary } from 'cluster'; 6 | 7 | const ONE_DAY = 24 * 60 * 60 * 1000; 8 | 9 | const wildcard_target = { 10 | url: `http://localhost:3000/`, 11 | opts: { 12 | ssl: { 13 | letsencrypt: { 14 | email: 'admin@optimalbits.com', 15 | production: false, 16 | }, 17 | }, 18 | }, 19 | }; 20 | 21 | async function sample1() { 22 | const proxy = new Redbird({ 23 | port: 8080, 24 | cluster: 4, 25 | keepAlive: true, 26 | log: { 27 | name: 'Redbird', 28 | }, 29 | ssl: { 30 | port: 8443, 31 | }, 32 | letsencrypt: { 33 | path: './.letsencrypt', // Path to store Let's Encrypt certificates 34 | port: 9999, // Port for Let's Encrypt challenge responses 35 | renewWithin: 30 * ONE_DAY, // Renew certificates when they are within 1 day of expiration 36 | }, 37 | resolvers: [ 38 | { 39 | fn: (hostname) => { 40 | return wildcard_target; 41 | }, 42 | // A negative priority will put this resolver at the end of the list 43 | priority: -1, 44 | }, 45 | ], 46 | }); 47 | } 48 | 49 | sample1(); 50 | 51 | if (!isPrimary) { 52 | createServer(function (req, res) { 53 | res.writeHead(200); 54 | res.write('hello world'); 55 | res.end(); 56 | }).listen(3000); 57 | } 58 | -------------------------------------------------------------------------------- /lib/interfaces/proxy-options.ts: -------------------------------------------------------------------------------- 1 | import http, { IncomingMessage, ServerResponse } from 'http'; 2 | import httpProxy, { ProxyTargetUrl } from 'http-proxy'; 3 | import { Socket } from 'net'; 4 | import pino from 'pino'; 5 | import { Resolver } from './resolver.js'; 6 | 7 | export interface SSLConfig { 8 | port?: number; 9 | ip?: string; 10 | key?: string; 11 | cert?: string; 12 | ca?: string; 13 | } 14 | 15 | export interface ProxyOptions { 16 | // The port to listen on 17 | port?: number; 18 | 19 | // The host to listen on 20 | host?: string; 21 | 22 | // Keep the connections alive 23 | keepAlive?: boolean; 24 | 25 | preferForwardedHost?: boolean; 26 | 27 | httpProxy?: httpProxy.ServerOptions; 28 | 29 | // Enable Logging 30 | logger?: pino.Logger; 31 | 32 | // Enable Cluster Mode 33 | cluster?: number; 34 | 35 | // Enable LetsEncrypt 36 | letsencrypt?: { 37 | path: string; 38 | port: number; 39 | renewWithin?: number; 40 | minRenewTime?: number; 41 | }; 42 | 43 | resolvers?: Resolver[]; 44 | 45 | // NTLM Auth 46 | ntlm?: boolean; 47 | 48 | // HttpProxy Opts 49 | xfwd?: boolean; 50 | secure?: boolean; 51 | timeout?: number; 52 | proxyTimeout?: number; 53 | 54 | // SSL 55 | ssl?: SSLConfig | SSLConfig[]; 56 | 57 | // Error handler 58 | errorHandler?: ( 59 | err: NodeJS.ErrnoException, 60 | req: IncomingMessage, 61 | res: ServerResponse | Socket, 62 | target?: ProxyTargetUrl 63 | ) => void; 64 | 65 | serverModule?: typeof http; 66 | } 67 | -------------------------------------------------------------------------------- /samples/bench.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple benchmark for Redbird proxy 3 | * We assume that there is a Redbird proxy running on localhost:8080 4 | * and a simple http server running on localhost:3000. 5 | * 6 | * This benchmark will create 1000 parallel requests to the proxy 7 | * and measure the time it takes to complete all requests. 8 | * 9 | * The proxy is running in a separate process, in this file we just create 10 | * the requests. 11 | */ 12 | 13 | import fetch from 'node-fetch'; 14 | const PROXY_PORT = 8080; 15 | 16 | async function benchmark(numRequests = 10000, batchSize = 50, numIterations = 10) { 17 | const numBatches = numRequests / batchSize; 18 | 19 | const results = []; 20 | 21 | for (let k = 0; k < numIterations; k++) { 22 | const start = Date.now(); 23 | const promises = []; 24 | 25 | for (let j = 0; j < numBatches; j++) { 26 | for (let i = 0; i < batchSize; i++) { 27 | promises.push(fetch(`http://localhost:${PROXY_PORT}` /*, { verbose: true }*/)); 28 | } 29 | await Promise.all(promises); 30 | } 31 | 32 | const totalTime = Date.now() - start; 33 | const reqsPerSecond = (numRequests * 1000) / totalTime; 34 | console.log(`Iter: ${k} -> Time taken: ${totalTime}ms, Request per second: ${reqsPerSecond}`); 35 | 36 | results.push(reqsPerSecond); 37 | } 38 | 39 | // Average Requests per second 40 | const avg = results.reduce((acc, val) => acc + val, 0) / results.length; 41 | console.log(`Average Requests per second for ${numIterations} iterations:`, avg); 42 | } 43 | 44 | benchmark(); 45 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/accounts/acme-staging.api.letsencrypt.org/directory/367e0270a5d31ab031561f9f284ca350/private_key.json: -------------------------------------------------------------------------------- 1 | {"kty":"RSA","n":"v7Q7VtM_s1obhDJrSef8oSZtT9-v91cHnqzeTyMf0-Sz3CjYXamzEz_v7ASMS4JsGoG1SeaDlk_GYMO4OyAUNTB5nUJSR5ImNPwNz0m6dmr42tqOQalGG20lbm0-ZA7UYrJes1WylLyeNE_sgMVkpI50f8GHfAbZlEsJz54Vt8jqv-DSbXXoK-PyuhvP0y-uMujJQs6cWvztgun_8xvAeR9EykxAhw-7n9h84P0j3zprARVI0JTMjKeZyJ14aupWLEZ350Hd_ryWz36D6dDQJgjWqHh6lvv36Md90KcR-CDB3I2qVHQT5kFJhUNVGK7-eJQs8C0APZvyHS9jynuX4Q","e":"AQAB","d":"iNfhG-OEL0T9K2rKR2GAZpCFq2Sjuc24NL51mswZ5in1cgz-Fi4TFISpgTLl6ujYvjsk6_HOsLeVhnFvy1Tk1-sYhPdYwJpFB8F9IiEhJ3LI3YDx11E8KEvLUn5M8SPc2-8zxpQ__AiAbhs3WdyOMSE3bBL74b8KBd9iy3-vRRa8SySu--eeR73LcAjWt6c-Mpt6mHBMT83sGQW-OXPAIDQK--vWXwEgZCgq-r6IwAyGcgV3IdTDu-XZwXyXviZ_D7c457YPN9bK4rZKlcW_FFoTEFN1Roq0UFmeIdOcYR6FeieEhICx3W09AvoLtvauPa8CPIzmH1kWLDNPViVigQ","p":"6ECuYDDcc2Z534pqB0tS8Pc89yE0jH1txMFEw-CpbUi8rykKGiEhqZq7xism0nBym2Zr3i6IhhEKRszQkNNR2fXA20L3OjRgDijRemzfO1-MU1m_PSlIQq9O85zB2F0GIvjLMXgvR_vgHNUOyutV0fWM2p8W1IHmPDds4OCGoyk","q":"004r_QTpQCdxWLS4zjVJgVikiUZkVPe8pQOFQmFjdv0C4V1_S23pQhdzrrdGB7gZ00-xeepK2MMHZiPPp-noMouRs7fmC3_-jOlBTe8R4mGjd5hHJZ_fAphyUiBy2uk971aPjiAQAoFlIk_VjLi-p464p14xFx-ls5eWQT1ZXfk","dp":"4xFp6u2aetDz0pP2-c6w9poiZtN2Fu0Cht0WKBPcUdZNc0tCby15ReLcNvE1cYUy57AJQh5op_q8-19_gji4y8ozlasxHxzZ1L4fn_wVfGz8OvmBuYBE_716CT93XdwFBegMcP856rzc7hN39PiE3VOfNJdZsMaXnMPrlSivOZE","dq":"NfoBCJgJkUbCEHvRvXMlPLJNDXf6xy2lda2Ji-ReyRVmd_UvQDBqZmShO187t1sS1cTEvDTaO7bOHAxHzkfU9ZxrcrImRIfEmXA4K1VHh0GTxUgT3IuTJxGUGmCJllwAYzQEZbTRAiLVl8c28MR8h0bQ6ogIGDUQWej-C9pFCtk","qi":"3eTbAKe0oiRQSs456A58jKac4UYS-VN6oGKHkROBq8w8YAYkhQI9jeTQbUm7N7g8bwEIruGUdyq-SXAb8WOS166GdEqB_5bhP9xJw1MLFVaerXVOrDYMCBB-hTKw3kY7SIDoCgVBP3-1GL-fhXbvz3-CdOprwDCf8JMExXcop8k"} -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/accounts/acme-v01.api.letsencrypt.org/directory/49881ab35d6ac7bb51f05ca3a220fbac/private_key.json: -------------------------------------------------------------------------------- 1 | {"kty":"RSA","n":"1sfAvmeOBdJelhXoG0HUrSiiY2PORPkEz9cDnfEVW8-G0w_584kpJHsKm2ixhtmZnjR0lismnaaE9wsqGXCZQ0Gn5dTUkcVKqRyntLKUh1emMY2SjEfTorEZqh6mq-7fUr6NtJU24QEJHWkDFxPp6PsQA8FREcrDIr5gt50xgaK65FxJ4YWCHH8kfCBY9XG5lD8NKya6S9upJFWJtldj7Qplx4JTf-se2YXCqvhVNXGkU_f23ZO9zOt9aPWtEekcZLTHjwTMUhq64vOFV7YlIuUPvQJhLG9AXVszYKjrWak-B_Eq6E74onOyI7W3aT7YjRIZl1RmBnnKmBYgysr2ww","e":"AQAB","d":"0mEDOP4yLR2srJJ0sg4_hgVhWr1uVD0fK35O-qwk4bNbOu5RRO07MZKcBzH7gj0urbpv4JAP2Sg84cc7y4NxfGGZVhSsysRXp2J8GxE5T4DZN3yW6XWJpbiXjP1NAOiQM3qXTyVBhg__n6E296n32s_hFeyLvkO_9A5Kqk_9KB40AtLBmgD99apZo43cvjQWoQTha28vx6MdKSG7cMGbKEoAulCufjYFoUvV8CUETo_vg5Vjzj4rZ0HpyOIfOxjzfLlEBoEPWWSc5NWV4toFU7uimlFW1eTKduyhRwnj9PiJ9aOtmNvvDH20X6BTunCLXM6OKp51jd7M207O8VNg0Q","p":"85Sc7ZWMD5mgx7ggzEPtehpsV1q1M0iVVOZi0955IsU7Fpe4avq4-kBg-9PPJCFrMnNpZOmr9Xc0e84SuShkT_5hmTZH-d8f8aHRknbJHNKerzABfHYgUhza0bpYR0u5Lf77fiRTIFnsyFykotMCP3E10B4HM7bPxN-j9KumDm8","q":"4bs3-irM80lGIW8Q0OkOkcADJUh4HlnK-tL08wIEJ-2rXb_PfPZ3aeMMvZrDoDL2YZerlKRe3vj1ceCkuZaNsnawNUM480y8V6AE4cjhEWo7fSLmvnqmkU2HlTLgUkJKAJXKTCLl3zBNKi9fKpK57EiGgtjjqOT4W6TDU0pXBu0","dp":"8Km1I0jOydsQcEQMo8W5rRrOUMDep3zfjrLSkmMNbL1SVFAzdf-jJB7Xs_jigOBD-eTuDTaTIERXJrvE5Ax0kFTWOXrYQpmiBivL1NpoeoHfJ1hXH5HW_UplKTLkZgz7OebktQ1O1HgE6zIduIKjhetlL-t7Ui0du3b7l5LAzyE","dq":"EPW8EvO8SlsrBcAOh2O7UIAYvGhhfgZJFedbuBZisY1N3tFWiZELD82bW3ORVyv9DwASSCzBZAdYiaHTPo5tPwdj3dybHsyZKgw_0acCIgjVR2Wj6JPWh_xHP5J_AC8y2DBo7qeAlfBPG-hLQiucBIC-en5JPJtXfas3cb6YI1E","qi":"1GV96yL6GV5S7B3rj5TWJUuKpkgTyKe4Q4sOTWh1pTsQ2PVL1Vr--FKM2CT_PvuAf4jWcaZgZoEZiaBGoLAPWBXYjuvaqXZ1dtdNqb-tLhOVSZJ3vAzA-jPJ-OUwQ_-BXl2FGD66plaPK0EvbaufK5a0h8D_9oK8E2TXtdSMtJ0"} -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/api/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA2xcbvINnnA0EuQo3vt6VZYl7Bcy078pybFnDe2Wo2BJTtoJp 3 | n2k4xUUHibMuwxLCZC4iCcQY/4RzMLp3ZDcYvrlB9+THrx6xKusdOgdhktTuDTbA 4 | F6f0W4EthfNpJPBGkwaI9JU8UvHcd9kukiLj9Kv7AbhTsQjGBHT+6PVqp8Gzkf75 5 | lVV4iQyzi1zexw2GMe1XQboykgPU7B1x8YIdV5J7vO7/TsLu/fRvCOy33g1kZ62m 6 | ksMvp+L1WtYwLCVCGgwmMbLndNcGfpHHM82aZL1yJxBcrNgH2m7S3mgfaHlZxe5O 7 | 8WlwxGbVAPZiDaCFDF+4JXaLCvfG41ERRj0bTQIDAQABAoIBAF6tWMYZPw/3rD/O 8 | g5KPG556T9iMwvAQ22upSsmrf9CH8vce2kgSL39IOl6uORoBpFGogfsYa/kXorO/ 9 | ENMU4DOjWTen/QbXS5aRbdriz66lJ448R7yxTu6wHx0QuDJHRyhIHa0cRKpPbIe6 10 | Kd7rBvl3zIvMvRX3BaNtb676RzHgvSrkp4f8c5kCFZ8YHPKsEJml0mIRKW4py7R7 11 | ZwCNOX1Lnbb5z2ZeqCM2RkFgf+UeL40TpFTizYzCETXEWBUn1F37t2i4XM0rwQt1 12 | KCfyhKb7ylsvp6W5wlnZ5cBl80LLnllcpa+GA+u5/5+QCZ1I1jr3SbsBhmEoyco7 13 | DUywyp0CgYEA+L7GXjrDfFzvuPX8v8khEswpSAriE0LCcUCnQMQ3qup/zxzNjodU 14 | Nr4VNGos3Y7+ZF/EoVluhY+cVAl8iTo5i06nWhkWYe7DCUzF4dk1qgXPCPSLyk/W 15 | KN9PZI7xaCSuaWWC5CMqoflDNqQc7ocgCX7QhKztpJmW4gYEMyRhvlMCgYEA4Xrr 16 | Iwbd/Zr7cle2VjuI73j6vMlY3REy9sM74CryldZJ4sYwqMHhxDWnaFNa/Zem46eF 17 | 8A7vflJiLpfnyUxwvVmB+ztVSalA9Q6rD1TEIqbKBmPFw5+oriVAm8QZf1UV+zlQ 18 | 4KqmzNNGcY8n4hAu72IoSHwA5se6jfhdeh2SS98CgYEAxOBCI0zBcsogFrXjgWxQ 19 | mA8tUU8D1pjNS1QPzOxA3y9RT30NmRS1a8qQ//ZVYlsOMCW4fLhLCL08zyre/cIu 20 | z3rGbEJU+9g9WDwClxoTJmoIjp73kX4VFC6DKSUWHwaBYPwuWCEZWi/uqe3E1Gnw 21 | ynMr2QcB5HiH+ocmhc/y6O8CgYAi+CFHiWUcU9DzZs8MiKcwHJ8mcEOr5WL2Ckla 22 | 9s4wls9WsE4Tnh4ZhAi2kVbnRYHIhM6s8GQMP1Kiz0RPX9+MPjl+cTFE/07nsqKs 23 | +gSBK0ThwM+HC1fpyjU+8ybRLK0ADV+RuGWuFoYyTnVtBf2BesOsmi65m/g+1GoK 24 | 6lMqGQKBgAD2RyL/KrbxKGMmqrkthYo4iw2/zAJ7XQ8yaP6OIuzW4pC/OeL52VZP 25 | LjDsegmQeHD4VcCRB/jdtGFZFmGlhtkzQ2iX5+Vbrt6udP80X5CrrK51k0d6LSdg 26 | nsDKs0appoZbfE/oaO2WpSPytyvlBQgoquq/kagYnOc+G2di5VBS 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/dash/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEApaKYlIm3YhX9em4il9cOZiEJZKoKW9O15rm897gPBKpZcnq1 3 | j7kxk6Ah5LCBl2Tlv/R3KoXYOmgvAp3nyM+rail1OURi9TT+RVTHFDPSkg2xzqDD 4 | /WfSMulkrhXowCP/0WCxIsxu1G1Uxt+3GnkWW9uB8MnpOEOoSzVlp2ou93DUqw24 5 | Izqhwt5OsPqZxCrkcF+dAY/9vf8yu+cPNXwSzneYWnx14oiMQ+bZNFlijzXOsIP2 6 | 0ogrdFV6V/LObcd+eMtudbr5Ay0BwHuJoOAr9pN6jk3ANnZCqJZyBoucFQ5ht3kQ 7 | 8xu6rbVXSx3O3kSqCXiOzVRrtsWd3MJS8sQ21QIDAQABAoIBAQCGw3Y1VJ96FL46 8 | AKXwuK8kdTi5SIhJEkXrxa90NbMybo98T06t81Xc8OrisKGf1h4AQh064c9+Jgop 9 | rORLRHwJUlXoFDYXn9hJ/KJFU6y93JqQrckIwpIJjk+2PGk0+5VGe89jqsV53MKf 10 | VKIbze1dK4nbqcwxVQr2tilRP1mbREhfVXV2BIog7rwVYkKKOQPMNaooEaI74sYd 11 | Iy7w8kdfbj1/h4wGFxCgIFxv5HO44C+nxRf4F6XSYJ0hDgMqz9NCOwWC3+deN10k 12 | 2oBN28m57I1wuvjdPHJWfpeq79dchKLCPXiJlA2Nw/1cePvHltz2AXqaV0/FQxPh 13 | reytGvvBAoGBANuFIsm3qZPITDwrKmHbaTXeOcaWE1omycgjM071c0EbBjjwWE2h 14 | ltPPEXLtjmJsZVItQsqvmghLLVAozNt+wqAKM72h4VFZh9sibhSIhjl8x1jODJsF 15 | 7U0yve8AZHcNy3m5/W2/CfoLMD1R/wHR/GQsQ2USQ65EMFoU416veoVJAoGBAMEp 16 | E9xrOwUFyo5oXLwqVzRtU5lhh9LuV6NYUHJAjA7sycFKEL56tueT1XbKc65x74b4 17 | NC6eIFFp5r+aZcXM1LvHKREeB158Jarpy5AJZomVn22DuKKP+4rCCJG6HeCS3Yzw 18 | fYQKLD0z9teBbeHVacWToAfiZ8W7PNWGx5TBiYEtAoGAFY3uC4Z4JSWerq3CXJdx 19 | rjNi0uf7gHecioVCTXd2WKcxpjebRAwgxi1n1jQTLgDctgPxsfsqEbRn/53x939r 20 | 1tEJoY4alKVI3LB1xJhfLZfd7w9UV4huc40O7z/HnZUCLLKherwuW5nro6nAc0pO 21 | EPvzpiHz+VGCueWhKbPrQNkCgYAY63rDehYQLNIYE0C7id7cRap+ZEXIobUuvqsK 22 | QmePWV8iD7MfT+ee8sScYbwQ6mQTjpv007OprTb2yy2MGkPrweL7cVtUBkI5zZXU 23 | jFHtOB9sWo0Mc/TozuWaH1/RZEEu+KvIyCMy9ixHW0xY0VanBceknMH0kZZkmdHP 24 | 0DQ0NQKBgCmVjyRG7qqkG0C38vV+z3Jte45rYUmeurnlmbl2ZcmtEMtuGEwhRfyf 25 | zv/lHSywKkt/7xnIeeYvyIyMZI8GE3TXXkWmZGJUTwC+EGo7Z8T60vEAeN81eVk8 26 | RBMp1Js9mfxU6BOs04d11Qx9rYn8CxJvAfwCUGtebSBQq2fnMd/T 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/dash_/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAtXi0Lf6/K2vy0FQJJY3rRK5J/sPezKWdmw0CgYzhhUqdy2HV 3 | fenpNRVxs7hQEMhtgWcsaGw7a3m9gNc25BgbNCJDS9gXYl+3+M6wjE7xBRR/EruQ 4 | 0SoL1euDfhBRmP9J/I5AeTHzaFlHGqgJ7QayJmo9IpU/A7AvFxpiWOrXk7I4rp2n 5 | c/r9jL1FeyDbcVRbbcy6euwb2tnPPU9LtWcp/oNw+ehZApxtLUxx2+hLgLntafzl 6 | OvjC3jUTAVG4AxK2CH3ka18LCjTFDKgswKnxGIj9O4sQNjUUeH8xQvr/HXQQ/bM3 7 | SAR8xJTsj9EMXSm4PiLfgFPj8C3ORE8HAe9V1wIDAQABAoIBAQCGpG0DJ1zNqcU5 8 | nvA+ZeLmTW5nUQqQylx6exed6Vo2XFthWFBfoNq/4Q5AqwL0wNDGzzcarxsTLftV 9 | idiXOe+NKdLGhikrextzxl8lazjWbROvYW0cU9b+PESOlysDmn5ZnW3mvhH8HSlt 10 | dLoQnAQ4DmEXtKZRZTo7KP9JO80BLlzHZ2LcvrCOLaPMsN9RzXap6ARGurBbMWPd 11 | 8pl4XeyYQelxA+7fkXP74TJOKiY4hkEL60XAQbZDIdeWLJFyhKf2Ky3YggNX6AVm 12 | 2ATptgEmQOCq1LRyVtmcz3+CuMNi/L8u74xQ8FqBstZDDA4d2FabdWj6ydcKpqvx 13 | p2LFMfqJAoGBAOeWbQBn5xwh4gf72vIvB/JWD3ZOSIZu532svZYv953qigiSZy52 14 | vzJIDvLmqWQoQ2o0+LyDder19vKwIJ6ij4E0IA/4iziKWPi5vRyIaVQoHDiobSkY 15 | B6khAwOPq0YNdiq9UoqUZIEYGbN+p1LiTHpO1ryUyKejIBgeB4mfzSX7AoGBAMiZ 16 | 2wDv91FIwAgt0XA31U7kqtgIJiOn1w1PcP3T8oT0LXUA2ZFWrCC3nAIgzIIsiU9f 17 | 2UzZ/NtzgurXuioY9VVyH/1biBFvYL4m3ZY4aoFx8uo4Km54TGMSkuJCngk0TAnV 18 | IVPTBpzGmnbe9NQSp/Bb+RJXGrT+V2Dj01Cby3TVAoGALntw0V4JcwoR9gxE+8sY 19 | yzkezV4VDHaCHCVpwBVMm/ORVPsdnqPS6GKyLWrCoQm7zjtnmV7BcjGAKWHUikKS 20 | jxpJPStjtit+hB2zqWBv06ZhU7Xqgw8Bqp6nnjVd6SeWiimJwarbKVYPAonvR6GI 21 | PBxK2Xr7czo4nN6aILNkV7UCgYB1VsHrL3LMYjCp2Bs9d/tXFZz1lvawPpolmAVx 22 | BExFBwub+C5LvJYc4SnpeMQHlQOQoXFbadtlhpDay+uCemzvWT1rFuJlyG+fat6M 23 | 410xcLT12nq/ebC89v1iSjNlEOk1iyzeen7Qr79krxApCOyhRTtRRhBCNNBpxXoz 24 | GZ6OLQKBgGKBdvOFEptWeSv7V8zqqGv8YEiDon2QILu2EtZV7K8PQ8Y/Iz1CZyJT 25 | gIzO0Y5nD1+ycVLcHkF51pRUjjHsXJI0RFgj/Jv+EiIlNnxGz/eHoQIa5+dbAnEh 26 | bJRbnhcAnAhdcvJ+nY7YQgP6z2Gz6Z5R8X/UCOfG84NTMMnWt5x0 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/api.com/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAwx0q3noqTM8+/XURJxJ02oHxf8MdqY0M46VsCw7u6dXqeVF/ 3 | K/piezQZ2ZKMmdyzHBnrifbkN6oaLsNuW1xEO12wKl3W6j4VlE9NR3CL7ZfmKrTA 4 | EVR/ynW7KOlXvNnN5p6vkuPM4xCu7YgtxpDVzkebE33FuhcgzQdElKuUqdQm7BDB 5 | FWV+S3W9UQyG01APIZ9MjM39NroYdz31U/tRTKwlffLpVGjphIhCivPUP++E/7LL 6 | vB5JAYjJaoZy98JsQ/tvzQ1EsPCkWQaKhtfuPFOaUviLGp7F2NV+wHrzA+b88v8z 7 | BNd2HunVSNp7GpFmBj+gv6syB2O5oBoxGBibgQIDAQABAoIBAFYK9c24qBDJUCjr 8 | yE2nuPpnVX2XKOyNdEKrv5K82iUqncU0aFWXjHhyiHfHRdPQXPgmghWMWCYoEHXQ 9 | 30jQzpIzha2ZRl50VIXb1uOLQVnco7bvkMfTsKsy8f9fr75ren6aOikX5lG4GLxN 10 | Uop/cpoOP9f/ngOrkV55NwgtBllBnOu1P9VoUR6gSdZ8i9b8UZT1FjzKpudYMWmT 11 | pOR8k62OKeze7jnxrqAhL/WzpsNM+rwE2pTvjo+lP3zu4sJqdhNzOK2irkDYHqrf 12 | rvdnkXlnMNCVhtD9qiU6cVijPwTU24hw7R1aY2qpwWdSm2tkw2oNnBD911X3H32E 13 | 26tGeHECgYEA8Cct9KSLUZeE+bllwI/uhIWRYpyH6Rn0FlAHIo/+Dfgl/QX9yTNk 14 | GI8/pUJ+tYClfWPQq+hmUx28zsyUFhqoCEFJDRbORFphGkJsod4RMBNAZokr2kuv 15 | J3qKt3DaX1QQXA+NI6gfg92sJYDv0gAJWrc5QhIVGbRuUypgMUcrjrUCgYEAz/0o 16 | fvtwwMMOoyp3bS8oPnccffi74+0FI+mSccn5GS734SLe2yd6G0SiOmv9W7LGfZ8q 17 | EjSfOZ1uknBBN8OyZ4BVZR9b1aOF5caTlfP3NsKOuQVUekHcrexyNWCuoyblw2VZ 18 | r+ztp0FT+OtWyA2JDkBJBh7+ba4Flpr2No5pTR0CgYEAqwCa/pq0AZNMwq07QRS8 19 | GG0rivY+6MLsRX8StY+mrbfHBRZhEgWf/sTx4vEoXIGQVWrfyakgQ4rnSLHvuJWe 20 | lNI3/DQDCDT688HcrJ39yyfKMbj3GufNfuUJJXocZMjtJUCFlaA/YJxV2hanrfcM 21 | siXJhbxuffE2pc2E1VICOAkCgYBUmIOAIoUZ4jxx4TPyFNwpjAjqs+C4NA+DK92E 22 | qsHGnHP1/ljmiof/z0qsuH+0bGKPdc2G2iBpLr9qkH32UIKf1nLlTnvryTcM3lfp 23 | BfHnM/sZBjH2CBPaKfHKBCkD8y5A61gvVg7TmJ6vAAmsFNVKFpudAb46ni1ntF+w 24 | kPwDgQKBgQDgR0Y4zWHD5HbmhRFs6SZl8WUF3UA4XXdsuyi3CoUDmxw3d7fTmqKb 25 | twyFuVrlm6I5ArTx6IGyYwidFaZglKDeQnLCToarM8GcMipquJzd4tmlkZJb9pOi 26 | cxJjNA6+D1uSGC2LW7gsXsAf/wJ6J7GPH2SzEiezhhgvALfujlX1Vw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/dash.com/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA1F+sb4pKuXmfYub7BHyu3qtTJp47IAv4ywcIXi1Q5ZANwZPV 3 | 1yzJ7r61RYqBG6N/RGr0xuXOkO/Dx3hJvPYYVtDRLq9lx6S2VycI/CO+lgESqJ4T 4 | qn76P5SHrPCYfA1G66t5GY5p8TjxIVyCYeFgMI5xJ6cavKck+LaMmgT1ZKuQLpzD 5 | 2+Fc172WpUItcZcNDByRe9FKQzi0hw2vDxhpF+Te9qYGAlEmn9ZosY7W1vqbpUoc 6 | kVVp8+5l3GJrToKdUoCznU/Z6d8A61oz52BMqDDDN7ar9Cgv3frnJIsG9b/p1n4q 7 | bYjjDgnuGWorG+V0YfLyXmpGYyMNRUrJzwunjQIDAQABAoIBAGoMlHjmX8Yam6Kx 8 | oOtur4v1lYIVRYUNNWF804rjqh/YPWZKwl++t9+GT9K4BRyeGjE6D506qFnrwKHc 9 | yUWYxjKj97EOWQ7Gi18d2Pi/iK4zPvQAa+WZnrEdJFsRkbNwldedOs2uNe0E+DQm 10 | k9z4xCW7G3onkcxe0rF3xuIqU7e+oukPHFW8eYSH/Vd5eMHpdrt8ky+6fB6OCMHy 11 | 3uRsud2L+lTA+73TXl1Q28YODQOrqW3ZwxnIxeA0/BMh9+/WWbp5vne2YRog0Ucy 12 | 1BwP8spoNCAFGogXSqCJZ5lM/FLOrAvRe71Zqh07buW28gTCskpQnwn8FmWrVAKG 13 | U+a5ZoECgYEA95Tp0oOtBp5ZIPqjcmZmIeegAG9SLjiSLIv2riWS4JvoQ6fE7CQC 14 | msR/yGxK95gfxeNYxGqSfS6RfabQ5CrS7n+XQy8nBngypyWOAcS+5HxWZqXdEdri 15 | 3fEQiC50lNFLSvADTBEEjBUhPRL/lzUiC3s2mCvZMrYpga43fBb5MpECgYEA25hK 16 | cEQoI7bOmL8z24MT0E3HgEpkE3nCrpfLKliGuEsg43UXApEK+U3hw78UMXZjtHf+ 17 | sqFbZ3urAUWOsBGBJGvYrQsio8kFLBrH3hlGOIuDwxcK2SeSXcUlQgMtknt7L1jl 18 | Wi96wOXfWbN3rLt/yJweC4uMBUC6k5H89YkIaz0CgYA2mIwHdCoPr5OQBjVM4O/c 19 | wisybVn9/1OcpzC6rmZ5SWgqozB1smswnexf0iGl/9Hh3YSRq76qBD7pKoQeDKN/ 20 | HHvKwPcmIhNpcIqkMTK3SAP1ltXtPguRTbuLjFMBDjZATDUt7QLHsVEnGq1qNrlP 21 | NtiPLfwzhqAYjMaHgrlpQQKBgQCon8ABh4TfL+BjUOesV3Iekatxqy4/+k7xrOQ4 22 | xzPkTuSZZW9e7CvmFtUXcCI8fTHBAifV2awLwd4lotkYAMkPQ7Vl49gctx8+p+30 23 | caoHf7KVW5tb91Qgp2Od1jznb+S/Dd9Iqo7zk1E4W2S5gl10mdVEfkruOa9L5F2/ 24 | 2hNZ7QKBgQCCrgCOMCExxA7jf0WSCCtX/pgZ1fdyI2QXxjso900WwOD2vNfAF2Ih 25 | /2b/8CbFjA68H02990lDxJrH/XEUauEGRWHdwGffiRCQzdVy9vAz1+aYUe0jdA3q 26 | neYlPwFl0tut3e4ouh4fsjTm8OpNRaLeT2JtfgS96ZirxGf2Df9Ugw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/example.com/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEArqJMYsysjp7CdtZQtXdxqUuuuCcx2eFjHObFbulKjf0k7ewj 3 | 2Qfxay6rKpBMZd5EgJl3ServMjrOW1Mv1OK3K1pPl2dUnVVO5+mU/eP6sH2WLFBF 4 | 0Wo7lXcZuDDKx73n5W4gj5DITJlqjQyFa6EXrMYivUWjjQtsh4mbHJ1U8kH2RHwI 5 | 7I2ceNeorSfCBnv5jkJXb8wrUw5KFuT6FqTQV72eJcHsziE5VOxbYzrrjlwWfQ0L 6 | v25LRFdINPG+7phU6HazgjOR4MuW5aMN+O/0DZhXmQ/6sQSLtePrCPURH4ll1WDD 7 | yzdR6oYYm/xQ3XeLGDkKkNU2FsB4INafqwfq/wIDAQABAoIBAE2aW71v3KKIFDyA 8 | 4l6xlHW55wt4h2OeD9AxEL1HuFS+kGGWFRwFtpd9ppyEgR6nleNbzzGuz9qPXVIh 9 | 9lhw4xrFyCasyWIdHbJbD1V/sNArDsfkaBI2VgIGagx6yjHWxy0iMh/6I7g+WKYT 10 | UVrRvFaPubJINvSfhfv4/0/I7o20XEzm2xSAu9YqA9gcTuytqATrRuKAMkHVvwQq 11 | CvRG4FiA+fPzRDETls5BXHDvwdWfzop9ypmqDZMPPH6ohzCi8Pd4obn/P7W9Y6nf 12 | YGVO6uocT1M+Zf5YJxjhgklJnGNJKC6hj7pp9OCNQB/PIE7nMboW0R/zwBKzYwRh 13 | vIr3ixkCgYEA1uPybWgkiDIzwWKOduf17yZ8hzq8f9a8DZHhgVXrPTSDoB1TLNzU 14 | jKl0oopFJqpjCwIR/js9MbhqQC7dhcNnbDQr6sUgwQAFioOkINi8N/QOAt4mpo1O 15 | jDIl9EXwkL466z5dmFk10RVQ/G3OeMcBCY3YzGIvG0DYDaURUV4oa2UCgYEA0ArU 16 | qMUVFbosKXI04al+QU32NuXFn1rQBKYySa2OA2mtkC5g4J81c5iBaWFrQOaYcl5H 17 | BI4XvNfSsxM/Jc+fDhO24rMYwsCJ6Q1hR1NroV5CzypW+hFJANU2y0BnQ23WNLD2 18 | ry+ST+kLyiqU5owrYOiPLCWETk7Av29caW4FQJMCgYArPF3QiX2gMYmcRToozm77 19 | GSFBDB5VEl1v1YQrw5+7Bs/c7UmI4z2Yt5eSBIP5TZrz4gzAvCaJ1HL8SvGjMjei 20 | 27RiXhtC+cAjqGzjdvgXwfD3vr7ED/ZX2tcsGM5YMQ4luryWJIzhboqG34kFX/Tj 21 | eTi/lpmnwBo4VJfxaSJ0yQKBgQDP7MM2GiNEn9lbYwVvNFM3OZAGtgaZic21l9VS 22 | xd7Vkl0haPjyBq0JZzaP+AmVx+I9C/S2nL0kxB+VUnsecy9ohlOWp6DrpDsxbWn4 23 | O2uqz+a93ncnXvczmeU1ppyOS8x2xRcHZ+g3bZeW6o//C8CfDk9ps+VTzmnd6pLV 24 | 3FvreQKBgH9zcS8feTJ0VLxIDsc1VELOTjnTLVxoXavFYnIX9dRI8Lio1vTuX9Dg 25 | c7wXYQtHQ82LdsS1EdxGAuf2ozwU0wkGeYhVMVJA/ZNh1TSwqHpzE16+gXzVCttp 26 | wNoXVJbAJfPpo25JUlP6ARy5/HrJR1MRjA9KmkR2xDS7UELyG7NQ 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/localhost/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAxgdSCza1Q/SWJEOj4+2S09Zd0s/WTtVyVdVqiimwzG7rfu7q 3 | GENRrh6vK0dVJ2EDKmLAv42fU20Dl90dqJuCAEqFWk8D7ySURbmLCJK5XzG6u7u1 4 | zuqTx9lWWn8FV4lw4J8Xwwc/tDZ7AFtQB4+rchc/cknhZnkwKS63O1Me2SREMoYj 5 | +Hq07bq5a87vQ7ccCqkjqVkZwxtmKkotgrGCj8IzZH0JxbDE+Ok5mfnQtKdjik2E 6 | mKvWxQiVmJbcpmJVl+9Tmd3Bwt6hMQ6GW445TX9EJYpLQZW4782oNoF/EvEV0ycg 7 | tTZlQYDlYRh3yZZaFzH56huPWOlfRdo9sXcB4QIDAQABAoIBAEaXYXXB4MgCrmrq 8 | +cdMbyS4q+V0VU7w47sZQstRpUaa27P58tUHWlyZb5Qb740EFh7L6S3fjEYu/DLs 9 | jaAHH0Z/Sh4xQJPFFF3ukFNUCmSW05wMg/jowhlhrljAIuVbhzNrQwsw0FKrgRlV 10 | c6feXR1kkCdrkr/2v2ZO0t6A+OQqWuPtXfpcUjjIgZncxf2pe+4+1nlZMijtZ/9G 11 | RERxKNdkSZphY/i1jQZCV7InA3pEpkkF1Q3sh5KcMHBk1kl4ZCYW6nUnsoHjmxYh 12 | ax9hHYtyxviKfLaK4aIR+km6hNm3CC5s6sk0d7IV+ol+inKoczrFBWOuVYNxqFJ9 13 | F4+edCkCgYEA48SKpXBr/s2TOkayepkOgrFBQo5+jpZQKZdzMp0YqJXdqaQxV8wG 14 | afSb3uMCjcRm6k0R+v897GjIXSvUIxETMlHF8y+tdWhU6Wsh+eQHYhqT/Dsak/9e 15 | QTtN0aT7j3i1Aald1BexM9ceC5u6jqe2qaFI6xsTchNuUQeReK3mbKMCgYEA3pMa 16 | 7DlEM6t84H3Sn2BCf0Wx50h8fGFf/eiSbprvjwoevGQ85eo4TtVotLfQFHDUy2fV 17 | +mzbXoCXD87oeocZpSoeloZC4vyEcoWNKCYg2wzIJSSFASBAXREZM7FFQXS3RPQ7 18 | 0ZPzggnIxlv2GHUk/RvxVx+kOvhNRpwGbfS/26sCgYEAoMsNfgHBm94RA+EI+te2 19 | kLkF8zCZU5v194a9gou47rruA2awluSn2oEe0Ni94ss2RE4oVWN/mbfXSz83wZG6 20 | VZm4/xc3g10mJKrHD5zVQYK12ij3eGedaLuvEkNAfGagkg24+ZPIO2qwAU3tA+yO 21 | XW5JBgDVV4E0LewD3IgX1bUCgYBg2Tfc1CpWJWeoM3fDu9oTkVsRHZx1btWbIWke 22 | UbKt1iR7q05IaPtpajkucdFMI7CkdaFJX7awz1lsGodUUZcaJFK9Atz18hUb0/sR 23 | Hk4rosswRkzNqZ/4HymNMbTF/6iDi5a/4hYSXnmLvpY+HDMlI9SHKZCHzGWrtNaj 24 | X91gEwKBgCkztWNAGpJosIkOkbxohDdGMoY1JQgl6Dd7gWNU8aXU49m9Vdf3urm5 25 | Bw+g20iI5DwZ3u2LQKTplA0u9CU3f0q2KxirQ6tYdxGISBIEh5TugqtgIaf4LKAJ 26 | C639wizq7anvWsmuc2MGr8kQjqdLVQx6ScCv7bMZnyw5LGfQugCn 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAuiM0Wnga2EAgOomqCgBtmjg5LUbsPWRCmZbl1TUKLJ5YVM5H 3 | Apco/zupjOEVKl2cOEs3Qz2Cy3/KkcQb7xrTl/1n7DMZCZOmcKxR6fBsGHIYCnoV 4 | Ybrrz5s18eLE2QshTrNrCuTzYINe5SPEHc8NXMiNs0/llSvQTFkVsOFDPkxBy3m/ 5 | C1p9C48ObJOZK4H38fBwpgvB6Ca7hTl1IkQJdvrFqtNiTPNxXwepUV7adcrflP13 6 | QqbG3A0MlIy383DdFrv81yzxTQGlQyk83YUlP6vc3vkZE+A0iR1w9u9npLyHYGw6 7 | DqV2woWar6qCKkDiOhJBRkxxPLALJyQZvbEXCwIDAQABAoIBADLNbvmONE13WxR/ 8 | BEDMkx13YOuhotKyrZa736jMXCWHZjZnQmxLk23t+72upRc2C3A13zRj8nHWRBR6 9 | wOEGol+mUxndbGT3voKcFZNTAj29zh/16CYPXVMBWrzVFsLiTcnsIDgN+vsJf/ns 10 | RBearlv3hO0+zjtjhOuBmPD42mCC+nb3r3nrshCCYrOdjlbpqhMWfga9oQYMhunN 11 | PtG7NLdxQVcFLS5I0utpAQHopNBZnOhygogLBPely8F3s5d/qSgjEFHQXeDNyG4P 12 | Wr2YPJdQNfspMngx2gXrmWBgTGjdM4+hCh0byO5mS3IyQT1vAm25S4E/Eavf4sLm 13 | qHocGPkCgYEA3ueaFyaZKlZCTcfXCfR8fU75lki6arDi6I78Ed4pWGCMUarp2jDR 14 | JI94JwQEYTtYJhkLTGIMBjY/6INBUuqIQpzbAXogTjyRPmZHyLj4HUtprUSSVri5 15 | HvFT16L9JbBZMfVLmwtgTTBVe+X2bIEACDfyQ9Q3BoJB44EwGVlCUE0CgYEA1cYd 16 | 4Nx69xRTVUKQ/n+2XLMDqSCus6IuuobGbmayOLa9yb1EkWClUvnW65U/eHvyyQa1 17 | tq8eqq7zO91gylUwNi4UNmq1IytLGei4R9ypZRqA017upK55Ezhq0fYMqp3R42kz 18 | daJr/Y8M5oUnJbzxc0BfQqpWENIUhgoWs/DIcLcCgYEAzz7ciKvNeoyKxxCHwey7 19 | ljJIYk8qa6ocvoa6nM5G+LGDpSbYmJIM0gZGe1gDzndDpOBiHdmHPntP/hmTMcl3 20 | eR+ni/8FbFhp3m9wTJKVtX75OSzpNpI3JCrSfko/Pbxxob5kVjpEhl/rCvArpoRm 21 | CD4kFKaJppaTNjhWBSt1OX0CgYEAnzqNYML5OHbER3poo5gfDlcsv9ofJqAD7F2d 22 | CfimgUXkgZLfsuVo3zBHHHyzpRu10HSV/zfbQMlFVW7kvHDNk12pIotC1qpVqzvD 23 | n4tGBY/DKy3H1ZQ7jMx2DGQYNTGOd7QRZ2qOw3O86St+6EYfFnh5PB/CMY85SEnV 24 | dTxBIGsCgYEArqD7jlmSVc4ivkrsUqjecVZ9ZnZClS7W1CIYPX6lD38djfz1lXVH 25 | 2LtfYd3YNDcZy83jayhgr6wbyLGNKH7b3/o//U7bHqNeb4qpK4wKlPRZ4EB+ueeS 26 | d1rGUJ7FE5qUuM9sZ43yEdrrqDr3/Ywyt9760/hnGZo3CNjVRBPrWy4= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem.bak: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAuiM0Wnga2EAgOomqCgBtmjg5LUbsPWRCmZbl1TUKLJ5YVM5H 3 | Apco/zupjOEVKl2cOEs3Qz2Cy3/KkcQb7xrTl/1n7DMZCZOmcKxR6fBsGHIYCnoV 4 | Ybrrz5s18eLE2QshTrNrCuTzYINe5SPEHc8NXMiNs0/llSvQTFkVsOFDPkxBy3m/ 5 | C1p9C48ObJOZK4H38fBwpgvB6Ca7hTl1IkQJdvrFqtNiTPNxXwepUV7adcrflP13 6 | QqbG3A0MlIy383DdFrv81yzxTQGlQyk83YUlP6vc3vkZE+A0iR1w9u9npLyHYGw6 7 | DqV2woWar6qCKkDiOhJBRkxxPLALJyQZvbEXCwIDAQABAoIBADLNbvmONE13WxR/ 8 | BEDMkx13YOuhotKyrZa736jMXCWHZjZnQmxLk23t+72upRc2C3A13zRj8nHWRBR6 9 | wOEGol+mUxndbGT3voKcFZNTAj29zh/16CYPXVMBWrzVFsLiTcnsIDgN+vsJf/ns 10 | RBearlv3hO0+zjtjhOuBmPD42mCC+nb3r3nrshCCYrOdjlbpqhMWfga9oQYMhunN 11 | PtG7NLdxQVcFLS5I0utpAQHopNBZnOhygogLBPely8F3s5d/qSgjEFHQXeDNyG4P 12 | Wr2YPJdQNfspMngx2gXrmWBgTGjdM4+hCh0byO5mS3IyQT1vAm25S4E/Eavf4sLm 13 | qHocGPkCgYEA3ueaFyaZKlZCTcfXCfR8fU75lki6arDi6I78Ed4pWGCMUarp2jDR 14 | JI94JwQEYTtYJhkLTGIMBjY/6INBUuqIQpzbAXogTjyRPmZHyLj4HUtprUSSVri5 15 | HvFT16L9JbBZMfVLmwtgTTBVe+X2bIEACDfyQ9Q3BoJB44EwGVlCUE0CgYEA1cYd 16 | 4Nx69xRTVUKQ/n+2XLMDqSCus6IuuobGbmayOLa9yb1EkWClUvnW65U/eHvyyQa1 17 | tq8eqq7zO91gylUwNi4UNmq1IytLGei4R9ypZRqA017upK55Ezhq0fYMqp3R42kz 18 | daJr/Y8M5oUnJbzxc0BfQqpWENIUhgoWs/DIcLcCgYEAzz7ciKvNeoyKxxCHwey7 19 | ljJIYk8qa6ocvoa6nM5G+LGDpSbYmJIM0gZGe1gDzndDpOBiHdmHPntP/hmTMcl3 20 | eR+ni/8FbFhp3m9wTJKVtX75OSzpNpI3JCrSfko/Pbxxob5kVjpEhl/rCvArpoRm 21 | CD4kFKaJppaTNjhWBSt1OX0CgYEAnzqNYML5OHbER3poo5gfDlcsv9ofJqAD7F2d 22 | CfimgUXkgZLfsuVo3zBHHHyzpRu10HSV/zfbQMlFVW7kvHDNk12pIotC1qpVqzvD 23 | n4tGBY/DKy3H1ZQ7jMx2DGQYNTGOd7QRZ2qOw3O86St+6EYfFnh5PB/CMY85SEnV 24 | dTxBIGsCgYEArqD7jlmSVc4ivkrsUqjecVZ9ZnZClS7W1CIYPX6lD38djfz1lXVH 25 | 2LtfYd3YNDcZy83jayhgr6wbyLGNKH7b3/o//U7bHqNeb4qpK4wKlPRZ4EB+ueeS 26 | d1rGUJ7FE5qUuM9sZ43yEdrrqDr3/Ywyt9760/hnGZo3CNjVRBPrWy4= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/privkey0.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAuiM0Wnga2EAgOomqCgBtmjg5LUbsPWRCmZbl1TUKLJ5YVM5H 3 | Apco/zupjOEVKl2cOEs3Qz2Cy3/KkcQb7xrTl/1n7DMZCZOmcKxR6fBsGHIYCnoV 4 | Ybrrz5s18eLE2QshTrNrCuTzYINe5SPEHc8NXMiNs0/llSvQTFkVsOFDPkxBy3m/ 5 | C1p9C48ObJOZK4H38fBwpgvB6Ca7hTl1IkQJdvrFqtNiTPNxXwepUV7adcrflP13 6 | QqbG3A0MlIy383DdFrv81yzxTQGlQyk83YUlP6vc3vkZE+A0iR1w9u9npLyHYGw6 7 | DqV2woWar6qCKkDiOhJBRkxxPLALJyQZvbEXCwIDAQABAoIBADLNbvmONE13WxR/ 8 | BEDMkx13YOuhotKyrZa736jMXCWHZjZnQmxLk23t+72upRc2C3A13zRj8nHWRBR6 9 | wOEGol+mUxndbGT3voKcFZNTAj29zh/16CYPXVMBWrzVFsLiTcnsIDgN+vsJf/ns 10 | RBearlv3hO0+zjtjhOuBmPD42mCC+nb3r3nrshCCYrOdjlbpqhMWfga9oQYMhunN 11 | PtG7NLdxQVcFLS5I0utpAQHopNBZnOhygogLBPely8F3s5d/qSgjEFHQXeDNyG4P 12 | Wr2YPJdQNfspMngx2gXrmWBgTGjdM4+hCh0byO5mS3IyQT1vAm25S4E/Eavf4sLm 13 | qHocGPkCgYEA3ueaFyaZKlZCTcfXCfR8fU75lki6arDi6I78Ed4pWGCMUarp2jDR 14 | JI94JwQEYTtYJhkLTGIMBjY/6INBUuqIQpzbAXogTjyRPmZHyLj4HUtprUSSVri5 15 | HvFT16L9JbBZMfVLmwtgTTBVe+X2bIEACDfyQ9Q3BoJB44EwGVlCUE0CgYEA1cYd 16 | 4Nx69xRTVUKQ/n+2XLMDqSCus6IuuobGbmayOLa9yb1EkWClUvnW65U/eHvyyQa1 17 | tq8eqq7zO91gylUwNi4UNmq1IytLGei4R9ypZRqA017upK55Ezhq0fYMqp3R42kz 18 | daJr/Y8M5oUnJbzxc0BfQqpWENIUhgoWs/DIcLcCgYEAzz7ciKvNeoyKxxCHwey7 19 | ljJIYk8qa6ocvoa6nM5G+LGDpSbYmJIM0gZGe1gDzndDpOBiHdmHPntP/hmTMcl3 20 | eR+ni/8FbFhp3m9wTJKVtX75OSzpNpI3JCrSfko/Pbxxob5kVjpEhl/rCvArpoRm 21 | CD4kFKaJppaTNjhWBSt1OX0CgYEAnzqNYML5OHbER3poo5gfDlcsv9ofJqAD7F2d 22 | CfimgUXkgZLfsuVo3zBHHHyzpRu10HSV/zfbQMlFVW7kvHDNk12pIotC1qpVqzvD 23 | n4tGBY/DKy3H1ZQ7jMx2DGQYNTGOd7QRZ2qOw3O86St+6EYfFnh5PB/CMY85SEnV 24 | dTxBIGsCgYEArqD7jlmSVc4ivkrsUqjecVZ9ZnZClS7W1CIYPX6lD38djfz1lXVH 25 | 2LtfYd3YNDcZy83jayhgr6wbyLGNKH7b3/o//U7bHqNeb4qpK4wKlPRZ4EB+ueeS 26 | d1rGUJ7FE5qUuM9sZ43yEdrrqDr3/Ywyt9760/hnGZo3CNjVRBPrWy4= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/fixtures/test_key.ts: -------------------------------------------------------------------------------- 1 | export const key = `-----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDa88Qvq81uKEaE 3 | sfQfQiSQhsFcmwfkB4YVfTssBzqAjKZbTbjgPRmQ5K3FtF9RKCIf1V/Rn5CnW58O 4 | UWMH3y9vCDLrxYVTuYRKi+4NtMps1GZ5RGB7H1WV4cgF12rOkeS/xuFt3GseSCuB 5 | s+XXhZpr4DFnUUxwN4lTCS6MooEUW/ZIXxJyiBEFM7OcDYhMhsUez+fp3wXcUX9X 6 | Hq1nMJffOJxygPYlE6zhEBmpnV5N1qvgpk8ndXJfpZIO+YHnEQTkl4qUIfE1XaoF 7 | +Vc08hxzBViE3MpV+iDCHdexUSUvQZcYJeEO1KRpaf8E06j0dCkDLd+UGffeZBZu 8 | Z2dWmp5jAgMBAAECggEAMD9eIGkZ1Cfg3jW+hzLSjtx6+HabJwb3Z33Yj6uqGxCj 9 | SeRXFGayXwjdN/82r9IJrv0cZ7tjK1XXt1Z83YuzTqK+YdD9P8VWHv01wGRx5Mhr 10 | KlbEce0mNWhy5MKi4REqGI5xN33GRv59Q9AKGlNxA0t8Z3l1QBck1hzkWo4mpn7y 11 | obYYKEYfU4AE7DybJG5GqmY/jJT/B4ESAOYwwqMkyjvV1UZc3FvrriIrwhyJcplr 12 | svbtwFzO3Z9YrtAT3GQDOT/NH0era+CY/3wvrzMi8S1MWlBX47+PmtTPfctfc4i/ 13 | wxgfNhYQi2tg2pt46q7iu0lSlY0m1GauxrwJz2sG/QKBgQD/hLBQNFpSD+f6R7Mp 14 | LstBgWvzcgDHky48McScVCu6cGTMQxSAm3iv5JeJUkH+KrJVWGjuhxi/XDwXiAxK 15 | 4KMCXKSMQnQ+fxDs5Kkpv5WiwlU5VPatTHTSFN+5r9Q3OKMyGSsw1bYiA8OlzsAt 16 | T8uIw7jY2TJX/1i3POJcaOU8jQKBgQDbXW5cKoPMYkzJ9iKYvrXClfSkIAFHM8Q2 17 | LetTQovEMTmUulyrzclBEs8pH2EynNsRfR9zcm5i7yoVIuz9gu0wINmul4H0M9lE 18 | 81g49MrvzC2FGIpdhnfa+f3qDk6LFeTYEA+b/AKojdZrHY3nMDKlwgEfY+FZ8ojh 19 | 7eMFGymirwKBgGs61P8jqU/M6w5GbrJ2t8fKt3HXwun+IyYRwK/WRu+b4UEzWXRL 20 | So+OP+XaBmXSpzpXutl3CnSsEW/SoU3/DYmdNVTDQqNDkefIYhFqHDLMmRVRSaAa 21 | eN/88JKtbkKqWxpYI57/4MpPiBiaNl4NoZ3IfVdLduwk9acYPuqG/sS1AoGBAJ8H 22 | SdVpQOkvrkmPtZ8USsKJYbSGO0b6IVnBymMu0BJoOT04KaynYCpbz3EZfaZqjmpM 23 | UjuojpwMlG6ERli2zOriNc1bqut1lyJkY+XOmTxkwe9GTjDlJgjKySggPRKZybBV 24 | XGuRs/+r6/D1mQUsLNt8HMI774f8yv03Lyhpe7KjAoGAONwTSqi/uKa5sAMzExS8 25 | w+ZwglPY0eGAzAfQ94QIqtZTs2uxqGNXv6GBGFLREIdPPht/4fTX3yf1oOsXOpIj 26 | N8sbrq/qwPqk4V6YFynjbsRay35OzHE308Uj0shn3eagtyXtVp1g4JRQ3h05q+yX 27 | ZhCLoRph0ojjH/pIpkjtHzs= 28 | -----END PRIVATE KEY----- 29 | `; 30 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/caturra.exactbytes.com/chain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw 3 | GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 4 | MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw 5 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 6 | 8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym 7 | oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 8 | ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN 9 | xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 10 | dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 11 | AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw 12 | HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 13 | BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu 14 | b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu 15 | Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq 16 | hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF 17 | UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 18 | AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp 19 | DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 20 | IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf 21 | zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI 22 | PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w 23 | SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em 24 | 2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 25 | WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt 26 | n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/chain0.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw 3 | GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 4 | MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw 5 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 6 | 8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym 7 | oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 8 | ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN 9 | xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 10 | dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 11 | AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw 12 | HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 13 | BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu 14 | b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu 15 | Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq 16 | hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF 17 | UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 18 | AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp 19 | DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 20 | IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf 21 | zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI 22 | PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w 23 | SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em 24 | 2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 25 | WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt 26 | n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/caturra.exactbytes.com/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE8TCCA9mgAwIBAgITAPoaP9spbg6rEZeE9ftc0nGrejANBgkqhkiG9w0BAQsF 3 | ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjA5MTgx 4 | MzI4MDBaFw0xNjEyMTcxMzI4MDBaMCExHzAdBgNVBAMTFmNhdHVycmEuZXhhY3Ri 5 | eXRlcy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6IzRaeBrY 6 | QCA6iaoKAG2aODktRuw9ZEKZluXVNQosnlhUzkcClyj/O6mM4RUqXZw4SzdDPYLL 7 | f8qRxBvvGtOX/WfsMxkJk6ZwrFHp8GwYchgKehVhuuvPmzXx4sTZCyFOs2sK5PNg 8 | g17lI8Qdzw1cyI2zT+WVK9BMWRWw4UM+TEHLeb8LWn0Ljw5sk5krgffx8HCmC8Ho 9 | JruFOXUiRAl2+sWq02JM83FfB6lRXtp1yt+U/XdCpsbcDQyUjLfzcN0Wu/zXLPFN 10 | AaVDKTzdhSU/q9ze+RkT4DSJHXD272ekvIdgbDoOpXbChZqvqoIqQOI6EkFGTHE8 11 | sAsnJBm9sRcLAgMBAAGjggIfMIICGzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYw 12 | FAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFBdA 13 | wGk8f15CB4YLr6G1JhTTrxUJMB8GA1UdIwQYMBaAFMDMA0a5WCDMXHJw8+EuyyCm 14 | 9Wg6MHgGCCsGAQUFBwEBBGwwajAzBggrBgEFBQcwAYYnaHR0cDovL29jc3Auc3Rn 15 | LWludC14MS5sZXRzZW5jcnlwdC5vcmcvMDMGCCsGAQUFBzAChidodHRwOi8vY2Vy 16 | dC5zdGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wIQYDVR0RBBowGIIWY2F0dXJy 17 | YS5leGFjdGJ5dGVzLmNvbTCB/gYDVR0gBIH2MIHzMAgGBmeBDAECATCB5gYLKwYB 18 | BAGC3xMBAQEwgdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQu 19 | b3JnMIGrBggrBgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkg 20 | YmUgcmVsaWVkIHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFj 21 | Y29yZGFuY2Ugd2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0 22 | dHBzOi8vbGV0c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUA 23 | A4IBAQCugwNrp0wmMHoU33jzeOlCWaHLnYplYxGp2zL0uU0/DxWLyVn5u6dj7hcq 24 | UggYh+8PL0iVLPSfLId4t1aI7gor1qmF6L9yzAQq0vderwXMXvLurOsVfEaGJjrA 25 | mpmTews20NdDFJaYly/4GrDfb07D8NTBlzu9sUkRUznBdJ/8u/SjBkFAK5ICxkJ7 26 | /6KiKwk7g0k9vyMMfAsLCNEPueY17mCnlzj7A2N90np4QXjgcFdFqibAQ6J/+La0 27 | flU1PWfms+UwRjt4vBebAmInd50lofObm6OPnB9X9IH0vVvnZBUIhk8BE+wxnlw9 28 | piOf4l9a4N8YiiR+VTUGSOOO7jsN 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/cert0.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE8TCCA9mgAwIBAgITAPoaP9spbg6rEZeE9ftc0nGrejANBgkqhkiG9w0BAQsF 3 | ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjA5MTgx 4 | MzI4MDBaFw0xNjEyMTcxMzI4MDBaMCExHzAdBgNVBAMTFmNhdHVycmEuZXhhY3Ri 5 | eXRlcy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6IzRaeBrY 6 | QCA6iaoKAG2aODktRuw9ZEKZluXVNQosnlhUzkcClyj/O6mM4RUqXZw4SzdDPYLL 7 | f8qRxBvvGtOX/WfsMxkJk6ZwrFHp8GwYchgKehVhuuvPmzXx4sTZCyFOs2sK5PNg 8 | g17lI8Qdzw1cyI2zT+WVK9BMWRWw4UM+TEHLeb8LWn0Ljw5sk5krgffx8HCmC8Ho 9 | JruFOXUiRAl2+sWq02JM83FfB6lRXtp1yt+U/XdCpsbcDQyUjLfzcN0Wu/zXLPFN 10 | AaVDKTzdhSU/q9ze+RkT4DSJHXD272ekvIdgbDoOpXbChZqvqoIqQOI6EkFGTHE8 11 | sAsnJBm9sRcLAgMBAAGjggIfMIICGzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYw 12 | FAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFBdA 13 | wGk8f15CB4YLr6G1JhTTrxUJMB8GA1UdIwQYMBaAFMDMA0a5WCDMXHJw8+EuyyCm 14 | 9Wg6MHgGCCsGAQUFBwEBBGwwajAzBggrBgEFBQcwAYYnaHR0cDovL29jc3Auc3Rn 15 | LWludC14MS5sZXRzZW5jcnlwdC5vcmcvMDMGCCsGAQUFBzAChidodHRwOi8vY2Vy 16 | dC5zdGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wIQYDVR0RBBowGIIWY2F0dXJy 17 | YS5leGFjdGJ5dGVzLmNvbTCB/gYDVR0gBIH2MIHzMAgGBmeBDAECATCB5gYLKwYB 18 | BAGC3xMBAQEwgdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQu 19 | b3JnMIGrBggrBgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkg 20 | YmUgcmVsaWVkIHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFj 21 | Y29yZGFuY2Ugd2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0 22 | dHBzOi8vbGV0c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUA 23 | A4IBAQCugwNrp0wmMHoU33jzeOlCWaHLnYplYxGp2zL0uU0/DxWLyVn5u6dj7hcq 24 | UggYh+8PL0iVLPSfLId4t1aI7gor1qmF6L9yzAQq0vderwXMXvLurOsVfEaGJjrA 25 | mpmTews20NdDFJaYly/4GrDfb07D8NTBlzu9sUkRUznBdJ/8u/SjBkFAK5ICxkJ7 26 | /6KiKwk7g0k9vyMMfAsLCNEPueY17mCnlzj7A2N90np4QXjgcFdFqibAQ6J/+La0 27 | flU1PWfms+UwRjt4vBebAmInd50lofObm6OPnB9X9IH0vVvnZBUIhk8BE+wxnlw9 28 | piOf4l9a4N8YiiR+VTUGSOOO7jsN 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /test/onrequest.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { describe, it, expect } from 'vitest'; 4 | import { Redbird } from '../lib/index.js'; // Adjust the import path if necessary 5 | import fetch from 'node-fetch'; 6 | import { createServer, IncomingMessage } from 'http'; 7 | 8 | const TEST_PORT = 3000; 9 | 10 | describe('onRequest hook', function () { 11 | it('should be able to modify headers for a route', async () => { 12 | let proxy; 13 | let proxyReq; 14 | let saveProxyHeaders; 15 | 16 | const promiseServer = testServer(); 17 | 18 | let target; 19 | proxy = new Redbird({ port: 18999 }); 20 | proxy.register({ 21 | src: 'localhost/x', 22 | target: `http://localhost:${TEST_PORT}/test`, 23 | onRequest: (req, res, tgt) => { 24 | proxyReq = req; 25 | saveProxyHeaders = Object.assign({}, req.headers); 26 | req.headers.foo = 'bar'; 27 | delete req.headers.blah; 28 | target = tgt; 29 | }, 30 | }); 31 | 32 | const res = await fetch('http://localhost:18999/x', { 33 | headers: { 34 | blah: 'xyz', 35 | }, 36 | }); 37 | 38 | expect(res.status).to.equal(200); 39 | expect(target).to.exist; 40 | expect(saveProxyHeaders).to.exist; 41 | expect(saveProxyHeaders.blah).to.equal('xyz'); 42 | 43 | await proxy.close(); 44 | 45 | const req = await promiseServer; 46 | expect(req).to.exist; 47 | expect(req.headers.foo).to.equal('bar'); 48 | expect(req.headers.blah).to.equal(undefined); 49 | }); 50 | }); 51 | 52 | function testServer() { 53 | return new Promise(function (resolve, reject) { 54 | const server = createServer(function (req, res) { 55 | res.write(''); 56 | res.end(); 57 | server.close((err) => { 58 | if (err) { 59 | return reject(err); 60 | } 61 | resolve(req); 62 | }); 63 | }); 64 | 65 | server.listen(TEST_PORT); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | 4 | var proxy = new require('../../index.js')({ 5 | /* 6 | letsencrypt: { 7 | path: __dirname + '/certs', 8 | port: 9999 9 | }, 10 | */ 11 | // bunyan: true, 12 | port: 8080, 13 | secure: true, 14 | // http2: true, 15 | // cluster: 8 16 | ssl: { port: 4443 }, 17 | }); 18 | 19 | /* 20 | proxy.register("caturra.exactbytes.com", "127.0.0.1:3000", { 21 | ssl: { 22 | key: path.join(__dirname, "certs/dev-key.pem"), 23 | cert: path.join(__dirname, "certs/dev-cert.pem"), 24 | } 25 | }); 26 | */ 27 | proxy.register('localhost', '127.0.0.1:3000', { 28 | ssl: { 29 | key: path.join(__dirname, 'certs/dev-key.pem'), 30 | cert: path.join(__dirname, 'certs/dev-cert.pem'), 31 | }, 32 | }); 33 | 34 | // proxy.register("localhost", "127.0.0.1:3000"); 35 | 36 | var http = require('http'); 37 | var keepAliveAgent = new http.Agent({ keepAlive: true, maxSockets: 1000 }); 38 | // http.globalAgent = keepAliveAgent; 39 | 40 | /* 41 | var httpProxy = require('http-proxy'); 42 | httpProxy.createProxyServer({target:'http://localhost:3000', agent: keepAliveAgent}).listen(8090); 43 | // httpProxy.createProxyServer({target:'http://localhost:3000'}).listen(8080); 44 | 45 | // var reqFast = require('req-fast'); 46 | // var request = require('request'); 47 | var needle = require('needle'); 48 | 49 | http.createServer(function(req, res){ 50 | // request.get('http://127.0.0.1:3000').pipe(res); 51 | // reqFast('http://127.0.0.1:3000').pipe(res); 52 | // needle.request('get', 'http://127.0.0.1:3000', null, {agent: keepAliveAgent, connection: 'keep-alive'}).pipe(res); 53 | http.get({ 54 | hostname: 'localhost', 55 | port: 3000, 56 | path: '/', 57 | agent: keepAliveAgent 58 | }, function(upstreamRes) { 59 | upstreamRes.pipe(res); 60 | }); 61 | }).listen(8080); 62 | */ 63 | 64 | var size = 32; 65 | console.log('SIZE:', size); 66 | var randomstring = require('randomstring'); 67 | var msg = randomstring.generate(size); 68 | http 69 | .createServer(function (req, res) { 70 | res.writeHead(200); 71 | res.write(msg); 72 | res.end(); 73 | }) 74 | .listen(3000); 75 | -------------------------------------------------------------------------------- /docs/CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | ## [1.0.2](https://github.com/OptimalBits/redbird/compare/v1.0.1...v1.0.2) (2024-09-30) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **package.json:** add missing main field ([55a35d3](https://github.com/OptimalBits/redbird/commit/55a35d368b5433554d0282a27c0836949568e1ea)) 7 | 8 | ## [1.0.1](https://github.com/OptimalBits/redbird/compare/v1.0.0...v1.0.1) (2024-09-19) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * add dist files to npm package ([80264ac](https://github.com/OptimalBits/redbird/commit/80264ac4b15667b0124afa0f7bf11649b0245203)) 14 | 15 | # 1.0.0 (2024-09-19) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * better error handling ([7c936ab](https://github.com/OptimalBits/redbird/commit/7c936ab8327f80341c167a10e8bc72f8ed2add4c)) 21 | * correct letsencrypt import ([2ea67b9](https://github.com/OptimalBits/redbird/commit/2ea67b99c1a680737794d075aa75c78e0f29311d)) 22 | * correct path generation for LE challenges ([96a6ff2](https://github.com/OptimalBits/redbird/commit/96a6ff244d562a2a29bd10312217276799b18e5c)) 23 | * incorporate greenlock files into redbird ([#295](https://github.com/OptimalBits/redbird/issues/295)) ([bfbe47c](https://github.com/OptimalBits/redbird/commit/bfbe47c18a5d5b1cd27fcedd910ef736fdf9ca8f)) 24 | * pass loopback port to LE ([0e42773](https://github.com/OptimalBits/redbird/commit/0e427737b3878bc43b885ffe6b29aa6ab699d3dd)) 25 | * Use object.assign instead of spread operator ([ca5451b](https://github.com/OptimalBits/redbird/commit/ca5451b96c5052f7d1db44d254533f3c517d393c)) 26 | 27 | 28 | ### Features 29 | 30 | * add support for custom resolvers certificates ([ea5922d](https://github.com/OptimalBits/redbird/commit/ea5922d0628f07671e1773e179486b05d6df639e)) 31 | * add support for keepAlive ([c4bb6c2](https://github.com/OptimalBits/redbird/commit/c4bb6c29eaf0cc33a2111f0243f823b9ff388168)) 32 | * add support for lazy let's encrypt certificates ([b4efeba](https://github.com/OptimalBits/redbird/commit/b4efeba277ff843dc8c1c627c7b699f42e2cd048)) 33 | * replace bunyan by pino ([6c860d6](https://github.com/OptimalBits/redbird/commit/6c860d68ac680a627740019dc9e2bacd7d64e113)) 34 | * Updated Readme.MD with new resolver parameter ([4dc39f5](https://github.com/OptimalBits/redbird/commit/4dc39f59816a89bf9d3a26f70cfe0352ede9cc80)) 35 | 36 | 37 | ### Performance Improvements 38 | 39 | * change while/shift to for loop ([e5c94df](https://github.com/OptimalBits/redbird/commit/e5c94df934f2b2b329bf66bae48a4f5b6a87d2be)) 40 | * enable keepAlive in sample code ([55049eb](https://github.com/OptimalBits/redbird/commit/55049eb11b205501b34baed3a864910633613ac2)) 41 | -------------------------------------------------------------------------------- /test/pathnames.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { createServer, get, IncomingMessage, ServerResponse } from 'http'; 4 | import { describe, it, expect } from 'vitest'; 5 | import { Redbird } from '../lib/index.js'; // Adjust the import path if necessary 6 | 7 | const TEST_PORT = 54673; 8 | const PROXY_PORT = 53432; 9 | 10 | const opts = { 11 | port: PROXY_PORT, 12 | }; 13 | 14 | describe('Target with pathnames', function () { 15 | it('Should be proxyed to target with pathname and source pathname concatenated', function () { 16 | const redbird = new Redbird(opts); 17 | 18 | expect(redbird.routing).to.be.an('object'); 19 | 20 | redbird.register('127.0.0.1', `127.0.0.1:${TEST_PORT}/foo/bar/qux`); 21 | 22 | expect(redbird.routing).to.have.property('127.0.0.1'); 23 | 24 | const promiseServer = testServer().then(function (req) { 25 | expect(req.url).to.be.eql('/foo/bar/qux/a/b/c'); 26 | }); 27 | 28 | return new Promise(function (resolve, reject) { 29 | get('http://127.0.0.1:' + PROXY_PORT + '/a/b/c', async function (res) { 30 | try { 31 | await redbird.close(); 32 | await promiseServer; 33 | resolve(); 34 | } catch (err) { 35 | reject(err); 36 | } 37 | }); 38 | }); 39 | }); 40 | 41 | it('Should be proxyed to target with pathname and source pathname concatenated case 2', function () { 42 | const redbird = new Redbird(opts); 43 | 44 | expect(redbird.routing).to.be.an('object'); 45 | 46 | redbird.register('127.0.0.1/path', '127.0.0.1:' + TEST_PORT + '/foo/bar/qux'); 47 | 48 | expect(redbird.routing).to.have.property('127.0.0.1'); 49 | 50 | const promiseServer = testServer().then((req: IncomingMessage) => { 51 | expect(req.url).to.be.eql('/foo/bar/qux/a/b/c'); 52 | }); 53 | 54 | return new Promise(function (resolve, reject) { 55 | get(`http://127.0.0.1:${PROXY_PORT}/path/a/b/c`, async (res: IncomingMessage) => { 56 | try { 57 | await redbird.close(); 58 | await promiseServer; 59 | resolve(); 60 | } catch (err) { 61 | reject(err); 62 | } 63 | }); 64 | }); 65 | }); 66 | }); 67 | 68 | function testServer() { 69 | return new Promise(function (resolve, reject) { 70 | const server = createServer(function (req, res) { 71 | res.write(''); 72 | res.end(); 73 | server.close((err) => { 74 | if (err) { 75 | return reject(err); 76 | } 77 | resolve(req); 78 | }); 79 | }); 80 | 81 | server.listen(TEST_PORT); 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/renewal/caturra.exactbytes.com.conf.bak: -------------------------------------------------------------------------------- 1 | #cert = :configDir/live/:hostname/cert.pem 2 | cert = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/cert.pem 3 | privkey = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem 4 | chain = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/chain.pem 5 | fullchain = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/fullchain.pem 6 | 7 | # Options and defaults used in the renewal process 8 | [renewalparams] 9 | apache_enmod = a2enmod 10 | no_verify_ssl = False 11 | ifaces = None 12 | apache_dismod = a2dismod 13 | register_unsafely_without_email = False 14 | uir = None 15 | installer = none 16 | config_dir = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs 17 | text_mode = True 18 | # junk? 19 | # https://github.com/letsencrypt/letsencrypt/issues/1955 20 | func = 21 | prepare = False 22 | work_dir = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/letsencrypt/var/lib 23 | tos = True 24 | init = False 25 | duplicate = False 26 | # this is for the domain 27 | key_path = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem 28 | nginx = False 29 | fullchain_path = :fullchain_path 30 | email = manuel@optimalbits.com 31 | csr = None 32 | agree_dev_preview = None 33 | redirect = None 34 | verbose_count = -3 35 | config_file = None 36 | renew_by_default = True 37 | hsts = False 38 | authenticator = webroot 39 | domains = caturra.exactbytes.com, #comma,delimited,list 40 | rsa_key_size = 2048 41 | # starts at 0 and increments at every renewal 42 | checkpoints = 0 43 | manual_test_mode = False 44 | apache = False 45 | cert_path = :cert_path 46 | webroot_path = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/.well-known/acme-challenge, # comma,delimited,list 47 | strict_permissions = False 48 | apache_server_root = /etc/apache2 49 | # https://github.com/letsencrypt/letsencrypt/issues/1948 50 | account = 367e0270a5d31ab031561f9f284ca350 51 | manual_public_ip_logging_ok = False 52 | chain_path = :chain_path 53 | standalone = False 54 | manual = False 55 | server = https://acme-staging.api.letsencrypt.org/directory 56 | standalone_supported_challenges = "http-01,tls-sni-01" 57 | webroot = True 58 | apache_init_script = None 59 | user_agent = None 60 | apache_ctl = apache2ctl 61 | apache_le_vhost_ext = -le-ssl.conf 62 | debug = False 63 | tls_sni_01_port = 443 64 | logs_dir = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/letsencrypt/var/log 65 | configurator = None 66 | [[webroot_map]] 67 | # :hostname = :webroot_path 68 | caturra.exactbytes.com = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/.well-known/acme-challenge 69 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/renewal/caturra.exactbytes.com.conf: -------------------------------------------------------------------------------- 1 | #cert = :configDir/live/:hostname/cert.pem 2 | cert = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/cert.pem 3 | privkey = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem 4 | chain = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/chain.pem 5 | fullchain = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/fullchain.pem 6 | 7 | # Options and defaults used in the renewal process 8 | [renewalparams] 9 | apache_enmod = a2enmod 10 | no_verify_ssl = False 11 | ifaces = None 12 | apache_dismod = a2dismod 13 | register_unsafely_without_email = False 14 | uir = None 15 | installer = none 16 | config_dir = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs 17 | text_mode = True 18 | # junk? 19 | # https://github.com/letsencrypt/letsencrypt/issues/1955 20 | func = 21 | prepare = False 22 | work_dir = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/letsencrypt/var/lib 23 | tos = True 24 | init = False 25 | duplicate = False# this is for the domain 26 | key_path = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem 27 | key_path = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem 28 | nginx = False 29 | fullchain_path = :fullchain_path 30 | email = manuel@optimalbits.com 31 | csr = None 32 | agree_dev_preview = None 33 | redirect = None 34 | verbose_count = -3 35 | config_file = None 36 | renew_by_default = True 37 | hsts = False 38 | authenticator = webroot #comma,delimited,list 39 | domains = caturra.exactbytes.com, 40 | rsa_key_size = 2048# starts at 0 and increments at every renewal 41 | checkpoints = 0 42 | checkpoints = 0 43 | manual_test_mode = False 44 | apache = False 45 | cert_path = :cert_path # comma,delimited,list 46 | webroot_path = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/.well-known/acme-challenge, 47 | strict_permissions = False 48 | apache_server_root = /etc/apache2# https://github.com/letsencrypt/letsencrypt/issues/1948 49 | account = 367e0270a5d31ab031561f9f284ca350 50 | account = 367e0270a5d31ab031561f9f284ca350 51 | manual_public_ip_logging_ok = False 52 | chain_path = :chain_path 53 | standalone = False 54 | manual = False 55 | server = https://acme-staging.api.letsencrypt.org/directory 56 | standalone_supported_challenges = "http-01,tls-sni-01" 57 | webroot = True 58 | apache_init_script = None 59 | user_agent = None 60 | apache_ctl = apache2ctl 61 | apache_le_vhost_ext = -le-ssl.conf 62 | debug = False 63 | tls_sni_01_port = 443 64 | logs_dir = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/letsencrypt/var/log 65 | configurator = None 66 | # :hostname = :webroot_path 67 | caturra.exactbytes.com = /Users/manuel/dev/redbird/hl-tests/letsencrypt/certs/caturra.exactbytes.com/.well-known/acme-challenge 68 | -------------------------------------------------------------------------------- /lib/redis-backend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var redis = require('redis'); 4 | var _ = require('lodash'); 5 | 6 | /** 7 | Instantiates a Redis Redbird backend. 8 | 9 | opts: { 10 | prefix: '', 11 | port: 6739, 12 | host: 'localhost', 13 | opts: {} 14 | } 15 | */ 16 | function RedisBackend(port, hostname, opts) { 17 | if (!(this instanceof RedisBackend)) { 18 | return new RedisBackend(port, hostname, opts); 19 | } 20 | 21 | opts = opts || {}; 22 | port = port || 6379; 23 | hostname = hostname || 'localhost'; 24 | 25 | this.redis = redis.createClient(port, hostname, opts); 26 | this.publish = redis.createClient(port, hostname, opts); 27 | 28 | this.prefix = opts.prefix + ''; 29 | 30 | this.baseKey = baseKey(this.prefix); 31 | } 32 | 33 | /** 34 | Returns a Promise that resolves to an array with all the 35 | registered services and removes the expired ones. 36 | */ 37 | RedisBackend.prototype.getServices = function () { 38 | var _this = this; 39 | var redis = this.redis; 40 | var baseKey = this.baseKey; 41 | 42 | // 43 | // Get all members in the service set. 44 | // 45 | return redis 46 | .smembersAsync(baseKey + 'ids') 47 | .then(function (serviceIds) { 48 | return Promise.all( 49 | _.map(serviceIds, function (id) { 50 | return _this.getService(id); 51 | }) 52 | ); 53 | }) 54 | .then(function (services) { 55 | // Clean expired services 56 | return _.compact(services); 57 | }); 58 | }; 59 | 60 | RedisBackend.prototype.getService = function (id) { 61 | var redis = this.redis; 62 | // 63 | // Get service hash 64 | // 65 | return redis.hgetallAsync(this.baseKey + id).then(function (service) { 66 | if (service) { 67 | return service; 68 | } else { 69 | // 70 | // Service has expired, we must delete it from the service set. 71 | // 72 | return redis.sremAsync(id); 73 | } 74 | }); 75 | }; 76 | 77 | RedisBackend.prototype.register = function (service) { 78 | var redis = this.redis; 79 | var publish = this.publish; 80 | var baseKey = this.baseKey; 81 | 82 | // 83 | // Get unique service ID. 84 | // 85 | return redis 86 | .incrAsync(baseKey + 'counter') 87 | .then(function (id) { 88 | // Store it 89 | redis.hset(baseKey + id, service).then(function () { 90 | return id; 91 | }); 92 | }) 93 | .then(function (id) { 94 | // 95 | // // Publish a meesage so that the proxy can react faster to a new registration. 96 | // 97 | return publish.publishAsync(baseKey + 'registered', id).then(function () { 98 | return id; 99 | }); 100 | }); 101 | }; 102 | 103 | RedisBackend.prototype.ping = function (id) { 104 | return this.redis.pexpireAsync(id, 5000); 105 | }; 106 | 107 | function baseKey(prefix) { 108 | return 'redbird-' + prefix + '-services-'; 109 | } 110 | 111 | module.exports = RedisBackend; 112 | -------------------------------------------------------------------------------- /lib/etcd-backend.ts: -------------------------------------------------------------------------------- 1 | /*eslint-env node */ 2 | 'use strict'; 3 | 4 | /** 5 | Redbird ETCD Module 6 | This module handles automatic proxy registration via etcd 7 | */ 8 | import { Redbird } from './proxy.js'; 9 | 10 | export class ETCDModule { 11 | private etcd: any; 12 | private etcd_dir: string; 13 | private watcher: any; 14 | 15 | constructor(private redbird: Redbird, options: any) { 16 | const Etcd = require('node-etcd'); 17 | 18 | // Create Redbird Instance and Log 19 | this.redbird = redbird; 20 | const log = redbird.logger; 21 | const _this = this; 22 | 23 | // Create node-etcd Instance 24 | this.etcd = new Etcd(options.hosts, options.ssloptions); 25 | this.etcd_dir = typeof options.path !== 'undefined' ? options.path : 'redbird'; 26 | 27 | // Create directory if not created 28 | this.etcd.get(this.etcd_dir, function (err: any, body: any, header: any) { 29 | if (err && err.errorCode == 100) { 30 | _this.etcd.mkdir(_this.etcd_dir, function (err: NodeJS.ErrnoException) { 31 | if (err) { 32 | log.error(err, 'etcd backend error'); 33 | } else { 34 | createWatcher(); 35 | } 36 | }); 37 | } else if (!err && body.node.dir) { 38 | createWatcher(); 39 | } else { 40 | log.error(err, 'etcd backend error'); 41 | } 42 | }); 43 | 44 | // Helper function to check if values contain settings 45 | function IsJsonString(str: string) { 46 | try { 47 | JSON.parse(str); 48 | } catch (e) { 49 | return false; 50 | } 51 | return true; 52 | } 53 | 54 | // Helper function to pretify etcd directory strings 55 | function removeEtcDir(str: string) { 56 | return str.replace(_this.etcd_dir, '').replace(/^\/+|\/+$/g, ''); 57 | } 58 | 59 | function createWatcher() { 60 | // Watch etcd directory 61 | _this.watcher = _this.etcd.watcher(_this.etcd_dir, null, { recursive: true }); 62 | 63 | // On Add/Update 64 | _this.watcher.on('change', function (body: any, headers: any) { 65 | if (body.node.key && body.node.value && !IsJsonString(body.node.value)) { 66 | _this.redbird.register(removeEtcDir(body.node.key), body.node.value); 67 | } else if (body.node.key && body.node.value && IsJsonString(body.node.value)) { 68 | var config = JSON.parse(body.node.value); 69 | if (typeof config.docker !== 'undefined') { 70 | require('./index.js') 71 | .docker(_this.redbird) 72 | .register(body.node.key, body.node.value.docker, body.node.value); 73 | } else { 74 | _this.redbird.register(removeEtcDir(body.node.key), config.hosts, config); 75 | } 76 | } 77 | }); 78 | 79 | // On Delete 80 | _this.watcher.on('delete', function (body: any, headers: any) { 81 | if (body.node.key) { 82 | _this.redbird.unregister(removeEtcDir(body.node.key)); 83 | } 84 | }); 85 | 86 | // Handle Errors 87 | _this.watcher.on('error', function (err: NodeJS.ErrnoException) { 88 | log.error(err, 'etcd backend error'); 89 | }); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redbird", 3 | "version": "1.0.2", 4 | "description": "A reverse proxy with support for dynamic tables", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "source": "./lib/index.ts", 10 | "scripts": { 11 | "test": "vitest", 12 | "build": "tsc", 13 | "format": "prettier --write *.js \"{samples,lib,test,hl-tests}/**/*.js\"", 14 | "prepublishOnly": "npm run build" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/OptimalBits/redbird.git" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [ 24 | "proxy", 25 | "reverse", 26 | "docker", 27 | "etcd" 28 | ], 29 | "author": "Manuel Astudillo", 30 | "license": "BSD-3-Clause-Attribution", 31 | "bugs": { 32 | "url": "https://github.com/OptimalBits/redbird/issues" 33 | }, 34 | "homepage": "https://github.com/OptimalBits/redbird", 35 | "dependencies": { 36 | "dolphin": "*", 37 | "greenlock": "^2.8.9", 38 | "http-proxy": "^1.18.0", 39 | "le-challenge-fs": "^2.0.9", 40 | "le-store-certbot": "^2.2.3", 41 | "lodash": "^4.17.21", 42 | "lru-cache": "^11.0.1", 43 | "mkdirp": "^3.0.1", 44 | "node-etcd": "^7.0.0", 45 | "object-hash": "^3.0.0", 46 | "pino": "^9.4.0", 47 | "safe-timers": "^1.1.0", 48 | "valid-url": "^1.0.9" 49 | }, 50 | "devDependencies": { 51 | "@semantic-release/changelog": "^6.0.3", 52 | "@semantic-release/commit-analyzer": "^13.0.0", 53 | "@semantic-release/git": "^10.0.1", 54 | "@semantic-release/github": "^10.3.4", 55 | "@semantic-release/npm": "^12.0.1", 56 | "@semantic-release/release-notes-generator": "^14.0.1", 57 | "@types/http-proxy": "^1.17.15", 58 | "@types/lodash": "^4.17.7", 59 | "@types/node": "^22.5.4", 60 | "@types/object-hash": "^3.0.6", 61 | "@types/safe-timers": "^1.1.2", 62 | "@types/valid-url": "^1.0.7", 63 | "gulp": "^5.0.0", 64 | "gulp-eslint": "^6.0.0", 65 | "node-fetch": "^3.3.2", 66 | "prettier": "^2.0.1", 67 | "tslib": "^2.7.0", 68 | "typescript": "^5.6.2", 69 | "vitest": "^2.0.5" 70 | }, 71 | "prettier": { 72 | "printWidth": 100, 73 | "singleQuote": true 74 | }, 75 | "release": { 76 | "plugins": [ 77 | [ 78 | "@semantic-release/commit-analyzer" 79 | ], 80 | "@semantic-release/release-notes-generator", 81 | [ 82 | "@semantic-release/changelog", 83 | { 84 | "changelogFile": "docs/CHANGELOG.MD" 85 | } 86 | ], 87 | [ 88 | "@semantic-release/npm", 89 | { 90 | "npmPublish": true 91 | } 92 | ], 93 | "@semantic-release/github", 94 | [ 95 | "@semantic-release/git", 96 | { 97 | "assets": [ 98 | "package.json", 99 | "yarn.lock", 100 | "docs/CHANGELOG.MD" 101 | ], 102 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 103 | } 104 | ] 105 | ] 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/caturra.exactbytes.com/fullchain.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE8TCCA9mgAwIBAgITAPoaP9spbg6rEZeE9ftc0nGrejANBgkqhkiG9w0BAQsF 3 | ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjA5MTgx 4 | MzI4MDBaFw0xNjEyMTcxMzI4MDBaMCExHzAdBgNVBAMTFmNhdHVycmEuZXhhY3Ri 5 | eXRlcy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6IzRaeBrY 6 | QCA6iaoKAG2aODktRuw9ZEKZluXVNQosnlhUzkcClyj/O6mM4RUqXZw4SzdDPYLL 7 | f8qRxBvvGtOX/WfsMxkJk6ZwrFHp8GwYchgKehVhuuvPmzXx4sTZCyFOs2sK5PNg 8 | g17lI8Qdzw1cyI2zT+WVK9BMWRWw4UM+TEHLeb8LWn0Ljw5sk5krgffx8HCmC8Ho 9 | JruFOXUiRAl2+sWq02JM83FfB6lRXtp1yt+U/XdCpsbcDQyUjLfzcN0Wu/zXLPFN 10 | AaVDKTzdhSU/q9ze+RkT4DSJHXD272ekvIdgbDoOpXbChZqvqoIqQOI6EkFGTHE8 11 | sAsnJBm9sRcLAgMBAAGjggIfMIICGzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYw 12 | FAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFBdA 13 | wGk8f15CB4YLr6G1JhTTrxUJMB8GA1UdIwQYMBaAFMDMA0a5WCDMXHJw8+EuyyCm 14 | 9Wg6MHgGCCsGAQUFBwEBBGwwajAzBggrBgEFBQcwAYYnaHR0cDovL29jc3Auc3Rn 15 | LWludC14MS5sZXRzZW5jcnlwdC5vcmcvMDMGCCsGAQUFBzAChidodHRwOi8vY2Vy 16 | dC5zdGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wIQYDVR0RBBowGIIWY2F0dXJy 17 | YS5leGFjdGJ5dGVzLmNvbTCB/gYDVR0gBIH2MIHzMAgGBmeBDAECATCB5gYLKwYB 18 | BAGC3xMBAQEwgdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQu 19 | b3JnMIGrBggrBgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkg 20 | YmUgcmVsaWVkIHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFj 21 | Y29yZGFuY2Ugd2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0 22 | dHBzOi8vbGV0c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUA 23 | A4IBAQCugwNrp0wmMHoU33jzeOlCWaHLnYplYxGp2zL0uU0/DxWLyVn5u6dj7hcq 24 | UggYh+8PL0iVLPSfLId4t1aI7gor1qmF6L9yzAQq0vderwXMXvLurOsVfEaGJjrA 25 | mpmTews20NdDFJaYly/4GrDfb07D8NTBlzu9sUkRUznBdJ/8u/SjBkFAK5ICxkJ7 26 | /6KiKwk7g0k9vyMMfAsLCNEPueY17mCnlzj7A2N90np4QXjgcFdFqibAQ6J/+La0 27 | flU1PWfms+UwRjt4vBebAmInd50lofObm6OPnB9X9IH0vVvnZBUIhk8BE+wxnlw9 28 | piOf4l9a4N8YiiR+VTUGSOOO7jsN 29 | -----END CERTIFICATE----- 30 | -----BEGIN CERTIFICATE----- 31 | MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw 32 | GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 33 | MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw 34 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 35 | 8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym 36 | oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 37 | ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN 38 | xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 39 | dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 40 | AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw 41 | HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 42 | BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu 43 | b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu 44 | Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq 45 | hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF 46 | UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 47 | AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp 48 | DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 49 | IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf 50 | zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI 51 | PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w 52 | SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em 53 | 2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 54 | WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt 55 | n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= 56 | -----END CERTIFICATE----- 57 | -------------------------------------------------------------------------------- /hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/fullchain0.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE8TCCA9mgAwIBAgITAPoaP9spbg6rEZeE9ftc0nGrejANBgkqhkiG9w0BAQsF 3 | ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjA5MTgx 4 | MzI4MDBaFw0xNjEyMTcxMzI4MDBaMCExHzAdBgNVBAMTFmNhdHVycmEuZXhhY3Ri 5 | eXRlcy5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6IzRaeBrY 6 | QCA6iaoKAG2aODktRuw9ZEKZluXVNQosnlhUzkcClyj/O6mM4RUqXZw4SzdDPYLL 7 | f8qRxBvvGtOX/WfsMxkJk6ZwrFHp8GwYchgKehVhuuvPmzXx4sTZCyFOs2sK5PNg 8 | g17lI8Qdzw1cyI2zT+WVK9BMWRWw4UM+TEHLeb8LWn0Ljw5sk5krgffx8HCmC8Ho 9 | JruFOXUiRAl2+sWq02JM83FfB6lRXtp1yt+U/XdCpsbcDQyUjLfzcN0Wu/zXLPFN 10 | AaVDKTzdhSU/q9ze+RkT4DSJHXD272ekvIdgbDoOpXbChZqvqoIqQOI6EkFGTHE8 11 | sAsnJBm9sRcLAgMBAAGjggIfMIICGzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYw 12 | FAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFBdA 13 | wGk8f15CB4YLr6G1JhTTrxUJMB8GA1UdIwQYMBaAFMDMA0a5WCDMXHJw8+EuyyCm 14 | 9Wg6MHgGCCsGAQUFBwEBBGwwajAzBggrBgEFBQcwAYYnaHR0cDovL29jc3Auc3Rn 15 | LWludC14MS5sZXRzZW5jcnlwdC5vcmcvMDMGCCsGAQUFBzAChidodHRwOi8vY2Vy 16 | dC5zdGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wIQYDVR0RBBowGIIWY2F0dXJy 17 | YS5leGFjdGJ5dGVzLmNvbTCB/gYDVR0gBIH2MIHzMAgGBmeBDAECATCB5gYLKwYB 18 | BAGC3xMBAQEwgdYwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQu 19 | b3JnMIGrBggrBgEFBQcCAjCBngyBm1RoaXMgQ2VydGlmaWNhdGUgbWF5IG9ubHkg 20 | YmUgcmVsaWVkIHVwb24gYnkgUmVseWluZyBQYXJ0aWVzIGFuZCBvbmx5IGluIGFj 21 | Y29yZGFuY2Ugd2l0aCB0aGUgQ2VydGlmaWNhdGUgUG9saWN5IGZvdW5kIGF0IGh0 22 | dHBzOi8vbGV0c2VuY3J5cHQub3JnL3JlcG9zaXRvcnkvMA0GCSqGSIb3DQEBCwUA 23 | A4IBAQCugwNrp0wmMHoU33jzeOlCWaHLnYplYxGp2zL0uU0/DxWLyVn5u6dj7hcq 24 | UggYh+8PL0iVLPSfLId4t1aI7gor1qmF6L9yzAQq0vderwXMXvLurOsVfEaGJjrA 25 | mpmTews20NdDFJaYly/4GrDfb07D8NTBlzu9sUkRUznBdJ/8u/SjBkFAK5ICxkJ7 26 | /6KiKwk7g0k9vyMMfAsLCNEPueY17mCnlzj7A2N90np4QXjgcFdFqibAQ6J/+La0 27 | flU1PWfms+UwRjt4vBebAmInd50lofObm6OPnB9X9IH0vVvnZBUIhk8BE+wxnlw9 28 | piOf4l9a4N8YiiR+VTUGSOOO7jsN 29 | -----END CERTIFICATE----- 30 | -----BEGIN CERTIFICATE----- 31 | MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw 32 | GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 33 | MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw 34 | ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 35 | 8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym 36 | oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 37 | ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN 38 | xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 39 | dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 40 | AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw 41 | HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 42 | BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu 43 | b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu 44 | Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq 45 | hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF 46 | UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 47 | AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp 48 | DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 49 | IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf 50 | zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI 51 | PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w 52 | SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em 53 | 2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 54 | WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt 55 | n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= 56 | -----END CERTIFICATE----- 57 | -------------------------------------------------------------------------------- /test/hostheader.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { describe, it, expect } from 'vitest'; 4 | import { Redbird } from '../lib/index.js'; // Adjust the import path if necessary 5 | import fetch from 'node-fetch'; 6 | import { testServer } from './tes_utils.js'; 7 | 8 | const TEST_PORT = 54674; 9 | const PROXY_PORT = 53433; 10 | 11 | const opts = { 12 | port: PROXY_PORT, 13 | }; 14 | 15 | describe('Target with a hostname', function () { 16 | it('Should have the host header passed to the target', async function () { 17 | const redbird = new Redbird(opts); 18 | 19 | expect(redbird.routing).to.be.an('object'); 20 | 21 | const target = `127.0.0.1.nip.io:${TEST_PORT}`; 22 | 23 | redbird.register('127.0.0.1', target, { 24 | useTargetHostHeader: true, 25 | }); 26 | 27 | expect(redbird.routing).to.have.property('127.0.0.1'); 28 | 29 | const promiseServer = testServer(TEST_PORT).then(function (req) { 30 | expect(req.headers['host']).to.be.eql(target); 31 | }); 32 | 33 | const res = await fetch(`http://127.0.0.1:${PROXY_PORT}`); 34 | expect(res.status).to.be.eql(200); 35 | await redbird.close(); 36 | 37 | return promiseServer; 38 | }); 39 | 40 | it('Should not have the host header passed to the target', async function () { 41 | const redbird = new Redbird(opts); 42 | 43 | expect(redbird.routing).to.be.an('object'); 44 | 45 | const target = `127.0.0.1.nip.io:${TEST_PORT}`; 46 | 47 | redbird.register('127.0.0.1', target); 48 | 49 | expect(redbird.routing).to.have.property('127.0.0.1'); 50 | 51 | const source = `127.0.0.1:${PROXY_PORT}`; 52 | 53 | const promiseServer = testServer(TEST_PORT).then(function (req) { 54 | expect(req.headers['host']).to.be.eql(source); 55 | }); 56 | 57 | const res = await fetch(`http://${source}`); 58 | expect(res.status).to.be.eql(200); 59 | await redbird.close(); 60 | 61 | return promiseServer; 62 | }); 63 | 64 | it('Should return 404 after route is unregister', async function () { 65 | const redbird = new Redbird(opts); 66 | 67 | expect(redbird.routing).to.be.an('object'); 68 | 69 | const target = `127.0.0.1.nip.io:${TEST_PORT}`; 70 | 71 | redbird.register('127.0.0.1', target); 72 | redbird.unregister('127.0.0.1', target); 73 | 74 | expect(redbird.routing).to.have.property('127.0.0.1'); 75 | 76 | const source = `127.0.0.1:${PROXY_PORT}`; 77 | 78 | const res = await fetch(`http://${source}`); 79 | expect(res.status).to.be.eql(404); 80 | await redbird.close(); 81 | }); 82 | 83 | it('Should return 502 after route with no backend', async function () { 84 | const redbird = new Redbird(opts); 85 | 86 | expect(redbird.routing).to.be.an('object'); 87 | 88 | redbird.register('127.0.0.1', '127.0.0.1.nip.io:502'); 89 | 90 | expect(redbird.routing).to.have.property('127.0.0.1'); 91 | 92 | const source = `127.0.0.1:${PROXY_PORT}`; 93 | 94 | try { 95 | const res = await fetch(`http://${source}`); 96 | 97 | expect(res.status).to.be.eql(502); 98 | } catch (e) { 99 | expect(e.code).to.be.eql('ECONNRESET'); 100 | } finally { 101 | await redbird.close(); 102 | } 103 | }); 104 | }); 105 | 106 | describe('Request with forwarded host header', function () { 107 | it('should prefer forwarded hostname if desired', function () { 108 | const redbird = new Redbird({ 109 | preferForwardedHost: true, 110 | }); 111 | 112 | expect(redbird.routing).to.be.an('object'); 113 | const req = { headers: { host: '127.0.0.1', 'x-forwarded-host': 'subdomain.example.com' } }; 114 | 115 | const source = redbird.getSource(req); 116 | expect(source).to.be.eql('subdomain.example.com'); 117 | 118 | redbird.close(); 119 | }); 120 | 121 | it('should use original host if not further specified', function () { 122 | const redbird = new Redbird(opts); 123 | 124 | expect(redbird.routing).to.be.an('object'); 125 | const req = { headers: { host: '127.0.0.1', 'x-forwarded-host': 'subdomain.example.com' } }; 126 | 127 | const source = redbird.getSource(req); 128 | expect(source).to.be.eql('127.0.0.1'); 129 | 130 | redbird.close(); 131 | }); 132 | }); 133 | 134 | -------------------------------------------------------------------------------- /lib/third-party/le-challenge-fs.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | Original code License: 5 | 6 | acme-http-01-webroot.js | MPL-2.0 | Terms of Use | Privacy Policy 7 | Copyright 2019 AJ ONeal Copyright 2019 The Root Group LLC 8 | 9 | https://git.rootprojects.org/root/acme-http-01-webroot.js.git 10 | */ 11 | 12 | import fs from 'fs'; 13 | import { IncomingMessage } from 'http'; 14 | import path from 'path'; 15 | import os from 'os'; 16 | import http from 'http'; 17 | import { mkdirp } from 'mkdirp'; 18 | 19 | const myDefaults = { 20 | //webrootPath: [ '~', 'letsencrypt', 'const', 'lib' ].join(path.sep) 21 | webrootPath: path.join(os.tmpdir(), 'acme-challenge'), 22 | loopbackTimeout: 5 * 1000, 23 | debug: false, 24 | }; 25 | 26 | const Challenge = { 27 | create: function (options: any) { 28 | const results: any = {}; 29 | 30 | Object.keys(Challenge).forEach(function (key: string) { 31 | results[key] = (Challenge)[key]; 32 | }); 33 | results.create = undefined; 34 | 35 | Object.keys(myDefaults).forEach(function (key) { 36 | if ('undefined' === typeof options[key]) { 37 | options[key] = (myDefaults)[key]; 38 | } 39 | }); 40 | results._options = options; 41 | 42 | results.getOptions = function () { 43 | return results._options; 44 | }; 45 | 46 | return results; 47 | }, 48 | 49 | // 50 | // NOTE: the "args" here in `set()` are NOT accessible to `get()` and `remove()` 51 | // They are provided so that you can store them in an implementation-specific way 52 | // if you need access to them. 53 | set: function ( 54 | args: any, 55 | domain: string, 56 | challengePath: string, 57 | keyAuthorization: any, 58 | done: (err?: NodeJS.ErrnoException) => void 59 | ) { 60 | keyAuthorization = String(keyAuthorization); 61 | 62 | mkdirp(args.webrootPath) 63 | .then(function (): void { 64 | fs.writeFile( 65 | path.join(args.webrootPath, challengePath), 66 | keyAuthorization, 67 | 'utf8', 68 | function (err: NodeJS.ErrnoException) { 69 | done(err); 70 | } 71 | ); 72 | }) 73 | .catch((err) => { 74 | if (err) { 75 | done(err); 76 | return; 77 | } 78 | }); 79 | }, 80 | 81 | // 82 | // NOTE: the "defaults" here are still merged and templated, just like "args" would be, 83 | // but if you specifically need "args" you must retrieve them from some storage mechanism 84 | // based on domain and key 85 | // 86 | get: function (defaults: any, domain: string, key: string, done: () => void) { 87 | fs.readFile(path.join(defaults.webrootPath, key), 'utf8', done); 88 | }, 89 | 90 | remove: function (defaults: any, domain: string, key: string, done: () => void) { 91 | fs.unlink(path.join(defaults.webrootPath, key), done); 92 | }, 93 | 94 | loopback: function ( 95 | defaults: any, 96 | domain: string, 97 | key: string, 98 | done: (err?: NodeJS.ErrnoException, value?: any) => void 99 | ) { 100 | const hostname = domain + (defaults.loopbackPort ? ':' + defaults.loopbackPort : ''); 101 | const urlstr = 'http://' + hostname + '/.well-known/acme-challenge/' + key; 102 | 103 | http 104 | .get(urlstr, function (res: IncomingMessage) { 105 | if (200 !== res.statusCode) { 106 | done(new Error('local loopback failed with statusCode ' + res.statusCode)); 107 | return; 108 | } 109 | const chunks: any[] = []; 110 | res.on('data', function (chunk) { 111 | chunks.push(chunk); 112 | }); 113 | res.on('end', function () { 114 | const str = Buffer.concat(chunks).toString('utf8').trim(); 115 | done(null, str); 116 | }); 117 | }) 118 | .setTimeout(defaults.loopbackTimeout, function () { 119 | done(new Error('loopback timeout, could not reach server')); 120 | }) 121 | .on('error', function (err: NodeJS.ErrnoException) { 122 | done(err); 123 | }); 124 | }, 125 | 126 | /* 127 | test: function ( 128 | args: any, 129 | domain: string, 130 | challenge: any, 131 | keyAuthorization: any, 132 | done: (err?: NodeJS.ErrnoException) => void 133 | ) { 134 | const me = this; 135 | const key = keyAuthorization || challenge; 136 | 137 | me.set(args, domain, challenge, key, function (err) { 138 | if (err) { 139 | done(err); 140 | return; 141 | } 142 | 143 | myDefaults.loopbackPort = args.loopbackPort; 144 | myDefaults.webrootPath = args.webrootPath; 145 | me.loopback(args, domain, challenge, function (err, _key) { 146 | if (err) { 147 | done(err); 148 | return; 149 | } 150 | 151 | if (key !== _key) { 152 | err = new Error( 153 | "keyAuthorization [original] '" + key + "'" + " did not match [result] '" + _key + "'" 154 | ); 155 | return; 156 | } 157 | 158 | me.remove(myDefaults, domain, challenge, function (_err) { 159 | if (_err) { 160 | done(_err); 161 | return; 162 | } 163 | 164 | done(err); 165 | }); 166 | }); 167 | }); 168 | }, 169 | */ 170 | }; 171 | 172 | export default Challenge; 173 | -------------------------------------------------------------------------------- /lib/letsencrypt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Letsecript module for Redbird (c) Optimalbits 2016-2024 3 | * 4 | * 5 | * 6 | */ 7 | 8 | import { IncomingMessage, ServerResponse, createServer } from 'http'; 9 | import path from 'path'; 10 | import url from 'url'; 11 | import fs from 'fs'; 12 | import pino from 'pino'; 13 | 14 | import LeChallengeFs from './third-party/le-challenge-fs.js'; 15 | 16 | /** 17 | * LetsEncrypt certificates are stored like the following: 18 | * 19 | * /example.com 20 | * / 21 | * 22 | * 23 | * 24 | */ 25 | let leStoreConfig = {}; 26 | const webrootPath = ':configDir/:hostname/.well-known/acme-challenge'; 27 | 28 | function init(certPath: string, port: number, logger: pino.Logger) { 29 | logger?.info('Initializing letsencrypt, path %s, port: %s', certPath, port); 30 | 31 | leStoreConfig = { 32 | configDir: certPath, 33 | privkeyPath: ':configDir/:hostname/privkey.pem', 34 | fullchainPath: ':configDir/:hostname/fullchain.pem', 35 | certPath: ':configDir/:hostname/cert.pem', 36 | chainPath: ':configDir/:hostname/chain.pem', 37 | 38 | workDir: ':configDir/letsencrypt/var/lib', 39 | logsDir: ':configDir/letsencrypt/var/log', 40 | 41 | webrootPath, 42 | debug: false, 43 | }; 44 | 45 | // we need to proxy for example: 'example.com/.well-known/acme-challenge' -> 'localhost:port/example.com/' 46 | return createServer(function (req: IncomingMessage, res: ServerResponse) { 47 | if (req.method !== 'GET') { 48 | res.statusCode = 405; // Method Not Allowed 49 | res.end(); 50 | return; 51 | } 52 | 53 | const reqPath = url.parse(req.url).pathname; 54 | const basePath = path.resolve(certPath); 55 | const safePath = path.normalize(reqPath).replace(/^(\.\.[\/\\])+/, ''); // Prevent directory traversal 56 | const fullPath = path.join(basePath, safePath); 57 | 58 | if (!fullPath.startsWith(basePath)) { 59 | logger?.info(`Attempted directory traversal attack: ${req.url}`); 60 | res.statusCode = 403; // Forbidden 61 | res.end('Access denied'); 62 | return; 63 | } 64 | 65 | logger?.info('LetsEncrypt CA trying to validate challenge %s', fullPath); 66 | 67 | fs.stat(fullPath, function (err: Error, stats: any) { 68 | if (err || !stats.isFile()) { 69 | res.writeHead(404, { 'Content-Type': 'text/plain' }); 70 | res.write('404 Not Found\n'); 71 | res.end(); 72 | return; 73 | } 74 | 75 | res.writeHead(200); 76 | fs.createReadStream(fullPath, 'binary').pipe(res); 77 | }); 78 | }).listen(port); 79 | } 80 | 81 | /** 82 | * Gets the certificates for the given domain. 83 | * Handles all the LetsEncrypt protocol. Uses 84 | * existing certificates if any, or negotiates a new one. 85 | * Returns a promise that resolves to an object with the certificates. 86 | * TODO: We should use something like https://github.com/PaquitoSoft/memored/blob/master/index.js 87 | * to avoid 88 | */ 89 | async function getCertificates( 90 | domain: string, 91 | email: string, 92 | loopbackPort: number, 93 | production: boolean, 94 | renew: boolean, 95 | logger: pino.Logger 96 | ) { 97 | const LE = (await import('greenlock')).default; 98 | 99 | // Storage Backend 100 | const leStore = (await import('le-store-certbot')).create(leStoreConfig); 101 | 102 | // ACME Challenge Handlers 103 | const leChallenge = LeChallengeFs.create({ 104 | loopbackPort: loopbackPort, 105 | webrootPath, 106 | debug: false, 107 | }); 108 | 109 | const le = LE.create({ 110 | server: production 111 | ? 'https://acme-v02.api.letsencrypt.org/directory' 112 | : 'https://acme-staging-v02.api.letsencrypt.org/directory', 113 | store: leStore, // handles saving of config, accounts, and certificates 114 | challenges: { 'http-01': leChallenge }, // handles /.well-known/acme-challege keys and tokens 115 | challengeType: 'http-01', // default to this challenge type 116 | debug: false, 117 | log: function () { 118 | logger?.info(arguments, 'Lets encrypt debugger'); 119 | }, 120 | }); 121 | 122 | // Check in-memory cache of certificates for the named domain 123 | const cert = await le.check({ domains: [domain] }); 124 | 125 | const opts: { 126 | domains: string[]; 127 | email: string; 128 | agreeTos: boolean; 129 | rsaKeySize: number; 130 | challengeType: string; 131 | duplicate?: boolean; 132 | } = { 133 | domains: [domain], 134 | email: email, 135 | agreeTos: true, 136 | rsaKeySize: 2048, // 2048 or higher 137 | challengeType: 'http-01', 138 | }; 139 | 140 | if (cert) { 141 | if (renew) { 142 | logger && logger.info('renewing cert for ' + domain); 143 | opts.duplicate = true; 144 | return le.renew(opts, cert).catch(function (err: Error) { 145 | logger && logger.error(err, 'Error renewing certificates for ', domain); 146 | }); 147 | } else { 148 | logger && logger.info('Using cached cert for ' + domain); 149 | return cert; 150 | } 151 | } else { 152 | // Register Certificate manually 153 | logger?.info('Manually registering certificate for %s', domain); 154 | return le.register(opts).catch(function (err: Error) { 155 | logger?.error(err, 'Error registering LetsEncrypt certificates'); 156 | }); 157 | } 158 | } 159 | 160 | export { init, getCertificates }; 161 | -------------------------------------------------------------------------------- /lib/docker.ts: -------------------------------------------------------------------------------- 1 | /*eslint-env node */ 2 | 'use strict'; 3 | 4 | import pino from 'pino'; 5 | import { Redbird } from './proxy.js'; 6 | 7 | /** 8 | Redbird Docker Module. 9 | 10 | This module handles automatic regitration and de-registration of 11 | services running on docker containers. 12 | */ 13 | 14 | export class DockerModule { 15 | private targets: Record>; 16 | private images: Record>; 17 | private ports: any; 18 | private dolphin: any; 19 | private events: any; 20 | private log: pino.Logger; 21 | 22 | constructor(private redbird: Redbird, url: string) { 23 | const Dolphin = require('dolphin'); 24 | 25 | this.redbird = redbird; 26 | this.log = redbird.logger; 27 | 28 | const targets: Record> = (this.targets = {}); 29 | this.ports = {}; 30 | 31 | // We keep an up-to-date table with all the images having 32 | // containers running on the system. 33 | const images: Record> = (this.images = {}); 34 | const dolphin = (this.dolphin = new Dolphin(url)); 35 | 36 | const _this = this; 37 | 38 | // Start docker event listener 39 | this.events = dolphin.events(); 40 | 41 | this.events.on('connected', () => { 42 | // Fetch all running containers and register them if 43 | // necessary. 44 | dolphin.containers({ filters: { status: ['running'] } }).then( 45 | ( 46 | containers: { 47 | Image: string; 48 | Id: string; 49 | Names: string[]; 50 | }[] 51 | ) => { 52 | for (var i = 0; i < containers.length; i++) { 53 | const container = containers[i]; 54 | this.registerIfNeeded( 55 | container.Image, 56 | container.Id, 57 | container.Names[0].replace('/', '') 58 | ); 59 | } 60 | } 61 | ); 62 | }); 63 | 64 | this.events.on( 65 | 'event', 66 | (evt: { 67 | status: string; 68 | from: string; 69 | id: string; 70 | Actor: { Attributes: { name: string } }; 71 | }) => { 72 | let image: Record; 73 | let target: Record; 74 | 75 | this.log?.info('Container %s changed to status %s', evt.Actor.Attributes.name, evt.status); 76 | 77 | switch (evt.status) { 78 | case 'start': 79 | case 'restart': 80 | case 'unpause': 81 | this.registerIfNeeded(evt.from, evt.id, evt.Actor.Attributes.name); 82 | break; 83 | case 'stop': 84 | case 'die': 85 | case 'pause': 86 | image = images[evt.from]; 87 | if (image) { 88 | for (var targetName in targets) { 89 | var match = isMatchingImageName(targetName, evt.from); 90 | if (image[evt.id] === 'running' && match && _this.ports[evt.id]) { 91 | target = targets[targetName]; 92 | this.log?.info( 93 | 'Un-registering container %s for target %s', 94 | evt.Actor.Attributes.name, 95 | target.src 96 | ); 97 | _this.redbird.unregister(target.src, _this.ports[evt.id]); 98 | } 99 | image[evt.id] = 'stopped'; 100 | } 101 | } 102 | break; 103 | default: 104 | // Nothing 105 | } 106 | } 107 | ); 108 | 109 | this.events.on('error', (err: Error) => { 110 | this.log.error(err, 'dolphin docker event error'); 111 | }); 112 | } 113 | 114 | registerIfNeeded(imageName: string, containerId: string, containerName: string) { 115 | const image = (this.images[imageName] = this.images[imageName] || {}); 116 | 117 | for (var targetName in this.targets) { 118 | const match = isMatchingImageName(targetName, imageName); 119 | 120 | if (match && image[containerId] !== 'running') { 121 | const target = this.targets[targetName]; 122 | this.log?.info('Registering container %s for target %s', containerName, target.src); 123 | this.registerContainer(target.src, containerId, target.opts); 124 | } 125 | } 126 | 127 | image[containerId] = 'running'; 128 | } 129 | 130 | /** 131 | * Register route from a source to a given target. 132 | * The target should be an image name. Starting several containers 133 | * from the same image will automatically deliver the requests 134 | * to each container in a round-robin fashion. 135 | * 136 | * @param src See {@link ReverseProxy.register} 137 | * @param target Docker image (this string is evaluated as regexExp) 138 | * @param opts Options like ssl and etc... 139 | */ 140 | register(src: string, target: string, opts: any) { 141 | var storedTarget = this.targets[target]; 142 | 143 | if (storedTarget && storedTarget.src == src) { 144 | throw Error('Cannot register the same src and target twice'); 145 | } 146 | 147 | this.targets[target] = { 148 | src, 149 | opts, 150 | }; 151 | 152 | for (var imageName in this.images) { 153 | const image = this.images[imageName]; 154 | for (var containerId in image) { 155 | this.registerIfNeeded(imageName, containerId, containerId); 156 | } 157 | } 158 | } 159 | 160 | async registerContainer(src: string | URL, containerId: string, opts: any) { 161 | const targetPort = await containerPort(this.dolphin, containerId); 162 | this.redbird.register(src, targetPort, opts); 163 | this.ports[containerId] = targetPort; 164 | } 165 | } 166 | 167 | function isMatchingImageName(targetName: string, imageName: string) { 168 | var regex = new RegExp('^' + targetName + '$'); 169 | return regex.test(imageName); 170 | } 171 | 172 | function containerPort(dolphin: any, containerId: string) { 173 | return dolphin.containers 174 | .inspect(containerId) 175 | .then((container: { NetworkSettings: { Ports: any; Networks: any }; IPAddress: string }) => { 176 | const port = Object.keys(container.NetworkSettings.Ports)[0].split('/')[0]; 177 | 178 | const netNames = Object.keys(container.NetworkSettings.Networks); 179 | if (netNames.length === 1) { 180 | const ip = container.NetworkSettings.Networks[netNames[0]].IPAddress; 181 | if (port && ip) { 182 | return 'http://' + ip + ':' + port; 183 | } 184 | } else { 185 | //TODO: Implements opts for manually choosing the network/ip/port... 186 | } 187 | throw Error('No valid address or port ' + container.IPAddress + ':' + port); 188 | }); 189 | } 190 | -------------------------------------------------------------------------------- /test/custom_resolver_certificates.spec.ts: -------------------------------------------------------------------------------- 1 | // tests/redbird.spec.ts 2 | 3 | import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; 4 | import http, { Server } from 'http'; 5 | import https from 'https'; 6 | import fs from 'fs'; 7 | import path from 'path'; 8 | 9 | import { certificate, key } from './fixtures/index.js'; 10 | 11 | const ONE_DAY = 24 * 60 * 60 * 1000; 12 | 13 | const MOCK_CERT_DATA = 'MOCK_CHAIN_DATA'; 14 | 15 | // Note: as we are mocking safe-timers we cannot use intervals larger than 2^31 - 1 ms. 16 | 17 | vi.mock('../lib/letsencrypt.js', () => ({ 18 | getCertificates: vi.fn().mockImplementation(async () => { 19 | return { 20 | privkey: key, 21 | cert: certificate, 22 | chain: MOCK_CERT_DATA, 23 | expiresAt: Date.now() + 22 * ONE_DAY, 24 | }; 25 | }), 26 | init: vi.fn(), 27 | })); 28 | 29 | const getCertificatesMock = vi.mocked((await import('../lib/letsencrypt.js')).getCertificates); 30 | 31 | // Mock 'safe-timers' module 32 | vi.mock('safe-timers', () => { 33 | return { 34 | default: { 35 | setTimeout: (callback, delay, ...args) => { 36 | return setTimeout(callback, delay, ...args); 37 | }, 38 | clearTimeout: (timerId) => { 39 | clearTimeout(timerId); 40 | }, 41 | // Include other methods if needed 42 | }, 43 | }; 44 | }); 45 | 46 | import { Redbird } from '../lib/index.js'; 47 | 48 | const testPort = 54679; 49 | const proxyPort = 8083; 50 | const sslPort = 8443; 51 | 52 | // Helper functions to make HTTP and HTTPS requests 53 | function makeHttpRequest(options) { 54 | return new Promise<{ status: number; headers: any; data: string }>((resolve, reject) => { 55 | const req = http.request(options, (res) => { 56 | let data = ''; 57 | res.on('data', (chunk) => { 58 | data += chunk; 59 | }); 60 | res.on('end', () => { 61 | resolve({ status: res.statusCode!, headers: res.headers, data }); 62 | }); 63 | }); 64 | req.on('error', (err) => { 65 | console.error('ERROR', err); 66 | reject(err); 67 | }); 68 | req.end(); 69 | }); 70 | } 71 | 72 | function makeHttpsRequest(options) { 73 | return new Promise<{ status: number; headers: any; data: string }>((resolve, reject) => { 74 | options.rejectUnauthorized = false; // For self-signed certificates 75 | const req = https.request(options, (res) => { 76 | let data = ''; 77 | res.on('data', (chunk) => { 78 | data += chunk; 79 | }); 80 | res.on('end', () => { 81 | resolve({ status: res.statusCode!, headers: res.headers, data }); 82 | }); 83 | }); 84 | req.on('error', (err) => { 85 | reject(err); 86 | }); 87 | req.end(); 88 | }); 89 | } 90 | 91 | const responseMessage = 'Hello from target server'; 92 | 93 | describe('Redbird Lets Encrypt SSL Certificate Generation For Custom Resolvers', () => { 94 | let proxy: Redbird; 95 | let targetServer: Server; 96 | let targetPort: number = testPort; 97 | 98 | beforeAll(async () => { 99 | // Start a simple HTTP server to act as the backend target 100 | targetServer = http.createServer((req, res) => { 101 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 102 | res.end(responseMessage); 103 | }); 104 | 105 | await new Promise((resolve) => { 106 | targetServer.listen(testPort, () => { 107 | resolve(null); 108 | }); 109 | }); 110 | 111 | const wildcard_target = { 112 | url: `http://localhost:${targetPort}/`, 113 | opts: { 114 | ssl: { 115 | letsencrypt: { 116 | email: 'admin@optimalbits.com', 117 | production: false, 118 | }, 119 | }, 120 | }, 121 | }; 122 | 123 | // Create a new instance of Redbird with SSL options 124 | proxy = new Redbird({ 125 | port: proxyPort, 126 | ssl: { 127 | port: sslPort, 128 | }, 129 | letsencrypt: { 130 | path: path.join(__dirname, 'letsencrypt'), // Path to store Let's Encrypt certificates 131 | port: 9999, // Port for Let's Encrypt challenge responses 132 | renewWithin: 1 * ONE_DAY, // Renew certificates when they are within 1 day of expiration 133 | }, 134 | resolvers: [ 135 | { 136 | // We will accept any hostname and return the same target. 137 | fn: (hostname) => { 138 | return wildcard_target; 139 | }, 140 | priority: -1, 141 | }, 142 | ], 143 | }); 144 | 145 | // Mocking the certificate files 146 | vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => true); 147 | vi.spyOn(fs, 'readFileSync').mockImplementation((filePath, encoding) => 'MOCK_CERT_DATA'); 148 | }); 149 | 150 | afterAll(async () => { 151 | // Close the proxy and target server 152 | await proxy.close(); 153 | await new Promise((resolve) => targetServer.close(() => resolve(null))); 154 | vi.restoreAllMocks(); 155 | }); 156 | 157 | it('should generate SSL certificates for new subdomains', async () => { 158 | const subdomain = 'secure.example.com'; 159 | 160 | // Make an HTTPS request to the new subdomain 161 | // First HTTPS request to trigger certificate generation 162 | const options = { 163 | hostname: 'localhost', 164 | port: sslPort, 165 | path: '/', 166 | method: 'GET', 167 | headers: { 168 | Host: subdomain, 169 | }, 170 | rejectUnauthorized: false, // Accept self-signed certificates 171 | }; 172 | 173 | const response = await makeHttpsRequest(options); 174 | expect(response.status).toBe(200); 175 | expect(response.data).toBe(responseMessage); 176 | }); 177 | 178 | it('should renew SSL certificates that are halfway to expire', async () => { 179 | getCertificatesMock.mockClear(); 180 | 181 | const subdomain = 'renew.example.com'; 182 | 183 | // Mock getCertificates to return the initial and renewed certificates 184 | getCertificatesMock 185 | .mockResolvedValueOnce({ 186 | privkey: key, 187 | cert: certificate, 188 | chain: MOCK_CERT_DATA, 189 | expiresAt: Date.now() + 10 * ONE_DAY, // Expires in 10 days 190 | }) 191 | .mockResolvedValueOnce({ 192 | privkey: key, 193 | cert: certificate, 194 | chain: MOCK_CERT_DATA, 195 | expiresAt: Date.now() + 15 * ONE_DAY, // Renewed, expires in 90 days 196 | }); 197 | 198 | expect(getCertificatesMock).toHaveBeenCalledTimes(0); 199 | 200 | // Use fake timers to simulate time passage 201 | vi.useFakeTimers(); 202 | 203 | // Initial HTTPS request to trigger certificate generation 204 | const options = { 205 | hostname: 'localhost', 206 | port: sslPort, 207 | path: '/', 208 | method: 'GET', 209 | headers: { 210 | Host: subdomain, 211 | }, 212 | rejectUnauthorized: false, 213 | }; 214 | 215 | const response = await makeHttpsRequest(options); 216 | expect(response.status).toBe(200); 217 | expect(response.data).toBe('Hello from target server'); 218 | 219 | expect(getCertificatesMock).toHaveBeenCalledTimes(1); 220 | 221 | vi.advanceTimersByTime(8 * ONE_DAY); 222 | 223 | expect(getCertificatesMock).toHaveBeenCalledTimes(1); 224 | 225 | // Advance all timers to execute pending callbacks 226 | vi.advanceTimersByTime(1 * ONE_DAY); 227 | 228 | expect(getCertificatesMock).toHaveBeenCalledTimes(2); 229 | 230 | // Second HTTPS request 231 | const response2 = await makeHttpsRequest(options); 232 | expect(response2.status).toBe(200); 233 | expect(response2.data).toBe('Hello from target server'); 234 | 235 | // Verify getCertificates was called twice 236 | expect(getCertificatesMock).toHaveBeenCalledTimes(2); 237 | 238 | // Restore real timers 239 | vi.useRealTimers(); 240 | }); 241 | 242 | it('should redirect HTTP requests to HTTPS', async () => { 243 | const subdomain = 'secure2.example.com'; 244 | 245 | // Make an HTTPS request to the new subdomain 246 | // First HTTPS request to trigger certificate generation 247 | const options = { 248 | hostname: 'localhost', 249 | port: proxyPort, 250 | path: '/', 251 | method: 'GET', 252 | headers: { 253 | Host: subdomain, 254 | }, 255 | rejectUnauthorized: false, // Accept self-signed certificates 256 | }; 257 | 258 | const response = await makeHttpRequest(options); 259 | expect(response.status).toBe(302); 260 | expect(response.headers.location).toBe(`https://${subdomain}:${sslPort}/`); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /test/custom_resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { Redbird, buildRoute } from '../lib/index.js'; // Adjust the import path if necessary 3 | import { IncomingMessage } from 'http'; 4 | import { ProxyRoute } from '../lib/interfaces/proxy-route.js'; 5 | import { ResolverFn } from '../lib/interfaces/resolver.js'; 6 | 7 | const opts = { 8 | port: 10000 + Math.ceil(Math.random() * 55535), 9 | // Additional comment for clarity or additional options 10 | }; 11 | 12 | describe('Custom Resolver', () => { 13 | it('Should contain one resolver by default', async () => { 14 | const redbird = new Redbird(opts); 15 | expect(redbird.resolvers).toBeInstanceOf(Array); 16 | expect(redbird.resolvers).toHaveLength(1); 17 | expect(redbird.resolvers[0]).toEqual(redbird.defaultResolver); 18 | 19 | await redbird.close(); 20 | }); 21 | 22 | it('Should register resolver with right priority', async () => { 23 | const resolver = { 24 | fn: (host: string, url: string) => 'http://127.0.0.1:8080', 25 | priority: 1, 26 | }; 27 | 28 | let options = { ...opts, resolvers: [resolver] }; 29 | let redbird = new Redbird(options); 30 | 31 | expect(redbird.resolvers).toHaveLength(2); 32 | expect(redbird.resolvers[0]).toEqual(resolver); 33 | 34 | await redbird.close(); 35 | 36 | resolver.priority = -1; 37 | redbird = new Redbird({ ...options, resolvers: [resolver] }); 38 | expect(redbird.resolvers[1]).toEqual(resolver); 39 | 40 | await redbird.close(); 41 | }); 42 | 43 | it('Should add and remove resolver after launch', async () => { 44 | const resolverFn: ResolverFn = (host: string, url: string, req?: IncomingMessage) => ''; 45 | 46 | const redbird = new Redbird(opts); 47 | redbird.addResolver(resolverFn, 1); 48 | expect(redbird.resolvers).toHaveLength(2); 49 | expect(redbird.resolvers[0].fn).toEqual(resolverFn); 50 | 51 | redbird.addResolver(resolverFn); 52 | expect(redbird.resolvers).toHaveLength(2); // Only allows uniques. 53 | 54 | redbird.removeResolver(resolverFn); 55 | expect(redbird.resolvers).toHaveLength(1); 56 | expect(redbird.resolvers[0]).toEqual(redbird.defaultResolver); 57 | 58 | await redbird.close(); 59 | }); 60 | 61 | it('Should properly convert and cache route to routeObject', () => { 62 | const builder = buildRoute; 63 | 64 | // Invalid input 65 | expect(builder((() => {}))).toBeNull(); 66 | expect(builder([])).toBeNull(); 67 | expect(builder(2016)).toBeNull(); 68 | 69 | const testRoute = { urls: [], path: '/' }; 70 | const testRouteResult = builder(testRoute); 71 | expect(testRouteResult).toEqual(testRoute); 72 | 73 | if (testRouteResult) { 74 | expect(testRouteResult.urls).toBeDefined(); 75 | expect(testRouteResult.urls).toHaveLength(0); 76 | } else { 77 | throw new Error('testRouteResult is not defined'); 78 | } 79 | 80 | // Case string: 81 | const testString = 'http://127.0.0.1:8888'; 82 | const result = builder(testString) as ProxyRoute; 83 | expect(result.path).toEqual('/'); 84 | expect(result.urls).toBeInstanceOf(Array); 85 | expect(result.urls?.length).toEqual(1); 86 | 87 | if (result.urls) { 88 | expect(result.urls[0].hostname).toEqual('127.0.0.1'); 89 | } else { 90 | throw new Error('urls is not defined'); 91 | } 92 | 93 | expect(result.isResolved).toBeTruthy(); 94 | 95 | const result2 = builder(testString); 96 | expect(result2).toEqual(result); 97 | 98 | // Case with object 99 | const testObject_1 = { path: '/api', url: 'http://127.0.0.1' }; 100 | const testObjectResult_1 = builder(testObject_1) as ProxyRoute; 101 | 102 | expect(testObjectResult_1.path).toEqual('/api'); 103 | expect(testObjectResult_1.urls).toBeInstanceOf(Array); 104 | 105 | if (testObjectResult_1.urls) { 106 | expect(testObjectResult_1.urls.length).toEqual(1); 107 | expect(testObjectResult_1.urls[0].hostname).toEqual('127.0.0.1'); 108 | } else { 109 | throw new Error('urls is not defined'); 110 | } 111 | 112 | expect(testObjectResult_1.isResolved).toBeTruthy(); 113 | 114 | // Test object caching. 115 | const testObjectResult_2 = builder(testObject_1); 116 | expect(testObjectResult_1).toEqual(testObjectResult_2); 117 | 118 | const testObject_2 = { url: ['http://127.0.0.1', 'http://123.1.1.1'] }; 119 | const testResult2 = builder(testObject_2); 120 | expect(testResult2!.urls).toBeDefined(); 121 | expect(testResult2!.urls!.length).toEqual(testObject_2.url.length); 122 | expect(testResult2!.urls![0].hostname).toEqual('127.0.0.1'); 123 | expect(testResult2!.urls![1].hostname).toEqual('123.1.1.1'); 124 | }); 125 | 126 | it('Should resolve properly as expected', async () => { 127 | const proxy = new Redbird(opts); 128 | const resolverFn = function (host: string, url: string) { 129 | return url.match(/\/ignore/i) ? null : 'http://172.12.0.1/home'; 130 | }; 131 | 132 | proxy.register('mysite.example.com', 'http://127.0.0.1:9999'); 133 | proxy.addResolver(resolverFn, 1); 134 | 135 | // must match the resolver 136 | const result = await proxy.resolve('randomsite.example.com', '/anywhere'); 137 | 138 | expect(result).to.not.be.null; 139 | expect(result).to.not.be.undefined; 140 | expect(result!.urls!.length).to.be.above(0); 141 | expect(result!.urls![0].hostname).to.be.eq('172.12.0.1'); 142 | 143 | // expect route to match resolver even though it matches registered address 144 | const result2 = await proxy.resolve('mysite.example.com', '/somewhere'); 145 | expect(result2!.urls![0].hostname).to.be.eq('172.12.0.1'); 146 | 147 | // use default resolver, as custom resolver should ignore input. 148 | const result3 = await proxy.resolve('mysite.example.com', '/ignore'); 149 | expect(result3!.urls![0].hostname).to.be.eq('127.0.0.1'); 150 | 151 | // make custom resolver low priority and test. 152 | // result should match default resolver 153 | proxy.addResolver(resolverFn, -1); 154 | const result4 = await proxy.resolve('mysite.example.com', '/somewhere'); 155 | expect(result4!.urls![0].hostname).to.be.eq('127.0.0.1'); 156 | 157 | // both custom and default resolvers should ignore 158 | const result5 = await proxy.resolve('somesite.example.com', '/ignore'); 159 | expect(result5).to.be.undefined; 160 | proxy.removeResolver(resolverFn); 161 | 162 | // for path-based routing 163 | // when resolver path doesn't match that of url, skip 164 | 165 | const resolverPath = function (host: string, url: string) { 166 | return { 167 | path: '/notme', 168 | url: 'http://172.12.0.1/home', 169 | }; 170 | }; 171 | proxy.addResolver(resolverPath, 1); 172 | 173 | const result6 = await proxy.resolve('somesite.example.com', '/notme'); 174 | 175 | expect(result6).to.not.be.undefined; 176 | expect(result6!.urls![0].hostname).to.be.eq('172.12.0.1'); 177 | 178 | const result7 = await proxy.resolve('somesite.example.com', '/notme/somewhere'); 179 | expect(result7!.urls![0].hostname).to.be.eq('172.12.0.1'); 180 | 181 | const result8 = await proxy.resolve('somesite.example.com', '/itsme/somewhere'); 182 | expect(result8).to.be.undefined; 183 | await proxy.close(); 184 | }); 185 | 186 | it('Should resolve array properly as expected', function () { 187 | const proxy = new Redbird(opts); 188 | 189 | const firstResolver = function (host: string, url: string) { 190 | if (url.endsWith('/first-resolver')) { 191 | return 'http://first-resolver/'; 192 | } 193 | }; 194 | firstResolver.priority = 2; 195 | 196 | const secondResolver = function (host: string, url: string) { 197 | return new Promise(function (resolve, reject) { 198 | if (url.endsWith('/second-resolver')) { 199 | resolve('http://second-resolver/'); 200 | } else { 201 | resolve(null); 202 | } 203 | }); 204 | }; 205 | secondResolver.priority = 1; 206 | 207 | proxy.resolvers = []; // remove the defaultResolver 208 | proxy.addResolver(firstResolver); 209 | proxy.addResolver(secondResolver); 210 | 211 | const cases = [ 212 | proxy.resolve('mysite.example.com', '/first-resolver').then(function (result) { 213 | expect(result!.urls!.length).to.be.above(0); 214 | expect(result!.urls![0].hostname).to.be.eq('first-resolver'); 215 | }), 216 | proxy.resolve('mysite.example.com', '/second-resolver').then(function (result) { 217 | expect(result!.urls!.length).to.be.above(0); 218 | expect(result!.urls![0].hostname).to.be.eq('second-resolver'); 219 | }), 220 | ]; 221 | 222 | return Promise.all(cases).then( 223 | () => proxy.close(), 224 | (err) => { 225 | proxy.close(); 226 | throw err; 227 | } 228 | ); 229 | }); 230 | }); 231 | -------------------------------------------------------------------------------- /test/letsencrypt_certificates.spec.ts: -------------------------------------------------------------------------------- 1 | // tests/redbird.spec.ts 2 | 3 | import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; 4 | import http, { Server } from 'http'; 5 | import https from 'https'; 6 | import fs from 'fs'; 7 | import path from 'path'; 8 | 9 | import { certificate, key } from './fixtures'; 10 | 11 | const ONE_DAY = 24 * 60 * 60 * 1000; 12 | 13 | const MOCK_CERT_DATA = 'MOCK_CHAIN_DATA'; 14 | 15 | // Note: as we are mocking safe-timers we cannot use intervals larger than 2^31 - 1 ms. 16 | 17 | vi.mock('../lib/letsencrypt.js', () => ({ 18 | getCertificates: vi.fn().mockImplementation(async () => { 19 | return { 20 | privkey: key, 21 | cert: certificate, 22 | chain: MOCK_CERT_DATA, 23 | expiresAt: Date.now() + 22 * ONE_DAY, 24 | }; 25 | }), 26 | init: vi.fn(), 27 | })); 28 | 29 | const getCertificatesMock = vi.mocked((await import('../lib/letsencrypt.js')).getCertificates); 30 | 31 | // Mock 'safe-timers' module 32 | vi.mock('safe-timers', () => { 33 | return { 34 | default: { 35 | setTimeout: (callback, delay, ...args) => { 36 | return setTimeout(callback, delay, ...args); 37 | }, 38 | clearTimeout: (timerId) => { 39 | clearTimeout(timerId); 40 | }, 41 | // Include other methods if needed 42 | }, 43 | }; 44 | }); 45 | 46 | import { Redbird } from '../lib'; 47 | 48 | const testPort = 54680; 49 | const sslPort = 8444; 50 | const proxyPort = 8081; 51 | 52 | // Helper functions to make HTTP and HTTPS requests 53 | function makeHttpRequest(options) { 54 | return new Promise<{ status: number; headers: any; data: string }>((resolve, reject) => { 55 | const req = http.request(options, (res) => { 56 | let data = ''; 57 | res.on('data', (chunk) => { 58 | data += chunk; 59 | }); 60 | res.on('end', () => { 61 | resolve({ status: res.statusCode!, headers: res.headers, data }); 62 | }); 63 | }); 64 | req.on('error', (err) => { 65 | reject(err); 66 | }); 67 | req.end(); 68 | }); 69 | } 70 | 71 | function makeHttpsRequest(options) { 72 | return new Promise<{ status: number; headers: any; data: string }>((resolve, reject) => { 73 | options.rejectUnauthorized = false; // For self-signed certificates 74 | const req = https.request(options, (res) => { 75 | let data = ''; 76 | res.on('data', (chunk) => { 77 | data += chunk; 78 | }); 79 | res.on('end', () => { 80 | resolve({ status: res.statusCode!, headers: res.headers, data }); 81 | }); 82 | }); 83 | req.on('error', (err) => { 84 | reject(err); 85 | }); 86 | req.end(); 87 | }); 88 | } 89 | 90 | const responseMessage = 'Hello from target server'; 91 | 92 | describe('Redbird Lets Encrypt SSL Certificate Generation', () => { 93 | let proxy: Redbird; 94 | let targetServer: Server; 95 | let targetPort: number = testPort; 96 | 97 | beforeAll(async () => { 98 | // Start a simple HTTP server to act as the backend target 99 | targetServer = http.createServer((req, res) => { 100 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 101 | res.end(responseMessage); 102 | }); 103 | 104 | await new Promise((resolve) => { 105 | targetServer.listen(testPort, () => { 106 | resolve(null); 107 | }); 108 | }); 109 | 110 | // Create a new instance of Redbird with SSL options 111 | proxy = new Redbird({ 112 | port: proxyPort, 113 | ssl: { 114 | port: sslPort, 115 | }, 116 | letsencrypt: { 117 | path: path.join(__dirname, 'letsencrypt'), // Path to store Let's Encrypt certificates 118 | port: 9999, // Port for Let's Encrypt challenge responses 119 | renewWithin: 1 * ONE_DAY, // Renew certificates when they are within 1 day of expiration 120 | }, 121 | }); 122 | 123 | // Mocking the certificate files 124 | vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => true); 125 | vi.spyOn(fs, 'readFileSync').mockImplementation((filePath, encoding) => 'MOCK_CERT_DATA'); 126 | }); 127 | 128 | afterAll(async () => { 129 | // Close the proxy and target server 130 | await proxy.close(); 131 | await new Promise((resolve) => targetServer.close(() => resolve(null))); 132 | vi.restoreAllMocks(); 133 | }); 134 | 135 | it('should generate SSL certificates for new subdomains', async () => { 136 | const subdomain = 'secure.example.com'; 137 | 138 | // Register a route for example.com with SSL generation 139 | await proxy.register(subdomain, `http://localhost:${targetPort}`, { 140 | ssl: { 141 | letsencrypt: { 142 | email: 'admin@example.com', 143 | production: false, // Set to false for testing 144 | }, 145 | }, 146 | }); 147 | 148 | // Make an HTTPS request to the new subdomain 149 | // First HTTPS request to trigger certificate generation 150 | const options = { 151 | hostname: 'localhost', 152 | port: sslPort, 153 | path: '/', 154 | method: 'GET', 155 | headers: { 156 | Host: subdomain, 157 | }, 158 | rejectUnauthorized: false, // Accept self-signed certificates 159 | }; 160 | 161 | const response = await makeHttpsRequest(options); 162 | expect(response.status).toBe(200); 163 | expect(response.data).toBe(responseMessage); 164 | }); 165 | 166 | it('should renew SSL certificates that are halfway to expire', async () => { 167 | getCertificatesMock.mockClear(); 168 | 169 | const subdomain = 'renew.example.com'; 170 | 171 | // Mock getCertificates to return the initial and renewed certificates 172 | getCertificatesMock 173 | .mockResolvedValueOnce({ 174 | privkey: key, 175 | cert: certificate, 176 | chain: MOCK_CERT_DATA, 177 | expiresAt: Date.now() + 10 * ONE_DAY, // Expires in 10 days 178 | }) 179 | .mockResolvedValueOnce({ 180 | privkey: key, 181 | cert: certificate, 182 | chain: MOCK_CERT_DATA, 183 | expiresAt: Date.now() + 15 * ONE_DAY, // Renewed, expires in 90 days 184 | }); 185 | 186 | expect(getCertificatesMock).toHaveBeenCalledTimes(0); 187 | 188 | // Use fake timers to simulate time passage 189 | vi.useFakeTimers(); 190 | 191 | // Register the domain 192 | await proxy.register(subdomain, `http://localhost:${targetPort}`, { 193 | ssl: { 194 | letsencrypt: { 195 | email: 'admin@example.com', 196 | production: false, 197 | }, 198 | }, 199 | }); 200 | 201 | expect(getCertificatesMock).toHaveBeenCalledTimes(1); 202 | 203 | vi.advanceTimersByTime(8 * ONE_DAY); 204 | 205 | expect(getCertificatesMock).toHaveBeenCalledTimes(1); 206 | 207 | // Initial HTTPS request to trigger certificate generation 208 | const options = { 209 | hostname: 'localhost', 210 | port: sslPort, 211 | path: '/', 212 | method: 'GET', 213 | headers: { 214 | Host: subdomain, 215 | }, 216 | rejectUnauthorized: false, 217 | }; 218 | 219 | const response = await makeHttpsRequest(options); 220 | expect(response.status).toBe(200); 221 | expect(response.data).toBe('Hello from target server'); 222 | 223 | // Advance all timers to execute pending callbacks 224 | vi.advanceTimersByTime(1 * ONE_DAY); 225 | 226 | expect(getCertificatesMock).toHaveBeenCalledTimes(2); 227 | 228 | // Second HTTPS request 229 | const response2 = await makeHttpsRequest(options); 230 | expect(response2.status).toBe(200); 231 | expect(response2.data).toBe('Hello from target server'); 232 | 233 | // Verify getCertificates was called twice 234 | expect(getCertificatesMock).toHaveBeenCalledTimes(2); 235 | 236 | // Restore real timers 237 | vi.useRealTimers(); 238 | }); 239 | 240 | it('should not request certificates immediately for lazy loaded domains', async () => { 241 | // Reset mocks 242 | getCertificatesMock.mockClear(); 243 | 244 | // Simulate registering a domain with lazy loading enabled 245 | await proxy.register('https://lazy.example.com', `http://localhost:${testPort}`, { 246 | ssl: { 247 | letsencrypt: { 248 | email: 'email@example.com', 249 | production: false, 250 | lazy: true, 251 | }, 252 | }, 253 | }); 254 | 255 | // Check that certificates were not requested during registration 256 | expect(getCertificatesMock).not.toHaveBeenCalled(); 257 | }); 258 | 259 | it('should request and cache certificates on first HTTPS request for lazy certificates', async () => { 260 | // Reset mocks 261 | getCertificatesMock.mockClear(); 262 | 263 | // Make an HTTPS request to trigger lazy loading of certificates 264 | const options = { 265 | hostname: 'localhost', 266 | port: sslPort, 267 | path: '/', 268 | method: 'GET', 269 | headers: { Host: 'lazy.example.com' }, // Required for virtual hosts 270 | rejectUnauthorized: false, // Accept self-signed certificates 271 | }; 272 | 273 | const response = await new Promise<{ statusCode: number; data: string }>((resolve, reject) => { 274 | const req = https.request(options, (res) => { 275 | let data = ''; 276 | res.on('data', (chunk) => { 277 | data += chunk; 278 | }); 279 | res.on('end', () => { 280 | resolve({ statusCode: res.statusCode || 0, data }); 281 | }); 282 | }); 283 | req.on('error', reject); 284 | req.end(); 285 | }); 286 | 287 | expect(response.statusCode).toBe(200); 288 | expect(response.data).toBe(responseMessage); 289 | 290 | // Ensure that certificates are now loaded 291 | expect(getCertificatesMock).toHaveBeenCalled(); 292 | }); 293 | }); 294 | -------------------------------------------------------------------------------- /test/register.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { describe, it, expect } from 'vitest'; 4 | import { Redbird } from '../lib'; 5 | 6 | const opts = {}; 7 | 8 | describe('Route registration', function () { 9 | it('should register a simple route', function () { 10 | const redbird = new Redbird(opts); 11 | 12 | expect(redbird.routing).to.be.an('object'); 13 | 14 | redbird.register('example.com', '192.168.1.2:8080'); 15 | 16 | expect(redbird.routing).to.have.property('example.com'); 17 | 18 | return redbird 19 | .resolve('example.com') 20 | .then(function (result) { 21 | expect(result).to.be.an('object'); 22 | 23 | const host = redbird.routing['example.com']; 24 | expect(host).to.be.an('array'); 25 | expect(host[0]).to.have.property('path'); 26 | expect(host[0].path).to.be.eql('/'); 27 | expect(host[0].urls).to.be.an('array'); 28 | expect(host[0].urls.length).to.be.eql(1); 29 | expect(host[0].urls[0].href).to.be.eql('http://192.168.1.2:8080/'); 30 | 31 | redbird.unregister('example.com', '192.168.1.2:8080'); 32 | 33 | return redbird.resolve('example.com'); 34 | }) 35 | .then(function (result) { 36 | expect(result).to.be.an('undefined'); 37 | redbird.close(); 38 | }); 39 | }); 40 | 41 | it('should resolve domains as case insensitive', async () => { 42 | const redbird = new Redbird(opts); 43 | 44 | expect(redbird.routing).to.be.an('object'); 45 | 46 | redbird.register('example.com', '192.168.1.2:8080'); 47 | 48 | const target = await redbird.resolve('Example.com'); 49 | expect(target).to.be.an('object'); 50 | expect(target!.urls![0].hostname).to.be.equal('192.168.1.2'); 51 | 52 | redbird.close(); 53 | }); 54 | 55 | it('should register multiple routes', function () { 56 | const redbird = new Redbird(opts); 57 | 58 | expect(redbird.routing).to.be.an('object'); 59 | 60 | redbird.register('example1.com', '192.168.1.2:8080'); 61 | redbird.register('example2.com', '192.168.1.3:8081'); 62 | redbird.register('example3.com', '192.168.1.4:8082'); 63 | redbird.register('example4.com', '192.168.1.5:8083'); 64 | redbird.register('example5.com', '192.168.1.6:8084'); 65 | 66 | expect(redbird.routing).to.have.property('example1.com'); 67 | expect(redbird.routing).to.have.property('example2.com'); 68 | expect(redbird.routing).to.have.property('example3.com'); 69 | expect(redbird.routing).to.have.property('example4.com'); 70 | expect(redbird.routing).to.have.property('example5.com'); 71 | 72 | let host; 73 | 74 | host = redbird.routing['example1.com']; 75 | expect(host[0].path).to.be.eql('/'); 76 | expect(host[0].urls[0].href).to.be.eql('http://192.168.1.2:8080/'); 77 | 78 | host = redbird.routing['example2.com']; 79 | expect(host[0].path).to.be.eql('/'); 80 | expect(host[0].urls[0].href).to.be.eql('http://192.168.1.3:8081/'); 81 | 82 | host = redbird.routing['example3.com']; 83 | expect(host[0].path).to.be.eql('/'); 84 | expect(host[0].urls[0].href).to.be.eql('http://192.168.1.4:8082/'); 85 | 86 | host = redbird.routing['example4.com']; 87 | expect(host[0].path).to.be.eql('/'); 88 | expect(host[0].urls[0].href).to.be.eql('http://192.168.1.5:8083/'); 89 | 90 | host = redbird.routing['example5.com']; 91 | expect(host[0].path).to.be.eql('/'); 92 | expect(host[0].urls[0].href).to.be.eql('http://192.168.1.6:8084/'); 93 | 94 | redbird.unregister('example1.com'); 95 | 96 | return redbird 97 | .resolve('example1.com') 98 | .then(function (result) { 99 | expect(result).to.be.an('undefined'); 100 | 101 | redbird.unregister('example2.com'); 102 | return redbird.resolve('example2.com'); 103 | }) 104 | .then(function (result) { 105 | expect(result).to.be.an('undefined'); 106 | 107 | redbird.unregister('example3.com'); 108 | return redbird.resolve('example3.com'); 109 | }) 110 | .then(function (result) { 111 | expect(result).to.be.an('undefined'); 112 | 113 | redbird.unregister('example4.com'); 114 | return redbird.resolve('example4.com'); 115 | }) 116 | .then(function (result) { 117 | expect(result).to.be.an('undefined'); 118 | 119 | redbird.unregister('example5.com'); 120 | return redbird.resolve('example5.com'); 121 | }) 122 | .then(function (result) { 123 | expect(result).to.be.an('undefined'); 124 | redbird.close(); 125 | }); 126 | }); 127 | it('should register several pathnames within a route', function () { 128 | const redbird = new Redbird(opts); 129 | 130 | expect(redbird.routing).to.be.an('object'); 131 | 132 | redbird.register('example.com', '192.168.1.2:8080'); 133 | redbird.register('example.com/qux/baz', '192.168.1.5:8080'); 134 | redbird.register('example.com/foo', '192.168.1.3:8080'); 135 | redbird.register('example.com/bar', '192.168.1.4:8080'); 136 | 137 | expect(redbird.routing).to.have.property('example.com'); 138 | 139 | const host = redbird.routing['example.com']; 140 | expect(host).to.be.an('array'); 141 | expect(host[0]).to.have.property('path'); 142 | expect(host[0].path).to.be.eql('/qux/baz'); 143 | expect(host[0].urls).to.be.an('array'); 144 | expect(host[0].urls.length).to.be.eql(1); 145 | expect(host[0].urls[0].href).to.be.eql('http://192.168.1.5:8080/'); 146 | 147 | expect(host[0].path.length).to.be.least(host[1].path.length); 148 | expect(host[1].path.length).to.be.least(host[2].path.length); 149 | expect(host[2].path.length).to.be.least(host[3].path.length); 150 | 151 | redbird.unregister('example.com'); 152 | return redbird 153 | .resolve('example.com') 154 | .then(function (result) { 155 | expect(result).to.be.an('undefined'); 156 | return redbird.resolve('example.com', '/foo'); 157 | }) 158 | .then(function (result) { 159 | expect(result).to.be.an('object'); 160 | 161 | redbird.unregister('example.com/foo'); 162 | return redbird.resolve('example.com', '/foo'); 163 | }) 164 | .then(function (result) { 165 | expect(result).to.be.an('undefined'); 166 | 167 | redbird.close(); 168 | }); 169 | }); 170 | it('shouldnt crash process in unregister of unregisted host', function () { 171 | const redbird = new Redbird(opts); 172 | 173 | redbird.unregister('example.com'); 174 | 175 | redbird.close(); 176 | }); 177 | }); 178 | 179 | describe('Route resolution', function () { 180 | it('should resolve to a correct route', function () { 181 | const redbird = new Redbird(opts); 182 | 183 | expect(redbird.routing).to.be.an('object'); 184 | 185 | redbird.register('example.com', '192.168.1.2:8080'); 186 | redbird.register('example.com/qux/baz', '192.168.1.5:8080'); 187 | redbird.register('example.com/foo', '192.168.1.3:8080'); 188 | redbird.register('example.com/bar', '192.168.1.4:8080'); 189 | redbird.register('example.com/foo/baz', '192.168.1.3:8080'); 190 | 191 | return redbird.resolve('example.com', '/foo/asd/1/2').then(function (route) { 192 | expect(route!.path).to.be.eql('/foo'); 193 | expect(route!.urls!.length).to.be.eql(1); 194 | expect(route!.urls![0].href).to.be.eql('http://192.168.1.3:8080/'); 195 | 196 | redbird.close(); 197 | }); 198 | }); 199 | 200 | it('should resolve to a correct route with complex path', function () { 201 | const redbird = new Redbird(opts); 202 | 203 | expect(redbird.routing).to.be.an('object'); 204 | 205 | redbird.register('example.com', '192.168.1.2:8080'); 206 | redbird.register('example.com/qux/baz', '192.168.1.5:8080'); 207 | redbird.register('example.com/foo', '192.168.1.3:8080'); 208 | redbird.register('example.com/bar', '192.168.1.4:8080'); 209 | redbird.register('example.com/foo/baz', '192.168.1.7:8080'); 210 | 211 | return redbird.resolve('example.com', '/foo/baz/a/b/c').then(function (route) { 212 | expect(route!.path).to.be.eql('/foo/baz'); 213 | expect(route!.urls!.length).to.be.eql(1); 214 | expect(route!.urls![0].href).to.be.eql('http://192.168.1.7:8080/'); 215 | 216 | redbird.close(); 217 | }); 218 | }); 219 | 220 | it('should resolve to undefined if route not available', function () { 221 | const redbird = new Redbird(opts); 222 | 223 | expect(redbird.routing).to.be.an('object'); 224 | 225 | redbird.register('example.com', '192.168.1.2:8080'); 226 | redbird.register('example.com/qux/baz', '192.168.1.5:8080'); 227 | redbird.register('example.com/foo', '192.168.1.3:8080'); 228 | redbird.register('foobar.com/bar', '192.168.1.4:8080'); 229 | redbird.register('foobar.com/foo/baz', '192.168.1.3:8080'); 230 | 231 | return redbird 232 | .resolve('wrong.com') 233 | .then(function (route) { 234 | expect(route).to.be.an('undefined'); 235 | 236 | return redbird.resolve('foobar.com'); 237 | }) 238 | .then(function (route) { 239 | expect(route).to.be.an('undefined'); 240 | 241 | redbird.close(); 242 | }); 243 | }); 244 | 245 | it('should get a target if route available', async function () { 246 | const redbird = new Redbird(opts); 247 | 248 | expect(redbird.routing).to.be.an('object'); 249 | 250 | redbird.register('example.com', '192.168.1.2:8080'); 251 | redbird.register('example.com/qux/baz', '192.168.1.5:8080'); 252 | redbird.register('example.com/foo', '192.168.1.3:8080'); 253 | redbird.register('foobar.com/bar', '192.168.1.4:8080'); 254 | redbird.register('foobar.com/foo/baz', '192.168.1.7:8080'); 255 | redbird.register('foobar.com/media', '192.168.1.7:8080'); 256 | 257 | let route = await redbird.resolve('example.com', '/qux/a/b/c'); 258 | expect(route!.path).to.be.eql('/'); 259 | 260 | route = await redbird.resolve('foobar.com', '/medias/'); 261 | expect(route).to.be.undefined; 262 | 263 | route = await redbird.resolve('foobar.com', '/mediasa'); 264 | expect(route).to.be.undefined; 265 | 266 | route = await redbird.resolve('foobar.com', '/media/sa'); 267 | expect(route!.path).to.be.eql('/media'); 268 | 269 | const target = await redbird.getTarget('example.com', { url: '/foo/baz/a/b/c' }); 270 | expect(target.href).to.be.eql('http://192.168.1.3:8080/'); 271 | 272 | await redbird.close(); 273 | }); 274 | 275 | it('should get a target with path when necessary', function () { 276 | const redbird = new Redbird(opts); 277 | 278 | expect(redbird.routing).to.be.an('object'); 279 | 280 | redbird.register('example.com', '192.168.1.2:8080'); 281 | redbird.register('example.com/qux/baz', '192.168.1.5:8080'); 282 | redbird.register('example.com/foo', '192.168.1.3:8080/a/b'); 283 | redbird.register('foobar.com/bar', '192.168.1.4:8080'); 284 | redbird.register('foobar.com/foo/baz', '192.168.1.7:8080'); 285 | 286 | const req = { url: '/foo/baz/a/b/c' }; 287 | return redbird 288 | .resolve('example.com', '/qux/a/b/c') 289 | .then(function (route) { 290 | expect(route!.path).to.be.eql('/'); 291 | 292 | return redbird.getTarget('example.com', req); 293 | }) 294 | .then(function (target) { 295 | expect(target.href).to.be.eql('http://192.168.1.3:8080/a/b'); 296 | expect(req.url).to.be.eql('/a/b/baz/a/b/c'); 297 | 298 | redbird.close(); 299 | }); 300 | }); 301 | }); 302 | 303 | describe('TLS/SSL', function () { 304 | it('should allow TLS/SSL certificates', function () { 305 | const redbird = new Redbird({ 306 | ssl: { 307 | port: 4430, 308 | }, 309 | }); 310 | 311 | expect(redbird.routing).to.be.an('object'); 312 | redbird.register('example.com', '192.168.1.1:8080', { 313 | ssl: { 314 | key: 'dummy', 315 | cert: 'dummy', 316 | }, 317 | }); 318 | 319 | redbird.register('example.com', '192.168.1.2:8080'); 320 | 321 | expect(redbird.certs).to.be.an('object'); 322 | expect(redbird.certs['example.com']).to.be.an('object'); 323 | 324 | redbird.unregister('example.com', '192.168.1.1:8080'); 325 | 326 | return redbird 327 | .resolve('example.com') 328 | .then(function (result) { 329 | expect(result).to.not.be.an('undefined'); 330 | expect(redbird.certs['example.com']).to.not.be.an('undefined'); 331 | redbird.unregister('example.com', '192.168.1.2:8080'); 332 | 333 | return redbird.resolve('example.com'); 334 | }) 335 | .then(function (result) { 336 | expect(result).to.be.an('undefined'); 337 | expect(redbird.certs['example.com']).to.be.an('undefined'); 338 | }); 339 | }); 340 | it('Should bind https servers to different ip addresses', async function () { 341 | const isPortTaken = function (port, ip) { 342 | return new Promise(function (resolve, reject) { 343 | const net = require('net'); 344 | const tester = net 345 | .createServer() 346 | .once('error', function (err) { 347 | if (err.code != 'EADDRINUSE') { 348 | return reject(err); 349 | } 350 | resolve(true); 351 | }) 352 | .once('listening', function () { 353 | tester 354 | .once('close', function () { 355 | resolve(false); 356 | }) 357 | .close(); 358 | }) 359 | .listen(port, ip); 360 | }); 361 | }; 362 | 363 | const redbird = new Redbird({ 364 | port: 8080, 365 | 366 | // Specify filenames to default SSL certificates (in case SNI is not supported by the 367 | // user's browser) 368 | ssl: [ 369 | { 370 | port: 4433, 371 | key: 'dummy', 372 | cert: 'dummy', 373 | ip: '127.0.0.1', 374 | }, 375 | { 376 | port: 4434, 377 | key: 'dummy', 378 | cert: 'dummy', 379 | ip: '127.0.0.1', 380 | }, 381 | ], 382 | }); 383 | 384 | redbird.register('mydomain.com', 'http://127.0.0.1:8001', { 385 | ssl: { 386 | key: 'dummy', 387 | cert: 'dummy', 388 | ca: 'dummym', 389 | }, 390 | }); 391 | 392 | let portsTaken = 0; 393 | let portsChecked = 0; 394 | 395 | function portsTakenDone(taken) { 396 | portsChecked++; 397 | 398 | if (taken) { 399 | portsTaken++; 400 | } 401 | if (portsChecked == 2) { 402 | portsCheckDone(); 403 | } 404 | } 405 | 406 | function portsCheckDone() { 407 | expect(portsTaken).to.be.eql(2); 408 | redbird.close(); 409 | } 410 | 411 | portsTakenDone(await isPortTaken(4433, '127.0.0.1')); 412 | portsTakenDone(await isPortTaken(4434, '127.0.0.1')); 413 | }); 414 | }); 415 | 416 | describe('Load balancing', function () { 417 | it('should load balance between several targets', function () { 418 | const redbird = new Redbird(opts); 419 | 420 | expect(redbird.routing).to.be.an('object'); 421 | 422 | redbird.register('example.com', '192.168.1.1:8080'); 423 | redbird.register('example.com', '192.168.1.2:8080'); 424 | redbird.register('example.com', '192.168.1.3:8080'); 425 | redbird.register('example.com', '192.168.1.4:8080'); 426 | 427 | expect(redbird.routing['example.com'][0].urls.length).to.be.eql(4); 428 | expect(redbird.routing['example.com'][0].rr).to.be.eql(0); 429 | 430 | return redbird 431 | .resolve('example.com', '/foo/qux/a/b/c') 432 | .then(async (route) => { 433 | expect(route!.urls!.length).to.be.eql(4); 434 | 435 | for (let i = 0; i < 1000; i++) { 436 | let target = await redbird.getTarget('example.com', { url: '/a/b/c' }); 437 | expect(target.href).to.eql('http://192.168.1.1:8080/'); 438 | expect(redbird.routing['example.com'][0].rr).to.eql(1); 439 | 440 | target = await redbird.getTarget('example.com', { url: '/x/y' }); 441 | expect(target.href).to.eql('http://192.168.1.2:8080/'); 442 | expect(redbird.routing['example.com'][0].rr).to.eql(2); 443 | 444 | target = await redbird.getTarget('example.com', { url: '/j' }); 445 | expect(target.href).to.eql('http://192.168.1.3:8080/'); 446 | expect(redbird.routing['example.com'][0].rr).to.eql(3); 447 | 448 | target = await redbird.getTarget('example.com', { url: '/k/' }); 449 | expect(target.href).to.eql('http://192.168.1.4:8080/'); 450 | expect(redbird.routing['example.com'][0].rr).to.eql(0); 451 | } 452 | }) 453 | .then(function () { 454 | redbird.unregister('example.com', '192.168.1.1:8080'); 455 | return redbird.resolve('example.com'); 456 | }) 457 | .then(function (result) { 458 | expect(result).to.not.be.an('undefined'); 459 | 460 | redbird.unregister('example.com', '192.168.1.2:8080'); 461 | return redbird.resolve('example.com'); 462 | }) 463 | .then(function (result) { 464 | expect(result).to.not.be.an('undefined'); 465 | redbird.unregister('example.com', '192.168.1.3:8080'); 466 | 467 | return redbird.resolve('example.com'); 468 | }) 469 | .then(function (result) { 470 | expect(result).to.not.be.an('undefined'); 471 | redbird.unregister('example.com', '192.168.1.4:8080'); 472 | 473 | return redbird.resolve('example.com'); 474 | }) 475 | .then(function (result) { 476 | expect(result).to.be.an('undefined'); 477 | 478 | redbird.close(); 479 | }); 480 | }); 481 | }); 482 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redbird Reverse Proxy 2 | 3 |
4 |
5 | 6 |
7 |
8 |

9 | It should be easy and robust to handle dynamic virtual hosts, load balancing, proxying web sockets and SSL encryption. 10 | With Redbird you get a lightweight and flexible library to build dynamic reverse proxies with the speed and robustness of http-proxy. 11 | Redbird includes everything you need for easy reverse routing of your applications. 12 | Great for routing many applications from different domains in one single host, handling SSL with ease, etc. 13 |

14 |
15 |
16 |

17 | 18 | 19 | 20 | 21 | 22 | 23 |

24 |

25 | Follow @manast for *important* Redbird news and updates! 26 |

27 |
28 | 29 | ## Features 30 | 31 | - Flexible and easy routing 32 | - Websockets 33 | - Seamless SSL Support (HTTPS -> HTTP proxy) 34 | - Automatic HTTP to HTTPS redirects 35 | - Automatic TLS Certificates generation and renewal using LetsEncrypt 36 | - Supports HTTP2 37 | - Load balancer 38 | - Register and unregister routes programmatically without restart (allows zero downtime deployments) 39 | - Docker support for automatic registration of running containers 40 | - Cluster support that enables automatic multi-process 41 | - Based on top of rock-solid node-http-proxy and battle tested on production in many sites 42 | - Optional logging, with Pino. 43 | 44 | ## Install 45 | 46 | ```sh 47 | npm install redbird 48 | ``` 49 | 50 | ## Example 51 | 52 | You can programmatically register or unregister routes dynamically even if the proxy is already running: 53 | 54 | ```js 55 | var proxy = require('redbird')({ port: 80 }); 56 | 57 | // OPTIONAL: Setup your proxy but disable the X-Forwarded-For header 58 | var proxy = require('redbird')({ port: 80, xfwd: false }); 59 | 60 | // Route to any global ip 61 | proxy.register('optimalbits.com', 'http://167.23.42.67:8000'); 62 | 63 | // Route to any local ip, for example from docker containers. 64 | proxy.register('example.com', 'http://172.17.42.1:8001'); 65 | 66 | // Route from hostnames as well as paths 67 | proxy.register('example.com/static', 'http://172.17.42.1:8002'); 68 | proxy.register('example.com/media', 'http://172.17.42.1:8003'); 69 | 70 | // Subdomains, paths, everything just works as expected 71 | proxy.register('abc.example.com', 'http://172.17.42.4:8080'); 72 | proxy.register('abc.example.com/media', 'http://172.17.42.5:8080'); 73 | 74 | // Route to any href including a target path 75 | proxy.register('foobar.example.com', 'http://172.17.42.6:8080/foobar'); 76 | 77 | // You can also enable load balancing by registering the same hostname with different 78 | // target hosts. The requests will be evenly balanced using a Round-Robin scheme. 79 | proxy.register('balance.me', 'http://172.17.40.6:8080'); 80 | proxy.register('balance.me', 'http://172.17.41.6:8080'); 81 | proxy.register('balance.me', 'http://172.17.42.6:8080'); 82 | proxy.register('balance.me', 'http://172.17.43.6:8080'); 83 | 84 | // You can unregister routes as well 85 | proxy.register('temporary.com', 'http://172.17.45.1:8004'); 86 | proxy.unregister('temporary.com', 'http://172.17.45.1:8004'); 87 | 88 | // LetsEncrypt support 89 | // With Redbird you can get zero conf and automatic SSL certificates for your domains 90 | redbird.register('example.com', 'http://172.60.80.2:8082', { 91 | ssl: { 92 | letsencrypt: { 93 | email: 'john@example.com', // Domain owner/admin email 94 | production: true, // WARNING: Only use this flag when the proxy is verified to work correctly to avoid being banned! 95 | }, 96 | }, 97 | }); 98 | 99 | // 100 | // LetsEncrypt requires a minimal web server for handling the challenges, this is by default on port 3000 101 | // it can be configured when initiating the proxy. This web server is only used by Redbird internally so most of the time 102 | // you do not need to do anything special other than avoid having other web services in the same host running 103 | // on the same port. 104 | 105 | // 106 | // HTTP2 Support using LetsEncrypt for the certificates 107 | // 108 | var proxy = require('redbird')({ 109 | port: 80, // http port is needed for LetsEncrypt challenge during request / renewal. Also enables automatic http->https redirection for registered https routes. 110 | letsencrypt: { 111 | path: __dirname + '/certs', 112 | port: 9999, // LetsEncrypt minimal web server port for handling challenges. Routed 80->9999, no need to open 9999 in firewall. Default 3000 if not defined. 113 | }, 114 | ssl: { 115 | http2: true, 116 | port: 443, // SSL port used to serve registered https routes with LetsEncrypt certificate. 117 | }, 118 | }); 119 | ``` 120 | 121 | ## About HTTPS 122 | 123 | The HTTPS proxy supports virtual hosts by using SNI (which most modern browsers support: IE7 and above). 124 | The proxying is performed by hostname, so you must use the same SSL certificates for a given hostname independently of its paths. 125 | 126 | ### LetsEncrypt 127 | 128 | Some important considerations when using LetsEncrypt. You need to agree to LetsEncrypt [terms of service](https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf). When using 129 | LetsEncrypt, the obtained certificates will be copied to disk to the specified path. Its your responsibility to backup, or save persistently when applicable. Keep in mind that these certificates needs to be handled with care so that they cannot be accessed by malicious users. The certificates will be renewed by default one month before expiration, forever. 130 | 131 | ## HTTPS Example using your own certificates 132 | 133 | (NOTE: This is a legacy example not needed when using LetsEncrypt) 134 | 135 | Conceptually HTTPS is easy, but it is also easy to struggle getting it right. With Redbird its straightforward, check this complete example: 136 | 137 | 1. Generate a localhost development SSL certificate: 138 | 139 | ```sh 140 | /certs $ openssl genrsa -out dev-key.pem 1024 141 | /certs $ openssl req -new -key dev-key.pem -out dev-csr.pem 142 | 143 | // IMPORTANT: Do not forget to fill the field! Common Name (e.g. server FQDN or YOUR name) []:localhost 144 | 145 | /certs $ openssl x509 -req -in dev-csr.pem -signkey dev-key.pem -out dev-cert.pem 146 | 147 | ``` 148 | 149 | Note: For production sites you need to buy valid SSL certificates from a trusted authority. 150 | 151 | 2. Create a simple redbird based proxy: 152 | 153 | ```js 154 | var redbird = new require('redbird')({ 155 | port: 8080, 156 | 157 | // Specify filenames to default SSL certificates (in case SNI is not supported by the 158 | // user's browser) 159 | ssl: { 160 | port: 8443, 161 | key: 'certs/dev-key.pem', 162 | cert: 'certs/dev-cert.pem', 163 | }, 164 | }); 165 | 166 | // Since we will only have one https host, we dont need to specify additional certificates. 167 | redbird.register('localhost', 'http://localhost:8082', { ssl: true }); 168 | ``` 169 | 170 | 3. Test it: 171 | 172 | Point your browser to `localhost:8000` and you will see how it automatically redirects to your https server and proxies you to 173 | your target server. 174 | 175 | You can define many virtual hosts, each with its own SSL certificate. And if you do not define any, they will use the default one 176 | as in the example above: 177 | 178 | ```js 179 | redbird.register('example.com', 'http://172.60.80.2:8082', { 180 | ssl: { 181 | key: '../certs/example.key', 182 | cert: '../certs/example.crt', 183 | ca: '../certs/example.ca', 184 | }, 185 | }); 186 | 187 | redbird.register('foobar.com', 'http://172.60.80.3:8082', { 188 | ssl: { 189 | key: '../certs/foobar.key', 190 | cert: '../certs/foobar.crt', 191 | }, 192 | }); 193 | ``` 194 | 195 | You can also specify https hosts as targets and also specify if you want the connection to the target host to be secure (default is true). 196 | 197 | ```js 198 | var redbird = require('redbird')({ 199 | port: 80, 200 | secure: false, 201 | ssl: { 202 | port: 443, 203 | key: '../certs/default.key', 204 | cert: '../certs/default.crt', 205 | }, 206 | }); 207 | redbird.register('tutorial.com', 'https://172.60.80.2:8083', { 208 | ssl: { 209 | key: '../certs/tutorial.key', 210 | cert: '../certs/tutorial.crt', 211 | }, 212 | }); 213 | ``` 214 | 215 | Edge case scenario: you have an HTTPS server with two IP addresses assigned to it and your clients use old software without SNI support. In this case, both IP addresses will receive the same fallback certificate. I.e. some of the domains will get a wrong certificate. To handle this case you can create two HTTPS servers each one bound to its own IP address and serving the appropriate certificate. 216 | 217 | ```js 218 | var redbird = new require('redbird')({ 219 | port: 8080, 220 | 221 | // Specify filenames to default SSL certificates (in case SNI is not supported by the 222 | // user's browser) 223 | ssl: [ 224 | { 225 | port: 443, 226 | ip: '123.45.67.10', // assigned to tutorial.com 227 | key: 'certs/tutorial.key', 228 | cert: 'certs/tutorial.crt', 229 | }, 230 | { 231 | port: 443, 232 | ip: '123.45.67.11', // assigned to my-other-domain.com 233 | key: 'certs/my-other-domain.key', 234 | cert: 'certs/my-other-domain.crt', 235 | }, 236 | ], 237 | }); 238 | 239 | // These certificates will be served if SNI is supported 240 | redbird.register('tutorial.com', 'http://192.168.0.10:8001', { 241 | ssl: { 242 | key: 'certs/tutorial.key', 243 | cert: 'certs/tutorial.crt', 244 | }, 245 | }); 246 | redbird.register('my-other-domain.com', 'http://192.168.0.12:8001', { 247 | ssl: { 248 | key: 'certs/my-other-domain.key', 249 | cert: 'certs/my-other-domain.crt', 250 | }, 251 | }); 252 | ``` 253 | 254 | ## Docker support 255 | 256 | If you use docker, you can tell Redbird to automatically register routes based on image 257 | names. You register your image name and then every time a container starts from that image, 258 | it gets registered, and unregistered if the container is stopped. If you run more than one 259 | container from the same image, Redbird will load balance following a round-robin algorithm: 260 | 261 | ```js 262 | var redbird = require('redbird')({ 263 | port: 8080, 264 | }); 265 | 266 | var docker = require('redbird').docker; 267 | docker(redbird).register('old.api.com', 'company/api:v1.0.0'); 268 | docker(redbird).register('stable.api.com', 'company/api:v2.*'); 269 | docker(redbird).register('preview.api.com', 'company/api:v[3-9].*'); 270 | ``` 271 | 272 | ## etcd backend 273 | 274 | Redbird can use [node-etcd](https://github.com/stianeikeland/node-etcd) to automatically create proxy records from an etcd cluster. Configuration 275 | is accomplished by passing an array of [options](https://github.com/stianeikeland/node-etcd#constructor-options), plus the hosts and path variables, 276 | which define which etcd cluster hosts, and which directory within those hosts, that Redbird should poll for updates. 277 | 278 | ```js 279 | var redbird = require('redbird')({ 280 | port:8080 281 | }); 282 | 283 | var options = { 284 | hosts: ['localhost:2379'], // REQUIRED - you must define array of cluster hosts 285 | path: ['redbird'], // OPTIONAL - path to etcd keys 286 | ... // OPTIONAL - pass in node-etcd connection options 287 | } 288 | require('redbird').etcd(redbird,options); 289 | ``` 290 | 291 | etcd records can be created in one of two ways, either as a target destination pair: 292 | `/redbird/example.com "8.8.8.8"` 293 | or by passing a JSON object containing multiple hosts, and Redbird options: 294 | 295 | ``` 296 | /redbird/derek.com { "hosts" : ["10.10.10.10", "11.11.11.11"]} 297 | /redbird/johnathan.com { "ssl" : true } 298 | /redbird/jeff.com { "docker" : "alpine/alpine:latest" } 299 | ``` 300 | 301 | ## Cluster support 302 | 303 | Redbird supports automatic node cluster generation. To use, just specify the number 304 | of processes that you want Redbird to use in the options object. Redbird will automatically 305 | restart any thread that crashes, increasing reliability. 306 | 307 | ```js 308 | var redbird = new require('redbird')({ 309 | port: 8080, 310 | cluster: 4, 311 | }); 312 | ``` 313 | 314 | ## NTLM support 315 | 316 | If you need NTLM support, you can tell Redbird to add the required header handler. This 317 | registers a response handler which makes sure the NTLM auth header is properly split into 318 | two entries from http-proxy. 319 | 320 | ```js 321 | var redbird = new require('redbird')({ 322 | port: 8080, 323 | ntlm: true, 324 | }); 325 | ``` 326 | 327 | ## Custom Resolvers 328 | 329 | With custom resolvers, you can decide how the proxy server handles request. Custom resolvers allow you to extend Redbird considerably. With custom resolvers, you can perform the following: 330 | 331 | - Do path-based routing. 332 | - Do headers based routing. 333 | - Do wildcard domain routing. 334 | - Use variable upstream servers based on availability, for example in conjunction with Etcd or any other service discovery platform. 335 | - And more. 336 | 337 | Resolvers should be: 338 | 339 | 1. Be invokable function. The `this` context of such function is the Redbird Proxy object. The resolver function takes in two parameters : `host` and `url` 340 | 2. Have a priority, resolvers with higher priorities are called before those of lower priorities. The default resolver, has a priority of 0. 341 | 3. A resolver should return a route object or a string when matches it matches the parameters passed in. If string is returned, then it must be a valid upstream URL, if object, then the object must conform to the following: 342 | 343 | ``` 344 | { 345 | url: string or array of string [required], when array, the urls will be load-balanced across. 346 | path: path prefix for route, [optional], defaults to '/', 347 | opts: {} // Redbird target options, see Redbird.register() [optional], 348 | } 349 | ``` 350 | 351 | ### Defining Resolvers 352 | 353 | Resolvers can be defined when initializing the proxy object with the `resolvers` parameter. An example is below: 354 | 355 | ```javascript 356 | // for every URL path that starts with /api/, send request to upstream API service 357 | const customResolver1 = { 358 | fn: (host, url, req)=> { 359 | if (/^\/api\//.test(url)) { 360 | return 'http://127.0.0.1:8888'; 361 | }, 362 | priority: 100 363 | }; 364 | } 365 | var proxy = new require('redbird')({ 366 | port: 8080, 367 | resolvers: [ 368 | customResolver1, 369 | ], 370 | }); 371 | ``` 372 | 373 | ### Adding and Removing Resolvers at Runtime. 374 | 375 | You can add or remove resolvers at runtime, this is useful in situations where your upstream is tied to a service discovery service system. 376 | 377 | ```javascript 378 | var topPriority = function (host, url, req) { 379 | return /app\.example\.com/.test(host) 380 | ? { 381 | // load balanced 382 | url: ['http://127.0.0.1:8000', 'http://128.0.1.1:9999'], 383 | } 384 | : null; 385 | }; 386 | 387 | proxy.addResolver(topPriority, 200); 388 | 389 | // remove top priority after 10 minutes, 390 | setTimeout(function () { 391 | proxy.removeResolver(topPriority); 392 | }, 600000); 393 | ``` 394 | 395 | ## Replacing the default HTTP/HTTPS server modules 396 | 397 | By passing `serverModule: module` or `ssl: {serverModule : module}` you can override the default http/https 398 | servers used to listen for connections with another module. 399 | 400 | One application for this is to enable support for PROXY protocol: This is useful if you want to use a module like 401 | [findhit-proxywrap](https://github.com/findhit/proxywrap) to enable support for the 402 | [PROXY protocol](http://www.haproxy.org/download/1.8/doc/proxy-protocol.txt). 403 | 404 | PROXY protocol is used in tools like HA-Proxy, and can be optionally enabled in Amazon ELB load balancers to pass the 405 | original client IP when proxying TCP connections (similar to an X-Forwarded-For header, but for raw TCP). This is useful 406 | if you want to run redbird on AWS behind an ELB load balancer, but have redbird terminate any HTTPS connections so you 407 | can have SNI/Let's Encrypt/HTTP2support. With this in place Redbird will see the client's IP address rather 408 | than the load-balancer's, and pass this through in an X-Forwarded-For header. 409 | 410 | ```javascript 411 | //Options for proxywrap. This means the proxy will also respond to regular HTTP requests without PROXY information as well. 412 | proxy_opts = { strict: false }; 413 | proxyWrap = require('findhit-proxywrap'); 414 | var opts = { 415 | port: process.env.HTTP_PORT, 416 | serverModule: proxyWrap.proxy(require('http'), proxy_opts), 417 | ssl: { 418 | //Do this if you want http2: 419 | http2: true, 420 | serverModule: proxyWrap.proxy(require('spdy').server, proxy_opts), 421 | //Do this if you only want regular https 422 | // serverModule: proxyWrap.proxy( require('http'), proxy_opts), 423 | port: process.env.HTTPS_PORT, 424 | }, 425 | }; 426 | 427 | // Create the proxy 428 | var proxy = require('redbird')(opts); 429 | ``` 430 | 431 | ## Roadmap 432 | 433 | - Statistics (number of connections, load, response times, etc) 434 | - Rate limiter. 435 | - Simple IP Filtering. 436 | - Automatic routing via Redis. 437 | 438 | ## Reference 439 | 440 | [constructor](#redbird) 441 | [register](#register) 442 | [unregister](#unregister) 443 | [notFound](#notFound) 444 | [close](#close) 445 | 446 | 447 | 448 | ### Redbird(opts) 449 | 450 | This is the Proxy constructor. Creates a new Proxy and starts listening to 451 | the given port. 452 | 453 | **Arguments** 454 | 455 | ``` 456 | opts {Object} Options to pass to the proxy: 457 | { 458 | port: {Number} // port number that the proxy will listen to. 459 | ssl: { // Optional SSL proxying. 460 | port: {Number} // SSL port the proxy will listen to. 461 | // Default certificates 462 | key: keyPath, 463 | cert: certPath, 464 | ca: caPath // Optional. 465 | redirectPort: port, // optional https port number to be redirected if entering using http. 466 | http2: false, //Optional, setting to true enables http2/spdy support 467 | serverModule : require('https') // Optional, override the https server module used to listen for https or http2 connections. Default is require('https') or require('spdy') 468 | } 469 | bunyan: {Object} Bunyan options. Check [bunyan](https://github.com/trentm/node-bunyan) for info. 470 | If you want to disable bunyan, just set this option to false. Keep in mind that 471 | having logs enabled incours in a performance penalty of about one order of magnitude per request. 472 | resolvers: {Function | Array} a list of custom resolvers. Can be a single function or an array of functions. See more details about resolvers above. 473 | serverModule : {Module} Optional - Override the http server module used to listen for http connections. Default is require('http') 474 | } 475 | ``` 476 | 477 | --- 478 | 479 | 480 | 481 | #### Redbird::register(src, target, opts) 482 | 483 | Register a new route. As soon as this method is called, the proxy will 484 | start routing the sources to the given targets. 485 | 486 | **Arguments** 487 | 488 | ```javascript 489 | src {String} {String|URL} A string or a url parsed by node url module. 490 | Note that port is ignored, since the proxy just listens to one port. 491 | 492 | target {String|URL} A string or a url parsed by node url module. 493 | opts {Object} route options: 494 | examples: 495 | {ssl : true} // Will use default ssl certificates. 496 | {ssl: { 497 | redirect: true, // False to disable HTTPS autoredirect to this route. 498 | key: keyPath, 499 | cert: certPath, 500 | ca: caPath, // optional 501 | secureOptions: constants.SSL_OP_NO_TLSv1 //optional, see below 502 | } 503 | } 504 | {onRequest: (req, res, target) => { 505 | // called before forwarding is occurred, you can modify req.headers for example 506 | // return undefined to forward to default target 507 | }} 508 | ``` 509 | 510 | > _Note: if you need to use **ssl.secureOptions**, to disable older, insecure TLS versions, import crypto/constants first:_ 511 | 512 | > `const { constants } = require('crypto')` 513 | 514 | --- 515 | 516 | 517 | 518 | #### Redbird.unregister(src, [target]) 519 | 520 | Unregisters a route. After calling this method, the given route will not 521 | be proxied anymore. 522 | 523 | **Arguments** 524 | 525 | ```javascript 526 | src {String|URL} A string or a url parsed by node url module. 527 | target {String|URL} A string or a url parsed by node url module. If not 528 | specified, it will unregister all routes for the given source. 529 | ``` 530 | 531 | --- 532 | 533 | 534 | 535 | #### Redbird.notFound(callback) 536 | 537 | Gives Redbird a callback function with two parameters, the HTTP request 538 | and response objects, respectively, which will be called when a proxy route is 539 | not found. The default is 540 | 541 | ```javascript 542 | function(req, res){ 543 | res.statusCode = 404; 544 | res.write('Not Found'); 545 | res.end(); 546 | }; 547 | ``` 548 | 549 | . 550 | 551 | **Arguments** 552 | 553 | ```javascript 554 | src {Function(req, res)} The callback which will be called with the HTTP 555 | request and response objects when a proxy route is not found. 556 | ``` 557 | 558 | --- 559 | 560 | 561 | 562 | #### Redbird.close() 563 | 564 | Close the proxy stopping all the incoming connections. 565 | 566 | --- 567 | -------------------------------------------------------------------------------- /lib/proxy.ts: -------------------------------------------------------------------------------- 1 | /*eslint-env node */ 2 | 'use strict'; 3 | 4 | // Built-in NodeJS modules. 5 | import path from 'path'; 6 | import { URL, parse as parseUrl } from 'url'; 7 | import cluster from 'cluster'; 8 | import http, { Agent, ClientRequest, IncomingMessage, Server, ServerResponse } from 'http'; 9 | import https from 'https'; 10 | import http2, { Http2ServerRequest, Http2ServerResponse } from 'http2'; 11 | import fs from 'fs'; 12 | import tls from 'tls'; 13 | 14 | // Third party modules. 15 | import validUrl from 'valid-url'; 16 | import httpProxy, { ServerOptions, ProxyTargetUrl } from 'http-proxy'; 17 | import lodash from 'lodash'; 18 | import { pino, Logger } from 'pino'; 19 | import hash from 'object-hash'; 20 | import safe from 'safe-timers'; 21 | import { LRUCache } from 'lru-cache'; 22 | 23 | // Custom modules. 24 | import * as letsencrypt from './letsencrypt.js'; 25 | import { ProxyOptions } from './interfaces/proxy-options.js'; 26 | import { Socket } from 'net'; 27 | import { ProxyRoute } from './interfaces/proxy-route.js'; 28 | import { RouteOptions } from './interfaces/route-options.js'; 29 | import { Resolver, ResolverFn, ResolverFnResult } from './interfaces/resolver.js'; 30 | 31 | const { isFunction, isObject, sortBy, uniq, remove, isString } = lodash; 32 | 33 | const routeCache = new LRUCache({ max: 5000 }); 34 | const defaultLetsencryptPort = 3000; 35 | const ONE_DAY = 60 * 60 * 24 * 1000; 36 | const ONE_MONTH = ONE_DAY * 30; 37 | 38 | export class Redbird { 39 | logger?: Logger; 40 | routing: any = {}; 41 | resolvers: Resolver[] = []; 42 | certs: any; 43 | lazyCerts: { 44 | [key: string]: { email: string; production: boolean; renewWithin: number }; 45 | } = {}; 46 | 47 | private _defaultResolver: any; 48 | private proxy: httpProxy; 49 | private agent: Agent; 50 | private server: any; 51 | 52 | private httpsServer: any; 53 | 54 | private letsencryptHost: string; 55 | private letsencryptServer: Server; 56 | 57 | get defaultResolver() { 58 | return this._defaultResolver; 59 | } 60 | 61 | constructor(private opts: ProxyOptions = {}) { 62 | if (this.opts.httpProxy == undefined) { 63 | this.opts.httpProxy = {}; 64 | } 65 | 66 | if (opts.logger) { 67 | this.logger = pino( 68 | opts.logger || { 69 | name: 'redbird', 70 | } 71 | ); 72 | } 73 | 74 | this._defaultResolver = { 75 | fn: (host: string, url: string) => { 76 | // Given a src resolve it to a target route if any available. 77 | if (!host) { 78 | return; 79 | } 80 | 81 | url = url || '/'; 82 | 83 | const routes = this.routing[host]; 84 | let i = 0; 85 | 86 | if (routes) { 87 | const len = routes.length; 88 | 89 | // 90 | // Find path that matches the start of req.url 91 | // 92 | for (i = 0; i < len; i++) { 93 | const route = routes[i]; 94 | 95 | if (route.path === '/' || startsWith(url, route.path)) { 96 | return route; 97 | } 98 | } 99 | } 100 | }, 101 | priority: 0, 102 | }; 103 | 104 | if ((opts.cluster && typeof opts.cluster !== 'number') || opts.cluster > 32) { 105 | throw Error('cluster setting must be an integer less than 32'); 106 | } 107 | 108 | if (opts.cluster && cluster.isPrimary) { 109 | for (let i = 0; i < opts.cluster; i++) { 110 | cluster.fork(); 111 | } 112 | 113 | cluster.on('exit', (worker, code, signal) => { 114 | // Fork if a worker dies. 115 | this.logger?.error( 116 | { 117 | code: code, 118 | signal: signal, 119 | }, 120 | 'worker died un-expectedly... restarting it.' 121 | ); 122 | cluster.fork(); 123 | }); 124 | } else { 125 | this.resolvers = [this._defaultResolver]; 126 | 127 | opts.port = opts.port || 8080; 128 | 129 | if (opts.letsencrypt) { 130 | this.setupLetsencrypt(opts); 131 | } 132 | 133 | if (opts.resolvers) { 134 | for (let i = 0; i < opts.resolvers.length; i++) { 135 | this.addResolver(opts.resolvers[i].fn, opts.resolvers[i].priority); 136 | } 137 | } 138 | 139 | const websocketsUpgrade = async (req: any, socket: Socket, head: Buffer) => { 140 | socket.on('error', (err) => { 141 | this.logger?.error(err, 'WebSockets error'); 142 | }); 143 | const src = this.getSource(req); 144 | const target = await this.getTarget(src, req); 145 | 146 | this.logger?.info({ headers: req.headers, target: target }, 'upgrade to websockets'); 147 | 148 | if (target) { 149 | if (target.useTargetHostHeader === true) { 150 | req.headers.host = target.host; 151 | } 152 | proxy.ws(req, socket, head, { target }); 153 | } else { 154 | respondNotFound(req, socket); 155 | } 156 | }; 157 | 158 | // 159 | // Create a proxy server with custom application logic 160 | // 161 | let agent; 162 | 163 | if (opts.keepAlive) { 164 | agent = this.agent = new Agent({ 165 | keepAlive: true, 166 | }); 167 | } 168 | 169 | const proxy = (this.proxy = httpProxy.createProxyServer({ 170 | xfwd: opts.xfwd != false, 171 | prependPath: false, 172 | secure: opts.secure !== false, 173 | timeout: opts.timeout, 174 | proxyTimeout: opts.proxyTimeout, 175 | agent, 176 | })); 177 | 178 | proxy.on( 179 | 'proxyReq', 180 | ( 181 | proxyReq: ClientRequest, 182 | req: IncomingMessage, 183 | res: ServerResponse, 184 | options: ServerOptions 185 | ) => { 186 | // According to typescript this is the correct way to access the host header 187 | // const host = req.headers.host; 188 | const host = (req)['host'] as string; 189 | if (host != null) { 190 | proxyReq.setHeader('host', host); 191 | } 192 | } 193 | ); 194 | 195 | // 196 | // Support NTLM auth 197 | // 198 | if (opts.ntlm) { 199 | proxy.on( 200 | 'proxyRes', 201 | (proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse) => { 202 | const key = 'www-authenticate'; 203 | (proxyRes).headers[key] = 204 | proxyRes.headers[key] && proxyRes.headers[key].split(','); 205 | } 206 | ); 207 | } 208 | 209 | // 210 | // Optionally create an https proxy server. 211 | // 212 | if (opts.ssl) { 213 | if (Array.isArray(opts.ssl)) { 214 | opts.ssl.forEach((sslOpts) => { 215 | this.setupHttpsProxy(proxy, websocketsUpgrade, sslOpts); 216 | }); 217 | } else { 218 | this.setupHttpsProxy(proxy, websocketsUpgrade, opts.ssl); 219 | } 220 | } 221 | 222 | // 223 | // Plain HTTP Proxy 224 | // 225 | const server = (this.server = this.setupHttpProxy( 226 | proxy, 227 | websocketsUpgrade, 228 | this.logger, 229 | opts 230 | )); 231 | server.listen(opts.port, opts.host); 232 | 233 | const handleProxyError = ( 234 | err: NodeJS.ErrnoException, 235 | req: IncomingMessage, 236 | resOrSocket: ServerResponse | Socket, 237 | target?: ProxyTargetUrl 238 | ) => { 239 | const res = resOrSocket instanceof ServerResponse ? resOrSocket : null; 240 | 241 | // 242 | // Send a 500 http status if headers have been sent 243 | // 244 | if (!res || !res.writeHead) { 245 | this.logger?.error(err, 'Proxy Error'); 246 | return; 247 | } else { 248 | if (err.code === 'ECONNREFUSED') { 249 | res.writeHead(502); 250 | } else if (!res.headersSent) { 251 | res.writeHead(500, err.message, { 'content-type': 'text/plain' }); 252 | } 253 | } 254 | 255 | // 256 | // Do not log this common error 257 | // 258 | if (err.message !== 'socket hang up') { 259 | this.logger?.error(err, 'Proxy Error'); 260 | } 261 | 262 | // 263 | // TODO: if err.code=ECONNREFUSED and there are more servers 264 | // for this route, try another one. 265 | // 266 | res.end(err.code); 267 | }; 268 | 269 | if (opts.errorHandler && isFunction(opts.errorHandler)) { 270 | proxy.on('error', opts.errorHandler); 271 | } else { 272 | proxy.on('error', handleProxyError); 273 | } 274 | 275 | this.logger?.info('Started a Redbird reverse proxy server on port %s', opts.port); 276 | } 277 | } 278 | 279 | setupHttpProxy(proxy: httpProxy, websocketsUpgrade: any, log: pino.Logger, opts: ProxyOptions) { 280 | const httpServerModule = opts.serverModule || http; 281 | const server = (this.server = httpServerModule.createServer( 282 | async (req: IncomingMessage, res: ServerResponse) => { 283 | const src = this.getSource(req); 284 | const target = await this.getTarget(src, req, res); 285 | 286 | if (target) { 287 | if (this.shouldRedirectToHttps(target)) { 288 | redirectToHttps(req, res, this.opts.ssl, this.logger); 289 | } else { 290 | proxy.web(req, res, { target, secure: !!opts.secure }); 291 | } 292 | } else { 293 | respondNotFound(req, res); 294 | } 295 | } 296 | )); 297 | 298 | // 299 | // Listen to the `upgrade` event and proxy the 300 | // WebSocket requests as well. 301 | // 302 | server.on('upgrade', websocketsUpgrade); 303 | 304 | server.on('error', function (err) { 305 | log && log.error(err, 'Server Error'); 306 | }); 307 | 308 | return server; 309 | } 310 | 311 | /** 312 | * Special resolver for handling Let's Encrypt ACME challenges. 313 | * @param opts 314 | */ 315 | setupLetsencrypt(opts: ProxyOptions) { 316 | if (!opts.letsencrypt.path) { 317 | throw Error('Missing certificate path for Lets Encrypt'); 318 | } 319 | const letsencryptPort = opts.letsencrypt.port || defaultLetsencryptPort; 320 | this.letsencryptServer = letsencrypt.init(opts.letsencrypt.path, letsencryptPort, this.logger); 321 | 322 | this.letsencryptHost = '127.0.0.1:' + letsencryptPort; 323 | const targetHost = 'http://' + this.letsencryptHost; 324 | const challengeResolver = (host: string, url: string) => { 325 | if (/^\/.well-known\/acme-challenge/.test(url)) { 326 | return `${targetHost}/${host}`; 327 | } 328 | }; 329 | this.addResolver(challengeResolver, 9999); 330 | } 331 | 332 | setupHttpsProxy(proxy: httpProxy, websocketsUpgrade: any, sslOpts: any) { 333 | let httpsModule; 334 | this.certs = this.certs || {}; 335 | const certs = this.certs; 336 | 337 | let ssl: { 338 | SNICallback: (hostname: string, cb: (err: any, ctx: any) => void) => void; 339 | key: any; 340 | cert: any; 341 | secureOptions?: number; 342 | ca?: any; 343 | opts?: any; 344 | } = { 345 | SNICallback: async (hostname: string, cb: (err: any, ctx?: any) => void) => { 346 | if (!certs[hostname]) { 347 | if (!this.opts?.letsencrypt?.path) { 348 | console.error('Missing certificate path for Lets Encrypt'); 349 | return cb(new Error('No certs for hostname ' + hostname)); 350 | } 351 | 352 | if (!this.lazyCerts[hostname]) { 353 | // Check if we have a resolver that matches the hostname and has letsencrypt enabled 354 | const results = await this.applyResolvers(this.resolvers, hostname, '', null); 355 | const route = results.find((route) => (route)?.opts?.ssl?.letsencrypt); 356 | const sslOpts = (route)?.opts?.ssl; 357 | if (route && sslOpts) { 358 | this.lazyCerts[hostname] = { 359 | email: sslOpts.letsencrypt?.email, 360 | production: sslOpts.letsencrypt?.production, 361 | renewWithin: this.opts?.letsencrypt?.renewWithin || ONE_MONTH, 362 | }; 363 | } else { 364 | return cb(new Error('No certs for hostname ' + hostname)); 365 | } 366 | } 367 | 368 | try { 369 | await this.updateCertificates( 370 | hostname, 371 | this.lazyCerts[hostname].email, 372 | this.lazyCerts[hostname].production, 373 | this.lazyCerts[hostname].renewWithin 374 | ); 375 | } catch (err) { 376 | console.error('Error getting LetsEncrypt certificates', err); 377 | return cb(err); 378 | } 379 | } else if (!certs[hostname]) { 380 | return cb(new Error('No certs for hostname ' + hostname)); 381 | } 382 | 383 | if (cb) { 384 | cb(null, certs[hostname]); 385 | } 386 | }, 387 | // 388 | // Default certs for clients that do not support SNI. 389 | // 390 | key: getCertData(sslOpts.key), 391 | cert: getCertData(sslOpts.cert), 392 | }; 393 | 394 | // Allows the option to disable older SSL/TLS versions 395 | if (sslOpts.secureOptions) { 396 | ssl.secureOptions = sslOpts.secureOptions; 397 | } 398 | 399 | if (sslOpts.ca) { 400 | ssl.ca = getCertData(sslOpts.ca, true); 401 | } 402 | 403 | if (sslOpts.opts) { 404 | ssl = { ...sslOpts.opts, ...ssl }; 405 | } 406 | 407 | if (sslOpts.http2) { 408 | httpsModule = sslOpts.serverModule || { 409 | createServer: ( 410 | sslOpts: any, 411 | cb: (req: Http2ServerRequest, res: Http2ServerResponse) => void 412 | ) => http2.createSecureServer(sslOpts, cb), 413 | }; 414 | } else { 415 | httpsModule = sslOpts.serverModule || https; 416 | } 417 | 418 | const httpsServer = (this.httpsServer = httpsModule.createServer( 419 | ssl, 420 | async (req: IncomingMessage, res: ServerResponse) => { 421 | const src = this.getSource(req); 422 | const httpProxyOpts = Object.assign({}, this.opts.httpProxy); 423 | 424 | const target = await this.getTarget(src, req, res); 425 | if (target) { 426 | httpProxyOpts.target = target; 427 | proxy.web(req, res, httpProxyOpts); 428 | } else { 429 | respondNotFound(req, res); 430 | } 431 | } 432 | )); 433 | 434 | httpsServer.on('upgrade', websocketsUpgrade); 435 | 436 | httpsServer.on('error', (err: NodeJS.ErrnoException) => { 437 | this.logger?.error(err, 'HTTPS Server Error'); 438 | }); 439 | 440 | httpsServer.on('clientError', (err: NodeJS.ErrnoException) => { 441 | this.logger?.error(err, 'HTTPS Client Error'); 442 | }); 443 | 444 | this.logger?.info('Listening to HTTPS requests on port %s', sslOpts.port); 445 | httpsServer.listen(sslOpts.port, sslOpts.ip); 446 | } 447 | 448 | addResolver(resolverFn: ResolverFn, priority?: number) { 449 | if (this.opts.cluster && cluster.isPrimary) { 450 | return this; 451 | } 452 | 453 | // Check if the resolver is already added if so just update its priority 454 | let found = false; 455 | for (let i = 0; i < this.resolvers.length; i++) { 456 | if (this.resolvers[i].fn === resolverFn) { 457 | this.resolvers[i].priority = priority || 0; 458 | found = true; 459 | break; 460 | } 461 | } 462 | 463 | if (!found) { 464 | this.resolvers.push({ 465 | fn: resolverFn, 466 | priority: priority || 0, 467 | }); 468 | } 469 | 470 | this.resolvers = sortBy(uniq(this.resolvers), 'priority').reverse(); 471 | } 472 | 473 | removeResolver(resolverFn: ResolverFn) { 474 | if (this.opts.cluster && cluster.isPrimary) { 475 | return this; 476 | } 477 | 478 | // Since unique resolvers are not checked for performance, 479 | // just remove every existence. 480 | this.resolvers = this.resolvers.filter(function (resolver) { 481 | return resolverFn !== resolver.fn; 482 | }); 483 | } 484 | 485 | /** 486 | Register a new route. 487 | 488 | @src {String|URL} A string or a url parsed by node url module. 489 | Note that port is ignored, since the proxy just listens to one port. 490 | 491 | @target {String|URL} A string or a url parsed by node url module. 492 | @opts {Object} Route options. 493 | */ 494 | register( 495 | opts: { 496 | src: string | URL; 497 | target: string | URL; 498 | } & RouteOptions 499 | ): Promise; 500 | register(src: string, opts: any): Promise; 501 | register(src: string | URL, target: string | URL, opts: RouteOptions): Promise; 502 | async register(src: any, target?: any, opts?: RouteOptions): Promise { 503 | if (this.opts.cluster && cluster.isPrimary) { 504 | return; 505 | } 506 | 507 | // allow registering with src or target as an object to pass in 508 | // options specific to each one. 509 | if (src && src.src) { 510 | target = src.target; 511 | opts = src; 512 | src = src.src; 513 | } else if (target && target.target) { 514 | opts = target; 515 | target = target.target; 516 | } 517 | 518 | if (!src || !target) { 519 | throw Error('Cannot register a new route with unspecified src or target'); 520 | } 521 | 522 | const routing = this.routing; 523 | 524 | src = prepareUrl(src); 525 | if (opts) { 526 | const ssl = opts.ssl; 527 | if (ssl) { 528 | if (!this.httpsServer) { 529 | throw Error('Cannot register https routes without defining a ssl port'); 530 | } 531 | 532 | if (!this.certs[src.hostname]) { 533 | if (ssl.key || ssl.cert || ssl.ca) { 534 | this.certs[src.hostname] = createCredentialContext(ssl.key, ssl.cert, ssl.ca); 535 | } else if (ssl.letsencrypt) { 536 | if (!this.opts.letsencrypt || !this.opts.letsencrypt.path) { 537 | console.error('Missing certificate path for Lets Encrypt'); 538 | return; 539 | } 540 | 541 | if (!ssl.letsencrypt.lazy) { 542 | this.logger?.info('Getting Lets Encrypt certificates for %s', src.hostname); 543 | await this.updateCertificates( 544 | src.hostname, 545 | ssl.letsencrypt.email, 546 | ssl.letsencrypt.production, 547 | this.opts.letsencrypt.renewWithin || ONE_MONTH 548 | ); 549 | } else { 550 | // We need to store the letsencrypt options for this domain somewhere 551 | this.logger?.info('Lazy loading Lets Encrypt certificates for %s', src.hostname); 552 | this.lazyCerts[src.hostname] = { 553 | ...ssl.letsencrypt, 554 | renewWithin: this.opts.letsencrypt.renewWithin || ONE_MONTH, 555 | }; 556 | } 557 | } else { 558 | // Trigger the use of the default certificates. 559 | this.certs[src.hostname] = void 0; 560 | } 561 | } 562 | } 563 | } 564 | target = buildTarget(target, opts); 565 | 566 | const hostname = src.hostname; 567 | const host = (routing[hostname] = routing[hostname] || []); 568 | const pathname = src.pathname || '/'; 569 | let route = host.find((route: { path: string }) => route.path === pathname); 570 | 571 | if (!route) { 572 | route = { 573 | path: pathname, 574 | rr: 0, 575 | urls: [], 576 | opts: Object.assign({}, opts), 577 | }; 578 | host.push(route); 579 | 580 | // 581 | // Sort routes 582 | // 583 | routing[src.hostname] = sortBy(host, function (_route) { 584 | return -_route.path.length; 585 | }); 586 | } 587 | 588 | route.urls.push(target); 589 | 590 | this.logger?.info({ from: src, to: target }, 'Registered a new route'); 591 | } 592 | 593 | async updateCertificates( 594 | domain: string, 595 | email: string, 596 | production: boolean, 597 | renewWithin: number, 598 | renew?: boolean 599 | ) { 600 | try { 601 | const certs = await letsencrypt.getCertificates( 602 | domain, 603 | email, 604 | this.opts.letsencrypt?.port, 605 | production, 606 | renew, 607 | this.logger 608 | ); 609 | if (certs) { 610 | const opts = { 611 | key: certs.privkey, 612 | cert: certs.cert + certs.chain, 613 | }; 614 | this.certs[domain] = tls.createSecureContext(opts).context; 615 | 616 | // 617 | // TODO: cluster friendly 618 | // 619 | let renewTime = certs.expiresAt - Date.now() - renewWithin; 620 | renewTime = 621 | renewTime > 0 ? renewTime : this.opts.letsencrypt.minRenewTime || 60 * 60 * 1000; 622 | 623 | this.logger?.info('Renewal of %s in %s days', domain, Math.floor(renewTime / ONE_DAY)); 624 | 625 | const renewCertificate = () => { 626 | this.logger?.info('Renewing letscrypt certificates for %s', domain); 627 | this.updateCertificates(domain, email, production, renewWithin, true); 628 | }; 629 | 630 | this.certs[domain].renewalTimeout = safe.setTimeout(renewCertificate, renewTime); 631 | } else { 632 | // 633 | // TODO: Try again, but we need an exponential backof to avoid getting banned. 634 | // 635 | this.logger?.info('Could not get any certs for %s', domain); 636 | } 637 | } catch (err) { 638 | console.error('Error getting LetsEncrypt certificates', err); 639 | } 640 | } 641 | 642 | unregister(src: string | URL, target?: string | URL): Redbird { 643 | if (this.opts.cluster && cluster.isPrimary) { 644 | return this; 645 | } 646 | 647 | if (!src) { 648 | return this; 649 | } 650 | 651 | const srcURL = prepareUrl(src); 652 | const routes = this.routing[srcURL.hostname] || []; 653 | const pathname = srcURL.pathname || '/'; 654 | let i; 655 | 656 | for (i = 0; i < routes.length; i++) { 657 | if (routes[i].path === pathname) { 658 | break; 659 | } 660 | } 661 | 662 | if (i < routes.length) { 663 | const route = routes[i]; 664 | 665 | if (target) { 666 | const targetURL = prepareUrl(target); 667 | remove(route.urls, (url: URL) => { 668 | return url.href === targetURL.href; 669 | }); 670 | } else { 671 | route.urls = []; 672 | } 673 | 674 | if (route.urls.length === 0) { 675 | routes.splice(i, 1); 676 | const certs = this.certs; 677 | if (certs) { 678 | if (certs[srcURL.hostname] && certs[srcURL.hostname].renewalTimeout) { 679 | safe.clearTimeout(certs[srcURL.hostname].renewalTimeout); 680 | } 681 | delete certs[srcURL.hostname]; 682 | } 683 | } 684 | 685 | this.logger?.info({ from: src, to: target }, 'Unregistered a route'); 686 | } 687 | return this; 688 | } 689 | 690 | private applyResolvers( 691 | resolvers: Resolver[], 692 | host: string, 693 | url: string, 694 | req?: IncomingMessage 695 | ): Promise { 696 | return Promise.all(resolvers.map((resolver) => resolver.fn(host, url, req))); 697 | } 698 | 699 | /** 700 | * Resolves to route 701 | * @param host 702 | * @param url 703 | * @returns {*} 704 | */ 705 | async resolve( 706 | host: string, 707 | url?: string, 708 | req?: IncomingMessage 709 | ): Promise { 710 | try { 711 | host = host.toLowerCase(); 712 | 713 | const resolverResults = await this.applyResolvers(this.resolvers, host, url, req); 714 | 715 | for (let i = 0; i < resolverResults.length; i++) { 716 | const route = resolverResults[i]; 717 | if (route) { 718 | const builtRoute = buildRoute(route); 719 | if (builtRoute) { 720 | // ensure resolved route has path that prefixes URL 721 | // no need to check for native routes. 722 | if ( 723 | !builtRoute.isResolved || 724 | builtRoute.path === '/' || 725 | startsWith(url, builtRoute.path) 726 | ) { 727 | return builtRoute; 728 | } 729 | } 730 | } 731 | } 732 | } catch (err) { 733 | console.error('Resolvers error:', err); 734 | } 735 | } 736 | 737 | async getTarget(src: string, req: IncomingMessage, res?: ServerResponse) { 738 | const url = req.url; 739 | 740 | const route = await this.resolve(src, url, req); 741 | if (!route) { 742 | this.logger?.warn({ src: src, url: url }, 'no valid route found for given source'); 743 | return; 744 | } 745 | 746 | const pathname = route.path; 747 | if (pathname.length > 1) { 748 | // 749 | // remove prefix from src 750 | // 751 | (req)._url = url; // save original url (hacky but works quite well) 752 | req.url = url.substr(pathname.length) || ''; 753 | } 754 | 755 | // 756 | // Perform Round-Robin on the available targets 757 | // TODO: if target errors with EHOSTUNREACH we should skip this 758 | // target and try with another. 759 | // 760 | const urls = route.urls; 761 | const j = route.rr; 762 | route.rr = (j + 1) % urls.length; // get and update Round-robin index. 763 | const target = route.urls[j]; 764 | 765 | // 766 | // Fix request url if targetname specified. 767 | // 768 | if (target.pathname) { 769 | if (req.url) { 770 | req.url = path.posix.join(target.pathname, req.url); 771 | } else { 772 | req.url = target.pathname; 773 | } 774 | } 775 | 776 | // 777 | // Host headers are passed through from the source by default 778 | // Often we want to use the host header of the target instead 779 | // 780 | if (target.useTargetHostHeader === true) { 781 | (req).host = target.host; 782 | } 783 | 784 | if (route.opts?.onRequest) { 785 | const resultFromRequestHandler = route.opts.onRequest(req, res, target); 786 | if (resultFromRequestHandler !== undefined) { 787 | this.logger?.info( 788 | 'Proxying %s received result from onRequest handler, returning.', 789 | src + url 790 | ); 791 | return resultFromRequestHandler; 792 | } 793 | } 794 | 795 | this.logger?.info('Proxying %s to %s', src + url, path.posix.join(target.host, req.url)); 796 | 797 | return target; 798 | } 799 | 800 | getSource(req: IncomingMessage) { 801 | if (this.opts.preferForwardedHost && req.headers['x-forwarded-host']) { 802 | return (req.headers['x-forwarded-host']).split(':')[0]; 803 | } 804 | if (req.headers.host) { 805 | return req.headers.host.split(':')[0]; 806 | } 807 | } 808 | 809 | async close() { 810 | this.proxy.close(); 811 | this.agent && this.agent.destroy(); 812 | 813 | // Clear any renewal timers 814 | if (this.certs) { 815 | Object.keys(this.certs).forEach((domain) => { 816 | const cert = this.certs[domain]; 817 | if (cert && cert.renewalTimeout) { 818 | safe.clearTimeout(cert.renewalTimeout); 819 | cert.renewalTimeout = null; 820 | } 821 | }); 822 | } 823 | 824 | this.letsencryptServer?.close(); 825 | 826 | await Promise.all( 827 | [this.server, this.httpsServer] 828 | .filter((s) => s) 829 | .map((server) => new Promise((resolve) => server.close(resolve))) 830 | ); 831 | } 832 | 833 | // 834 | // Helpers 835 | // 836 | /** 837 | Routing table structure. An object with hostname as key, and an array as value. 838 | The array has one element per path associated to the given hostname. 839 | Every path has a Round-Robin value (rr) and urls array, with all the urls available 840 | for this target route. 841 | 842 | { 843 | hostA : 844 | [ 845 | { 846 | path: '/', 847 | rr: 3, 848 | urls: [] 849 | } 850 | ] 851 | } 852 | */ 853 | 854 | notFound(callback: any) { 855 | if (typeof callback == 'function') { 856 | respondNotFound = callback; 857 | } else { 858 | throw Error('notFound callback is not a function'); 859 | } 860 | } 861 | 862 | shouldRedirectToHttps(target: any) { 863 | return target.sslRedirect && target.host != this.letsencryptHost; 864 | } 865 | } 866 | 867 | // 868 | // Redirect to the HTTPS proxy 869 | // 870 | function redirectToHttps(req: IncomingMessage, res: ServerResponse, ssl: any, log: pino.Logger) { 871 | req.url = (req)._url || req.url; // Get the original url since we are going to redirect. 872 | 873 | const targetPort = ssl.redirectPort || ssl.port; 874 | const hostname = req.headers.host.split(':')[0] + (targetPort ? ':' + targetPort : ''); 875 | const url = 'https://' + path.posix.join(hostname, req.url); 876 | log && log.info('Redirecting %s to %s', path.posix.join(req.headers.host, req.url), url); 877 | // 878 | // We can use 301 for permanent redirect, but its bad for debugging, we may have it as 879 | // a configurable option. 880 | // 881 | res.writeHead(302, { Location: url }); 882 | res.end(); 883 | } 884 | 885 | function prepareUrl(url: string | URL) { 886 | if (isString(url)) { 887 | url = setHttp(url); 888 | 889 | if (!validUrl.isHttpUri(url) && !validUrl.isHttpsUri(url)) { 890 | throw Error(`uri is not a valid http uri ${url}`); 891 | } 892 | 893 | return parseUrl(url); 894 | } 895 | return url; 896 | } 897 | 898 | function getCertData(source: string | Buffer | string[] | Buffer[], unbundle?: boolean): any { 899 | let data: string | undefined; 900 | 901 | // Handle different source types 902 | if (source) { 903 | if (Array.isArray(source)) { 904 | // Recursively process each item in the array and flatten the result 905 | const sources = source; 906 | return sources.map((src) => getCertData(src, unbundle)).flat(); 907 | } else if (Buffer.isBuffer(source)) { 908 | // If source is a buffer, convert to string 909 | data = source.toString('utf8'); 910 | } else if (fs.existsSync(source)) { 911 | // If source is a file path, read the file content 912 | data = fs.readFileSync(source, 'utf8'); 913 | } 914 | } 915 | 916 | // Return unbundled certificate data if required, or raw data 917 | if (data) { 918 | return unbundle ? unbundleCert(data) : data; 919 | } 920 | 921 | return null; // Return null if no valid data is found 922 | } 923 | 924 | /** 925 | Unbundles a file composed of several certificates. 926 | http://www.benjiegillam.com/2012/06/node-dot-js-ssl-certificate-chain/ 927 | */ 928 | function unbundleCert(bundle: string) { 929 | const chain = bundle.trim().split('\n'); 930 | 931 | const ca = []; 932 | const cert = []; 933 | 934 | for (let i = 0, len = chain.length; i < len; i++) { 935 | const line = chain[i].trim(); 936 | if (!(line.length !== 0)) { 937 | continue; 938 | } 939 | cert.push(line); 940 | if (line.match(/-END CERTIFICATE-/)) { 941 | const joined = cert.join('\n'); 942 | ca.push(joined); 943 | //cert = []; 944 | cert.length = 0; 945 | } 946 | } 947 | return ca; 948 | } 949 | 950 | function createCredentialContext(key: string, cert: string, ca: string) { 951 | const opts: { 952 | key?: string; 953 | cert?: string; 954 | ca?: string; 955 | } = {}; 956 | 957 | opts.key = getCertData(key); 958 | opts.cert = getCertData(cert); 959 | if (ca) { 960 | opts.ca = getCertData(ca, true); 961 | } 962 | 963 | const credentials = tls.createSecureContext(opts); 964 | 965 | return credentials.context; 966 | } 967 | 968 | // 969 | // https://stackoverflow.com/questions/18052919/javascript-regular-expression-to-add-protocol-to-url-string/18053700#18053700 970 | // Adds http protocol if non specified. 971 | function setHttp(link: string) { 972 | if (link.search(/^http[s]?\:\/\//) === -1) { 973 | link = 'http://' + link; 974 | } 975 | return link; 976 | } 977 | 978 | let respondNotFound = function (req: IncomingMessage, res: Socket | ServerResponse) { 979 | if (res instanceof ServerResponse) { 980 | res.statusCode = 404; 981 | } 982 | res.write('Not Found'); 983 | res.end(); 984 | }; 985 | 986 | export const buildRoute = function (route: string | ProxyRoute): ProxyRoute | null { 987 | if (!isString(route) && !isObject(route)) { 988 | return null; 989 | } 990 | 991 | if (isObject(route) && route.hasOwnProperty('urls') && route.hasOwnProperty('path')) { 992 | // default route type matched. 993 | return route; 994 | } 995 | 996 | const cacheKey = isString(route) ? route : hash(route); 997 | const entry = routeCache.get(cacheKey) as ProxyRoute; 998 | if (entry) { 999 | return entry; 1000 | } 1001 | 1002 | const routeObject: { 1003 | urls?: any[]; 1004 | path?: string; 1005 | rr: number; 1006 | isResolved: boolean; 1007 | } = { rr: 0, isResolved: true }; 1008 | 1009 | if (isString(route)) { 1010 | routeObject.urls = [buildTarget(route)]; 1011 | routeObject.path = '/'; 1012 | } else { 1013 | if (!route.hasOwnProperty('url')) { 1014 | return null; 1015 | } 1016 | 1017 | routeObject.urls = ( 1018 | Array.isArray((route).url) ? (route).url : [(route).url] 1019 | ).map(function (url: string) { 1020 | return buildTarget(url, (route).opts || {}); 1021 | }); 1022 | 1023 | routeObject.path = (route).path || '/'; 1024 | } 1025 | routeCache.set(cacheKey, routeObject); 1026 | return routeObject; 1027 | }; 1028 | 1029 | export const buildTarget = function ( 1030 | target: string | URL, 1031 | opts?: { ssl?: any; useTargetHostHeader?: boolean } 1032 | ) { 1033 | opts = opts || {}; 1034 | const targetURL = prepareUrl(target); 1035 | 1036 | return { 1037 | ...targetURL, 1038 | sslRedirect: opts.ssl && opts.ssl.redirect !== false, 1039 | useTargetHostHeader: opts.useTargetHostHeader === true, 1040 | }; 1041 | }; 1042 | 1043 | function startsWith(input: string, str: string) { 1044 | return ( 1045 | input.slice(0, str.length) === str && (input.length === str.length || input[str.length] === '/') 1046 | ); 1047 | } 1048 | --------------------------------------------------------------------------------