package aQute.http.testservers; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.net.Inet4Address; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import aQute.lib.converter.Converter; import aQute.lib.io.IO; import aQute.lib.json.JSONCodec; public class HttpTestServer implements AutoCloseable, Closeable { private static final JSONCodec json = new JSONCodec(); private final List<Failure> failures = new ArrayList<>(); private Config config; private Server server; public static class Config { public boolean https; public int port; public String host = "localhost"; public Map<String,String> users = new HashMap<>(); public int backlog = 0; public int keysize = 1024; } public static class Request implements Comparable<Request> { public final long time = System.currentTimeMillis(); public String method; public URI uri; public TreeMap<String,String> headers; public Map<String,String> args = new HashMap<>(); public byte[] content; public String ip; @Override public int compareTo(Request o) { return Long.compare(time, o.time); } } public static class Response { public Map<String,String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); public byte[] content; public int code = 200; public String mimeType; public int length = -1; public InputStream stream; } public static class Failure { public Request request; public Response response; public String throwable; public String stackTrace; } public HttpTestServer(Config config) throws Exception { this.config = config == null ? config = new Config() : config; if (config.host == null) config.host = Inet4Address.getLoopbackAddress().getHostAddress(); server = new Server(config); } public void start() throws IOException { setMethodHandlers(); server.start(5000, true); } public InetSocketAddress getAddress() { return new InetSocketAddress(server.getHostname(), server.getListeningPort()); } void setMethodHandlers() { for (final Method m : getClass().getMethods()) { if (m.getName().startsWith("_")) { String path = methodToPath(m); createContext(m, path); } } } public String methodToPath(final Method m) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < m.getName().length(); i++) { char c = m.getName().charAt(i); switch (c) { case '_' : sb.append('/'); break; case '$' : String hex = m.getName().substring(i + 1, i + 3); int charCode = Integer.parseInt(hex, 16); sb.append((char) charCode); i += 2; break; default : sb.append(c); break; } } String path = sb.toString(); if (path.equals("//")) path = "/"; return path; } void createContext(final Method m, String path) { server.createContext(new HttpHandler() { @Override public void handle(String path, Request request, Response response) throws IOException { try { List<Object> args = assign(m, request, response, path); Object[] array = args.toArray(); Object result = m.invoke(HttpTestServer.this, array); if (response.content == null) { if (result == null) { response.content = new byte[0]; response.length = 0; } else if (result instanceof byte[]) { response.content = (byte[]) result; response.mimeType = "application/octet-stream"; } else if (result instanceof InputStream) { response.stream = (InputStream) result; response.mimeType = "application/octet-stream"; } else if (result instanceof String) { String s = (String) result; response.content = s.getBytes(StandardCharsets.UTF_8); response.mimeType = "text/" + (s.startsWith("<") ? "html" : "plain"); } else if (result instanceof File) { response.content = IO.read((File) result); } else { response.content = toJSON(result); response.mimeType = "application/json"; } } } catch (Throwable e) { try { e.printStackTrace(); Failure failure = new Failure(); failure.throwable = e.toString(); failure.request = request; failure.response = response; synchronized (failures) { failures.add(failure); while (failures.size() > 1000) failures.remove(0); } } catch (Exception ee) { throw new RuntimeException(ee); } } } public List<Object> assign(final Method m, Request request, Response response, String path) throws Exception { List<Object> args = new ArrayList<>(); Type[] types = m.getGenericParameterTypes(); int i = 0; if (i < types.length) { if (types[i] == Request.class) { args.add(request); i++; } if (i < types.length) { if (types[i] == Response.class) { args.add(response); i++; } String extraArgs[] = path.isEmpty() ? new String[0] : path.split("/"); int extra = 0; while (i < types.length && extra < extraArgs.length) { Object converted = Converter.cnv(types[i], extraArgs[extra]); args.add(converted); extra++; i++; } } } return args; } }, path); } private byte[] toJSON(Object result) throws Exception { return json.enc().put(result).toString().getBytes("UTF-8"); } public URI getBaseURI() throws URISyntaxException { int port = server.getListeningPort(); StringBuilder sb = new StringBuilder(); if (config.https) { sb.append("https://").append(config.host); if (port != 443) sb.append(":").append(port); } else { sb.append("http://").append(config.host); if (port != 80) sb.append(":").append(port); } return new URI(sb.toString()); } public URI getBaseURI(String path) throws URISyntaxException { while (path.startsWith("/")) path = path.substring(1); return new URI(getBaseURI() + "/" + path); } protected void getResource(Response rsp, String name, String mime) throws IOException { if (getResource(getClass(), rsp, name, mime)) return; if (getClass() != HttpTestServer.class && getResource(HttpTestServer.class, rsp, name, mime)) return; throw new FileNotFoundException(name); } protected boolean getResource(Class< ? > clazz, Response rsp, String name, String mime) throws IOException { try (InputStream in = clazz.getResourceAsStream("www/" + name)) { if (in == null) return false; rsp.content = aQute.lib.io.IO.read(in); rsp.mimeType = mime; return true; } } public List<Failure> getFailuresAndClear() { synchronized (failures) { ArrayList<Failure> tmp = new ArrayList<>(failures); failures.clear(); return tmp; } } @Override public void close() throws IOException { server.closeAllConnections(); } public List<File> getTrustedCertificateFiles(File dir) throws Exception { X509Certificate[] cc = server.getCertificateChain(); if (cc == null) return Collections.emptyList(); List<File> files = new ArrayList<>(); for (X509Certificate c : cc) { File f = aQute.lib.io.IO.createTempFile(dir, "cert", ".cer"); aQute.lib.io.IO.copy(c.getEncoded(), f); files.add(f); } return files; } public X509Certificate[] getCertificateChain() { return server.getCertificateChain(); } }