Awesome
Proloxy: Prolog Reverse Proxy
Introduction
A reverse proxy relays requests to different web services. The main advantage is isolation of concerns: You can run different and independent web services and serve them under a common umbrella URL.
Proloxy requires SWI-Prolog <b>7.5.8</b> or later.
Project page:
https://www.metalevel.at/proloxy/
Configuration
Proloxy uses an extensible Prolog predicate to relay requests to
different web services: For each arriving HTTP request, Proloxy
calls the predicate request_prefix_target(+Request, -Prefix, -Target)
.
Its arguments are:
Request
is the instantiated HTTP request.Prefix
is prepended to relative paths in HTTP redirects that the target service emits, so that the next client request is again relayed to the intended target service.Target
is the URI of the target service.
You configure Proloxy by providing a Prolog file that contains the
definition of request_prefix_target/3
and any additional predicates
and directives you need. For each web service you want to make
available, add a clause of request_prefix_target/3
to relate an
instantiated HTTP request to a prefix and the
desired target. Each clause may use arbitrary Prolog code to
analyse the request and form the target.
When dispatching an HTTP request, Proloxy considers the clauses of
request_prefix_target/3
in the order they appear in your
configuration file and commits to the first clause that
succeeds. It relays the request to the computed target, and then
sends the target's response to the client.
For example, the following clause relays all requests to a local web server on port 3031, passing along the original request path. The target server can for example host the site's main page, to be used if no other rules apply:
request_prefix_target(Request, '', Target) :-
memberchk(request_uri(URI), Request),
atomic_list_concat(['http://localhost:3031',URI], Target).
config.pl shows a sample configuration file that uses Prolog rules to dispatch requests to two different web services.
Virtual hosts
The rule language is general enough to express virtual hosts. In the case of name-based virtual hosts, this means that you dispatch requests to different web services based on the domain name that is used to access your site.
For example, to dispatch all requests of users who access your server
via your-domain.com
to a web server running on port 4040 (while
leaving the path unchanged), use:
request_prefix_target(Request, '', Target) :-
memberchk(host('your-domain.com'), Request),
memberchk(request_uri(URI), Request),
atomic_list_concat(['http://localhost:4040',URI], Target).
Using this method, you can host multiple domains with a single Proloxy instance, dispatching requests to different underlying services.
Redirections and other replies
You can also use the predicates http_404/2
and http_redirect/3
from
library(http/http_dispatch)
in your configuration files.
For example, the following snippet responds with "HTTP 404
not found" if the URI contains .git
:
:- use_module(library(http/http_dispatch)).
request_prefix_target(Request, _, _) :-
memberchk(request_uri(Path), Request),
sub_atom(Path, _, _, _, '.git/'),
http_404([], Request).
In some cases, it is convenient to respond directly with plain text or
HTML content instead of relaying the request to a different web
service. If a clause of request_prefix_target/3
emits any text on
standout output, then this output is sent to the client as the
HTTP response. Such responses typically start with Content-type: text/plain
(or text/html
), followed by two newlines and
the body of the reply. In rules that emit output, Target
must be the
atom -
to avoid relaying the request to a different service.
Proloxy provides the predicate output_from_process(+Program, +Args)
to emit process output (from stdout
and stderr
) on standard
output. For example, we can configure Proloxy to show the system's
uptime when the URL /uptime
is accessed:
request_prefix_target(Request, _, -) :-
memberchk(request_uri('/uptime'), Request),
format("Content-type: text/plain; charset=utf-8~n~n"),
output_from_process('/usr/bin/uptime', []).
Auxiliary programs and scripts can be conveniently invoked with this method.
Relaying header fields
The extensible predicate transmit_header_field/1
allows you to relay
header fields that the target service emits to the client. The
argument is the name of the header field you want to transmit if it
exists in the target's response. For example, you can put the
following in config.pl
:
transmit_header_field(last_modified).
The name of the header field is matched case-insensitively and
underscore (_
) matches hyphen (-
).
By default, Proloxy does not relay any response header fields.
Here is a recommended configuration:
transmit_header_field(cache_control).
transmit_header_field(expires).
transmit_header_field(last_modified).
transmit_header_field(pragma).
Further, the extensible predicate add_header/1
lets you add custom
header fields to the response. An example is:
add_header(strict_transport_security('max-age=63072000; includeSubdomains')).
This enables HTTP Strict Transport Security (HSTS), which is useful when running HTTPS servers.
Testing the configuration
Since each configuration file is also a valid Prolog program, you can
easily test your configuration. Consulting the Prolog program in
SWI-Prolog lets you detect syntax errors and singleton variables in
your configuration file. To test whether HTTP requests are dispatched
as you intend, query request_prefix_target/3
. For example:
Note that:
- we are using
once/1
to commit to the first clause that succeeds. - we are simulating an actual HTTP request, using a list of header fields.
- the answers tell us how the given HTTP requests are dispatched.
The ability to conveniently test your configuration is a nice property, and a natural consequence of using Prolog as the configuration language. You can also write unit tests for your configuration and therefore easily detect regressions.
Running Proloxy
You can run Proloxy as a Unix daemon. See the SWI-Prolog documentation for invocation options.
In the following, assume that your proxy rules are stored in the file
called config.pl
.
To run Proloxy as a Unix daemon on the standard HTTP port (80) as
user web
, use:
$ sudo swipl config.pl proloxy.pl --user=web
To run the process in the foreground and with a Prolog toplevel, use:
<pre> $ sudo swipl config.pl proloxy.pl --user=web <b>--interactive</b> </pre>You can also use a different port that does not need root privileges:
<pre> $ swipl config.pl proloxy.pl --interactive <b>--port=3040</b> </pre>Launching Proloxy on system startup
proloxy.service is a sample systemd
unit file that runs Proloxy on system startup. Adapt the paths
and options as needed, copy the file to /etc/systemd/system/
and install it using:
$ sudo systemctl enable /etc/systemd/system/proloxy.service
$ sudo systemctl start proloxy.service
Security: Run HTTPS servers
You can run Proloxy as an HTTPS server and thus encrypt traffic for all hosted services at once.
See LetSWICrypt for more information.
A common use case when using HTTPS is to run a second Proloxy instance as a regular HTTP server on port 80 to redirect each request for http://X to https://X. You can do this with the following configuration file for the HTTP server:
:- use_module(library(http/http_dispatch)).
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Redirect each request X to https://X
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
request_prefix_target(Request, _, _) :-
memberchk(request_uri(URI), Request),
memberchk(host(Host), Request),
atomic_list_concat(['https://',Host,URI], Target),
http_redirect(moved, Target, Request).
WebSocket connections
Proloxy supports proxying of WebSocket connections.
As an example, consider making
noVNC available via /vnc/
,
assuming that noVNC listens on port 6080. The following clauses
accomplish the configuration:
request_prefix_target(Request, '/vnc', Target) :-
memberchk(request_uri(URI), Request),
atom_concat('/vnc', Rest, URI),
atomic_list_concat(['http://localhost:6080',Rest], Target).
request_prefix_target(Request, '/websockify', Target) :-
memberchk(request_uri(URI), Request),
atom_concat('/websockify', Rest, URI),
atomic_list_concat(['ws://localhost:6080',Rest], Target).
WebSocket connections are automatically detected via the
Upgrade: websocket
and other header fields.
Serving very large files
In typical use cases, Proloxy relays requests to other web servers, and sends their answers to the client. The overhead is typically negligible, since the other web services usually reside on the same machine.
However, if a web server sends very large files in response to some requests, Proloxy may not have enough global stack space to collect the response.
In such cases, one solution is to configure Proloxy so that such
large files are sent directly by Proloxy, without involving a
different web service. For example, the following snippet
configures Proloxy to directly send any files (such as
ISO images) that are located in /home/web/iso
, and are accessed
via /iso/
.
:- use_module(library(http/http_dispatch)).
request_prefix_target(Request, '', _) :-
memberchk(request_uri(URI), Request),
atom_concat('/iso/', Rest, URI),
http_safe_file(Rest, []),
atom_concat('/home/web/iso', URI, Path),
exists_file(Path),
http_reply_file(Path, [unsafe(true)], Request).