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 android.content.Context; 8 import android.os.Build; 9 10 import org.chromium.base.Log; 11 import org.chromium.net.test.util.CertTestUtil; 12 13 import java.io.File; 14 import java.util.concurrent.Callable; 15 import java.util.concurrent.CountDownLatch; 16 import java.util.concurrent.ExecutorService; 17 import java.util.concurrent.Executors; 18 19 import io.netty.bootstrap.ServerBootstrap; 20 import io.netty.channel.Channel; 21 import io.netty.channel.ChannelHandlerContext; 22 import io.netty.channel.ChannelInitializer; 23 import io.netty.channel.ChannelOption; 24 import io.netty.channel.EventLoopGroup; 25 import io.netty.channel.nio.NioEventLoopGroup; 26 import io.netty.channel.socket.SocketChannel; 27 import io.netty.channel.socket.nio.NioServerSocketChannel; 28 import io.netty.handler.codec.http2.Http2SecurityUtil; 29 import io.netty.handler.logging.LogLevel; 30 import io.netty.handler.logging.LoggingHandler; 31 import io.netty.handler.ssl.ApplicationProtocolConfig; 32 import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; 33 import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; 34 import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; 35 import io.netty.handler.ssl.ApplicationProtocolNames; 36 import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; 37 import io.netty.handler.ssl.OpenSslServerContext; 38 import io.netty.handler.ssl.SslContext; 39 import io.netty.handler.ssl.SupportedCipherSuiteFilter; 40 41 /** Wrapper class to start a HTTP/2 test server. */ 42 public final class Http2TestServer { 43 private static Channel sServerChannel; 44 private static final String TAG = Http2TestServer.class.getSimpleName(); 45 46 private static final String HOST = "localhost"; 47 // Server port. 48 private static final int PORT = 8443; 49 50 private static ReportingCollector sReportingCollector; 51 52 public static final String SERVER_CERT_PEM; 53 private static final String SERVER_KEY_PKCS8_PEM; 54 // Used to start http2 test server. 55 private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(1); 56 57 static { 58 // TODO(crbug/1490552): Fallback to MockCertVerifier when custom CAs are not supported. 59 // Currently, MockCertVerifier uses different certificates, so make the server also use 60 // those. 61 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { 62 SERVER_CERT_PEM = "quic-chain.pem"; 63 SERVER_KEY_PKCS8_PEM = "quic-leaf-cert.key.pkcs8.pem"; 64 } else { 65 SERVER_CERT_PEM = "cronet-quic-chain.pem"; 66 SERVER_KEY_PKCS8_PEM = "cronet-quic-leaf-cert.key.pkcs8.pem"; 67 } 68 } 69 shutdownHttp2TestServer()70 public static boolean shutdownHttp2TestServer() throws Exception { 71 if (sServerChannel != null) { 72 sServerChannel.close().sync(); 73 sServerChannel = null; 74 sReportingCollector = null; 75 return true; 76 } 77 return false; 78 } 79 getServerHost()80 public static String getServerHost() { 81 return HOST; 82 } 83 getServerPort()84 public static int getServerPort() { 85 return PORT; 86 } 87 getServerUrl()88 public static String getServerUrl() { 89 return "https://" + HOST + ":" + PORT; 90 } 91 getReportingCollector()92 public static ReportingCollector getReportingCollector() { 93 return sReportingCollector; 94 } 95 getEchoAllHeadersUrl()96 public static String getEchoAllHeadersUrl() { 97 return getServerUrl() + Http2TestHandler.ECHO_ALL_HEADERS_PATH; 98 } 99 getEchoHeaderUrl(String headerName)100 public static String getEchoHeaderUrl(String headerName) { 101 return getServerUrl() + Http2TestHandler.ECHO_HEADER_PATH + "?" + headerName; 102 } 103 getEchoMethodUrl()104 public static String getEchoMethodUrl() { 105 return getServerUrl() + Http2TestHandler.ECHO_METHOD_PATH; 106 } 107 108 /** 109 * When using this you must provide a CountDownLatch in the call to startHttp2TestServer. 110 * The request handler will continue to hang until the provided CountDownLatch reaches 0. 111 * 112 * @return url of the server resource which will hang indefinitely. 113 */ getHangingRequestUrl()114 public static String getHangingRequestUrl() { 115 return getServerUrl() + Http2TestHandler.HANGING_REQUEST_PATH; 116 } 117 118 /** @return url of the server resource which will echo every received stream data frame. */ getEchoStreamUrl()119 public static String getEchoStreamUrl() { 120 return getServerUrl() + Http2TestHandler.ECHO_STREAM_PATH; 121 } 122 123 /** @return url of the server resource which will echo request headers as response trailers. */ getEchoTrailersUrl()124 public static String getEchoTrailersUrl() { 125 return getServerUrl() + Http2TestHandler.ECHO_TRAILERS_PATH; 126 } 127 128 /** @return url of a brotli-encoded server resource. */ getServeSimpleBrotliResponse()129 public static String getServeSimpleBrotliResponse() { 130 return getServerUrl() + Http2TestHandler.SERVE_SIMPLE_BROTLI_RESPONSE; 131 } 132 133 /** @return url of the reporting collector */ getReportingCollectorUrl()134 public static String getReportingCollectorUrl() { 135 return getServerUrl() + Http2TestHandler.REPORTING_COLLECTOR_PATH; 136 } 137 138 /** @return url of a resource that includes Reporting and NEL policy headers in its response */ getSuccessWithNELHeadersUrl()139 public static String getSuccessWithNELHeadersUrl() { 140 return getServerUrl() + Http2TestHandler.SUCCESS_WITH_NEL_HEADERS_PATH; 141 } 142 143 /** @return url of a resource that sends response headers with the same key */ getCombinedHeadersUrl()144 public static String getCombinedHeadersUrl() { 145 return getServerUrl() + Http2TestHandler.COMBINED_HEADERS_PATH; 146 } 147 startHttp2TestServer(Context context)148 public static boolean startHttp2TestServer(Context context) throws Exception { 149 TestFilesInstaller.installIfNeeded(context); 150 return startHttp2TestServer(context, SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM, null); 151 } 152 startHttp2TestServer(Context context, CountDownLatch hangingUrlLatch)153 public static boolean startHttp2TestServer(Context context, CountDownLatch hangingUrlLatch) 154 throws Exception { 155 TestFilesInstaller.installIfNeeded(context); 156 return startHttp2TestServer( 157 context, SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM, hangingUrlLatch); 158 } 159 startHttp2TestServer( Context context, String certFileName, String keyFileName, CountDownLatch hangingUrlLatch)160 private static boolean startHttp2TestServer( 161 Context context, 162 String certFileName, 163 String keyFileName, 164 CountDownLatch hangingUrlLatch) 165 throws Exception { 166 sReportingCollector = new ReportingCollector(); 167 Http2TestServerRunnable http2TestServerRunnable = 168 new Http2TestServerRunnable( 169 new File(CertTestUtil.CERTS_DIRECTORY + certFileName), 170 new File(CertTestUtil.CERTS_DIRECTORY + keyFileName), 171 hangingUrlLatch); 172 // This will run synchronously as we can't run the test before we have 173 // started the test-server, if the test-server has failed to start then 174 // the caller should assert on the value returned to make sure that the test 175 // fails if the server has failed to start up. 176 return EXECUTOR.submit(http2TestServerRunnable).get(); 177 } 178 Http2TestServer()179 private Http2TestServer() {} 180 181 private static class Http2TestServerRunnable implements Callable<Boolean> { 182 private final SslContext mSslCtx; 183 private final CountDownLatch mHangingUrlLatch; 184 Http2TestServerRunnable(File certFile, File keyFile, CountDownLatch hangingUrlLatch)185 Http2TestServerRunnable(File certFile, File keyFile, CountDownLatch hangingUrlLatch) 186 throws Exception { 187 ApplicationProtocolConfig applicationProtocolConfig = 188 new ApplicationProtocolConfig( 189 Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE, 190 SelectedListenerFailureBehavior.ACCEPT, 191 ApplicationProtocolNames.HTTP_2); 192 193 // Don't make netty use java.security.KeyStore.getInstance("JKS") as it doesn't 194 // exist. Just avoid a KeyManagerFactory as it's unnecessary for our testing. 195 System.setProperty("io.netty.handler.ssl.openssl.useKeyManagerFactory", "false"); 196 197 mSslCtx = 198 new OpenSslServerContext( 199 certFile, 200 keyFile, 201 null, 202 null, 203 Http2SecurityUtil.CIPHERS, 204 SupportedCipherSuiteFilter.INSTANCE, 205 applicationProtocolConfig, 206 0, 207 0); 208 209 mHangingUrlLatch = hangingUrlLatch; 210 } 211 212 @Override call()213 public Boolean call() throws Exception { 214 for(int retries = 0; retries < 10; retries++) { 215 try { 216 // Configure the server. 217 EventLoopGroup group = new NioEventLoopGroup(); 218 ServerBootstrap b = new ServerBootstrap(); 219 b.option(ChannelOption.SO_BACKLOG, 1024); 220 b.group(group) 221 .channel(NioServerSocketChannel.class) 222 .handler(new LoggingHandler(LogLevel.INFO)) 223 .childHandler(new Http2ServerInitializer(mSslCtx, mHangingUrlLatch)); 224 225 sServerChannel = b.bind(PORT).sync().channel(); 226 Log.i(TAG, "Netty HTTP/2 server started on " + getServerUrl()); 227 return true; 228 } catch (Exception e) { 229 // Netty test server fails to startup and this is a common issue 230 // https://github.com/netty/netty/issues/2616. It is not well understood 231 // why this is happening or how to fix it, we can workaround this by 232 // trying to restart the server several times before giving up. 233 // See crbug/1519471 for more information. 234 Log.w(TAG, "Netty server failed to start", e); 235 // Sleep for half a second before trying again. 236 Thread.sleep(/* milliseconds = */ 500); 237 } 238 } 239 return false; 240 } 241 } 242 243 /** Sets up the Netty pipeline for the test server. */ 244 private static class Http2ServerInitializer extends ChannelInitializer<SocketChannel> { 245 private final SslContext mSslCtx; 246 private final CountDownLatch mHangingUrlLatch; 247 Http2ServerInitializer(SslContext sslCtx, CountDownLatch hangingUrlLatch)248 public Http2ServerInitializer(SslContext sslCtx, CountDownLatch hangingUrlLatch) { 249 mSslCtx = sslCtx; 250 mHangingUrlLatch = hangingUrlLatch; 251 } 252 253 @Override initChannel(SocketChannel ch)254 public void initChannel(SocketChannel ch) { 255 ch.pipeline() 256 .addLast( 257 mSslCtx.newHandler(ch.alloc()), 258 new Http2NegotiationHandler(mHangingUrlLatch)); 259 } 260 } 261 262 private static class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler { 263 private final CountDownLatch mHangingUrlLatch; 264 Http2NegotiationHandler(CountDownLatch hangingUrlLatch)265 protected Http2NegotiationHandler(CountDownLatch hangingUrlLatch) { 266 super(ApplicationProtocolNames.HTTP_1_1); 267 mHangingUrlLatch = hangingUrlLatch; 268 } 269 270 @Override configurePipeline(ChannelHandlerContext ctx, String protocol)271 protected void configurePipeline(ChannelHandlerContext ctx, String protocol) 272 throws Exception { 273 if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { 274 ctx.pipeline() 275 .addLast( 276 new Http2TestHandler.Builder() 277 .setReportingCollector(sReportingCollector) 278 .setServerUrl(getServerUrl()) 279 .setHangingUrlLatch(mHangingUrlLatch) 280 .build()); 281 return; 282 } 283 284 throw new IllegalStateException("unknown protocol: " + protocol); 285 } 286 } 287 } 288