anvilauth-injector/src/main/java/moe/yushi/authlibinjector/httpd/URLProcessor.java

249 lines
8.5 KiB
Java
Raw Normal View History

2019-01-12 11:58:35 +01:00
/*
2022-04-30 07:27:54 +02:00
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
2019-01-12 11:58:35 +01:00
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
2018-12-29 13:39:49 +01:00
package moe.yushi.authlibinjector.httpd;
import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_TEXT;
2018-12-30 17:52:48 +01:00
import static moe.yushi.authlibinjector.util.IOUtils.transfer;
import static moe.yushi.authlibinjector.util.Logging.log;
import static moe.yushi.authlibinjector.util.Logging.Level.DEBUG;
import static moe.yushi.authlibinjector.util.Logging.Level.INFO;
import static moe.yushi.authlibinjector.util.Logging.Level.WARNING;
2018-12-29 13:39:49 +01:00
import java.io.IOException;
2018-12-30 17:52:48 +01:00
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;
2018-12-29 13:39:49 +01:00
import java.util.List;
2018-12-30 17:52:48 +01:00
import java.util.Map;
import java.util.Map.Entry;
2018-12-29 13:39:49 +01:00
import java.util.Optional;
2018-12-30 17:52:48 +01:00
import java.util.Set;
2018-12-29 13:39:49 +01:00
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import moe.yushi.authlibinjector.Config;
2018-12-30 11:15:48 +01:00
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
2018-12-30 17:52:48 +01:00
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IStatus;
2018-12-30 09:31:08 +01:00
import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD;
2018-12-30 11:15:48 +01:00
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status;
2018-12-29 13:39:49 +01:00
public class URLProcessor {
2020-04-10 13:40:02 +02:00
private static final Pattern URL_REGEX = Pattern.compile("^(?<protocol>https?):\\/\\/(?<domain>[^\\/]+)(?<path>\\/?.*)$");
2018-12-30 17:52:48 +01:00
private static final Pattern LOCAL_URL_REGEX = Pattern.compile("^/(?<protocol>https?)/(?<domain>[^\\/]+)(?<path>\\/.*)$");
2018-12-29 13:39:49 +01:00
private List<URLFilter> filters;
private URLRedirector redirector;
public URLProcessor(List<URLFilter> filters, URLRedirector redirector) {
this.filters = filters;
this.redirector = redirector;
}
/**
* Transforms the input URL(which is grabbed from the bytecode).
*
* If any filter is interested in the URL, the URL will be redirected to the local HTTP server.
* Otherwise, the URLRedirector will be invoked to determine whether the URL should be modified
* and pointed to the customized authentication server.
* If none of above happens, empty is returned.
*
* @return the transformed URL, or empty if it doesn't need to be transformed
*/
2018-12-29 13:39:49 +01:00
public Optional<String> transformURL(String inputUrl) {
2022-05-02 16:30:37 +02:00
if (!inputUrl.startsWith("http")) {
// fast path
return Optional.empty();
}
2018-12-29 13:39:49 +01:00
Matcher matcher = URL_REGEX.matcher(inputUrl);
if (!matcher.find()) {
return Optional.empty();
}
2018-12-30 17:52:48 +01:00
String protocol = matcher.group("protocol");
2018-12-29 13:39:49 +01:00
String domain = matcher.group("domain");
String path = matcher.group("path");
2018-12-30 17:52:48 +01:00
Optional<String> result = transform(protocol, domain, path);
2018-12-29 13:39:49 +01:00
if (result.isPresent()) {
log(DEBUG, "Transformed url [" + inputUrl + "] to [" + result.get() + "]");
2018-12-29 13:39:49 +01:00
}
return result;
}
2018-12-30 17:52:48 +01:00
private Optional<String> transform(String protocol, String domain, String path) {
2018-12-29 13:39:49 +01:00
boolean handleLocally = false;
for (URLFilter filter : filters) {
if (filter.canHandle(domain)) {
2018-12-29 13:39:49 +01:00
handleLocally = true;
break;
}
}
if (handleLocally) {
2018-12-30 17:52:48 +01:00
return Optional.of("http://127.0.0.1:" + getLocalApiPort() + "/" + protocol + "/" + domain + path);
2018-12-29 13:39:49 +01:00
}
return redirector.redirect(domain, path);
}
2022-04-30 07:27:54 +02:00
private DebugApiEndpoint debugApi = new DebugApiEndpoint();
2018-12-29 13:39:49 +01:00
private volatile NanoHTTPD httpd;
private final Object httpdLock = new Object();
private int getLocalApiPort() {
synchronized (httpdLock) {
if (httpd == null) {
httpd = createHttpd();
try {
httpd.start();
} catch (IOException e) {
throw new IllegalStateException("Httpd failed to start");
}
log(INFO, "Httpd is running on port " + httpd.getListeningPort());
2018-12-29 13:39:49 +01:00
}
return httpd.getListeningPort();
}
}
private NanoHTTPD createHttpd() {
return new NanoHTTPD("127.0.0.1", Config.httpdPort) {
2018-12-29 13:39:49 +01:00
@Override
public Response serve(IHTTPSession session) {
2022-04-30 07:27:54 +02:00
if (session.getUri().startsWith("/debug/")) {
return debugApi.serve(session);
}
2018-12-29 13:39:49 +01:00
Matcher matcher = LOCAL_URL_REGEX.matcher(session.getUri());
if (matcher.find()) {
2018-12-30 17:52:48 +01:00
String protocol = matcher.group("protocol");
2018-12-29 13:39:49 +01:00
String domain = matcher.group("domain");
String path = matcher.group("path");
for (URLFilter filter : filters) {
if (filter.canHandle(domain)) {
2018-12-29 13:39:49 +01:00
Optional<Response> result;
try {
result = filter.handle(domain, path, session);
} catch (Throwable e) {
log(WARNING, "An error occurred while processing request [" + session.getUri() + "]", e);
return Response.newFixedLength(Status.INTERNAL_ERROR, CONTENT_TYPE_TEXT, "Internal Server Error");
2018-12-29 13:39:49 +01:00
}
if (result.isPresent()) {
log(DEBUG, "Request to [" + session.getUri() + "] is handled by [" + filter + "]");
2018-12-29 13:39:49 +01:00
return result.get();
}
}
}
2018-12-30 17:52:48 +01:00
String target = redirector.redirect(domain, path)
.orElseGet(() -> protocol + "://" + domain + path);
try {
return reverseProxy(session, target);
} catch (IOException e) {
log(WARNING, "Reverse proxy error", e);
return Response.newFixedLength(Status.BAD_GATEWAY, CONTENT_TYPE_TEXT, "Bad Gateway");
2018-12-30 17:52:48 +01:00
}
} else {
log(DEBUG, "No handler is found for [" + session.getUri() + "]");
return Response.newFixedLength(Status.NOT_FOUND, CONTENT_TYPE_TEXT, "Not Found");
2018-12-29 13:39:49 +01:00
}
2018-12-30 17:52:48 +01:00
}
};
}
private static final Set<String> 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();
2018-12-29 13:39:49 +01:00
2018-12-30 17:52:48 +01:00
String url = session.getQueryParameterString() == null ? upstream : upstream + "?" + session.getQueryParameterString();
2020-08-25 17:29:54 +02:00
Map<String, String> requestHeaders = new LinkedHashMap<>(session.getHeaders());
2018-12-30 17:52:48 +01:00
ignoredHeaders.forEach(requestHeaders::remove);
InputStream clientIn = session.getInputStream();
log(DEBUG, "Reverse proxy: > " + method + " " + url + ", headers: " + requestHeaders);
2018-12-30 17:52:48 +01:00
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod(method);
conn.setDoOutput(clientIn != null);
requestHeaders.forEach(conn::setRequestProperty);
2024-02-17 18:13:27 +01:00
if (clientIn != null && !method.equalsIgnoreCase("GET") && !method.equalsIgnoreCase("HEAD")) {
2018-12-30 17:52:48 +01:00
try (OutputStream upstreamOut = conn.getOutputStream()) {
transfer(clientIn, upstreamOut);
}
}
int responseCode = conn.getResponseCode();
String reponseMessage = conn.getResponseMessage();
Map<String, List<String>> 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();
}
log(DEBUG, "Reverse proxy: < " + responseCode + " " + reponseMessage + " , headers: " + responseHeaders);
2018-12-30 17:52:48 +01:00
IStatus status = new IStatus() {
@Override
public int getRequestStatus() {
return responseCode;
}
@Override
public String getDescription() {
return responseCode + " " + reponseMessage;
2018-12-29 13:39:49 +01:00
}
};
2018-12-30 17:52:48 +01:00
long contentLength = -1;
for (Entry<String, List<String>> header : responseHeaders.entrySet()) {
if ("content-length".equalsIgnoreCase(header.getKey())) {
contentLength = Long.parseLong(header.getValue().get(0));
break;
}
}
2018-12-30 17:52:48 +01:00
Response response;
if (contentLength == -1) {
if (conn.getHeaderField("transfer-encoding") == null) {
// no content
response = Response.newFixedLength(status, null, upstreamIn, 0);
} else {
response = Response.newChunked(status, null, upstreamIn);
}
2018-12-30 17:52:48 +01:00
} else {
response = Response.newFixedLength(status, null, upstreamIn, contentLength);
2018-12-30 17:52:48 +01:00
}
responseHeaders.forEach((name, values) -> values.forEach(value -> response.addHeader(name, value)));
return response;
2018-12-29 13:39:49 +01:00
}
}