1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package tests.http; 18 19 import java.io.BufferedInputStream; 20 import java.io.BufferedOutputStream; 21 import java.io.ByteArrayOutputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.OutputStream; 25 import java.net.MalformedURLException; 26 import java.net.ServerSocket; 27 import java.net.Socket; 28 import java.net.URL; 29 import java.util.ArrayList; 30 import java.util.LinkedList; 31 import java.util.List; 32 import java.util.Queue; 33 import java.util.concurrent.BlockingQueue; 34 import java.util.concurrent.Callable; 35 import java.util.concurrent.ExecutionException; 36 import java.util.concurrent.ExecutorService; 37 import java.util.concurrent.Executors; 38 import java.util.concurrent.Future; 39 import java.util.concurrent.LinkedBlockingQueue; 40 import java.util.concurrent.TimeUnit; 41 import java.util.concurrent.TimeoutException; 42 43 /** 44 * A scriptable web server. Callers supply canned responses and the server 45 * replays them upon request in sequence. 46 * 47 * TODO: merge with the version from libcore/support/src/tests/java once it's in. 48 */ 49 public final class MockWebServer { 50 static final String ASCII = "US-ASCII"; 51 52 private final BlockingQueue<RecordedRequest> requestQueue 53 = new LinkedBlockingQueue<RecordedRequest>(); 54 private final BlockingQueue<MockResponse> responseQueue 55 = new LinkedBlockingQueue<MockResponse>(); 56 private int bodyLimit = Integer.MAX_VALUE; 57 private final ExecutorService executor = Executors.newCachedThreadPool(); 58 // keep Futures around so we can rethrow any exceptions thrown by Callables 59 private final Queue<Future<?>> futures = new LinkedList<Future<?>>(); 60 61 private int port = -1; 62 getPort()63 public int getPort() { 64 if (port == -1) { 65 throw new IllegalStateException("Cannot retrieve port before calling play()"); 66 } 67 return port; 68 } 69 70 /** 71 * Returns a URL for connecting to this server. 72 * 73 * @param path the request path, such as "/". 74 */ getUrl(String path)75 public URL getUrl(String path) throws MalformedURLException { 76 return new URL("http://localhost:" + getPort() + path); 77 } 78 79 /** 80 * Sets the number of bytes of the POST body to keep in memory to the given 81 * limit. 82 */ setBodyLimit(int maxBodyLength)83 public void setBodyLimit(int maxBodyLength) { 84 this.bodyLimit = maxBodyLength; 85 } 86 enqueue(MockResponse response)87 public void enqueue(MockResponse response) { 88 responseQueue.add(response); 89 } 90 91 /** 92 * Awaits the next HTTP request, removes it, and returns it. Callers should 93 * use this to verify the request sent was as intended. 94 */ takeRequest()95 public RecordedRequest takeRequest() throws InterruptedException { 96 return requestQueue.take(); 97 } 98 takeRequestWithTimeout(long timeoutMillis)99 public RecordedRequest takeRequestWithTimeout(long timeoutMillis) throws InterruptedException { 100 return requestQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS); 101 } 102 drainRequests()103 public List<RecordedRequest> drainRequests() { 104 List<RecordedRequest> requests = new ArrayList<RecordedRequest>(); 105 requestQueue.drainTo(requests); 106 return requests; 107 } 108 109 /** 110 * Starts the server, serves all enqueued requests, and shuts the server 111 * down. 112 */ play()113 public void play() throws IOException { 114 final ServerSocket ss = new ServerSocket(0); 115 ss.setReuseAddress(true); 116 port = ss.getLocalPort(); 117 submitCallable(new Callable<Void>() { 118 public Void call() throws Exception { 119 int count = 0; 120 while (true) { 121 if (count > 0 && responseQueue.isEmpty()) { 122 ss.close(); 123 executor.shutdown(); 124 return null; 125 } 126 127 serveConnection(ss.accept()); 128 count++; 129 } 130 } 131 }); 132 } 133 serveConnection(final Socket s)134 private void serveConnection(final Socket s) { 135 submitCallable(new Callable<Void>() { 136 public Void call() throws Exception { 137 InputStream in = new BufferedInputStream(s.getInputStream()); 138 OutputStream out = new BufferedOutputStream(s.getOutputStream()); 139 140 int sequenceNumber = 0; 141 while (true) { 142 RecordedRequest request = readRequest(in, sequenceNumber); 143 if (request == null) { 144 if (sequenceNumber == 0) { 145 throw new IllegalStateException("Connection without any request!"); 146 } else { 147 break; 148 } 149 } 150 requestQueue.add(request); 151 MockResponse response = computeResponse(request); 152 writeResponse(out, response); 153 if (response.shouldCloseConnectionAfter()) { 154 break; 155 } 156 sequenceNumber++; 157 } 158 159 in.close(); 160 out.close(); 161 return null; 162 } 163 }); 164 } 165 submitCallable(Callable<?> callable)166 private void submitCallable(Callable<?> callable) { 167 Future<?> future = executor.submit(callable); 168 futures.add(future); 169 } 170 171 /** 172 * Check for and raise any exceptions that have been thrown by child threads. Will not block on 173 * children still running. 174 * @throws ExecutionException for the first child thread that threw an exception 175 */ checkForExceptions()176 public void checkForExceptions() throws ExecutionException, InterruptedException { 177 final int originalSize = futures.size(); 178 for (int i = 0; i < originalSize; i++) { 179 Future<?> future = futures.remove(); 180 try { 181 future.get(0, TimeUnit.SECONDS); 182 } catch (TimeoutException e) { 183 futures.add(future); // still running 184 } 185 } 186 } 187 188 /** 189 * @param sequenceNumber the index of this request on this connection. 190 */ readRequest(InputStream in, int sequenceNumber)191 private RecordedRequest readRequest(InputStream in, int sequenceNumber) throws IOException { 192 String request = readAsciiUntilCrlf(in); 193 if (request.equals("")) { 194 return null; // end of data; no more requests 195 } 196 197 List<String> headers = new ArrayList<String>(); 198 int contentLength = -1; 199 boolean chunked = false; 200 String header; 201 while (!(header = readAsciiUntilCrlf(in)).equals("")) { 202 headers.add(header); 203 String lowercaseHeader = header.toLowerCase(); 204 if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) { 205 contentLength = Integer.parseInt(header.substring(15).trim()); 206 } 207 if (lowercaseHeader.startsWith("transfer-encoding:") && 208 lowercaseHeader.substring(18).trim().equals("chunked")) { 209 chunked = true; 210 } 211 } 212 213 boolean hasBody = false; 214 TruncatingOutputStream requestBody = new TruncatingOutputStream(); 215 List<Integer> chunkSizes = new ArrayList<Integer>(); 216 if (contentLength != -1) { 217 hasBody = true; 218 transfer(contentLength, in, requestBody); 219 } else if (chunked) { 220 hasBody = true; 221 while (true) { 222 int chunkSize = Integer.parseInt(readAsciiUntilCrlf(in).trim(), 16); 223 if (chunkSize == 0) { 224 readEmptyLine(in); 225 break; 226 } 227 chunkSizes.add(chunkSize); 228 transfer(chunkSize, in, requestBody); 229 readEmptyLine(in); 230 } 231 } 232 233 if (request.startsWith("GET ")) { 234 if (hasBody) { 235 throw new IllegalArgumentException("GET requests should not have a body!"); 236 } 237 } else if (request.startsWith("POST ")) { 238 if (!hasBody) { 239 throw new IllegalArgumentException("POST requests must have a body!"); 240 } 241 } else { 242 throw new UnsupportedOperationException("Unexpected method: " + request); 243 } 244 245 return new RecordedRequest(request, headers, chunkSizes, 246 requestBody.numBytesReceived, requestBody.toByteArray(), sequenceNumber); 247 } 248 249 /** 250 * Returns a response to satisfy {@code request}. 251 */ computeResponse(RecordedRequest request)252 private MockResponse computeResponse(RecordedRequest request) throws InterruptedException { 253 if (responseQueue.isEmpty()) { 254 throw new IllegalStateException("Unexpected request: " + request); 255 } 256 return responseQueue.take(); 257 } 258 writeResponse(OutputStream out, MockResponse response)259 private void writeResponse(OutputStream out, MockResponse response) throws IOException { 260 out.write((response.getStatus() + "\r\n").getBytes(ASCII)); 261 for (String header : response.getHeaders()) { 262 out.write((header + "\r\n").getBytes(ASCII)); 263 } 264 out.write(("\r\n").getBytes(ASCII)); 265 out.write(response.getBody()); 266 out.flush(); 267 } 268 269 /** 270 * Transfer bytes from {@code in} to {@code out} until either {@code length} 271 * bytes have been transferred or {@code in} is exhausted. 272 */ transfer(int length, InputStream in, OutputStream out)273 private void transfer(int length, InputStream in, OutputStream out) throws IOException { 274 byte[] buffer = new byte[1024]; 275 while (length > 0) { 276 int count = in.read(buffer, 0, Math.min(buffer.length, length)); 277 if (count == -1) { 278 return; 279 } 280 out.write(buffer, 0, count); 281 length -= count; 282 } 283 } 284 285 /** 286 * Returns the text from {@code in} until the next "\r\n", or null if 287 * {@code in} is exhausted. 288 */ readAsciiUntilCrlf(InputStream in)289 private String readAsciiUntilCrlf(InputStream in) throws IOException { 290 StringBuilder builder = new StringBuilder(); 291 while (true) { 292 int c = in.read(); 293 if (c == '\n' && builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') { 294 builder.deleteCharAt(builder.length() - 1); 295 return builder.toString(); 296 } else if (c == -1) { 297 return builder.toString(); 298 } else { 299 builder.append((char) c); 300 } 301 } 302 } 303 readEmptyLine(InputStream in)304 private void readEmptyLine(InputStream in) throws IOException { 305 String line = readAsciiUntilCrlf(in); 306 if (!line.equals("")) { 307 throw new IllegalStateException("Expected empty but was: " + line); 308 } 309 } 310 311 /** 312 * An output stream that drops data after bodyLimit bytes. 313 */ 314 private class TruncatingOutputStream extends ByteArrayOutputStream { 315 private int numBytesReceived = 0; write(byte[] buffer, int offset, int len)316 @Override public void write(byte[] buffer, int offset, int len) { 317 numBytesReceived += len; 318 super.write(buffer, offset, Math.min(len, bodyLimit - count)); 319 } write(int oneByte)320 @Override public void write(int oneByte) { 321 numBytesReceived++; 322 if (count < bodyLimit) { 323 super.write(oneByte); 324 } 325 } 326 } 327 } 328