304 lines
12 KiB
Java
304 lines
12 KiB
Java
/*
|
|
* Copyright (C) 2023 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
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 androidx.annotation.NonNull;
|
|
|
|
import org.chromium.net.ExperimentalCronetEngine;
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
import java.time.Instant;
|
|
import java.util.ArrayList;
|
|
import java.util.Date;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
public class CronetEngineBuilderWrapper extends IHttpEngineBuilder {
|
|
|
|
private final List<ExperimentalOptionsPatch> mExperimentalOptionsPatches = new ArrayList<>();
|
|
private JSONObject mParsedExperimentalOptions = new JSONObject();
|
|
private final ExperimentalCronetEngine.Builder backend;
|
|
|
|
public CronetEngineBuilderWrapper(ExperimentalCronetEngine.Builder backend) {
|
|
this.backend = backend;
|
|
}
|
|
|
|
@Override
|
|
public String getDefaultUserAgent() {
|
|
return backend.getDefaultUserAgent();
|
|
}
|
|
|
|
@Override
|
|
public IHttpEngineBuilder setUserAgent(String userAgent) {
|
|
backend.setUserAgent(userAgent);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public IHttpEngineBuilder setStoragePath(String value) {
|
|
backend.setStoragePath(value);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public IHttpEngineBuilder setEnableQuic(boolean value) {
|
|
backend.enableQuic(value);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public IHttpEngineBuilder setEnableHttp2(boolean value) {
|
|
backend.enableHttp2(value);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public IHttpEngineBuilder setEnableBrotli(boolean value) {
|
|
backend.enableBrotli(value);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public IHttpEngineBuilder setEnableHttpCache(int cacheMode, long maxSize) {
|
|
backend.enableHttpCache(cacheMode, maxSize);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public IHttpEngineBuilder addQuicHint(String host, int port, int alternatePort) {
|
|
backend.addQuicHint(host, port, alternatePort);
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public IHttpEngineBuilder addPublicKeyPins(
|
|
String hostName,
|
|
Set<byte[]> pinsSha256,
|
|
boolean includeSubdomains,
|
|
Instant expirationInstant) {
|
|
backend.addPublicKeyPins(
|
|
hostName, pinsSha256, includeSubdomains, Date.from(expirationInstant));
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public IHttpEngineBuilder setEnablePublicKeyPinningBypassForLocalTrustAnchors(boolean value) {
|
|
backend.enablePublicKeyPinningBypassForLocalTrustAnchors(value);
|
|
return this;
|
|
}
|
|
|
|
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 IHttpEngineBuilder setQuicOptions(@NonNull android.net.http.QuicOptions options) {
|
|
mExperimentalOptionsPatches.add(
|
|
(experimentalOptions) -> {
|
|
JSONObject quicOptions = createDefaultIfAbsent(experimentalOptions, "QUIC");
|
|
|
|
if (!options.getAllowedQuicHosts().isEmpty()) {
|
|
quicOptions.put(
|
|
"host_whitelist", String.join(",", options.getAllowedQuicHosts()));
|
|
}
|
|
|
|
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.getIdleConnectionTimeout() != null) {
|
|
quicOptions.put(
|
|
"idle_connection_timeout_seconds",
|
|
options.getIdleConnectionTimeout().toSeconds());
|
|
}
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public IHttpEngineBuilder setDnsOptions(@NonNull android.net.http.DnsOptions options) {
|
|
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) {
|
|
android.net.http.DnsOptions.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
|
|
public IHttpEngineBuilder setConnectionMigrationOptions(
|
|
@NonNull android.net.http.ConnectionMigrationOptions options) {
|
|
// 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.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);
|
|
skipPortMigrationFlag = true;
|
|
} else {
|
|
quicOptions.put("migrate_sessions_early_v2", false);
|
|
}
|
|
}
|
|
|
|
if (!skipPortMigrationFlag) {
|
|
quicOptions.put("allow_port_migration", pathDegradationValue);
|
|
}
|
|
}
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Build a {@link CronetEngine} using this builder's configuration.
|
|
*
|
|
* @return constructed {@link CronetEngine}.
|
|
*/
|
|
public HttpEngine build() {
|
|
for (ExperimentalOptionsPatch patch : mExperimentalOptionsPatches) {
|
|
try {
|
|
patch.applyTo(mParsedExperimentalOptions);
|
|
} catch (JSONException e) {
|
|
throw new IllegalStateException("Unable to apply JSON patch!", e);
|
|
}
|
|
}
|
|
backend.setExperimentalOptions(mParsedExperimentalOptions.toString());
|
|
return new CronetEngineWrapper(backend.build());
|
|
}
|
|
}
|