154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson/* 254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Copyright (C) 2011 Google Inc. 354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * 454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Licensed under the Apache License, Version 2.0 (the "License"); 554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * you may not use this file except in compliance with the License. 654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * You may obtain a copy of the License at 754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * 854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * http://www.apache.org/licenses/LICENSE-2.0 954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * 1054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Unless required by applicable law or agreed to in writing, software 1154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * distributed under the License is distributed on an "AS IS" BASIS, 1254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * See the License for the specific language governing permissions and 1454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * limitations under the License. 1554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 1654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 1754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonpackage com.google.mockwebserver; 1854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 1954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START; 2024198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilsonimport static com.google.mockwebserver.SocketPolicy.FAIL_HANDSHAKE; 2154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.io.BufferedInputStream; 2254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.io.BufferedOutputStream; 2354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.io.ByteArrayOutputStream; 2454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.io.IOException; 2554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.io.InputStream; 2654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.io.OutputStream; 2754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.net.InetAddress; 2854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.net.InetSocketAddress; 2954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.net.MalformedURLException; 3054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.net.Proxy; 3154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.net.ServerSocket; 3254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.net.Socket; 3354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.net.SocketException; 3454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.net.URL; 3554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.net.UnknownHostException; 3624198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilsonimport java.security.cert.CertificateException; 3724198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilsonimport java.security.cert.X509Certificate; 3854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.ArrayList; 3954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.Iterator; 4054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.List; 417a68ed6a49c3060b235810391a82412a95f9c979jwilsonimport java.util.Map; 4254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.concurrent.BlockingQueue; 4354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.concurrent.ConcurrentHashMap; 4454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.concurrent.ExecutorService; 4554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.concurrent.Executors; 4654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.concurrent.LinkedBlockingQueue; 4754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.concurrent.atomic.AtomicInteger; 4854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.logging.Level; 4954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport java.util.logging.Logger; 5024198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilsonimport javax.net.ssl.SSLContext; 5154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport javax.net.ssl.SSLSocket; 5254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonimport javax.net.ssl.SSLSocketFactory; 5324198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilsonimport javax.net.ssl.TrustManager; 5424198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilsonimport javax.net.ssl.X509TrustManager; 5554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 5654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson/** 5754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * A scriptable web server. Callers supply canned responses and the server 5854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * replays them upon request in sequence. 5954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 6054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilsonpublic final class MockWebServer { 6154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 6254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson static final String ASCII = "US-ASCII"; 6354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 6454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private static final Logger logger = Logger.getLogger(MockWebServer.class.getName()); 6554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private final BlockingQueue<RecordedRequest> requestQueue 6654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson = new LinkedBlockingQueue<RecordedRequest>(); 677a68ed6a49c3060b235810391a82412a95f9c979jwilson /** All map values are Boolean.TRUE. (Collections.newSetFromMap isn't available in Froyo) */ 687a68ed6a49c3060b235810391a82412a95f9c979jwilson private final Map<Socket, Boolean> openClientSockets = new ConcurrentHashMap<Socket, Boolean>(); 6954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private final AtomicInteger requestCount = new AtomicInteger(); 7054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private int bodyLimit = Integer.MAX_VALUE; 7154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private ServerSocket serverSocket; 7254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private SSLSocketFactory sslSocketFactory; 738ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller private ExecutorService acceptExecutor; 748ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller private ExecutorService requestExecutor; 7554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private boolean tunnelProxy; 767a68ed6a49c3060b235810391a82412a95f9c979jwilson private Dispatcher dispatcher = new QueueDispatcher(); 7754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 7854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private int port = -1; 798ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller private int workerThreads = Integer.MAX_VALUE; 808ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller 8154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 8254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public int getPort() { 8354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (port == -1) { 8454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson throw new IllegalStateException("Cannot retrieve port before calling play()"); 8554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 8654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return port; 8754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 8854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 8954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public String getHostName() { 9054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson try { 9154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return InetAddress.getLocalHost().getHostName(); 9254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } catch (UnknownHostException e) { 9354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson throw new AssertionError(); 9454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 9554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 9654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 9754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public Proxy toProxyAddress() { 9854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(getHostName(), getPort())); 9954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 10054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 10154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 10254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Returns a URL for connecting to this server. 10354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * 10454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * @param path the request path, such as "/". 10554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 1067a68ed6a49c3060b235810391a82412a95f9c979jwilson public URL getUrl(String path) { 1077a68ed6a49c3060b235810391a82412a95f9c979jwilson try { 1087a68ed6a49c3060b235810391a82412a95f9c979jwilson return sslSocketFactory != null 1097a68ed6a49c3060b235810391a82412a95f9c979jwilson ? new URL("https://" + getHostName() + ":" + getPort() + path) 1107a68ed6a49c3060b235810391a82412a95f9c979jwilson : new URL("http://" + getHostName() + ":" + getPort() + path); 1117a68ed6a49c3060b235810391a82412a95f9c979jwilson } catch (MalformedURLException e) { 1127a68ed6a49c3060b235810391a82412a95f9c979jwilson throw new AssertionError(e); 1137a68ed6a49c3060b235810391a82412a95f9c979jwilson } 11454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 11554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 11654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 11718819471837d75b4e579bc0c23d6f8c8d9011653Jesse Wilson * Returns a cookie domain for this server. This returns the server's 11818819471837d75b4e579bc0c23d6f8c8d9011653Jesse Wilson * non-loopback host name if it is known. Otherwise this returns ".local" 11918819471837d75b4e579bc0c23d6f8c8d9011653Jesse Wilson * for this server's loopback name. 12018819471837d75b4e579bc0c23d6f8c8d9011653Jesse Wilson */ 12118819471837d75b4e579bc0c23d6f8c8d9011653Jesse Wilson public String getCookieDomain() { 12218819471837d75b4e579bc0c23d6f8c8d9011653Jesse Wilson String hostName = getHostName(); 12318819471837d75b4e579bc0c23d6f8c8d9011653Jesse Wilson return hostName.contains(".") ? hostName : ".local"; 12418819471837d75b4e579bc0c23d6f8c8d9011653Jesse Wilson } 12518819471837d75b4e579bc0c23d6f8c8d9011653Jesse Wilson 1268ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller public void setWorkerThreads(int threads) { 1278ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller this.workerThreads = threads; 1288ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller } 1298ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller 13018819471837d75b4e579bc0c23d6f8c8d9011653Jesse Wilson /** 13154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Sets the number of bytes of the POST body to keep in memory to the given 13254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * limit. 13354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 13454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public void setBodyLimit(int maxBodyLength) { 13554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson this.bodyLimit = maxBodyLength; 13654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 13754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 13854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 13954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Serve requests with HTTPS rather than otherwise. 14054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * 14154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * @param tunnelProxy whether to expect the HTTP CONNECT method before 14254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * negotiating TLS. 14354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 14454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public void useHttps(SSLSocketFactory sslSocketFactory, boolean tunnelProxy) { 14554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson this.sslSocketFactory = sslSocketFactory; 14654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson this.tunnelProxy = tunnelProxy; 14754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 14854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 14954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 15054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Awaits the next HTTP request, removes it, and returns it. Callers should 15154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * use this to verify the request sent was as intended. 15254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 15354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public RecordedRequest takeRequest() throws InterruptedException { 15454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return requestQueue.take(); 15554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 15654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 15754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 15854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Returns the number of HTTP requests received thus far by this server. 15954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * This may exceed the number of HTTP connections when connection reuse is 16054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * in practice. 16154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 16254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public int getRequestCount() { 16354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return requestCount.get(); 16454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 16554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 16654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 1677a68ed6a49c3060b235810391a82412a95f9c979jwilson * Scripts {@code response} to be returned to a request made in sequence. 1687a68ed6a49c3060b235810391a82412a95f9c979jwilson * The first request is served by the first enqueued response; the second 1697a68ed6a49c3060b235810391a82412a95f9c979jwilson * request by the second enqueued response; and so on. 17054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * 1717a68ed6a49c3060b235810391a82412a95f9c979jwilson * @throws ClassCastException if the default dispatcher has been replaced 1727a68ed6a49c3060b235810391a82412a95f9c979jwilson * with {@link #setDispatcher(Dispatcher)}. 17354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 1747a68ed6a49c3060b235810391a82412a95f9c979jwilson public void enqueue(MockResponse response) { 1757a68ed6a49c3060b235810391a82412a95f9c979jwilson ((QueueDispatcher) dispatcher).enqueueResponse(response.clone()); 17654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 17754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 17854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 17954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Equivalent to {@code play(0)}. 18054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 18154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public void play() throws IOException { 18254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson play(0); 18354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 18454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 18554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 18654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Starts the server, serves all enqueued requests, and shuts the server 18754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * down. 18854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * 18954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * @param port the port to listen to, or 0 for any available port. 19054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Automated tests should always use port 0 to avoid flakiness when a 19154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * specific port is unavailable. 19254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 19354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public void play(int port) throws IOException { 1948ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller if (acceptExecutor != null) { 1956ae1753c9c0b3c0b2479a7c4099e38e4249dc50eBrian Carlstrom throw new IllegalStateException("play() already called"); 1966ae1753c9c0b3c0b2479a7c4099e38e4249dc50eBrian Carlstrom } 1978ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // The acceptExecutor handles the Socket.accept() and hands each request off to the 1988ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // requestExecutor. It also handles shutdown. 1998ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller acceptExecutor = Executors.newSingleThreadExecutor(); 2008ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // The requestExecutor has a fixed number of worker threads. In order to get strict 2018ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // guarantees that requests are handled in the order in which they are accepted 2028ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // workerThreads should be set to 1. 2038ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller requestExecutor = Executors.newFixedThreadPool(workerThreads); 20454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson serverSocket = new ServerSocket(port); 20554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson serverSocket.setReuseAddress(true); 20654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 20754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson this.port = serverSocket.getLocalPort(); 2088ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller acceptExecutor.execute(namedRunnable("MockWebServer-accept-" + port, new Runnable() { 20954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public void run() { 21054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson try { 21154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson acceptConnections(); 21254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } catch (Throwable e) { 21354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson logger.log(Level.WARNING, "MockWebServer connection failed", e); 21454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 21554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 21654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /* 21754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * This gnarly block of code will release all sockets and 21854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * all thread, even if any close fails. 21954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 22054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson try { 22154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson serverSocket.close(); 22254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } catch (Throwable e) { 22354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson logger.log(Level.WARNING, "MockWebServer server socket close failed", e); 22454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 2257a68ed6a49c3060b235810391a82412a95f9c979jwilson for (Iterator<Socket> s = openClientSockets.keySet().iterator(); s.hasNext(); ) { 22654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson try { 22754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson s.next().close(); 22854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson s.remove(); 22954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } catch (Throwable e) { 23054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson logger.log(Level.WARNING, "MockWebServer socket close failed", e); 23154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 23254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 23354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson try { 2348ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller acceptExecutor.shutdown(); 2358ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller } catch (Throwable e) { 2368ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller logger.log(Level.WARNING, "MockWebServer acceptExecutor shutdown failed", e); 2378ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller } 2388ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller try { 2398ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller requestExecutor.shutdown(); 24054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } catch (Throwable e) { 2418ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller logger.log(Level.WARNING, "MockWebServer requestExecutor shutdown failed", e); 24254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 24354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 24454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 24554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private void acceptConnections() throws Exception { 2467a68ed6a49c3060b235810391a82412a95f9c979jwilson while (true) { 24754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson Socket socket; 24854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson try { 24954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson socket = serverSocket.accept(); 2507a68ed6a49c3060b235810391a82412a95f9c979jwilson } catch (SocketException e) { 2517a68ed6a49c3060b235810391a82412a95f9c979jwilson return; 25254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 2537a68ed6a49c3060b235810391a82412a95f9c979jwilson final SocketPolicy socketPolicy = dispatcher.peekSocketPolicy(); 2547a68ed6a49c3060b235810391a82412a95f9c979jwilson if (socketPolicy == DISCONNECT_AT_START) { 2557a68ed6a49c3060b235810391a82412a95f9c979jwilson dispatchBookkeepingRequest(0, socket); 25654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson socket.close(); 25754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } else { 2587a68ed6a49c3060b235810391a82412a95f9c979jwilson openClientSockets.put(socket, true); 25954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson serveConnection(socket); 26054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 2617a68ed6a49c3060b235810391a82412a95f9c979jwilson } 26254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 26354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson })); 26454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 26554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 26654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public void shutdown() throws IOException { 26754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (serverSocket != null) { 26854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson serverSocket.close(); // should cause acceptConnections() to break out 26954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 27054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 27154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 27254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private void serveConnection(final Socket raw) { 27354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson String name = "MockWebServer-" + raw.getRemoteSocketAddress(); 2748ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller requestExecutor.execute(namedRunnable(name, new Runnable() { 27554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson int sequenceNumber = 0; 27654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 27754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public void run() { 27854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson try { 27954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson processConnection(); 28054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } catch (Exception e) { 28154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson logger.log(Level.WARNING, "MockWebServer connection failed", e); 28254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 28354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 28454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 28554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public void processConnection() throws Exception { 28654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson Socket socket; 28754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (sslSocketFactory != null) { 28854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (tunnelProxy) { 28954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson createTunnel(); 29054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 2917a68ed6a49c3060b235810391a82412a95f9c979jwilson final SocketPolicy socketPolicy = dispatcher.peekSocketPolicy(); 2927a68ed6a49c3060b235810391a82412a95f9c979jwilson if (socketPolicy == FAIL_HANDSHAKE) { 2937a68ed6a49c3060b235810391a82412a95f9c979jwilson dispatchBookkeepingRequest(sequenceNumber, raw); 29424198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson processHandshakeFailure(raw, sequenceNumber++); 29524198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson return; 29624198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson } 29754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson socket = sslSocketFactory.createSocket( 29854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson raw, raw.getInetAddress().getHostAddress(), raw.getPort(), true); 29954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson ((SSLSocket) socket).setUseClientMode(false); 3007a68ed6a49c3060b235810391a82412a95f9c979jwilson openClientSockets.put(socket, true); 30154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson openClientSockets.remove(raw); 30254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } else { 30354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson socket = raw; 30454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 30554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 30654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson InputStream in = new BufferedInputStream(socket.getInputStream()); 30754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson OutputStream out = new BufferedOutputStream(socket.getOutputStream()); 30854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 3097a68ed6a49c3060b235810391a82412a95f9c979jwilson while (processOneRequest(socket, in, out)) { 3107a68ed6a49c3060b235810391a82412a95f9c979jwilson } 31154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 31254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (sequenceNumber == 0) { 31354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson logger.warning("MockWebServer connection didn't make a request"); 31454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 31554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 31654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson in.close(); 31754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson out.close(); 31854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson socket.close(); 31954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson openClientSockets.remove(socket); 32054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 32154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 32254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 32354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Respond to CONNECT requests until a SWITCH_TO_SSL_AT_END response 32454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * is dispatched. 32554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 32654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private void createTunnel() throws IOException, InterruptedException { 32754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson while (true) { 3287a68ed6a49c3060b235810391a82412a95f9c979jwilson final SocketPolicy socketPolicy = dispatcher.peekSocketPolicy(); 32924198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson if (!processOneRequest(raw, raw.getInputStream(), raw.getOutputStream())) { 33054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson throw new IllegalStateException("Tunnel without any CONNECT!"); 33154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 3327a68ed6a49c3060b235810391a82412a95f9c979jwilson if (socketPolicy == SocketPolicy.UPGRADE_TO_SSL_AT_END) { 33354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return; 33454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 33554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 33654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 33754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 33854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 33954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Reads a request and writes its response. Returns true if a request 34054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * was processed. 34154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 34224198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson private boolean processOneRequest(Socket socket, InputStream in, OutputStream out) 34354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson throws IOException, InterruptedException { 34424198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson RecordedRequest request = readRequest(socket, in, sequenceNumber); 34554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (request == null) { 34654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return false; 34754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 3487a68ed6a49c3060b235810391a82412a95f9c979jwilson requestCount.incrementAndGet(); 3497a68ed6a49c3060b235810391a82412a95f9c979jwilson requestQueue.add(request); 3507a68ed6a49c3060b235810391a82412a95f9c979jwilson MockResponse response = dispatcher.dispatch(request); 3513088bcc1d22f3e163e5e7ecbc997daadfdaaeec3Narayan Kamath if (response.getSocketPolicy() == SocketPolicy.DISCONNECT_AFTER_READING_REQUEST) { 3523088bcc1d22f3e163e5e7ecbc997daadfdaaeec3Narayan Kamath logger.info("Received request: " + request + " and disconnected without responding"); 3533088bcc1d22f3e163e5e7ecbc997daadfdaaeec3Narayan Kamath return false; 3543088bcc1d22f3e163e5e7ecbc997daadfdaaeec3Narayan Kamath } 3557a68ed6a49c3060b235810391a82412a95f9c979jwilson writeResponse(out, response); 3568ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller 3578ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // For socket policies that poison the socket after the response is written: 3588ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // The client has received the response and will no longer be blocked after 3598ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // writeResponse() has returned. A client can then re-use the connection before 3608ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // the socket is poisoned (i.e. keep-alive / connection pooling). The second 3618ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // request/response may fail at the beginning, middle, end, or even succeed 3628ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // depending on scheduling. Delays can be required in tests to improve the chances 3638ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // of sockets being in a known state when subsequent requests are made. 3648ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // 3658ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // For SHUTDOWN_OUTPUT_AT_END the client may detect a problem with its input socket 3668ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // after the request has been made but before the server has chosen a response. 3678ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // For clients that perform retries, this can cause the client to issue a retry 3688ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // request. The retry handler may call dispatcher.dispatch(request) before the 3698ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // initial, failed request handler does and cause non-obvious response ordering. 3708ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // Setting workerThreads = 1 ensures that the dispatcher is called for requests in 3718ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller // the order they are received. 3728ea0a457ae604fe61aa119e05aad3ac71f042b90Neil Fuller 37354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (response.getSocketPolicy() == SocketPolicy.DISCONNECT_AT_END) { 37454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson in.close(); 37554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson out.close(); 37654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } else if (response.getSocketPolicy() == SocketPolicy.SHUTDOWN_INPUT_AT_END) { 37754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson socket.shutdownInput(); 37854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } else if (response.getSocketPolicy() == SocketPolicy.SHUTDOWN_OUTPUT_AT_END) { 37954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson socket.shutdownOutput(); 38054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 3813b406d4de9b5f0f1caa990f079075eb0bfc5220aJeff Sharkey logger.info("Received request: " + request + " and responded: " + response); 38254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson sequenceNumber++; 38354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return true; 38454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 38554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson })); 38654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 38754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 38824198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson private void processHandshakeFailure(Socket raw, int sequenceNumber) throws Exception { 38924198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson X509TrustManager untrusted = new X509TrustManager() { 39024198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson @Override public void checkClientTrusted(X509Certificate[] chain, String authType) 39124198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson throws CertificateException { 39224198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson throw new CertificateException(); 39324198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson } 39424198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { 39524198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson throw new AssertionError(); 39624198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson } 39724198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson @Override public X509Certificate[] getAcceptedIssuers() { 39824198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson throw new AssertionError(); 39924198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson } 40024198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson }; 40124198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson SSLContext context = SSLContext.getInstance("TLS"); 40224198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson context.init(null, new TrustManager[] { untrusted }, new java.security.SecureRandom()); 40324198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson SSLSocketFactory sslSocketFactory = context.getSocketFactory(); 40424198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson SSLSocket socket = (SSLSocket) sslSocketFactory.createSocket( 40524198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson raw, raw.getInetAddress().getHostAddress(), raw.getPort(), true); 40624198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson try { 40724198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson socket.startHandshake(); // we're testing a handshake failure 40824198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson throw new AssertionError(); 40924198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson } catch (IOException expected) { 41024198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson } 41124198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson socket.close(); 4127a68ed6a49c3060b235810391a82412a95f9c979jwilson } 4137a68ed6a49c3060b235810391a82412a95f9c979jwilson 4147a68ed6a49c3060b235810391a82412a95f9c979jwilson private void dispatchBookkeepingRequest(int sequenceNumber, Socket socket) throws InterruptedException { 41524198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson requestCount.incrementAndGet(); 416a4a53d9c9bf86165a5e2bd9ce4bcb834f09d4c37Narayan Kamath RecordedRequest request = new RecordedRequest(null, null, null, -1, null, sequenceNumber, 417a4a53d9c9bf86165a5e2bd9ce4bcb834f09d4c37Narayan Kamath socket); 418a4a53d9c9bf86165a5e2bd9ce4bcb834f09d4c37Narayan Kamath dispatcher.dispatch(request); 419a4a53d9c9bf86165a5e2bd9ce4bcb834f09d4c37Narayan Kamath requestQueue.add(request); 42024198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson } 42124198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson 42254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 42354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * @param sequenceNumber the index of this request on this connection. 42454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 42524198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson private RecordedRequest readRequest(Socket socket, InputStream in, int sequenceNumber) 42624198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson throws IOException { 42754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson String request; 42854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson try { 42954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson request = readAsciiUntilCrlf(in); 43054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } catch (IOException streamIsClosed) { 43154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return null; // no request because we closed the stream 43254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 4337a68ed6a49c3060b235810391a82412a95f9c979jwilson if (request.length() == 0) { 43454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return null; // no request because the stream is exhausted 43554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 43654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 43754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson List<String> headers = new ArrayList<String>(); 43854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson int contentLength = -1; 43954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson boolean chunked = false; 44054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson String header; 4417a68ed6a49c3060b235810391a82412a95f9c979jwilson while ((header = readAsciiUntilCrlf(in)).length() != 0) { 44254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson headers.add(header); 44354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson String lowercaseHeader = header.toLowerCase(); 44454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) { 44554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson contentLength = Integer.parseInt(header.substring(15).trim()); 44654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 44754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (lowercaseHeader.startsWith("transfer-encoding:") && 44854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson lowercaseHeader.substring(18).trim().equals("chunked")) { 44954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson chunked = true; 45054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 45154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 45254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 45354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson boolean hasBody = false; 45454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson TruncatingOutputStream requestBody = new TruncatingOutputStream(); 45554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson List<Integer> chunkSizes = new ArrayList<Integer>(); 45654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (contentLength != -1) { 45754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson hasBody = true; 45854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson transfer(contentLength, in, requestBody); 45954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } else if (chunked) { 46054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson hasBody = true; 46154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson while (true) { 46254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson int chunkSize = Integer.parseInt(readAsciiUntilCrlf(in).trim(), 16); 46354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (chunkSize == 0) { 46454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson readEmptyLine(in); 46554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson break; 46654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 46754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson chunkSizes.add(chunkSize); 46854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson transfer(chunkSize, in, requestBody); 46954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson readEmptyLine(in); 47054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 47154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 47254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 47354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (request.startsWith("OPTIONS ") || request.startsWith("GET ") 47454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson || request.startsWith("HEAD ") || request.startsWith("DELETE ") 4757a68ed6a49c3060b235810391a82412a95f9c979jwilson || request.startsWith("TRACE ") || request.startsWith("CONNECT ")) { 47654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (hasBody) { 47754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson throw new IllegalArgumentException("Request must not have a body: " + request); 47854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 4797a68ed6a49c3060b235810391a82412a95f9c979jwilson } else if (!request.startsWith("POST ") && !request.startsWith("PUT ")) { 48054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson throw new UnsupportedOperationException("Unexpected method: " + request); 48154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 48254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 48354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return new RecordedRequest(request, headers, chunkSizes, 48424198dd39014c4490b963f039b04d52e01d79bc5Jesse Wilson requestBody.numBytesReceived, requestBody.toByteArray(), sequenceNumber, socket); 48554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 48654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 4877a68ed6a49c3060b235810391a82412a95f9c979jwilson private void writeResponse(OutputStream out, MockResponse response) throws IOException { 4887a68ed6a49c3060b235810391a82412a95f9c979jwilson out.write((response.getStatus() + "\r\n").getBytes(ASCII)); 4897a68ed6a49c3060b235810391a82412a95f9c979jwilson for (String header : response.getHeaders()) { 4907a68ed6a49c3060b235810391a82412a95f9c979jwilson out.write((header + "\r\n").getBytes(ASCII)); 49154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 4927a68ed6a49c3060b235810391a82412a95f9c979jwilson out.write(("\r\n").getBytes(ASCII)); 4937a68ed6a49c3060b235810391a82412a95f9c979jwilson out.flush(); 49454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 4957a68ed6a49c3060b235810391a82412a95f9c979jwilson final InputStream in = response.getBodyStream(); 4967a68ed6a49c3060b235810391a82412a95f9c979jwilson if (in == null) { 4977a68ed6a49c3060b235810391a82412a95f9c979jwilson return; 49854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 4997a68ed6a49c3060b235810391a82412a95f9c979jwilson final int bytesPerSecond = response.getBytesPerSecond(); 50054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 5017a68ed6a49c3060b235810391a82412a95f9c979jwilson // Stream data in MTU-sized increments 5027a68ed6a49c3060b235810391a82412a95f9c979jwilson final byte[] buffer = new byte[1452]; 5037a68ed6a49c3060b235810391a82412a95f9c979jwilson final long delayMs; 5047a68ed6a49c3060b235810391a82412a95f9c979jwilson if (bytesPerSecond == Integer.MAX_VALUE) { 5057a68ed6a49c3060b235810391a82412a95f9c979jwilson delayMs = 0; 50654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } else { 5077a68ed6a49c3060b235810391a82412a95f9c979jwilson delayMs = (1000 * buffer.length) / bytesPerSecond; 5087a68ed6a49c3060b235810391a82412a95f9c979jwilson } 5097a68ed6a49c3060b235810391a82412a95f9c979jwilson 5107a68ed6a49c3060b235810391a82412a95f9c979jwilson int read; 5117a68ed6a49c3060b235810391a82412a95f9c979jwilson long sinceDelay = 0; 5127a68ed6a49c3060b235810391a82412a95f9c979jwilson while ((read = in.read(buffer)) != -1) { 5137a68ed6a49c3060b235810391a82412a95f9c979jwilson out.write(buffer, 0, read); 5147a68ed6a49c3060b235810391a82412a95f9c979jwilson out.flush(); 5157a68ed6a49c3060b235810391a82412a95f9c979jwilson 5167a68ed6a49c3060b235810391a82412a95f9c979jwilson sinceDelay += read; 5177a68ed6a49c3060b235810391a82412a95f9c979jwilson if (sinceDelay >= buffer.length && delayMs > 0) { 5187a68ed6a49c3060b235810391a82412a95f9c979jwilson sinceDelay %= buffer.length; 5197a68ed6a49c3060b235810391a82412a95f9c979jwilson try { 5207a68ed6a49c3060b235810391a82412a95f9c979jwilson Thread.sleep(delayMs); 5217a68ed6a49c3060b235810391a82412a95f9c979jwilson } catch (InterruptedException e) { 5227a68ed6a49c3060b235810391a82412a95f9c979jwilson throw new AssertionError(); 5237a68ed6a49c3060b235810391a82412a95f9c979jwilson } 5247a68ed6a49c3060b235810391a82412a95f9c979jwilson } 52554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 52654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 52754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 52854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 52954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Transfer bytes from {@code in} to {@code out} until either {@code length} 53054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * bytes have been transferred or {@code in} is exhausted. 53154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 53254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private void transfer(int length, InputStream in, OutputStream out) throws IOException { 53354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson byte[] buffer = new byte[1024]; 53454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson while (length > 0) { 53554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson int count = in.read(buffer, 0, Math.min(buffer.length, length)); 53654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (count == -1) { 53754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return; 53854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 53954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson out.write(buffer, 0, count); 54054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson length -= count; 54154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 54254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 54354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 54454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 54554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * Returns the text from {@code in} until the next "\r\n", or null if 54654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * {@code in} is exhausted. 54754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 54854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private String readAsciiUntilCrlf(InputStream in) throws IOException { 54954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson StringBuilder builder = new StringBuilder(); 55054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson while (true) { 55154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson int c = in.read(); 55254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (c == '\n' && builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') { 55354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson builder.deleteCharAt(builder.length() - 1); 55454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return builder.toString(); 55554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } else if (c == -1) { 55654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return builder.toString(); 55754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } else { 55854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson builder.append((char) c); 55954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 56054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 56154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 56254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 56354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private void readEmptyLine(InputStream in) throws IOException { 56454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson String line = readAsciiUntilCrlf(in); 5657a68ed6a49c3060b235810391a82412a95f9c979jwilson if (line.length() != 0) { 56654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson throw new IllegalStateException("Expected empty but was: " + line); 56754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 56854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 56954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 57054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson /** 5717a68ed6a49c3060b235810391a82412a95f9c979jwilson * Sets the dispatcher used to match incoming requests to mock responses. 5727a68ed6a49c3060b235810391a82412a95f9c979jwilson * The default dispatcher simply serves a fixed sequence of responses from 5737a68ed6a49c3060b235810391a82412a95f9c979jwilson * a {@link #enqueue(MockResponse) queue}; custom dispatchers can vary the 5747a68ed6a49c3060b235810391a82412a95f9c979jwilson * response based on timing or the content of the request. 5757a68ed6a49c3060b235810391a82412a95f9c979jwilson */ 5767a68ed6a49c3060b235810391a82412a95f9c979jwilson public void setDispatcher(Dispatcher dispatcher) { 5777a68ed6a49c3060b235810391a82412a95f9c979jwilson if (dispatcher == null) { 5787a68ed6a49c3060b235810391a82412a95f9c979jwilson throw new NullPointerException(); 5797a68ed6a49c3060b235810391a82412a95f9c979jwilson } 5807a68ed6a49c3060b235810391a82412a95f9c979jwilson this.dispatcher = dispatcher; 5817a68ed6a49c3060b235810391a82412a95f9c979jwilson } 5827a68ed6a49c3060b235810391a82412a95f9c979jwilson 5837a68ed6a49c3060b235810391a82412a95f9c979jwilson /** 58454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson * An output stream that drops data after bodyLimit bytes. 58554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson */ 58654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private class TruncatingOutputStream extends ByteArrayOutputStream { 58754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private int numBytesReceived = 0; 58854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson @Override public void write(byte[] buffer, int offset, int len) { 58954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson numBytesReceived += len; 59054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson super.write(buffer, offset, Math.min(len, bodyLimit - count)); 59154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 59254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson @Override public void write(int oneByte) { 59354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson numBytesReceived++; 59454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson if (count < bodyLimit) { 59554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson super.write(oneByte); 59654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 59754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 59854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 59954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson 60054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson private static Runnable namedRunnable(final String name, final Runnable runnable) { 60154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson return new Runnable() { 60254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson public void run() { 60354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson String originalName = Thread.currentThread().getName(); 60454ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson Thread.currentThread().setName(name); 60554ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson try { 60654ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson runnable.run(); 60754ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } finally { 60854ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson Thread.currentThread().setName(originalName); 60954ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 61054ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 61154ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson }; 61254ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson } 61354ce6cb5d13f732a3e71aa3555cd3709d5bf3cf5Jesse Wilson} 614