445 lines
16 KiB
Plaintext
445 lines
16 KiB
Plaintext
#include "WWWConnection.h"
|
|
|
|
// WARNING: this MUST be c decl (NSString ctor will be called after +load, so we cant really change its value)
|
|
|
|
// If you need to communicate with HTTPS server with self signed certificate you might consider UnityWWWConnectionSelfSignedCertDelegate
|
|
// Though use it on your own risk. Blindly accepting self signed certificate is prone to MITM attack
|
|
|
|
//const char* WWWDelegateClassName = "UnityWWWConnectionSelfSignedCertDelegate";
|
|
const char* WWWDelegateClassName = "UnityWWWConnectionDelegate";
|
|
const char* WWWRequestProviderClassName = "UnityWWWRequestDefaultProvider";
|
|
const CFIndex streamSize = 1024;
|
|
static NSOperationQueue *webOperationQueue;
|
|
|
|
@interface UnityWWWConnectionDelegate ()
|
|
@property (readwrite, nonatomic) void* udata;
|
|
@property (readwrite, retain, nonatomic) NSURL* url;
|
|
@property (readwrite, retain, nonatomic) NSString* user;
|
|
@property (readwrite, retain, nonatomic) NSString* password;
|
|
@property (readwrite, retain, atomic) NSMutableURLRequest* request;
|
|
@property (readwrite, retain, atomic) NSURLConnection* connection;
|
|
@property (nonatomic) BOOL manuallyHandleRedirect;
|
|
@property (nonatomic) BOOL wantCertificateCallback;
|
|
@property (readwrite, retain, nonatomic) NSOutputStream* outputStream;
|
|
@end
|
|
|
|
|
|
@implementation UnityWWWConnectionDelegate
|
|
{
|
|
// link to unity WWW implementation
|
|
void* _udata;
|
|
|
|
// connection parameters
|
|
NSMutableURLRequest* _request;
|
|
// connection that we manage
|
|
NSURLConnection* _connection;
|
|
|
|
// NSURLConnection do not quite handle user:pass@host urls
|
|
// so we need to extract user/pass ourselves
|
|
NSURL* _url;
|
|
NSString* _user;
|
|
NSString* _password;
|
|
|
|
// response
|
|
NSInteger _status;
|
|
size_t _estimatedLength;
|
|
size_t _dataRecievd;
|
|
int _retryCount;
|
|
NSOutputStream* _outputStream;
|
|
|
|
BOOL _connectionStarted;
|
|
BOOL _connectionCancelled;
|
|
}
|
|
|
|
@synthesize url = _url;
|
|
@synthesize user = _user;
|
|
@synthesize password = _password;
|
|
@synthesize request = _request;
|
|
@synthesize connection = _connection;
|
|
|
|
@synthesize udata = _udata;
|
|
@synthesize outputStream = _outputStream;
|
|
|
|
- (NSURL*)extractUserPassFromUrl:(NSURL*)url
|
|
{
|
|
self.user = url.user;
|
|
self.password = url.password;
|
|
|
|
// strip user/pass from url
|
|
NSString* newUrl = [NSString stringWithFormat: @"%@://%@%s%s%@%s%s",
|
|
url.scheme, url.host,
|
|
url.port ? ":" : "", url.port ? [[url.port stringValue] UTF8String] : "",
|
|
url.path,
|
|
url.fragment ? "#" : "", url.fragment ? [url.fragment UTF8String] : ""
|
|
];
|
|
return [NSURL URLWithString: newUrl];
|
|
}
|
|
|
|
- (id)initWithURL:(NSURL*)url udata:(void*)udata;
|
|
{
|
|
self->_retryCount = 0;
|
|
if ((self = [super init]))
|
|
{
|
|
self.url = url.user != nil ? [self extractUserPassFromUrl: url] : url;
|
|
self.udata = udata;
|
|
|
|
if ([url.scheme caseInsensitiveCompare: @"http"] == NSOrderedSame)
|
|
NSLog(@"You are using download over http. Currently Unity adds NSAllowsArbitraryLoads to Info.plist to simplify transition, but it will be removed soon. Please consider updating to https.");
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
+ (id)newDelegateWithURL:(NSURL*)url udata:(void*)udata
|
|
{
|
|
Class target = NSClassFromString([NSString stringWithUTF8String: WWWDelegateClassName]);
|
|
NSAssert([target isSubclassOfClass: [UnityWWWConnectionDelegate class]], @"You MUST subclass UnityWWWConnectionDelegate");
|
|
|
|
return [[target alloc] initWithURL: url udata: udata];
|
|
}
|
|
|
|
+ (id)newDelegateWithCStringURL:(const char*)url udata:(void*)udata
|
|
{
|
|
return [UnityWWWConnectionDelegate newDelegateWithURL: [NSURL URLWithString: [NSString stringWithUTF8String: url]] udata: udata];
|
|
}
|
|
|
|
+ (NSMutableURLRequest*)newRequestForHTTPMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers
|
|
{
|
|
Class target = NSClassFromString([NSString stringWithUTF8String: WWWRequestProviderClassName]);
|
|
NSAssert([target conformsToProtocol: @protocol(UnityWWWRequestProvider)], @"You MUST implement UnityWWWRequestProvider protocol");
|
|
|
|
return [target allocRequestForHTTPMethod: method url: url headers: headers];
|
|
}
|
|
|
|
- (void)startConnection
|
|
{
|
|
if (!_connectionCancelled)
|
|
[self.connection start];
|
|
_connectionStarted = YES;
|
|
}
|
|
|
|
- (void)cancelConnection
|
|
{
|
|
if (_connectionStarted)
|
|
[self.connection cancel];
|
|
_connectionCancelled = YES;
|
|
}
|
|
|
|
- (void)abort
|
|
{
|
|
[self cancelConnection];
|
|
}
|
|
|
|
- (void)cleanup
|
|
{
|
|
[self cancelConnection];
|
|
self.connection = nil;
|
|
self.request = nil;
|
|
}
|
|
|
|
// NSURLConnection Delegate Methods
|
|
- (NSURLRequest *)connection:(NSURLConnection *)connection
|
|
willSendRequest:(NSURLRequest *)request
|
|
redirectResponse:(NSURLResponse *)response;
|
|
{
|
|
if (response && self.manuallyHandleRedirect)
|
|
{
|
|
// notify TransportiPhone of the redirect and signal to process the next response.
|
|
if ([response isKindOfClass: [NSHTTPURLResponse class]])
|
|
{
|
|
NSHTTPURLResponse *httpresponse = (NSHTTPURLResponse*)response;
|
|
NSMutableDictionary *headers = [httpresponse.allHeaderFields mutableCopy];
|
|
// grab the correct URL from the request that would have
|
|
// automatically been called through NSURLConnection.
|
|
// The reason we do this is that WebRequestProto's state needs to
|
|
// get updated internally, so we intercept redirects, cancel the current
|
|
// NSURLConnection, notify WebRequestProto and let it construct a new
|
|
// request from the updated URL
|
|
[headers setObject: [request.URL absoluteString] forKey: @"Location"];
|
|
httpresponse = [[NSHTTPURLResponse alloc] initWithURL: response.URL statusCode: httpresponse.statusCode HTTPVersion: nil headerFields: headers];
|
|
[self handleResponse: httpresponse];
|
|
}
|
|
else
|
|
{
|
|
[self handleResponse: response];
|
|
}
|
|
[self cancelConnection];
|
|
return nil;
|
|
}
|
|
return request;
|
|
}
|
|
|
|
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response
|
|
{
|
|
[self handleResponse: response];
|
|
}
|
|
|
|
- (void)handleResponse:(NSURLResponse*)response
|
|
{
|
|
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
|
|
NSDictionary* respHeader = [httpResponse allHeaderFields];
|
|
NSEnumerator* headerEnum = [respHeader keyEnumerator];
|
|
|
|
self->_status = [httpResponse statusCode];
|
|
UnityReportWWWStatus(self.udata, (int)self->_status);
|
|
|
|
for (id headerKey = [headerEnum nextObject]; headerKey; headerKey = [headerEnum nextObject])
|
|
UnityReportWWWResponseHeader(self.udata, [headerKey UTF8String], [[respHeader objectForKey: headerKey] UTF8String]);
|
|
|
|
long long contentLength = [response expectedContentLength];
|
|
|
|
// ignore any data that we might have recieved during a redirect
|
|
self->_estimatedLength = contentLength > 0 && (self->_status / 100 != 3) ? contentLength : 0;
|
|
self->_dataRecievd = 0;
|
|
UnityReportWWWReceivedResponse(self.udata, (unsigned int)self->_estimatedLength);
|
|
}
|
|
|
|
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
|
|
{
|
|
UnityReportWWWReceivedData(self.udata, data.bytes, (unsigned int)[data length], (unsigned int)self->_estimatedLength);
|
|
}
|
|
|
|
- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
|
|
{
|
|
UnityReportWWWNetworkError(self.udata, (int)[error code]);
|
|
UnityReportWWWFinishedLoadingData(self.udata);
|
|
}
|
|
|
|
- (void)connectionDidFinishLoading:(NSURLConnection*)connection
|
|
{
|
|
UnityReportWWWFinishedLoadingData(self.udata);
|
|
}
|
|
|
|
- (void)connection:(NSURLConnection*)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
|
|
{
|
|
UnityReportWWWSentData(self.udata, (unsigned int)totalBytesWritten, (unsigned int)totalBytesExpectedToWrite);
|
|
if (_outputStream != nil)
|
|
{
|
|
unsigned dataSize = streamSize;
|
|
unsigned transmitted = 0;
|
|
const UInt8* bytes = (const UInt8*)UnityWWWGetUploadData(_udata, &dataSize);
|
|
if (dataSize > 0)
|
|
{
|
|
transmitted = [_outputStream write: bytes maxLength: dataSize];
|
|
UnityWWWConsumeUploadData(_udata, transmitted);
|
|
}
|
|
if (dataSize < streamSize && transmitted >= dataSize)
|
|
{
|
|
[_outputStream close];
|
|
_outputStream = nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (BOOL)connection:(NSURLConnection*)connection handleAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (void)connection:(NSURLConnection*)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
|
|
{
|
|
if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodServerTrust)
|
|
{
|
|
if (!self.wantCertificateCallback)
|
|
{
|
|
[challenge.sender performDefaultHandlingForAuthenticationChallenge: challenge];
|
|
return;
|
|
}
|
|
|
|
#if !defined(DISABLE_WEBREQUEST_CERTIFICATE_CALLBACK)
|
|
SecTrustResultType systemResult;
|
|
SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
|
|
if (serverTrust == nil || errSecSuccess != SecTrustEvaluate(serverTrust, &systemResult))
|
|
{
|
|
systemResult = kSecTrustResultOtherError;
|
|
}
|
|
|
|
switch (systemResult)
|
|
{
|
|
case kSecTrustResultUnspecified:
|
|
case kSecTrustResultProceed:
|
|
case kSecTrustResultRecoverableTrustFailure:
|
|
break;
|
|
default:
|
|
[challenge.sender performDefaultHandlingForAuthenticationChallenge: challenge];
|
|
return;
|
|
}
|
|
|
|
SecCertificateRef serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0);
|
|
if (serverCertificate != nil)
|
|
{
|
|
CFDataRef serverCertificateData = SecCertificateCopyData(serverCertificate);
|
|
const UInt8* const data = CFDataGetBytePtr(serverCertificateData);
|
|
const CFIndex size = CFDataGetLength(serverCertificateData);
|
|
bool trust = UnityReportWWWValidateCertificate(self.udata, (const char*)data, (unsigned)size);
|
|
CFRelease(serverCertificateData);
|
|
if (trust)
|
|
{
|
|
NSURLCredential *credential = [NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust];
|
|
[challenge.sender useCredential: credential forAuthenticationChallenge: challenge];
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
[challenge.sender cancelAuthenticationChallenge: challenge];
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
BOOL authHandled = [self connection: connection handleAuthenticationChallenge: challenge];
|
|
|
|
if (authHandled == NO)
|
|
{
|
|
self->_retryCount++;
|
|
|
|
// Empty user or password
|
|
if (self->_retryCount > 1 || self.user == nil || [self.user length] == 0 || self.password == nil || [self.password length] == 0)
|
|
{
|
|
[[challenge sender] cancelAuthenticationChallenge: challenge];
|
|
return;
|
|
}
|
|
|
|
NSURLCredential* newCredential =
|
|
[NSURLCredential credentialWithUser: self.user password: self.password persistence: NSURLCredentialPersistenceNone];
|
|
|
|
[challenge.sender useCredential: newCredential forAuthenticationChallenge: challenge];
|
|
}
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation UnityWWWConnectionSelfSignedCertDelegate
|
|
|
|
- (BOOL)connection:(NSURLConnection*)connection handleAuthenticationChallenge:(NSURLAuthenticationChallenge*)challenge
|
|
{
|
|
if ([[challenge.protectionSpace authenticationMethod] isEqualToString: @"NSURLAuthenticationMethodServerTrust"])
|
|
{
|
|
[challenge.sender useCredential: [NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust]
|
|
forAuthenticationChallenge: challenge];
|
|
|
|
return YES;
|
|
}
|
|
|
|
return [super connection: connection handleAuthenticationChallenge: challenge];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation UnityWWWRequestDefaultProvider
|
|
+ (NSMutableURLRequest*)allocRequestForHTTPMethod:(NSString*)method url:(NSURL*)url headers:(NSDictionary*)headers
|
|
{
|
|
NSMutableURLRequest* request = [[NSMutableURLRequest alloc] init];
|
|
[request setURL: url];
|
|
[request setHTTPMethod: method];
|
|
[request setAllHTTPHeaderFields: headers];
|
|
[request setCachePolicy: NSURLRequestReloadIgnoringLocalCacheData];
|
|
|
|
return request;
|
|
}
|
|
|
|
@end
|
|
|
|
//
|
|
// unity interface
|
|
//
|
|
|
|
extern "C" void UnitySendWWWConnection(void* connection, const void* data, unsigned length, bool blockImmediately, unsigned long timeoutSec, bool wantCertificateCallback)
|
|
{
|
|
UnityWWWConnectionDelegate* delegate = (__bridge UnityWWWConnectionDelegate*)connection;
|
|
|
|
NSMutableURLRequest* request = delegate.request;
|
|
|
|
if (length > 0)
|
|
{
|
|
if (data != nil)
|
|
{
|
|
[request setHTTPBody: [NSData dataWithBytes: data length: length]];
|
|
[request setValue: [NSString stringWithFormat: @"%d", length] forHTTPHeaderField: @"Content-Length"];
|
|
}
|
|
else
|
|
{
|
|
unsigned dataSize = streamSize;
|
|
const void* bytes = UnityWWWGetUploadData(delegate.udata, &dataSize);
|
|
if (dataSize > 0)
|
|
{
|
|
CFReadStreamRef readStream;
|
|
CFWriteStreamRef writeStream;
|
|
CFStreamCreateBoundPair(kCFAllocatorDefault, &readStream, &writeStream, streamSize);
|
|
[request setHTTPBodyStream: (__bridge NSInputStream*)readStream];
|
|
[request setValue: @"chunked" forHTTPHeaderField: @"Transfer-Encoding"];
|
|
CFWriteStreamOpen(writeStream);
|
|
unsigned transmitted = CFWriteStreamWrite(writeStream, (UInt8*)bytes, dataSize);
|
|
UnityWWWConsumeUploadData(delegate.udata, transmitted);
|
|
if (dataSize < streamSize && transmitted >= dataSize)
|
|
CFWriteStreamClose(writeStream);
|
|
else
|
|
delegate.outputStream = (__bridge NSOutputStream*)writeStream;
|
|
}
|
|
}
|
|
}
|
|
|
|
[request setTimeoutInterval: timeoutSec];
|
|
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
webOperationQueue = [[NSOperationQueue alloc] init];
|
|
webOperationQueue.maxConcurrentOperationCount = [NSProcessInfo processInfo].activeProcessorCount * 5;
|
|
webOperationQueue.name = @"com.unity3d.WebOperationQueue";
|
|
});
|
|
|
|
if (wantCertificateCallback)
|
|
{
|
|
delegate.wantCertificateCallback = YES;
|
|
}
|
|
|
|
delegate.connection = [[NSURLConnection alloc] initWithRequest: request delegate: delegate startImmediately: NO];
|
|
delegate.manuallyHandleRedirect = YES;
|
|
[delegate.connection setDelegateQueue: webOperationQueue];
|
|
[delegate startConnection];
|
|
}
|
|
|
|
extern "C" void* UnityStartWWWConnectionCustom(void* udata, const char* methodString, const void* headerDict, const char* url)
|
|
{
|
|
UnityWWWConnectionDelegate* delegate = [UnityWWWConnectionDelegate newDelegateWithCStringURL: url udata: udata];
|
|
|
|
delegate.request = [UnityWWWConnectionDelegate newRequestForHTTPMethod: [NSString stringWithUTF8String: methodString] url: delegate.url headers: (__bridge NSDictionary*)headerDict];
|
|
|
|
return (__bridge_retained void*)delegate;
|
|
}
|
|
|
|
extern "C" bool UnityBlockWWWConnectionIsDone(void* connection)
|
|
{
|
|
UnityWWWConnectionDelegate* delegate = (__bridge UnityWWWConnectionDelegate*)connection;
|
|
return (delegate.request == nil);
|
|
}
|
|
|
|
extern "C" void UnityDestroyWWWConnection(void* connection)
|
|
{
|
|
UnityWWWConnectionDelegate* delegate = (__bridge_transfer UnityWWWConnectionDelegate*)connection;
|
|
|
|
[delegate cleanup];
|
|
delegate = nil;
|
|
}
|
|
|
|
extern "C" void UnityShouldCancelWWW(const void* connection)
|
|
{
|
|
UnityWWWConnectionDelegate* delegate = (__bridge UnityWWWConnectionDelegate*)connection;
|
|
[delegate cancelConnection];
|
|
}
|
|
|
|
extern "C" void UnityWWWClearCookieCache(const char* domain)
|
|
{
|
|
NSArray<NSHTTPCookie*>* cookies;
|
|
NSHTTPCookieStorage* cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
|
|
if (domain == NULL)
|
|
cookies = [cookieStorage cookies];
|
|
else
|
|
cookies = [cookieStorage cookiesForURL: [NSURL URLWithString: [NSString stringWithUTF8String: domain]]];
|
|
NSUInteger cookieCount = [cookies count];
|
|
for (int i = 0; i < cookieCount; ++i)
|
|
[cookieStorage deleteCookie: cookies[i]];
|
|
}
|