• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2015 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net;
6 
7 import static io.netty.buffer.Unpooled.copiedBuffer;
8 import static io.netty.buffer.Unpooled.unreleasableBuffer;
9 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
10 import static io.netty.handler.codec.http.HttpResponseStatus.OK;
11 import static io.netty.handler.logging.LogLevel.INFO;
12 
13 import org.chromium.base.Log;
14 
15 import java.io.ByteArrayOutputStream;
16 import java.io.IOException;
17 import java.io.UnsupportedEncodingException;
18 import java.util.HashMap;
19 import java.util.Locale;
20 import java.util.Map;
21 import java.util.concurrent.CountDownLatch;
22 
23 import io.netty.buffer.ByteBuf;
24 import io.netty.buffer.ByteBufUtil;
25 import io.netty.channel.ChannelHandlerContext;
26 import io.netty.handler.codec.http2.AbstractHttp2ConnectionHandlerBuilder;
27 import io.netty.handler.codec.http2.DefaultHttp2Headers;
28 import io.netty.handler.codec.http2.Http2ConnectionDecoder;
29 import io.netty.handler.codec.http2.Http2ConnectionEncoder;
30 import io.netty.handler.codec.http2.Http2ConnectionHandler;
31 import io.netty.handler.codec.http2.Http2Exception;
32 import io.netty.handler.codec.http2.Http2Flags;
33 import io.netty.handler.codec.http2.Http2FrameListener;
34 import io.netty.handler.codec.http2.Http2FrameLogger;
35 import io.netty.handler.codec.http2.Http2Headers;
36 import io.netty.handler.codec.http2.Http2Settings;
37 import io.netty.util.CharsetUtil;
38 
39 /**
40  * HTTP/2 test handler for Cronet BidirectionalStream tests.
41  */
42 public final class Http2TestHandler extends Http2ConnectionHandler implements Http2FrameListener {
43     // Some Url Paths that have special meaning.
44     public static final String ECHO_ALL_HEADERS_PATH = "/echoallheaders";
45     public static final String ECHO_HEADER_PATH = "/echoheader";
46     public static final String ECHO_METHOD_PATH = "/echomethod";
47     public static final String ECHO_STREAM_PATH = "/echostream";
48     public static final String ECHO_TRAILERS_PATH = "/echotrailers";
49     public static final String SERVE_SIMPLE_BROTLI_RESPONSE = "/simplebrotli";
50     public static final String REPORTING_COLLECTOR_PATH = "/reporting-collector";
51     public static final String SUCCESS_WITH_NEL_HEADERS_PATH = "/success-with-nel";
52     public static final String COMBINED_HEADERS_PATH = "/combinedheaders";
53     public static final String HANGING_REQUEST_PATH = "/hanging-request";
54 
55     private static final String TAG = Http2TestHandler.class.getSimpleName();
56     private static final Http2FrameLogger sLogger =
57             new Http2FrameLogger(INFO, Http2TestHandler.class);
58     private static final ByteBuf RESPONSE_BYTES =
59             unreleasableBuffer(copiedBuffer("HTTP/2 Test Server", CharsetUtil.UTF_8));
60 
61     private HashMap<Integer, RequestResponder> mResponderMap = new HashMap<>();
62 
63     private ReportingCollector mReportingCollector;
64     private String mServerUrl;
65     private CountDownLatch mHangingUrlLatch;
66 
67     /**
68      * Builder for HTTP/2 test handler.
69      */
70     public static final class Builder
71             extends AbstractHttp2ConnectionHandlerBuilder<Http2TestHandler, Builder> {
Builder()72         public Builder() {
73             frameLogger(sLogger);
74         }
75 
setReportingCollector(ReportingCollector reportingCollector)76         public Builder setReportingCollector(ReportingCollector reportingCollector) {
77             mReportingCollector = reportingCollector;
78             return this;
79         }
80 
setServerUrl(String serverUrl)81         public Builder setServerUrl(String serverUrl) {
82             mServerUrl = serverUrl;
83             return this;
84         }
85 
setHangingUrlLatch(CountDownLatch hangingUrlLatch)86         public Builder setHangingUrlLatch(CountDownLatch hangingUrlLatch) {
87             mHangingUrlLatch = hangingUrlLatch;
88             return this;
89         }
90 
91         @Override
build()92         public Http2TestHandler build() {
93             return super.build();
94         }
95 
96         @Override
build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings)97         protected Http2TestHandler build(Http2ConnectionDecoder decoder,
98                 Http2ConnectionEncoder encoder, Http2Settings initialSettings) {
99             Http2TestHandler handler = new Http2TestHandler(decoder, encoder, initialSettings,
100                     mReportingCollector, mServerUrl, mHangingUrlLatch);
101             frameListener(handler);
102             return handler;
103         }
104 
105         private ReportingCollector mReportingCollector;
106         private String mServerUrl;
107         private CountDownLatch mHangingUrlLatch;
108     }
109 
110     private class RequestResponder {
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)111         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
112                 Http2Headers headers) {
113             encoder().writeHeaders(ctx, streamId, createResponseHeadersFromRequestHeaders(headers),
114                     0, endOfStream, ctx.newPromise());
115             ctx.flush();
116         }
117 
onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)118         int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
119                 boolean endOfStream) {
120             int processed = data.readableBytes() + padding;
121             encoder().writeData(ctx, streamId, data.retain(), 0, true, ctx.newPromise());
122             ctx.flush();
123             return processed;
124         }
125 
sendResponseString(ChannelHandlerContext ctx, int streamId, String responseString)126         void sendResponseString(ChannelHandlerContext ctx, int streamId, String responseString) {
127             ByteBuf content = ctx.alloc().buffer();
128             ByteBufUtil.writeAscii(content, responseString);
129             encoder().writeHeaders(
130                     ctx, streamId, createDefaultResponseHeaders(), 0, false, ctx.newPromise());
131             encoder().writeData(ctx, streamId, content, 0, true, ctx.newPromise());
132             ctx.flush();
133         }
134     }
135 
136     private class EchoStreamResponder extends RequestResponder {
137         @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)138         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
139                 Http2Headers headers) {
140             // Send a frame for the response headers.
141             encoder().writeHeaders(ctx, streamId, createResponseHeadersFromRequestHeaders(headers),
142                     0, endOfStream, ctx.newPromise());
143             ctx.flush();
144         }
145 
146         @Override
onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)147         int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
148                 boolean endOfStream) {
149             int processed = data.readableBytes() + padding;
150             encoder().writeData(ctx, streamId, data.retain(), 0, endOfStream, ctx.newPromise());
151             ctx.flush();
152             return processed;
153         }
154     }
155 
156     private class CombinedHeadersResponder extends RequestResponder {
157         @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)158         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
159                 Http2Headers headers) {
160             ByteBuf content = ctx.alloc().buffer();
161             ByteBufUtil.writeAscii(content, "GET");
162             Http2Headers responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText());
163             // Upon receiving, the following two headers will be jointed by '\0'.
164             responseHeaders.add("foo", "bar");
165             responseHeaders.add("foo", "bar2");
166             encoder().writeHeaders(ctx, streamId, responseHeaders, 0, false, ctx.newPromise());
167             encoder().writeData(ctx, streamId, content, 0, true, ctx.newPromise());
168             ctx.flush();
169         }
170     }
171 
172     private class HangingRequestResponder extends RequestResponder {
173         @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)174         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
175                 Http2Headers headers) {
176             try {
177                 mHangingUrlLatch.await();
178             } catch (InterruptedException e) {
179             }
180         }
181     }
182 
183     private class EchoHeaderResponder extends RequestResponder {
184         @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)185         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
186                 Http2Headers headers) {
187             String[] splitPath = headers.path().toString().split("\\?");
188             if (splitPath.length <= 1) {
189                 sendResponseString(ctx, streamId, "Header name not found.");
190                 return;
191             }
192 
193             String headerName = splitPath[1].toLowerCase(Locale.US);
194             if (headers.get(headerName) == null) {
195                 sendResponseString(ctx, streamId, "Header not found:" + headerName);
196                 return;
197             }
198 
199             sendResponseString(ctx, streamId, headers.get(headerName).toString());
200         }
201     }
202 
203     private class EchoAllHeadersResponder extends RequestResponder {
204         @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)205         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
206                 Http2Headers headers) {
207             StringBuilder response = new StringBuilder();
208             for (Map.Entry<CharSequence, CharSequence> header : headers) {
209                 response.append(header.getKey() + ": " + header.getValue() + "\r\n");
210             }
211             sendResponseString(ctx, streamId, response.toString());
212         }
213     }
214 
215     private class EchoMethodResponder extends RequestResponder {
216         @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)217         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
218                 Http2Headers headers) {
219             sendResponseString(ctx, streamId, headers.method().toString());
220         }
221     }
222 
223     private class EchoTrailersResponder extends RequestResponder {
224         @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)225         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
226                 Http2Headers headers) {
227             encoder().writeHeaders(
228                     ctx, streamId, createDefaultResponseHeaders(), 0, false, ctx.newPromise());
229             encoder().writeData(
230                     ctx, streamId, RESPONSE_BYTES.duplicate(), 0, false, ctx.newPromise());
231             Http2Headers responseTrailers = createResponseHeadersFromRequestHeaders(headers).add(
232                     "trailer", "value1", "Value2");
233             encoder().writeHeaders(ctx, streamId, responseTrailers, 0, true, ctx.newPromise());
234             ctx.flush();
235         }
236     }
237 
238     // A RequestResponder that serves a simple Brotli-encoded response.
239     private class ServeSimpleBrotliResponder extends RequestResponder {
240         @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)241         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
242                 Http2Headers headers) {
243             Http2Headers responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText());
244             byte[] quickfoxCompressed = {0x0b, 0x15, -0x80, 0x54, 0x68, 0x65, 0x20, 0x71, 0x75,
245                     0x69, 0x63, 0x6b, 0x20, 0x62, 0x72, 0x6f, 0x77, 0x6e, 0x20, 0x66, 0x6f, 0x78,
246                     0x20, 0x6a, 0x75, 0x6d, 0x70, 0x73, 0x20, 0x6f, 0x76, 0x65, 0x72, 0x20, 0x74,
247                     0x68, 0x65, 0x20, 0x6c, 0x61, 0x7a, 0x79, 0x20, 0x64, 0x6f, 0x67, 0x03};
248             ByteBuf content = copiedBuffer(quickfoxCompressed);
249             responseHeaders.add("content-encoding", "br");
250             encoder().writeHeaders(ctx, streamId, responseHeaders, 0, false, ctx.newPromise());
251             encoder().writeData(ctx, streamId, content, 0, true, ctx.newPromise());
252             ctx.flush();
253         }
254     }
255 
256     // A RequestResponder that implements a Reporting collector.
257     private class ReportingCollectorResponder extends RequestResponder {
258         private ByteArrayOutputStream mPartialPayload = new ByteArrayOutputStream();
259 
260         @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)261         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
262                 Http2Headers headers) {}
263 
264         @Override
onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)265         int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
266                 boolean endOfStream) {
267             int processed = data.readableBytes() + padding;
268             try {
269                 data.readBytes(mPartialPayload, data.readableBytes());
270             } catch (IOException e) {
271             }
272             if (endOfStream) {
273                 processPayload(ctx, streamId);
274             }
275             return processed;
276         }
277 
processPayload(ChannelHandlerContext ctx, int streamId)278         private void processPayload(ChannelHandlerContext ctx, int streamId) {
279             boolean succeeded = false;
280             try {
281                 String payload = mPartialPayload.toString(CharsetUtil.UTF_8.name());
282                 succeeded = mReportingCollector.addReports(payload);
283             } catch (UnsupportedEncodingException e) {
284             }
285             Http2Headers responseHeaders;
286             if (succeeded) {
287                 responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText());
288             } else {
289                 responseHeaders = new DefaultHttp2Headers().status(BAD_REQUEST.codeAsText());
290             }
291             encoder().writeHeaders(ctx, streamId, responseHeaders, 0, true, ctx.newPromise());
292             ctx.flush();
293         }
294     }
295 
296     // A RequestResponder that serves a successful response with Reporting and NEL headers
297     private class SuccessWithNELHeadersResponder extends RequestResponder {
298         @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream, Http2Headers headers)299         void onHeadersRead(ChannelHandlerContext ctx, int streamId, boolean endOfStream,
300                 Http2Headers headers) {
301             Http2Headers responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText());
302             responseHeaders.add("report-to", getReportToHeader());
303             responseHeaders.add("nel", getNELHeader());
304             encoder().writeHeaders(ctx, streamId, responseHeaders, 0, true, ctx.newPromise());
305             ctx.flush();
306         }
307 
308         @Override
onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)309         int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
310                 boolean endOfStream) {
311             int processed = data.readableBytes() + padding;
312             return processed;
313         }
314 
getReportToHeader()315         private String getReportToHeader() {
316             return String.format("{\"group\": \"nel\", \"max_age\": 86400, "
317                             + "\"endpoints\": [{\"url\": \"%s%s\"}]}",
318                     mServerUrl, REPORTING_COLLECTOR_PATH);
319         }
320 
getNELHeader()321         private String getNELHeader() {
322             return "{\"report_to\": \"nel\", \"max_age\": 86400, \"success_fraction\": 1.0}";
323         }
324     }
325 
createDefaultResponseHeaders()326     private static Http2Headers createDefaultResponseHeaders() {
327         return new DefaultHttp2Headers().status(OK.codeAsText());
328     }
329 
createResponseHeadersFromRequestHeaders( Http2Headers requestHeaders)330     private static Http2Headers createResponseHeadersFromRequestHeaders(
331             Http2Headers requestHeaders) {
332         // Create response headers by echoing request headers.
333         Http2Headers responseHeaders = new DefaultHttp2Headers().status(OK.codeAsText());
334         for (Map.Entry<CharSequence, CharSequence> header : requestHeaders) {
335             if (!header.getKey().toString().startsWith(":")) {
336                 responseHeaders.add("echo-" + header.getKey(), header.getValue());
337             }
338         }
339 
340         responseHeaders.add("echo-method", requestHeaders.get(":method").toString());
341         return responseHeaders;
342     }
343 
Http2TestHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings, ReportingCollector reportingCollector, String serverUrl, CountDownLatch hangingUrlLatch)344     private Http2TestHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
345             Http2Settings initialSettings, ReportingCollector reportingCollector, String serverUrl,
346             CountDownLatch hangingUrlLatch) {
347         super(decoder, encoder, initialSettings);
348         mReportingCollector = reportingCollector;
349         mServerUrl = serverUrl;
350         mHangingUrlLatch = hangingUrlLatch;
351     }
352 
353     @Override
exceptionCaught(ChannelHandlerContext ctx, Throwable cause)354     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
355         super.exceptionCaught(ctx, cause);
356         Log.e(TAG, "An exception was caught", cause);
357         ctx.close();
358         throw new Exception("Exception Caught", cause);
359     }
360 
361     @Override
onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)362     public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
363             boolean endOfStream) throws Http2Exception {
364         RequestResponder responder = mResponderMap.get(streamId);
365         if (endOfStream) {
366             mResponderMap.remove(streamId);
367         }
368         return responder.onDataRead(ctx, streamId, data, padding, endOfStream);
369     }
370 
371     @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, boolean endOfStream)372     public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
373             int padding, boolean endOfStream) throws Http2Exception {
374         String path = headers.path().toString();
375         RequestResponder responder;
376         if (path.startsWith(ECHO_STREAM_PATH)) {
377             responder = new EchoStreamResponder();
378         } else if (path.startsWith(ECHO_TRAILERS_PATH)) {
379             responder = new EchoTrailersResponder();
380         } else if (path.startsWith(ECHO_ALL_HEADERS_PATH)) {
381             responder = new EchoAllHeadersResponder();
382         } else if (path.startsWith(ECHO_HEADER_PATH)) {
383             responder = new EchoHeaderResponder();
384         } else if (path.startsWith(ECHO_METHOD_PATH)) {
385             responder = new EchoMethodResponder();
386         } else if (path.startsWith(SERVE_SIMPLE_BROTLI_RESPONSE)) {
387             responder = new ServeSimpleBrotliResponder();
388         } else if (path.startsWith(REPORTING_COLLECTOR_PATH)) {
389             responder = new ReportingCollectorResponder();
390         } else if (path.startsWith(SUCCESS_WITH_NEL_HEADERS_PATH)) {
391             responder = new SuccessWithNELHeadersResponder();
392         } else if (path.startsWith(COMBINED_HEADERS_PATH)) {
393             responder = new CombinedHeadersResponder();
394         } else if (path.startsWith(HANGING_REQUEST_PATH)) {
395             responder = new HangingRequestResponder();
396         } else {
397             responder = new RequestResponder();
398         }
399 
400         responder.onHeadersRead(ctx, streamId, endOfStream, headers);
401 
402         if (!endOfStream) {
403             mResponderMap.put(streamId, responder);
404         }
405     }
406 
407     @Override
onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream)408     public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
409             int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream)
410             throws Http2Exception {
411         onHeadersRead(ctx, streamId, headers, padding, endOfStream);
412     }
413 
414     @Override
onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight, boolean exclusive)415     public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency,
416             short weight, boolean exclusive) throws Http2Exception {}
417 
418     @Override
onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode)419     public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode)
420             throws Http2Exception {}
421 
422     @Override
onSettingsAckRead(ChannelHandlerContext ctx)423     public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {}
424 
425     @Override
onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings)426     public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings)
427             throws Http2Exception {}
428 
429     @Override
onPingRead(ChannelHandlerContext ctx, ByteBuf data)430     public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {}
431 
432     @Override
onPingAckRead(ChannelHandlerContext ctx, ByteBuf data)433     public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {}
434 
435     @Override
onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers, int padding)436     public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
437             Http2Headers headers, int padding) throws Http2Exception {}
438 
439     @Override
onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)440     public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode,
441             ByteBuf debugData) throws Http2Exception {}
442 
443     @Override
onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)444     public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
445             throws Http2Exception {}
446 
447     @Override
onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, ByteBuf payload)448     public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId,
449             Http2Flags flags, ByteBuf payload) throws Http2Exception {}
450 }
451