[go: nahoru, domu]

blob: 0d001b5aef016061ace3d07952d9ef3e369051bb [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/download/internal/background_service/ios/background_download_task_helper.h"
#import <Foundation/Foundation.h>
#include <deque>
#include "base/apple/foundation_util.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#import "base/task/single_thread_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "components/download/public/background_service/background_download_service.h"
#include "components/download/public/background_service/download_params.h"
#include "components/download/public/background_service/features.h"
#include "net/base/mac/url_conversions.h"
namespace {
bool g_ignore_localhost_ssl_error_for_testing = false;
}
using AuthenticationChallengeBlock =
void (^)(NSURLSessionAuthChallengeDisposition disposition,
NSURLCredential* credential);
using CompletionCallback =
download::BackgroundDownloadTaskHelper::CompletionCallback;
using UpdateCallback = download::BackgroundDownloadTaskHelper::UpdateCallback;
class DownloadTaskInfo {
public:
DownloadTaskInfo(const base::FilePath& download_path,
CompletionCallback completion_callback,
UpdateCallback update_callback)
: download_path_(download_path),
completion_callback_(std::move(completion_callback)),
update_callback_(update_callback) {}
~DownloadTaskInfo() = default;
base::FilePath download_path_;
CompletionCallback completion_callback_;
UpdateCallback update_callback_;
};
@interface BackgroundDownloadDelegate
: NSObject <NSURLSessionDownloadDelegate> {
@private
// Callback to invoke once background session completes.
base::OnceClosure _sessionCompletionHandler;
}
- (instancetype)initWithTaskRunner:
(scoped_refptr<base::SingleThreadTaskRunner>)taskRunner;
- (void)setSessionCompletionHandler:(base::OnceClosure)sessionCompletionHandler;
@end
@implementation BackgroundDownloadDelegate {
std::map<NSURLSessionDownloadTask*, std::unique_ptr<DownloadTaskInfo>>
_downloadTaskInfos;
scoped_refptr<base::SingleThreadTaskRunner> _taskRunner;
}
- (instancetype)initWithTaskRunner:
(scoped_refptr<base::SingleThreadTaskRunner>)taskRunner {
_taskRunner = taskRunner;
return self;
}
- (void)addDownloadTask:(NSURLSessionDownloadTask*)downloadTask
downloadTaskInfo:(std::unique_ptr<DownloadTaskInfo>)downloadTaskInfo {
_downloadTaskInfos[downloadTask] = std::move(downloadTaskInfo);
}
- (void)onDownloadCompletion:(bool)success
downloadTask:(NSURLSessionDownloadTask*)downloadTask
fileSize:(int64_t)fileSize {
std::unique_ptr<DownloadTaskInfo> taskInfo;
// Remove the download from map if it exists.
auto it = _downloadTaskInfos.find(downloadTask);
if (it != _downloadTaskInfos.end()) {
taskInfo = std::move(it->second);
_downloadTaskInfos.erase(it);
}
base::FilePath filePath;
if (taskInfo) {
filePath = taskInfo->download_path_;
}
if (taskInfo && taskInfo->completion_callback_) {
// Invoke the completion callback on main thread.
_taskRunner->PostTask(
FROM_HERE, base::BindOnce(std::move(taskInfo->completion_callback_),
success, filePath, fileSize));
}
}
- (void)setSessionCompletionHandler:
(base::OnceClosure)sessionCompletionHandler {
_sessionCompletionHandler = std::move(sessionCompletionHandler);
}
#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession*)session
downloadTask:(NSURLSessionDownloadTask*)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes {
DVLOG(1) << __func__ << " , offset:" << fileOffset
<< " , expectedTotalBytes:" << expectedTotalBytes;
NOTIMPLEMENTED();
}
- (void)URLSession:(NSURLSession*)session
downloadTask:(NSURLSessionDownloadTask*)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
DVLOG(1) << __func__ << ",byte written: " << bytesWritten
<< ", totalBytesWritten:" << totalBytesWritten
<< ", totalBytesExpectedToWrite:" << totalBytesExpectedToWrite;
auto it = _downloadTaskInfos.find(downloadTask);
if (it != _downloadTaskInfos.end() && it->second->update_callback_) {
_taskRunner->PostTask(
FROM_HERE,
base::BindRepeating(it->second->update_callback_, totalBytesWritten));
}
}
- (void)URLSession:(NSURLSession*)session
downloadTask:(NSURLSessionDownloadTask*)downloadTask
didFinishDownloadingToURL:(NSURL*)location {
DVLOG(1) << __func__;
if (!location) {
[self onDownloadCompletion:/*success=*/false
downloadTask:downloadTask
fileSize:0];
return;
}
// Analyze the response code. Treat non http 200 as failure downloads.
NSURLResponse* response = [downloadTask response];
if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
if ([httpResponse statusCode] != 200) {
[self onDownloadCompletion:/*success=*/false
downloadTask:downloadTask
fileSize:0];
return;
}
}
auto it = _downloadTaskInfos.find(downloadTask);
if (it == _downloadTaskInfos.end()) {
LOG(ERROR) << "Failed to find the download task.";
[self onDownloadCompletion:/*success=*/false
downloadTask:downloadTask
fileSize:0];
return;
}
// Move the downloaded file from platform temporary directory to download
// service's target directory. This must happen immediately on the current
// thread or iOS may delete the file.
const base::FilePath tempPath =
base::apple::NSStringToFilePath([location path]);
if (!base::Move(tempPath, it->second->download_path_)) {
LOG(ERROR) << "Failed to move file from:" << tempPath
<< ", to:" << it->second->download_path_;
[self onDownloadCompletion:/*success=*/false
downloadTask:downloadTask
fileSize:0];
return;
}
// Get the file size on current thread.
int64_t fileSize = 0;
if (!base::GetFileSize(it->second->download_path_, &fileSize)) {
LOG(ERROR) << "Failed to get file size from:" << it->second->download_path_;
[self onDownloadCompletion:/*success=*/false
downloadTask:downloadTask
fileSize:0];
return;
}
[self onDownloadCompletion:/*success=*/true
downloadTask:downloadTask
fileSize:fileSize];
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:
(NSURLSession*)session {
if (!_sessionCompletionHandler.is_null()) {
// Nothing should be called after invoking completionHandler.
std::move(_sessionCompletionHandler).Run();
}
}
#pragma mark - NSURLSessionDelegate
- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didCompleteWithError:(NSError*)error {
VLOG(1) << __func__;
if (!error)
return;
NSURLSessionDownloadTask* downloadTask =
[task isKindOfClass:[NSURLSessionDownloadTask class]]
? (NSURLSessionDownloadTask*)task
: nil;
if (!downloadTask) {
LOG(ERROR) << "Encountered errors unrelated to download.";
return;
}
[self onDownloadCompletion:/*success=*/false
downloadTask:downloadTask
fileSize:0];
}
- (void)URLSession:(NSURLSession*)session
didReceiveChallenge:(NSURLAuthenticationChallenge*)challenge
completionHandler:(AuthenticationChallengeBlock)completionHandler {
DCHECK(completionHandler);
if ([challenge.protectionSpace.authenticationMethod
isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if (g_ignore_localhost_ssl_error_for_testing &&
[challenge.protectionSpace.host isEqualToString:@"127.0.0.1"]) {
NSURLCredential* credential = [NSURLCredential
credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
return;
}
}
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
@end
namespace download {
// Object for passing the delegate and session to the UI thread as unique_ptr
// doesn't work on both of those.
struct URLSessionHelper {
URLSessionHelper(BackgroundDownloadDelegate* delegate, NSURLSession* session)
: delegate(delegate), session(session) {}
BackgroundDownloadDelegate* delegate = nullptr;
NSURLSession* session = nullptr;
};
using CreateUrlSessionCallback =
base::OnceCallback<void(std::unique_ptr<URLSessionHelper>)>;
void CreateNSURLSession(scoped_refptr<base::SingleThreadTaskRunner> task_runner,
CreateUrlSessionCallback callback) {
const int kIdentifierSuffix = 1000000;
std::string identifier =
base::StringPrintf("%s-%d", download::kBackgroundDownloadIdentifierPrefix,
base::RandInt(0, kIdentifierSuffix));
NSURLSessionConfiguration* configuration =
base::FeatureList::IsEnabled(
download::kDownloadServiceForegroundSessionIOSFeature)
? [NSURLSessionConfiguration defaultSessionConfiguration]
: [NSURLSessionConfiguration
backgroundSessionConfigurationWithIdentifier:
base::SysUTF8ToNSString(identifier)];
configuration.sessionSendsLaunchEvents = YES;
// TODO(qinmin): Check if we need 2 sessions here, since discretionary
// value may be different.
configuration.discretionary = true;
BackgroundDownloadDelegate* delegate =
[[BackgroundDownloadDelegate alloc] initWithTaskRunner:task_runner];
NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration
delegate:delegate
delegateQueue:nil];
task_runner->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback),
std::make_unique<URLSessionHelper>(delegate, session)));
}
// Implementation of BackgroundDownloadTaskHelper based on
// NSURLSessionDownloadTask api.
// This class lives on main thread and all the callbacks will be invoked on main
// thread. The NSURLSessionDownloadDelegate it uses will broadcast download
// events on a background thread.
class BackgroundDownloadTaskHelperImpl : public BackgroundDownloadTaskHelper {
public:
BackgroundDownloadTaskHelperImpl() = default;
~BackgroundDownloadTaskHelperImpl() override {
delegate_ = nullptr;
[session_ invalidateAndCancel];
}
private:
struct DownloadTask {
DownloadTask(const std::string& guid,
const base::FilePath& target_path,
const RequestParams& request_params,
CompletionCallback completion_callback,
UpdateCallback update_callback)
: guid(guid),
target_path(target_path),
request_params(request_params),
completion_callback(std::move(completion_callback)),
update_callback(update_callback) {}
std::string guid;
base::FilePath target_path;
RequestParams request_params;
CompletionCallback completion_callback;
UpdateCallback update_callback;
};
void OnNSURLSessionCreated(std::unique_ptr<URLSessionHelper> session_helper) {
delegate_ = session_helper->delegate;
session_ = session_helper->session;
ProcessDownloadTasks();
}
void StartDownload(const std::string& guid,
const base::FilePath& target_path,
const RequestParams& request_params,
const SchedulingParams& scheduling_params,
CompletionCallback completion_callback,
UpdateCallback update_callback) override {
DCHECK(!guid.empty());
DCHECK(!target_path.empty());
download_tasks_.emplace_back(guid, target_path, request_params,
std::move(completion_callback),
update_callback);
// Initialize the NSURLSession and delegate on another thread due to
// http://crbug.com/1359437.
if (!is_initializing_) {
is_initializing_ = true;
CreateUrlSessionCallback cb = base::BindOnce(
&BackgroundDownloadTaskHelperImpl::OnNSURLSessionCreated,
weak_ptr_factory_.GetWeakPtr());
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&CreateNSURLSession,
base::SingleThreadTaskRunner::GetCurrentDefault(),
std::move(cb)));
return;
}
if (delegate_)
ProcessDownloadTasks();
}
void ProcessDownloadTasks() {
while (!download_tasks_.empty()) {
ProcessDownloadTask(download_tasks_.front());
download_tasks_.pop_front();
}
}
void ProcessDownloadTask(DownloadTask& task) {
NSURL* url = net::NSURLWithGURL(task.request_params.url);
NSMutableURLRequest* request =
[[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:base::SysUTF8ToNSString(task.request_params.method)];
net::HttpRequestHeaders::Iterator it(task.request_params.request_headers);
while (it.GetNext()) {
[request setValue:base::SysUTF8ToNSString(it.value())
forHTTPHeaderField:base::SysUTF8ToNSString(it.name())];
}
NSURLSessionDownloadTask* downloadTask =
[session_ downloadTaskWithRequest:request];
auto download_task_info = std::make_unique<DownloadTaskInfo>(
task.target_path, std::move(task.completion_callback),
task.update_callback);
[delegate_ addDownloadTask:downloadTask
downloadTaskInfo:std::move(download_task_info)];
[downloadTask resume];
}
void HandleEventsForBackgroundURLSession(
base::OnceClosure completion_handler) override {
delegate_.sessionCompletionHandler = std::move(completion_handler);
}
BackgroundDownloadDelegate* delegate_ = nullptr;
NSURLSession* session_ = nullptr;
std::deque<DownloadTask> download_tasks_;
bool is_initializing_ = false;
base::WeakPtrFactory<BackgroundDownloadTaskHelperImpl> weak_ptr_factory_{
this};
};
// static
std::unique_ptr<BackgroundDownloadTaskHelper>
BackgroundDownloadTaskHelper::Create() {
return std::make_unique<BackgroundDownloadTaskHelperImpl>();
}
// static
void BackgroundDownloadTaskHelper::SetIgnoreLocalSSLErrorForTesting(
bool ignore) {
g_ignore_localhost_ssl_error_for_testing = ignore;
}
} // namespace download