xref: /aosp_15_r20/external/cronet/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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