http-reverse-proxy-ts
v1.0.1
Published
An Http reverse proxy implementation in Typescript
Downloads
49
Maintainers
Readme
HTTP/HTTPS Reverse Proxy
This package implements an http/https reverse proxy. It allows multiple web services/sites to share a single internet interface (router) and share ports 80 and 443 (or any other ports you use). The goal is to provide a secure, easy to implement, proxy for privately hosted servers.
Installation
If you already have nodejs and npm installed then the package can be installed from npm via
npm install --save http-reverse-proxy-ts
Running a simple http reverse proxy server
For this simple test you need a couple of entries in the hosts file:
# add host entries for testing
127.0.0.1 server1.qzqzqz.com
127.0.0.1 server2.qzqzqz.com
A good tutorial for editing the hosts file on most common system types can be found here
The simplest example of using this package is demonstrated by setting up some routes using http.
The index.ts would contain:
TypeScript
import {
HttpReverseProxy,
SimpleHttpServer,
Statistics,
StatisticsServer,
StatisticsServerOptions,
Logger
} from 'http-reverse-proxy-ts'
const stats = new Statistics()
const statisticsServerOptions: StatisticsServerOptions = {
stats: stats,
htmlFilename: './public/statisticsAndLoggingPage.html'
}
const statisticsServer = new StatisticsServer(statisticsServerOptions)
const logger = new Logger()
const server1 = new SimpleHttpServer(1, 8001)
const server2 = new SimpleHttpServer(2, 8002)
server1.start()
server2.start()
const proxy = new HttpReverseProxy({ stats: stats, log: logger })
proxy.addRoute('http://server1.qzqzqz.com', 'localhost:8001')
proxy.addRoute('http://server2.qzqzqz.com', 'localhost:8002')
logger.info(null,'Proxy server started')
The SimpleHTTPServer is a small implementation of a web server; it responds with the server number, hostname and url.
You will need to compile the project. It is recommended for this example that you set the outDir
in the tsconfig.json
file to .
.
Run the project:
node index.js
If you want to view the statistics from the server you will need to copy the the file 'statisticsAndLoggingPage.html' from the public
folder of the package (in node_modules) to a public
folder at the root of your project. The statistics server defaults to localhost:3001.
If you want to view the log from the browser change the line:
const logger = new Logger()
to
const logger = new Logger({port: 3002, logLevel: 10})
This is cause the logger to create an http server at port 3002 which will forward log message to the statistics web page.
In the browser address bar on the same machine type: http://server1.qzqzqz.com
This should bring up the hello message from server1.
Change the address to http://server2.qzqzqz.com and the second server should respond.
You are now running a reverse proxy sharing a single front end ip address (port 80) and routing requests to two applications.
Change the address to http://localhost:3001 to view the statistics.
If you add another route: proxy.addRoute('server1.qzqzqz.com/statistics', 'localhost:3001')
the statistics can be viewed via the address http://server1.qzqzqz.com/statistics.
Running a simple secure HTTPS server
Running an Https server is a bit more complex. The complexity is due to the requirement for certficates
.
Certificates verify the authenticity of the server (usually the domain or host name) and set the groundwork for encrypting the data passing between the browser and the server.
A certificate is only valid if it is backed-up by a trusted certificate authority.
There are a large number of organizations that will grant a certificate for a domain or host name. Most of them charge a fee.
For this example of an https server we will use a self-signed certificate. A self-signed certificate is generated locally and does not have a valid certificate authority backing it up.
There is also free service that will grant certificates if you can prove you own the domain or host name. This
service is called LetsEncrypt
. A later example shows how to set that up.
Typescript
import {
HttpReverseProxyOptions,
HttpReverseProxy,
LetsEncryptSelfSignedOptions,
LetsEncryptUsingSelfSigned,
RouteRegistrationOptions,
SimpleHttpServer,
Statistics,
StatisticsServerOptions,
StatisticsServer,
Logger
} from 'http-reverse-proxy-ts'
const stats = new Statistics()
const logger = new Logger()
const statisticsServerOptions: StatisticsServerOptions = {
stats: stats,
htmlFilename: './public/statisticsAndLoggingPage.html'
}
const letsEncryptServerOptions: LetsEncryptSelfSignedOptions = {
organizationName: 'Self testing',
country: 'US',
state: 'AnyState',
locality: 'AnyTown'
}
const httpReverseProxyOptions: HttpReverseProxyOptions = {
letsEncryptOptions: letsEncryptServerOptions,
httpsOptions: {
port: 443,
certificates: {
certificateStoreRoot: './certificates'
},
},
stats: stats,
log: logger,
}
const routingOptions: RouteRegistrationOptions = {
https: {
redirectToHttps: true,
letsEncrypt: {
email: '[email protected]',
production: false,
}
}
}
const statisticsServer = new StatisticsServer(statisticsServerOptions)
const server1 = new SimpleHttpServer(1, 8001)
const server2 = new SimpleHttpServer(2, 8002)
server1.start()
server2.start()
const proxy = new HttpReverseProxy(httpReverseProxyOptions, LetsEncryptUsingSelfSigned)
proxy.addRoute('https://server1.qzqzqz.com', 'localhost:8001', routingOptions)
proxy.addRoute('https://server2.qzqzqz.com', 'localhost:8002', routingOptions)
logger.info(null, 'Https Reverse Proxy server started')
HttpReverseProxyOptions | LetsEncryptSelfSignedOptions | RouteRegistrationOptions | StatisticsServerOptions
As in the http example we set up two local http servers. These servers do not use https.
The reverse proxy is configured to accept http and https connections. If certificates are required (which will be true the first time the example is run) they will be provided by the Let's Encrypt self signed service which will run locally.
These certificates will will give you a warning in the browser.
The routes are configured to force an http connection from the browser to be redirected to an https connection on the proxy.
The certificates will be stored
in the file system at the location specified by the certificateStoreRoot
. The directory structure for the
certificate store will be:
{certificateStoreRoot}
|
|-> server1_qzqzqz_com
| |
| |- server1_qzqzqz_com-crt.pem
| |- server1_qzqzqz_com-key.pem
|
|-> server2_qzqzqz_com
|
|- server2_qzqzqz_com-crt.pem
|- server2_qzqzqz_com-key.pem
Compile and run the project.
In the browser address bar on the same machine type: http://server1.qzqzqz.com
The browser should be redirected to an https connection. This connection should display an error in the browser stating that the connection is not secure. Select the option to open the page anyway (this varies by prowser).
This should bring up the hello message from server1.
Change the address to http://server2.qzqzqz.com and after being redirected and accepting the insecure certificates the second server should respond.
You are now running a (somewhat) secure reverse proxy sharing a single front end ip address (port 443).
Notice that the servers themselves (localhost:8001 and localhost:8002) are not secure. The packets from the proxy to the local servers are not encrypted. This is fine when the servers are on your local machine or on a local (secure) network. For servers that are outside of your control the connection on the back side should also be secure. See below.
Running a Let's Encrypt secure proxy
To test the retrieval of certificates from Let's Encrypt, you need a hostname directed to your ip-address (the network side of the router). You also need to instruct your router to forward packets arriving on port 80 and port 443 to your local system. A starting point can be found here
You can obtain a temporary host name from sites like no-ip or DysDNS. I am sure there a many others.
Typescript
import {
HttpReverseProxyOptions,
HttpReverseProxy,
LetsEncryptClientOptions,
LetsEncryptUsingAcmeClient,
RouteRegistrationOptions,
SimpleHttpServer,
Statistics,
StatisticsServerOptions,
StatisticsServer,
Logger
} from 'http-reverse-proxy-ts'
const hostname = '<Your Host Name>' // replace this with your actual host name
const stats = new Statistics()
const logger = new Logger()
const statisticsServerOptions: StatisticsServerOptions = {
stats: stats,
htmlFilename: './public/statisticsAndLoggingPage.html'
}
const letsEncryptServerOptions: LetsEncryptClientOptions = {
noVerify: true
}
const httpReverseProxyOptions: HttpReverseProxyOptions = {
letsEncryptOptions: letsEncryptServerOptions,
httpsOptions: {
port: 443,
certificates: {
certificateStoreRoot: './certificates'
},
},
stats: stats,
log: logger,
}
const routingOptions: RouteRegistrationOptions = {
https: {
redirectToHttps: true,
letsEncrypt: {
email: '[email protected]', // This needs a real email address
production: false, // change this to true once testing is complete
}
}
}
const server1 = new SimpleHttpServer(1, 8001)
const server2 = new SimpleHttpServer(2, 8002)
const statisticsServer = new StatisticsServer(statisticsServerOptions)
// @ts-ignore
if ( hostname === '<Your Host Name>'){
logger.error({hostname:hostname}, `hostname in 'letsEncryptHostTestProxy.ts' must be set to your registered host name`)
process.exit(0)
}
server1.start()
server2.start()
const proxy = new HttpReverseProxy(httpReverseProxyOptions, LetsEncryptUsingAcmeClient)
proxy.addRoute(hostname, 'localhost:8001', routingOptions)
proxy.addRoute(hostname, 'localhost:8002', routingOptions) // round robin between servers
logger.info({hostname: hostname}, 'Https Lets Encrypt Test Proxy started')
HttpReverseProxyOptions | LetsEncryptClientOptions | RouteRegistrationOptions | StatisticsServerOptions
Once the ground-work is laid, replace <Your Host Name>
with your registered hostname. Compile and run the project.
The proxy will start and request a certificate from Let's Encrypt for your hostname. This certificate will not be backed by a certificate authority
. However, Once you have verified the system is working you can change the routingOptions.https.letsEncrypt.production to 'true', delete the old certificates and run it again.
In a manner similar to the prior examples, enter your host name into the browser and you should receive a response from Server1 or Server2. If you have not received a producton certificate you will get the same warning as the self signed certificates.
Some modern routers will not allow you to open a page with the web address of the router. This is an attempt to twart a hack called DNS rebinding
. If the browser cannot open the page, try your phone with the wi-fi turned off.
Http Reverse Proxy
This is the class providing the primary interface to the reverse poxy server. Other than the plethora of options it has two main interfaces:
Add Route
addRoute (from: string | Partial<URL>,
to: string | ProxyUrl | (string | ProxyUrl)[],
registrationOptions?: RouteRegistrationOptions): HttpReverseProxy
addRoute() will add a non-duplicate route to the routing server. from
refers to the inbound host and url (the source) and to
refers to the outbound host and url (target). Routes are duplicate if the source host and url are equivalent and the destination host and url are equivalent. The RouteRegistrationOptions are the specifications for this particular route. Adding additional targets to a route will not override the options from the first instantiation of the route.
Remove route
removeRoute (from: string | Partial<URL>,
to?: string | ProxyUrl | (string | ProxyUrl)[]): HttpReverseProxy
removeRoute() will remove one or more routes. If no targets are specified, all of the targets will be removed. When the route has no more targets, it will be removed. RemoveRoute will silently ignore requests to remove a route that does not exist.
Both addRoute and removeRoute can be chained in standard .
notation:
proxy = new HttpReverseProxy ()
.addRoute('server1.qzqzqz.com', 'localhost:8000')
.addRoute('server2.qzqzqz.com', 'localhost:8001')
Statistics
The statistics service will collect runtime statistics for the proxy. For performance considerations the in-memory statistics table is about as simple as possible. Each statistic consists of a name and a count. All counts are updated in place and no history is provided. The Statistics container and Statistics service should be started before the proxy:
const statistics = new Statistics()
const statisticsServerOptions: StatisticsServerOptions = {
stats: statistics,
http: {
port: 3001
},
webSocket: {
interval: 1000
}
}
const statisticsServer = new StatisticsServer(statisticsServerOptions)
const httpProxyOptions: HttpProxyOptions = {
// any http options required
stats: statistics
}
const proxy = new HttpReverseProxy(httpProxyOptions)
// add a route to the proxy to access the statistics server from the outside
proxy.addRoute ('server1.qzqzqz.com/statistics', 'localhost:3001')
This configuration will allow access to the statistics server through 'localhost:3001' or as 'server1.qzqzqz.com/statistics'.
The server does not provide any security.
The Statistics container will collect the statistics as long as the proxy is running.
The server will provide the current state of the statistics table through a webSocket interface. The table is sent as a single object in standard JSON format.
Each key (property) of the object is a measurement point and the value of the property is the current count. The key consists of the workerId (number) followed by a :
followed by the name. The name portion may also contain additional :
characters so splitting out the workderId should be done carefully.
The default web page served by the statistics server is read from ./public/statisticsAndLoggingPage.html
. This default page can be overridden by setting the htmlFilename
in the StatisticsServerOptions.
The default web page is a minimal implementation requiring no outside libraries. It will display the table with a single row for each statistic name and a column for each workerId. WorkerId 0 is the master. In a non-clustered configuration all statistics will be associated with the master.
Logging
The logging component will function in non-clustered and clustered environments without any options.
The options for the Logger are only required if you wish to view the log remotely via a web page or custom application. Or if you what to change the default logging level.
Clustering
The examples given above each run in a single process. This is sufficent for small scale testing.
For a larger production environment the proxy can be run as a cluster. In a cluster a single master process is started with a number of worker processes providing the routing. The master monitors the workers and restarts any worker that exits unexpectedly.
Clustering is enabled by setting the clustered
option of the httpServerOptions to either true
or a number.
If the value is set to true
the master process will start worker processes based on the number of cores in the cpu. You can override this by setting clustered
to a number. The minimum is 2 the maximum is 32.
Clustering should be employed only after the non-clustered router is tested and running properly.
An example clustered server:
import cluster from 'cluster'
import {
HttpReverseProxy,
SimpleHttpServer,
Statistics,
StatisticsServer,
StatisticsServerOptions,
Logger } from 'http-reverse-proxy-ts'
const stats = new Statistics()
let logger: Logger
/**
* In a clustered environment you only want the support services
* running on the master
*/
if (cluster.isMaster) {
const statisticsServerOptions: StatisticsServerOptions = {
stats: stats,
htmlFilename: './public/statisticsAndLoggingPage.html'
}
const server1 = new SimpleHttpServer(1, 8001)
const server2 = new SimpleHttpServer(2, 8002)
const statisticsServer = new StatisticsServer(statisticsServerOptions)
logger = new Logger(
{
port: 3002,
logLevel: 10
}
)
server1.start()
server2.start()
}
else {
logger = new Logger()
}
const proxy = new HttpReverseProxy({ clustered: true, stats: stats, log: logger })
proxy.addRoute('http://server1.test.com', 'localhost:8001')
proxy.addRoute('http://server2.test.com', 'localhost:8002')
logger.info(null, 'Proxy server started')
This example will allow you to view the statistics and log from a single web page similar to the examples above.
Configuration Options
HTTP Server options
interface HTTPReverseProxyOptions {
port?: number
host?: string
proxyOptions?: ExtendedProxyOptions
httpsOptions?: HttpsServerOptions
clustered?: boolean | number
letsEncryptOptions?: BaseLetsEncryptOptions
preferForwardedHost?: boolean,
log?: Logger
stats?: Statistics
}
ExtendedProxyOptions | HttpsServerOptions | BaseLetsEncryptOptions
Option | Type | Default | Description
--- | --- | --- | ---
port | number | 80
| The inbound port used to listen for http connections.
host | network-address | all | The network interface to listen for http connections. Defaults to all interfaces. This would only be used to force the system to listen on a single network. The format is a standard IPV4 or IPV6 network address. This has no relation to a host or hostname in a URL.
proxyOptions | object | See below | Options passed to the node-http-proxy
instance used by this package. A complete list of the options can be found here. Defaults below.
httpsOptions | object | See below | The https interface options.
clustered | boolean or number | false
| If specified the system will run a number of individual monitored proxy processes. The master process will automatically restart any worker process that dies unexpectedly. If this option is a boolean true
the number of worker processes will equal the number of cores on the processor. If this option is a number it is the number of worker processes to start. The minimum is 2 the maximum is 32 and is silently enforced.
letsEncryptOptions | object | See below | The Let's Encrypt server options.
preferForwardedHost | boolean | false
| This is not normally set unless the proxy server is behind other proxies. When true the forwarded host (if one is specified) from the http header is used as the key to the routing table, otherwise it is the host field of the request.
log | object | null
| The logging element
stats | object | null
| An instance of a statitics class
HTTPS server options
interface HttpsServerOptions {
port?: number
certificates: Certificates | CertificateOptions
host?: string
keyFilename?: string
certificateFilename?: string
caFilename?: string
httpsServerOptions?: https.ServerOptions
}
Option | Type | Default | Description
---|---|---|---
port | number | 443
| The inbound port used to listen for https connections
certificates | object | See Below | Certificate object.
host | network-address | http host | The network interface to listen for https connections. This would only be used to force the system to listen on a single network. The format is a standard IPV4 or IPV6 network address. This has no relation to a host or hostname in a URL.
keyFilename | string | null
| Optional path and file name for the default certificate private key. The default certificate is used when a https route does not specify key and certificate files or is not configured to use LetsEncrypt. This should be a PEM encoded private key file.
certificateFilename | string | null
| Optional path and file name for the default certificate file. This should be a PEM encoded certificate file.
caFilename | string | null
| Optional path and file name for the default certificate authority file. This should be a PEM encoded certificate authority file.
httpsServerOptions | object | null
| The set of options as specified by the node https create server found here.
Let's Encrypt Options
interface BaseLetsEncryptOptions {
host?: string
port?: number
certificates?: Certificates
dnsChallenge?: AbstractDNSUpdate
dnsNameServer?: string
log?: Logger
stats?: Statistics
}
Certificates | AbstractDNSUpdate | Logger | Statistics
Option | Type | Default | Description
---|---|---|---
host | string | all | The network interface to listen for http connections. Defaults to all interfaces. This would only be used to force the system to listen on a single network. The format is a standard IPV4 or IPV6 network address. This has no relation to a host or hostname in a URL.
port | number | 3000
| The inbound port used to listen for http connections for the LetsEncrypt local server.
certificate | Certificates | httpOptions.certificates | The certificate store for theLetsEncrypt managed certificates.
dnsChallenge | BaseDNSUpdate | null | For LetsEncrypt registrations that require the use of the dns-01 challenge (i.e. wildcard host names: *.qzqzqz.com) this is the implementation of the DNS challenge handler for the DNS service. If the challenge handler for the DNS service you use is not provided one must be written to access the DNS and add/remove the appropriate DNS TXT record.
dnsNameServer | string | null | After writing the entry to the DNS table, the DNS challenge may verify the entry has been propagated within the cluster of name servers on the service before asking LetsEncrypt to look for it.
log | object | null | The logging element
stats | object | null | the Statistics element
DNS Update Options
The DNS update requires a targeted implementation for each DNS service. The initial release only supports GoDaddy. However, this should provide users with an understanding of the framework required to implement other interfaces.
To facilitate the implementation of other interfaces the DNS update is supported by an abstract base class:
export interface BaseDNSUpdateOptions{
stats?: Statistics
log?:Logger
}
Option | Type | Default | Description ---|---|---|--- stats | Statistics | null | A reference to the Statistics object. log | Logger | null | A reference to a logging object.
The DNS class exposes two abstract methods for managing the update:
abstract async addAcmeChallengeToDNS (domain: string, challenge: string): Promise<boolean>
abstract async removeAcmeChallengeFromDNS (domain: string): Promise<boolean>
addAcmeChallengeToDNS should add a DNS TXT record for the domain.
removeAcmeChallengeFromDNS should remove the TXT record.
The package contains a implementation of the DNS challenge for the GoDaddy DNS service. You can use this as a template for implementing the DNS challenge on other services.
DNS Update Using GoDaddy
The GoDaddy DNS update requires an APIKey and secret. These can be generated via the developer interface on GoDaddy
export interface GoDaddyDNSUpdateOptions extends BaseDNSUpdateOptions {
APIKey: string
secret: string
}
Option | Type | Default | Description ---|---|---|--- APIKey | string | none | The APIKey generated by GoDaddy secret | string | none | The secret generated by GoDaddy. This should not be published in your code.
Route Registration Options
export interface RouteRegistrationOptions {
https?: RegistrationHttpsOptions,
secureOutbound?: boolean
useTargetHostHeader?: boolean
stats?: Statistics
}
Option | Type | Default | Description
---|---|---|---
https | object | null | The specification of the front side (inbound) https connection.
secureOutbound | boolean | false | Specifies the outbound connection should be secure (https) and the credentials should be checked.
useTargetHostHeader | boolean | false | If true and the inbound http packet has an x-forwarded-host
header the first element of the x-forwarded-host header is used as the host name. Otherwise the host header is used. This should be set to true if your proxy sits behind another proxy.
stats | object | null | If not null (or undefined) the Statistics object will be used to keep track of the route statistics.
Route registration https options
export interface RegistrationHttpsOptions {
redirectToHttps: boolean
keyFilename?: string
certificateFilename?: string
caFilename?: string
letsEncrypt?: RegistrationLetsEncryptOptions
}
Option | Type | Default | Description ---|---|---|--- redirectToHttps | boolean | none | If true http connections will be redirected to use the https connection. Otherwise the http connection will will be routed to the specified server. In normal https proxying this should be set to true. keyFilename | string | none | If the host for this route has a commercially generated certificate this should be the path and filename for the private key file for the certificate. This file reference is global. It is not relative to the certificateStoreRoot. certificateFilename | string | none | If the host for this route has a commercially generated certificate this should be the path and filename for the certificate file for the certificate. This file reference is global. It is not relative to the certificateStoreRoot. caFilename | string | none | If the host for this route has a commercially generated certificate this should be the path and filename for the certificate authority file for the certificate. This file reference is global. It is not relative to the certificateStoreRoot. letsEncrypt | object | none | An https route should have either a set of commercial certificate files (keyFilename, certificateFilename) or use letsEncrypt to generate the certificate pair.
Route registration Lets Encrypt options
export interface RegistrationLetsEncryptOptions {
email: string
production?: boolean,
renewWithin?: number,
forceRenew?: boolean
}
Option | Type | Default | Description ---|---|---|--- email | string | none | This is the email address that will be used to set up an account on the LetsEncrypt service. It must be a vaild email address. production | boolean | false | The LetsEncrypt service provides a testing/staging environment which allows you to verify you have everything configured correctly before you request a real certificate. The certificates generated in the staging environment will have the same issues in the browser as the self signed certificates. renewWithin | number | 30 | The LetsEncrypt certificates are valid for 90 days. They need to be renewed periodically. This value is the number of days prior to expiration a new certificate should be requested. forceRenew | boolean | false | If true a new certificate will always be requested at startup.
The proxy maintains timers to re-generate the request for a new certificate before they expire. If the request fails, it will wait a day and try again.
Certificates
certificateStoreRoot: string
log?: Logger
stats?: Statistics
Option | Type | Default | Description ---|---|---|--- certificateStoreRoot | string | none | This is the path (relative or fixed) to the root folder of the certificate files managed by LetsEncrypt. log | object | null | The logging element stats | object | null | The Statistics element
At a minimum this must specify the file path to the root of the certificate store. i.e.:
certificateStoreRoot: '../certificates'
This is the default value used by the https server if none is provided.
Statistics server options
export interface StatisticsServerOptions {
stats: Statistics
noStart?: boolean
htmlFilename?: string
http?: StatisticsServerHttpOptions
websocket?: StatisticsServerWebsocketOptions
}
Option | Type | Default | Description ---|---|---|--- stats | object | none | The instance of the Statistics class maintaining the counts. noStart | boolean | false | When set to true the server must be started manually later in the startup process. htmlFilename | string | ./public/statisticsPage.html | The page served from the Statistics server. http | object | {port: 3001} | The configuration options for the http side of the statistics server. websocket | object | {interval: 5000} | The configuration options for the websocket side of the statistics server.
Statistics server http options
export interface StatisticsServerHttpOptions {
host?: string
port: number
}
Option | Type | Default | Description ---|---|---|--- host | network-interface | all | The network interface. port | number | 3001 | The inbound port used to listen for http connections.
Statistics server websocket options
export interface StatisticsServerWebsocketOptions {
updateInterval?: number
filter?: string[]
}
Option | Type | Default | Description ---|---|---|--- updateInterval | number | 5000 | The interval between updates being pushed from the statistics server to the web client, in milliseconds filter | string[] | none | A set of filters to limit the number of properties sent. Each filter is compared to the start of the name portion of the property. An exact match allows the property to be sent.
Logging server options
export interface LoggingServerOptions {
host?: string
port?: number
htmlFilename?: string
logLevel?: number
}
Option | Type | Default | Description ---|---|---|--- host | network-interface | all | The network interface. port | number | 3002 | The inbound port used to listen for http connections. htmlFilename | string | none | The page served from the Logging server. logLevel | number | 40 | The severity of the type of log message. 10 - debugging and above, 20 - tracing and above, 30 - info and above, 40 warnings and above, 50 - errors and above, 60 - Fatal errors only
Defaults
When no options are passed the following default options are used:
Default http options
port: 80,
proxyOptions: defaultProxyOptions,
httpsOptions: null,
preferForwardedHost: false,
Default proxy options
ntlm: false,
prependPath: false,
secure: true,
Default https options
port: 443,
certificates: {
certificateStoreRoot: '../certificates'
}
Default letsEncrypt options
port: 3000
Default statistics server options
port = 3001
htmlFilename = './public/statisticsAndLoggingPage.html'
updateInterval = 5000
How it works
The server listens on the designated http port. This defaults to port 80.
The server will also listen on the designated https port when it is configured with HttpsServerOptions. The port defaults to 443.
Both servers will listen on all networks if a host
is not specified.
When a request is received on either port, the system will determine the correct outbound server for the request. The process of specifying the relationship between the inbound request and the outbound request is handled via the addRoute method.
Each call to addRoute specifies an inbound host:port/url and an outbound host:port/url. The simplest route would be something like:
proxyServer.addRoute('myserver.mydomain.com', 'localhost:9000')
This would forward http requests with a host header of myserver.mydomain.com
to the http server listening to port 9000 on the local machine. The url from the inbound request would be appended to the outbound request:
myserver.mydomain.com/login => localhost:9000/login
Adding a second route using the same inbound host would cause the proxy server to alternate requests between the servers:
proxyServer.addRoute('myserver.mydomain.com', 'localhost:9001')
This adds a second server (listening on a different port) to handle requests to 'myserver.mydomain.com'. The first request might go to the server on port 9000, the second to the server on port 9001, the third to the server on port 9000, etc.
The round robin forwarding is for each http request, not each page. There are frequently many http requests to retrieve the contents of a single page. There should be no expectation of the server choosen.
Both calls can be combined as follows:
proxyServer.addRoute('myserver.mydomain.com', ['localhost:9000', 'localhost:9001'])
There are practical limits to the number of target servers. A request to add a target server more than once will be silently ignored.
The load or responsibilities of the target servers can also be managed by routing some requests to one server and other requests to a different server:
proxyServer.addRoute('myserver.mydomain.com', 'localhost:9000')
proxyServer.addRoute('myserver.mydomain.com/api', 'localhost:9001')
This configuration would route requests with '/api' as the root of the inbound url to go to the server on port 9001. All other requests would go to the server on port 9000. The url forwarded to the server on port 9001 would have the root ('/api') removed:
myserver.mydomain.com/api/getusers => localhost:9001/getusers
The target server can also specify a base route:
proxyServer.addRoute('myserver.mydomain.com', 'localhost:9001/api')
The resulting requests would have the root ('/api') prepended to the url received by the server at port 9001
myserver.mydomain.com/getusers => localhost:9001/api/getusers
Urls can be on both sides of the route specification:
proxyServer.addRoute('myserver.mydomain.com/api', 'localhost:9001/apihandler')
myserver.mydomain.com/api/getusers => localhost:9001/apihandler/getusers
Acknowledgements
The node team continues to do exceptional work.
The core of the proxy system is provided by http-proxy. It is a solid package.
The acme-client provides the Lets Encrypt interface. Again, this is a solid piece of work.
The certificate and encryption tools are provided by forge. These people are deep in the weeds.
I got the idea for this from redbird. This is a great http reverse proxy implementation. I just wanted one coded in TypeScript.
Then there is TypeScript. If you don't use it, you should try. It attempts to bring the past 50 years of programming language development to the JavaScript world.
I work exclusively in vsCode. Pretty close to perfect.