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