NaviServer Programmable Web Server

revproxy(n)

NaviServer Module – 5.0.0a


[ Main Table Of Contents | Table Of Contents | Keyword Index ]

Name

revproxy - Reverse Proxy Module for NaviServer (Version 0.23)

Table Of Contents

Synopsis

Description

The revproxy module implements a reverse proxy for NaviServer. It accepts external HTTP(S) requests and forwards them to one or more internal backend servers, shielding those servers from direct external access. The module also supports proxying of (secure) WebSocket connections.

Use the revproxy module to route requests by HTTP method and URL pattern to create hybrid deployments: serve some paths directly from NaviServer and delegate others to external services. For fine-grained control—such as allowing only authenticated users to reach certain backends—you can invoke revproxy::upstream directly in server-side code (for example, in OpenACS .vuh pages).

NaviServer lets you specify when in the request lifecycle the proxy runs, and how it connects to each backend.

CONFIGURATION

The reverse proxy module is configured by extending your standard NaviServer configuration file. In the “per-backend” style, each backend is defined in its own ns_section. It is also possible to register the calls via ns_registerproc and ns_registerfilter by calling directly in the "ns_param register" section the API revproxy::upstream.

To define sections per backend:

Loading the Module

First, load the revproxy module in your server’s “modules” section:

 ns_section "ns/server/$server/modules" {
   ns_param revproxy tcl
 }

Global Reverse Proxy Settings

Next, set any global defaults for all backends. Here we define the verbosity and the "backendconnection" type. The latter can be defined for individual backends.

 ns_section ns/server/$server/module/revproxy {
   #ns_param verbose 1
   #
   # Register extra handlers or filters. The following command
   # registgered a handler for /nsstats.tcl to obtain stats from the
   # reverse proxy itself, when this URL is used.  This assumes,
   # that nsstats.{tcl,adp} has been installed into the pages
   # directory of the proxy.
 
   ns_param register {
     ns_register_tcl GET /nsstats.tcl
   }
 }

Defining Backends

Each backend requires its own ns_section under the server’s revproxy module. The parameters, which can be set per backend are the same as those, which can be set via the API Options.

In the first example, "backend1" forwards "/api/*" over HTTPS with custom timeouts, and uses the target’s Host header on POST:

 ns_section ns/server/$server/module/revproxy/backend1 {
   # Required: target URL for this backend
   ns_param target               https://server1.local:8080/
 
   # Optional: pick connection methods, timeouts, callbacks, headers
   ns_param connecttimeout         2s                     ;# default: 1s
   ns_param receivetimeout         15s                    ;# default: 10s
   ns_param sendtimeout            15s                    ;# default: 10s
 
   # Define which paths map to this backend
   ns_param map "GET  /api/*"
   ns_param map "POST /api/* {-use_target_host_header true}"
 }

Here, "backend2" handles static files over HTTP with shorter timeouts, and is enabled for GET and HEAD requests:

 ns_section ns/server/$server/module/revproxy/backend2 {
   ns_param target               http://server2.local/
   ns_param connecttimeout       1s
   ns_param receivetimeout       1s
 
   # Define which paths map to this backend
   ns_param map "GET  /static/*"
   ns_param map "HEAD /static/*"
 }

The third backend forwards requests via HTTPS via a "preauth" filter after removing the prefix "/shiny" from the URL.

 ns_section ns/server/$server/module/revproxy/backend3 {
   ns_param target               https://server3.local/
   ns_param backendconnection    preauth
   ns_param regsubs              {{/shiny ""}}
   ns_param map "GET  /shiny/*"
   ns_param map "POST /shiny/*"
 }

Example: Using the API directly to a Register the Reverse Proxy via a Request Handler

Forward GET /doc/* requests to two backend servers with a 20-second connect timeout:

 ns_section ns/server/default/module/revproxy {
   set target {http://srv1.local:8080/ http://srv2.local:8080/}
   ns_param register [subst {
       ns_register_proc GET /doc {
           ::revproxy::upstream proc -connecttimeout 20s -target [list ${target}]
       }
   }]
 }

This configuration uses simple load distribution across multiple backends (round-robin). Failover checks (health checks) for the backends are currently not performed.

Backend Connection Methods

The revproxy module supports three backend connection methods:

  1. ns_connchan (Tcl-based)

    • Fully event-driven, suitable for streaming HTML and WebSockets

    • Supports background operation (does not block connection threads)

    • Proven stability and robustness

    • May be required for requests that cannot currently be handled by ns_http (e.g., certain WebSocket upgrade requests)

    • No persistent connections supported

    • Partial reads/writes and error handling in Tcl (more complex)

  2. ns_http (C-based)

    • Efficient partial reads/writes in C

    • Supports persistent connections (NaviServer 5.0+) for repeated requests

    • Integrates with (multiple) task threads, scaling well under heavy load

    • Provides separate logging and statistics for backend connections

    • For certain types of requests (e.g., the websocket upgrade request), automatic fall back to ns_connchan possible.

    • Not optimal for streaming, since data arrives after full request has finished.

  3. ns_http+ns_connchan (Combined Implementation, default)

    • Uses ns_http to send request data and ns_connchan to spool responses

    • Supports persistent connections and streaming

    • Supports background operation (non‐blocking)

You can set the default connection method globally via the configuration file, and override it per backend via the backendconnection parameter

Figure 1 shows the interaction between a client, the reverse proxy server and the backend server, when the backendconnection is set to ns_http+ns_connchan. The incoming request is always processed by the network driver of NaviServer (nssock or nsssl). After the request is processed, we have the request header in the form of an ns_set and the request body either as spool file or as string. At this time, the URL rewrite callbacj and later the validation callback is fired (see Advanced Customization via API) for more details). The chosen backendconnection defines then, how the request is sent to the backend, and how the results are received and delivered to the client. In the case of ns_http+ns_connchan, the response headers from the backend server are sent the client before the response body is received. Also, the response body is forwarded to the client incrementally.

Reverse Proxy Server with backendconnection ns_http+ns_connchan

Figure 1: Reverse Proxy Server with configuration ns_http+ns_connchan

When the backendconnection is ns_http, forwarding to the client happens after the full request has been received by the reverse proxy. When ns_connchan is used, the transfer is also incrementally, but with the drawbacks mentioned above.

Advanced Customization via API

The core command for proxying is revproxy::upstream, which accepts:

revproxy::upstream when -target value ?-backendconnection value? ?-insecure? ?-connecttimeout value? ?-receivetimeout value? ?-sendtimeout value? ?-timeout value? ?-use_target_host_header true|false? ?-targethost value? ?-regsubs value ...? ?-url_rewrite_callback value? ?-response_header_callback value? ?-backend_response_callback value? ?-exception_callback value? ?-validation_callback value?

This command defines the proxying semantics. It can be registered as a request handler, as a filter, or it can be called programmatically whenever needed. When registered as a filter, the parameter when is automatically added upon invocation. When registered as a request handler proc, provide constant "proc" as first argument.

General Options

-target value

Required. The value is a list of one or more backend URLs. By default, the incoming path is appended before forwarding; use -regsubs or -url_rewrite_callback to alter it.

-backendconnection value

Overrides the global connection method for this invocation.

-insecure

Allow insecure TLS connections; do not verify the server certificate (For details, see ns_http documentation).

Timeout Options

-connecttimeout value

Time allowed to establish a connection (default: 1s).

-receivetimeout value

Time allowed to receive a response (default: 10s).

-sendtimeout value

Time allowed to send the request (default: 10s).

-timeout value

Overall read/write timeout (default: 10s).

Host Header Options

-use_target_host_header true|false

If true, sets the Host header to the backend’s host. By default, the incoming Host header from the client is preserved. The option is a shortcut for -targethost.

-targethost value

Explicit Host header value for backend requests. By default, the incoming Host header from the client is preserved.

URL rewriting and Callbacks

-regsubs value

A list of regsub patterns for simple URL rewriting.

-url_rewrite_callback value

A callback to compute the final backend URL. This callback can be used to dynamically compute the final upstream URL. This can be used in cases, where the option -regsubs is not sufficient. The default implementation is:

 nsf::proc ::revproxy::rewrite_url { -target -url {-query ""} {-fragment ""}} {
   #
   # Transforms an incoming URL and query into a URL used for the
   # backend system.
   #
   # Parameters:
   #   target: backend system (prefix), value provided in the ::revproxy::upstream invocation
   #   url: URL path of the incoming request, as provided via [ns_conn url]
   #   query: query of the incoming request as provided via [ns_conn query]
   #   fragment: fragment of the incoming request as provided via [ns_conn fragment]
   #
   # Join the URL by avoiding double slashes
   #
   set newUrl [string trimright $target /]/[string trimleft $url /.]
   if {$query ne ""} {append newUrl ?$query}
   if {$fragment ne ""} {append newUrl #$fragment}
   return $newUrl
 }
-response_header_callback value

This callback is called, when the header fields of the backend have been received. The callback can be used to modify backend response headers, such as e.g. the "location" header field handling redirects. This callback has no default value. Template for defining the callback:

 nsf::proc ::my_backend_response_callback {-responseHeaders -status -requester -url} {
   #
   # Modify or remove header fields in "responseHeaders" as needed.
   #
   # Parameters:
   #   responseHeaders: ns_set containing the responseHeaders (which
   #              might be altered
   #   status: HTTP status code of the response
   #   requester: contains the location (scheme + authority of RFC 3986),
   #              might be required for redirects to the proxy server.
   #   url: requesting URL
 }
-backend_response_callback value

This callback is called, when the full response of the request was received. In streaming mode, the response might have been also already been transmitted to the client. This callback has no default value. Template for defining the callback:

 nsf::proc ::my_backend_response_callback {-responseHeaders -status -url {response ""}} {
   #
   # Perform actions, when the request was received.
   #
   # response: response dict, will be empty when streaming via ns_connchan.
 }
-exception_callback value

A callback to handle proxy errors, can be used to generate custom error pages on failures. Default: ::revproxy::exception

 nsf::proc ::revproxy::exception {
     {-status 503}
     {-msg ""}
     -error
     -url
     {-frontendChan ""}
 } {
   if {$msg eq ""} {
     ns_log warning "revproxy exception backend with URL '$url' failed with status $status"
     set msg "Backend error: [ns_quotehtml $error]"
   }
   if {$frontendChan ne ""} {
     switch $status {
       502 {set phrase "Bad Gateway"}
       503 {set phrase "Service Unavailable"}
       504 {set phrase "Gateway Timeout"}
       default {set phrase Error}
     }
     ns_connchan write $frontendChan "HTTP/1.0 $status $phrase\r\n\r\n$status $phrase: $url"
   } else {
     ns_returnerror $status $msg
   }
 }
-validation_callback value

A callback to validate or modify the complete request before proxying. This callback receives the -url and -request, and has the complete control over the proxying request. It can still modify the headers, the HTTP method and the request data. The request dict contains keys like headers, binary, and either content or contentfile for uploaded request data. The callback has no default. Example signature:

 nsf::proc ::my_validation_callback {
   -url
   -request
 } {
   # Check final URL and request data provided by the "request" dict.
   # Return unmodified or updated dict, or "" to abort.
   # ....
   return $request
 }

Ensuring Network Drivers are Loaded

To use HTTP or HTTPS in backend connections, ensure the appropriate drivers (nssock for HTTP, nsssl for HTTPS) are loaded. Loading the network drivers entails listening on a port. If you wish to disable a particular listening port (e.g., HTTPS) while still using HTTPS for backend requests, configure the network driverf to listen on port 0:

 ns_section ns/modules {
   ns_param http  nssock
   ns_param https nsssl
 }
 
 ns_section ns/module/https {
   ns_param port 0       ;# disable direct HTTPS listening if desired
 }

Full Configuration Example

Below is a sample configuration file illustrating a possible reverse proxy setup. Save this file (e.g., "/usr/local/ns/conf/nsd-config-revproxy.tcl") and start NaviServer with the appropriate shell/environment variables:

 ########################################################################
 # Sample configuration file for NaviServer with reverse proxy setup.
 ########################################################################
 
 set home [file dirname [file dirname [info nameofexecutable]]]
 
 ########################################################################
 # Per default, the reverse proxy server uses the following
 # configuration variables. Their values can be overloaded on startup
 # via environment variables with the "nsd_" prefix, when starting the
 # server, e.g., setting a different "revproxy_target" via:
 #
 #    nsd_revproxy_target=https://localhost:8445 /usr/local/ns/bin/nsd -f ...
 #
 ########################################################################
 
 dict set defaultConfig httpport                   48000
 dict set defaultConfig httpsport                  48443
 dict set defaultConfig revproxy_target            http://127.0.0.1:8080
 dict set defaultConfig revproxy_backendconnection ns_http+ns_connchan
 dict set defaultConfig address                    0.0.0.0
 
 ns_configure_variables "nsd_" $defaultConfig
 
 ns_section ns/parameters {
   ns_param home       $home
   ns_param tcllibrary tcl
   #ns_param systemlog nsd.log
 }
 
 ns_section ns/servers {
   ns_param default "Reverse proxy"
 }
 
 ns_section ns/modules {
   if {$httpsport ne ""} { ns_param https nsssl }
   if {$httpport ne ""}  { ns_param http nssock }
 }
 
 ns_section ns/module/http {
   ns_param port      $httpport
   ns_param address   $address
   ns_param maxupload 1MB
   ns_param writerthreads 1
 }
 
 ns_section ns/module/https {
   ns_param port         $httpsport
   ns_param address      $address
   ns_param maxupload    1MB
   ns_param writerthreads 1
   ns_param ciphers     "ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!RC4"
   ns_param protocols    "!SSLv2:!SSLv3"
   ns_param certificate  /usr/local/ns/etc/server.pem
 }
 
 ns_section ns/module/http/servers {
   ns_param defaultserver default
   ns_param default       localhost
   ns_param default       [ns_info hostname]
 }
 ns_section ns/module/https/servers {
   ns_param defaultserver default
   ns_param default       [ns_info hostname]
   ns_param default       localhost
 }
 
 ########################################################################
 #  Settings for the "default" server
 ########################################################################
 
 ns_section ns/server/default {
   ns_param connsperthread 1000
   ns_param minthreads      5
   ns_param maxthreads      100
   ns_param maxconnections  100
   ns_param rejectoverrun   true
 }
 
 ns_section ns/server/default/fastpath {
   #
   # From where to serve pages that are not proxied to a backend.
   #
   ns_param pagedir pages-revproxy
 }
 
 ns_section ns/server/default/modules {
   ns_param nslog   nslog
   ns_param revproxy tcl
 }
 
 ns_section ns/server/default/module/revproxy {
   ns_param backendconnection $revproxy_backendconnection
   ns_param verbose 1
   ns_param register {
     ns_register_tcl GET /nsstats.tcl
   }
 }
 
 ns_section ns/server/default/module/revproxy/backend1 {
    ns_param target           $revproxy_target
    ns_param map "GET  /*"
    ns_param map "POST /*"
 }
 ns_section ns/server/default/httpclient {
   #
   # Activate persistent connections for ns_http and request logging (with connection reuse statistics)
   #
   ns_param keepalive 5s
   ns_param logging   on
   ns_param logfile   httpclient-revproxy.log
 }
 
 set ::env(RANDFILE) $home/.rnd
 set ::env(HOME)     $home
 set ::env(LANG)     en_US.UTF-8

Invoke NaviServer with this configuration file, for example:

nsd_revproxy_target=https://localhost:8445 \
    nsd_httpport=48000 \
    /usr/local/ns/bin/nsd -f -u nsadmin -g nsadmin -t /usr/local/ns/conf/nsd-config-revproxy.tcl 2>&1

In general, there are many way how to use the revproxy framework. The following examples shows, how to define a virtual server accepting requests with the host header field cvs.local (e.g., add this domain name as alias to your host and use the name in the URL). By defining the reverse proxy as a virtual server, one can specify different resource limits (number of connection threads, upload limits), different log files, different backend connection methods, etc.

Add this snippet to your NaviServer configuration and adjust the URLs/ports/names according to your needs. As it is, it will use the reverse proxy for HTTP/HTTPS requests addressed to cvs.local to the backend server 127.0.0.1:8060.

 ########################################################################
 # Virtual Server definition for e.g. handling CVS browser
 ########################################################################
 
 ns_section ns/servers {
   ns_param cvs "Reverse proxy to CVS repository"
 }
 ns_section ns/module/http/servers {
   ns_param cvs       cvs.local
 }
 ns_section ns/module/https/servers {
   ns_param cvs       cvs.local
 }
 ns_section ns/server/cvs/httpclient {
   ns_param keepalive 5s       ;# default: 0s
 }
 ns_section ns/server/cvs/modules {
   ns_param revproxy  tcl
 }
 ns_section "ns/server/cvs/module/revproxy" {
    ns_param backendconnection $revproxy_backendconnection
    ns_param verbose  0
 }
 ns_section ns/server/cvs/module/revproxy/backend1 {
    ns_param target   http://127.0.0.1:8060/
    ns_param map "GET  /*"
    ns_param map "POST /*"
 }
}

There are many configuration options possible combining the functionality of a reverse proxy with an application server and a traditional WEB server. One could make the default server the reverse proxy, and use other servers for other purposes. One could also combine mass virtual hosting with the reverse proxy by defining appropriate mapping rules.

REQUIREMENTS

See Also

ns_connchan, ns_http, ns_register

Keywords

HTTP, HTTP-client, HTTPS, configuration, filter, handler, logging, module, nsf, proxy, request, reverse proxy