From 5a3946162230444822e6b1e1ec332227bcb83a67 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Mon, 4 Sep 2023 09:53:58 +0200 Subject: [PATCH] Introduce static http handler --- .../src/org/argeo/cms/http/HttpHeader.java | 1 + .../cms/http/server/StaticHttpHandler.java | 177 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 org.argeo.cms/src/org/argeo/cms/http/server/StaticHttpHandler.java diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java b/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java index 91ca1f2af..d23c19d12 100644 --- a/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java +++ b/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java @@ -7,6 +7,7 @@ public enum HttpHeader { ALLOW("Allow"), // VIA("Via"), // CONTENT_TYPE("Content-Type"), // + CONTENT_LENGTH("Content-Length"), // // WebDav DAV("DAV"), // diff --git a/org.argeo.cms/src/org/argeo/cms/http/server/StaticHttpHandler.java b/org.argeo.cms/src/org/argeo/cms/http/server/StaticHttpHandler.java new file mode 100644 index 000000000..49cc242c7 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/http/server/StaticHttpHandler.java @@ -0,0 +1,177 @@ +package org.argeo.cms.http.server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.lang.System.Logger; +import java.lang.System.Logger.Level; +import java.net.FileNameMap; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; + +import org.argeo.cms.acr.ContentUtils; +import org.argeo.cms.http.HttpHeader; +import org.argeo.cms.http.HttpStatus; +import org.argeo.cms.util.StreamUtils; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +/** A simple {@link HttpHandler} which just serves or proxy resources. */ +public class StaticHttpHandler implements HttpHandler { + private final static Logger logger = System.getLogger(StaticHttpHandler.class.getName()); + + private static FileNameMap fileNameMap = URLConnection.getFileNameMap(); + + private NavigableMap binds = new TreeMap<>(); + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String path = HttpServerUtils.subPath(exchange); + Map.Entry bindEntry = findBind(path); + boolean isRoot = "/".equals(bindEntry.getKey()); + + String relPath = isRoot ? path.substring(bindEntry.getKey().length()) + : path.substring(bindEntry.getKey().length() + 1); + process(bindEntry.getValue(), exchange, relPath); + } catch (Exception e) { + logger.log(Level.ERROR, exchange.getRequestURI().toString(), e); + } + } + + public void addBind(String path, Object bind) { + if (binds.containsKey(path)) + throw new IllegalStateException("Path '" + path + "' is already bound"); + Object bindToUse = checkBindSupport(bind); + binds.put(path, bindToUse); + } + + protected Map.Entry findBind(String path) { + Map.Entry entry = binds.floorEntry(path); + if (entry == null) + return null; + String mountPath = entry.getKey(); + if (!path.startsWith(mountPath)) { + // FIXME make it more robust and find when there is no content provider + String[] parent = ContentUtils.getParentPath(path); + return findBind(parent[0]); + } + return entry; + } + + protected void process(Object bind, HttpExchange httpExchange, String relativePath) throws IOException { + OutputStream out = null; + + try { + String contentType = fileNameMap.getContentTypeFor(relativePath); + if (contentType != null) + httpExchange.getResponseHeaders().set(HttpHeader.CONTENT_TYPE.getHeaderName(), contentType); + + if (bind instanceof Path bindPath) { + Path path = bindPath.resolve(relativePath); + if (!Files.exists(path)) { + httpExchange.sendResponseHeaders(HttpStatus.NOT_FOUND.getCode(), -1); + return; + } + long size = Files.size(path); + httpExchange.sendResponseHeaders(HttpStatus.OK.getCode(), size); + out = httpExchange.getResponseBody(); + Files.copy(path, out); + } else if (bind instanceof URL bindUrl) { + URL url = new URL(bindUrl.toString() + relativePath); + URLConnection urlConnection; + try { + urlConnection = url.openConnection(); + urlConnection.connect(); + } catch (IOException e) { + httpExchange.sendResponseHeaders(HttpStatus.NOT_FOUND.getCode(), -1); + return; + } + // TODO check other headers? + // TODO use Proxy? + String contentLengthStr = urlConnection.getHeaderField(HttpHeader.CONTENT_LENGTH.getHeaderName()); + httpExchange.sendResponseHeaders(HttpStatus.OK.getCode(), + contentLengthStr != null ? Long.parseLong(contentLengthStr) : 0); + try (InputStream in = urlConnection.getInputStream()) { + out = httpExchange.getResponseBody(); + StreamUtils.copy(in, out); + } finally { + } + } + // make sure everything is flushed + httpExchange.getResponseBody().flush(); + } catch (RuntimeException e) { + try { + httpExchange.sendResponseHeaders(HttpStatus.INTERNAL_SERVER_ERROR.getCode(), -1); + } catch (IOException e1) { + // silent + } + throw e; + } finally { + if (out != null) { + try { + out.close(); + } catch (IOException e) { + throw e; + } + } + } + } + + /** + * Checks whether this bind type is supported. This can be overridden in order + * to ass new bind type. + * + * @see #process(Object, HttpExchange, String) for overriding the actual + * implementation. + * + * @param bind the bind to check + * @return the bind object to actually use (an URI will have been converted to + * URL) + * @throws UnsupportedOperationException if this bind type is not supported + */ + protected Object checkBindSupport(Object bind) throws UnsupportedOperationException { + if (bind instanceof Path) + return bind; + if (bind instanceof URL) + return bind; + if (bind instanceof URI uri) { + try { + return uri.toURL(); + } catch (MalformedURLException e) { + throw new UnsupportedOperationException("URI " + uri + " cannot be connverted to URL.", e); + } + } + // TODO string as a path within the server? + throw new UnsupportedOperationException("Bind " + bind + " type " + bind.getClass() + " is not supported."); + } + + public static void main(String... args) { + try { + HttpServer httpServer = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 6060), 0); + + StaticHttpHandler staticHttpHandler = new StaticHttpHandler(); + staticHttpHandler.addBind("/", Paths.get("/home/mbaudier/dev/workspaces/test-node-js/test-static")); + staticHttpHandler.addBind("/js", + Paths.get("/home/mbaudier/dev/workspaces/test-node-js/test-static/node_modules")); + + httpServer.createContext("/", staticHttpHandler); + httpServer.start(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} -- 2.30.2