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();
}
}