• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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