package org.rascalmpl.library.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringReader; import java.net.URI; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.rascalmpl.debug.IRascalMonitor; import org.rascalmpl.interpreter.TypeReifier; import org.rascalmpl.interpreter.control_exceptions.Throw; import org.rascalmpl.interpreter.utils.RuntimeExceptionFactory; import org.rascalmpl.library.experiments.Compiler.RVM.Interpreter.ICallableCompiledValue; import org.rascalmpl.library.experiments.Compiler.RVM.Interpreter.RascalExecutionContext; import org.rascalmpl.library.experiments.Compiler.RVM.Interpreter.java2rascal.Java2Rascal; import org.rascalmpl.library.lang.json.io.JsonValueReader; import org.rascalmpl.library.lang.json.io.JsonValueWriter; import org.rascalmpl.library.util.IWebserver.KWRequest; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.value.IBool; import org.rascalmpl.value.IConstructor; import org.rascalmpl.value.IMap; import org.rascalmpl.value.IMapWriter; import org.rascalmpl.value.ISourceLocation; import org.rascalmpl.value.IString; import org.rascalmpl.value.IValue; import org.rascalmpl.value.IValueFactory; import org.rascalmpl.value.IWithKeywordParameters; import org.rascalmpl.value.exceptions.FactTypeUseException; import org.rascalmpl.value.type.Type; import org.rascalmpl.value.type.TypeStore; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import fi.iki.elonen.NanoHTTPD; import fi.iki.elonen.NanoHTTPD.Response.Status; public class WebserverCompiled { private final IValueFactory vf; private final static Map<ISourceLocation, NanoHTTPD> servers = new HashMap<>(); private final static Map<IConstructor,Status> statusValues = new HashMap<>(); private Type requestType; private IWebserver webserver; public WebserverCompiled(IValueFactory vf) { this.vf = vf; } public void serve(ISourceLocation url, final IValue callback, final RascalExecutionContext rex) throws IOException { URI uri = url.getURI(); initMethodAndStatusValues(rex); int port = uri.getPort() != -1 ? uri.getPort() : 80; String host = uri.getHost() != null ? uri.getHost() : "localhost"; host = host.equals("localhost") ? "127.0.0.1" : host; // NanoHttp tries to resolve localhost, which isn't what we want! final ICallableCompiledValue callee = (ICallableCompiledValue) callback; NanoHTTPD server = new NanoHTTPD(host, port) { @Override public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) { try { IConstructor request = makeRequest(uri, method, headers, parms, files); synchronized (callee) { return translateResponse(method, callee.call(new Type[] {requestType}, new IValue[] { request }, null)); } } catch (Throw rascalException) { rex.getStdErr().println(rascalException.getMessage()); return newFixedLengthResponse(Status.INTERNAL_ERROR, MIME_PLAINTEXT, rascalException.getMessage()); } catch (Throwable unexpected) { rex.getStdErr().println(unexpected.getMessage()); unexpected.printStackTrace(rex.getStdErr()); return newFixedLengthResponse(Status.INTERNAL_ERROR, MIME_PLAINTEXT, unexpected.getMessage()); } } private IConstructor makeRequest(String path, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) throws FactTypeUseException, IOException { KWRequest kws = webserver.kw_Request().parameters(makeMap(parms)).uploads(makeMap(files)).headers(makeMap(headers)); switch (method) { case HEAD: return webserver.head(vf.string(path)); case DELETE: return webserver.delete(vf.string(path)); case GET: return webserver.get(vf.string(path), kws); case PUT: return webserver.put(vf.string(path), getContent(files, "content"), kws); case POST: return webserver.put(vf.string(path), getContent(files, "postData"), kws); default: throw new IOException("Unhandled request " + method); } } protected IValue getContent(Map<String, String> parms, String contentParamName) throws IOException { class InlineCallableCompiledValue implements ICallableCompiledValue { @Override public IValue call(IRascalMonitor monitor, Type[] argTypes, IValue[] argValues, Map<String, IValue> keyArgValues) { return call(argTypes, argValues, keyArgValues); } @Override public IValue call(Type[] argTypes, IValue[] argValues, Map<String, IValue> keyArgValues) { try { TypeStore store = new TypeStore(); Type topType = new TypeReifier(vf).valueToType((IConstructor) argValues[0], store); if (topType.isString()) { return vf.string(parms.get(contentParamName)); } else { IValue dtf = keyArgValues.get("dateTimeFormat"); IValue ics = keyArgValues.get("implicitConstructors"); IValue icn = keyArgValues.get("implicitNodes"); return new JsonValueReader(vf, store) .setCalendarFormat((dtf != null) ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setConstructorsAsObjects((ics != null) ? ((IBool) ics).getValue() : true) .setNodesAsObjects((icn != null) ? ((IBool) icn).getValue() : true) .read(new JsonReader(new StringReader(parms.get(contentParamName))), topType); } } catch (IOException e) { throw RuntimeExceptionFactory.io(vf.string(e.getMessage()), null, null); } } }; return (IValue) new InlineCallableCompiledValue(); } private Response translateResponse(Method method, IValue value) throws IOException { IConstructor cons = (IConstructor) value; initMethodAndStatusValues(rex); switch (cons.getName()) { case "fileResponse": return translateFileResponse(method, cons); case "jsonResponse": return translateJsonResponse(method, cons); case "response": return translateTextResponse(method, cons); default: throw new IOException("Unknown response kind: " + value); } } private Response translateJsonResponse(Method method, IConstructor cons) throws IOException { IMap header = (IMap) cons.get("header"); IValue data = cons.get("val"); Status status = translateStatus((IConstructor) cons.get("status")); IWithKeywordParameters<? extends IConstructor> kws = cons.asWithKeywordParameters(); IValue dtf = kws.getParameter("dateTimeFormat"); IValue ics = kws.getParameter("implicitConstructors"); IValue ipn = kws.getParameter("implicitNodes"); IValue dai = kws.getParameter("dateTimeAsInt"); JsonValueWriter writer = new JsonValueWriter() .setCalendarFormat(dtf != null ? ((IString) dtf).getValue() : "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'") .setConstructorsAsObjects(ics != null ? ((IBool) ics).getValue() : true) .setNodesAsObjects(ipn != null ? ((IBool) ipn).getValue() : true) .setDatesAsInt(dai != null ? ((IBool) dai).getValue() : true); try { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); JsonWriter out = new JsonWriter(new OutputStreamWriter(baos, Charset.forName("UTF8"))); writer.write(out, data); out.flush(); out.close(); Response response = newFixedLengthResponse(status, "application/json", new ByteArrayInputStream(baos.toByteArray()), baos.size()); addHeaders(response, header); return response; } catch (IOException e) { // this should not happen in theory throw new RuntimeException("Could not create piped inputstream"); } } private Response translateFileResponse(Method method, IConstructor cons) { ISourceLocation l = (ISourceLocation) cons.get("file"); IString mimeType = (IString) cons.get("mimeType"); IMap header = (IMap) cons.get("header"); Response response; try { response = newChunkedResponse(Status.OK, mimeType.getValue(), URIResolverRegistry.getInstance().getInputStream(l)); addHeaders(response, header); return response; } catch (IOException e) { e.printStackTrace(rex.getStdErr()); return newFixedLengthResponse(Status.NOT_FOUND, "text/plain", l + " not found.\n" + e); } } private Response translateTextResponse(Method method, IConstructor cons) throws IOException { IString mimeType = (IString) cons.get("mimeType"); IMap header = (IMap) cons.get("header"); IString data = (IString) cons.get("content"); Status status = translateStatus((IConstructor) cons.get("status")); if (method != Method.HEAD) { switch (status) { case BAD_REQUEST: case UNAUTHORIZED: case NOT_FOUND: case FORBIDDEN: case RANGE_NOT_SATISFIABLE: case INTERNAL_ERROR: if (data.length() == 0) { data = vf.string(status.getDescription()); } default: break; } } Response response = newFixedLengthResponse(status, mimeType.getValue(), data.getValue()); addHeaders(response, header); return response; } private void addHeaders(Response response, IMap header) { // TODO add first class support for cache control on the Rascal side. For // now we prevent any form of client-side caching with this.. hopefully. response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate"); response.addHeader("Pragma", "no-cache"); response.addHeader("Expires", "0"); for (IValue key : header) { response.addHeader(((IString) key).getValue(), ((IString) header.get(key)).getValue()); } } private Status translateStatus(IConstructor cons) throws IOException { initMethodAndStatusValues(rex); return statusValues.get(cons); } private IMap makeMap(Map<String, String> headers) { IMapWriter writer = vf.mapWriter(); for (Entry<String, String> entry : headers.entrySet()) { writer.put(vf.string(entry.getKey()), vf.string(entry.getValue())); } return writer.done(); } }; try { server.start(); servers.put(url, server); } catch (IOException e) { throw RuntimeExceptionFactory.io(vf.string(e.getMessage()), null, null); } } public void shutdown(ISourceLocation server) { NanoHTTPD nano = servers.get(server); if (nano != null) { //if (nano.isAlive()) { nano.stop(); servers.remove(server); //} } else { throw RuntimeExceptionFactory.illegalArgument(server, null, null, "could not shutdown"); } } @Override protected void finalize() throws Throwable { for (NanoHTTPD server : servers.values()) { if (server != null && server.wasStarted()) { server.stop(); } } } private void initMethodAndStatusValues(final RascalExecutionContext rex) throws IOException { if (statusValues.isEmpty() || requestType == null) { webserver = Java2Rascal.Builder.bridge(vf, rex.getPathConfig(), IWebserver.class).build(); statusValues.put(webserver.ok(), Status.OK); statusValues.put(webserver.created(), Status.CREATED); statusValues.put(webserver.accepted(), Status.ACCEPTED); statusValues.put(webserver.noContent(), Status.NO_CONTENT); statusValues.put(webserver.partialContent(), Status.PARTIAL_CONTENT); statusValues.put(webserver.redirect(), Status.REDIRECT); statusValues.put(webserver.notModified(), Status.NOT_MODIFIED); statusValues.put(webserver.badRequest(), Status.BAD_REQUEST); statusValues.put(webserver.unauthorized(), Status.UNAUTHORIZED); statusValues.put(webserver.forbidden(), Status.FORBIDDEN); statusValues.put(webserver.notFound(), Status.NOT_FOUND); statusValues.put(webserver.rangeNotSatisfiable(), Status.RANGE_NOT_SATISFIABLE); statusValues.put(webserver.internalError(), Status.INTERNAL_ERROR); } } }