| // 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; |
| } |
| } |