blob: 0d2c3d149bea10986a8e4e538a44ca527607c49b [file] [log] [blame]
// Copyright 2016 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 androidx.annotation.VisibleForTesting;
import org.json.JSONException;
import org.json.JSONObject;
import org.chromium.net.DnsOptions.StaleDnsOptions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An implementation of ICronetEngineBuilder which handles translation of configuration options to
* json-based experimental options, if necessary.
*
* <p>{@hide internal class}
*/
@VisibleForTesting
public final class ExperimentalOptionsTranslatingCronetEngineBuilder extends ICronetEngineBuilder {
private static final Set<Integer> SUPPORTED_OPTIONS =
Collections.unmodifiableSet(
new HashSet(
Arrays.asList(
ICronetEngineBuilder.CONNECTION_MIGRATION_OPTIONS,
ICronetEngineBuilder.DNS_OPTIONS,
ICronetEngineBuilder.QUIC_OPTIONS)));
private JSONObject mParsedExperimentalOptions;
private final List<ExperimentalOptionsPatch> mExperimentalOptionsPatches = new ArrayList<>();
private final ICronetEngineBuilder mDelegate;
ExperimentalOptionsTranslatingCronetEngineBuilder(ICronetEngineBuilder delegate) {
this.mDelegate = delegate;
}
@Override
public ICronetEngineBuilder setQuicOptions(QuicOptions options) {
// If the delegate builder supports enabling connection migration directly, just use it
if (mDelegate.getSupportedConfigOptions().contains(ICronetEngineBuilder.QUIC_OPTIONS)) {
mDelegate.setQuicOptions(options);
return this;
}
// If not, we'll have to work around it by modifying the experimental options JSON.
mExperimentalOptionsPatches.add(
(experimentalOptions) -> {
JSONObject quicOptions = createDefaultIfAbsent(experimentalOptions, "QUIC");
// Note: using the experimental APIs always overwrites what's in the
// experimental JSON, even though "repeated" fields could in theory be
// additive.
if (!options.getQuicHostAllowlist().isEmpty()) {
quicOptions.put(
"host_whitelist", String.join(",", options.getQuicHostAllowlist()));
}
if (!options.getEnabledQuicVersions().isEmpty()) {
quicOptions.put(
"quic_version", String.join(",", options.getEnabledQuicVersions()));
}
if (!options.getConnectionOptions().isEmpty()) {
quicOptions.put(
"connection_options",
String.join(",", options.getConnectionOptions()));
}
if (!options.getClientConnectionOptions().isEmpty()) {
quicOptions.put(
"client_connection_options",
String.join(",", options.getClientConnectionOptions()));
}
if (!options.getExtraQuicheFlags().isEmpty()) {
quicOptions.put(
"set_quic_flags", String.join(",", options.getExtraQuicheFlags()));
}
if (options.getInMemoryServerConfigsCacheSize() != null) {
quicOptions.put(
"max_server_configs_stored_in_properties",
options.getInMemoryServerConfigsCacheSize());
}
if (options.getHandshakeUserAgent() != null) {
quicOptions.put("user_agent_id", options.getHandshakeUserAgent());
}
if (options.getRetryWithoutAltSvcOnQuicErrors() != null) {
quicOptions.put(
"retry_without_alt_svc_on_quic_errors",
options.getRetryWithoutAltSvcOnQuicErrors());
}
if (options.getEnableTlsZeroRtt() != null) {
quicOptions.put("disable_tls_zero_rtt", !options.getEnableTlsZeroRtt());
}
if (options.getPreCryptoHandshakeIdleTimeoutSeconds() != null) {
quicOptions.put(
"max_idle_time_before_crypto_handshake_seconds",
options.getPreCryptoHandshakeIdleTimeoutSeconds());
}
if (options.getCryptoHandshakeTimeoutSeconds() != null) {
quicOptions.put(
"max_time_before_crypto_handshake_seconds",
options.getCryptoHandshakeTimeoutSeconds());
}
if (options.getIdleConnectionTimeoutSeconds() != null) {
quicOptions.put(
"idle_connection_timeout_seconds",
options.getIdleConnectionTimeoutSeconds());
}
if (options.getRetransmittableOnWireTimeoutMillis() != null) {
quicOptions.put(
"retransmittable_on_wire_timeout_milliseconds",
options.getRetransmittableOnWireTimeoutMillis());
}
if (options.getCloseSessionsOnIpChange() != null) {
quicOptions.put(
"close_sessions_on_ip_change",
options.getCloseSessionsOnIpChange());
}
if (options.getGoawaySessionsOnIpChange() != null) {
quicOptions.put(
"goaway_sessions_on_ip_change",
options.getGoawaySessionsOnIpChange());
}
if (options.getInitialBrokenServicePeriodSeconds() != null) {
quicOptions.put(
"initial_delay_for_broken_alternative_service_seconds",
options.getInitialBrokenServicePeriodSeconds());
}
if (options.getIncreaseBrokenServicePeriodExponentially() != null) {
quicOptions.put(
"exponential_backoff_on_initial_delay",
options.getIncreaseBrokenServicePeriodExponentially());
}
if (options.getDelayJobsWithAvailableSpdySession() != null) {
quicOptions.put(
"delay_main_job_with_available_spdy_session",
options.getDelayJobsWithAvailableSpdySession());
}
});
return this;
}
@Override
public ICronetEngineBuilder setDnsOptions(DnsOptions options) {
// If the delegate builder supports enabling connection migration directly, just use it
if (mDelegate.getSupportedConfigOptions().contains(ICronetEngineBuilder.DNS_OPTIONS)) {
mDelegate.setDnsOptions(options);
return this;
}
// If not, we'll have to work around it by modifying the experimental options JSON.
mExperimentalOptionsPatches.add(
(experimentalOptions) -> {
JSONObject asyncDnsOptions =
createDefaultIfAbsent(experimentalOptions, "AsyncDNS");
if (options.getUseBuiltInDnsResolver() != null) {
asyncDnsOptions.put("enable", options.getUseBuiltInDnsResolver());
}
JSONObject staleDnsOptions =
createDefaultIfAbsent(experimentalOptions, "StaleDNS");
if (options.getEnableStaleDns() != null) {
staleDnsOptions.put("enable", options.getEnableStaleDns());
}
if (options.getPersistHostCache() != null) {
staleDnsOptions.put("persist_to_disk", options.getPersistHostCache());
}
if (options.getPersistHostCachePeriodMillis() != null) {
staleDnsOptions.put(
"persist_delay_ms", options.getPersistHostCachePeriodMillis());
}
if (options.getStaleDnsOptions() != null) {
StaleDnsOptions staleDnsOptionsJava = options.getStaleDnsOptions();
if (staleDnsOptionsJava.getAllowCrossNetworkUsage() != null) {
staleDnsOptions.put(
"allow_other_network",
staleDnsOptionsJava.getAllowCrossNetworkUsage());
}
if (staleDnsOptionsJava.getFreshLookupTimeoutMillis() != null) {
staleDnsOptions.put(
"delay_ms", staleDnsOptionsJava.getFreshLookupTimeoutMillis());
}
if (staleDnsOptionsJava.getUseStaleOnNameNotResolved() != null) {
staleDnsOptions.put(
"use_stale_on_name_not_resolved",
staleDnsOptionsJava.getUseStaleOnNameNotResolved());
}
if (staleDnsOptionsJava.getMaxExpiredDelayMillis() != null) {
staleDnsOptions.put(
"max_expired_time_ms",
staleDnsOptionsJava.getMaxExpiredDelayMillis());
}
}
JSONObject quicOptions = createDefaultIfAbsent(experimentalOptions, "QUIC");
if (options.getPreestablishConnectionsToStaleDnsResults() != null) {
quicOptions.put(
"race_stale_dns_on_connection",
options.getPreestablishConnectionsToStaleDnsResults());
}
});
return this;
}
@Override
public ICronetEngineBuilder setConnectionMigrationOptions(ConnectionMigrationOptions options) {
// If the delegate builder supports enabling connection migration directly, just use it
if (mDelegate
.getSupportedConfigOptions()
.contains(ICronetEngineBuilder.CONNECTION_MIGRATION_OPTIONS)) {
mDelegate.setConnectionMigrationOptions(options);
return this;
}
// If not, we'll have to work around it by modifying the experimental options JSON.
mExperimentalOptionsPatches.add(
(experimentalOptions) -> {
JSONObject quicOptions = createDefaultIfAbsent(experimentalOptions, "QUIC");
if (options.getEnableDefaultNetworkMigration() != null) {
quicOptions.put(
"migrate_sessions_on_network_change_v2",
options.getEnableDefaultNetworkMigration());
}
if (options.getAllowServerMigration() != null) {
quicOptions.put(
"allow_server_migration", options.getAllowServerMigration());
}
if (options.getMigrateIdleConnections() != null) {
quicOptions.put(
"migrate_idle_sessions", options.getMigrateIdleConnections());
}
if (options.getIdleMigrationPeriodSeconds() != null) {
quicOptions.put(
"idle_session_migration_period_seconds",
options.getIdleMigrationPeriodSeconds());
}
if (options.getRetryPreHandshakeErrorsOnAlternateNetwork() != null) {
quicOptions.put(
"retry_on_alternate_network_before_handshake",
options.getRetryPreHandshakeErrorsOnAlternateNetwork());
}
if (options.getMaxTimeOnNonDefaultNetworkSeconds() != null) {
quicOptions.put(
"max_time_on_non_default_network_seconds",
options.getMaxTimeOnNonDefaultNetworkSeconds());
}
if (options.getMaxPathDegradingEagerMigrationsCount() != null) {
quicOptions.put(
"max_migrations_to_non_default_network_on_path_degrading",
options.getMaxPathDegradingEagerMigrationsCount());
}
if (options.getMaxWriteErrorEagerMigrationsCount() != null) {
quicOptions.put(
"max_migrations_to_non_default_network_on_write_error",
options.getMaxWriteErrorEagerMigrationsCount());
}
if (options.getEnablePathDegradationMigration() != null) {
boolean pathDegradationValue = options.getEnablePathDegradationMigration();
boolean skipPortMigrationFlag = false;
if (options.getAllowNonDefaultNetworkUsage() != null) {
boolean nonDefaultNetworkValue =
options.getAllowNonDefaultNetworkUsage();
if (!pathDegradationValue && nonDefaultNetworkValue) {
// Misconfiguration which doesn't translate easily to the JSON flags
throw new IllegalArgumentException(
"Unable to turn on non-default network usage without path "
+ "degradation migration!");
} else if (pathDegradationValue && nonDefaultNetworkValue) {
// Both values being true results in the non-default network
// migration being enabled.
quicOptions.put("migrate_sessions_early_v2", true);
skipPortMigrationFlag = true;
} else {
quicOptions.put("migrate_sessions_early_v2", false);
}
}
if (!skipPortMigrationFlag) {
quicOptions.put("allow_port_migration", pathDegradationValue);
}
}
});
return this;
}
@Override
public ICronetEngineBuilder setExperimentalOptions(String options) {
if (options == null || options.isEmpty()) {
mParsedExperimentalOptions = null;
} else {
mParsedExperimentalOptions = parseExperimentalOptions(options);
}
return this;
}
@Override
protected Set<Integer> getSupportedConfigOptions() {
return SUPPORTED_OPTIONS;
}
@Override
public ExperimentalCronetEngine build() {
if (mParsedExperimentalOptions == null && mExperimentalOptionsPatches.isEmpty()) {
return mDelegate.build();
}
if (mParsedExperimentalOptions == null) {
mParsedExperimentalOptions = new JSONObject();
}
for (ExperimentalOptionsPatch patch : mExperimentalOptionsPatches) {
try {
patch.applyTo(mParsedExperimentalOptions);
} catch (JSONException e) {
throw new IllegalStateException("Unable to apply JSON patch!", e);
}
}
mDelegate.setExperimentalOptions(mParsedExperimentalOptions.toString());
return mDelegate.build();
}
private static JSONObject parseExperimentalOptions(String jsonString) {
try {
return new JSONObject(jsonString);
} catch (JSONException e) {
throw new IllegalArgumentException("Experimental options parsing failed", e);
}
}
private static JSONObject createDefaultIfAbsent(JSONObject jsonObject, String key) {
JSONObject object = jsonObject.optJSONObject(key);
if (object == null) {
object = new JSONObject();
try {
jsonObject.put(key, object);
} catch (JSONException e) {
throw new IllegalArgumentException(
"Failed adding a default object for key [" + key + "]", e);
}
}
return object;
}
@VisibleForTesting
public ICronetEngineBuilder getDelegate() {
return mDelegate;
}
@FunctionalInterface
private interface ExperimentalOptionsPatch {
void applyTo(JSONObject experimentalOptions) throws JSONException;
}
// Delegating-only methods
@Override
public ICronetEngineBuilder addPublicKeyPins(
String hostName,
Set<byte[]> pinsSha256,
boolean includeSubdomains,
Date expirationDate) {
mDelegate.addPublicKeyPins(hostName, pinsSha256, includeSubdomains, expirationDate);
return this;
}
@Override
public ICronetEngineBuilder addQuicHint(String host, int port, int alternatePort) {
mDelegate.addQuicHint(host, port, alternatePort);
return this;
}
@Override
public ICronetEngineBuilder enableHttp2(boolean value) {
mDelegate.enableHttp2(value);
return this;
}
@Override
public ICronetEngineBuilder enableHttpCache(int cacheMode, long maxSize) {
mDelegate.enableHttpCache(cacheMode, maxSize);
return this;
}
@Override
public ICronetEngineBuilder enablePublicKeyPinningBypassForLocalTrustAnchors(boolean value) {
mDelegate.enablePublicKeyPinningBypassForLocalTrustAnchors(value);
return this;
}
@Override
public ICronetEngineBuilder enableQuic(boolean value) {
mDelegate.enableQuic(value);
return this;
}
@Override
public ICronetEngineBuilder enableSdch(boolean value) {
mDelegate.enableSdch(value);
return this;
}
@Override
public ICronetEngineBuilder enableBrotli(boolean value) {
mDelegate.enableBrotli(value);
return this;
}
@Override
public ICronetEngineBuilder setLibraryLoader(CronetEngine.Builder.LibraryLoader loader) {
mDelegate.setLibraryLoader(loader);
return this;
}
@Override
public ICronetEngineBuilder setStoragePath(String value) {
mDelegate.setStoragePath(value);
return this;
}
@Override
public ICronetEngineBuilder setUserAgent(String userAgent) {
mDelegate.setUserAgent(userAgent);
return this;
}
@Override
public String getDefaultUserAgent() {
return mDelegate.getDefaultUserAgent();
}
@Override
public ICronetEngineBuilder enableNetworkQualityEstimator(boolean value) {
mDelegate.enableNetworkQualityEstimator(value);
return this;
}
@Override
public ICronetEngineBuilder setThreadPriority(int priority) {
mDelegate.setThreadPriority(priority);
return this;
}
}