chromium/components/cronet/android/test/src/org/chromium/net/Http2TestServer.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.net;

import android.content.Context;
import android.os.Build;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.OpenSslServerContext;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;

import org.chromium.base.Log;
import org.chromium.net.test.util.CertTestUtil;

import java.io.File;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/** Wrapper class to start a HTTP/2 test server. */
public final class Http2TestServer {
    private static Channel sServerChannel;
    private static final String TAG = Http2TestServer.class.getSimpleName();

    private static final String HOST = "localhost";
    // Server port.
    private static final int PORT = 8443;

    private static ReportingCollector sReportingCollector;

    public static final String SERVER_CERT_PEM;
    private static final String SERVER_KEY_PKCS8_PEM;
    // Used to start http2 test server.
    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(1);

    static {
        // TODO(crbug.com/40284777): Fallback to MockCertVerifier when custom CAs are not supported.
        // Currently, MockCertVerifier uses different certificates, so make the server also use
        // those.
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
            SERVER_CERT_PEM = "quic-chain.pem";
            SERVER_KEY_PKCS8_PEM = "quic-leaf-cert.key.pkcs8.pem";
        } else {
            SERVER_CERT_PEM = "cronet-quic-chain.pem";
            SERVER_KEY_PKCS8_PEM = "cronet-quic-leaf-cert.key.pkcs8.pem";
        }
    }

    public static boolean shutdownHttp2TestServer() throws Exception {
        if (sServerChannel != null) {
            sServerChannel.close().sync();
            sServerChannel = null;
            sReportingCollector = null;
            return true;
        }
        return false;
    }

    public static String getServerHost() {
        return HOST;
    }

    public static int getServerPort() {
        return PORT;
    }

    public static String getServerUrl() {
        return "https://" + HOST + ":" + PORT;
    }

    public static ReportingCollector getReportingCollector() {
        return sReportingCollector;
    }

    public static String getEchoAllHeadersUrl() {
        return getServerUrl() + Http2TestHandler.ECHO_ALL_HEADERS_PATH;
    }

    public static String getEchoHeaderUrl(String headerName) {
        return getServerUrl() + Http2TestHandler.ECHO_HEADER_PATH + "?" + headerName;
    }

    public static String getEchoMethodUrl() {
        return getServerUrl() + Http2TestHandler.ECHO_METHOD_PATH;
    }

    /**
     * When using this you must provide a CountDownLatch in the call to startHttp2TestServer.
     * The request handler will continue to hang until the provided CountDownLatch reaches 0.
     *
     * @return url of the server resource which will hang indefinitely.
     */
    public static String getHangingRequestUrl() {
        return getServerUrl() + Http2TestHandler.HANGING_REQUEST_PATH;
    }

    /** @return url of the server resource which will echo every received stream data frame. */
    public static String getEchoStreamUrl() {
        return getServerUrl() + Http2TestHandler.ECHO_STREAM_PATH;
    }

    /** @return url of the server resource which will echo request headers as response trailers. */
    public static String getEchoTrailersUrl() {
        return getServerUrl() + Http2TestHandler.ECHO_TRAILERS_PATH;
    }

    /** @return url of a brotli-encoded server resource. */
    public static String getServeSimpleBrotliResponse() {
        return getServerUrl() + Http2TestHandler.SERVE_SIMPLE_BROTLI_RESPONSE;
    }

    /**
     * @return url of a shared-brotli-encoded server resource.
     */
    public static String getServeSharedBrotliResponse() {
        return getServerUrl() + Http2TestHandler.SERVE_SHARED_BROTLI_RESPONSE;
    }

    /**
     * @return url of the reporting collector
     */
    public static String getReportingCollectorUrl() {
        return getServerUrl() + Http2TestHandler.REPORTING_COLLECTOR_PATH;
    }

    /** @return url of a resource that includes Reporting and NEL policy headers in its response */
    public static String getSuccessWithNELHeadersUrl() {
        return getServerUrl() + Http2TestHandler.SUCCESS_WITH_NEL_HEADERS_PATH;
    }

    /** @return url of a resource that sends response headers with the same key */
    public static String getCombinedHeadersUrl() {
        return getServerUrl() + Http2TestHandler.COMBINED_HEADERS_PATH;
    }

    public static boolean startHttp2TestServer(Context context) throws Exception {
        TestFilesInstaller.installIfNeeded(context);
        return startHttp2TestServer(context, SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM, null);
    }

    public static boolean startHttp2TestServer(Context context, CountDownLatch hangingUrlLatch)
            throws Exception {
        TestFilesInstaller.installIfNeeded(context);
        return startHttp2TestServer(
                context, SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM, hangingUrlLatch);
    }

    private static boolean startHttp2TestServer(
            Context context,
            String certFileName,
            String keyFileName,
            CountDownLatch hangingUrlLatch)
            throws Exception {
        sReportingCollector = new ReportingCollector();
        Http2TestServerRunnable http2TestServerRunnable =
                new Http2TestServerRunnable(
                        new File(CertTestUtil.CERTS_DIRECTORY + certFileName),
                        new File(CertTestUtil.CERTS_DIRECTORY + keyFileName),
                        hangingUrlLatch);
        // This will run synchronously as we can't run the test before we have
        // started the test-server, if the test-server has failed to start then
        // the caller should assert on the value returned to make sure that the test
        // fails if the server has failed to start up.
        return EXECUTOR.submit(http2TestServerRunnable).get();
    }

    private Http2TestServer() {}

    private static class Http2TestServerRunnable implements Callable<Boolean> {
        private final SslContext mSslCtx;
        private final CountDownLatch mHangingUrlLatch;

        Http2TestServerRunnable(File certFile, File keyFile, CountDownLatch hangingUrlLatch)
                throws Exception {
            ApplicationProtocolConfig applicationProtocolConfig =
                    new ApplicationProtocolConfig(
                            Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE,
                            SelectedListenerFailureBehavior.ACCEPT,
                                    ApplicationProtocolNames.HTTP_2);

            // Don't make netty use java.security.KeyStore.getInstance("JKS") as it doesn't
            // exist.  Just avoid a KeyManagerFactory as it's unnecessary for our testing.
            System.setProperty("io.netty.handler.ssl.openssl.useKeyManagerFactory", "false");

            mSslCtx =
                    new OpenSslServerContext(
                            certFile,
                            keyFile,
                            null,
                            null,
                            Http2SecurityUtil.CIPHERS,
                            SupportedCipherSuiteFilter.INSTANCE,
                            applicationProtocolConfig,
                            0,
                            0);

            mHangingUrlLatch = hangingUrlLatch;
        }

        @Override
        public Boolean call() throws Exception {
            for(int retries = 0; retries < 10; retries++) {
                try {
                    // Configure the server.
                    EventLoopGroup group = new NioEventLoopGroup();
                    ServerBootstrap b = new ServerBootstrap();
                    b.option(ChannelOption.SO_BACKLOG, 1024);
                    b.group(group)
                            .channel(NioServerSocketChannel.class)
                            .handler(new LoggingHandler(LogLevel.INFO))
                            .childHandler(new Http2ServerInitializer(mSslCtx, mHangingUrlLatch));

                    sServerChannel = b.bind(PORT).sync().channel();
                    Log.i(TAG, "Netty HTTP/2 server started on " + getServerUrl());
                    return true;
                } catch (Exception e) {
                    // Netty test server fails to startup and this is a common issue
                    // https://github.com/netty/netty/issues/2616. It is not well understood
                    // why this is happening or how to fix it, we can workaround this by
                    // trying to restart the server several times before giving up.
                    // See crbug/1519471 for more information.
                    Log.w(TAG, "Netty server failed to start", e);
                    // Sleep for half a second before trying again.
                    Thread.sleep(/* milliseconds = */ 500);
                }
            }
            return false;
        }
    }

    /** Sets up the Netty pipeline for the test server. */
    private static class Http2ServerInitializer extends ChannelInitializer<SocketChannel> {
        private final SslContext mSslCtx;
        private final CountDownLatch mHangingUrlLatch;

        public Http2ServerInitializer(SslContext sslCtx, CountDownLatch hangingUrlLatch) {
            mSslCtx = sslCtx;
            mHangingUrlLatch = hangingUrlLatch;
        }

        @Override
        public void initChannel(SocketChannel ch) {
            ch.pipeline()
                    .addLast(
                            mSslCtx.newHandler(ch.alloc()),
                            new Http2NegotiationHandler(mHangingUrlLatch));
        }
    }

    private static class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler {
        private final CountDownLatch mHangingUrlLatch;

        protected Http2NegotiationHandler(CountDownLatch hangingUrlLatch) {
            super(ApplicationProtocolNames.HTTP_1_1);
            mHangingUrlLatch = hangingUrlLatch;
        }

        @Override
        protected void configurePipeline(ChannelHandlerContext ctx, String protocol)
                throws Exception {
            if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
                ctx.pipeline()
                        .addLast(
                                new Http2TestHandler.Builder()
                                        .setReportingCollector(sReportingCollector)
                                        .setServerUrl(getServerUrl())
                                        .setHangingUrlLatch(mHangingUrlLatch)
                                        .build());
                return;
            }

            throw new IllegalStateException("unknown protocol: " + protocol);
        }
    }
}