| /* |
| * Copyright (c) 2016 Manumitting Technologies Inc and others. |
| * This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 |
| * which is available at https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Manumitting Technologies Inc - initial API and implementation |
| */ |
| package org.eclipse.userstorage.oauth; |
| |
| import static org.hamcrest.CoreMatchers.startsWith; |
| |
| import java.io.BufferedReader; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.net.InetAddress; |
| import java.net.InetSocketAddress; |
| import java.net.ServerSocket; |
| import java.net.Socket; |
| import java.net.SocketAddress; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.userstorage.internal.util.StringUtil; |
| import org.hamcrest.Matcher; |
| import org.hamcrest.StringDescription; |
| |
| /** |
| * A little HTTP server that is programmed with a sequence of responses. The |
| * requests are matched using Hamcrest {@link Matcher}s. |
| */ |
| public class MockServer { |
| private static boolean DEBUG = Boolean.getBoolean("org.eclipse.userstorage.session.debug"); |
| private static final SimpleDateFormat rfc1123Format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH); |
| |
| private String host; |
| private int port; |
| |
| private ServerSocket serverSocket; |
| private Thread serverThread; |
| |
| private List<Matcher<? extends String>> expectedRequests = new ArrayList<Matcher<? extends String>>(); |
| private List<String[]> expectedResponses = new ArrayList<String[]>(); |
| private volatile boolean success = true; |
| private volatile int handledRequests = 0; |
| |
| /** |
| * Create with a random port. |
| */ |
| public MockServer() { |
| this(null, -1); |
| } |
| |
| /** |
| * Create with a fixed port. If {@code port ≤ 0} then allocated with a |
| * random port. |
| */ |
| public MockServer(int port) { |
| this(null, port); |
| } |
| |
| /** |
| * Create a server that accepts on a particular interface. If |
| * {@code port ≤ 0} then allocated with a random port. |
| */ |
| public MockServer(String host, int port) { |
| this.host = host; |
| this.port = port; |
| } |
| |
| /** |
| * Providing the next request matches {@code requestMatcher}, send the |
| * provided response. |
| */ |
| public MockServer expect(Matcher<? extends String> requestMatcher, String header) { |
| return expect(requestMatcher, new String[] { header }); |
| } |
| |
| /** |
| * Providing the next request matches {@code requestMatcher}, send the |
| * provided response. |
| */ |
| public MockServer expect(Matcher<? extends String> requestMatcher, String[] headerAndResponse) { |
| if (headerAndResponse.length == 0 || headerAndResponse.length > 2) { |
| throw new IllegalArgumentException("response is misshaped"); |
| } |
| try { |
| if (headerAndResponse.length > 1 && headerAndResponse[1] != null |
| && headerAndResponse[1].getBytes(StringUtil.UTF8).length != headerAndResponse[1].length()) { |
| throw new IllegalArgumentException("response is multibyte"); |
| } |
| } catch (UnsupportedEncodingException ex) { |
| throw new IllegalArgumentException(ex); |
| } |
| |
| expectedRequests.add(requestMatcher); |
| expectedResponses.add(headerAndResponse); |
| return this; |
| } |
| |
| public MockServer start() throws IOException { |
| SocketAddress endpoint = null; |
| if (port > 0) { |
| endpoint = new InetSocketAddress(host, port); |
| } |
| serverSocket = new ServerSocket(); |
| serverSocket.bind(endpoint); |
| // serverSocket.setSoTimeout(5000 /* ms */); |
| serverThread = new Thread(new Runnable() { |
| @Override |
| public void run() { |
| listen(); |
| } |
| }); |
| serverThread.start(); |
| return this; |
| } |
| |
| public void stop() { |
| if (serverSocket != null) { |
| try { |
| serverSocket.close(); |
| } catch (IOException e) { |
| warn("MockServer: exception closing server socket: " + e); |
| } |
| } |
| } |
| |
| public boolean isOK() { |
| return success; |
| } |
| |
| public URI getURI() { |
| try { |
| InetAddress address = serverSocket.getInetAddress(); |
| return new URI("http", null, address.isAnyLocalAddress() ? "localhost" : address.getHostAddress(), |
| serverSocket.getLocalPort(), null, null, null); |
| } catch (URISyntaxException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private void listen() { |
| for (handledRequests = 0; expectedRequests.isEmpty() |
| || handledRequests < expectedRequests.size(); handledRequests++) { |
| Socket socket = null; |
| InputStreamReader reader = null; |
| OutputStreamWriter writer = null; |
| try { |
| socket = serverSocket.accept(); |
| reader = new InputStreamReader(socket.getInputStream(), StringUtil.UTF8); |
| writer = new OutputStreamWriter(socket.getOutputStream(), StringUtil.UTF8); |
| |
| handleIncomingRequest(handledRequests, reader, writer); |
| } catch (IOException e) { |
| if (!serverSocket.isClosed()) { |
| warn("MockServer: exception occurred: " + e); |
| e.printStackTrace(); |
| } |
| } finally { |
| close(writer); |
| close(reader); |
| close(socket); |
| } |
| } |
| try { |
| serverSocket.close(); |
| } catch (IOException e) { |
| warn(e.toString()); |
| } |
| } |
| |
| private void handleIncomingRequest(int requestIndex, InputStreamReader reader, OutputStreamWriter writer) { |
| String request = readRequest(reader); |
| Matcher<? extends String> matcher = expectedRequests.get(requestIndex); |
| success &= matcher.matches(request); |
| try { |
| if (success) { |
| String[] response = expectedResponses.get(requestIndex); |
| debug("MockServer: " + request.split("\n")[0] + " >>> " + response[0].split("\n")[0]); |
| writer.append(response[0].trim()); |
| writer.append("\nDate: " + rfc1123Format.format(new Date())); |
| if (response.length == 2 && response[1] != null && response[1].length() > 0) { |
| // this is ok as response should not be multi-byte |
| writer.append("\nContent-Length: " + response[1].length()); |
| writer.append("\n\n"); |
| writer.append(response[1]); |
| } else { |
| writer.append("\n\n"); |
| } |
| } else { |
| debug("MockServer: REQUEST MISMATCH: " + request.split("\n")[0]); |
| StringDescription description = new StringDescription(); |
| description.appendText("Looking for: "); |
| matcher.describeTo(description); |
| description.appendText("\nMismatch:\n"); |
| matcher.describeMismatch(request, description); |
| description.appendText("\n"); |
| |
| writer.write("HTTP/1.1 500 Server Error\n"); |
| writer.write("Content-Type: text/plain\n"); |
| writer.write("Content-Length: "); |
| writer.write(Integer.toString(description.toString().length())); |
| writer.write("\n\n"); |
| writer.write(description.toString()); |
| writer.flush(); |
| } |
| } catch (IOException e) { |
| warn("MockServer: exception writing response: " + e); |
| } |
| } |
| |
| public int getHandledRequests() { |
| return handledRequests; |
| } |
| |
| /** Assumes we don't have binary content */ |
| private String readRequest(InputStreamReader reader) { |
| StringBuilder sb = new StringBuilder(); |
| try { |
| BufferedReader br = new BufferedReader(reader); |
| String line; |
| int contentLength = -1; |
| while ((line = br.readLine()) != null && !line.isEmpty()) { |
| if (line.startsWith("Content-Length:")) { |
| try { |
| contentLength = Integer.parseInt(line.split(": *")[1]); |
| } catch (NumberFormatException e) { |
| warn("MockServer: invalid Content-Length: " + line); |
| } |
| } |
| sb.append(line).append('\n'); |
| } |
| if (contentLength > 0) { |
| sb.append('\n'); |
| char[] buffer = new char[1024]; |
| int len; |
| while (contentLength > 0 && (len = br.read(buffer, 0, Math.min(contentLength, buffer.length))) > 0) { |
| sb.append(buffer, 0, len); |
| contentLength -= len; |
| } |
| } |
| } catch (IOException e) { |
| warn("MockServer: exception reading request: " + e); |
| } |
| return sb.toString(); |
| } |
| |
| private void close(Closeable closeable) { |
| try { |
| if (closeable != null) { |
| closeable.close(); |
| } |
| } catch (IOException e) { |
| warn("MockServer: Error closing object: " + e); |
| } |
| } |
| |
| private void close(Socket closeable) { |
| try { |
| if (closeable != null) { |
| closeable.close(); |
| } |
| } catch (IOException e) { |
| warn("MockServer: Error closing object: " + e); |
| } |
| } |
| |
| private void debug(String text) { |
| if (DEBUG) { |
| System.out.println(text); |
| } |
| } |
| |
| private void warn(String text) { |
| if (DEBUG) { |
| System.err.println(text); |
| } |
| } |
| |
| public static Matcher<String> isGet() { |
| return startsWith("GET "); |
| } |
| |
| public static Matcher<String> isGet(String path) { |
| return startsWith("GET " + path); |
| } |
| |
| public static Matcher<String> isPost() { |
| return startsWith("POST "); |
| } |
| |
| public static Matcher<String> isPost(String path) { |
| return startsWith("POST " + path); |
| } |
| |
| public static Matcher<String> isPut() { |
| return startsWith("PUT "); |
| } |
| |
| public static Matcher<String> isPut(String path) { |
| return startsWith("PUT " + path); |
| } |
| |
| public static Matcher<String> hasQueryParameter(String paramName, String paramValue) { |
| return RegexMatcher.matches("^\\p{Alpha}+ \\S*[?&]" + paramName + "=" + paramValue + ".*", |
| Pattern.MULTILINE | Pattern.DOTALL); |
| } |
| |
| public static Matcher<? super String> hasHeader(String headerName, String headerValue) { |
| return RegexMatcher.matches(".*\n" + headerName + ": " + headerValue + ".*", |
| Pattern.MULTILINE | Pattern.DOTALL); |
| } |
| |
| public static Matcher<String> hasBodyParameter(String paramName, String paramValue) { |
| return RegexMatcher.matches(".*\n\n(\\S*[?&])?" + paramName + "=" + paramValue + ".*", |
| Pattern.MULTILINE | Pattern.DOTALL); |
| } |
| } |