// 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. * *

{@hide internal class} */ @VisibleForTesting public final class ExperimentalOptionsTranslatingCronetEngineBuilder extends ICronetEngineBuilder { private static final Set SUPPORTED_OPTIONS = Collections.unmodifiableSet( new HashSet( Arrays.asList( ICronetEngineBuilder.CONNECTION_MIGRATION_OPTIONS, ICronetEngineBuilder.DNS_OPTIONS, ICronetEngineBuilder.QUIC_OPTIONS))); private JSONObject mParsedExperimentalOptions; private final List 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 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 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; } }