/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.nifi.cluster.manager.testutils; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.Reader; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.apache.nifi.cluster.manager.testutils.HttpRequest.HttpRequestBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A simple HTTP web server that allows clients to register canned-responses to respond to received requests. * */ public class HttpServer { private static final Logger logger = LoggerFactory.getLogger(HttpServer.class); private final ExecutorService executorService; private final ServerSocket serverSocket; private final Queue<HttpResponseAction> responseQueue = new ConcurrentLinkedQueue<>(); private final Map<String, String> checkedHeaders = new HashMap<>(); private final Map<String, List<String>> checkedParameters = new HashMap<>(); private final int port; public HttpServer(int numThreads, int port) throws IOException { this.port = port; executorService = Executors.newFixedThreadPool(numThreads); serverSocket = new ServerSocket(port); } public void start() { new Thread() { @Override public void run() { while (isRunning()) { try { final Socket conn = serverSocket.accept(); executorService.execute(new Runnable() { @Override public void run() { handleRequest(conn); if (conn.isClosed() == false) { try { conn.close(); } catch (IOException ioe) { } } } }); } catch (final SocketException se) { /* ignored */ } catch (final IOException ioe) { if (logger.isDebugEnabled()) { logger.warn("", ioe); } } } } ; } .start(); } public boolean isRunning() { return executorService.isShutdown() == false; } public void stop() { // shutdown server socket try { if (serverSocket.isClosed() == false) { serverSocket.close(); } } catch (final Exception ex) { throw new RuntimeException(ex); } // shutdown executor service try { executorService.shutdown(); executorService.awaitTermination(3, TimeUnit.SECONDS); } catch (final Exception ex) { throw new RuntimeException(ex); } } public int getPort() { if (isRunning()) { return serverSocket.getLocalPort(); } else { return port; } } public Queue<HttpResponseAction> addResponseAction(final HttpResponseAction response) { responseQueue.add(response); return responseQueue; } public void addCheckedHeaders(final Map<String, String> headers) { checkedHeaders.putAll(headers); } public void addCheckedParameters(final Map<String, List<String>> parameters) { checkedParameters.putAll(parameters); } private void handleRequest(final Socket conn) { try { final HttpRequest httpRequest = buildRequest(conn.getInputStream()); if (logger.isDebugEnabled()) { logger.debug("\n" + httpRequest); } // check headers final Map<String, String> reqHeaders = httpRequest.getHeaders(); for (final Map.Entry<String, String> entry : checkedHeaders.entrySet()) { if (reqHeaders.containsKey(entry.getKey())) { if (entry.getValue().equals(reqHeaders.get(entry.getKey()))) { logger.error("Incorrect HTTP request header value received for checked header: " + entry.getKey()); conn.close(); return; } } else { logger.error("Missing checked header: " + entry.getKey()); conn.close(); return; } } // check parameters final Map<String, List<String>> reqParams = httpRequest.getParameters(); for (final Map.Entry<String, List<String>> entry : checkedParameters.entrySet()) { if (reqParams.containsKey(entry.getKey())) { if (entry.getValue().equals(reqParams.get(entry.getKey())) == false) { logger.error("Incorrect HTTP request parameter values received for checked parameter: " + entry.getKey()); conn.close(); return; } } else { logger.error("Missing checked parameter: " + entry.getKey()); conn.close(); return; } } // apply the next response final HttpResponseAction response = responseQueue.remove(); response.apply(); // send the response to client final PrintWriter pw = new PrintWriter(conn.getOutputStream(), true); if (logger.isDebugEnabled()) { logger.debug("\n" + response.getResponse()); } pw.print(response.getResponse()); pw.flush(); } catch (IOException ioe) { /* ignored */ } } private HttpRequest buildRequest(final InputStream requestIs) throws IOException { return new HttpRequestReader().read(new InputStreamReader(requestIs)); } // reads an HTTP request from the given reader private class HttpRequestReader { public HttpRequest read(final Reader reader) throws IOException { HttpRequestBuilder builder = null; String line = ""; boolean isRequestLine = true; while ((line = readLine(reader)).isEmpty() == false) { if (isRequestLine) { builder = HttpRequest.createFromRequestLine(line); isRequestLine = false; } else { builder.addHeader(line); } } if (builder != null) { builder.addBody(reader); } return builder.build(); } private String readLine(final Reader reader) throws IOException { /* read character at time to prevent blocking */ final StringBuilder strb = new StringBuilder(); char c; while ((c = (char) reader.read()) != '\n') { if (c != '\r') { strb.append(c); } } return strb.toString(); } } }