#import "ConduitCURLInterface.h" #import // I type this a LOT in here: #define c(STRING) [STRING cStringUsingEncoding:NSUTF8StringEncoding] typedef struct { dispatch_source_t readSource; dispatch_source_t writeSource; } CurlSocketContext_S; @interface ConduitCURLInterface () @property (readonly) dispatch_queue_t disQ; @end @implementation ConduitCURLInterface { dispatch_queue_t disQ; dispatch_source_t timeoutSource; CURLM * curlMulti; NSMutableArray * handleContexts; __unsafe_unretained id delegate; } - (ConduitCURLHandleData *)addUploadWithURL:(NSString *)aURL file:(NSString *)aFile username:(NSString *)aUsername password:(NSString *)aPassword usingSSL:(BOOL)shouldUseSSL context:(id)aContext { if (!aURL || !aFile) { NSLog(@"[CURLInterface] Invalid upload data, URL = %@, file = %@", aURL, aFile); return nil; } NSData * fileData = [NSData dataWithContentsOfFile:aFile]; if (!fileData) return nil; // Make a data object to associate with the easy-handle. ConduitCURLHandleData * handleData = [[ConduitCURLHandleData alloc] init]; [handleData setDataRef:fileData]; [handleData setContext:aContext]; [handleData setInterface:self]; [handleData setFile:aFile]; [handleContexts addObject:handleData]; // Set up the easy-handle. CURL * curlEasy = curl_easy_init(); if (!curlEasy) return nil; curl_easy_setopt(curlEasy, CURLOPT_PRIVATE, handleData); // Attach our data thingy. curl_easy_setopt(curlEasy, CURLOPT_URL, c(aURL)); curl_easy_setopt(curlEasy, CURLOPT_READFUNCTION, &read_function); curl_easy_setopt(curlEasy, CURLOPT_READDATA, handleData); // We need access to the handle's data here too. curl_easy_setopt(curlEasy, CURLOPT_INFILESIZE_LARGE, [fileData length]); curl_easy_setopt(curlEasy, CURLOPT_NOPROGRESS, NO); // This is such a backwards option. curl_easy_setopt(curlEasy, CURLOPT_PROGRESSFUNCTION, &progress_callback); curl_easy_setopt(curlEasy, CURLOPT_PROGRESSDATA, handleData); // …and here… curl_easy_setopt(curlEasy, CURLOPT_VERBOSE, [self debug]); curl_easy_setopt(curlEasy, CURLOPT_DEBUGFUNCTION, &debug_callback); // These two hook up the error handling to our data object: curl_easy_setopt(curlEasy, CURLOPT_DEBUGDATA, [handleData debugOutput]); curl_easy_setopt(curlEasy, CURLOPT_ERRORBUFFER, [handleData errorString]); curl_easy_setopt(curlEasy, CURLOPT_SOCKOPTFUNCTION, &socket_function); curl_easy_setopt(curlEasy, CURLOPT_SOCKOPTDATA, curlEasy); curl_easy_setopt(curlEasy, CURLOPT_NOSIGNAL, YES); curl_easy_setopt(curlEasy, CURLOPT_USERNAME, c(aUsername)); curl_easy_setopt(curlEasy, CURLOPT_PASSWORD, c(aPassword)); if (shouldUseSSL) { curl_easy_setopt(curlEasy, CURLOPT_USE_SSL, CURLUSESSL_ALL); } else { curl_easy_setopt(curlEasy, CURLOPT_USE_SSL, CURLUSESSL_NONE); } curl_easy_setopt(curlEasy, CURLOPT_UPLOAD, YES); dispatch_async(disQ, ^{ curl_multi_add_handle(curlMulti, curlEasy); if ([[self delegate] respondsToSelector:@selector(uploadDidStartWithHandleData:)]) { BOOL proceed = [[self delegate] uploadDidStartWithHandleData:handleData]; if (!proceed) { [handleData setCancelled:YES]; } } NSLog(@"[CURLInterface] Added handle to curlMulti."); }); return handleData; } #pragma mark - Doing stuff // This is where the interesting stuff happens. - (void)performPartialUploadForSocket:(curl_socket_t)aSocket usingActions:(int)someActions { int running_handles; int pending; CURLMcode status = curl_multi_socket_action(curlMulti, aSocket, someActions, &running_handles); if (status) NSLog(@"multi-status: %d", status); // So we need to read through all of the available info for the handles in the multiHandle. CURLMsg * message = NULL; while ((message = curl_multi_info_read(curlMulti, &pending))) { if (message->msg != CURLMSG_DONE) { // This should never happen, so we'll just log and skip this info. NSLog(@"[CURLInterface] Strange CURLMSG: %d (Should be 1/CURLMSG_DONE)", message->msg); continue; } char * privateData = NULL; CURLcode retrievedPrivate = curl_easy_getinfo(message->easy_handle, CURLINFO_PRIVATE, &privateData); if (retrievedPrivate != CURLE_OK) { NSLog(@"Failed to retrieve private data pointer from libcurl. Ack."); continue; } // Now THIS is a pretty line of code: ConduitCURLHandleData * currentHandleData = (__bridge ConduitCURLHandleData *)((void *)privateData); [currentHandleData setErrorCode:(message->data.result)]; if ([currentHandleData errorCode] == CURLE_OK) { if ([[self delegate] respondsToSelector:@selector(uploadDidCompleteWithHandleData:)]) { [[self delegate] uploadDidCompleteWithHandleData:currentHandleData]; } } else { if ([[self delegate] respondsToSelector:@selector(uploadDidFailWithHandleData:)]) { [[self delegate] uploadDidFailWithHandleData:currentHandleData]; } } // Do some final cleanup of the completed handle. curl_multi_remove_handle(curlMulti, message->easy_handle); curl_easy_cleanup(message->easy_handle); [handleContexts removeObject:currentHandleData]; // This will allow the context to be cleaned up. } } #pragma mark - Utility @synthesize disQ; - (CURLM *)multiHandle { return curlMulti; } - (dispatch_source_t)attachSourceOfType:(dispatch_source_type_t)aType forSocket:(curl_socket_t)aSocket usingAction:(int)anAction { dispatch_source_t source = dispatch_source_create(aType, aSocket, 0, disQ); dispatch_source_set_event_handler(source, ^{ //NSLog(@"socket %d: %s", aSocket, (anAction == CURL_CSELECT_IN) ? "read" : "write"); [self performPartialUploadForSocket:aSocket usingActions:anAction]; }); dispatch_source_set_cancel_handler(source, ^{ NSLog(@"cancelling %d", aSocket); dispatch_release(source); }); dispatch_resume(source); return source; } #pragma mark - CURL easy callbacks size_t read_function(void * buf, size_t size, size_t itemCount, void * data) /// @param data: Used here as a pointer to the appropriate handleData. { NSLog(@"hi read"); ConduitCURLHandleData * hData = (__bridge ConduitCURLHandleData *)data; if ([hData cancelled]) return CURL_READFUNC_ABORT; if ([hData currentByte] >= [[hData dataRef] length]) return 0; // We're done! NSUInteger length = MIN([[hData dataRef] length] - [hData currentByte], size * itemCount); [[hData dataRef] getBytes:buf range:NSMakeRange([hData currentByte], length)]; [hData setCurrentByte:([hData currentByte] + length)]; return length; } int progress_callback(void * data, double dltotal, double dlnow, double ultotal, double ulnow) /// @param data: Used here as a pointer to the appropriate handleData. { ConduitCURLHandleData * hData = (__bridge ConduitCURLHandleData *)data; id delegate = [[hData interface] delegate]; // Don't call the delegate on the first few useless calls of this function, or if the total size failed to get set. if (ultotal <= 0) return 0; if ([delegate respondsToSelector:@selector(uploadDidProgressTo:withContext:)]) { BOOL proceed = [delegate uploadDidProgressTo:@((ulnow / ultotal) * 100.0) withHandleData:hData]; if (!proceed) { [hData setCancelled:YES]; return 1; // May as well bail out here. } } return 0; } int debug_callback(CURL * theCURL, curl_infotype infoType, char * debugInfo, size_t length, void * debugStorage) /*** Omitted irrelevant function ***/ int socket_function(void * userData, curl_socket_t curlfd, curlsocktype purpose) { CURL * handle = (CURL *)userData; if (purpose == CURLSOCKTYPE_IPCXN) { char* url = NULL; curl_easy_getinfo(handle, CURLINFO_EFFECTIVE_URL, &url); if (strncmp(url, "ftp:", 4) == 0) { int keepAlive = 1; socklen_t keepAliveLen = sizeof(keepAlive); int result = setsockopt(curlfd, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, keepAliveLen); if (result) { NSLog(@"Unable to set FTP control connection keepalive with error: %i", result); } } } return 0; } #pragma mark - CURL multi-socket callbacks int socket_callback(CURL * easyHandle, curl_socket_t socket, int action, void * userData, void * socketData) /// @param userData: Used here as an alias to this ConduitCURLInterface. /// @param socketData: Used here as a pointer to the structure that maintains the dispatch sources for the given socket. { ConduitCURLInterface * interface = (__bridge ConduitCURLInterface *)userData; CurlSocketContext_S * currentContext = (CurlSocketContext_S *)socketData; if (action & CURL_POLL_IN || action & CURL_POLL_OUT) { if (!currentContext) { NSLog(@"Creating CurlSocketContext_S for %d", socket); // If this socket doesn't have a context attached yet, make one: currentContext = malloc(sizeof(CurlSocketContext_S)); memset(currentContext, 0, sizeof(CurlSocketContext_S)); curl_multi_assign([interface multiHandle], socket, currentContext); } } if (action & CURL_POLL_IN) { NSLog(@"CURL_POLL_IN for %d", socket); currentContext->readSource = [interface attachSourceOfType:DISPATCH_SOURCE_TYPE_READ forSocket:socket usingAction:CURL_CSELECT_IN]; } if (action & CURL_POLL_OUT) { NSLog(@"CURL_POLL_OUT for %d", socket); currentContext->writeSource = [interface attachSourceOfType:DISPATCH_SOURCE_TYPE_WRITE forSocket:socket usingAction:CURL_CSELECT_OUT]; } if ((action & CURL_POLL_REMOVE) && currentContext) { NSLog(@"CURL_POLL_REMOVE for %d", socket); if (currentContext->readSource) { dispatch_source_cancel(currentContext->readSource); } if (currentContext->writeSource) { dispatch_source_cancel(currentContext->writeSource); } free(currentContext); curl_multi_assign([interface multiHandle], socket, NULL); } return 0; // Apparently it has to. } void timer_callback(CURLM * multiHandle, long timeout_ms, void * userData) /// @param userData: Used here as an alias to timeoutSource. { if (timeout_ms <= 0) { timeout_ms = 1; } NSLog(@"new timeout = %ld", timeout_ms); int64_t timeout_ns = timeout_ms * NSEC_PER_MSEC; dispatch_source_set_timer((dispatch_source_t)userData, DISPATCH_TIME_NOW, timeout_ns, timeout_ns / 100); } #pragma mark - Plumbing @synthesize debug, delegate; - (id)init { if (!(self = [super init])) return nil; if (curl_global_init(CURL_GLOBAL_NOTHING)) return nil; curlMulti = curl_multi_init(); // Doing this first to avoid having to roll-back if it fails. if (!curlMulti) return nil; // Get a dispatch queue and add the timeout source. disQ = dispatch_queue_create(DISPATCH_QUEUE_SERIAL, 0); timeoutSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, disQ); dispatch_source_set_event_handler(timeoutSource, ^{ [self performPartialUploadForSocket:CURL_SOCKET_TIMEOUT usingActions:0]; }); dispatch_resume(timeoutSource); // Set up the cURL multi-handle. curl_multi_setopt(curlMulti, CURLMOPT_SOCKETFUNCTION, &socket_callback); curl_multi_setopt(curlMulti, CURLMOPT_SOCKETDATA, self); curl_multi_setopt(curlMulti, CURLMOPT_TIMERFUNCTION, &timer_callback); curl_multi_setopt(curlMulti, CURLMOPT_TIMERDATA, timeoutSource); handleContexts = [NSMutableArray array]; return self; } - (void)dealloc /// @todo: Clean up any remaining handles first. { curl_multi_cleanup(curlMulti); } @end