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.CmsContent; 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 = CmsContent.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); } } }