scu-dorm-manager / proxy /SpaceProxy.java
cacodex's picture
Upload 15 files
dc611f3 verified
Raw
History Blame Contribute Delete
12.4 kB
package com.example.space;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
public final class SpaceProxy {
private static final int DEFAULT_PORT = 7860;
private static final int BACKEND_PORT = 8080;
private static final String WEB_ROOT = "/app/web";
private static final String BACKEND_CLASSPATH = "/app/backend/classes:/app/backend/lib/*";
public static void main(String[] args) throws Exception {
Process backendProcess = startBackend();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (backendProcess.isAlive()) {
backendProcess.destroy();
}
}));
waitForBackend(backendProcess);
int port = readPort();
HttpServer server = HttpServer.create(new InetSocketAddress("0.0.0.0", port), 0);
server.createContext("/", exchange -> {
String path = normalizePath(exchange.getRequestURI().getRawPath());
if (path.startsWith("/api/")) {
proxyToBackend(exchange);
return;
}
serveStatic(exchange, path);
});
server.setExecutor(Executors.newCachedThreadPool());
server.start();
System.out.println("Space proxy started at http://0.0.0.0:" + port);
backendProcess.onExit().thenRun(() -> {
System.err.println("Backend process exited unexpectedly.");
server.stop(0);
System.exit(1);
});
}
private static Process startBackend() throws IOException {
ProcessBuilder builder = new ProcessBuilder(
"java",
"-cp",
BACKEND_CLASSPATH,
"com.example.dorm.SimpleHttpServer"
);
builder.directory(new File("/app"));
builder.inheritIO();
return builder.start();
}
private static void waitForBackend(Process backendProcess) throws InterruptedException, IOException {
long deadline = System.currentTimeMillis() + 30000;
while (System.currentTimeMillis() < deadline) {
if (!backendProcess.isAlive()) {
throw new IllegalStateException("Backend process exited before it became ready.");
}
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("127.0.0.1", BACKEND_PORT), 500);
return;
} catch (IOException ignored) {
Thread.sleep(250);
}
}
throw new IllegalStateException("Timed out waiting for backend to start.");
}
private static int readPort() {
String portValue = System.getenv("PORT");
if (portValue == null || portValue.trim().isEmpty()) {
return DEFAULT_PORT;
}
try {
return Integer.parseInt(portValue.trim());
} catch (NumberFormatException ex) {
return DEFAULT_PORT;
}
}
private static String normalizePath(String rawPath) {
if (rawPath == null || rawPath.isEmpty()) {
return "/";
}
try {
return URLDecoder.decode(rawPath, StandardCharsets.UTF_8.name());
} catch (Exception ex) {
return rawPath;
}
}
private static void serveStatic(HttpExchange exchange, String path) throws IOException {
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(204, -1);
exchange.close();
return;
}
if (path.equals("/") || path.isEmpty()) {
path = "/index.html";
}
if (path.endsWith("/")) {
path = path + "index.html";
}
Path basePath = Paths.get(WEB_ROOT).toAbsolutePath().normalize();
String relativePath = path.startsWith("/") ? path.substring(1) : path;
Path candidate = basePath.resolve(relativePath).normalize();
if (!candidate.startsWith(basePath) || !Files.isRegularFile(candidate)) {
sendText(exchange, 404, "Not Found");
return;
}
String fileName = candidate.getFileName().toString();
byte[] bytes;
String contentType;
if ("index.html".equalsIgnoreCase(fileName)) {
String html = Files.readString(candidate, StandardCharsets.UTF_8);
html = injectFetchPatch(html);
bytes = html.getBytes(StandardCharsets.UTF_8);
contentType = "text/html; charset=UTF-8";
} else {
bytes = Files.readAllBytes(candidate);
contentType = guessContentType(fileName);
}
exchange.getResponseHeaders().add("Content-Type", contentType);
exchange.sendResponseHeaders(200, bytes.length);
try (OutputStream output = exchange.getResponseBody()) {
output.write(bytes);
}
}
private static String injectFetchPatch(String html) {
String patch = "<script>(function(){var originalFetch=window.fetch;window.fetch=function(input,init){if(typeof input==='string'&&input.indexOf('http://localhost:8080/api')===0){input=input.replace('http://localhost:8080/api','/api');}else if(input&&input.url&&input.url.indexOf('http://localhost:8080/api')===0){input=new Request(input.url.replace('http://localhost:8080/api','/api'),input);}return originalFetch.call(window,input,init);};})();</script>";
int headIndex = html.indexOf("</head>");
if (headIndex < 0) {
return patch + html;
}
return html.substring(0, headIndex) + patch + html.substring(headIndex);
}
private static void proxyToBackend(HttpExchange exchange) throws IOException {
String path = exchange.getRequestURI().getRawPath();
String query = exchange.getRequestURI().getRawQuery();
String target = "http://127.0.0.1:" + BACKEND_PORT + path + (query == null ? "" : "?" + query);
HttpURLConnection connection = (HttpURLConnection) new URL(target).openConnection();
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod(exchange.getRequestMethod());
connection.setConnectTimeout(5000);
connection.setReadTimeout(30000);
connection.setDoInput(true);
copyRequestHeaders(exchange, connection);
byte[] requestBody = readAllBytes(exchange.getRequestBody());
if (requestBody.length > 0 && allowsRequestBody(exchange.getRequestMethod())) {
connection.setDoOutput(true);
try (OutputStream output = connection.getOutputStream()) {
output.write(requestBody);
}
}
int status = connection.getResponseCode();
byte[] responseBody = readAllBytes(status >= 400 ? connection.getErrorStream() : connection.getInputStream());
Headers responseHeaders = exchange.getResponseHeaders();
copyResponseHeaders(connection, responseHeaders);
long responseLength = responseBody == null ? 0 : responseBody.length;
if (status == 204 || status == 304) {
responseLength = -1;
}
exchange.sendResponseHeaders(status, responseLength);
if (responseLength != -1) {
try (OutputStream output = exchange.getResponseBody()) {
if (responseBody != null) {
output.write(responseBody);
}
}
} else {
exchange.getResponseBody().close();
}
}
private static void copyRequestHeaders(HttpExchange exchange, HttpURLConnection connection) {
for (Map.Entry<String, List<String>> entry : exchange.getRequestHeaders().entrySet()) {
String name = entry.getKey();
if (name == null || isHopByHopHeader(name) || "Host".equalsIgnoreCase(name)) {
continue;
}
List<String> values = entry.getValue();
if (values == null || values.isEmpty()) {
continue;
}
connection.setRequestProperty(name, String.join(",", values));
}
}
private static void copyResponseHeaders(HttpURLConnection connection, Headers responseHeaders) {
for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
String name = entry.getKey();
if (name == null || isHopByHopHeader(name) || "Content-Length".equalsIgnoreCase(name)) {
continue;
}
List<String> values = entry.getValue();
if (values == null) {
continue;
}
for (String value : values) {
if (value != null) {
responseHeaders.add(name, value);
}
}
}
}
private static boolean allowsRequestBody(String method) {
return Arrays.asList("POST", "PUT", "PATCH", "DELETE").contains(method.toUpperCase());
}
private static boolean isHopByHopHeader(String name) {
return "Connection".equalsIgnoreCase(name)
|| "Keep-Alive".equalsIgnoreCase(name)
|| "Proxy-Authenticate".equalsIgnoreCase(name)
|| "Proxy-Authorization".equalsIgnoreCase(name)
|| "TE".equalsIgnoreCase(name)
|| "Trailer".equalsIgnoreCase(name)
|| "Transfer-Encoding".equalsIgnoreCase(name)
|| "Upgrade".equalsIgnoreCase(name)
|| "Content-Length".equalsIgnoreCase(name);
}
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
if (inputStream == null) {
return new byte[0];
}
try (InputStream in = inputStream; ByteArrayOutputStream output = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
return output.toByteArray();
}
}
private static void sendText(HttpExchange exchange, int status, String text) throws IOException {
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=UTF-8");
exchange.sendResponseHeaders(status, bytes.length);
try (OutputStream output = exchange.getResponseBody()) {
output.write(bytes);
}
}
private static String guessContentType(String fileName) {
String lowerName = fileName.toLowerCase();
if (lowerName.endsWith(".html") || lowerName.endsWith(".htm")) return "text/html; charset=UTF-8";
if (lowerName.endsWith(".js")) return "application/javascript; charset=UTF-8";
if (lowerName.endsWith(".css")) return "text/css; charset=UTF-8";
if (lowerName.endsWith(".json")) return "application/json; charset=UTF-8";
if (lowerName.endsWith(".txt")) return "text/plain; charset=UTF-8";
if (lowerName.endsWith(".svg")) return "image/svg+xml";
if (lowerName.endsWith(".png")) return "image/png";
if (lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg")) return "image/jpeg";
if (lowerName.endsWith(".gif")) return "image/gif";
if (lowerName.endsWith(".webp")) return "image/webp";
if (lowerName.endsWith(".ico")) return "image/x-icon";
if (lowerName.endsWith(".xlsx")) return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
return "application/octet-stream";
}
}