package org.openslx.bwlp.sat.web; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.zip.GZIPOutputStream; import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.log4j.Logger; import org.kamranzafar.jtar.TarEntry; import org.kamranzafar.jtar.TarHeader; import org.kamranzafar.jtar.TarOutputStream; import org.openslx.bwlp.sat.database.mappers.DbLecture; import org.openslx.bwlp.sat.database.mappers.DbLecture.LaunchData; import org.openslx.bwlp.sat.database.mappers.DbLecture.RunScript; import org.openslx.bwlp.sat.fileserv.FileServer; import org.openslx.bwlp.thrift.iface.NetRule; import org.openslx.bwlp.thrift.iface.NetShare; import org.openslx.bwlp.thrift.iface.NetShareAuth; import org.openslx.bwlp.thrift.iface.TNotFoundException; import org.openslx.util.GrowingThreadPoolExecutor; import org.openslx.util.Json; import org.openslx.util.Util; import org.simpleframework.xml.Serializer; import org.simpleframework.xml.core.Persister; import fi.iki.elonen.NanoHTTPD; public class WebServer extends NanoHTTPD { private static final Logger LOGGER = Logger.getLogger(WebServer.class); private static final ThreadPoolExecutor tpe = new GrowingThreadPoolExecutor(1, 8, 1, TimeUnit.MINUTES, new LinkedBlockingQueue(16)); private static final Serializer serializer = new Persister(); public WebServer(int port) { super("127.0.0.1", port); super.maxRequestSize = 65535; } /** * Extract request source ip address. Honors the x-forwarded-for header. * * @param headers * map of headers as supplied by nanohttpd * @return IP address, or empty string if unknown */ private String extractIp(Map headers) { if (headers == null || headers.isEmpty()) return ""; String ip; ip = headers.get("remote-addr"); if (ip != null && !ip.equals("127.0.0.1")) return ip; ip = headers.get("x-forwarded-for"); if (ip == null || ip.isEmpty()) return ""; final int i = ip.lastIndexOf(','); if (i == -1) return ip.trim(); return ip.substring(i + 1).trim(); } @Override public Response serve(IHTTPSession session) { String uri = session.getUri(); if (uri == null || uri.length() == 0) { return internalServerError(); } // Sanitize if (uri.contains("//")) { uri = uri.replaceAll("//+", "/"); } try { return handle(session, uri); } catch (Throwable t) { LOGGER.debug("Could not handle request", t); return internalServerError(); } } private Response handle(IHTTPSession session, String uri) { // Our special stuff String[] parts = uri.replaceFirst("^/+", "").split("/+"); // /vmchooser/* if (parts.length > 1 && parts[0].equals("vmchooser")) { if (parts[1].equals("list")) { try { return serveVmChooserList(session.getParms()); } catch (Exception e) { LOGGER.debug("problem while retrieving the vmChooserList", e); return internalServerError(); } } if (parts[1].equals("lecture")) { if (parts.length < 4) return badRequest("Bad Request"); if (parts[3].equals("metadata")) return serveMetaData(parts[2]); if (parts[3].equals("netrules")) return serveLectureNetRules(parts[2]); } return notFound(); } if (uri.startsWith("/status/fileserver")) { return serveStatus(); } if (session.getMethod() == Method.POST && uri.startsWith("/do/")) { try { session.parseBody(null); } catch (IOException | ResponseException e) { LOGGER.debug("could not parse request body", e); return internalServerError(); } return WebRpc.handle(uri.substring(4), session.getParms()); } return notFound(); } private Response serveStatus() { return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "application/json; charset=utf-8", Json.serialize(FileServer.instance().getStatus())); } private static void tarPutFile(TarOutputStream output, String fileName, String data) throws IOException { if (data == null) return; tarPutFile(output, fileName, data.getBytes(StandardCharsets.UTF_8)); } private static void tarPutFile(TarOutputStream output, String fileName, byte[] data) throws IOException { if (data == null) return; output.putNextEntry(new TarEntry(TarHeader.createHeader(fileName, data.length, Util.unixTime(), false, 0644))); output.write(data); } /** * Return meta data (eg. *.vmx) required to start the given lecture. * * @param lectureId * @return */ private Response serveMetaData(final String lectureId) { PipedInputStream sink = new PipedInputStream(10000); try { final TarOutputStream output = new TarOutputStream(new GZIPOutputStream(new PipedOutputStream(sink))); final LaunchData ld; try { ld = DbLecture.getClientLaunchData(lectureId); } catch (TNotFoundException e) { // TODO better virt error handling return notFound(); } catch (SQLException e) { return internalServerError(); } // Meta is required, everything else is optional tpe.execute(new Runnable() { @Override public void run() { try { tarPutFile(output, "vmx", ld.vmx.getFilteredDefinitionArray()); tarPutFile(output, "runscript", ld.legacyRunScript); tarPutFile(output, "netshares", serializeNetShares(ld.netShares)); if (ld.runScript != null) { int cnt = 0; for (RunScript rs : ld.runScript) { tarPutFile(output, String.format("adminrun/%04d-%d-%d.%s", cnt++, rs.visibility, rs.passCreds ? 1 : 0, rs.extension), rs.content); } } } catch (IOException e) { LOGGER.warn("Error writing to tar stream", e); } finally { Util.safeClose(output); } } }); } catch (IOException e1) { LOGGER.warn("Could not create tar output stream", e1); return internalServerError(); } catch (RejectedExecutionException e2) { LOGGER.warn("Server overloaded; rejecting VM Metadata request", e2); return internalServerError(); } return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "application/gzip", sink); } private Response serveLectureNetRules(String lectureId) { List list = new ArrayList<>(); boolean defaultAllowed; try { defaultAllowed = DbLecture.getFirewallRules(lectureId, list); } catch (SQLException e) { return internalServerError(); } catch (TNotFoundException e) { return notFound(); } StringBuilder sb = new StringBuilder(); for (NetRule rule : list) { sb.append(rule.direction.name()); sb.append(' '); sb.append(rule.host); sb.append(' '); sb.append(rule.port); sb.append(' '); sb.append(defaultAllowed ? "REJECT" : "ACCEPT"); sb.append('\n'); } if (defaultAllowed) { sb.append("IN * 0 ACCEPT\n"); sb.append("OUT * 0 ACCEPT\n"); } else { sb.append("IN * 0 REJECT\n"); sb.append("OUT * 0 REJECT\n"); } return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "text/plain; charset=utf-8", sb.toString()); } private String serializeNetShares(List list) { // openslx.exe expects shares in the following format // // letter is either a drive letter for Windows VMs, // or a mount point for Linux VMs. StringBuilder sb = new StringBuilder(); if (!list.isEmpty()) { for (NetShare share : list) { sb.append(share.path); sb.append('\t'); sb.append(share.mountpoint); sb.append('\t'); sb.append(share.displayname); if (share.auth == NetShareAuth.LOGIN_USER) { // TODO how to mark that it should use the logged in user's credentials } if (share.auth == NetShareAuth.OTHER_USER && share.isSetUsername()) { sb.append('\t'); sb.append(share.username); if (share.isSetPassword()) { sb.append('\t'); sb.append(share.password); // TODO fixme } } sb.append("\n"); } } return sb.toString(); } /** * Return full list of lectures matching given location(s). * * @return * @throws Exception */ private Response serveVmChooserList(Map params) throws Exception { String locations = params.get("locations"); boolean exams = params.containsKey("exams"); VmChooserListXml listXml = DbLecture.getUsableListXml(exams, locations); ByteArrayOutputStream baos = new ByteArrayOutputStream(); serializer.write(listXml, baos); return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, "text/xml; charset=utf-8", new ByteArrayInputStream(baos.toByteArray())); } /** * Helper for returning "Internal Server Error" Status * @param body Message */ public static Response internalServerError(String body) { return new NanoHTTPD.Response(NanoHTTPD.Response.Status.INTERNAL_ERROR, "text/plain", body); } public static Response internalServerError() { return internalServerError("Internal Server Error"); } /** * Helper for returning "404 Not Found" Status */ public static Response notFound() { return new NanoHTTPD.Response(NanoHTTPD.Response.Status.NOT_FOUND, "text/plain", "Nicht gefunden!"); } /** * Helper for returning "Bad Request" Status */ public static Response badRequest(String message) { if (message == null) { message = "Schlechte Anfrage!"; } return new NanoHTTPD.Response(NanoHTTPD.Response.Status.BAD_REQUEST, "text/plain", message); } }