com.danielflower.apprunner.web.AppReverseProxy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of app-runner Show documentation
Show all versions of app-runner Show documentation
A self-hosted platform-as-a-service that hosts web apps written in Java, Clojure, NodeJS, Python, golang and Scala.
package com.danielflower.apprunner.web;
import io.muserver.*;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.DeferredContentProvider;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
import static java.util.Arrays.asList;
public class AppReverseProxy implements RouteHandler {
private static final Logger log = LoggerFactory.getLogger(AppReverseProxy.class);
private static final Set HOP_BY_HOP_HEADERS = Collections.unmodifiableSet(new HashSet<>(asList(
"keep-alive", "transfer-encoding", "te", "connection", "trailer", "upgrade", "proxy-authorization", "proxy-authenticate")));
private final AtomicLong counter = new AtomicLong();
private final HttpClient httpClient;
private final ProxyMap proxyMap;
private final long totalTimeoutInMillis;
private static final String ipAddress;
static {
String ip;
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
ip = "127.0.0.1";
log.info("Could not fine local address so using " + ip);
}
ipAddress = ip;
}
AppReverseProxy(HttpClient httpClient, ProxyMap proxyMap, long totalTimeoutInMillis) {
this.httpClient = httpClient;
this.proxyMap = proxyMap;
this.totalTimeoutInMillis = totalTimeoutInMillis;
}
@Override
public void handle(MuRequest clientReq, MuResponse clientResp, Map pathParams) throws Exception {
String name = pathParams.get("appName");
URL targetURL = proxyMap.get(name);
if (targetURL == null) {
clientResp.status(404);
clientResp.contentType(ContentTypes.TEXT_HTML);
clientResp.write("404 Not Found
");
return;
}
URI target = targetURL.toURI();
final long start = System.currentTimeMillis();
clientResp.headers().remove(HeaderNames.DATE); // so that the target's date can be used
String qs = clientReq.uri().getRawQuery() == null ? "" : "?" + clientReq.uri().getRawQuery();
URI newTarget = target.resolve(clientReq.uri().getRawPath() + qs);
final AsyncHandle asyncHandle = clientReq.handleAsync();
final long id = counter.incrementAndGet();
log.info("[" + id + "] Proxying from " + clientReq.uri() + " to " + newTarget);
Request targetReq = httpClient.newRequest(newTarget);
targetReq.method(clientReq.method().name());
boolean hasRequestBody = setHeaders(clientReq, targetReq);
if (hasRequestBody) {
DeferredContentProvider targetReqBody = new DeferredContentProvider();
asyncHandle.setReadListener(new RequestBodyListener() {
@Override
public void onDataReceived(ByteBuffer buffer) {
targetReqBody.offer(buffer);
}
@Override
public void onComplete() {
targetReqBody.close();
}
@Override
public void onError(Throwable t) {
targetReqBody.failed(t);
}
});
targetReq.content(targetReqBody);
}
targetReq.onResponseHeaders(response -> {
clientResp.status(response.getStatus());
HttpFields targetRespHeaders = response.getHeaders();
List customHopByHopHeaders = getCustomHopByHopHeaders(targetRespHeaders.get(HttpHeader.CONNECTION));
for (HttpField targetRespHeader : targetRespHeaders) {
String lowerName = targetRespHeader.getName().toLowerCase();
if (HOP_BY_HOP_HEADERS.contains(lowerName) || customHopByHopHeaders.contains(lowerName)) {
continue;
}
String value = targetRespHeader.getValue();
clientResp.headers().add(targetRespHeader.getName(), value);
}
clientResp.headers().set(HeaderNames.VIA, "HTTP/1.1 apprunner");
});
targetReq.onResponseContentAsync((response, content, callback) -> asyncHandle.write(content,
new WriteCallback() {
@Override
public void onFailure(Throwable reason) {
callback.failed(reason);
}
@Override
public void onSuccess() {
callback.succeeded();
}
}));
targetReq.timeout(totalTimeoutInMillis, TimeUnit.MILLISECONDS);
targetReq.send(result -> {
try {
long duration = System.currentTimeMillis() - start;
if (result.isFailed()) {
String errorID = UUID.randomUUID().toString();
log.error("Failed to proxy response. ErrorID=" + errorID + " for " + result, result.getFailure());
if (!clientResp.hasStartedSendingData()) {
clientResp.contentType(ContentTypes.TEXT_HTML);
if (result.getFailure() instanceof TimeoutException) {
clientResp.status(504);
clientResp.write("504 Gateway Timeout
ErrorID=" + errorID + "
");
} else {
clientResp.status(502);
clientResp.write("502 Bad Gateway
ErrorID=" + errorID + "
");
}
}
} else {
log.info("[" + id + "] completed in " + duration + "ms: " + result);
}
} finally {
asyncHandle.complete();
}
});
}
private static boolean setHeaders(MuRequest clientReq, Request targetReq) {
Headers reqHeaders = clientReq.headers();
List customHopByHop = getCustomHopByHopHeaders(reqHeaders.get(HeaderNames.CONNECTION));
boolean hasContentLengthOrTransferEncoding = false;
for (Map.Entry clientHeader : reqHeaders) {
String key = clientHeader.getKey();
String lowKey = key.toLowerCase();
if (HOP_BY_HOP_HEADERS.contains(lowKey) || customHopByHop.contains(lowKey)) {
continue;
}
hasContentLengthOrTransferEncoding |= lowKey.equals("content-length") || lowKey.equals("transfer-encoding");
targetReq.header(key, clientHeader.getValue());
}
String proto = clientReq.uri().getScheme();
String originHost = clientReq.uri().getAuthority();
targetReq.header(HttpHeader.VIA, "HTTP/1.1 apprunner");
targetReq.header(HttpHeader.X_FORWARDED_PROTO, proto);
targetReq.header(HttpHeader.X_FORWARDED_HOST, originHost);
targetReq.header(HttpHeader.X_FORWARDED_SERVER, ipAddress);
String forwardedFor = clientReq.remoteAddress();
targetReq.header(HttpHeader.X_FORWARDED_FOR, forwardedFor);
targetReq.header(HttpHeader.FORWARDED, "by=" + ipAddress + "; for=" + forwardedFor + "; host=" + originHost + "; proto=" + proto);
return hasContentLengthOrTransferEncoding;
}
private static List getCustomHopByHopHeaders(String connectionHeaderValue) {
if (connectionHeaderValue == null) {
return Collections.emptyList();
}
List customHopByHop = new ArrayList<>();
String[] split = connectionHeaderValue.split(" *, *");
for (String s : split) {
customHopByHop.add(s.toLowerCase());
}
return customHopByHop;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy