// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search; import com.yahoo.text.Utf8; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * As the name implies, a stupid, single-threaded bad-excuse-for-HTTP server. * * @author Einar M R Rosenvinge */ public class StupidSingleThreadedHttpServer implements Runnable { private static final Logger log = Logger.getLogger(StupidSingleThreadedHttpServer.class.getName()); private final ServerSocket serverSocket; private final int delaySeconds; private Thread serverThread = null; private CompletableFuture requestFuture = new CompletableFuture<>(); private final Pattern contentLengthPattern = Pattern.compile("content-length: (\\d+)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE); public StupidSingleThreadedHttpServer() throws IOException { this(0, 0); } public StupidSingleThreadedHttpServer(int port, int delaySeconds) throws IOException { this.delaySeconds = delaySeconds; this.serverSocket = new ServerSocket(port); } public void start() { serverThread = new Thread(this); serverThread.setDaemon(true); serverThread.start(); } public void run() { try { while(true) { Socket socket = serverSocket.accept(); StringBuilder request = new StringBuilder(); socket.setSoLinger(true, 60); BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); int contentLength = -1; String inputLine; while (!"".equals(inputLine = in.readLine())) { //read header: request.append(inputLine).append("\r\n"); if (inputLine.toLowerCase(Locale.US).contains("content-length")) { Matcher contentLengthMatcher = contentLengthPattern.matcher(inputLine); if (contentLengthMatcher.matches()) { contentLength = Integer.parseInt(contentLengthMatcher.group(1)); } } } request.append("\r\n"); if (contentLength < 0) { System.err.println("WARNING! Got no Content-Length header!!"); } else { char[] requestBody = new char[contentLength]; int readRemaining = contentLength; do { int read = in.read(requestBody, (contentLength - readRemaining), readRemaining); if (read < 0) { throw new IllegalStateException("Should not get EOF here!!"); } readRemaining -= read; } while (readRemaining > 0); request.append(new String(requestBody)); } // Simulate service slowness if (delaySeconds > 0) { try { System.out.println(this.getClass().getCanonicalName() + " sleeping in " + delaySeconds + " s before responding..."); Thread.sleep((long) (delaySeconds * 1000)); System.out.println("done sleeping, responding"); } catch (InterruptedException e) { //ignore } } socket.getOutputStream().write(getResponse(request.toString())); socket.getOutputStream().flush(); in.close(); socket.close(); boolean wasCompleted = requestFuture.complete(request.toString()); if (!wasCompleted) { log.log(Level.INFO, "Only the first request will be stored, ignoring. " + "Old value: " + requestFuture.get() + ", New value: " + request.toString()); } } } catch (SocketException se) { if ("Socket closed".equals(se.getMessage())) { //ignore } else { throw new RuntimeException(se); } } catch (IOException|InterruptedException|ExecutionException e) { throw new RuntimeException(e); } } protected byte[] getResponse(String request) { return Utf8.toBytes("HTTP/1.1 200 OK\r\n" + "Content-Type: text/xml; charset=UTF-8\r\n" + "Connection: close\r\n" + "Content-Length: 0\r\n" + "\r\n"); } protected byte[] getResponseBody() { return new byte[0]; } public void stop() { if (!serverSocket.isClosed()) { try { serverSocket.close(); } catch (IOException e) { throw new RuntimeException(e); } } try { serverThread.interrupt(); } catch (Exception e) { //ignore } } public int getServerPort() { return serverSocket.getLocalPort(); } public String getRequest() { try { return requestFuture.get(1, TimeUnit.MINUTES); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new AssertionError("Failed waiting for request. ", e); } } }