chromium/components/cronet/android/java/src/org/chromium/net/impl/AndroidHttpEngineBuilderWrapper.java

// Copyright 2023 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.impl;

import static org.chromium.net.impl.HttpEngineNativeProvider.EXT_API_LEVEL;
import static org.chromium.net.impl.HttpEngineNativeProvider.EXT_VERSION;

import android.net.http.HttpEngine;
import android.os.Process;
import android.util.Log;

import androidx.annotation.RequiresExtension;
import androidx.annotation.VisibleForTesting;

import org.chromium.net.CronetEngine;
import org.chromium.net.ExperimentalCronetEngine;
import org.chromium.net.ICronetEngineBuilder;
import org.chromium.net.telemetry.ExperimentalOptions;
import org.chromium.net.telemetry.OptionalBoolean;

import java.time.Duration;
import java.util.Date;
import java.util.Set;

@RequiresExtension(extension = EXT_API_LEVEL, version = EXT_VERSION)
class AndroidHttpEngineBuilderWrapper extends ICronetEngineBuilder {
    private static final String TAG = "HttpEngBuilderWrap";

    private static boolean sLibraryLoaderUnsupportedLogged;
    private static boolean sNQEUnsupportedLogged;

    private final HttpEngine.Builder mBackend;
    private int mThreadPriority = Integer.MIN_VALUE;

    public AndroidHttpEngineBuilderWrapper(HttpEngine.Builder backend) {
        this.mBackend = backend;
    }

    @Override
    public String getDefaultUserAgent() {
        return mBackend.getDefaultUserAgent();
    }

    @Override
    public ICronetEngineBuilder setUserAgent(String userAgent) {
        mBackend.setUserAgent(userAgent);
        return this;
    }

    @Override
    public ICronetEngineBuilder setStoragePath(String value) {
        mBackend.setStoragePath(value);
        return this;
    }

    @Override
    public ICronetEngineBuilder setLibraryLoader(CronetEngine.Builder.LibraryLoader loader) {
        if (!sLibraryLoaderUnsupportedLogged) {
            Log.i(
                    TAG,
                    "Custom library loader is unsupported when HttpEngineNativeProvider is used.");
            sLibraryLoaderUnsupportedLogged = true;
        }
        return this;
    }

    @Override
    public ICronetEngineBuilder enableQuic(boolean value) {
        mBackend.setEnableQuic(value);
        return this;
    }

    @Override
    public ICronetEngineBuilder enableSdch(boolean value) {
        // Deprecated and unused by upper layers, do nothing.
        return this;
    }

    @Override
    public ICronetEngineBuilder enableHttp2(boolean value) {
        mBackend.setEnableHttp2(value);
        return this;
    }

    @Override
    public ICronetEngineBuilder enableBrotli(boolean value) {
        mBackend.setEnableBrotli(value);
        return this;
    }

    @Override
    public ICronetEngineBuilder enableHttpCache(int cacheMode, long maxSize) {
        mBackend.setEnableHttpCache(cacheMode, maxSize);
        return this;
    }

    @Override
    public ICronetEngineBuilder addQuicHint(String host, int port, int alternatePort) {
        mBackend.addQuicHint(host, port, alternatePort);
        return this;
    }

    @Override
    public ICronetEngineBuilder addPublicKeyPins(
            String hostName,
            Set<byte[]> pinsSha256,
            boolean includeSubdomains,
            Date expirationDate) {
        mBackend.addPublicKeyPins(
                hostName, pinsSha256, includeSubdomains, expirationDate.toInstant());
        return this;
    }

    @Override
    public ICronetEngineBuilder enablePublicKeyPinningBypassForLocalTrustAnchors(boolean value) {
        mBackend.setEnablePublicKeyPinningBypassForLocalTrustAnchors(value);
        return this;
    }

    @Override
    public ICronetEngineBuilder setThreadPriority(int priority) {
        // not supported by HttpEngine hence implemented in wrapper
        if (priority > Process.THREAD_PRIORITY_LOWEST || priority < -20) {
            throw new IllegalArgumentException("Thread priority invalid");
        }
        mThreadPriority = priority;
        return this;
    }

    @Override
    public ICronetEngineBuilder setExperimentalOptions(String stringOptions) {
        // This only translates known experimental options
        ExperimentalOptions options = new ExperimentalOptions(stringOptions);
        mBackend.setConnectionMigrationOptions(parseConnectionMigrationOptions(options));
        mBackend.setDnsOptions(parseDnsOptions(options));
        mBackend.setQuicOptions(parseQuicOptions(options));
        return this;
    }

    @Override
    public ICronetEngineBuilder enableNetworkQualityEstimator(boolean value) {
        if (!sNQEUnsupportedLogged) {
            Log.i(
                    TAG,
                    "NetworkQualityEstimator is unsupported when HttpEngineNativeProvider is used");
            sNQEUnsupportedLogged = true;
        }
        return this;
    }

    /**
     * Build a {@link CronetEngine} using this builder's configuration.
     *
     * @return constructed {@link CronetEngine}.
     */
    @Override
    public ExperimentalCronetEngine build() {
        return new AndroidHttpEngineWrapper(mBackend.build(), mThreadPriority);
    }

    @VisibleForTesting
    public static android.net.http.ConnectionMigrationOptions parseConnectionMigrationOptions(
            ExperimentalOptions options) {
        android.net.http.ConnectionMigrationOptions.Builder cmOptionsBuilder =
                new android.net.http.ConnectionMigrationOptions.Builder();

        cmOptionsBuilder.setDefaultNetworkMigration(
                optionalBooleanToMigrationOptionState(
                        options.getMigrateSessionsOnNetworkChangeV2Option()));
        cmOptionsBuilder.setPathDegradationMigration(
                optionalBooleanToMigrationOptionState(options.getAllowPortMigration()));

        OptionalBoolean migrateSessionsEarly = options.getMigrateSessionsEarlyV2();
        cmOptionsBuilder.setAllowNonDefaultNetworkUsage(
                optionalBooleanToMigrationOptionState(migrateSessionsEarly));
        if (migrateSessionsEarly == OptionalBoolean.TRUE) {
            cmOptionsBuilder.setPathDegradationMigration(
                    optionalBooleanToMigrationOptionState(OptionalBoolean.TRUE));
        }

        return cmOptionsBuilder.build();
    }

    @VisibleForTesting
    public static android.net.http.DnsOptions parseDnsOptions(ExperimentalOptions options) {
        android.net.http.DnsOptions.StaleDnsOptions.Builder staleDnsOptionBuilder =
                new android.net.http.DnsOptions.StaleDnsOptions.Builder();
        int staleDnsDelay = options.getStaleDnsDelayMillisOption();
        if (staleDnsDelay != ExperimentalOptions.UNSET_INT_VALUE) {
            staleDnsOptionBuilder.setFreshLookupTimeout(Duration.ofMillis(staleDnsDelay));
        }

        int expiredDelay = options.getStaleDnsMaxExpiredTimeMillisOption();
        if (expiredDelay != ExperimentalOptions.UNSET_INT_VALUE) {
            staleDnsOptionBuilder.setMaxExpiredDelay(Duration.ofMillis(expiredDelay));
        }

        staleDnsOptionBuilder
                .setAllowCrossNetworkUsage(
                        optionalBooleanToMigrationOptionState(
                                options.getStaleDnsAllowOtherNetworkOption()))
                .setUseStaleOnNameNotResolved(
                        optionalBooleanToMigrationOptionState(
                                options.getStaleDnsUseStaleOnNameNotResolvedOption()));

        android.net.http.DnsOptions.Builder dnsOptionsBuilder =
                new android.net.http.DnsOptions.Builder();
        dnsOptionsBuilder
                .setUseHttpStackDnsResolver(
                        optionalBooleanToMigrationOptionState(options.getAsyncDnsEnableOption()))
                .setStaleDns(
                        optionalBooleanToMigrationOptionState(options.getStaleDnsEnableOption()))
                .setStaleDnsOptions(staleDnsOptionBuilder.build())
                .setPreestablishConnectionsToStaleDnsResults(
                        optionalBooleanToMigrationOptionState(
                                options.getRaceStaleDnsOnConnection()))
                .setPersistHostCache(
                        optionalBooleanToMigrationOptionState(
                                options.getStaleDnsPersistToDiskOption()));
        int persistHostCachePeriod = options.getStaleDnsPersistDelayMillisOption();
        if (persistHostCachePeriod != ExperimentalOptions.UNSET_INT_VALUE) {
            dnsOptionsBuilder.setPersistHostCachePeriod(Duration.ofMillis(persistHostCachePeriod));
        }

        return dnsOptionsBuilder.build();
    }

    @VisibleForTesting
    public static android.net.http.QuicOptions parseQuicOptions(ExperimentalOptions options) {
        android.net.http.QuicOptions.Builder quicOptionsBuilder =
                new android.net.http.QuicOptions.Builder();

        if (options.getHostWhitelist() != null) {
            for (String host : options.getHostWhitelist().split(",")) {
                quicOptionsBuilder.addAllowedQuicHost(host);
            }
        }

        int inMemoryServerConfigsCacheSize = options.getMaxServerConfigsStoredInPropertiesOption();
        if (inMemoryServerConfigsCacheSize != ExperimentalOptions.UNSET_INT_VALUE) {
            quicOptionsBuilder.setInMemoryServerConfigsCacheSize(inMemoryServerConfigsCacheSize);
        }

        String handshakeUserAgent = options.getUserAgentId();
        if (handshakeUserAgent != null) {
            quicOptionsBuilder.setHandshakeUserAgent(handshakeUserAgent);
        }

        int idleConnectionTimeoutSeconds = options.getIdleConnectionTimeoutSecondsOption();
        if (idleConnectionTimeoutSeconds != ExperimentalOptions.UNSET_INT_VALUE) {
            quicOptionsBuilder.setIdleConnectionTimeout(
                    Duration.ofSeconds(idleConnectionTimeoutSeconds));
        }

        return quicOptionsBuilder.build();
    }

    /**
     * HttpEngine XOptions exposes X_OPTION_* IntDefs that map to the same integer values. To
     * simplify the code, we are reusing ConnectionMigrationOptions.MIGRATION_OPTION_* for
     * DnsOptions and QuicOptions.
     */
    private static int optionalBooleanToMigrationOptionState(OptionalBoolean value) {
        switch (value) {
            case TRUE:
                return android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED;
            case FALSE:
                return android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_DISABLED;
            case UNSET:
                return android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_UNSPECIFIED;
        }

        throw new AssertionError("Invalid OptionalBoolean value: " + value);
    }
}