MockWebServer.java revision b7f4d6c3968c372767b2510f38a3d506067aced6
1b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson/* 2b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * Copyright (C) 2010 The Android Open Source Project 3b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * 4b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * Licensed under the Apache License, Version 2.0 (the "License"); 5b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * you may not use this file except in compliance with the License. 6b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * You may obtain a copy of the License at 7b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * 8b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * http://www.apache.org/licenses/LICENSE-2.0 9b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * 10b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * Unless required by applicable law or agreed to in writing, software 11b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * distributed under the License is distributed on an "AS IS" BASIS, 12b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * See the License for the specific language governing permissions and 14b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * limitations under the License. 15b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 16b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 17b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonpackage tests.http; 18b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 19b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.io.BufferedInputStream; 20b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.io.BufferedOutputStream; 21b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.io.ByteArrayOutputStream; 22b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.io.IOException; 23b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.io.InputStream; 24b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.io.OutputStream; 2500feece22909b7dc79fc96d666d157390b93858eJesse Wilsonimport java.net.InetAddress; 2660476787f0e0f052366d8031c74e507ffd3d16a3Jesse Wilsonimport java.net.InetSocketAddress; 27b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.net.MalformedURLException; 2860476787f0e0f052366d8031c74e507ffd3d16a3Jesse Wilsonimport java.net.Proxy; 29b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.net.ServerSocket; 30b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.net.Socket; 31b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilsonimport java.net.SocketException; 32b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.net.URL; 3300feece22909b7dc79fc96d666d157390b93858eJesse Wilsonimport java.net.UnknownHostException; 34b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.util.ArrayList; 35096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilsonimport java.util.Collections; 36096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilsonimport java.util.HashSet; 37096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilsonimport java.util.Iterator; 38b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.util.List; 39096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilsonimport java.util.Set; 40b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.util.concurrent.BlockingQueue; 41b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.util.concurrent.ExecutorService; 42b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.util.concurrent.Executors; 43b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.util.concurrent.LinkedBlockingDeque; 44b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonimport java.util.concurrent.LinkedBlockingQueue; 4551e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilsonimport java.util.concurrent.atomic.AtomicInteger; 46b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilsonimport java.util.logging.Level; 47b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilsonimport java.util.logging.Logger; 48706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilsonimport javax.net.ssl.SSLSocket; 49706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilsonimport javax.net.ssl.SSLSocketFactory; 50e942f46f10bb9384a1b186b3d7b74f9704c57090Jesse Wilsonimport static tests.http.SocketPolicy.DISCONNECT_AT_START; 51b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 52b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson/** 53b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * A scriptable web server. Callers supply canned responses and the server 54b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * replays them upon request in sequence. 55b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 56b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilsonpublic final class MockWebServer { 57b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 58b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson static final String ASCII = "US-ASCII"; 59b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 60b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson private static final Logger logger = Logger.getLogger(MockWebServer.class.getName()); 61b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private final BlockingQueue<RecordedRequest> requestQueue 62b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson = new LinkedBlockingQueue<RecordedRequest>(); 63b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private final BlockingQueue<MockResponse> responseQueue 64b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson = new LinkedBlockingDeque<MockResponse>(); 65096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson private final Set<Socket> openClientSockets 66096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson = Collections.synchronizedSet(new HashSet<Socket>()); 67096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson private boolean singleResponse; 6851e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson private final AtomicInteger requestCount = new AtomicInteger(); 69b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private int bodyLimit = Integer.MAX_VALUE; 7051e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson private ServerSocket serverSocket; 71706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson private SSLSocketFactory sslSocketFactory; 7251e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson private ExecutorService executor; 73706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson private boolean tunnelProxy; 74b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 75b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private int port = -1; 76b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 77b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson public int getPort() { 78b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson if (port == -1) { 79b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson throw new IllegalStateException("Cannot retrieve port before calling play()"); 80b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 81b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson return port; 82b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 83b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 8460476787f0e0f052366d8031c74e507ffd3d16a3Jesse Wilson public Proxy toProxyAddress() { 8560476787f0e0f052366d8031c74e507ffd3d16a3Jesse Wilson return new Proxy(Proxy.Type.HTTP, new InetSocketAddress("localhost", getPort())); 8660476787f0e0f052366d8031c74e507ffd3d16a3Jesse Wilson } 8760476787f0e0f052366d8031c74e507ffd3d16a3Jesse Wilson 88b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson /** 89b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * Returns a URL for connecting to this server. 90b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * 91b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * @param path the request path, such as "/". 92b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 9300feece22909b7dc79fc96d666d157390b93858eJesse Wilson public URL getUrl(String path) throws MalformedURLException, UnknownHostException { 9400feece22909b7dc79fc96d666d157390b93858eJesse Wilson String host = InetAddress.getLocalHost().getHostName(); 95096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson return sslSocketFactory != null 9600feece22909b7dc79fc96d666d157390b93858eJesse Wilson ? new URL("https://" + host + ":" + getPort() + path) 9700feece22909b7dc79fc96d666d157390b93858eJesse Wilson : new URL("http://" + host + ":" + getPort() + path); 98b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 99b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 100b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson /** 101b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * Sets the number of bytes of the POST body to keep in memory to the given 102b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * limit. 103b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 104b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson public void setBodyLimit(int maxBodyLength) { 105b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson this.bodyLimit = maxBodyLength; 106b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 107b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 108706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson /** 109706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson * Serve requests with HTTPS rather than otherwise. 110706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson * 111706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson * @param tunnelProxy whether to expect the HTTP CONNECT method before 112706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson * negotiating TLS. 113706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson */ 114706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson public void useHttps(SSLSocketFactory sslSocketFactory, boolean tunnelProxy) { 115706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson this.sslSocketFactory = sslSocketFactory; 116706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson this.tunnelProxy = tunnelProxy; 117b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 118b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 119b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson /** 120b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * Awaits the next HTTP request, removes it, and returns it. Callers should 121b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * use this to verify the request sent was as intended. 122b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 123b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson public RecordedRequest takeRequest() throws InterruptedException { 124b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson return requestQueue.take(); 125b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 126b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 12751e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson /** 12851e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson * Returns the number of HTTP requests received thus far by this server. 12951e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson * This may exceed the number of HTTP connections when connection reuse is 13051e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson * in practice. 13151e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson */ 13251e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson public int getRequestCount() { 13351e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson return requestCount.get(); 13451e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson } 13551e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson 136706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson public void enqueue(MockResponse response) { 137706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson responseQueue.add(response); 138706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson } 139706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson 140b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson /** 141096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson * By default, this class processes requests coming in by adding them to a 142096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson * queue and serves responses by removing them from another queue. This mode 143096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson * is appropriate for correctness testing. 144096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson * 145096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson * <p>Serving a single response causes the server to be stateless: requests 146096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson * are not enqueued, and responses are not dequeued. This mode is appropriate 147096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson * for benchmarking. 148096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson */ 149096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson public void setSingleResponse(boolean singleResponse) { 150096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson this.singleResponse = singleResponse; 151096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 152096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson 153096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson /** 154b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * Starts the server, serves all enqueued requests, and shuts the server 155b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * down. 156b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 157b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson public void play() throws IOException { 15851e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson executor = Executors.newCachedThreadPool(); 15951e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson serverSocket = new ServerSocket(0); 16051e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson serverSocket.setReuseAddress(true); 161706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson 16251e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson port = serverSocket.getLocalPort(); 163b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson executor.execute(namedRunnable("MockWebServer-accept-" + port, new Runnable() { 164b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson public void run() { 16551e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson try { 166096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson acceptConnections(); 167096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } catch (Throwable e) { 168b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson logger.log(Level.WARNING, "MockWebServer connection failed", e); 169096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 170b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 171096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson /* 172096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson * This gnarly block of code will release all sockets and 173096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson * all thread, even if any close fails. 174096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson */ 175096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson try { 176096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson serverSocket.close(); 177096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } catch (Throwable e) { 178b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson logger.log(Level.WARNING, "MockWebServer server socket close failed", e); 179096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 180b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson for (Iterator<Socket> s = openClientSockets.iterator(); s.hasNext();) { 181096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson try { 182096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson s.next().close(); 183096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson s.remove(); 184096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } catch (Throwable e) { 185b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson logger.log(Level.WARNING, "MockWebServer socket close failed", e); 18651e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson } 187096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 188096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson try { 189096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson executor.shutdown(); 190096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } catch (Throwable e) { 191b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson logger.log(Level.WARNING, "MockWebServer executor shutdown failed", e); 192b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 193b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 194096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson 195b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson private void acceptConnections() throws Exception { 196b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson while (!responseQueue.isEmpty()) { 197b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson Socket socket; 198b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson try { 199b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson socket = serverSocket.accept(); 200b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson } catch (SocketException ignored) { 201b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson continue; 202096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 203b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson MockResponse peek = responseQueue.peek(); 204b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson if (peek != null && peek.getSocketPolicy() == DISCONNECT_AT_START) { 2054559b1d37edcb5d7f1da086cf2e3290388d74f46Brian Carlstrom responseQueue.take(); 2064559b1d37edcb5d7f1da086cf2e3290388d74f46Brian Carlstrom socket.close(); 207b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson } else { 208b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson openClientSockets.add(socket); 209b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson serveConnection(socket); 2104559b1d37edcb5d7f1da086cf2e3290388d74f46Brian Carlstrom } 211096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 212096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 213096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson })); 214b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 215b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 21651e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson public void shutdown() throws IOException { 21751e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson if (serverSocket != null) { 218096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson serverSocket.close(); // should cause acceptConnections() to break out 21951e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson } 22051e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson } 22151e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson 222706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson private void serveConnection(final Socket raw) { 223096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson String name = "MockWebServer-" + raw.getRemoteSocketAddress(); 224b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson executor.execute(namedRunnable(name, new Runnable() { 225706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson int sequenceNumber = 0; 226b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 227b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson public void run() { 228b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson try { 229b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson processConnection(); 230b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson } catch (Exception e) { 231b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson logger.log(Level.WARNING, "MockWebServer connection failed", e); 232b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson } 233b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson } 234b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson 235b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson public void processConnection() throws Exception { 236706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson Socket socket; 237706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson if (sslSocketFactory != null) { 238706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson if (tunnelProxy) { 239e942f46f10bb9384a1b186b3d7b74f9704c57090Jesse Wilson if (!processOneRequest(raw.getInputStream(), raw.getOutputStream(), raw)) { 240706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson throw new IllegalStateException("Tunnel without any CONNECT!"); 241b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 242b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 243706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson socket = sslSocketFactory.createSocket( 244706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson raw, raw.getInetAddress().getHostAddress(), raw.getPort(), true); 245706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson ((SSLSocket) socket).setUseClientMode(false); 2468ac847a52e72f0cefbb20a6850ae04468d433a9eJesse Wilson openClientSockets.add(socket); 2478ac847a52e72f0cefbb20a6850ae04468d433a9eJesse Wilson openClientSockets.remove(raw); 248706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson } else { 249706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson socket = raw; 250b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 251b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 252706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson InputStream in = new BufferedInputStream(socket.getInputStream()); 253706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson OutputStream out = new BufferedOutputStream(socket.getOutputStream()); 254706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson 255b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson while (!responseQueue.isEmpty() && processOneRequest(in, out, socket)) {} 256b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson 257b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson if (sequenceNumber == 0) { 258b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson logger.warning("MockWebServer connection didn't make a request"); 259706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson } 260706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson 261b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson in.close(); 262b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson out.close(); 2638ac847a52e72f0cefbb20a6850ae04468d433a9eJesse Wilson socket.close(); 264b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson if (responseQueue.isEmpty()) { 265b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson shutdown(); 266b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson } 2678ac847a52e72f0cefbb20a6850ae04468d433a9eJesse Wilson openClientSockets.remove(socket); 268b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 269706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson 270706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson /** 271706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson * Reads a request and writes its response. Returns true if a request 272706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson * was processed. 273706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson */ 274e942f46f10bb9384a1b186b3d7b74f9704c57090Jesse Wilson private boolean processOneRequest(InputStream in, OutputStream out, Socket socket) 275706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson throws IOException, InterruptedException { 276706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson RecordedRequest request = readRequest(in, sequenceNumber); 277706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson if (request == null) { 278706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson return false; 279706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson } 280096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson MockResponse response = dispatch(request); 28151e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson writeResponse(out, response); 282e942f46f10bb9384a1b186b3d7b74f9704c57090Jesse Wilson if (response.getSocketPolicy() == SocketPolicy.DISCONNECT_AT_END) { 28351e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson in.close(); 28451e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson out.close(); 285e942f46f10bb9384a1b186b3d7b74f9704c57090Jesse Wilson } else if (response.getSocketPolicy() == SocketPolicy.SHUTDOWN_INPUT_AT_END) { 286e942f46f10bb9384a1b186b3d7b74f9704c57090Jesse Wilson socket.shutdownInput(); 287e942f46f10bb9384a1b186b3d7b74f9704c57090Jesse Wilson } else if (response.getSocketPolicy() == SocketPolicy.SHUTDOWN_OUTPUT_AT_END) { 288e942f46f10bb9384a1b186b3d7b74f9704c57090Jesse Wilson socket.shutdownOutput(); 28951e468abf2628ce964d3657042f3ac8f2c947504Jesse Wilson } 290706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson sequenceNumber++; 291706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson return true; 292706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson } 293096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson })); 294b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 295b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 296b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson /** 297b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * @param sequenceNumber the index of this request on this connection. 298b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 299b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private RecordedRequest readRequest(InputStream in, int sequenceNumber) throws IOException { 300b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson String request; 301b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson try { 302b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson request = readAsciiUntilCrlf(in); 303b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson } catch (IOException streamIsClosed) { 304b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson return null; // no request because we closed the stream 305b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson } 306b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson if (request.isEmpty()) { 307b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson return null; // no request because the stream is exhausted 308b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 309b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 310b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson List<String> headers = new ArrayList<String>(); 311b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson int contentLength = -1; 312b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson boolean chunked = false; 313b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson String header; 314b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson while (!(header = readAsciiUntilCrlf(in)).isEmpty()) { 315b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson headers.add(header); 316b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson String lowercaseHeader = header.toLowerCase(); 317b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) { 318b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson contentLength = Integer.parseInt(header.substring(15).trim()); 319b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 320b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson if (lowercaseHeader.startsWith("transfer-encoding:") && 321b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson lowercaseHeader.substring(18).trim().equals("chunked")) { 322b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson chunked = true; 323b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 324b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 325b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 326fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson boolean hasBody = false; 327b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson TruncatingOutputStream requestBody = new TruncatingOutputStream(); 328b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson List<Integer> chunkSizes = new ArrayList<Integer>(); 329b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson if (contentLength != -1) { 330fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson hasBody = true; 331b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson transfer(contentLength, in, requestBody); 332b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } else if (chunked) { 333fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson hasBody = true; 334b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson while (true) { 335b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson int chunkSize = Integer.parseInt(readAsciiUntilCrlf(in).trim(), 16); 336b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson if (chunkSize == 0) { 337b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson readEmptyLine(in); 338b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson break; 339b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 340b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson chunkSizes.add(chunkSize); 341b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson transfer(chunkSize, in, requestBody); 342b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson readEmptyLine(in); 343b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 344b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 345b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 346706d53593cd8841d378dbe298a8d1940db1e71dfJesse Wilson if (request.startsWith("GET ") || request.startsWith("CONNECT ")) { 347fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson if (hasBody) { 348fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson throw new IllegalArgumentException("GET requests should not have a body!"); 349fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson } 350fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson } else if (request.startsWith("POST ")) { 351fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson if (!hasBody) { 352fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson throw new IllegalArgumentException("POST requests must have a body!"); 353fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson } 354fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson } else { 355fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson throw new UnsupportedOperationException("Unexpected method: " + request); 356fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson } 357fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson 358b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson return new RecordedRequest(request, headers, chunkSizes, 359b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson requestBody.numBytesReceived, requestBody.toByteArray(), sequenceNumber); 360b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 361b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 362b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson /** 363b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * Returns a response to satisfy {@code request}. 364b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 365096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson private MockResponse dispatch(RecordedRequest request) throws InterruptedException { 366211d3bbada505912bb16e9d1a6c1a9f1f5c16cffJesse Wilson if (responseQueue.isEmpty()) { 367b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson throw new IllegalStateException("Unexpected request: " + request); 368b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 369096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson 370096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson if (singleResponse) { 371096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson return responseQueue.peek(); 372096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } else { 373096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson requestCount.incrementAndGet(); 374096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson requestQueue.add(request); 375096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson return responseQueue.take(); 376096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 377b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 378b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 379b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private void writeResponse(OutputStream out, MockResponse response) throws IOException { 380b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson out.write((response.getStatus() + "\r\n").getBytes(ASCII)); 381b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson for (String header : response.getHeaders()) { 382b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson out.write((header + "\r\n").getBytes(ASCII)); 383b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 384b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson out.write(("\r\n").getBytes(ASCII)); 385b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson out.write(response.getBody()); 386b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson out.flush(); 387b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 388b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 389b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson /** 390b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * Transfer bytes from {@code in} to {@code out} until either {@code length} 391b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * bytes have been transferred or {@code in} is exhausted. 392b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 393b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private void transfer(int length, InputStream in, OutputStream out) throws IOException { 394b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson byte[] buffer = new byte[1024]; 395b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson while (length > 0) { 396b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson int count = in.read(buffer, 0, Math.min(buffer.length, length)); 397b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson if (count == -1) { 398b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson return; 399b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 400b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson out.write(buffer, 0, count); 401b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson length -= count; 402b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 403b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 404b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 405b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson /** 406b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * Returns the text from {@code in} until the next "\r\n", or null if 407b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * {@code in} is exhausted. 408b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 409b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private String readAsciiUntilCrlf(InputStream in) throws IOException { 410b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson StringBuilder builder = new StringBuilder(); 411b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson while (true) { 412b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson int c = in.read(); 413b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson if (c == '\n' && builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') { 414b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson builder.deleteCharAt(builder.length() - 1); 415b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson return builder.toString(); 416b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } else if (c == -1) { 417b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson return builder.toString(); 418b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } else { 419b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson builder.append((char) c); 420b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 421b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 422b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 423b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 424b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private void readEmptyLine(InputStream in) throws IOException { 425b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson String line = readAsciiUntilCrlf(in); 426b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson if (!line.isEmpty()) { 427b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson throw new IllegalStateException("Expected empty but was: " + line); 428b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 429b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 430b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson 431b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson /** 432b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson * An output stream that drops data after bodyLimit bytes. 433b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson */ 434b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private class TruncatingOutputStream extends ByteArrayOutputStream { 435b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson private int numBytesReceived = 0; 436b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson @Override public void write(byte[] buffer, int offset, int len) { 437b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson numBytesReceived += len; 438b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson super.write(buffer, offset, Math.min(len, bodyLimit - count)); 439b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 440b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson @Override public void write(int oneByte) { 441b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson numBytesReceived++; 442b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson if (count < bodyLimit) { 443b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson super.write(oneByte); 444b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 445b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 446b1b5baac449d2725002338735f4db34bec8fd001Jesse Wilson } 447096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson 448b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson private static Runnable namedRunnable(final String name, final Runnable runnable) { 449b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson return new Runnable() { 450b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson public void run() { 451096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson String originalName = Thread.currentThread().getName(); 452096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson Thread.currentThread().setName(name); 453096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson try { 454b7f4d6c3968c372767b2510f38a3d506067aced6Jesse Wilson runnable.run(); 455096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } finally { 456096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson Thread.currentThread().setName(originalName); 457096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 458096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 459096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson }; 460096aac7b8a607d3da237900f52cab1c5066bf992Jesse Wilson } 461fd4350050d7a0d333f9d346560b167040c3f44dfJesse Wilson} 462