blob: 2c478588684cc0811ff77ccbbb1eeb4364ee4ccf [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.
#include "url_loader_impl.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/logging.h"
#include "base/mac/scoped_nsautorelease_pool.h"
#include "mojo/data_pipe_utils/data_pipe_drainer.h"
#import <Foundation/Foundation.h>
@interface URLLoaderConnectionDelegate : NSObject<NSURLConnectionDataDelegate>
@property(nonatomic) mojo::URLLoaderImpl::StartCallback startCallback;
@property(nonatomic, retain) NSURLRequest* originalRequest;
@end
@implementation URLLoaderConnectionDelegate {
mojo::URLResponsePtr _response;
mojo::ScopedDataPipeProducerHandle _producer;
}
@synthesize startCallback = _startCallback;
@synthesize originalRequest = _originalRequest;
- (void)connection:(NSURLConnection*)connection
didReceiveResponse:(NSHTTPURLResponse*)response {
_response = mojo::URLResponse::New();
_response->status_code = response.statusCode;
_response->url =
mojo::String(self.originalRequest.URL.absoluteString.UTF8String);
NSUInteger headerCount = response.allHeaderFields.count;
if (headerCount > 0) {
_response->headers = mojo::Array<mojo::HttpHeaderPtr>::New(0);
[response.allHeaderFields enumerateKeysAndObjectsUsingBlock:^(
NSString* key, NSString* value, BOOL* stop) {
auto header = mojo::HttpHeader::New();
header->name = key.UTF8String;
header->value = value.UTF8String;
_response->headers.push_back(header.Pass());
}];
}
mojo::DataPipe pipe;
_response->body = pipe.consumer_handle.Pass();
_producer = pipe.producer_handle.Pass();
}
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data {
if (!_startCallback.is_null()) {
DCHECK(_response);
_startCallback.Run(_response.Pass());
_startCallback.reset();
_response.reset();
}
uint32_t length = data.length;
// TODO(eseidel): This can't work. The data pipe could be full, we need to
// write an async writter for filling the pipe and use it here.
MojoResult result = WriteDataRaw(_producer.get(), data.bytes, &length,
MOJO_WRITE_DATA_FLAG_ALL_OR_NONE);
// FIXME(csg): Handle buffers in case of failures
DCHECK(result == MOJO_RESULT_OK);
DCHECK(length == data.length);
}
- (void)connectionDidFinishLoading:(NSURLConnection*)connection {
DCHECK(_response.is_null());
DCHECK(_startCallback.is_null());
_producer.reset();
}
- (void)connection:(NSURLConnection*)connection
didFailWithError:(NSError*)error {
if (!_startCallback.is_null()) {
if (_response.is_null()) {
_response = mojo::URLResponse::New();
_response->url =
mojo::String(self.originalRequest.URL.absoluteString.UTF8String);
}
_response->error = mojo::NetworkError::New();
_response->error->description =
mojo::String(error.localizedDescription.UTF8String);
_startCallback.Run(_response.Pass());
_startCallback.reset();
}
_response.reset();
_producer.reset();
}
- (NSCachedURLResponse*)connection:(NSURLConnection*)connection
willCacheResponse:(NSCachedURLResponse*)cachedResponse {
return nil;
}
- (void)dealloc {
[_originalRequest release];
DCHECK(_response.is_null());
DCHECK(_startCallback.is_null());
_producer.reset();
[super dealloc];
}
@end
namespace mojo {
class AsyncNSDataDrainer : common::DataPipeDrainer::Client {
public:
using CompletionCallback = base::Callback<void(NSData* /* transfer-none */)>;
AsyncNSDataDrainer()
: data_([[NSMutableData alloc] init]), draining_(false) {}
void StartWithCompletionCallback(ScopedDataPipeConsumerHandle source,
CompletionCallback callback) {
DCHECK(!draining_)
<< "A instance of an AsyncNSDataDrainer can only be used to drain once";
DCHECK(drainer_ == nullptr);
draining_ = true;
callback_ = callback;
// There is no "Start" method on a data pipe drainer. Instantiating
// an instance automatically starts the drain.
drainer_ = std::unique_ptr<common::DataPipeDrainer>(
new common::DataPipeDrainer(this, source.Pass()));
}
~AsyncNSDataDrainer() override { [data_ release]; }
private:
CompletionCallback callback_;
NSMutableData* data_;
std::unique_ptr<common::DataPipeDrainer> drainer_;
bool draining_;
void OnDataAvailable(const void* data, size_t num_bytes) override {
[data_ appendBytes:data length:num_bytes];
}
void OnDataComplete() override {
auto callback = callback_;
NSMutableData* data = data_;
[data retain];
// The owner of this NSData drainer may cause its collection in the callback
// If this is the first thing that happens in the callback, the final
// data_ reference may be released (in dtor) before the callback accesses
// that data further along in the same callback. So make sure we keep an
// extra reference for the duration of the callback.
callback.Run(data);
[data release];
}
DISALLOW_COPY_AND_ASSIGN(AsyncNSDataDrainer);
};
URLLoaderImpl::URLLoaderImpl(InterfaceRequest<URLLoader> request)
: binding_(this, request.Pass()), pending_connection_(nullptr) {}
URLLoaderImpl::~URLLoaderImpl() {
[(id)pending_connection_ release];
}
void URLLoaderImpl::Start(URLRequestPtr request,
const StartCallback& callback) {
if (request->body.size() == 1) {
// If the body has request data, try to drain that
request_data_drainer_ =
std::unique_ptr<AsyncNSDataDrainer>(new AsyncNSDataDrainer());
request_data_drainer_->StartWithCompletionCallback(
request->body[0].Pass(), // handle
base::Bind(&URLLoaderImpl::StartNow, base::Unretained(this),
base::Passed(&request), callback));
} else {
StartNow(request.Pass(), callback, nullptr);
}
}
void URLLoaderImpl::StartNow(URLRequestPtr request,
const StartCallback& callback,
NSData* body_data) {
base::mac::ScopedNSAutoreleasePool pool;
request_data_drainer_.reset();
NSURL* url = [NSURL URLWithString:@(request->url.data())];
NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];
req.HTTPMethod = @(request->method.data());
req.HTTPBody = body_data; // by copy
for (const auto& header : request->headers) {
NSString* name = @(header->name.data());
NSString* value = @(header->value.data());
[req addValue:value forHTTPHeaderField:name];
};
URLLoaderConnectionDelegate* delegate =
[[URLLoaderConnectionDelegate alloc] init];
NSURLConnection* connection =
[[NSURLConnection alloc] initWithRequest:req
delegate:delegate
startImmediately:NO];
delegate.startCallback = callback;
delegate.originalRequest = req;
[delegate release];
[connection scheduleInRunLoop:[NSRunLoop mainRunLoop]
forMode:NSRunLoopCommonModes];
[connection start];
pending_connection_ = connection;
}
void URLLoaderImpl::FollowRedirect(const FollowRedirectCallback& callback) {
base::mac::ScopedNSAutoreleasePool pool;
DCHECK(false);
}
void URLLoaderImpl::QueryStatus(const QueryStatusCallback& callback) {
base::mac::ScopedNSAutoreleasePool pool;
DCHECK(false);
}
} // namespace mojo