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