diff --git a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java index 74817b7..7e8005d 100644 --- a/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java +++ b/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java @@ -1,13 +1,26 @@ package moe.yushi.authlibinjector.httpd; +import static moe.yushi.authlibinjector.util.IOUtils.transfer; + import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession; +import moe.yushi.authlibinjector.internal.fi.iki.elonen.IStatus; import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD; import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response; import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status; @@ -15,8 +28,8 @@ import moe.yushi.authlibinjector.util.Logging; public class URLProcessor { - private static final Pattern URL_REGEX = Pattern.compile("^https?:\\/\\/(?[^\\/]+)(?\\/.*)$"); - private static final Pattern LOCAL_URL_REGEX = Pattern.compile("^/(?[^\\/]+)(?\\/.*)$"); + private static final Pattern URL_REGEX = Pattern.compile("^(?https?):\\/\\/(?[^\\/]+)(?\\/.*)$"); + private static final Pattern LOCAL_URL_REGEX = Pattern.compile("^/(?https?)/(?[^\\/]+)(?\\/.*)$"); private List filters; private URLRedirector redirector; @@ -31,17 +44,18 @@ public class URLProcessor { if (!matcher.find()) { return Optional.empty(); } + String protocol = matcher.group("protocol"); String domain = matcher.group("domain"); String path = matcher.group("path"); - Optional result = transform(domain, path); + Optional result = transform(protocol, domain, path); if (result.isPresent()) { Logging.TRANSFORM.fine("Transformed url [" + inputUrl + "] to [" + result.get() + "]"); } return result; } - private Optional transform(String domain, String path) { + private Optional transform(String protocol, String domain, String path) { boolean handleLocally = false; for (URLFilter filter : filters) { if (filter.canHandle(domain, path)) { @@ -51,7 +65,7 @@ public class URLProcessor { } if (handleLocally) { - return Optional.of("http://127.0.0.1:" + getLocalApiPort() + "/" + domain + path); + return Optional.of("http://127.0.0.1:" + getLocalApiPort() + "/" + protocol + "/" + domain + path); } return redirector.redirect(domain, path); @@ -81,6 +95,7 @@ public class URLProcessor { public Response serve(IHTTPSession session) { Matcher matcher = LOCAL_URL_REGEX.matcher(session.getUri()); if (matcher.find()) { + String protocol = matcher.group("protocol"); String domain = matcher.group("domain"); String path = matcher.group("path"); for (URLFilter filter : filters) { @@ -90,7 +105,7 @@ public class URLProcessor { result = filter.handle(domain, path, session); } catch (Throwable e) { Logging.HTTPD.log(Level.WARNING, "An error occurred while processing request [" + session.getUri() + "]", e); - return Response.newFixedLength(Status.INTERNAL_ERROR, null, null); + return Response.newFixedLength(Status.INTERNAL_ERROR, MIME_PLAINTEXT, "Internal Server Error"); } if (result.isPresent()) { @@ -99,11 +114,93 @@ public class URLProcessor { } } } - } - Logging.HTTPD.fine("No handler is found for [" + session.getUri() + "]"); - return Response.newFixedLength(Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found"); + String target = redirector.redirect(domain, path) + .orElseGet(() -> protocol + "://" + domain + path); + try { + return reverseProxy(session, target); + } catch (IOException e) { + Logging.HTTPD.log(Level.WARNING, "Reserve proxy error", e); + return Response.newFixedLength(Status.BAD_GATEWAY, MIME_PLAINTEXT, "Bad Gateway"); + } + } else { + Logging.HTTPD.fine("No handler is found for [" + session.getUri() + "]"); + return Response.newFixedLength(Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found"); + } } }; } + + private static final Set ignoredHeaders = new HashSet<>(Arrays.asList("host", "expect", "connection", "keep-alive", "transfer-encoding")); + + @SuppressWarnings("resource") + private Response reverseProxy(IHTTPSession session, String upstream) throws IOException { + String method = session.getMethod(); + + String url = session.getQueryParameterString() == null ? upstream : upstream + "?" + session.getQueryParameterString(); + + Map requestHeaders = session.getHeaders(); + ignoredHeaders.forEach(requestHeaders::remove); + + InputStream clientIn = session.getInputStream(); + + Logging.HTTPD.fine(() -> "Reserve proxy: > " + method + " " + url + ", headers: " + requestHeaders); + + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod(method); + conn.setDoOutput(clientIn != null); + requestHeaders.forEach(conn::setRequestProperty); + conn.connect(); + + if (clientIn != null) { + try (OutputStream upstreamOut = conn.getOutputStream()) { + transfer(clientIn, upstreamOut); + } + } + + int responseCode = conn.getResponseCode(); + String reponseMessage = conn.getResponseMessage(); + Map> responseHeaders = new LinkedHashMap<>(); + conn.getHeaderFields().forEach((name, values) -> { + if (name != null && !ignoredHeaders.contains(name.toLowerCase())) { + responseHeaders.put(name, values); + } + }); + InputStream upstreamIn; + try { + upstreamIn = conn.getInputStream(); + } catch (IOException e) { + upstreamIn = conn.getErrorStream(); + } + Logging.HTTPD.fine(() -> "Reserve proxy: < " + responseCode + " " + reponseMessage + " , headers: " + responseHeaders); + + IStatus status = new IStatus() { + @Override + public int getRequestStatus() { + return responseCode; + } + + @Override + public String getDescription() { + return responseCode + " " + reponseMessage; + } + }; + + long contentLength = -1; + for (Entry> header : responseHeaders.entrySet()) { + if ("content-length".equalsIgnoreCase(header.getKey())) { + contentLength = Long.parseLong(header.getValue().get(0)); + break; + } + } + Response response; + if (contentLength != -1) { + response = Response.newFixedLength(status, null, upstreamIn, contentLength); + } else { + response = Response.newChunked(status, null, upstreamIn); + } + responseHeaders.forEach((name, values) -> values.forEach(value -> response.addHeader(name, value))); + + return response; + } } diff --git a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java index 55640ad..4d32a14 100644 --- a/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java +++ b/src/main/java/moe/yushi/authlibinjector/internal/fi/iki/elonen/Status.java @@ -45,6 +45,7 @@ public enum Status implements IStatus { INTERNAL_ERROR(500, "Internal Server Error"), NOT_IMPLEMENTED(501, "Not Implemented"), + BAD_GATEWAY(502, "Bad Gateway"), SERVICE_UNAVAILABLE(503, "Service Unavailable"), UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported"); diff --git a/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java b/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java index b8ed853..9f4fc6c 100644 --- a/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java +++ b/src/main/java/moe/yushi/authlibinjector/util/IOUtils.java @@ -40,12 +40,16 @@ public final class IOUtils { public static byte[] asBytes(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); + transfer(in, out); + return out.toByteArray(); + } + + public static void transfer(InputStream from, OutputStream to) throws IOException { byte[] buf = new byte[8192]; int read; - while ((read = in.read(buf)) != -1) { - out.write(buf, 0, read); + while ((read = from.read(buf)) != -1) { + to.write(buf, 0, read); } - return out.toByteArray(); } public static String asString(byte[] bytes) {