Extremely light and powerful Node JS reverse proxy and forward-proxy
- Follows KISS (Keep it simple, stupid) principle, lighter than every other competitors
- Focus on High Performance and High availability
- Easy to fork and modify
- Flexible and easy routing
- Easy to deploy and upgrade without downtime
- Secured by default in a carefully systemd sandbox
- Seamless SSL Support (HTTPS -> HTTP proxy)
- Automatic HTTP to HTTPS redirects
- Works with Let's Encrypt, activate https as soon as a certificate is available
- Multi-thread (cluster by default)
- DNS Failover
- Websockets, HTTP2
- Zero Point of Failure (multi-server master-slave config)
- Zero downtime: register and unregister routes, add certificates without restarting
- Passes tests of heavily used
node-http-proxy
module but without memory leaks and with better performance - Reload-safe: if config file is broken, it keeps previous config in memory and in the disk (zero downtime)
- (TODO) Includes awesome statistics
- top-10 slowest queries
- histogram per hour, per response time
- send real-time statistics to Netdata
GoHA relies on systemd to run with a high level of security and availability.
It provides automatic deployment scripts and a CLI for administration
# Download the binary
wget https://github.com/Ideolys/goha/releases/download/1.0.0-beta.2/goha-linux-1-0-0-beta-2
# To install it, execute the installation command directly from the binary and follow instructions
./goha-linux-1-0-0-beta-2 install
Now GoHA is installed and running. See the Configuration part below to configure the proxy.
By default, the service is installed in
/usr/local/bin/goha
and run withgoha
user. The working directory, where the configuration is stored, is/var/www/goha
. It is possible to overwrite this values with environment variablesGOHA_USER
andGOHA_WORKDIR
.
- For testing purpose
# If you want to just execute "in place" for testing purpose
# Create a working directory
mkdir ~/goha
# Download executable
wget https://github.com/Ideolys/goha/releases/download/1.0.0-beta.2/goha-linux-1-0-0-beta-2
# And start goha directly in the directory (=the working directory)
./goha-linux-1-0-0-beta-2 go
- For development:
# Clone the source code and run:
npm install
# start the proxy locally without systemd
bin/goha go
# tests
npm test
# build binary
npm run build
goha [commands]
# Production commands. For Linux only, GoHA must be installed before:
#
# start : Start the proxy
# stop : Stops the proxy
# reload : Reloads the configuration or upgrade seamlessly (no socket lost, no packet lost)
# log : Shows logs in realtime
# restart : Restarts completely the proxy with service interruption
# --version [-v] : Get current version
# --help [-h] : Show this help
#
# Installation and tests commands:
#
# install : Install the proxy as a systemd service (Linux only)
# Options:
# --non-interactive : install without user interaction
# --no-start : do not start or restart service
# go : Start the proxy without systemd, only for test purpose
#
GoHA can be configured with three methods
- [method 1] : a static JSON file
- [method 2] : an HTTP API, which updates the static JSON file. Available only if the method 1 is used.
- [method 3] : a dynamic Javascript file which build and exports the JSON configuration file.
Configuration files are stored in the working directory in $GOHA_WORKDIR
(/var/www/goha
by default).
Here is the file structure of the working directory:
|- config.json # [method 1] user-defined configuration file
|- config.js # [method 3] user-defined javascript which exports the configuration
|- .config-runtime.json # last valid configuration file currently used in production (DO NOT MODIFY).
|- backup
|- config-20210505121001.json # automatic backup of previous configuration file
|- config-20210603121011.json # GoHA keeps all history for the moment
| middleware
|- loadBalancing.js # (TODO) load balancing middleware functions
|- onRequest.js # (TODO) on request middleware
|- onResponse.js # (TODO) on response middleware
| public
|- 404.html # (TO_IMPROVE) default mainteance or 404 error page
Here are all options available in config.json
{
"port" : 80,
"portSSL" : 443, // GoHA reads Let's Encript certificate automatically in /etc/letsencrypt/live
"portAdmin" : 3000, // REST API to update configuration remotely (method 2) and dashboard + monitoring URLs
"domains" : {
// every request coming to blabla.company.net will be routed to http://100.100.100.100:8101
"blabla.company.net" : "http://100.100.100.100:8101",
"toto.company.net" : "http://100.100.100.101:8102",
// Underscore char can be used to deactivate a redirection wihout removing the line from the JSON [TODO]
"_titi.company.net" : "http://100.100.100.101:8102",
// Add custom headers like x-schema for ideos multitenant
"saas-client-1.company.net" : {
"backends" : [{ "url" : "http://100.100.100.101:8102" }],
"headers" : {"x-schema" : "5000" }
},
// Manage Load balancing
"saas-client-2.company.net" : {
"backends" : [
{ "url" : "http://100.100.10.101:8102" }, // backend server 1
{ "url" : "http://100.100.10.101:8103" }, // backend server 2, ...
{ "url" : "http://100.100.10.100:8103", "isReady" : false }, // if isReady is false, no requests will be sent to this backend
{ "url" : "http://100.100.10.100:8104", "version" : "2" } // routing by version
],
"versioning" : {
"header" : "App-Version", // case-insensitive version header name
"default" : "0", // default version if client has no version header
},
// Default load balancing method used if loadBalanceFn is undefined : Least Connection.
// But you can overwrite it with this optional function.
// For each request, a loop calls this function for each backend where isReady is true
// It sends the request to the backend for which the function returns the lowest number
// Be carefull, this function is stringified so you cannot use variable coming from outside
"loadBalancingFn" : "", // [TODO]
// custom error page if saas-client-2.company.net is not available
"errorPage" : "custom404.html", // [TODO]
// accept traffic coming from this interface only
"listen" : "192.161.1.1" // [TODO]
}
},
// default error page if a website is not available
"errorPage" : "404.html",
// Automatically generate new domains with duplication rules.
// Ex. duplicate all domains URL replacing ".company.net" by ".1.alt.company.com" and ".2.alt.company.com"
"alternativeDomains" : {
".company.net" : [".1.alt.company.com", ".2.alt.company.com"]
}
}
GoHA listens portAdmin
for administration if portAdmin
is defined.
List of APIs:
GET /
: [HTML] show a simple dashboard (TODO)GET /config
: [JSON] get current runtime configPOST /config
: [JSON] overwrite existing configuration. All missing element are deleted⚠️ PUT /config
: [JSON] merge with existing configuration (update and add elements only)GET /status
: [HTML] status of GoHA (TODO)GET /metric
: [HTML] open metrics (TODO)
The configuration sent by API is merged with the existing one. It adds missing domains, modify existing attributes and it does not delete anything
Parameter
The JSON format is exactly the same as the configuration file on the disk.
Body
{
"domains" : {
"blabla.company.net" : "http://100.100.100.100:8101", // add a domain
}
}
Response:
It returns the whole config in "data" attribute
{
"data": {
"domains" : {
"blabla.company.net" : "http://100.100.100.100:8101",
"toto.company.net" : "http://100.100.100.101:8102" // existing config
},
"message" : "success message"
}
The config file can be a dynamic Javascript file. So you can write code to generate a dynamic config file.
This file must export the config object.
let config = {}; // same config as explained earlier
module.exports = config;
⚠️ config.json
is ignored if there is aconfig.js
in the working directly.
The master does little things. Everything is done in workers.
-
The master starts
- It copies
config.json
to.config-runtime.json
if the latter does not exists - It starts workers
- It copies
-
When a worker starts
- It read only
.config-runtime.json
- It read only
-
When the signal to reload the configuration is received
- The master catches the signal
- The master sends a message to one worker to test the new
config.json
- If the worker can read the configuration with success, it sends a "success" signal to the master
- If the master receives the "success" signal, it replaces
.config-runtime.json
by the content ofconfig.json
and creates a backup of the previous config inbackup
directory - The master sends a signal to all workers to really reload the configuration by reading
.config-runtime.json
.config-runtime.json
can be different from config.json
if the latter contains an error and cannot be loaded.
In that case, .config-runtime.json
is used as a backup to run the last working configuration in production (Very useful when the machine restarts).
You can use goha log
to logs in live mode. It is a shortcut of journalctl -n 500 -f -u $GOHA_SERVICE_NAME
Log format : "[TYPE]" "LEVEL" "message"
-
[TYPE]
can be[APP]
: application message, such as config reload, start/stop info[REQ]
: request logs
-
LEVEL
can beINFO
: general info messagesERROR
: error messagesWARN
: carning messages
-
message
can be- for
[APP]
message, general info - for
[REQ]
see format below
- for
GoHA relies on systemd to lotates logs
Logs output (TODO)
[REQ] INFO 192.168.1.1 www.mysite.com http://192.161.1.10:3001 GET /css/main.css unique_request_id
[REQ] INFO 192.168.1.1 www.mysite.com http://192.161.1.10:3001 GET /css/main.css unique_request_id
GoHA can route packets according to version header. Each backend must have a version.
'saas-client-2.company.net' : {
backends : [
{ url : 'http://100.100.10.101:8102', version : '1' },
{ url : 'http://100.100.10.100:8104', version : '2' }, // matches with version '2', '2.0', '2.1.1-beta'
{ url : 'http://100.100.10.100:8105', version : '2.1' }, // matches with version '2.1', '2.1.2', '2.1.2-beta'
{ url : 'http://100.100.10.100:8106', version : '2010-01-01' } // matches with version '2010-01-01', '2010-01-01.1020'
],
versioning : {
header : 'App-Version', // case-insensitive version header name
default : '1' // default version if client has no version header
}
}
If there is a conflicts, GoHA selects the backend which has the longest version string.
In the example above, if the client has a header "App-Version" : "2.1"
, the backend which has the port 8105 is choosen.
GoHA redirects automatically the client to https if there is a corresponding certificate for the domain in /etc/letsencrypt/live
.
GoHA does not generate and renew cerificates himself (For the moment, it must be done manually or in a cron tab).
Let's encrypt does not accept wildcard certificates, so we must generate one certificate per sub-domain/domain.
Let's encrypt cannot generate more than 5 certificates per week (rate limit), so take your time ;).
-
Install Let's encrypt
On ubuntu 16.04 LTS (source : https://certbot.eff.org/#ubuntuxenial-other)
sudo apt-get install software-properties-common sudo add-apt-repository ppa:certbot/certbot sudo apt-get update sudo apt-get install certbot
-
Generate a certificate for one domain
certbot certonly --webroot -w /var/www/goha/public -d failover.myapp.net
When the first certificate is created, change owner of these directory to make it GoHA-accessible:
sudo chown -R ubuntu:ubuntu /etc/letsencrypt/ sudo chown -R ubuntu:ubuntu /var/log/letsencrypt/ sudo chown -R ubuntu:ubuntu /var/lib/letsencrypt/
-
Reload GoHA
goha reload
Each month, renew all certificates in a cron tab with
certbot renew
cd /var/www/goha/
goha reload
https://buzut.net/certbot-challenge-dns-ovh-wildcard/
GoHA can be used for load balancing between many backends servers defined in an array like this:
'saas-client-1.company.net' : {
backends : [
{ url: 'http://100.100.100.10:8102' },
{ url: 'http://100.100.100.101:8102' , isReady : false },// if isReady is false, no requests will be sent to this backend
{ url: 'http://100.100.100.101:8103' } // by default : isReady : true
],
loadBalancingFn : (req, backend) => { return backend.nbConnection }
}
GoHA uses "the least connections" algorithm by default. Read this doc about all possible load balancing algorithms
But you can overwrite with your own function or one of these built-in functions :
/**
* If the request is a GET, it redirects the URL `/render/<base64IPv4` to the
* backend which matches with the <base64IPv4> encoded with Buffer.from('127.0.0.2'.padEnd(15, ' ')).toString('base64');
*/
loadBalancing.selectBackendUsingBase64IP(/^\/render\/(.*)/)
# If the file cert.pem does not exists
( [ ! -f \"./cert.pem\" ]
# And npm install is executed without the option "--production" (used by Gihtub Actions to speed up npm install, avoiding devDeps)
&& [ -z \"$npm_config_production\" ]
)
# Generate a certificate for development
&& openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 -subj \"/C=FR/ST=FR/L=Paris/O=Goha/OU=Goha/CN=localhost/emailAddress=no@goha.io\" -keyout ./privkey.pem -out ./cert.pem
# Else, print a message
|| echo \"Default certificate already generated or production install\" "