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