blob: 35d677e126017fddec7904ff2a2f1426ea2ca3b0 [file] [log] [blame]
// Copyright 2015 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 <Cronet/Cronet.h>
#import <Foundation/Foundation.h>
#include <stdint.h>
#include "base/logging.h"
#include "base/mac/scoped_nsobject.h"
#include "base/strings/sys_string_conversions.h"
#include "components/cronet/ios/test/start_cronet.h"
#include "components/cronet/ios/test/test_server.h"
#include "components/grpc_support/test/quic_test_server.h"
#include "net/base/mac/url_conversions.h"
#include "net/base/net_errors.h"
#include "net/cert/mock_cert_verifier.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "url/gurl.h"
@interface Cronet (ExposedForTesting)
+ (void)shutdownForTesting;
@end
@interface TestDelegate : NSObject<NSURLSessionDataDelegate,
NSURLSessionDelegate,
NSURLSessionTaskDelegate>
// Completion semaphore for this TestDelegate. When the request this delegate is
// attached to finishes (either successfully or with an error), this delegate
// signals this semaphore.
@property(assign, atomic) dispatch_semaphore_t semaphore;
// Error the request this delegate is attached to failed with, if any.
@property(retain, atomic) NSError* error;
// Contains total amount of received data.
@property(readonly) long totalBytesReceived;
@end
@implementation TestDelegate
@synthesize semaphore = _semaphore;
@synthesize error = _error;
@synthesize totalBytesReceived = _totalBytesReceived;
NSMutableArray<NSData*>* _responseData;
- (id)init {
if (self = [super init]) {
_semaphore = dispatch_semaphore_create(0);
}
return self;
}
- (void)dealloc {
dispatch_release(_semaphore);
[_error release];
_error = nil;
[super dealloc];
}
- (void)reset {
[_responseData dealloc];
_responseData = nil;
_error = nil;
_totalBytesReceived = 0;
}
- (NSString*)responseBody {
if (_responseData == nil) {
return nil;
}
NSMutableString* body = [NSMutableString string];
for (NSData* data in _responseData) {
[body appendString:[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding]];
}
VLOG(3) << "responseBody size:" << [body length]
<< " chunks:" << [_responseData count];
return body;
}
- (void)URLSession:(NSURLSession*)session
didBecomeInvalidWithError:(NSError*)error {
}
- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didCompleteWithError:(NSError*)error {
[self setError:error];
dispatch_semaphore_signal(_semaphore);
}
- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didReceiveChallenge:(NSURLAuthenticationChallenge*)challenge
completionHandler:
(void (^)(NSURLSessionAuthChallengeDisposition disp,
NSURLCredential* credential))completionHandler {
completionHandler(NSURLSessionAuthChallengeUseCredential, nil);
}
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveResponse:(NSURLResponse*)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))
completionHandler {
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveData:(NSData*)data {
_totalBytesReceived += [data length];
if (_responseData == nil) {
_responseData = [[NSMutableArray alloc] init];
}
[_responseData addObject:data];
}
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
willCacheResponse:(NSCachedURLResponse*)proposedResponse
completionHandler:
(void (^)(NSCachedURLResponse* cachedResponse))completionHandler {
completionHandler(proposedResponse);
}
@end
namespace cronet {
// base::TimeDelta would normally be ideal for this but it does not support
// nanosecond resolution.
static const int64_t ns_in_second = 1000000000LL;
const char kUserAgent[] = "CronetTest/1.0.0.0";
class HttpTest : public ::testing::Test {
protected:
HttpTest() {}
~HttpTest() override {}
void SetUp() override {
grpc_support::StartQuicTestServer();
TestServer::Start();
[Cronet setRequestFilterBlock:^(NSURLRequest* request) {
return YES;
}];
StartCronet(grpc_support::GetQuicTestServerPort());
[Cronet registerHttpProtocolHandler];
NSURLSessionConfiguration* config =
[NSURLSessionConfiguration ephemeralSessionConfiguration];
[Cronet installIntoSessionConfiguration:config];
delegate_.reset([[TestDelegate alloc] init]);
NSURLSession* session = [NSURLSession sessionWithConfiguration:config
delegate:delegate_
delegateQueue:nil];
// Take a reference to the session and store it so it doesn't get
// deallocated until this object does.
session_.reset([session retain]);
}
void TearDown() override {
grpc_support::ShutdownQuicTestServer();
TestServer::Shutdown();
[Cronet stopNetLog];
[Cronet shutdownForTesting];
}
// Launches the supplied |task| and blocks until it completes, with a timeout
// of 1 second.
void StartDataTaskAndWaitForCompletion(NSURLSessionDataTask* task) {
[delegate_ reset];
[task resume];
int64_t deadline_ns = 20 * ns_in_second;
ASSERT_EQ(0, dispatch_semaphore_wait(
[delegate_ semaphore],
dispatch_time(DISPATCH_TIME_NOW, deadline_ns)));
}
base::scoped_nsobject<NSURLSession> session_;
base::scoped_nsobject<TestDelegate> delegate_;
};
TEST_F(HttpTest, CreateSslKeyLogFile) {
// Shutdown Cronet so that it can be restarted with specific configuration
// (SSL key log file specified in experimental options) for this one test.
// This is necessary because SslKeyLogFile can only be set once, before any
// SSL Client Sockets are created.
[Cronet shutdownForTesting];
NSString* ssl_key_log_file = [Cronet getNetLogPathForFile:@"SSLKEYLOGFILE"];
// Ensure that the keylog file doesn't exist.
[[NSFileManager defaultManager] removeItemAtPath:ssl_key_log_file error:nil];
[Cronet setExperimentalOptions:
[NSString stringWithFormat:@"{\"ssl_key_log_file\":\"%@\"}",
ssl_key_log_file]];
StartCronet(grpc_support::GetQuicTestServerPort());
bool ssl_file_created =
[[NSFileManager defaultManager] fileExistsAtPath:ssl_key_log_file];
[[NSFileManager defaultManager] removeItemAtPath:ssl_key_log_file error:nil];
[Cronet shutdownForTesting];
[Cronet setExperimentalOptions:@""];
EXPECT_TRUE(ssl_file_created);
}
TEST_F(HttpTest, NSURLSessionReceivesData) {
NSURL* url = net::NSURLWithGURL(GURL(grpc_support::kTestServerSimpleUrl));
__block BOOL block_used = NO;
NSURLSessionDataTask* task = [session_ dataTaskWithURL:url];
[Cronet setRequestFilterBlock:^(NSURLRequest* request) {
block_used = YES;
EXPECT_EQ([request URL], url);
return YES;
}];
StartDataTaskAndWaitForCompletion(task);
EXPECT_TRUE(block_used);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_STREQ(grpc_support::kSimpleBodyValue,
base::SysNSStringToUTF8([delegate_ responseBody]).c_str());
}
TEST_F(HttpTest, NSURLSessionReceivesBigHttpDataLoop) {
int iterations = 50;
long size = 10 * 1024 * 1024;
LOG(INFO) << "Downloading " << size << " bytes " << iterations << " times.";
NSTimeInterval elapsed_avg = 0;
NSTimeInterval elapsed_max = 0;
NSURL* url = net::NSURLWithGURL(GURL(TestServer::PrepareBigDataURL(size)));
for (int i = 0; i < iterations; ++i) {
[delegate_ reset];
__block BOOL block_used = NO;
NSURLSessionDataTask* task = [session_ dataTaskWithURL:url];
[Cronet setRequestFilterBlock:^(NSURLRequest* request) {
block_used = YES;
EXPECT_EQ([request URL], url);
return YES;
}];
NSDate* start = [NSDate date];
StartDataTaskAndWaitForCompletion(task);
NSTimeInterval elapsed = -[start timeIntervalSinceNow];
elapsed_avg += elapsed;
if (elapsed > elapsed_max)
elapsed_max = elapsed;
EXPECT_TRUE(block_used);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_EQ(size, [delegate_ totalBytesReceived]);
}
// Release the response buffer.
TestServer::ReleaseBigDataURL();
LOG(INFO) << "Elapsed Average:" << elapsed_avg * 1000 / iterations
<< "ms Max:" << elapsed_max * 1000 << "ms";
}
TEST_F(HttpTest, GetGlobalMetricsDeltas) {
NSData* delta1 = [Cronet getGlobalMetricsDeltas];
NSURL* url = net::NSURLWithGURL(GURL(grpc_support::kTestServerSimpleUrl));
NSURLSessionDataTask* task = [session_ dataTaskWithURL:url];
StartDataTaskAndWaitForCompletion(task);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_STREQ(grpc_support::kSimpleBodyValue,
base::SysNSStringToUTF8([delegate_ responseBody]).c_str());
NSData* delta2 = [Cronet getGlobalMetricsDeltas];
EXPECT_FALSE([delta2 isEqualToData:delta1]);
}
TEST_F(HttpTest, SdchDisabledByDefault) {
NSURL* url =
net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("Accept-Encoding")));
NSURLSessionDataTask* task = [session_ dataTaskWithURL:url];
StartDataTaskAndWaitForCompletion(task);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_FALSE([[delegate_ responseBody] containsString:@"sdch"]);
}
// Verify that explictly setting Accept-Encoding request header to 'gzip,sdch"
// is passed to the server and does not trigger any failures. This behavior may
// In the future Cronet may not allow caller to set Accept-Encoding header and
// could limit it to set of internally suported and enabled encodings, matching
// behavior of Cronet on Android.
TEST_F(HttpTest, AcceptEncodingSdchIsAllowed) {
NSURL* url =
net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("Accept-Encoding")));
NSMutableURLRequest* mutableRequest =
[[NSURLRequest requestWithURL:url] mutableCopy];
[mutableRequest addValue:@"gzip,sdch" forHTTPHeaderField:@"Accept-Encoding"];
NSURLSessionDataTask* task = [session_ dataTaskWithRequest:mutableRequest];
StartDataTaskAndWaitForCompletion(task);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_TRUE([[delegate_ responseBody] containsString:@"gzip,sdch"]);
}
// Verify that explictly setting Accept-Encoding request header to 'foo,bar"
// is passed to the server and does not trigger any failures. This behavior may
// In the future Cronet may not allow caller to set Accept-Encoding header and
// could limit it to set of internally suported and enabled encodings, matching
// behavior of Cronet on Android.
TEST_F(HttpTest, AcceptEncodingFooBarIsAllowed) {
NSURL* url =
net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("Accept-Encoding")));
NSMutableURLRequest* mutableRequest =
[[NSURLRequest requestWithURL:url] mutableCopy];
[mutableRequest addValue:@"foo,bar" forHTTPHeaderField:@"Accept-Encoding"];
NSURLSessionDataTask* task = [session_ dataTaskWithRequest:mutableRequest];
StartDataTaskAndWaitForCompletion(task);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_TRUE([[delegate_ responseBody] containsString:@"foo,bar"]);
}
TEST_F(HttpTest, NSURLSessionAcceptLanguage) {
NSURL* url =
net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("Accept-Language")));
NSURLSessionDataTask* task = [session_ dataTaskWithURL:url];
StartDataTaskAndWaitForCompletion(task);
EXPECT_EQ(nil, [delegate_ error]);
ASSERT_STREQ("en-US,en", [[delegate_ responseBody] UTF8String]);
}
TEST_F(HttpTest, SetUserAgentIsExact) {
NSURL* url =
net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("User-Agent")));
[Cronet setRequestFilterBlock:nil];
NSURLSessionDataTask* task = [session_ dataTaskWithURL:url];
StartDataTaskAndWaitForCompletion(task);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_STREQ(kUserAgent, [[delegate_ responseBody] UTF8String]);
}
TEST_F(HttpTest, SetCookie) {
const char kCookieHeader[] = "Cookie";
NSString* cookieName =
[NSString stringWithFormat:@"SetCookie-%@", [[NSUUID UUID] UUIDString]];
NSString* cookieValue = [[NSUUID UUID] UUIDString];
NSString* cookieLine =
[NSString stringWithFormat:@"%@=%@", cookieName, cookieValue];
NSHTTPCookieStorage* systemCookieStorage =
[NSHTTPCookieStorage sharedHTTPCookieStorage];
NSURL* cookieUrl =
net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL(kCookieHeader)));
// Verify that cookie is not set in system storage.
for (NSHTTPCookie* cookie in [systemCookieStorage cookiesForURL:cookieUrl]) {
EXPECT_FALSE([[cookie name] isEqualToString:cookieName]);
}
StartDataTaskAndWaitForCompletion([session_ dataTaskWithURL:cookieUrl]);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_EQ(nil, [delegate_ responseBody]);
NSURL* setCookieUrl = net::NSURLWithGURL(
GURL(TestServer::GetSetCookieURL(base::SysNSStringToUTF8(cookieLine))));
StartDataTaskAndWaitForCompletion([session_ dataTaskWithURL:setCookieUrl]);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_TRUE([[delegate_ responseBody] containsString:cookieLine]);
StartDataTaskAndWaitForCompletion([session_ dataTaskWithURL:cookieUrl]);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_TRUE([[delegate_ responseBody] containsString:cookieLine]);
// Verify that cookie is set in system storage.
NSHTTPCookie* systemCookie = nil;
for (NSHTTPCookie* cookie in [systemCookieStorage cookiesForURL:cookieUrl]) {
if ([cookie.name isEqualToString:cookieName]) {
systemCookie = cookie;
break;
}
}
EXPECT_TRUE([[systemCookie value] isEqualToString:cookieValue]);
[systemCookieStorage deleteCookie:systemCookie];
}
TEST_F(HttpTest, SetSystemCookie) {
const char kCookieHeader[] = "Cookie";
NSString* cookieName = [NSString
stringWithFormat:@"SetSystemCookie-%@", [[NSUUID UUID] UUIDString]];
NSString* cookieValue = [[NSUUID UUID] UUIDString];
NSHTTPCookieStorage* systemCookieStorage =
[NSHTTPCookieStorage sharedHTTPCookieStorage];
NSURL* echoCookieUrl =
net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL(kCookieHeader)));
NSHTTPCookie* systemCookie = [NSHTTPCookie cookieWithProperties:@{
NSHTTPCookiePath : [echoCookieUrl path],
NSHTTPCookieName : cookieName,
NSHTTPCookieValue : cookieValue,
NSHTTPCookieDomain : [echoCookieUrl host],
}];
[systemCookieStorage setCookie:systemCookie];
StartDataTaskAndWaitForCompletion([session_ dataTaskWithURL:echoCookieUrl]);
[systemCookieStorage deleteCookie:systemCookie];
EXPECT_EQ(nil, [delegate_ error]);
// Verify that cookie set in system store was sent to the serever.
EXPECT_TRUE([[delegate_ responseBody] containsString:cookieName]);
EXPECT_TRUE([[delegate_ responseBody] containsString:cookieValue]);
}
TEST_F(HttpTest, SystemCookieWithNullCreationTime) {
const char kCookieHeader[] = "Cookie";
NSString* cookieName = [NSString
stringWithFormat:@"SetSystemCookie-%@", [[NSUUID UUID] UUIDString]];
NSString* cookieValue = [[NSUUID UUID] UUIDString];
NSHTTPCookieStorage* systemCookieStorage =
[NSHTTPCookieStorage sharedHTTPCookieStorage];
NSURL* echoCookieUrl =
net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL(kCookieHeader)));
NSHTTPCookie* nullCreationTimeCookie = [NSHTTPCookie cookieWithProperties:@{
NSHTTPCookiePath : [echoCookieUrl path],
NSHTTPCookieName : cookieName,
NSHTTPCookieValue : cookieValue,
NSHTTPCookieDomain : [echoCookieUrl host],
@"Created" : [NSNumber numberWithDouble:0.0],
}];
[systemCookieStorage setCookie:nullCreationTimeCookie];
NSHTTPCookie* normalCookie = [NSHTTPCookie cookieWithProperties:@{
NSHTTPCookiePath : [echoCookieUrl path],
NSHTTPCookieName : [cookieName stringByAppendingString:@"-normal"],
NSHTTPCookieValue : cookieValue,
NSHTTPCookieDomain : [echoCookieUrl host],
}];
[systemCookieStorage setCookie:normalCookie];
StartDataTaskAndWaitForCompletion([session_ dataTaskWithURL:echoCookieUrl]);
[systemCookieStorage deleteCookie:nullCreationTimeCookie];
[systemCookieStorage deleteCookie:normalCookie];
EXPECT_EQ(nil, [delegate_ error]);
// Verify that cookie set in system store was sent to the serever.
EXPECT_TRUE([[delegate_ responseBody] containsString:cookieName]);
EXPECT_TRUE([[delegate_ responseBody] containsString:cookieValue]);
}
TEST_F(HttpTest, FilterOutRequest) {
NSURL* url =
net::NSURLWithGURL(GURL(TestServer::GetEchoHeaderURL("User-Agent")));
__block BOOL block_used = NO;
NSURLSessionDataTask* task = [session_ dataTaskWithURL:url];
[Cronet setRequestFilterBlock:^(NSURLRequest* request) {
block_used = YES;
EXPECT_EQ([request URL], url);
return NO;
}];
StartDataTaskAndWaitForCompletion(task);
EXPECT_TRUE(block_used);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_FALSE([[delegate_ responseBody]
containsString:base::SysUTF8ToNSString(kUserAgent)]);
EXPECT_TRUE([[delegate_ responseBody] containsString:@"CFNetwork"]);
}
TEST_F(HttpTest, FileSchemeNotSupported) {
NSString* fileData = @"Hello, World!";
NSString* documentsDirectory = [NSSearchPathForDirectoriesInDomains(
NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString* filePath = [documentsDirectory
stringByAppendingPathComponent:[[NSProcessInfo processInfo]
globallyUniqueString]];
[fileData writeToFile:filePath
atomically:YES
encoding:NSUTF8StringEncoding
error:nil];
NSURL* url = [NSURL fileURLWithPath:filePath];
NSURLSessionDataTask* task = [session_ dataTaskWithURL:url];
[Cronet setRequestFilterBlock:^(NSURLRequest* request) {
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
EXPECT_TRUE(false) << "Block should not be called for unsupported requests";
return YES;
}];
StartDataTaskAndWaitForCompletion(task);
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_TRUE([[delegate_ responseBody] containsString:fileData]);
}
TEST_F(HttpTest, DataSchemeNotSupported) {
NSString* testString = @"Hello, World!";
NSData* testData = [testString dataUsingEncoding:NSUTF8StringEncoding];
NSString* dataString =
[NSString stringWithFormat:@"data:text/plain;base64,%@",
[testData base64EncodedStringWithOptions:0]];
NSURL* url = [NSURL URLWithString:dataString];
NSURLSessionDataTask* task = [session_ dataTaskWithURL:url];
[Cronet setRequestFilterBlock:^(NSURLRequest* request) {
EXPECT_TRUE(false) << "Block should not be called for unsupported requests";
return YES;
}];
StartDataTaskAndWaitForCompletion(task);
EXPECT_EQ(nil, [delegate_ error]);
EXPECT_TRUE([[delegate_ responseBody] containsString:testString]);
}
} // namespace cronet