blob: 712567a06757938607499a78d8d874bc80df5bf1 [file] [log] [blame]
// Copyright 2017 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.
#include "cronet_test_base.h"
#include "base/location.h"
#include "base/threading/thread.h"
#include "components/grpc_support/test/quic_test_server.h"
#include "crypto/sha2.h"
#include "net/base/net_errors.h"
#include "net/cert/asn1_util.h"
#include "net/cert/mock_cert_verifier.h"
#include "net/cert/x509_util.h"
#include "net/test/cert_test_util.h"
#include "net/test/test_data_directory.h"
#pragma mark
@implementation TestDelegate {
// Dictionary which maps tasks to completion semaphores for this TestDelegate.
// When a request this delegate is attached to finishes (either successfully
// or with an error), this delegate signals that task's semaphore.
NSMutableDictionary<NSURLSessionTask*, dispatch_semaphore_t>* _semaphores;
NSMutableDictionary<NSURLSessionDataTask*, NSMutableArray<NSData*>*>*
_responseDataPerTask;
}
@synthesize errorPerTask = _errorPerTask;
@synthesize totalBytesReceivedPerTask = _totalBytesReceivedPerTask;
@synthesize expectedContentLengthPerTask = _expectedContentLengthPerTask;
@synthesize taskMetrics = _taskMetrics;
@synthesize responsePerTask = _responsePerTask;
- (id)init {
if (self = [super init]) {
_semaphores = [NSMutableDictionary dictionaryWithCapacity:0];
}
return self;
}
- (void)reset {
_responseDataPerTask = [NSMutableDictionary dictionaryWithCapacity:0];
_errorPerTask = [NSMutableDictionary dictionaryWithCapacity:0];
_totalBytesReceivedPerTask = [NSMutableDictionary dictionaryWithCapacity:0];
_expectedContentLengthPerTask =
[NSMutableDictionary dictionaryWithCapacity:0];
_responsePerTask = [NSMutableDictionary dictionaryWithCapacity:0];
_taskMetrics = nil;
}
- (NSError*)error {
if ([_errorPerTask count] == 0)
return nil;
DCHECK([_errorPerTask count] == 1);
return [[_errorPerTask objectEnumerator] nextObject];
}
- (long)totalBytesReceived {
DCHECK([_totalBytesReceivedPerTask count] == 1);
return [[[_totalBytesReceivedPerTask objectEnumerator] nextObject] intValue];
}
- (long)expectedContentLength {
DCHECK([_expectedContentLengthPerTask count] == 1);
return
[[[_expectedContentLengthPerTask objectEnumerator] nextObject] intValue];
}
- (NSString*)responseBody {
if ([_responseDataPerTask count] == 0)
return nil;
DCHECK([_responseDataPerTask count] == 1);
NSURLSessionDataTask* task =
[[_responseDataPerTask keyEnumerator] nextObject];
return [self responseBody:task];
}
- (NSString*)responseBody:(NSURLSessionDataTask*)task {
if (_responseDataPerTask[task] == nil) {
return nil;
}
NSMutableString* body = [NSMutableString string];
for (NSData* data in _responseDataPerTask[task]) {
[body appendString:[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding]];
}
VLOG(3) << "responseBody size:" << [body length]
<< " chunks:" << [_responseDataPerTask[task] count];
return body;
}
- (dispatch_semaphore_t)getSemaphoreForTask:(NSURLSessionTask*)task {
@synchronized(_semaphores) {
if (!_semaphores[task]) {
_semaphores[task] = dispatch_semaphore_create(0);
}
return _semaphores[task];
}
}
// |timeout_ns|, if positive, specifies how long to wait before timing out in
// nanoseconds, a value of 0 or less means do not ever time out.
- (BOOL)waitForDone:(NSURLSessionDataTask*)task
withTimeout:(int64_t)timeout_ns {
dispatch_semaphore_t _semaphore = [self getSemaphoreForTask:task];
if (timeout_ns > 0) {
BOOL request_completed =
dispatch_semaphore_wait(
_semaphore, dispatch_time(DISPATCH_TIME_NOW, timeout_ns)) == 0;
if (!request_completed) {
// Cancel the pending request; otherwise, the request is still active and
// may invoke the delegate methods later.
[task cancel];
LOG(WARNING) << "The request was canceled due to timeout.";
// Give the canceled request some time to execute didCompleteWithError
// method with NSURLErrorCancelled error code.
dispatch_semaphore_wait(
_semaphore, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC));
}
return request_completed;
}
return dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER) == 0;
}
- (void)URLSession:(NSURLSession*)session
didBecomeInvalidWithError:(NSError*)error {
}
- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didCompleteWithError:(NSError*)error {
if (error)
_errorPerTask[task] = error;
dispatch_semaphore_t _semaphore = [self getSemaphoreForTask:task];
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
task:(NSURLSessionTask*)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics*)metrics
NS_AVAILABLE_IOS(10.0) {
_taskMetrics = metrics;
}
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveResponse:(NSURLResponse*)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))
completionHandler {
_expectedContentLengthPerTask[dataTask] =
[NSNumber numberWithInt:[response expectedContentLength]];
_responsePerTask[dataTask] = static_cast<NSHTTPURLResponse*>(response);
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveData:(NSData*)data {
if (_totalBytesReceivedPerTask[dataTask]) {
_totalBytesReceivedPerTask[dataTask] = [NSNumber
numberWithInt:[_totalBytesReceivedPerTask[dataTask] intValue] +
[data length]];
} else {
_totalBytesReceivedPerTask[dataTask] =
[NSNumber numberWithInt:[data length]];
}
if (_responseDataPerTask[dataTask] == nil) {
_responseDataPerTask[dataTask] = [[NSMutableArray alloc] init];
}
[_responseDataPerTask[dataTask] addObject:data];
}
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
willCacheResponse:(NSCachedURLResponse*)proposedResponse
completionHandler:
(void (^)(NSCachedURLResponse* cachedResponse))completionHandler {
completionHandler(proposedResponse);
}
@end
namespace cronet {
void CronetTestBase::SetUp() {
::testing::Test::SetUp();
grpc_support::StartQuicTestServer();
delegate_ = [[TestDelegate alloc] init];
}
void CronetTestBase::TearDown() {
grpc_support::ShutdownQuicTestServer();
::testing::Test::TearDown();
}
// Launches the supplied |task| and blocks until it completes, with a default
// timeout of 20 seconds. |deadline_ns|, if specified, is in nanoseconds.
// If |deadline_ns| is 0 or negative, the request will not time out.
bool CronetTestBase::StartDataTaskAndWaitForCompletion(
NSURLSessionDataTask* task,
int64_t deadline_ns) {
[delegate_ reset];
[task resume];
return [delegate_ waitForDone:task withTimeout:deadline_ns];
}
::testing::AssertionResult CronetTestBase::IsResponseSuccessful(
NSURLSessionDataTask* task) {
if ([delegate_ errorPerTask][task]) {
return ::testing::AssertionFailure() << "error in response: " <<
[[[delegate_ error] description]
cStringUsingEncoding:NSUTF8StringEncoding];
}
if (![delegate_ responsePerTask][task]) {
return ::testing::AssertionFailure() << " no response has been received";
}
NSInteger statusCode = [delegate_ responsePerTask][task].statusCode;
if (statusCode < 200 || statusCode > 299) {
return ::testing::AssertionFailure()
<< " the response code was " << statusCode;
}
return ::testing::AssertionSuccess() << "no errors in response";
}
::testing::AssertionResult CronetTestBase::IsResponseCanceled(
NSURLSessionDataTask* task) {
NSError* error = [delegate_ errorPerTask][task];
if (error && [error code] == NSURLErrorCancelled)
return ::testing::AssertionSuccess() << "the response is canceled";
return ::testing::AssertionFailure() << "the response is not canceled."
<< " The response error is " <<
[[error description] cStringUsingEncoding:NSUTF8StringEncoding];
}
std::unique_ptr<net::MockCertVerifier> CronetTestBase::CreateMockCertVerifier(
const std::vector<std::string>& certs,
bool known_root) {
std::unique_ptr<net::MockCertVerifier> mock_cert_verifier(
new net::MockCertVerifier());
for (const auto& cert : certs) {
net::CertVerifyResult verify_result;
verify_result.verified_cert =
net::ImportCertFromFile(net::GetTestCertsDirectory(), cert);
// By default, HPKP verification is enabled for known trust roots only.
verify_result.is_issued_by_known_root = known_root;
// Calculate the public key hash and add it to the verify_result.
net::HashValue hashValue;
CHECK(CalculatePublicKeySha256(*verify_result.verified_cert.get(),
&hashValue));
verify_result.public_key_hashes.push_back(hashValue);
mock_cert_verifier->AddResultForCert(verify_result.verified_cert.get(),
verify_result, net::OK);
}
return mock_cert_verifier;
}
void CronetTestBase::PostBlockToFileThread(const base::Location& from_here,
BlockType block) {
base::SingleThreadTaskRunner* file_runner =
[Cronet getFileThreadRunnerForTesting];
file_runner->PostTask(from_here,
base::BindOnce(&CronetTestBase::ExecuteBlock,
base::Unretained(this), block));
}
void CronetTestBase::PostBlockToNetworkThread(const base::Location& from_here,
BlockType block) {
base::SingleThreadTaskRunner* network_runner =
[Cronet getNetworkThreadRunnerForTesting];
network_runner->PostTask(from_here,
base::BindOnce(&CronetTestBase::ExecuteBlock,
base::Unretained(this), block));
}
bool CronetTestBase::CalculatePublicKeySha256(const net::X509Certificate& cert,
net::HashValue* out_hash_value) {
// Extract the public key from the cert.
base::StringPiece spki_bytes;
if (!net::asn1::ExtractSPKIFromDERCert(
net::x509_util::CryptoBufferAsStringPiece(cert.cert_buffer()),
&spki_bytes)) {
LOG(INFO) << "Unable to retrieve the public key from the DER cert";
return false;
}
// Calculate SHA256 hash of public key bytes.
*out_hash_value = net::HashValue(net::HASH_VALUE_SHA256);
crypto::SHA256HashString(spki_bytes, out_hash_value->data(),
crypto::kSHA256Length);
return true;
}
void CronetTestBase::ExecuteBlock(BlockType block) {
block();
}
} // namespace cronet