1 // Copyright 2023 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net.impl;
6 
7 import static org.chromium.net.impl.HttpEngineNativeProvider.EXT_API_LEVEL;
8 import static org.chromium.net.impl.HttpEngineNativeProvider.EXT_VERSION;
9 
10 import android.net.http.HttpEngine;
11 import android.util.Log;
12 
13 import androidx.annotation.RequiresExtension;
14 import androidx.annotation.VisibleForTesting;
15 
16 import org.chromium.net.CronetEngine;
17 import org.chromium.net.ExperimentalCronetEngine;
18 import org.chromium.net.ICronetEngineBuilder;
19 import org.chromium.net.telemetry.ExperimentalOptions;
20 import org.chromium.net.telemetry.OptionalBoolean;
21 
22 import java.time.Duration;
23 import java.util.Date;
24 import java.util.Set;
25 
26 @RequiresExtension(extension = EXT_API_LEVEL, version = EXT_VERSION)
27 class AndroidHttpEngineBuilderWrapper extends ICronetEngineBuilder {
28     private static final String TAG = "HttpEngBuilderWrap";
29 
30     private final HttpEngine.Builder mBackend;
31 
AndroidHttpEngineBuilderWrapper(HttpEngine.Builder backend)32     public AndroidHttpEngineBuilderWrapper(HttpEngine.Builder backend) {
33         this.mBackend = backend;
34     }
35 
36     @Override
getDefaultUserAgent()37     public String getDefaultUserAgent() {
38         return mBackend.getDefaultUserAgent();
39     }
40 
41     @Override
setUserAgent(String userAgent)42     public ICronetEngineBuilder setUserAgent(String userAgent) {
43         mBackend.setUserAgent(userAgent);
44         return this;
45     }
46 
47     @Override
setStoragePath(String value)48     public ICronetEngineBuilder setStoragePath(String value) {
49         mBackend.setStoragePath(value);
50         return this;
51     }
52 
53     @Override
setLibraryLoader(CronetEngine.Builder.LibraryLoader loader)54     public ICronetEngineBuilder setLibraryLoader(CronetEngine.Builder.LibraryLoader loader) {
55         Log.w(
56                 TAG,
57                 "Custom library loader isn't supported when using the platform Cronet provider."
58                         + " Ignoring...");
59         return this;
60     }
61 
62     @Override
enableQuic(boolean value)63     public ICronetEngineBuilder enableQuic(boolean value) {
64         mBackend.setEnableQuic(value);
65         return this;
66     }
67 
68     @Override
enableSdch(boolean value)69     public ICronetEngineBuilder enableSdch(boolean value) {
70         // Deprecated and unused by upper layers, do nothing.
71         return this;
72     }
73 
74     @Override
enableHttp2(boolean value)75     public ICronetEngineBuilder enableHttp2(boolean value) {
76         mBackend.setEnableHttp2(value);
77         return this;
78     }
79 
80     @Override
enableBrotli(boolean value)81     public ICronetEngineBuilder enableBrotli(boolean value) {
82         mBackend.setEnableBrotli(value);
83         return this;
84     }
85 
86     @Override
enableHttpCache(int cacheMode, long maxSize)87     public ICronetEngineBuilder enableHttpCache(int cacheMode, long maxSize) {
88         mBackend.setEnableHttpCache(cacheMode, maxSize);
89         return this;
90     }
91 
92     @Override
addQuicHint(String host, int port, int alternatePort)93     public ICronetEngineBuilder addQuicHint(String host, int port, int alternatePort) {
94         mBackend.addQuicHint(host, port, alternatePort);
95         return this;
96     }
97 
98     @Override
addPublicKeyPins( String hostName, Set<byte[]> pinsSha256, boolean includeSubdomains, Date expirationDate)99     public ICronetEngineBuilder addPublicKeyPins(
100             String hostName,
101             Set<byte[]> pinsSha256,
102             boolean includeSubdomains,
103             Date expirationDate) {
104         mBackend.addPublicKeyPins(
105                 hostName, pinsSha256, includeSubdomains, expirationDate.toInstant());
106         return this;
107     }
108 
109     @Override
enablePublicKeyPinningBypassForLocalTrustAnchors(boolean value)110     public ICronetEngineBuilder enablePublicKeyPinningBypassForLocalTrustAnchors(boolean value) {
111         mBackend.setEnablePublicKeyPinningBypassForLocalTrustAnchors(value);
112         return this;
113     }
114 
115     @Override
setExperimentalOptions(String stringOptions)116     public ICronetEngineBuilder setExperimentalOptions(String stringOptions) {
117         // This only translates known experimental options
118         ExperimentalOptions options = new ExperimentalOptions(stringOptions);
119         mBackend.setConnectionMigrationOptions(parseConnectionMigrationOptions(options));
120         mBackend.setDnsOptions(parseDnsOptions(options));
121         mBackend.setQuicOptions(parseQuicOptions(options));
122         return this;
123     }
124 
125     /**
126      * Build a {@link CronetEngine} using this builder's configuration.
127      *
128      * @return constructed {@link CronetEngine}.
129      */
130     @Override
build()131     public ExperimentalCronetEngine build() {
132         return new AndroidHttpEngineWrapper(mBackend.build());
133     }
134 
135     @VisibleForTesting
parseConnectionMigrationOptions( ExperimentalOptions options)136     public static android.net.http.ConnectionMigrationOptions parseConnectionMigrationOptions(
137             ExperimentalOptions options) {
138         android.net.http.ConnectionMigrationOptions.Builder cmOptionsBuilder =
139                 new android.net.http.ConnectionMigrationOptions.Builder();
140 
141         cmOptionsBuilder.setDefaultNetworkMigration(
142                 optionalBooleanToMigrationOptionState(
143                         options.getMigrateSessionsOnNetworkChangeV2Option()));
144         cmOptionsBuilder.setPathDegradationMigration(
145                 optionalBooleanToMigrationOptionState(options.getAllowPortMigration()));
146 
147         OptionalBoolean migrateSessionsEarly = options.getMigrateSessionsEarlyV2();
148         cmOptionsBuilder.setAllowNonDefaultNetworkUsage(
149                 optionalBooleanToMigrationOptionState(migrateSessionsEarly));
150         if (migrateSessionsEarly == OptionalBoolean.TRUE) {
151             cmOptionsBuilder.setPathDegradationMigration(
152                     optionalBooleanToMigrationOptionState(OptionalBoolean.TRUE));
153         }
154 
155         return cmOptionsBuilder.build();
156     }
157 
158     @VisibleForTesting
parseDnsOptions(ExperimentalOptions options)159     public static android.net.http.DnsOptions parseDnsOptions(ExperimentalOptions options) {
160         android.net.http.DnsOptions.StaleDnsOptions.Builder staleDnsOptionBuilder =
161                 new android.net.http.DnsOptions.StaleDnsOptions.Builder();
162         int staleDnsDelay = options.getStaleDnsDelayMillisOption();
163         if (staleDnsDelay != ExperimentalOptions.UNSET_INT_VALUE) {
164             staleDnsOptionBuilder.setFreshLookupTimeout(Duration.ofMillis(staleDnsDelay));
165         }
166 
167         int expiredDelay = options.getStaleDnsMaxExpiredTimeMillisOption();
168         if (expiredDelay != ExperimentalOptions.UNSET_INT_VALUE) {
169             staleDnsOptionBuilder.setMaxExpiredDelay(Duration.ofMillis(expiredDelay));
170         }
171 
172         staleDnsOptionBuilder
173                 .setAllowCrossNetworkUsage(
174                         optionalBooleanToMigrationOptionState(
175                                 options.getStaleDnsAllowOtherNetworkOption()))
176                 .setUseStaleOnNameNotResolved(
177                         optionalBooleanToMigrationOptionState(
178                                 options.getStaleDnsUseStaleOnNameNotResolvedOption()));
179 
180         android.net.http.DnsOptions.Builder dnsOptionsBuilder =
181                 new android.net.http.DnsOptions.Builder();
182         dnsOptionsBuilder
183                 .setUseHttpStackDnsResolver(
184                         optionalBooleanToMigrationOptionState(options.getAsyncDnsEnableOption()))
185                 .setStaleDns(
186                         optionalBooleanToMigrationOptionState(options.getStaleDnsEnableOption()))
187                 .setStaleDnsOptions(staleDnsOptionBuilder.build())
188                 .setPreestablishConnectionsToStaleDnsResults(
189                         optionalBooleanToMigrationOptionState(
190                                 options.getRaceStaleDnsOnConnection()))
191                 .setPersistHostCache(
192                         optionalBooleanToMigrationOptionState(
193                                 options.getStaleDnsPersistToDiskOption()));
194         int persistHostCachePeriod = options.getStaleDnsPersistDelayMillisOption();
195         if (persistHostCachePeriod != ExperimentalOptions.UNSET_INT_VALUE) {
196             dnsOptionsBuilder.setPersistHostCachePeriod(Duration.ofMillis(persistHostCachePeriod));
197         }
198 
199         return dnsOptionsBuilder.build();
200     }
201 
202     @VisibleForTesting
parseQuicOptions(ExperimentalOptions options)203     public static android.net.http.QuicOptions parseQuicOptions(ExperimentalOptions options) {
204         android.net.http.QuicOptions.Builder quicOptionsBuilder =
205                 new android.net.http.QuicOptions.Builder();
206 
207         if (options.getHostWhitelist() != null) {
208             for (String host : options.getHostWhitelist().split(",")) {
209                 quicOptionsBuilder.addAllowedQuicHost(host);
210             }
211         }
212 
213         int inMemoryServerConfigsCacheSize = options.getMaxServerConfigsStoredInPropertiesOption();
214         if (inMemoryServerConfigsCacheSize != ExperimentalOptions.UNSET_INT_VALUE) {
215             quicOptionsBuilder.setInMemoryServerConfigsCacheSize(inMemoryServerConfigsCacheSize);
216         }
217 
218         String handshakeUserAgent = options.getUserAgentId();
219         if (handshakeUserAgent != null) {
220             quicOptionsBuilder.setHandshakeUserAgent(handshakeUserAgent);
221         }
222 
223         int idleConnectionTimeoutSeconds = options.getIdleConnectionTimeoutSecondsOption();
224         if (idleConnectionTimeoutSeconds != ExperimentalOptions.UNSET_INT_VALUE) {
225             quicOptionsBuilder.setIdleConnectionTimeout(
226                     Duration.ofSeconds(idleConnectionTimeoutSeconds));
227         }
228 
229         return quicOptionsBuilder.build();
230     }
231 
232     /**
233      * HttpEngine XOptions exposes X_OPTION_* IntDefs that map to the same integer values. To
234      * simplify the code, we are reusing ConnectionMigrationOptions.MIGRATION_OPTION_* for
235      * DnsOptions and QuicOptions.
236      */
optionalBooleanToMigrationOptionState(OptionalBoolean value)237     private static int optionalBooleanToMigrationOptionState(OptionalBoolean value) {
238         switch (value) {
239             case TRUE:
240                 return android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED;
241             case FALSE:
242                 return android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_DISABLED;
243             case UNSET:
244                 return android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_UNSPECIFIED;
245         }
246 
247         throw new AssertionError("Invalid OptionalBoolean value: " + value);
248     }
249 }
250