| // Copyright 2014 The Crashpad Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "util/net/http_transport.h" |
| |
| #include <CoreFoundation/CoreFoundation.h> |
| #import <Foundation/Foundation.h> |
| #include <sys/utsname.h> |
| |
| #include "base/mac/foundation_util.h" |
| #import "base/mac/scoped_nsobject.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "build/build_config.h" |
| #include "package.h" |
| #include "third_party/apple_cf/CFStreamAbstract.h" |
| #include "util/file/file_io.h" |
| #include "util/misc/implicit_cast.h" |
| #include "util/net/http_body.h" |
| |
| namespace crashpad { |
| |
| namespace { |
| |
| NSString* AppendEscapedFormat(NSString* base, |
| NSString* format, |
| NSString* data) { |
| return [base stringByAppendingFormat: |
| format, |
| [data stringByAddingPercentEncodingWithAllowedCharacters: |
| [[NSCharacterSet |
| characterSetWithCharactersInString: |
| @"()<>@,;:\\\"/[]?={} \t"] invertedSet]]]; |
| } |
| |
| // This builds the same User-Agent string that CFNetwork would build internally, |
| // but it uses PACKAGE_NAME and PACKAGE_VERSION in place of values obtained from |
| // the main bundle’s Info.plist. |
| NSString* UserAgentString() { |
| NSString* user_agent = [NSString string]; |
| |
| // CFNetwork would use the main bundle’s CFBundleName, or the main |
| // executable’s filename if none. |
| user_agent = AppendEscapedFormat( |
| user_agent, @"%@", [NSString stringWithUTF8String:PACKAGE_NAME]); |
| |
| // CFNetwork would use the main bundle’s CFBundleVersion, or the string |
| // “(unknown version)” if none. |
| user_agent = AppendEscapedFormat( |
| user_agent, @"/%@", [NSString stringWithUTF8String:PACKAGE_VERSION]); |
| |
| // Expected to be CFNetwork. |
| NSBundle* nsurl_bundle = [NSBundle bundleForClass:[NSURLRequest class]]; |
| NSString* bundle_name = base::mac::ObjCCast<NSString>([nsurl_bundle |
| objectForInfoDictionaryKey:base::mac::CFToNSCast(kCFBundleNameKey)]); |
| if (bundle_name) { |
| user_agent = AppendEscapedFormat(user_agent, @" %@", bundle_name); |
| |
| NSString* bundle_version = base::mac::ObjCCast<NSString>([nsurl_bundle |
| objectForInfoDictionaryKey:base::mac::CFToNSCast(kCFBundleVersionKey)]); |
| if (bundle_version) { |
| user_agent = AppendEscapedFormat(user_agent, @"/%@", bundle_version); |
| } |
| } |
| |
| utsname os; |
| if (uname(&os) != 0) { |
| PLOG(WARNING) << "uname"; |
| } else { |
| user_agent = AppendEscapedFormat( |
| user_agent, @" %@", [NSString stringWithUTF8String:os.sysname]); |
| user_agent = AppendEscapedFormat( |
| user_agent, @"/%@", [NSString stringWithUTF8String:os.release]); |
| |
| // CFNetwork just uses the equivalent of os.machine to obtain the native |
| // (kernel) architecture. Here, give the process’ architecture as well as |
| // the native architecture. Use the same strings that the kernel would, so |
| // that they can be de-duplicated. |
| #if defined(ARCH_CPU_X86) |
| NSString* arch = @"i386"; |
| #elif defined(ARCH_CPU_X86_64) |
| NSString* arch = @"x86_64"; |
| #else |
| #error Port |
| #endif |
| user_agent = AppendEscapedFormat(user_agent, @" (%@", arch); |
| |
| NSString* machine = [NSString stringWithUTF8String:os.machine]; |
| if (![machine isEqualToString:arch]) { |
| user_agent = AppendEscapedFormat(user_agent, @"; %@", machine); |
| } |
| |
| user_agent = [user_agent stringByAppendingString:@")"]; |
| } |
| |
| return user_agent; |
| } |
| |
| // An implementation of CFReadStream. This implements the V0 callback |
| // scheme. |
| class HTTPBodyStreamCFReadStream { |
| public: |
| explicit HTTPBodyStreamCFReadStream(HTTPBodyStream* body_stream) |
| : body_stream_(body_stream) { |
| } |
| |
| // Creates a new NSInputStream, which the caller owns. |
| NSInputStream* CreateInputStream() { |
| CFStreamClientContext context = { |
| .version = 0, |
| .info = this, |
| .retain = nullptr, |
| .release = nullptr, |
| .copyDescription = nullptr |
| }; |
| constexpr CFReadStreamCallBacksV0 callbacks = { |
| .version = 0, |
| .open = &Open, |
| .openCompleted = &OpenCompleted, |
| .read = &Read, |
| .getBuffer = &GetBuffer, |
| .canRead = &CanRead, |
| .close = &Close, |
| .copyProperty = &CopyProperty, |
| .schedule = &Schedule, |
| .unschedule = &Unschedule |
| }; |
| CFReadStreamRef read_stream = CFReadStreamCreate(nullptr, |
| reinterpret_cast<const CFReadStreamCallBacks*>(&callbacks), &context); |
| return base::mac::CFToNSCast(read_stream); |
| } |
| |
| private: |
| static HTTPBodyStream* GetStream(void* info) { |
| return static_cast<HTTPBodyStreamCFReadStream*>(info)->body_stream_; |
| } |
| |
| static Boolean Open(CFReadStreamRef stream, |
| CFStreamError* error, |
| Boolean* open_complete, |
| void* info) { |
| *open_complete = TRUE; |
| return TRUE; |
| } |
| |
| static Boolean OpenCompleted(CFReadStreamRef stream, |
| CFStreamError* error, |
| void* info) { |
| return TRUE; |
| } |
| |
| static CFIndex Read(CFReadStreamRef stream, |
| UInt8* buffer, |
| CFIndex buffer_length, |
| CFStreamError* error, |
| Boolean* at_eof, |
| void* info) { |
| if (buffer_length == 0) { |
| *at_eof = FALSE; |
| return 0; |
| } |
| |
| FileOperationResult bytes_read = |
| GetStream(info)->GetBytesBuffer(buffer, buffer_length); |
| if (bytes_read < 0) { |
| error->error = -1; |
| error->domain = kCFStreamErrorDomainCustom; |
| } else { |
| *at_eof = bytes_read == 0; |
| } |
| |
| return bytes_read; |
| } |
| |
| static const UInt8* GetBuffer(CFReadStreamRef stream, |
| CFIndex max_bytes_to_read, |
| CFIndex* num_bytes_read, |
| CFStreamError* error, |
| Boolean* at_eof, |
| void* info) { |
| return nullptr; |
| } |
| |
| static Boolean CanRead(CFReadStreamRef stream, void* info) { |
| return TRUE; |
| } |
| |
| static void Close(CFReadStreamRef stream, void* info) {} |
| |
| static CFTypeRef CopyProperty(CFReadStreamRef stream, |
| CFStringRef property_name, |
| void* info) { |
| return nullptr; |
| } |
| |
| static void Schedule(CFReadStreamRef stream, |
| CFRunLoopRef run_loop, |
| CFStringRef run_loop_mode, |
| void* info) {} |
| |
| static void Unschedule(CFReadStreamRef stream, |
| CFRunLoopRef run_loop, |
| CFStringRef run_loop_mode, |
| void* info) {} |
| |
| HTTPBodyStream* body_stream_; // weak |
| |
| DISALLOW_COPY_AND_ASSIGN(HTTPBodyStreamCFReadStream); |
| }; |
| |
| class HTTPTransportMac final : public HTTPTransport { |
| public: |
| HTTPTransportMac(); |
| ~HTTPTransportMac() override; |
| |
| bool ExecuteSynchronously(std::string* response_body) override; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(HTTPTransportMac); |
| }; |
| |
| HTTPTransportMac::HTTPTransportMac() : HTTPTransport() { |
| } |
| |
| HTTPTransportMac::~HTTPTransportMac() { |
| } |
| |
| bool HTTPTransportMac::ExecuteSynchronously(std::string* response_body) { |
| DCHECK(body_stream()); |
| |
| @autoreleasepool { |
| NSString* url_ns_string = base::SysUTF8ToNSString(url()); |
| NSURL* url = [NSURL URLWithString:url_ns_string]; |
| NSMutableURLRequest* request = |
| [NSMutableURLRequest requestWithURL:url |
| cachePolicy:NSURLRequestUseProtocolCachePolicy |
| timeoutInterval:timeout()]; |
| [request setHTTPMethod:base::SysUTF8ToNSString(method())]; |
| |
| // If left to its own devices, CFNetwork would build a user-agent string |
| // based on keys in the main bundle’s Info.plist, giving ugly results if |
| // there is no Info.plist. Provide a User-Agent string similar to the one |
| // that CFNetwork would use, but with appropriate values in place of the |
| // Info.plist-derived strings. |
| [request setValue:UserAgentString() forHTTPHeaderField:@"User-Agent"]; |
| |
| for (const auto& pair : headers()) { |
| [request setValue:base::SysUTF8ToNSString(pair.second) |
| forHTTPHeaderField:base::SysUTF8ToNSString(pair.first)]; |
| } |
| |
| HTTPBodyStreamCFReadStream body_stream_cf(body_stream()); |
| base::scoped_nsobject<NSInputStream> input_stream( |
| body_stream_cf.CreateInputStream()); |
| [request setHTTPBodyStream:input_stream.get()]; |
| |
| NSURLResponse* response = nil; |
| NSError* error = nil; |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| // Deprecated in OS X 10.11. The suggested replacement, NSURLSession, is |
| // only available on 10.9 and later, and this needs to run on earlier |
| // releases. |
| NSData* body = [NSURLConnection sendSynchronousRequest:request |
| returningResponse:&response |
| error:&error]; |
| #pragma clang diagnostic pop |
| |
| if (error) { |
| LOG(ERROR) << [[error localizedDescription] UTF8String] << " (" |
| << [[error domain] UTF8String] << " " << [error code] << ")"; |
| return false; |
| } |
| if (!response) { |
| LOG(ERROR) << "no response"; |
| return false; |
| } |
| NSHTTPURLResponse* http_response = |
| base::mac::ObjCCast<NSHTTPURLResponse>(response); |
| if (!http_response) { |
| LOG(ERROR) << "no http_response"; |
| return false; |
| } |
| NSInteger http_status = [http_response statusCode]; |
| if (http_status < 200 || http_status > 203) { |
| LOG(ERROR) << base::StringPrintf("HTTP status %ld", |
| implicit_cast<long>(http_status)); |
| return false; |
| } |
| |
| if (response_body) { |
| response_body->assign(static_cast<const char*>([body bytes]), |
| [body length]); |
| } |
| |
| return true; |
| } |
| } |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<HTTPTransport> HTTPTransport::Create() { |
| return std::unique_ptr<HTTPTransport>(new HTTPTransportMac()); |
| } |
| |
| } // namespace crashpad |