blob: f525ad497a816145d65f4644faa31df41fe96cd5 [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 android.net.http;
import static android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED;
import static android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_UNSPECIFIED;
import static android.net.http.DnsOptions.DNS_OPTION_ENABLED;
import static android.net.http.DnsOptions.DNS_OPTION_UNSPECIFIED;
import android.content.Context;
import android.net.http.DnsOptions.StaleDnsOptions;
import androidx.annotation.VisibleForTesting;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* {@link HttpEngine} that exposes experimental features.
*
* <p>{@hide since this class exposes experimental features that should be hidden.}
*
* @deprecated scheduled for deletion, don't use in new code.
*/
@Deprecated
public abstract class ExperimentalHttpEngine extends HttpEngine {
/**
* The value of a connection metric is unknown.
*/
public static final int CONNECTION_METRIC_UNKNOWN = -1;
/**
* The estimate of the effective connection type is unknown.
*
* @see #getEffectiveConnectionType
*/
public static final int EFFECTIVE_CONNECTION_TYPE_UNKNOWN = 0;
/**
* The device is offline.
*
* @see #getEffectiveConnectionType
*/
public static final int EFFECTIVE_CONNECTION_TYPE_OFFLINE = 1;
/**
* The estimate of the effective connection type is slow 2G.
*
* @see #getEffectiveConnectionType
*/
public static final int EFFECTIVE_CONNECTION_TYPE_SLOW_2G = 2;
/**
* The estimate of the effective connection type is 2G.
*
* @see #getEffectiveConnectionType
*/
public static final int EFFECTIVE_CONNECTION_TYPE_2G = 3;
/**
* The estimate of the effective connection type is 3G.
*
* @see #getEffectiveConnectionType
*/
public static final int EFFECTIVE_CONNECTION_TYPE_3G = 4;
/**
* The estimate of the effective connection type is 4G.
*
* @see #getEffectiveConnectionType
*/
public static final int EFFECTIVE_CONNECTION_TYPE_4G = 5;
/** The value to be used to undo any previous network binding. */
public static final long UNBIND_NETWORK_HANDLE = -1;
/**
* A version of {@link HttpEngine.Builder} that exposes experimental features. Instances of
* this class are not meant for general use, but instead only to access experimental features.
* Experimental features may be deprecated in the future. Use at your own risk.
*/
public static class Builder extends HttpEngine.Builder {
private JSONObject mParsedExperimentalOptions;
private final List<ExperimentalOptionsPatch> mExperimentalOptionsPatches =
new ArrayList<>();
/**
* Constructs a {@link Builder} object that facilitates creating a {@link HttpEngine}. The
* default configuration enables HTTP/2 and disables QUIC, SDCH and the HTTP cache.
*
* @param context Android {@link Context}, which is used by the Builder to retrieve the
* application context. A reference to only the application context will be kept, so as
* to avoid extending the lifetime of {@code context} unnecessarily.
*/
public Builder(Context context) {
super(context);
}
/**
* Constructs {@link Builder} with a given delegate that provides the actual implementation
* of the {@code Builder} methods. This constructor is used only by the internal
* implementation.
*
* @param builderDelegate delegate that provides the actual implementation.
* <p>{@hide}
*/
public Builder(IHttpEngineBuilder builderDelegate) {
super(builderDelegate);
}
/**
* Sets experimental options to be used.
*
* @param options JSON formatted experimental options.
* @return the builder to facilitate chaining.
*/
public Builder setExperimentalOptions(String options) {
if (options == null || options.isEmpty()) {
mParsedExperimentalOptions = null;
} else {
mParsedExperimentalOptions = parseExperimentalOptions(options);
}
return this;
}
/**
* Enables the network quality estimator, which collects and reports
* measurements of round trip time (RTT) and downstream throughput at
* various layers of the network stack. After enabling the estimator,
* listeners of RTT and throughput can be added with
* {@link #addRttListener} and {@link #addThroughputListener} and
* removed with {@link #removeRttListener} and
* {@link #removeThroughputListener}. The estimator uses memory and CPU
* only when enabled.
* @param value {@code true} to enable network quality estimator,
* {@code false} to disable.
* @return the builder to facilitate chaining.
*/
public Builder enableNetworkQualityEstimator(boolean value) {
mBuilderDelegate.enableNetworkQualityEstimator(value);
return this;
}
/**
* Sets the thread priority of the internal thread.
*
* @param priority the thread priority of the internal thread.
* A Linux priority level, from -20 for highest scheduling
* priority to 19 for lowest scheduling priority. For more
* information on values, see
* {@link android.os.Process#setThreadPriority(int, int)} and
* {@link android.os.Process#THREAD_PRIORITY_DEFAULT
* THREAD_PRIORITY_*} values.
* @return the builder to facilitate chaining.
*/
public Builder setThreadPriority(int priority) {
mBuilderDelegate.setThreadPriority(priority);
return this;
}
/**
* Returns delegate, only for testing.
*
* @hide
*/
@VisibleForTesting
public IHttpEngineBuilder getBuilderDelegate() {
return mBuilderDelegate;
}
// To support method chaining, override superclass methods to return an
// instance of this class instead of the parent.
@Override
public Builder setUserAgent(String userAgent) {
super.setUserAgent(userAgent);
return this;
}
@Override
public Builder setStoragePath(String value) {
super.setStoragePath(value);
return this;
}
@Override
public Builder setEnableQuic(boolean value) {
super.setEnableQuic(value);
return this;
}
@Override
public Builder setEnableHttp2(boolean value) {
super.setEnableHttp2(value);
return this;
}
@Override
@QuicOptions.Experimental
public Builder setQuicOptions(QuicOptions options) {
// If the delegate builder supports enabling connection migration directly, just use it
if (mBuilderDelegate.getSupportedConfigOptions().contains(
IHttpEngineBuilder.QUIC_OPTIONS)) {
mBuilderDelegate.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.getAllowedQuicHosts().isEmpty()) {
quicOptions.put(
"host_whitelist", String.join(",", options.getAllowedQuicHosts()));
}
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.hasInMemoryServerConfigsCacheSize()) {
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.getPreCryptoHandshakeIdleTimeout() != null) {
quicOptions.put("max_idle_time_before_crypto_handshake_seconds",
options.getPreCryptoHandshakeIdleTimeout().toSeconds());
}
if (options.getCryptoHandshakeTimeout() != null) {
quicOptions.put("max_time_before_crypto_handshake_seconds",
options.getCryptoHandshakeTimeout().toSeconds());
}
if (options.getIdleConnectionTimeout() != null) {
quicOptions.put("idle_connection_timeout_seconds",
options.getIdleConnectionTimeout().toSeconds());
}
if (options.getRetransmittableOnWireTimeout() != null) {
quicOptions.put("retransmittable_on_wire_timeout_milliseconds",
options.getRetransmittableOnWireTimeout().toMillis());
}
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.getInitialBrokenServicePeriod() != null) {
quicOptions.put("initial_delay_for_broken_alternative_service_seconds",
options.getInitialBrokenServicePeriod().toSeconds());
}
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
@DnsOptions.Experimental
public Builder setDnsOptions(DnsOptions options) {
// If the delegate builder supports enabling connection migration directly, just use it
if (mBuilderDelegate.getSupportedConfigOptions().contains(
IHttpEngineBuilder.DNS_OPTIONS)) {
mBuilderDelegate.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.getUseHttpStackDnsResolver() != DNS_OPTION_UNSPECIFIED) {
asyncDnsOptions.put("enable",
options.getUseHttpStackDnsResolver() == DNS_OPTION_ENABLED);
}
JSONObject staleDnsOptions = createDefaultIfAbsent(experimentalOptions, "StaleDNS");
if (options.getStaleDns() != DNS_OPTION_UNSPECIFIED) {
staleDnsOptions.put("enable",
options.getStaleDns() == DNS_OPTION_ENABLED);
}
if (options.getPersistHostCache() != DNS_OPTION_UNSPECIFIED) {
staleDnsOptions.put("persist_to_disk",
options.getPersistHostCache() == DNS_OPTION_ENABLED);
}
if (options.getPersistHostCachePeriod() != null) {
staleDnsOptions.put(
"persist_delay_ms", options.getPersistHostCachePeriod().toMillis());
}
if (options.getStaleDnsOptions() != null) {
StaleDnsOptions staleDnsOptionsJava = options.getStaleDnsOptions();
if (staleDnsOptionsJava.getAllowCrossNetworkUsage()
!= DNS_OPTION_UNSPECIFIED) {
staleDnsOptions.put("allow_other_network",
staleDnsOptionsJava.getAllowCrossNetworkUsage()
== DNS_OPTION_ENABLED);
}
if (staleDnsOptionsJava.getFreshLookupTimeout() != null) {
staleDnsOptions.put(
"delay_ms", staleDnsOptionsJava.getFreshLookupTimeout().toMillis());
}
if (staleDnsOptionsJava.getUseStaleOnNameNotResolved()
!= DNS_OPTION_UNSPECIFIED) {
staleDnsOptions.put("use_stale_on_name_not_resolved",
staleDnsOptionsJava.getUseStaleOnNameNotResolved()
== DNS_OPTION_ENABLED);
}
if (staleDnsOptionsJava.getMaxExpiredDelay() != null) {
staleDnsOptions.put("max_expired_time_ms",
staleDnsOptionsJava.getMaxExpiredDelay().toMillis());
}
}
JSONObject quicOptions = createDefaultIfAbsent(experimentalOptions, "QUIC");
if (options.getPreestablishConnectionsToStaleDnsResults()
!= DNS_OPTION_UNSPECIFIED) {
quicOptions.put("race_stale_dns_on_connection",
options.getPreestablishConnectionsToStaleDnsResults()
== DNS_OPTION_ENABLED);
}
});
return this;
}
@Override
@ConnectionMigrationOptions.Experimental
public Builder setConnectionMigrationOptions(ConnectionMigrationOptions options) {
// If the delegate builder supports enabling connection migration directly, just use it
if (mBuilderDelegate.getSupportedConfigOptions().contains(
IHttpEngineBuilder.CONNECTION_MIGRATION_OPTIONS)) {
mBuilderDelegate.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.getDefaultNetworkMigration() != MIGRATION_OPTION_UNSPECIFIED) {
quicOptions.put("migrate_sessions_on_network_change_v2",
options.getDefaultNetworkMigration()
== MIGRATION_OPTION_ENABLED);
}
if (options.getAllowServerMigration() != null) {
quicOptions.put("allow_server_migration", options.getAllowServerMigration());
}
if (options.getMigrateIdleConnections() != null) {
quicOptions.put("migrate_idle_sessions", options.getMigrateIdleConnections());
}
if (options.getIdleMigrationPeriod() != null) {
quicOptions.put("idle_session_migration_period_seconds",
options.getIdleMigrationPeriod().toSeconds());
}
if (options.getMaxTimeOnNonDefaultNetwork() != null) {
quicOptions.put("max_time_on_non_default_network_seconds",
options.getMaxTimeOnNonDefaultNetwork().toSeconds());
}
if (options.getMaxPathDegradingNonDefaultMigrationsCount() != null) {
quicOptions.put("max_migrations_to_non_default_network_on_path_degrading",
options.getMaxPathDegradingNonDefaultMigrationsCount());
}
if (options.getMaxWriteErrorNonDefaultNetworkMigrationsCount() != null) {
quicOptions.put("max_migrations_to_non_default_network_on_write_error",
options.getMaxWriteErrorNonDefaultNetworkMigrationsCount());
}
if (options.getPathDegradationMigration() != MIGRATION_OPTION_UNSPECIFIED) {
boolean pathDegradationValue = (options.getPathDegradationMigration()
== MIGRATION_OPTION_ENABLED);
boolean skipPortMigrationFlag = false;
if (options.getAllowNonDefaultNetworkUsage()
!= MIGRATION_OPTION_UNSPECIFIED) {
boolean nonDefaultNetworkValue =
(options.getAllowNonDefaultNetworkUsage()
== MIGRATION_OPTION_ENABLED);
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);
quicOptions.put("retry_on_alternate_network_before_handshake", true);
skipPortMigrationFlag = true;
} else {
quicOptions.put("migrate_sessions_early_v2", false);
}
}
if (!skipPortMigrationFlag) {
quicOptions.put("allow_port_migration", pathDegradationValue);
}
}
});
return this;
}
@Override
public Builder setEnableHttpCache(int cacheMode, long maxSize) {
super.setEnableHttpCache(cacheMode, maxSize);
return this;
}
@Override
public Builder addQuicHint(String host, int port, int alternatePort) {
super.addQuicHint(host, port, alternatePort);
return this;
}
@Override
public Builder addPublicKeyPins(String hostName, Set<byte[]> pinsSha256,
boolean includeSubdomains, Instant expirationInstant) {
super.addPublicKeyPins(hostName, pinsSha256, includeSubdomains, expirationInstant);
return this;
}
@Override
public Builder setEnablePublicKeyPinningBypassForLocalTrustAnchors(boolean value) {
super.setEnablePublicKeyPinningBypassForLocalTrustAnchors(value);
return this;
}
@Override
public ExperimentalHttpEngine build() {
if (mParsedExperimentalOptions == null && mExperimentalOptionsPatches.isEmpty()) {
return mBuilderDelegate.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);
}
}
mBuilderDelegate.setExperimentalOptions(mParsedExperimentalOptions.toString());
return mBuilderDelegate.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;
}
@FunctionalInterface
private interface ExperimentalOptionsPatch {
void applyTo(JSONObject experimentalOptions) throws JSONException;
}
}
@Override
public abstract ExperimentalBidirectionalStream.Builder newBidirectionalStreamBuilder(
String url, Executor executor, BidirectionalStream.Callback callback);
@Override
public abstract ExperimentalUrlRequest.Builder newUrlRequestBuilder(
String url, Executor executor, UrlRequest.Callback callback);
/**
* Starts NetLog logging to a specified directory with a bounded size. The NetLog will contain
* events emitted by all live CronetEngines. The NetLog is useful for debugging.
* Once logging has stopped {@link #stopNetLog}, the data will be written
* to netlog.json in {@code dirPath}. If logging is interrupted, you can
* stitch the files found in .inprogress subdirectory manually using:
* https://chromium.googlesource.com/chromium/src/+/main/net/tools/stitch_net_log_files.py.
* The log can be viewed using a Chrome browser navigated to chrome://net-internals/#import.
* @param dirPath the directory where the netlog.json file will be created. dirPath must
* already exist. NetLog files must not exist in the directory. If actively
* logging, this method is ignored.
* @param logAll {@code true} to include basic events, user cookies,
* credentials and all transferred bytes in the log. This option presents a
* privacy risk, since it exposes the user's credentials, and should only be
* used with the user's consent and in situations where the log won't be public.
* {@code false} to just include basic events.
* @param maxSize the maximum total disk space in bytes that should be used by NetLog. Actual
* disk space usage may exceed this limit slightly.
*/
public void startNetLogToDisk(String dirPath, boolean logAll, int maxSize) {}
/**
* Returns an estimate of the effective connection type computed by the network quality
* estimator. Call {@link Builder#enableNetworkQualityEstimator} to begin computing this
* value.
*
* @return the estimated connection type. The returned value is one of
* {@link #EFFECTIVE_CONNECTION_TYPE_UNKNOWN EFFECTIVE_CONNECTION_TYPE_* }.
*/
public int getEffectiveConnectionType() {
return EFFECTIVE_CONNECTION_TYPE_UNKNOWN;
}
/**
* Configures the network quality estimator for testing. This must be called
* before round trip time and throughput listeners are added, and after the
* network quality estimator has been enabled.
* @param useLocalHostRequests include requests to localhost in estimates.
* @param useSmallerResponses include small responses in throughput estimates.
* @param disableOfflineCheck when set to true, disables the device offline checks when
* computing the effective connection type or when writing the prefs.
*/
public void configureNetworkQualityEstimatorForTesting(boolean useLocalHostRequests,
boolean useSmallerResponses, boolean disableOfflineCheck) {}
/**
* Registers a listener that gets called whenever the network quality
* estimator witnesses a sample round trip time. This must be called
* after {@link Builder#enableNetworkQualityEstimator}, and with throw an
* exception otherwise. Round trip times may be recorded at various layers
* of the network stack, including TCP, QUIC, and at the URL request layer.
* The listener is called on the {@link java.util.concurrent.Executor} that
* is passed to {@link Builder#enableNetworkQualityEstimator}.
* @param listener the listener of round trip times.
*/
public void addRttListener(NetworkQualityRttListener listener) {}
/**
* Removes a listener of round trip times if previously registered with
* {@link #addRttListener}. This should be called after a
* {@link NetworkQualityRttListener} is added in order to stop receiving
* observations.
* @param listener the listener of round trip times.
*/
public void removeRttListener(NetworkQualityRttListener listener) {}
/**
* Registers a listener that gets called whenever the network quality
* estimator witnesses a sample throughput measurement. This must be called
* after {@link Builder#enableNetworkQualityEstimator}. Throughput observations
* are computed by measuring bytes read over the active network interface
* at times when at least one URL response is being received. The listener
* is called on the {@link java.util.concurrent.Executor} that is passed to
* {@link Builder#enableNetworkQualityEstimator}.
* @param listener the listener of throughput.
*/
public void addThroughputListener(NetworkQualityThroughputListener listener) {}
/**
* Removes a listener of throughput. This should be called after a
* {@link NetworkQualityThroughputListener} is added with
* {@link #addThroughputListener} in order to stop receiving observations.
* @param listener the listener of throughput.
*/
public void removeThroughputListener(NetworkQualityThroughputListener listener) {}
/**
* Establishes a new connection to the resource specified by the {@link URL} {@code url}
* using the given proxy.
* <p>
* <b>Note:</b> this {@link java.net.HttpURLConnection} implementation is subject to certain
* limitations, see {@link #createUrlStreamHandlerFactory} for details.
*
* @param url URL of resource to connect to.
* @param proxy proxy to use when establishing connection.
* @return an {@link java.net.HttpURLConnection} instance implemented by this HttpEngine.
* @throws IOException if an error occurs while opening the connection.
*/
// TODO(pauljensen): Expose once implemented, http://crbug.com/418111
public URLConnection openConnection(URL url, Proxy proxy) throws IOException {
return url.openConnection(proxy);
}
/**
* Registers a listener that gets called after the end of each request with the request info.
*
* <p>The listener is called on an {@link java.util.concurrent.Executor} provided by the
* listener.
*
* @param listener the listener for finished requests.
*/
public void addRequestFinishedListener(RequestFinishedInfo.Listener listener) {}
/**
* Removes a finished request listener.
*
* @param listener the listener to remove.
*/
public void removeRequestFinishedListener(RequestFinishedInfo.Listener listener) {}
/**
* Returns the HTTP RTT estimate (in milliseconds) computed by the network
* quality estimator. Set to {@link #CONNECTION_METRIC_UNKNOWN} if the value
* is unavailable. This must be called after
* {@link Builder#enableNetworkQualityEstimator}, and will throw an
* exception otherwise.
* @return Estimate of the HTTP RTT in milliseconds.
*/
public int getHttpRttMs() {
return CONNECTION_METRIC_UNKNOWN;
}
/**
* Returns the transport RTT estimate (in milliseconds) computed by the
* network quality estimator. Set to {@link #CONNECTION_METRIC_UNKNOWN} if
* the value is unavailable. This must be called after
* {@link Builder#enableNetworkQualityEstimator}, and will throw an
* exception otherwise.
* @return Estimate of the transport RTT in milliseconds.
*/
public int getTransportRttMs() {
return CONNECTION_METRIC_UNKNOWN;
}
/**
* Returns the downstream throughput estimate (in kilobits per second)
* computed by the network quality estimator. Set to
* {@link #CONNECTION_METRIC_UNKNOWN} if the value is
* unavailable. This must be called after
* {@link Builder#enableNetworkQualityEstimator}, and will
* throw an exception otherwise.
* @return Estimate of the downstream throughput in kilobits per second.
*/
public int getDownstreamThroughputKbps() {
return CONNECTION_METRIC_UNKNOWN;
}
}