• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2016 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 package org.chromium.net.impl;
5 
6 import static android.os.Process.THREAD_PRIORITY_LOWEST;
7 
8 import android.content.Context;
9 import android.util.Base64;
10 
11 import androidx.annotation.IntDef;
12 import androidx.annotation.VisibleForTesting;
13 
14 import android.net.http.HttpEngine;
15 import android.net.http.IHttpEngineBuilder;
16 
17 import java.io.File;
18 import java.lang.annotation.Retention;
19 import java.lang.annotation.RetentionPolicy;
20 import java.net.IDN;
21 import java.time.Instant;
22 import java.util.HashMap;
23 import java.util.LinkedList;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.regex.Pattern;
28 
29 /**
30  * Implementation of {@link IHttpEngineBuilder}.
31  */
32 public abstract class CronetEngineBuilderImpl extends IHttpEngineBuilder {
33     /**
34      * A hint that a host supports QUIC.
35      */
36     public static class QuicHint {
37         // The host.
38         final String mHost;
39         // Port of the server that supports QUIC.
40         final int mPort;
41         // Alternate protocol port.
42         final int mAlternatePort;
43 
QuicHint(String host, int port, int alternatePort)44         QuicHint(String host, int port, int alternatePort) {
45             mHost = host;
46             mPort = port;
47             mAlternatePort = alternatePort;
48         }
49     }
50 
51     /**
52      * A public key pin.
53      */
54     public static class Pkp {
55         // Host to pin for.
56         final String mHost;
57         // Array of SHA-256 hashes of keys.
58         final byte[][] mHashes;
59         // Should pin apply to subdomains?
60         final boolean mIncludeSubdomains;
61         // When the pin expires.
62         final Instant mExpirationInsant;
63 
Pkp(String host, byte[][] hashes, boolean includeSubdomains, Instant expirationInstant)64         Pkp(String host, byte[][] hashes, boolean includeSubdomains, Instant expirationInstant) {
65             mHost = host;
66             mHashes = hashes;
67             mIncludeSubdomains = includeSubdomains;
68             mExpirationInsant = expirationInstant;
69         }
70     }
71 
72     /**
73      * Mapping between public builder view of HttpCacheMode and internal builder one.
74      */
75     @VisibleForTesting
76     public enum HttpCacheMode {
77         DISABLED(HttpCacheType.DISABLED, false),
78         DISK(HttpCacheType.DISK, true),
79         DISK_NO_HTTP(HttpCacheType.DISK, false),
80         MEMORY(HttpCacheType.MEMORY, true);
81 
82         private final int mType;
83         private final boolean mContentCacheEnabled;
84 
HttpCacheMode(int type, boolean contentCacheEnabled)85         private HttpCacheMode(int type, boolean contentCacheEnabled) {
86             mContentCacheEnabled = contentCacheEnabled;
87             mType = type;
88         }
89 
getType()90         int getType() {
91             return mType;
92         }
93 
isContentCacheEnabled()94         boolean isContentCacheEnabled() {
95             return mContentCacheEnabled;
96         }
97 
98         @HttpCacheSetting
99         @VisibleForTesting
toPublicBuilderCacheMode()100         public int toPublicBuilderCacheMode() {
101             switch (this) {
102                 case DISABLED:
103                     return HttpEngine.Builder.HTTP_CACHE_DISABLED;
104                 case DISK_NO_HTTP:
105                     return HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP;
106                 case DISK:
107                     return HttpEngine.Builder.HTTP_CACHE_DISK;
108                 case MEMORY:
109                     return HttpEngine.Builder.HTTP_CACHE_IN_MEMORY;
110                 default:
111                     throw new IllegalArgumentException("Unknown internal builder cache mode");
112             }
113         }
114 
115         @VisibleForTesting
fromPublicBuilderCacheMode(@ttpCacheSetting int cacheMode)116         public static HttpCacheMode fromPublicBuilderCacheMode(@HttpCacheSetting int cacheMode) {
117             switch (cacheMode) {
118                 case HttpEngine.Builder.HTTP_CACHE_DISABLED:
119                     return DISABLED;
120                 case HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP:
121                     return DISK_NO_HTTP;
122                 case HttpEngine.Builder.HTTP_CACHE_DISK:
123                     return DISK;
124                 case HttpEngine.Builder.HTTP_CACHE_IN_MEMORY:
125                     return MEMORY;
126                 default:
127                     throw new IllegalArgumentException("Unknown public builder cache mode");
128             }
129         }
130     }
131 
132     private static final Pattern INVALID_PKP_HOST_NAME = Pattern.compile("^[0-9\\.]*$");
133 
134     private static final int INVALID_THREAD_PRIORITY = THREAD_PRIORITY_LOWEST + 1;
135 
136     // Private fields are simply storage of configuration for the resulting CronetEngine.
137     // See setters below for verbose descriptions.
138     private final Context mApplicationContext;
139     private final List<QuicHint> mQuicHints = new LinkedList<>();
140     private final List<Pkp> mPkps = new LinkedList<>();
141     private boolean mPublicKeyPinningBypassForLocalTrustAnchorsEnabled;
142     private String mUserAgent;
143     private String mStoragePath;
144     private boolean mQuicEnabled;
145     private boolean mHttp2Enabled;
146     private boolean mBrotiEnabled;
147     private boolean mDisableCache;
148     private HttpCacheMode mHttpCacheMode;
149     private long mHttpCacheMaxSize;
150     private String mExperimentalOptions;
151     protected long mMockCertVerifier;
152     private boolean mNetworkQualityEstimatorEnabled;
153     private int mThreadPriority = INVALID_THREAD_PRIORITY;
154 
155     /**
156      * Default config enables SPDY and QUIC, disables SDCH and HTTP cache.
157      * @param context Android {@link Context} for engine to use.
158      */
CronetEngineBuilderImpl(Context context)159     public CronetEngineBuilderImpl(Context context) {
160         mApplicationContext = context.getApplicationContext();
161         enableQuic(true);
162         enableHttp2(true);
163         enableBrotli(false);
164         enableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISABLED, 0);
165         enableNetworkQualityEstimator(false);
166         enablePublicKeyPinningBypassForLocalTrustAnchors(true);
167     }
168 
169     @Override
getDefaultUserAgent()170     public String getDefaultUserAgent() {
171         return UserAgent.getDefault();
172     }
173 
174     @Override
setUserAgent(String userAgent)175     public CronetEngineBuilderImpl setUserAgent(String userAgent) {
176         mUserAgent = userAgent;
177         return this;
178     }
179 
180     @VisibleForTesting
getUserAgent()181     public String getUserAgent() {
182         return mUserAgent;
183     }
184 
185     @Override
setStoragePath(String value)186     public CronetEngineBuilderImpl setStoragePath(String value) {
187         if (!new File(value).isDirectory()) {
188             throw new IllegalArgumentException("Storage path must be set to existing directory");
189         }
190         mStoragePath = value;
191         return this;
192     }
193 
194     @VisibleForTesting
storagePath()195     public String storagePath() {
196         return mStoragePath;
197     }
198 
199     @Override
enableQuic(boolean value)200     public CronetEngineBuilderImpl enableQuic(boolean value) {
201         mQuicEnabled = value;
202         return this;
203     }
204 
205     @VisibleForTesting
quicEnabled()206     public boolean quicEnabled() {
207         return mQuicEnabled;
208     }
209 
210     /**
211      * Constructs default QUIC User Agent Id string including application name
212      * and Cronet version. Returns empty string if QUIC is not enabled.
213      *
214      * @return QUIC User Agent ID string.
215      */
getDefaultQuicUserAgentId()216     String getDefaultQuicUserAgentId() {
217         return mQuicEnabled ? UserAgent.getDefaultQuicUserAgentId() : "";
218     }
219 
220     @Override
enableHttp2(boolean value)221     public CronetEngineBuilderImpl enableHttp2(boolean value) {
222         mHttp2Enabled = value;
223         return this;
224     }
225 
226     @VisibleForTesting
http2Enabled()227     public boolean http2Enabled() {
228         return mHttp2Enabled;
229     }
230 
231     @Override
enableSdch(boolean value)232     public CronetEngineBuilderImpl enableSdch(boolean value) {
233         return this;
234     }
235 
236     @Override
enableBrotli(boolean value)237     public CronetEngineBuilderImpl enableBrotli(boolean value) {
238         mBrotiEnabled = value;
239         return this;
240     }
241 
242     @VisibleForTesting
brotliEnabled()243     public boolean brotliEnabled() {
244         return mBrotiEnabled;
245     }
246 
247     @IntDef({HttpEngine.Builder.HTTP_CACHE_DISABLED, HttpEngine.Builder.HTTP_CACHE_IN_MEMORY,
248             HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, HttpEngine.Builder.HTTP_CACHE_DISK})
249     @Retention(RetentionPolicy.SOURCE)
250     public @interface HttpCacheSetting {}
251 
252     @Override
enableHttpCache(@ttpCacheSetting int cacheMode, long maxSize)253     public CronetEngineBuilderImpl enableHttpCache(@HttpCacheSetting int cacheMode, long maxSize) {
254         HttpCacheMode cacheModeEnum = HttpCacheMode.fromPublicBuilderCacheMode(cacheMode);
255 
256         if (cacheModeEnum.getType() == HttpCacheType.DISK && storagePath() == null) {
257             throw new IllegalArgumentException("Storage path must be set");
258         }
259 
260         mHttpCacheMode = cacheModeEnum;
261         mHttpCacheMaxSize = maxSize;
262 
263         return this;
264     }
265 
cacheDisabled()266     boolean cacheDisabled() {
267         return !mHttpCacheMode.isContentCacheEnabled();
268     }
269 
httpCacheMaxSize()270     long httpCacheMaxSize() {
271         return mHttpCacheMaxSize;
272     }
273 
274     @VisibleForTesting
httpCacheMode()275     public int httpCacheMode() {
276         return mHttpCacheMode.getType();
277     }
278 
279     @HttpCacheSetting
publicBuilderHttpCacheMode()280     int publicBuilderHttpCacheMode() {
281         return mHttpCacheMode.toPublicBuilderCacheMode();
282     }
283 
284     @Override
addQuicHint(String host, int port, int alternatePort)285     public CronetEngineBuilderImpl addQuicHint(String host, int port, int alternatePort) {
286         if (host.contains("/")) {
287             throw new IllegalArgumentException("Illegal QUIC Hint Host: " + host);
288         }
289         mQuicHints.add(new QuicHint(host, port, alternatePort));
290         return this;
291     }
292 
quicHints()293     List<QuicHint> quicHints() {
294         return mQuicHints;
295     }
296 
297     @Override
addPublicKeyPins(String hostName, Set<byte[]> pinsSha256, boolean includeSubdomains, Instant expirationInstant)298     public CronetEngineBuilderImpl addPublicKeyPins(String hostName, Set<byte[]> pinsSha256,
299             boolean includeSubdomains, Instant expirationInstant) {
300         if (hostName == null) {
301             throw new NullPointerException("The hostname cannot be null");
302         }
303         if (pinsSha256 == null) {
304             throw new NullPointerException("The set of SHA256 pins cannot be null");
305         }
306         if (expirationInstant == null) {
307             throw new NullPointerException("The pin expiration date cannot be null");
308         }
309         String idnHostName = validateHostNameForPinningAndConvert(hostName);
310         // Convert the pin to BASE64 encoding to remove duplicates.
311         Map<String, byte[]> hashes = new HashMap<>();
312         for (byte[] pinSha256 : pinsSha256) {
313             if (pinSha256 == null || pinSha256.length != 32) {
314                 throw new IllegalArgumentException("Public key pin is invalid");
315             }
316             hashes.put(Base64.encodeToString(pinSha256, 0), pinSha256);
317         }
318         // Add new element to PKP list.
319         mPkps.add(new Pkp(idnHostName, hashes.values().toArray(new byte[hashes.size()][]),
320                 includeSubdomains, expirationInstant));
321         return this;
322     }
323 
324     /**
325      * Returns list of public key pins.
326      * @return list of public key pins.
327      */
publicKeyPins()328     List<Pkp> publicKeyPins() {
329         return mPkps;
330     }
331 
332     @Override
enablePublicKeyPinningBypassForLocalTrustAnchors(boolean value)333     public CronetEngineBuilderImpl enablePublicKeyPinningBypassForLocalTrustAnchors(boolean value) {
334         mPublicKeyPinningBypassForLocalTrustAnchorsEnabled = value;
335         return this;
336     }
337 
338     @VisibleForTesting
publicKeyPinningBypassForLocalTrustAnchorsEnabled()339     public boolean publicKeyPinningBypassForLocalTrustAnchorsEnabled() {
340         return mPublicKeyPinningBypassForLocalTrustAnchorsEnabled;
341     }
342 
343     /**
344      * Checks whether a given string represents a valid host name for PKP and converts it
345      * to ASCII Compatible Encoding representation according to RFC 1122, RFC 1123 and
346      * RFC 3490. This method is more restrictive than required by RFC 7469. Thus, a host
347      * that contains digits and the dot character only is considered invalid.
348      *
349      * Note: Currently Cronet doesn't have native implementation of host name validation that
350      *       can be used. There is code that parses a provided URL but doesn't ensure its
351      *       correctness. The implementation relies on {@code getaddrinfo} function.
352      *
353      * @param hostName host name to check and convert.
354      * @return true if the string is a valid host name.
355      * @throws IllegalArgumentException if the the given string does not represent a valid
356      *                                  hostname.
357      */
validateHostNameForPinningAndConvert(String hostName)358     private static String validateHostNameForPinningAndConvert(String hostName)
359             throws IllegalArgumentException {
360         if (INVALID_PKP_HOST_NAME.matcher(hostName).matches()) {
361             throw new IllegalArgumentException("Hostname " + hostName + " is illegal."
362                     + " A hostname should not consist of digits and/or dots only.");
363         }
364         // Workaround for crash, see crbug.com/634914
365         if (hostName.length() > 255) {
366             throw new IllegalArgumentException("Hostname " + hostName + " is too long."
367                     + " The name of the host does not comply with RFC 1122 and RFC 1123.");
368         }
369         try {
370             return IDN.toASCII(hostName, IDN.USE_STD3_ASCII_RULES);
371         } catch (IllegalArgumentException ex) {
372             throw new IllegalArgumentException("Hostname " + hostName + " is illegal."
373                     + " The name of the host does not comply with RFC 1122 and RFC 1123.");
374         }
375     }
376 
377     @Override
setExperimentalOptions(String options)378     public CronetEngineBuilderImpl setExperimentalOptions(String options) {
379         mExperimentalOptions = options;
380         return this;
381     }
382 
experimentalOptions()383     public String experimentalOptions() {
384         return mExperimentalOptions;
385     }
386 
387     /**
388      * Sets a native MockCertVerifier for testing. See
389      * {@code MockCertVerifier.createMockCertVerifier} for a method that
390      * can be used to create a MockCertVerifier.
391      * @param mockCertVerifier pointer to native MockCertVerifier.
392      * @return the builder to facilitate chaining.
393      */
394     @VisibleForTesting
setMockCertVerifierForTesting(long mockCertVerifier)395     public CronetEngineBuilderImpl setMockCertVerifierForTesting(long mockCertVerifier) {
396         mMockCertVerifier = mockCertVerifier;
397         return this;
398     }
399 
mockCertVerifier()400     long mockCertVerifier() {
401         return mMockCertVerifier;
402     }
403 
404     /**
405      * @return true if the network quality estimator has been enabled for
406      * this builder.
407      */
408     @VisibleForTesting
networkQualityEstimatorEnabled()409     public boolean networkQualityEstimatorEnabled() {
410         return mNetworkQualityEstimatorEnabled;
411     }
412 
413     @Override
enableNetworkQualityEstimator(boolean value)414     public CronetEngineBuilderImpl enableNetworkQualityEstimator(boolean value) {
415         mNetworkQualityEstimatorEnabled = value;
416         return this;
417     }
418 
419     @Override
setThreadPriority(int priority)420     public CronetEngineBuilderImpl setThreadPriority(int priority) {
421         if (priority > THREAD_PRIORITY_LOWEST || priority < -20) {
422             throw new IllegalArgumentException("Thread priority invalid");
423         }
424         mThreadPriority = priority;
425         return this;
426     }
427 
428     /**
429      * @return thread priority provided by user, or {@code defaultThreadPriority} if none provided.
430      */
431     @VisibleForTesting
threadPriority(int defaultThreadPriority)432     public int threadPriority(int defaultThreadPriority) {
433         return mThreadPriority == INVALID_THREAD_PRIORITY ? defaultThreadPriority : mThreadPriority;
434     }
435 
436     /**
437      * Returns {@link Context} for builder.
438      *
439      * @return {@link Context} for builder.
440      */
getContext()441     Context getContext() {
442         return mApplicationContext;
443     }
444 }
445