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