Murp: the reverse proxy handler
A reverse proxy simply receives requests from a client and forwards them to another server. If you need to proxy requests to other serves, then you may consider using Murp: the Mu Reverse Proxy which is described below.
Step one: add the Murp dependency
<dependency>
<groupId>io.muserver</groupId>
<artifactId>mu-server</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>io.muserver</groupId>
<artifactId>murp</artifactId>
<version>1.1.4</version>
</dependency>
Warning: Murp has a dependency on Jetty Client which brings in some Jetty
dependencies. If your project already has direct or indirect dependencies on Jetty, check
that the versions of the jetty-*
libraries are all the same version.
Step two: create a Mu Server and add the reverse proxy as a handler
Murp does not start a server for you, instead you need to create your own server which allows you to configure
it however you want (e.g. with HTTPS). The reverse proxy is just added like any other handler, which means you
can add handlers before it (perhaps to add or remove custom headers) or after it (if the URI mapping function
returns null
then the next handler will be invoked).
The reverse proxy is configured with the ReverseProxyBuilder which has
only one required configuration option: withUriMapper
which takes a UriMapper.
The URI mapper is how you convert an incoming request URI to the target URI.
In the following example, a target server is started on HTTP which simply replies with the requested URI and headers. The reverse proxy is started on HTTPS and just blindly forwards all requests to the target server.
public class ReverseProxyExample {
public static void main(String[] args) {
// Start a target server which requests will be proxied to
MuServer targetServer = httpServer()
.addHandler((req, resp) -> {
resp.sendChunk("Received " + req + " with headers:\n\n");
req.headers().forEach(e -> resp.sendChunk(e.getKey() + "=" + e.getValue() + "\n"));
return true;
})
.start();
System.out.println("The target server is at " + targetServer.uri());
// Start a reverse proxy that sends all requests to the target server
MuServer reverseProxy = httpsServer()
.addHandler((req, resp) -> {
req.headers().set("X-Send-To-Target", "A value sent to the target");
resp.headers().set("X-Send-To-Client", "A value returned to the client");
return false;
})
.addHandler(
ReverseProxyBuilder.reverseProxy()
.withUriMapper(request -> {
// This maps the client's URI to the target URI, which in this demo
// is simply the target server's URI with the client's path and querystring
// appended.
String pathAndQuery = Murp.pathAndQuery(request.uri());
return targetServer.uri().resolve(pathAndQuery);
})
.sendLegacyForwardedHeaders(true) // Adds X-Forwarded-*
.withViaName("myreverseproxy")
.proxyHostHeader(false)
.addProxyCompleteListener((clientRequest, clientResponse, targetUri, durationMillis) -> {
System.out.println("Proxied " + clientRequest + " to " + targetUri +
" and returned " + clientResponse.status() + " in " + durationMillis + "ms");
})
)
.start();
System.out.println("Reverse proxy started at " + reverseProxy.uri());
System.out.println("Example URLs: " + reverseProxy.uri().resolve("/blah") + " / "
+ reverseProxy.uri().resolve("/blah?greeting=hello%20world"));
}
}
(see full file)
Note that further customization of the proxied request and response can be done by adding listeners before the
request is sent to the target with ReverseProxyBuilder.withRequestInterceptor(RequestInterceptor)
and before the response
is sent to the client with ReverseProxyBuilder.withResponseInterceptor(ResponseInterceptor)
.
The Host header
There are certain headers that reverse proxies do not forward, called Hop by Hop Headers.
However, by default the Host
header is supposed to be proxied from the client to
the target server, which is the default in Murp.
However if proxying requests from the browser to another HTTPS website, you may get errors related to invalid Host headers, or related to SNI. This is because the Host header does not match the header that the target website matches.
In these cases, you can instruct Murp to not forward the Host
header by calling .proxyHostHeader(false)
on the ReverseProxyBuilder.
Timeouts
There are several types of time outs when working with reverse proxies:
- Name resolution - the lookup of the address of the target domain name
- Connection timeout - the time taken to establish a connection to the target
- Idle timeout - after the response has started, this is the amount of time allowed without receiving any bytes from the target. Because there is no way to reliably detect disconnections over HTTP, this value is needed to kill zombie connections.
- Total timeout - the maximum allowed time of the full response.
All of these except the total timeout are defined on the HTTP client. The following example shows how to configure each of them:
public class ReverseProxyTimeoutsExample {
public static void main(String[] args) {
MuServer reverseProxy = httpServer()
.addHandler(
ReverseProxyBuilder.reverseProxy()
.withUriMapper(UriMapper.toDomain(URI.create("http://example.org")))
.proxyHostHeader(false)
.withTotalTimeout(20000) // 20 seconds
.withHttpClient(
ReverseProxyBuilder.createHttpClientBuilder(false)
.connectTimeout(Duration.ofSeconds(15))
.build()
)
)
.start();
System.out.println("Reverse proxy started at " + reverseProxy.uri());
}
}
(see full file)
If a timeout occurs before any headers are sent back to the client, then a 504
status code is sent to the client.