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