blob: 9d0994b1ab255899d814c11f6b80b043305bc079 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "components/cronet/ios/Cronet.h"
#include <memory>
#include <vector>
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/mac/bundle_locations.h"
#include "base/mac/scoped_block.h"
#include "base/strings/sys_string_conversions.h"
#include "base/synchronization/lock.h"
#include "components/cronet/cronet_global_state.h"
#include "components/cronet/ios/accept_languages_table.h"
#include "components/cronet/ios/cronet_environment.h"
#include "components/cronet/ios/cronet_metrics.h"
#include "components/cronet/native/url_request.h"
#include "components/cronet/url_request_context_config.h"
#include "ios/net/crn_http_protocol_handler.h"
#include "ios/net/empty_nsurlcache.h"
#include "net/base/url_util.h"
#include "net/cert/cert_verifier.h"
#include "net/url_request/url_request_context_getter.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
// Cronet NSError constants.
NSString* const CRNCronetErrorDomain = @"CRNCronetErrorDomain";
NSString* const CRNInvalidArgumentKey = @"CRNInvalidArgumentKey";
namespace {
class CronetHttpProtocolHandlerDelegate;
using QuicHintVector =
std::vector<std::unique_ptr<cronet::URLRequestContextConfig::QuicHint>>;
// Currently there is one and only one instance of CronetEnvironment,
// which is leaked at the shutdown. We should consider allowing multiple
// instances if that makes sense in the future.
base::LazyInstance<std::unique_ptr<cronet::CronetEnvironment>>::Leaky
gChromeNet = LAZY_INSTANCE_INITIALIZER;
base::LazyInstance<std::unique_ptr<CronetHttpProtocolHandlerDelegate>>::Leaky
gHttpProtocolHandlerDelegate = LAZY_INSTANCE_INITIALIZER;
base::LazyInstance<std::unique_ptr<cronet::CronetMetricsDelegate>>::Leaky
gMetricsDelegate = LAZY_INSTANCE_INITIALIZER;
// See [Cronet initialize] method to set the default values of the global
// variables.
BOOL gHttp2Enabled;
BOOL gQuicEnabled;
BOOL gBrotliEnabled;
BOOL gMetricsEnabled;
cronet::URLRequestContextConfig::HttpCacheType gHttpCache;
QuicHintVector gQuicHints;
NSString* gExperimentalOptions;
NSString* gUserAgent;
BOOL gUserAgentPartial;
double gNetworkThreadPriority;
NSString* gSslKeyLogFileName;
std::vector<std::unique_ptr<cronet::URLRequestContextConfig::Pkp>> gPkpList;
RequestFilterBlock gRequestFilterBlock;
NSURLCache* gPreservedSharedURLCache;
BOOL gEnableTestCertVerifierForTesting;
std::unique_ptr<net::CertVerifier> gMockCertVerifier;
NSString* gAcceptLanguages;
BOOL gEnablePKPBypassForLocalTrustAnchors;
dispatch_once_t gSwizzleOnceToken;
// CertVerifier, which allows any certificates for testing.
class TestCertVerifier : public net::CertVerifier {
int Verify(const RequestParams& params,
net::CertVerifyResult* verify_result,
net::CompletionOnceCallback callback,
std::unique_ptr<Request>* out_req,
const net::NetLogWithSource& net_log) override {
net::Error result = net::OK;
verify_result->verified_cert = params.certificate();
verify_result->cert_status = net::MapNetErrorToCertStatus(result);
return result;
}
void SetConfig(const Config& config) override {}
};
// net::HTTPProtocolHandlerDelegate for Cronet.
class CronetHttpProtocolHandlerDelegate
: public net::HTTPProtocolHandlerDelegate {
public:
CronetHttpProtocolHandlerDelegate(net::URLRequestContextGetter* getter,
RequestFilterBlock filter)
: getter_(getter), filter_(filter) {}
void SetRequestFilterBlock(RequestFilterBlock filter) {
base::AutoLock auto_lock(lock_);
filter_.reset(filter);
}
private:
// net::HTTPProtocolHandlerDelegate implementation:
bool CanHandleRequest(NSURLRequest* request) override {
base::AutoLock auto_lock(lock_);
if (!IsRequestSupported(request))
return false;
if (filter_) {
RequestFilterBlock block = filter_.get();
return block(request);
}
return true;
}
bool IsRequestSupported(NSURLRequest* request) override {
NSString* scheme = [[request URL] scheme];
if (!scheme)
return false;
return [scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@"https"] == NSOrderedSame;
}
net::URLRequestContextGetter* GetDefaultURLRequestContext() override {
return getter_.get();
}
scoped_refptr<net::URLRequestContextGetter> getter_;
base::mac::ScopedBlock<RequestFilterBlock> filter_;
base::Lock lock_;
};
} // namespace
@implementation Cronet
+ (void)configureCronetEnvironmentForTesting:
(cronet::CronetEnvironment*)cronetEnvironment {
if (gEnableTestCertVerifierForTesting) {
std::unique_ptr<TestCertVerifier> test_cert_verifier =
std::make_unique<TestCertVerifier>();
cronetEnvironment->set_mock_cert_verifier(std::move(test_cert_verifier));
}
if (gMockCertVerifier) {
gChromeNet.Get()->set_mock_cert_verifier(std::move(gMockCertVerifier));
}
}
+ (NSString*)getAcceptLanguagesFromPreferredLanguages:
(NSArray<NSString*>*)languages {
NSMutableArray* acceptLanguages = [NSMutableArray new];
for (NSString* lang_region in languages) {
NSString* lang = [lang_region componentsSeparatedByString:@"-"][0];
NSString* localeAcceptLangs = acceptLangs[lang_region] ?: acceptLangs[lang];
if (localeAcceptLangs)
[acceptLanguages
addObjectsFromArray:[localeAcceptLangs
componentsSeparatedByString:@","]];
}
NSString* acceptLanguageString =
[[[NSOrderedSet orderedSetWithArray:acceptLanguages] array]
componentsJoinedByString:@","];
return [acceptLanguageString length] != 0 ? acceptLanguageString
: @"en-US,en";
}
+ (NSString*)getAcceptLanguages {
return [self
getAcceptLanguagesFromPreferredLanguages:[NSLocale preferredLanguages]];
}
+ (void)setAcceptLanguages:(NSString*)acceptLanguages {
[self checkNotStarted];
gAcceptLanguages = acceptLanguages;
}
// TODO(lilyhoughton) this should either be removed, or made more sophisticated
+ (void)checkNotStarted {
CHECK(!gChromeNet.Get()) << "Cronet is already started.";
}
+ (void)setHttp2Enabled:(BOOL)http2Enabled {
[self checkNotStarted];
gHttp2Enabled = http2Enabled;
}
+ (void)setQuicEnabled:(BOOL)quicEnabled {
[self checkNotStarted];
gQuicEnabled = quicEnabled;
}
+ (void)setBrotliEnabled:(BOOL)brotliEnabled {
[self checkNotStarted];
gBrotliEnabled = brotliEnabled;
}
+ (void)setMetricsEnabled:(BOOL)metricsEnabled {
// https://crbug.com/878589
// Don't collect NSURLSessionTaskMetrics until iOS 10.2 to avoid crash in iOS.
if (@available(iOS 10.2, *)) {
[self checkNotStarted];
gMetricsEnabled = metricsEnabled;
}
}
+ (BOOL)addQuicHint:(NSString*)host port:(int)port altPort:(int)altPort {
[self checkNotStarted];
std::string quic_host = base::SysNSStringToUTF8(host);
url::CanonHostInfo host_info;
std::string canon_host(net::CanonicalizeHost(quic_host, &host_info));
if (!host_info.IsIPAddress() &&
!net::IsCanonicalizedHostCompliant(canon_host)) {
LOG(ERROR) << "Invalid QUIC hint host: " << quic_host;
return NO;
}
gQuicHints.push_back(
std::make_unique<cronet::URLRequestContextConfig::QuicHint>(
quic_host, port, altPort));
return YES;
}
+ (void)setExperimentalOptions:(NSString*)experimentalOptions {
[self checkNotStarted];
gExperimentalOptions = experimentalOptions;
}
+ (void)setUserAgent:(NSString*)userAgent partial:(BOOL)partial {
[self checkNotStarted];
gUserAgent = userAgent;
gUserAgentPartial = partial;
}
+ (void)setSslKeyLogFileName:(NSString*)sslKeyLogFileName {
[self checkNotStarted];
gSslKeyLogFileName = [self getNetLogPathForFile:sslKeyLogFileName];
}
+ (void)setHttpCacheType:(CRNHttpCacheType)httpCacheType {
[self checkNotStarted];
switch (httpCacheType) {
case CRNHttpCacheTypeDisabled:
gHttpCache = cronet::URLRequestContextConfig::HttpCacheType::DISABLED;
break;
case CRNHttpCacheTypeDisk:
gHttpCache = cronet::URLRequestContextConfig::HttpCacheType::DISK;
break;
case CRNHttpCacheTypeMemory:
gHttpCache = cronet::URLRequestContextConfig::HttpCacheType::MEMORY;
break;
default:
DCHECK(NO) << "Invalid HTTP cache type: " << httpCacheType;
}
}
+ (void)setRequestFilterBlock:(RequestFilterBlock)block {
if (gHttpProtocolHandlerDelegate.Get().get())
gHttpProtocolHandlerDelegate.Get().get()->SetRequestFilterBlock(block);
else
gRequestFilterBlock = block;
}
+ (BOOL)addPublicKeyPinsForHost:(NSString*)host
pinHashes:(NSSet<NSData*>*)pinHashes
includeSubdomains:(BOOL)includeSubdomains
expirationDate:(NSDate*)expirationDate
error:(NSError**)outError {
[self checkNotStarted];
// Pinning a key only makes sense if pin bypassing has been disabled
if (gEnablePKPBypassForLocalTrustAnchors) {
if (outError != nil) {
*outError =
[self createUnsupportedConfigurationError:
@"Cannot pin keys while public key pinning is bypassed"];
}
return NO;
}
auto pkp = std::make_unique<cronet::URLRequestContextConfig::Pkp>(
base::SysNSStringToUTF8(host), includeSubdomains,
base::Time::FromCFAbsoluteTime(
[expirationDate timeIntervalSinceReferenceDate]));
for (NSData* hash in pinHashes) {
net::SHA256HashValue hashValue = net::SHA256HashValue();
if (sizeof(hashValue.data) != hash.length) {
*outError =
[self createIllegalArgumentErrorWithArgument:@"pinHashes"
reason:
@"The length of PKP SHA256 "
@"hash should be 256 bits"];
return NO;
}
memcpy((void*)(hashValue.data), [hash bytes], sizeof(hashValue.data));
pkp->pin_hashes.push_back(net::HashValue(hashValue));
}
gPkpList.push_back(std::move(pkp));
if (outError) {
*outError = nil;
}
return YES;
}
+ (void)setEnablePublicKeyPinningBypassForLocalTrustAnchors:(BOOL)enable {
gEnablePKPBypassForLocalTrustAnchors = enable;
}
+ (base::SingleThreadTaskRunner*)getFileThreadRunnerForTesting {
return gChromeNet.Get()->GetFileThreadRunnerForTesting();
}
+ (base::SingleThreadTaskRunner*)getNetworkThreadRunnerForTesting {
return gChromeNet.Get()->GetNetworkThreadRunnerForTesting();
}
+ (void)startInternal {
std::string user_agent = base::SysNSStringToUTF8(gUserAgent);
gChromeNet.Get().reset(
new cronet::CronetEnvironment(user_agent, gUserAgentPartial));
gChromeNet.Get()->set_accept_language(
base::SysNSStringToUTF8(gAcceptLanguages ?: [self getAcceptLanguages]));
gChromeNet.Get()->set_http2_enabled(gHttp2Enabled);
gChromeNet.Get()->set_quic_enabled(gQuicEnabled);
gChromeNet.Get()->set_brotli_enabled(gBrotliEnabled);
gChromeNet.Get()->set_experimental_options(
base::SysNSStringToUTF8(gExperimentalOptions));
gChromeNet.Get()->set_http_cache(gHttpCache);
gChromeNet.Get()->set_ssl_key_log_file_name(
base::SysNSStringToUTF8(gSslKeyLogFileName));
gChromeNet.Get()->set_pkp_list(std::move(gPkpList));
gChromeNet.Get()
->set_enable_public_key_pinning_bypass_for_local_trust_anchors(
gEnablePKPBypassForLocalTrustAnchors);
if (gNetworkThreadPriority !=
cronet::CronetEnvironment::kKeepDefaultThreadPriority) {
gChromeNet.Get()->SetNetworkThreadPriority(gNetworkThreadPriority);
}
for (const auto& quicHint : gQuicHints) {
gChromeNet.Get()->AddQuicHint(quicHint->host, quicHint->port,
quicHint->alternate_port);
}
[self configureCronetEnvironmentForTesting:gChromeNet.Get().get()];
gChromeNet.Get()->Start();
gHttpProtocolHandlerDelegate.Get().reset(
new CronetHttpProtocolHandlerDelegate(
gChromeNet.Get()->GetURLRequestContextGetter(), gRequestFilterBlock));
net::HTTPProtocolHandlerDelegate::SetInstance(
gHttpProtocolHandlerDelegate.Get().get());
if (gMetricsEnabled) {
gMetricsDelegate.Get().reset(new cronet::CronetMetricsDelegate());
net::MetricsDelegate::SetInstance(gMetricsDelegate.Get().get());
dispatch_once(&gSwizzleOnceToken, ^{
cronet::SwizzleSessionWithConfiguration();
});
} else {
net::MetricsDelegate::SetInstance(nullptr);
}
gRequestFilterBlock = nil;
}
+ (void)start {
cronet::EnsureInitialized();
[self startInternal];
}
+ (void)unswizzleForTesting {
if (gSwizzleOnceToken)
cronet::SwizzleSessionWithConfiguration();
gSwizzleOnceToken = 0;
}
+ (void)shutdownForTesting {
[Cronet unswizzleForTesting];
[Cronet initialize];
}
+ (void)registerHttpProtocolHandler {
if (gPreservedSharedURLCache == nil) {
gPreservedSharedURLCache = [NSURLCache sharedURLCache];
}
// Disable the default cache.
[NSURLCache setSharedURLCache:[EmptyNSURLCache emptyNSURLCache]];
// Register the chrome http protocol handler to replace the default one.
BOOL success =
[NSURLProtocol registerClass:[CRNHTTPProtocolHandler class]];
DCHECK(success);
}
+ (void)unregisterHttpProtocolHandler {
// Set up SharedURLCache preserved in registerHttpProtocolHandler.
if (gPreservedSharedURLCache != nil) {
[NSURLCache setSharedURLCache:gPreservedSharedURLCache];
gPreservedSharedURLCache = nil;
}
[NSURLProtocol unregisterClass:[CRNHTTPProtocolHandler class]];
}
+ (void)installIntoSessionConfiguration:(NSURLSessionConfiguration*)config {
config.protocolClasses = @[ [CRNHTTPProtocolHandler class] ];
}
+ (NSString*)getNetLogPathForFile:(NSString*)fileName {
return [[[[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask]
lastObject] URLByAppendingPathComponent:fileName] path];
}
+ (BOOL)startNetLogToFile:(NSString*)fileName logBytes:(BOOL)logBytes {
if (gChromeNet.Get().get() && [fileName length] &&
![fileName isAbsolutePath]) {
return gChromeNet.Get()->StartNetLog(
base::SysNSStringToUTF8([self getNetLogPathForFile:fileName]),
logBytes);
}
return NO;
}
+ (void)stopNetLog {
if (gChromeNet.Get().get()) {
gChromeNet.Get()->StopNetLog();
}
}
+ (NSString*)getUserAgent {
if (!gChromeNet.Get().get()) {
return nil;
}
return [NSString stringWithCString:gChromeNet.Get()->user_agent().c_str()
encoding:[NSString defaultCStringEncoding]];
}
+ (void)setNetworkThreadPriority:(double)priority {
gNetworkThreadPriority = priority;
if (gChromeNet.Get()) {
gChromeNet.Get()->SetNetworkThreadPriority(priority);
};
}
+ (stream_engine*)getGlobalEngine {
DCHECK(gChromeNet.Get().get());
if (gChromeNet.Get().get()) {
static stream_engine engine;
engine.obj = gChromeNet.Get()->GetURLRequestContextGetter();
return &engine;
}
return nil;
}
+ (NSData*)getGlobalMetricsDeltas {
if (!gChromeNet.Get().get()) {
return nil;
}
std::vector<uint8_t> deltas(gChromeNet.Get()->GetHistogramDeltas());
return [NSData dataWithBytes:deltas.data() length:deltas.size()];
}
+ (void)enableTestCertVerifierForTesting {
gEnableTestCertVerifierForTesting = YES;
}
+ (void)setMockCertVerifierForTesting:
(std::unique_ptr<net::CertVerifier>)certVerifier {
gMockCertVerifier = std::move(certVerifier);
}
+ (void)setHostResolverRulesForTesting:(NSString*)hostResolverRulesForTesting {
DCHECK(gChromeNet.Get().get());
gChromeNet.Get()->SetHostResolverRules(
base::SysNSStringToUTF8(hostResolverRulesForTesting));
}
// This is a private dummy method that prevents the linker from stripping out
// the otherwise unreferenced methods from 'bidirectional_stream.cc'.
+ (void)preventStrippingCronetBidirectionalStream {
bidirectional_stream_create(NULL, 0, 0);
}
// This is a private dummy method that prevents the linker from stripping out
// the otherwise unreferenced modules from 'native'.
+ (void)preventStrippingNativeCronetModules {
Cronet_Buffer_Create();
Cronet_Engine_Create();
Cronet_UrlRequest_Create();
}
+ (NSError*)createIllegalArgumentErrorWithArgument:(NSString*)argumentName
reason:(NSString*)reason {
NSMutableDictionary* errorDictionary =
[[NSMutableDictionary alloc] initWithDictionary:@{
NSLocalizedDescriptionKey :
[NSString stringWithFormat:@"Invalid argument: %@", argumentName],
CRNInvalidArgumentKey : argumentName
}];
if (reason) {
errorDictionary[NSLocalizedFailureReasonErrorKey] = reason;
}
return [self createCronetErrorWithCode:CRNErrorInvalidArgument
userInfo:errorDictionary];
}
+ (NSError*)createUnsupportedConfigurationError:(NSString*)contradiction {
NSMutableDictionary* errorDictionary =
[[NSMutableDictionary alloc] initWithDictionary:@{
NSLocalizedDescriptionKey : @"Unsupported configuration",
NSLocalizedRecoverySuggestionErrorKey :
@"Try disabling Public Key Pinning Bypass before pinning keys.",
NSLocalizedFailureReasonErrorKey : @"Pinning public keys while local "
@"anchor bypass is enabled is "
@"currently not supported.",
}];
if (contradiction) {
errorDictionary[NSLocalizedFailureReasonErrorKey] = contradiction;
}
return [self createCronetErrorWithCode:CRNErrorUnsupportedConfig
userInfo:errorDictionary];
}
+ (NSError*)createCronetErrorWithCode:(int)errorCode
userInfo:(NSDictionary*)userInfo {
return [NSError errorWithDomain:CRNCronetErrorDomain
code:errorCode
userInfo:userInfo];
}
// Used by tests to query the size of the map that contains metrics for
// individual NSURLSession tasks.
+ (size_t)getMetricsMapSize {
return cronet::CronetMetricsDelegate::GetMetricsMapSize();
}
// Static class initializer.
+ (void)initialize {
gChromeNet.Get().reset();
gHttp2Enabled = YES;
gQuicEnabled = NO;
gBrotliEnabled = NO;
gMetricsEnabled = NO;
gHttpCache = cronet::URLRequestContextConfig::HttpCacheType::DISK;
gQuicHints.clear();
gExperimentalOptions = @"{}";
gUserAgent = nil;
gUserAgentPartial = NO;
gNetworkThreadPriority =
cronet::CronetEnvironment::kKeepDefaultThreadPriority;
gSslKeyLogFileName = nil;
gPkpList.clear();
gRequestFilterBlock = nil;
gHttpProtocolHandlerDelegate.Get().reset(nullptr);
gMetricsDelegate.Get().reset(nullptr);
gPreservedSharedURLCache = nil;
gEnableTestCertVerifierForTesting = NO;
gMockCertVerifier.reset(nullptr);
gAcceptLanguages = nil;
gEnablePKPBypassForLocalTrustAnchors = YES;
}
@end