// // NSURLProtocol.m // mySTEP // // Created by Dr. H. Nikolaus Schaller on Wed Jan 04 2006. // Copyright (c) 2006-2009 Golden Delicous Computers. All rights reserved. // #import #import #import #import #import #import #import #import #import #import "NSPrivate.h" @class _NSHTTPURLProtocol; #define CAN_GZIP 0 @interface _NSHTTPSerialization : NSObject { // for handling a single protocol entity according to http://www.faqs.org/ftp/rfc/rfc2616.pdf NSMutableArray *_requestQueue; // all queued requests _NSHTTPURLProtocol *_currentRequest; // current request // sending NSOutputStream *_outputStream; NSInputStream *_headerStream; // header while sending NSInputStream *_bodyStream; // for sending the body BOOL _shouldClose; // server will close after current request - we must requeue other requests on a new connection BOOL _sendChunked; // sending with transfer-encoding: chunked // receiving NSInputStream *_inputStream; unsigned _statusCode; // status code defined by response NSMutableDictionary *_headers; // received headers unsigned long long _contentLength; // if explicitly specified by header NSMutableString *_headerLine; // current header line unsigned int _chunkLength; // current chunk length for receiver char _lastChr; // previouds character while reading header BOOL _readingBody; // done with reading header BOOL _isChunked; // transfer-encoding: chunked BOOL _willClose; // server has announced to close the connection } + (_NSHTTPSerialization *) serializerForProtocol:(_NSHTTPURLProtocol *) protocol; // get connection queue for handling this request (may create a new one) - (void) startLoading:(_NSHTTPURLProtocol *) proto; // add to queue - (void) stopLoading:(_NSHTTPURLProtocol *) proto; // remove from queue - may cancel/close connection if it is current request or simply stop notifications // internal methods - (BOOL) connectToServer; // connect to server - (void) headersReceived; - (void) bodyReceived; - (void) trailerReceived; - (void) endOfUseability; // connection became invalid @end @interface _NSHTTPURLProtocol : NSURLProtocol { _NSHTTPSerialization /* nonretained */ *_connection; // where we have been queued up NSMutableArray *_runLoops; // additional runloops to schedule NSMutableArray *_modes; // additional modes to schedule } - (NSString *) _uniqueKey; // a key to identify the same server connection - (void) _setConnection:(_NSHTTPSerialization *) connection; - (_NSHTTPSerialization *) _connection; - (void) didFailWithError:(NSError *) error; - (void) didLoadData:(NSData *) data; - (void) didFinishLoading; - (void) didReceiveResponse:(NSHTTPURLResponse *) response; @end @interface _NSFTPURLProtocol : NSURLProtocol { NSInputStream *_inputStream; NSOutputStream *_outputStream; } @end @interface _NSFileURLProtocol : NSURLProtocol { BOOL _stopLoading; } @end // NOTE: Cocoa has this without _ @interface _NSAboutURLProtocol : NSURLProtocol { BOOL _stopLoading; } @end @interface _NSDataURLProtocol : NSURLProtocol { BOOL _stopLoading; } @end @implementation NSURLProtocol static NSMutableArray *_registeredClasses; + (void) initialize; { _registeredClasses=[[NSMutableArray alloc] initWithCapacity:10]; [self registerClass:[_NSHTTPURLProtocol class]]; [self registerClass:[_NSFTPURLProtocol class]]; [self registerClass:[_NSFileURLProtocol class]]; [self registerClass:[_NSAboutURLProtocol class]]; [self registerClass:[_NSDataURLProtocol class]]; } + (BOOL) canInitWithRequest:(NSURLRequest *) request; { SUBCLASS; return NO; } + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *) request; { SUBCLASS; return nil; } + (id) propertyForKey:(NSString *) key inRequest:(NSURLRequest *) request; { SUBCLASS; return nil; } + (void) setProperty:(id) value forKey:(NSString *) key inRequest:(NSMutableURLRequest *) request; { SUBCLASS; } + (void) removePropertyForKey:(NSString *) key inReq:(NSMutableURLRequest *) req; { SUBCLASS; } + (BOOL) registerClass:(Class) protocolClass; { if(![protocolClass isSubclassOfClass:[NSURLProtocol class]]) return NO; [_registeredClasses addObject:protocolClass]; // find most recent version first return YES; } + (BOOL) requestIsCacheEquivalent:(NSURLRequest *) a toRequest:(NSURLRequest *) b; { // default equivalence check a=[self canonicalRequestForRequest:a]; b=[self canonicalRequestForRequest:b]; return [a isEqual:b]; } + (void) unregisterClass:(Class) protocolClass; { [_registeredClasses removeObject:protocolClass]; } - (NSString *) description; { return [NSString stringWithFormat:@"%@ %@", NSStringFromClass(isa), _request]; } - (NSCachedURLResponse *) cachedResponse; { return _cachedResponse; } - (id ) client; { return _client; } - (NSURLRequest *) request; { return _request; } - (id) initWithRequest:(NSURLRequest *) request cachedResponse:(NSCachedURLResponse *) cachedResponse client:(id ) client; { NSEnumerator *e=[_registeredClasses reverseObjectEnumerator]; // go through classes starting with last one first Class c; #if 0 NSLog(@"%@ initWithRequest:%@ client:%@", NSStringFromClass(isa), request, client); #endif if([self class] == [NSURLProtocol class]) { // not a subclass [self release]; while((c=[e nextObject])) { #if 0 NSLog(@"check %@", NSStringFromClass(c)); #endif if([c canInitWithRequest:request]) { // found! return [[c alloc] initWithRequest:request cachedResponse:nil client:client]; } } return nil; } if((self=[super init])) { // here we are called for a subclass _request=[request copy]; // save a copy of the request _cachedResponse=[cachedResponse retain]; _client=[(NSObject *) client retain]; // we must retain the client (or it may disappear while we still receive data) } #if 0 NSLog(@" -> %@", self); #endif return self; } - (void) dealloc; { if(isa != [NSURLProtocol class]) { // has been initialized [self stopLoading]; [_request release]; [_cachedResponse release]; [(NSObject *) _client release]; } [super dealloc]; } - (void) startLoading; { SUBCLASS; } - (void) stopLoading; { SUBCLASS; } // such undocumented methods must exist since NSURLConnection can be scheduled on several runloops and modes in parallel - (void) scheduleInRunLoop:(NSRunLoop *) runLoop forMode:(NSString *) mode; { return; } - (void) unscheduleFromRunLoop:(NSRunLoop *) runLoop forMode:(NSString *) mode; { return; } @end /* - (void) URLProtocol:(NSURLProtocol *) proto cachedResponseIsValid:(NSCachedURLResponse *) resp; - (void) URLProtocol:(NSURLProtocol *) proto didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *) chall; - (void) URLProtocol:(NSURLProtocol *) proto didFailWithError:(NSError *) error; - (void) URLProtocol:(NSURLProtocol *) proto didLoadData:(NSData *) data; - (void) URLProtocol:(NSURLProtocol *) proto didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *) chall; - (void) URLProtocol:(NSURLProtocol *) proto didReceiveResponse:(NSURLResponse *) response cacheStoragePolicy:(NSURLCacheStoragePolicy) policy; - (void) URLProtocol:(NSURLProtocol *) proto wasRedirectedToRequest:(NSURLRequest *) request redirectResponse:(NSURLResponse *) redirectResponse; - (void) URLProtocolDidFinishLoading:(NSURLProtocol *) proto; */ @implementation _NSHTTPSerialization // see http://www.w3.org/Protocols/rfc2616/rfc2616.html // or http://www.faqs.org/ftp/rfc/rfc2616.pdf // and a very good tutorial: http://www.jmarshall.com/easy/http/ // http://www.io.com/~maus/HttpKeepAlive.html // http://java.sun.com/j2se/1.5.0/docs/guide/net/http-keepalive.html static NSMutableDictionary *_httpConnections; - (BOOL) willClose; { // we have announced (in request "Connection: close") to close or server has announced (in reply) - i.e. don't queue up more requests return _shouldClose || _willClose; } + (_NSHTTPSerialization *) serializerForProtocol:(_NSHTTPURLProtocol *) protocol; { // get connection queue for handling this request (may create a new one) NSString *key=[protocol _uniqueKey]; _NSHTTPSerialization *ser=[_httpConnections objectForKey:key]; // could also store an array! if(!ser || [ser willClose]) { // not found or server has announced to close connection: we need a new connection ser=[self new]; #if 1 NSLog(@"%@: new serializer %@", key, ser); #endif if(!_httpConnections) _httpConnections=[[NSMutableDictionary alloc] initWithCapacity:10]; // we also may open several serializers for the same combination but HTTP 1.1 recommends to use no more than 2 in parallel [_httpConnections setObject:ser forKey:key]; [ser release]; } #if 1 else { NSLog(@"%@: reuse serializer %@", key, ser); } #endif return ser; } - (id) init { if((self=[super init])) { _requestQueue=[NSMutableArray new]; _headerLine=[[NSMutableString alloc] initWithCapacity:50]; // typical length of a header line } return self; } - (void) dealloc; { #if 1 NSLog(@"dealloc %@", self); NSLog(@" connections: %d", [_httpConnections count]); #endif NSAssert([_requestQueue count] == 0, @"unprocessed requests left over!"); // otherwise we loose requests [_currentRequest _setConnection:nil]; // has been processed [_currentRequest release]; // if still stored [_requestQueue release]; // no longer needed [_headerLine release]; // if left over [_headers release]; // received headers [_headerStream release]; // for sending the header [_bodyStream release]; // for sending the body [_inputStream close]; // if still open [_inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [_inputStream release]; // if still sitting around for any reason (e.g. the runloop did no longer run) [_outputStream close]; [_outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [_outputStream release]; [super dealloc]; } - (void) endOfUseability { NSArray *keys; #if 1 NSLog(@"endOfUseability %@", self); #endif [_inputStream close]; [_inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [_inputStream release]; _inputStream=nil; [_outputStream close]; [_outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [_outputStream release]; _outputStream=nil; [self retain]; // the next two lines could otherwise -dealloc and dealloc the request queue keys=[_httpConnections allKeysForObject:self]; // get all my keys [_httpConnections removeObjectsForKeys:keys]; // remove us from the list of active connections if we are still there [_requestQueue makeObjectsPerformSelector:@selector(_restartLoading)]; // this removes any pending requests from the queue and reschedules in a new/different serializer queue [self autorelease]; } - (BOOL) connectToServer { // we have no open connection yet NSURLRequest *request=[_currentRequest request]; NSURL *url=[request URL]; BOOL isHttps=[[url scheme] isEqualToString:@"https"]; // we assume that ther can't be a http and a https connection in parallel on the same host:port pair NSHost *host=[NSHost hostWithName:[url host]]; // try to resolve (NOTE: this may block for some seconds! Therefore, the resolver should be run in a separate thread! int port=[[url port] intValue]; if(!host) host=[NSHost hostWithAddress:[url host]]; // try dotted notation if(!host) { // still not resolved [_currentRequest didFailWithError:[NSError errorWithDomain:@"NSURLErrorDomain" code:-1003 userInfo:[NSDictionary dictionaryWithObjectsAndKeys: url, @"NSErrorFailingURLKey", [url absoluteString], @"NSErrorFailingURLStringKey", @"can't resolve host name", @"NSLocalizedDescription", nil]]]; return NO; } if(!port) port=isHttps?433:80; // default port if none is specified [NSStream getStreamsToHost:host port:port inputStream:&_inputStream outputStream:&_outputStream]; if(!_inputStream || !_outputStream) { // error opening the streams #if 1 NSLog(@"could not create streams for %@:%u", host, [[url port] intValue]); #endif [_currentRequest didFailWithError:[NSError errorWithDomain:@"NSURLErrorDomain" code:-1004 userInfo:[NSDictionary dictionaryWithObjectsAndKeys: url, @"NSErrorFailingURLKey", host, @"NSErrorFailingURLStringKey", @"can't open connections to host", @"NSLocalizedDescription", nil]]]; _inputStream=nil; _outputStream=nil; return NO; } [_inputStream retain]; [_outputStream retain]; // endOfUseability will do a release [_inputStream setDelegate:self]; [_outputStream setDelegate:self]; if(isHttps) { // use SSL [_inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey]; [_outputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey]; } #if 1 NSLog(@"did initialize streams for %@", self); NSLog(@" input %@", _inputStream); NSLog(@" output %@", _outputStream); #endif [_inputStream open]; [_outputStream open]; [_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; // and schedule for reception return YES; } - (void) startLoadingNextRequest { NSURLProtocol *protocol=[_requestQueue objectAtIndex:0]; NSURLRequest *request=[protocol request]; NSURL *url=[request URL]; NSString *method=[request HTTPMethod]; NSString *path=[url path]; NSMutableData *headerData; NSMutableDictionary *requestHeaders; NSData *body; NSEnumerator *e; NSString *key; NSString *header; NSCachedURLResponse *cachedResponse; [_currentRequest release]; // release any done request _currentRequest=[protocol retain]; [_requestQueue removeObjectAtIndex:0]; // remove from queue if(!_outputStream && ![self connectToServer]) // connect to server { [self endOfUseability]; // current request will be lost return; // we can't connect } #if 1 NSLog(@"startLoading: %@ on %@", _currentRequest, self); #endif headerData=[[NSMutableData alloc] initWithCapacity:200]; header=[NSString stringWithFormat:@"%@ %@ HTTP/1.1\r\n", method, [path length] > 0?[path stringByAddingPercentEscapesUsingEncoding:NSISOLatin1StringEncoding]:(NSString *)@"/"]; #if 1 NSLog(@"request: %@", header); #endif [headerData appendData:[header dataUsingEncoding:NSUTF8StringEncoding]]; // CHECKME: // CHECKME: what about lower/uppercase in the user provided header fields??? requestHeaders=[[request allHTTPHeaderFields] mutableCopy]; // start with the provided headers first so that we can overwrite and remove spurious headers if(!requestHeaders) requestHeaders=[[NSMutableDictionary alloc] initWithCapacity:5]; // no headers provided by request if([request HTTPShouldHandleCookies]) { NSHTTPCookieStorage *cs=[NSHTTPCookieStorage sharedHTTPCookieStorage]; NSDictionary *cdict=[NSHTTPCookie requestHeaderFieldsWithCookies:[cs cookiesForURL:url]]; [requestHeaders addEntriesFromDictionary:cdict]; // add to headers } if([url port]) header=[NSString stringWithFormat:@"%@:%u", [url host], [[url port] intValue]]; // non-default port else header=[url host]; [requestHeaders setObject:header forKey:@"Host"]; if((cachedResponse=[_currentRequest cachedResponse]) && ([method isEqualToString:@"GET"] || [method isEqualToString:@"HEAD"])) { // ask server to send a new version or a 304 so that we use the cached response NSHTTPURLResponse *resp=(NSHTTPURLResponse *) [cachedResponse response]; NSString *lastModified=[[resp allHeaderFields] objectForKey:@"last-modified"]; #if 1 NSLog(@"last-modified -> if-modified-since %@", lastModified); #endif if(lastModified) [requestHeaders setObject:lastModified forKey:@"If-Modified-Since"]; // copy into the new request } #if CAN_GZIP [requestHeaders setObject:@"identity, gzip" forKey:@"Accept-Encoding"]; // set what we can uncompress #else [requestHeaders setObject:@"identity" forKey:@"Accept-Encoding"]; #endif if((_bodyStream=[request HTTPBodyStream])) { // is provided by a stream object [_bodyStream retain]; [_bodyStream setProperty:[NSNumber numberWithInt:0] forKey:NSStreamFileCurrentOffsetKey]; // rewind (if possible) [requestHeaders setObject:@"chunked" forKey:@"Transfer-Encoding"]; // we must send chunked because we don't know the length in advance } else if((body=[request HTTPBody])) { // fixed NSData object unsigned long bodyLength=[body length]; [requestHeaders setObject:[NSString stringWithFormat:@"%lu", bodyLength] forKey:@"Content-Length"]; _bodyStream=[[NSInputStream alloc] initWithData:body]; // prepare to send request body from NSData object } else [requestHeaders removeObjectForKey:@"Date"]; // must not send a Date: header if we have no body // [requestHeaders setObject:@"identity" forKey:@"TE"]; // what we accept in responses [requestHeaders removeObjectForKey:@"Keep-Alive"]; // HHTP 1.0 feature #if 1 NSLog(@"headers to send: %@", requestHeaders); #endif e=[requestHeaders keyEnumerator]; while((key=[e nextObject])) { // attributes NSString *val=[requestHeaders objectForKey:key]; #if 1 NSLog(@"sending %@: %@", key, val); #endif val=[val stringByAddingPercentEscapesUsingEncoding:NSISOLatin1StringEncoding]; [headerData appendData:[[NSString stringWithFormat:@"%@: %@\r\n", key, val] dataUsingEncoding:NSUTF8StringEncoding]]; } [headerData appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; #if 1 NSLog(@"header=%@\n", headerData, [[[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding] autorelease]); #endif _headerStream=[[NSInputStream alloc] initWithData:headerData]; // convert into a stream [headerData release]; [_headerStream open]; _shouldClose=(header=[requestHeaders objectForKey:@"Connection"]) && [header caseInsensitiveCompare:@"close"] == NSOrderedSame; // close after sending the request _sendChunked=(header=[requestHeaders objectForKey:@"Transfer-Encoding"]) && [header caseInsensitiveCompare:@"chunked"] == NSOrderedSame; [requestHeaders release]; // dictionary no more needed [_bodyStream open]; // if any #if 1 NSLog(@"ready to send"); #endif [_outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; // start handling output _lastChr=0; // prepare for reading response [_headerLine setString:@""]; } - (void) startLoading:(_NSHTTPURLProtocol *) proto; { // add to queue [[proto _connection] stopLoading:proto]; // remove from other queue (if any) [_requestQueue addObject:proto]; // append to our queue [proto _setConnection:self]; if(!_currentRequest) [self startLoadingNextRequest]; // is the first request we are waiting for } - (void) stopLoading:(_NSHTTPURLProtocol *) proto; { // remove from queue - may cancel/close connection if it is current request or simply stop notifications [proto _setConnection:nil]; if(_currentRequest == proto) { // FIXME: really cancel // at least stop from delivering any notifications to the client } [_requestQueue removeObject:proto]; } - (void) headersReceived { // end of header block received NSURLRequest *request=[_currentRequest request]; NSURL *url=[request URL]; NSString *header; #if 1 NSLog(@"headers received %@", self); #endif if([request HTTPShouldHandleCookies]) { // auto-process cookies if requested NSArray *cookies=[NSHTTPCookie cookiesWithResponseHeaderFields:_headers forURL:url]; [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies forURL:url mainDocumentURL:url]; } if((header=[_headers objectForKey:@"content-encoding"])) { // handle header compression if([header isEqualToString:@"gzip"]) NSLog(@"body is gzip compressed"); // we have the private method [NSData inflate] // we must do that here since we receive the stream here // NOTE: this may be a , separated list of encodings to be applied in sequence! // so we have to loop over [encoding componentsSeparatedByString:@","] - trimmed and compared case-insensitive } _contentLength = [[_headers objectForKey:@"content-length"] longLongValue]; _isChunked=(header=[_headers objectForKey:@"transfer-encoding"]) && [header caseInsensitiveCompare:@"chunked"] == NSOrderedSame; _willClose=(header=[_headers objectForKey:@"Connection"]) && [header caseInsensitiveCompare:@"close"] == NSOrderedSame; // will close after completing the request if(!_isChunked) // ??? must we notify (partial) response before we send any data ??? { NSHTTPURLResponse *response=[[[NSHTTPURLResponse alloc] _initWithURL:url headerFields:_headers andStatusCode:_statusCode] autorelease]; [_currentRequest didReceiveResponse:response]; } _readingBody = !(_statusCode/100 == 1 || _statusCode == 204 || _statusCode == 304 || [[request HTTPMethod] isEqualToString:@"HEAD"]); // decide if we expect to receive a body } - (void) bodyReceived { #if 1 NSLog(@"body received %@", self); #endif _readingBody=NO; // start over reading headers/trailer // apply MD5 checking [_headers objectForKey:@"Content-MD5"] // apply content-encoding (after MD5) if(!_isChunked) [self trailerReceived]; // there is no trailer if not chunked } - (void) trailerReceived { #if 1 NSLog(@"trailers received %@", self); #endif if(_isChunked) { // notify all headers after receiving trailer NSHTTPURLResponse *response=[[[NSHTTPURLResponse alloc] _initWithURL:[[_currentRequest request] URL] headerFields:_headers andStatusCode:_statusCode] autorelease]; [_currentRequest didReceiveResponse:response]; _isChunked=NO; } [_currentRequest didFinishLoading]; [_headers release]; // have been stored in NSHTTPURLResponse _headers=nil; [_currentRequest _setConnection:nil]; // has been processed [_currentRequest release]; _currentRequest=nil; if(_shouldClose) [self endOfUseability]; else if([_requestQueue count] > 0) [self startLoadingNextRequest]; // send next request from queue } - (void) processHeaderLine:(NSString *) line; { // process header line NSString *key, *val; NSRange colon; #if 1 NSLog(@"process header line %@", line); #endif if([line length] == 0) { // empty line received if(_isChunked) [self trailerReceived]; else if(_headers) [self headersReceived]; return; // else CRLF before header - be tolerant according to chapter 19.3 } if(!_headers) { // should be/must be the header line unsigned major, minor; if(sscanf([line UTF8String], "HTTP/%u.%u %u", &major, &minor, &_statusCode) == 3) { // response header line if(major != 1 || minor > 1) { [_currentRequest didFailWithError:[NSError errorWithDomain:@"Bad HTTP version received" code:0 userInfo:nil]]; } _headers=[[NSMutableDictionary alloc] initWithCapacity:10]; // start collecting headers #if 1 NSLog(@"Received response: %@", line); #endif return; // process next line } else { #if 1 NSLog(@"Received instead of header line: %@", line); #endif [_currentRequest didFailWithError:[NSError errorWithDomain:@"Invalid HTTP response" code:0 userInfo:nil]]; [self endOfUseability]; } return; // process next line } colon=[line rangeOfString:@":"]; if(colon.location == NSNotFound) return; // no colon found! Ignore to prevent DoS attacks... key=[[[line substringToIndex:colon.location] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] lowercaseString]; // convert key to all lowercase val=[[line substringFromIndex:colon.location+1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if([_headers objectForKey:key]) val=[NSString stringWithFormat:@"%@, %@", [_headers objectForKey:key], val]; // merge multiple headers with same key into a single one - comma separated [_headers setObject:val forKey:key]; if([key isEqualToString:@"warning"]) NSLog(@"HTTP Warning: %@", val); // print on console log #if 1 NSLog(@"header: %@:%@", key, val); #endif } - (void) handleInputEvent:(NSStreamEvent) event { switch(event) { case NSStreamEventOpenCompleted: { // ready to receive header #if 1 NSLog(@"HTTP input stream opened"); #endif return; } case NSStreamEventHasBytesAvailable: { unsigned char buffer[512]; unsigned maxLength=sizeof(buffer); int len; if(_readingBody && (!_isChunked || _chunkLength > 0)) { if(_isChunked && _chunkLength < maxLength) maxLength=_chunkLength; // limit to current chunk size if(_contentLength > 0 && _contentLength < maxLength) maxLength=_contentLength; // limit to expected size } else maxLength=1; // so that we don't miss the Content-Length: header entry even if it directly precedes the \r\n\r\nbody len=[_inputStream read:buffer maxLength:maxLength]; if(len == 0) return; // ignore (or when does this occur? - if EOF by server?) if(len <= 0) { NSDictionary *info=[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithUTF8String:strerror(errno)], @"Error", nil]; #if 1 NSLog(@"receive error %s", strerror(errno)); #endif [_currentRequest didFailWithError:[NSError errorWithDomain:@"receive error" code:errno userInfo:info]]; [self endOfUseability]; return; } #if 0 NSLog(@"received %d bytes", len); #endif if(_readingBody) { if(_isChunked && _chunkLength == 0) { // reading chunk size #if 0 NSLog(@"will process %02x into %@", buffer[0], _headerLine); #endif if(buffer[0] == '\r') return; // ignore CR if(buffer[0] == '\n') { // decode chunk length if([_headerLine length] > 0) { // there should follow a CRLF after the body resulting in a empty line NSScanner *sc=[NSScanner scannerWithString:_headerLine]; #if 1 NSLog(@"chunk length=%@", _headerLine); #endif _chunkLength=0; if(![sc scanHexInt:&_chunkLength]) // is hex coded (we even ignore an optional 0x) { NSLog(@"invalid chunk length %@", _headerLine); [_currentRequest didFailWithError:[NSError errorWithDomain:@"invalid chunk length" code:0 userInfo:0]]; [self endOfUseability]; return; } // may be followed by ; name=var if(_chunkLength == 0) [self bodyReceived]; // done reading body - continue with trailer [_headerLine setString:@""]; // has been processed } } else [_headerLine appendFormat:@"%c", buffer[0]&0xff]; // we should try to optimize that... return; } [_currentRequest didLoadData:[NSData dataWithBytes:buffer length:len]]; // notify if(_chunkLength > 0) _chunkLength-=len; // if this becomes 0 we are looking for the next chunk length if(_contentLength > 0) { _contentLength -= len; if(_contentLength == 0) { // we have received as much as expected [self bodyReceived]; } } return; } #if 0 NSLog(@"will process %02x _lastChr=%02x into %@", buffer[0], _lastChr, _headerLine); #endif if(_lastChr == '\n') { // first character in new line received if(buffer[0] != ' ' && buffer[0] != '\t') { // process what we have (even if empty) [self processHeaderLine:_headerLine]; [_headerLine setString:@""]; // has been processed } } if(buffer[0] == '\r') return; // ignore in headers if(buffer[0] != '\n') [_headerLine appendFormat:@"%c", buffer[0]&0xff]; // we should try to optimize that... _lastChr=buffer[0]; #if 0 NSLog(@"did process %02x _lastChr=%02x into", buffer[0], _lastChr, _headerLine); #endif return; } case NSStreamEventEndEncountered: { #if 1 NSLog(@"input connection closed by server: %@", self); #endif if(!_readingBody) [_currentRequest didFailWithError:[NSError errorWithDomain:@"incomplete header received" code:0 userInfo:nil]]; if([_headers objectForKey:@"content-length"]) { if(_contentLength > 0) { [_currentRequest didFailWithError:[NSError errorWithDomain:@"connection closed by server while receiving body" code:0 userInfo:nil]]; // we did not receive the announced contentLength } } else [self bodyReceived]; // implicit content length defined by EOF [self endOfUseability]; return; } default: break; } NSLog(@"An error %@ occurred on the event %08x of stream %@ of %@", [_inputStream streamError], event, _inputStream, self); [_currentRequest didFailWithError:[_inputStream streamError]]; [self endOfUseability]; } - (void) handleOutputEvent:(NSStreamEvent) event { // send header & body of current request (if any) /* e.g. POST /wiki/Spezial:Search HTTP/1.1 Host: de.wikipedia.org Content-Type: application/x-www-form-urlencoded Content-Length: 24 search=Katzen&go=Artikel <- body */ switch(event) { case NSStreamEventOpenCompleted: { // ready to send header #if 1 NSLog(@"HTTP output stream opened"); #endif return; } case NSStreamEventHasSpaceAvailable: { unsigned char buffer[512]; // max size of chunks to send to TCP subsystem to avoid blocking if(_headerStream) { // we are still sending the header if([_headerStream hasBytesAvailable]) { // send next part until done int len=[_headerStream read:buffer maxLength:sizeof(buffer)]; // read next block from stream if(len < 0) { NSDictionary *info=[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithUTF8String:strerror(errno)], @"Error"]; #if 1 NSLog(@"error while reading from HTTPHeader stream %s", strerror(errno)); #endif [_currentRequest didFailWithError:[NSError errorWithDomain:@"HTTPHeaderStream" code:errno userInfo:info]]; [self endOfUseability]; } else { [_outputStream write:buffer maxLength:len]; // send #if 1 NSLog(@"%d bytes header sent", len); #endif } return; // done sending next chunk } #if 1 NSLog(@"header completely sent"); #endif [_headerStream close]; [_headerStream release]; // done sending header, continue with body (if available) _headerStream=nil; } if(_bodyStream) { // we are still sending the body if([_bodyStream hasBytesAvailable]) // FIXME: if we send chunked this is not the correct indication and we should stall sending until new data becomes available { // send next part until done int len=[_bodyStream read:buffer maxLength:sizeof(buffer)]; // read next block from stream if(len < 0) { NSDictionary *info=[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithUTF8String:strerror(errno)], @"Error"]; #if 1 NSLog(@"error while reading from HTTPBody stream %s", strerror(errno)); #endif [_currentRequest didFailWithError:[NSError errorWithDomain:@"HTTPBodyStream" code:errno userInfo:info]]; [self endOfUseability]; return; // done } else { if(_sendChunked) { char chunkLen[32]; sprintf(chunkLen, "%x\r\n", len); [_outputStream write:(unsigned char *) chunkLen maxLength:strlen(chunkLen)]; // send length [_outputStream write:buffer maxLength:len]; // send what we have [_outputStream write:(unsigned char *) "\r\n" maxLength:2]; // and a CRLF #if 1 NSLog(@"chunk with %d bytes sent\nHeader: %s", len, chunkLen); #endif if(len != 0) return; // more to send (at least a 0-length header) } else { [_outputStream write:buffer maxLength:len]; // send what we have #if 1 NSLog(@"%d bytes body sent", len); #endif return; // done } } } #if 1 NSLog(@"body completely sent"); #endif [_bodyStream close]; // close body stream (if open) [_bodyStream release]; _bodyStream=nil; // we might send additional headers according to the protocol - but we have already sent them // this would only be useful if we want to allow the client to add/modify headers while generating the body stram // in that case we would have to mark all headers if they are sent before or after the chunked body and send only the minimum headers before } if(_shouldClose) { // we have announced Connection: close #if 1 NSLog(@"can't keep connection alive because we announced Connection: close"); #endif [_outputStream close]; [_outputStream release]; _outputStream=nil; } else [_outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; // unschedule until we send the next request return; } case NSStreamEventEndEncountered: if([_headerStream hasBytesAvailable]) [_currentRequest didFailWithError:[NSError errorWithDomain:@"connection closed by server while sending header" code:errno userInfo:nil]]; else if([_bodyStream hasBytesAvailable]) [_currentRequest didFailWithError:[NSError errorWithDomain:@"connection closed by server while sending body" code:errno userInfo:nil]]; else { #if 1 NSLog(@"server has disconnected - can't keep alive: %@", self); #endif } [_headerStream close]; [_headerStream release]; // done sending header _headerStream=nil; [_bodyStream close]; // close body stream (if open) [_bodyStream release]; _bodyStream=nil; [self endOfUseability]; return; default: break; } NSLog(@"An error %@ occurred on the event %08x of stream %@ of %@", [_outputStream streamError], event, _outputStream, self); [_currentRequest didFailWithError:[_outputStream streamError]]; [self endOfUseability]; } - (void) stream:(NSStream *) stream handleEvent:(NSStreamEvent) event { #if 0 NSLog(@"stream:%@ handleEvent:%x for:%@", stream, event, self); #endif if(stream == _inputStream) [self handleInputEvent:event]; else if(stream == _outputStream) [self handleOutputEvent:event]; } @end @implementation _NSHTTPURLProtocol + (BOOL) canInitWithRequest:(NSURLRequest *) request; { NSString *scheme = [[request URL] scheme]; return [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"];; } + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *) request; { NSURL *url=[request URL]; NSString *frag=[url fragment]; if([frag length] > 0) { // map different fragments to same base file NSString *s=[url absoluteString]; s=[s substringToIndex:[s length]-[frag length]]; // remove fragment return [[[NSURLRequest alloc] initWithURL:[NSURL URLWithString:s]] autorelease]; } return request; } - (void) dealloc; { #if 1 NSLog(@"dealloc %@", self); #endif [self stopLoading]; // if still running [super dealloc]; } - (NSString *) _uniqueKey; { // all requests with the same uniqueKey *can* be multiplexed over a kept-alive HTTP 1.1 channel NSURL *url=[_request URL]; return [NSString stringWithFormat:@"%@://%@:%@", [url scheme], [url host], [url port]]; // we can ignore user&password since HTTP does } - (void) _setConnection:(_NSHTTPSerialization *) c; { _connection=c; } // our shared connection - (_NSHTTPSerialization *) _connection; { return _connection; } - (void) _restartLoading { #if 1 NSLog(@"_restartLoading %@", self); #endif [_connection stopLoading:self]; // remove from current queue [[_NSHTTPSerialization serializerForProtocol:self] startLoading:self]; // and reschedule (on same or other some other queue) } - (void) startLoading; { static NSDictionary *methods; if(_connection) return; // already queued if(!methods) { // initialize methods=[[NSDictionary alloc] initWithObjectsAndKeys: self, @"HEAD", self, @"GET", self, @"POST", self, @"PUT", self, @"DELETE", self, @"TRACE", self, @"OPTIONS", self, @"CONNECT", nil]; } if(![methods objectForKey:[_request HTTPMethod]]) { // unknown method NSLog(@"Invalid HTTP Method: %@", _request); [_client URLProtocol:self didFailWithError:[NSError errorWithDomain:@"Invalid HTTP Method" code:0 userInfo:nil]]; return; } if(_cachedResponse) { /* FIXME: if we have a cached response and that one has an "Expires" date or a "Cache-Control" with a max-age parameter and it is still valid, directly respond from the cached response without contacting the server check with [_request cachePolicy] */ } [[_NSHTTPSerialization serializerForProtocol:self] startLoading:self]; // add our request to (new) queue } - (void) stopLoading; { [_connection stopLoading:self]; // interrupt and/or remove us from the queue } - (void) didFailWithError:(NSError *) error; { // forward to client as last message... [_client URLProtocol:self didFailWithError:error]; [(NSObject *)_client release]; _client=nil; } - (void) didLoadData:(NSData *) data; { // forward to client [_client URLProtocol:self didLoadData:data]; } - (void) didFinishLoading; { // forward to client as last message... [_client URLProtocolDidFinishLoading:self]; [(NSObject *)_client release]; _client=nil; } - (void) didReceiveResponse:(NSHTTPURLResponse *) response; { NSDictionary *headers=[response allHeaderFields]; NSString *loc; switch([response statusCode]) { case 100: return; // continue - ignore case 401: { // FIXME: read auth challenge from HTTP headers NSURLAuthenticationChallenge *chall=nil; [_client URLProtocol:self didReceiveAuthenticationChallenge:chall]; // retry or abort? return; } case 407: // notify client and add authentication info + repeat break; case 503: // retry // check if within reasonable future (retry-after) and then repeat break; // case 206: // optional case 304: [_client URLProtocol:self cachedResponseIsValid:_cachedResponse]; // will get data from cache [_client URLProtocol:self didLoadData:[_cachedResponse data]]; // and pass data from cache return; } if(([response statusCode]/100 == 3) && (loc=[headers objectForKey:@"Location"])) { // redirect NSURLRequest *request=[NSURLRequest requestWithURL:[NSURL URLWithString:loc relativeToURL:[_request URL]]]; // may be relative to current URL [_client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response]; // this may trigger a retry for the new request return; } // FIXME: there are response-headers that control how the response should be cached, i.e. translate into cacheStoragePolicy [_client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:0]; // notify client /* how do we generate these: - (void) URLProtocol:(NSURLProtocol *) proto didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *) chall; */ } - (void) scheduleInRunLoop:(NSRunLoop *) runLoop forMode:(NSString *) mode; { // FIXME // we should store each pair in a mutable array // and use them by HTTPSerialization on demand // but documentation says that it is possible to change them after download has started loading (and only delegate messages may arrive in the wrong thread) // therefore, we should just forward this to the HTTPSerialization objects // NOTE: latest documentation (10.7) appears to have removed this description } - (void) unscheduleFromRunLoop:(NSRunLoop *) runLoop forMode:(NSString *) mode; { // FIXME } @end @implementation _NSFTPURLProtocol + (BOOL) canInitWithRequest:(NSURLRequest *) request; { return NO; // FIXME: return [[[request URL] scheme] isEqualToString:@"ftp"]; } + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *) request; { return request; } - (void) scheduleInRunLoop:(NSRunLoop *) runLoop forMode:(NSString *) mode; { [_inputStream scheduleInRunLoop:runLoop forMode:mode]; [_outputStream scheduleInRunLoop:runLoop forMode:mode]; // should we save this list? } - (void) unscheduleFromRunLoop:(NSRunLoop *) runLoop forMode:(NSString *) mode; { [_inputStream removeFromRunLoop:runLoop forMode:mode]; [_outputStream removeFromRunLoop:runLoop forMode:mode]; } - (void) startLoading; { if(_cachedResponse) { // handle from cache } else { NSURL *url=[_request URL]; NSHost *host=[NSHost hostWithName:[url host]]; if(!host) host=[NSHost hostWithAddress:[url host]]; [NSStream getStreamsToHost:host port:[[url port] intValue] inputStream:&_inputStream outputStream:&_outputStream]; if(!_inputStream || !_outputStream) { // error [_client URLProtocol:self didFailWithError:[NSError errorWithDomain:@"can't connect" code:0 userInfo:nil]]; return; } [_inputStream retain]; [_outputStream retain]; [_inputStream setDelegate:self]; [_outputStream setDelegate:self]; // set socket options for ftps requests [_inputStream open]; [_outputStream open]; } } - (void) stopLoading; { if(_inputStream) { [_inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [_outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [_inputStream close]; [_outputStream close]; [_inputStream release]; [_outputStream release]; _inputStream=nil; _outputStream=nil; } } - (void) stream:(NSStream *) stream handleEvent:(NSStreamEvent) event { if(stream == _inputStream) { switch(event) { case NSStreamEventHasBytesAvailable: { NSLog(@"FTP input stream has bytes available"); // implement FTP protocol // [_client URLProtocol:self didLoadData:[NSData dataWithBytes:buffer length:len]]; // notify return; } case NSStreamEventEndEncountered: // can this occur in parallel to NSStreamEventHasBytesAvailable??? NSLog(@"FTP input stream did end"); [_client URLProtocolDidFinishLoading:self]; return; case NSStreamEventOpenCompleted: // prepare to receive header NSLog(@"FTP input stream opened"); return; default: break; } } else if(stream == _outputStream) { NSLog(@"An event occurred on the output stream."); // if successfully opened, send out FTP request header } NSLog(@"An error %@ occurred on the event %08x of stream %@ of %@", [stream streamError], event, stream, self); [_client URLProtocol:self didFailWithError:[stream streamError]]; _client=nil; } @end @implementation _NSFileURLProtocol + (BOOL) canInitWithRequest:(NSURLRequest *) request; { return [[[request URL] scheme] isEqualToString:@"file"]; } + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *) request; { return request; } - (void) startLoading; { // check for GET/PUT/DELETE etc so that we can also write to a file NSString *path=[[_request URL] path]; NSData *data=[NSData dataWithContentsOfFile:path /* options: error: - don't use that because it is based on self */]; NSURLResponse *r; NSString *enc=@"unknown"; if(!data) { [_client URLProtocol:self didFailWithError:[NSError errorWithDomain:@"can't load file" code:0 userInfo:[NSDictionary dictionaryWithObjectsAndKeys: [_request URL], @"URL", [[_request URL] path], @"path", nil] ]]; return; } r=[[NSURLResponse alloc] initWithURL:[_request URL] MIMEType:nil // should try to substitute from file extension expectedContentLength:[data length] textEncodingName:enc]; [_client URLProtocol:self didReceiveResponse:r cacheStoragePolicy:NSURLRequestUseProtocolCachePolicy]; if(!_stopLoading) [_client URLProtocol:self didLoadData:data]; if(!_stopLoading) [_client URLProtocolDidFinishLoading:self]; [r release]; } - (void) stopLoading; { _stopLoading=YES; } @end @implementation _NSAboutURLProtocol + (BOOL) canInitWithRequest:(NSURLRequest *) request; { return [[[request URL] scheme] isEqualToString:@"about"]; } + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *) request; { return request; } - (void) startLoading; { NSURLResponse *r; NSData *data=[NSData data]; // about provides no data // we could pass different content depending on the [url path] r=[[NSURLResponse alloc] initWithURL:[_request URL] MIMEType:@"text/html" expectedContentLength:[data length] textEncodingName:@"utf-8"]; [_client URLProtocol:self didReceiveResponse:r cacheStoragePolicy:NSURLRequestUseProtocolCachePolicy]; if(!_stopLoading) [_client URLProtocol:self didLoadData:data]; if(!_stopLoading) [_client URLProtocolDidFinishLoading:self]; [r release]; } - (void) stopLoading; { _stopLoading=YES; } @end @implementation _NSDataURLProtocol // RFC2397 http://www.ietf.org/rfc/rfc2397.txt /* examples data:,A%20brief%20note data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAw AAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFz ByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSp a/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJl ZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uis F81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PH hhx4dbgYKAAA7 data:text/plain;charset=iso-8859-7,%be%fg%be */ + (BOOL) canInitWithRequest:(NSURLRequest *) request; { // data:[][;base64], return [[[request URL] scheme] isEqualToString:@"data"]; // could also check for well-formed URL } + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *) request; { return request; } - (void) startLoading; { NSURLResponse *r; NSString *mime=@"text/plain"; NSString *encoding=@"US-ASCII"; NSData *data; NSString *path=[[_request URL] path]; NSRange comma=[path rangeOfString:@","]; NSEnumerator *types; NSString *type; BOOL base64=NO; if(comma.location == NSNotFound) { [_client URLProtocol:self didFailWithError:[NSError errorWithDomain:@"can't load data" code:0 userInfo:[NSDictionary dictionaryWithObjectsAndKeys: [_request URL], @"URL", [[_request URL] path], @"path", nil] ]]; return; } types=[[[path substringToIndex:comma.location] componentsSeparatedByString:@";"] objectEnumerator]; while((type=[types nextObject])) { if([type isEqualToString:@"base64"]) base64=YES; else if([type hasPrefix:@"charset="]) encoding=[type substringFromIndex:8]; else if([type length] > 0) mime=type; } path=[path substringFromIndex:comma.location+1]; // data after , if(base64) data=[[[NSData alloc] _initWithBase64String:path] autorelease]; // decode base64 (private extension of NSData) else data=[[path stringByReplacingPercentEscapesUsingEncoding:NSISOLatin1StringEncoding] dataUsingEncoding:NSUTF8StringEncoding]; r=[[NSURLResponse alloc] initWithURL:[_request URL] MIMEType:mime expectedContentLength:[data length] textEncodingName:encoding]; [_client URLProtocol:self didReceiveResponse:r cacheStoragePolicy:NSURLRequestUseProtocolCachePolicy]; if(!_stopLoading) [_client URLProtocol:self didLoadData:data]; if(!_stopLoading) [_client URLProtocolDidFinishLoading:self]; [r release]; } - (void) stopLoading; { _stopLoading=YES; } @end