package jp.vmi.selenium.testutils; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executors; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.text.StrLookup; import org.apache.commons.lang3.text.StrSubstitutor; import org.openqa.selenium.net.PortProber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.sun.net.httpserver.Headers; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; /** * Webserver for unit test. */ @SuppressWarnings("restriction") public class WebServer { private static final Logger log = LoggerFactory.getLogger(WebServer.class); private static String urlDecode(String s) { try { return URLDecoder.decode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { return ""; } } private static void parseQuery(Map<String, String> params, String queryString) { if (StringUtils.isEmpty(queryString)) return; for (String entryString : queryString.split("&")) { String[] pair = entryString.split("=", 2); String key = urlDecode(pair[0]); String value = (pair.length == 2) ? urlDecode(pair[1]) : ""; params.put(key, value); } } private static String getContentType(Path path) { String ext = FilenameUtils.getExtension(path.toString()).toLowerCase(); switch (ext) { case "html": return "text/html; charset=UTF-8"; case "js": return "application/javascript"; case "jpg": ext = "jpeg"; // fall through case "jpeg": case "png": return "image/" + ext; case "ico": return "image/vnd.microsoft.icon"; default: throw new UnsupportedOperationException(path.toString()); } } private final int port; private final String htdocs; private String fqdn = "localhost"; private HttpServer server = null; private Thread shutdownHook = null; /** * Constructor. * * This use free port. */ public WebServer() { this(PortProber.findFreePort(), null); } /** * Constructor. * * @param port port. * @param htdocs document root. */ public WebServer(int port, String htdocs) { this.port = port; this.htdocs = htdocs != null ? htdocs : WebServer.class.getResource("/htdocs").getFile(); } private static class HttpErrorException extends Exception { private static final long serialVersionUID = 1L; public final Status status; public HttpErrorException(Status status) { this.status = status; } } private static enum Status { OK(200, "OK"), NOT_FOUND(403, "Not Found"), FORBIDDEN(404, "Forbidden"), INTERNAL_SERVER_ERROR(500, "Internal Server Error"), ; private final int code; private final String message; private Status(int status, String message) { this.code = status; this.message = message; } @Override public String toString() { return code + " " + message; } } private static class Content { public Status status; public String type; public byte[] body; public Content(Status status, String message) { this.status = status; this.type = "text/html; charset=UTF-8"; this.body = message.getBytes(StandardCharsets.UTF_8); } public Content(String type) { this.status = Status.OK; this.type = type; } } private static class SimpleTemplateHandler implements HttpHandler { private static final String WAIT = "wait"; private final String htdocs; public SimpleTemplateHandler(String htdocs) { this.htdocs = htdocs; } private static Content handleContent(Path path, final Map<String, String> params) throws HttpErrorException { try { Content content = new Content(getContentType(path)); if (content.type.startsWith("text/html")) { String tmpl = IOUtils.toString(Files.newInputStream(path), StandardCharsets.UTF_8); content.body = new StrSubstitutor(new StrLookup<String>() { @Override public String lookup(String key) { String value = params.get(key); return value != null ? StringEscapeUtils.escapeHtml4(value) : ""; } }).replace(tmpl).getBytes(StandardCharsets.UTF_8); } else { content.body = Files.readAllBytes(path); } if (params.containsKey(WAIT)) { int wait = NumberUtils.toInt(params.get(WAIT)); if (wait > 0) { try { log.debug("[{}] Waiting {} sec.", Thread.currentThread().getName(), wait); Thread.sleep(wait * 1000); } catch (InterruptedException e) { // no operation. } } } return content; } catch (FileNotFoundException e) { throw new HttpErrorException(Status.NOT_FOUND); } catch (IOException e) { throw new HttpErrorException(Status.INTERNAL_SERVER_ERROR); } } private static Content dirList(Path path, String name) { String escName = StringEscapeUtils.escapeHtml4(name); StringBuilder html = new StringBuilder("<!DOCTYPE html>" + "<html>" + "<head>" + "<title>") .append(escName) .append("</title>" + "</head>" + "<body>" + "<h1>") .append(escName) .append("</h1>" + "<hr>" + "<ul>"); String[] filenames = path.toFile().list(); for (String filename : filenames) { String escFilename = StringEscapeUtils.escapeHtml4(filename); html.append("<li><a href=\"").append(escFilename).append("\">") .append(escFilename) .append("</a></li>"); } html.append("<hr>" + "</body>" + "</html>"); return new Content(Status.OK, html.toString()); } private Content handlePath(HttpExchange he) throws HttpErrorException { URI uri = he.getRequestURI(); String uriPath = uri.getPath(); if (uriPath.matches("\\\\|//|/\\.|\\.\\.")) throw new HttpErrorException(Status.FORBIDDEN); Map<String, String> params = new HashMap<>(); try { String postData = IOUtils.toString(he.getRequestBody(), StandardCharsets.UTF_8); parseQuery(params, postData); } catch (IOException e) { // no operation. } parseQuery(params, uri.getRawQuery()); Path path = Paths.get(htdocs, uriPath); if (Files.isRegularFile(path)) { return handleContent(path, params); } else if (Files.isDirectory(path)) { Path index = path.resolve("index.html"); if (Files.isRegularFile(index)) return handleContent(index, params); else return dirList(path, uri.getPath()); } else { throw new HttpErrorException(Status.NOT_FOUND); } } private Content handleError(Status status) { return new Content(status, "<!DOCTYPE html><html><head><title>" + status + "</title></head>" + "<body>" + status + "</body></html>"); } @Override public void handle(HttpExchange he) throws IOException { Content content; if (he.getRemoteAddress().getAddress().isLoopbackAddress()) { try { content = handlePath(he); } catch (HttpErrorException e) { content = handleError(e.status); } } else { content = handleError(Status.FORBIDDEN); } Headers headers = he.getResponseHeaders(); headers.put("Content-Type", Arrays.asList(content.type)); he.sendResponseHeaders(content.status.code, content.body.length); try (OutputStream os = he.getResponseBody()) { os.write(content.body); } log.debug("[{}] {} {} [{}] {}", Thread.currentThread().getName(), he.getRequestMethod(), he.getRequestURI(), content.status, content.type); } } /** * Start web server. */ public synchronized void start() { InetSocketAddress sock = new InetSocketAddress(fqdn, port); try { server = HttpServer.create(sock, 0); } catch (IOException e) { throw new RuntimeException(e); } server.setExecutor(Executors.newCachedThreadPool()); server.createContext("/", new SimpleTemplateHandler(htdocs)); //server.createContext("/basic", new BasicAuthenticationHandler(new InMemoryPasswords().add("user", "pass"))); //server.createContext("/redirect", new RedirectHandler("http://" + getServerNameString() + "/index.html")); //server.createContext("/basic/redirect", new RedirectHandler("http://" + getServerNameString() + "/basic/index.html")); shutdownHook = new Thread(new Runnable() { @Override public void run() { synchronized (WebServer.this) { shutdownHook = null; if (server != null) { System.err.println(); stop(); } } } }); Runtime.getRuntime().addShutdownHook(shutdownHook); server.start(); log.info("Started."); } /** * Stop web server. */ public synchronized void stop() { if (shutdownHook != null) { Runtime.getRuntime().removeShutdownHook(shutdownHook); shutdownHook = null; } if (server != null) { server.stop(0); server = null; log.info("Stopped."); } else { log.info("Already stopped."); } } /** * Get server port. * * @return port number. */ public int getServerPort() { return port; } /** * Get server name. * * @return server name. */ public String getServerNameString() { return fqdn + ":" + port; } /** * Get base URL. * * @return base URL. */ public String getBaseURL() { return "http://" + getServerNameString() + "/"; } /** * Set FQDN of base URL for android test. * * @param fqdn FQDN. */ public void setFqdn(String fqdn) { this.fqdn = fqdn; } /** * Start test web server. * * @param args ignored. */ public static void main(String[] args) { int port = 8080; String htdocs = null; switch (args.length) { case 2: htdocs = args[1]; // fall through case 1: port = Integer.parseInt(args[0]); // fall through case 0: break; default: throw new IllegalArgumentException(StringUtils.join(args, ' ')); } new WebServer(port, htdocs).start(); } }