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.ConditionVariable; 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.CountDownLatch; 15 16 import io.netty.bootstrap.ServerBootstrap; 17 import io.netty.channel.Channel; 18 import io.netty.channel.ChannelHandlerContext; 19 import io.netty.channel.ChannelInitializer; 20 import io.netty.channel.ChannelOption; 21 import io.netty.channel.EventLoopGroup; 22 import io.netty.channel.nio.NioEventLoopGroup; 23 import io.netty.channel.socket.SocketChannel; 24 import io.netty.channel.socket.nio.NioServerSocketChannel; 25 import io.netty.handler.codec.http2.Http2SecurityUtil; 26 import io.netty.handler.logging.LogLevel; 27 import io.netty.handler.logging.LoggingHandler; 28 import io.netty.handler.ssl.ApplicationProtocolConfig; 29 import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; 30 import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; 31 import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; 32 import io.netty.handler.ssl.ApplicationProtocolNames; 33 import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; 34 import io.netty.handler.ssl.OpenSslServerContext; 35 import io.netty.handler.ssl.SslContext; 36 import io.netty.handler.ssl.SupportedCipherSuiteFilter; 37 38 /** 39 * Wrapper class to start a HTTP/2 test server. 40 */ 41 public final class Http2TestServer { 42 private static Channel sServerChannel; 43 private static final String TAG = Http2TestServer.class.getSimpleName(); 44 45 private static final String HOST = "127.0.0.1"; 46 // Server port. 47 private static final int PORT = 8443; 48 49 private static ReportingCollector sReportingCollector; 50 shutdownHttp2TestServer()51 public static boolean shutdownHttp2TestServer() throws Exception { 52 if (sServerChannel != null) { 53 sServerChannel.close().sync(); 54 sServerChannel = null; 55 sReportingCollector = null; 56 return true; 57 } 58 return false; 59 } 60 getServerHost()61 public static String getServerHost() { 62 return HOST; 63 } 64 getServerPort()65 public static int getServerPort() { 66 return PORT; 67 } 68 getServerUrl()69 public static String getServerUrl() { 70 return "https://" + HOST + ":" + PORT; 71 } 72 getReportingCollector()73 public static ReportingCollector getReportingCollector() { 74 return sReportingCollector; 75 } 76 getEchoAllHeadersUrl()77 public static String getEchoAllHeadersUrl() { 78 return getServerUrl() + Http2TestHandler.ECHO_ALL_HEADERS_PATH; 79 } 80 getEchoHeaderUrl(String headerName)81 public static String getEchoHeaderUrl(String headerName) { 82 return getServerUrl() + Http2TestHandler.ECHO_HEADER_PATH + "?" + headerName; 83 } 84 getEchoMethodUrl()85 public static String getEchoMethodUrl() { 86 return getServerUrl() + Http2TestHandler.ECHO_METHOD_PATH; 87 } 88 89 /** 90 * When using this you must provide a CountDownLatch in the call to startHttp2TestServer. 91 * The request handler will continue to hang until the provided CountDownLatch reaches 0. 92 * 93 * @return url of the server resource which will hang indefinitely. 94 */ getHangingRequestUrl()95 public static String getHangingRequestUrl() { 96 return getServerUrl() + Http2TestHandler.HANGING_REQUEST_PATH; 97 } 98 99 /** 100 * @return url of the server resource which will echo every received stream data frame. 101 */ getEchoStreamUrl()102 public static String getEchoStreamUrl() { 103 return getServerUrl() + Http2TestHandler.ECHO_STREAM_PATH; 104 } 105 106 /** 107 * @return url of the server resource which will echo request headers as response trailers. 108 */ getEchoTrailersUrl()109 public static String getEchoTrailersUrl() { 110 return getServerUrl() + Http2TestHandler.ECHO_TRAILERS_PATH; 111 } 112 113 /** 114 * @return url of a brotli-encoded server resource. 115 */ getServeSimpleBrotliResponse()116 public static String getServeSimpleBrotliResponse() { 117 return getServerUrl() + Http2TestHandler.SERVE_SIMPLE_BROTLI_RESPONSE; 118 } 119 120 /** 121 * @return url of the reporting collector 122 */ getReportingCollectorUrl()123 public static String getReportingCollectorUrl() { 124 return getServerUrl() + Http2TestHandler.REPORTING_COLLECTOR_PATH; 125 } 126 127 /** 128 * @return url of a resource that includes Reporting and NEL policy headers in its response 129 */ getSuccessWithNELHeadersUrl()130 public static String getSuccessWithNELHeadersUrl() { 131 return getServerUrl() + Http2TestHandler.SUCCESS_WITH_NEL_HEADERS_PATH; 132 } 133 134 /** 135 * @return url of a resource that sends response headers with the same key 136 */ getCombinedHeadersUrl()137 public static String getCombinedHeadersUrl() { 138 return getServerUrl() + Http2TestHandler.COMBINED_HEADERS_PATH; 139 } 140 startHttp2TestServer( Context context, String certFileName, String keyFileName)141 public static boolean startHttp2TestServer( 142 Context context, String certFileName, String keyFileName) throws Exception { 143 return startHttp2TestServer(context, certFileName, keyFileName, null); 144 } 145 startHttp2TestServer(Context context, String certFileName, String keyFileName, CountDownLatch hangingUrlLatch)146 public static boolean startHttp2TestServer(Context context, String certFileName, 147 String keyFileName, CountDownLatch hangingUrlLatch) throws Exception { 148 sReportingCollector = new ReportingCollector(); 149 Http2TestServerRunnable http2TestServerRunnable = 150 new Http2TestServerRunnable(new File(CertTestUtil.CERTS_DIRECTORY + certFileName), 151 new File(CertTestUtil.CERTS_DIRECTORY + keyFileName), hangingUrlLatch); 152 new Thread(http2TestServerRunnable).start(); 153 http2TestServerRunnable.blockUntilStarted(); 154 return true; 155 } 156 Http2TestServer()157 private Http2TestServer() {} 158 159 private static class Http2TestServerRunnable implements Runnable { 160 private final ConditionVariable mBlock = new ConditionVariable(); 161 private final SslContext mSslCtx; 162 private final CountDownLatch mHangingUrlLatch; 163 Http2TestServerRunnable(File certFile, File keyFile, CountDownLatch hangingUrlLatch)164 Http2TestServerRunnable(File certFile, File keyFile, CountDownLatch hangingUrlLatch) 165 throws Exception { 166 ApplicationProtocolConfig applicationProtocolConfig = new ApplicationProtocolConfig( 167 Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE, 168 SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2); 169 170 // Don't make netty use java.security.KeyStore.getInstance("JKS") as it doesn't 171 // exist. Just avoid a KeyManagerFactory as it's unnecessary for our testing. 172 System.setProperty("io.netty.handler.ssl.openssl.useKeyManagerFactory", "false"); 173 174 mSslCtx = new OpenSslServerContext(certFile, keyFile, null, null, 175 Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE, 176 applicationProtocolConfig, 0, 0); 177 178 mHangingUrlLatch = hangingUrlLatch; 179 } 180 blockUntilStarted()181 public void blockUntilStarted() { 182 mBlock.block(); 183 } 184 185 @Override run()186 public void run() { 187 boolean retry = false; 188 do { 189 try { 190 // Configure the server. 191 EventLoopGroup group = new NioEventLoopGroup(); 192 try { 193 ServerBootstrap b = new ServerBootstrap(); 194 b.option(ChannelOption.SO_BACKLOG, 1024); 195 b.group(group) 196 .channel(NioServerSocketChannel.class) 197 .handler(new LoggingHandler(LogLevel.INFO)) 198 .childHandler( 199 new Http2ServerInitializer(mSslCtx, mHangingUrlLatch)); 200 201 sServerChannel = b.bind(PORT).sync().channel(); 202 Log.i(TAG, "Netty HTTP/2 server started on " + getServerUrl()); 203 mBlock.open(); 204 sServerChannel.closeFuture().sync(); 205 } finally { 206 group.shutdownGracefully(); 207 } 208 Log.i(TAG, "Stopped Http2TestServerRunnable!"); 209 retry = false; 210 } catch (Exception e) { 211 Log.e(TAG, "Netty server failed to start", e); 212 // Retry once if we hit https://github.com/netty/netty/issues/2616 before the 213 // server starts. 214 retry = !retry && sServerChannel == null 215 && e.toString().contains("java.nio.channels.ClosedChannelException"); 216 } 217 } while (retry); 218 } 219 } 220 221 /** 222 * Sets up the Netty pipeline for the test server. 223 */ 224 private static class Http2ServerInitializer extends ChannelInitializer<SocketChannel> { 225 private final SslContext mSslCtx; 226 private final CountDownLatch mHangingUrlLatch; 227 Http2ServerInitializer(SslContext sslCtx, CountDownLatch hangingUrlLatch)228 public Http2ServerInitializer(SslContext sslCtx, CountDownLatch hangingUrlLatch) { 229 mSslCtx = sslCtx; 230 mHangingUrlLatch = hangingUrlLatch; 231 } 232 233 @Override initChannel(SocketChannel ch)234 public void initChannel(SocketChannel ch) { 235 ch.pipeline().addLast( 236 mSslCtx.newHandler(ch.alloc()), new Http2NegotiationHandler(mHangingUrlLatch)); 237 } 238 } 239 240 private static class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler { 241 private final CountDownLatch mHangingUrlLatch; 242 Http2NegotiationHandler(CountDownLatch hangingUrlLatch)243 protected Http2NegotiationHandler(CountDownLatch hangingUrlLatch) { 244 super(ApplicationProtocolNames.HTTP_1_1); 245 mHangingUrlLatch = hangingUrlLatch; 246 } 247 248 @Override configurePipeline(ChannelHandlerContext ctx, String protocol)249 protected void configurePipeline(ChannelHandlerContext ctx, String protocol) 250 throws Exception { 251 if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { 252 ctx.pipeline().addLast(new Http2TestHandler.Builder() 253 .setReportingCollector(sReportingCollector) 254 .setServerUrl(getServerUrl()) 255 .setHangingUrlLatch(mHangingUrlLatch) 256 .build()); 257 return; 258 } 259 260 throw new IllegalStateException("unknown protocol: " + protocol); 261 } 262 } 263 } 264