mirror of
https://github.com/gnustep/libs-base.git
synced 2025-04-22 16:33:29 +00:00
2245 lines
55 KiB
Objective-C
2245 lines
55 KiB
Objective-C
/* Implementation for NSURLProtocol for GNUstep
|
|
Copyright (C) 2006 Software Foundation, Inc.
|
|
|
|
Written by: Richard Frith-Macdonald <rfm@gnu.org>
|
|
Date: 2006
|
|
Parts (FTP and About in particular) based on later code by Nikolaus Schaller
|
|
|
|
This file is part of the GNUstep Base Library.
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to the Free
|
|
Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
Boston, MA 02110 USA.
|
|
*/
|
|
|
|
#import "common.h"
|
|
|
|
//#define EXPOSE_NSURLProtocol_IVARS 1
|
|
#import "Foundation/NSError.h"
|
|
#import "Foundation/NSHost.h"
|
|
#import "Foundation/NSNotification.h"
|
|
#import "Foundation/NSRunLoop.h"
|
|
#import "Foundation/NSValue.h"
|
|
|
|
#if GS_HAVE_NSURLSESSION
|
|
#import "Foundation/NSURLSession.h"
|
|
#else
|
|
@class NSURLSessionTask;
|
|
#endif
|
|
|
|
#import "GSPrivate.h"
|
|
#import "GSURLPrivate.h"
|
|
#import "GNUstepBase/GSMime.h"
|
|
#import "GNUstepBase/GSTLS.h"
|
|
#import "GNUstepBase/NSData+GNUstepBase.h"
|
|
#import "GNUstepBase/NSStream+GNUstepBase.h"
|
|
#import "GNUstepBase/NSString+GNUstepBase.h"
|
|
#import "GNUstepBase/NSURL+GNUstepBase.h"
|
|
|
|
@interface GSMimeHeader (HTTPRequest)
|
|
- (void) addToBuffer: (NSMutableData*)buf
|
|
masking: (NSMutableData**)masked;
|
|
@end
|
|
|
|
/* Define to 1 for experimental (net yet working) compression support
|
|
*/
|
|
#ifdef USE_ZLIB
|
|
# undef USE_ZLIB
|
|
#endif
|
|
#define USE_ZLIB 0
|
|
|
|
|
|
#if USE_ZLIB
|
|
#if defined(HAVE_ZLIB_H)
|
|
#include <zlib.h>
|
|
|
|
static void*
|
|
zalloc(void *opaque, unsigned nitems, unsigned size)
|
|
{
|
|
return calloc(nitems, size);
|
|
}
|
|
static void
|
|
zfree(void *opaque, void *mem)
|
|
{
|
|
free(mem);
|
|
}
|
|
#else
|
|
# undef USE_ZLIB
|
|
# define USE_ZLIB 0
|
|
#endif
|
|
#endif
|
|
|
|
|
|
@interface NSURLProtocol (Debug)
|
|
- (NSString*) in;
|
|
- (NSString*) out;
|
|
@end
|
|
|
|
static void
|
|
debugRead(id handle, int len, const unsigned char *ptr)
|
|
{
|
|
int pos;
|
|
uint8_t *hex;
|
|
NSUInteger hl;
|
|
|
|
hl = ((len + 2) / 3) * 4;
|
|
hex = malloc(hl + 1);
|
|
hex[hl] = '\0';
|
|
GSPrivateEncodeBase64(ptr, (NSUInteger)len, hex);
|
|
|
|
for (pos = 0; pos < len; pos++)
|
|
{
|
|
if (0 == ptr[pos])
|
|
{
|
|
NSData *data;
|
|
char *esc;
|
|
|
|
data = [[NSData alloc] initWithBytesNoCopy: (void*)ptr
|
|
length: len
|
|
freeWhenDone: NO];
|
|
esc = [data escapedRepresentation: 0];
|
|
|
|
NSLog(@"Read for %p %@ of %d bytes (escaped) - '%s'\n<[%s]>",
|
|
handle, [handle in], len, esc, hex);
|
|
free(esc);
|
|
RELEASE(data);
|
|
free(hex);
|
|
return;
|
|
}
|
|
}
|
|
NSLog(@"Read for %p %@ of %d bytes - '%*.*s'\n<[%s]>",
|
|
handle, [handle in], len, len, len, ptr, hex);
|
|
free(hex);
|
|
}
|
|
static void
|
|
debugWrite(id handle, int len, const unsigned char *ptr)
|
|
{
|
|
int pos;
|
|
uint8_t *hex;
|
|
NSUInteger hl;
|
|
|
|
hl = ((len + 2) / 3) * 4;
|
|
hex = malloc(hl + 1);
|
|
hex[hl] = '\0';
|
|
GSPrivateEncodeBase64(ptr, (NSUInteger)len, hex);
|
|
|
|
for (pos = 0; pos < len; pos++)
|
|
{
|
|
if (0 == ptr[pos])
|
|
{
|
|
NSData *data;
|
|
char *esc;
|
|
|
|
data = [[NSData alloc] initWithBytesNoCopy: (void*)ptr
|
|
length: len
|
|
freeWhenDone: NO];
|
|
esc = [data escapedRepresentation: 0];
|
|
NSLog(@"Write for %p %@ of %d bytes (escaped) - '%s'\n<[%s]>",
|
|
handle, [handle out], len, esc, hex);
|
|
free(esc);
|
|
RELEASE(data);
|
|
free(hex);
|
|
return;
|
|
}
|
|
}
|
|
NSLog(@"Write for %p %@ of %d bytes - '%*.*s'\n<[%s]>",
|
|
handle, [handle out], len, len, len, ptr, hex);
|
|
free(hex);
|
|
}
|
|
|
|
@interface GSSocketStreamPair : NSObject
|
|
{
|
|
NSInputStream *ip;
|
|
NSOutputStream *op;
|
|
NSHost *host;
|
|
uint16_t port;
|
|
NSDate *expires;
|
|
BOOL ssl;
|
|
}
|
|
+ (void) purge: (NSNotification*)n;
|
|
- (void) cache: (NSDate*)when;
|
|
- (void) close;
|
|
- (NSDate*) expires;
|
|
- (id) initWithHost: (NSHost*)h port: (uint16_t)p forSSL: (BOOL)s;
|
|
- (NSInputStream*) inputStream;
|
|
- (NSOutputStream*) outputStream;
|
|
@end
|
|
|
|
@implementation GSSocketStreamPair
|
|
|
|
static NSMutableArray *pairCache = nil;
|
|
static NSLock *pairLock = nil;
|
|
|
|
+ (void) initialize
|
|
{
|
|
if (pairCache == nil)
|
|
{
|
|
/* No use trying to use a dictionary ... NSHost objects all hash
|
|
* to the same value.
|
|
*/
|
|
pairCache = [NSMutableArray new];
|
|
[[NSObject leakAt: &pairCache] release];
|
|
pairLock = [NSLock new];
|
|
[[NSObject leakAt: &pairLock] release];
|
|
/* Purge expired pairs at intervals.
|
|
*/
|
|
[[NSNotificationCenter defaultCenter] addObserver: self
|
|
selector: @selector(purge:)
|
|
name: @"GSHousekeeping" object: nil];
|
|
}
|
|
}
|
|
|
|
+ (void) purge: (NSNotification*)n
|
|
{
|
|
NSDate *now = [NSDate date];
|
|
unsigned count;
|
|
|
|
[pairLock lock];
|
|
count = [pairCache count];
|
|
while (count-- > 0)
|
|
{
|
|
GSSocketStreamPair *p = [pairCache objectAtIndex: count];
|
|
|
|
if ([[p expires] timeIntervalSinceDate: now] <= 0.0)
|
|
{
|
|
[pairCache removeObjectAtIndex: count];
|
|
}
|
|
}
|
|
[pairLock unlock];
|
|
}
|
|
|
|
- (void) cache: (NSDate*)when
|
|
{
|
|
NSTimeInterval ti = [when timeIntervalSinceNow];
|
|
|
|
if (ti <= 0.0)
|
|
{
|
|
[self close];
|
|
return;
|
|
}
|
|
NSAssert(ip != nil, NSGenericException);
|
|
if (ti > 120.0)
|
|
{
|
|
ASSIGN(expires, [NSDate dateWithTimeIntervalSinceNow: 120.0]);
|
|
}
|
|
else
|
|
{
|
|
ASSIGN(expires, when);
|
|
}
|
|
[pairLock lock];
|
|
[pairCache addObject: self];
|
|
[pairLock unlock];
|
|
}
|
|
|
|
- (void) close
|
|
{
|
|
[ip setDelegate: nil];
|
|
[op setDelegate: nil];
|
|
[ip removeFromRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[op removeFromRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[ip close];
|
|
[op close];
|
|
DESTROY(ip);
|
|
DESTROY(op);
|
|
}
|
|
|
|
- (void) dealloc
|
|
{
|
|
[self close];
|
|
DESTROY(host);
|
|
DESTROY(expires);
|
|
[super dealloc];
|
|
}
|
|
|
|
- (NSDate*) expires
|
|
{
|
|
return expires;
|
|
}
|
|
|
|
- (id) init
|
|
{
|
|
DESTROY(self);
|
|
return nil;
|
|
}
|
|
|
|
- (id) initWithHost: (NSHost*)h port: (uint16_t)p forSSL: (BOOL)s;
|
|
{
|
|
unsigned count;
|
|
NSDate *now;
|
|
|
|
now = [NSDate date];
|
|
[pairLock lock];
|
|
count = [pairCache count];
|
|
while (count-- > 0)
|
|
{
|
|
GSSocketStreamPair *pair = [pairCache objectAtIndex: count];
|
|
|
|
if ([pair->expires timeIntervalSinceDate: now] <= 0.0)
|
|
{
|
|
[pairCache removeObjectAtIndex: count];
|
|
}
|
|
else if (pair->port == p && pair->ssl == s && [pair->host isEqual: h])
|
|
{
|
|
/* Found a match ... remove from cache and return as self.
|
|
*/
|
|
DESTROY(self);
|
|
self = [pair retain];
|
|
[pairCache removeObjectAtIndex: count];
|
|
[pairLock unlock];
|
|
return self;
|
|
}
|
|
}
|
|
[pairLock unlock];
|
|
|
|
if ((self = [super init]) != nil)
|
|
{
|
|
[NSStream getStreamsToHost: host
|
|
port: port
|
|
inputStream: &ip
|
|
outputStream: &op];
|
|
if (ip == nil || op == nil)
|
|
{
|
|
DESTROY(self);
|
|
return nil;
|
|
}
|
|
ssl = s;
|
|
port = p;
|
|
host = [h retain];
|
|
[ip retain];
|
|
[op retain];
|
|
if (ssl == YES)
|
|
{
|
|
[ip setProperty: NSStreamSocketSecurityLevelNegotiatedSSL
|
|
forKey: NSStreamSocketSecurityLevelKey];
|
|
[op setProperty: NSStreamSocketSecurityLevelNegotiatedSSL
|
|
forKey: NSStreamSocketSecurityLevelKey];
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSInputStream*) inputStream
|
|
{
|
|
return ip;
|
|
}
|
|
|
|
- (NSOutputStream*) outputStream
|
|
{
|
|
return op;
|
|
}
|
|
|
|
@end
|
|
|
|
@interface _NSAboutURLProtocol : NSURLProtocol
|
|
@end
|
|
|
|
@interface _NSFTPURLProtocol : NSURLProtocol
|
|
@end
|
|
|
|
@interface _NSFileURLProtocol : NSURLProtocol
|
|
@end
|
|
|
|
@interface _NSHTTPURLProtocol : NSURLProtocol
|
|
<NSURLAuthenticationChallengeSender>
|
|
{
|
|
GSMimeParser *_parser; // Parser handling incoming data
|
|
unsigned _parseOffset; // Bytes of body loaded in parser.
|
|
float _version; // The HTTP version in use.
|
|
int _statusCode; // The HTTP status code returned.
|
|
NSInputStream *_body; // for sending the body
|
|
unsigned _writeOffset; // Request bytes written so far
|
|
NSData *_writeData; // Request data to write
|
|
NSData *_masked; // Request masked data
|
|
BOOL _complete;
|
|
BOOL _debug;
|
|
BOOL _isLoading;
|
|
BOOL _shouldClose;
|
|
NSURLAuthenticationChallenge *_challenge;
|
|
NSURLCredential *_credential;
|
|
NSHTTPURLResponse *_response;
|
|
id<GSLogDelegate> _logDelegate;
|
|
}
|
|
@end
|
|
|
|
@interface _NSHTTPSURLProtocol : _NSHTTPURLProtocol
|
|
@end
|
|
|
|
@interface _NSDataURLProtocol : NSURLProtocol
|
|
@end
|
|
|
|
static NSMutableArray *registered = nil;
|
|
static NSLock *regLock = nil;
|
|
static Class abstractClass = nil;
|
|
static Class placeholderClass = nil;
|
|
static NSURLProtocol *placeholder = nil;
|
|
|
|
@interface NSURLProtocolPlaceholder : NSURLProtocol
|
|
@end
|
|
@implementation NSURLProtocolPlaceholder
|
|
- (void) dealloc
|
|
{
|
|
if (self == placeholder)
|
|
{
|
|
[self retain];
|
|
return;
|
|
}
|
|
[super dealloc];
|
|
}
|
|
- (oneway void) release
|
|
{
|
|
/* In a multi-threaded environment we could have two threads release the
|
|
* class at the same time ... causing -dealloc to be called twice at the
|
|
* same time, so that we can get an exception as we try to decrement the
|
|
* retain count beyond zero. To avoid this we make the placeholder be a
|
|
* subclass whose -retain method prevents us even calling -dealoc in any
|
|
* normal circumstances.
|
|
*/
|
|
return;
|
|
}
|
|
@end
|
|
|
|
|
|
|
|
@implementation NSURLProtocol
|
|
|
|
#if GS_NONFRAGILE
|
|
{
|
|
@protected
|
|
#else
|
|
// Internal data storage
|
|
typedef struct {
|
|
#endif
|
|
|
|
NSInputStream *input;
|
|
NSOutputStream *output;
|
|
NSCachedURLResponse *cachedResponse;
|
|
id <NSURLProtocolClient> client;
|
|
NSURLRequest *request;
|
|
NSURLSessionTask *task;
|
|
NSString *in;
|
|
NSString *out;
|
|
#if USE_ZLIB
|
|
z_stream z; // context for decompress
|
|
BOOL compressing; // are we compressing?
|
|
BOOL decompressing; // are we decompressing?
|
|
NSData *compressed; // only partially decompressed
|
|
#endif
|
|
|
|
#if GS_NONFRAGILE
|
|
}
|
|
#define this self
|
|
#define inst o
|
|
#else
|
|
} Internal;
|
|
#define this ((Internal*)(self->_NSURLProtocolInternal))
|
|
#define inst ((Internal*)(o->_NSURLProtocolInternal))
|
|
#endif
|
|
|
|
|
|
+ (id) allocWithZone: (NSZone*)z
|
|
{
|
|
NSURLProtocol *o;
|
|
|
|
if ((self == abstractClass) && (z == 0 || z == NSDefaultMallocZone()))
|
|
{
|
|
/* Return a default placeholder instance to avoid the overhead of
|
|
* creating and destroying instances of the abstract class.
|
|
*/
|
|
o = placeholder;
|
|
}
|
|
else
|
|
{
|
|
/* Create and return an instance of the concrete subclass.
|
|
*/
|
|
o = (NSURLProtocol*)NSAllocateObject(self, 0, z);
|
|
}
|
|
return o;
|
|
}
|
|
|
|
+ (void) initialize
|
|
{
|
|
if (registered == nil)
|
|
{
|
|
abstractClass = [NSURLProtocol class];
|
|
placeholderClass = [NSURLProtocolPlaceholder class];
|
|
placeholder = (NSURLProtocol*)NSAllocateObject(placeholderClass, 0,
|
|
NSDefaultMallocZone());
|
|
[[NSObject leakAt: &placeholder] release];
|
|
registered = [NSMutableArray new];
|
|
[[NSObject leakAt: ®istered] release];
|
|
regLock = [NSLock new];
|
|
[[NSObject leakAt: ®Lock] release];
|
|
[self registerClass: [_NSHTTPURLProtocol class]];
|
|
[self registerClass: [_NSHTTPSURLProtocol class]];
|
|
[self registerClass: [_NSFTPURLProtocol class]];
|
|
[self registerClass: [_NSFileURLProtocol class]];
|
|
[self registerClass: [_NSAboutURLProtocol class]];
|
|
[self registerClass: [_NSDataURLProtocol class]];
|
|
}
|
|
}
|
|
|
|
+ (id) propertyForKey: (NSString *)key inRequest: (NSURLRequest *)request
|
|
{
|
|
return [request _propertyForKey: key];
|
|
}
|
|
|
|
+ (BOOL) registerClass: (Class)protocolClass
|
|
{
|
|
if ([protocolClass isSubclassOfClass: [NSURLProtocol class]] == YES)
|
|
{
|
|
[regLock lock];
|
|
[registered addObject: protocolClass];
|
|
[regLock unlock];
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
+ (void) setProperty: (id)value
|
|
forKey: (NSString *)key
|
|
inRequest: (NSMutableURLRequest *)request
|
|
{
|
|
[request _setProperty: value forKey: key];
|
|
}
|
|
|
|
+ (void) unregisterClass: (Class)protocolClass
|
|
{
|
|
[regLock lock];
|
|
[registered removeObjectIdenticalTo: protocolClass];
|
|
[regLock unlock];
|
|
}
|
|
|
|
- (NSCachedURLResponse *) cachedResponse
|
|
{
|
|
return this->cachedResponse;
|
|
}
|
|
|
|
- (id <NSURLProtocolClient>) client
|
|
{
|
|
return this->client;
|
|
}
|
|
|
|
- (void) dealloc
|
|
{
|
|
if (this != 0)
|
|
{
|
|
if (this->input != nil)
|
|
{
|
|
[this->input setDelegate: nil];
|
|
[this->output setDelegate: nil];
|
|
[this->input removeFromRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->output removeFromRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->input close];
|
|
[this->output close];
|
|
DESTROY(this->input);
|
|
DESTROY(this->output);
|
|
DESTROY(this->in);
|
|
DESTROY(this->out);
|
|
}
|
|
DESTROY(this->cachedResponse);
|
|
DESTROY(this->request);
|
|
#if GS_HAVE_NSURLSESSION
|
|
DESTROY(this->task);
|
|
#endif
|
|
DESTROY(this->client);
|
|
|
|
#if USE_ZLIB
|
|
if (this->compressing == YES)
|
|
{
|
|
deflateEnd(&this->z);
|
|
}
|
|
else if (this->decompressing == YES)
|
|
{
|
|
inflateEnd(&this->z);
|
|
}
|
|
DESTROY(this->compressed);
|
|
#endif
|
|
#if !GS_NONFRAGILE
|
|
NSZoneFree([self zone], this);
|
|
_NSURLProtocolInternal = 0;
|
|
#endif
|
|
}
|
|
[super dealloc];
|
|
}
|
|
|
|
- (NSString*) description
|
|
{
|
|
return [NSString stringWithFormat:@"%@ %@",
|
|
[super description], this ? (id)this->request : nil];
|
|
}
|
|
|
|
- (id) init
|
|
{
|
|
if ((self = [super init]) != nil)
|
|
{
|
|
#if !GS_NONFRAGILE
|
|
Class c = object_getClass(self);
|
|
|
|
if (c != abstractClass && c != placeholderClass)
|
|
{
|
|
_NSURLProtocolInternal = NSZoneCalloc([self zone],
|
|
1, sizeof(Internal));
|
|
}
|
|
#endif
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (id) initWithRequest: (NSURLRequest*)_request
|
|
cachedResponse: (NSCachedURLResponse*)_cachedResponse
|
|
client: (id <NSURLProtocolClient>)_client
|
|
{
|
|
Class c = object_getClass(self);
|
|
|
|
if (c == abstractClass || c == placeholderClass)
|
|
{
|
|
unsigned count;
|
|
|
|
DESTROY(self);
|
|
[regLock lock];
|
|
count = [registered count];
|
|
while (count-- > 0)
|
|
{
|
|
Class proto = [registered objectAtIndex: count];
|
|
|
|
if ([proto canInitWithRequest: _request] == YES)
|
|
{
|
|
self = [proto alloc];
|
|
break;
|
|
}
|
|
}
|
|
[regLock unlock];
|
|
return [self initWithRequest: _request
|
|
cachedResponse: _cachedResponse
|
|
client: _client];
|
|
}
|
|
if ((self = [self init]) != nil)
|
|
{
|
|
this->request = [_request copy];
|
|
this->cachedResponse = RETAIN(_cachedResponse);
|
|
if (nil == _client)
|
|
{
|
|
_client = [[self class] _ProtocolClient];
|
|
}
|
|
this->client = RETAIN(_client);
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (instancetype) initWithTask: (NSURLSessionTask*)_task
|
|
cachedResponse: (NSCachedURLResponse*)_cachedResponse
|
|
client: (id<NSURLProtocolClient>)_client
|
|
{
|
|
#if GS_HAVE_NSURLSESSION
|
|
if (nil != (self = [self initWithRequest: [_task currentRequest]
|
|
cachedResponse: _cachedResponse
|
|
client: _client]))
|
|
{
|
|
ASSIGN(this->task, _task);
|
|
}
|
|
#else
|
|
DESTROY(self);
|
|
#endif
|
|
return self;
|
|
}
|
|
|
|
- (NSURLRequest *) request
|
|
{
|
|
return this->request;
|
|
}
|
|
|
|
- (NSURLSessionTask*) task
|
|
{
|
|
return this->task;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation NSURLProtocol (Debug)
|
|
- (NSString*) in
|
|
{
|
|
return (this) ? (this->in) : nil;
|
|
}
|
|
- (NSString*) out
|
|
{
|
|
return (this) ? (this->out) : nil;
|
|
}
|
|
@end
|
|
|
|
@implementation NSURLProtocol (Private)
|
|
|
|
+ (Class) _classToHandleRequest:(NSURLRequest *)request
|
|
{
|
|
Class protoClass = nil;
|
|
int count;
|
|
[regLock lock];
|
|
|
|
count = [registered count];
|
|
while (count-- > 0)
|
|
{
|
|
Class proto = [registered objectAtIndex: count];
|
|
|
|
if ([proto canInitWithRequest: request] == YES)
|
|
{
|
|
protoClass = proto;
|
|
break;
|
|
}
|
|
}
|
|
[regLock unlock];
|
|
return protoClass;
|
|
}
|
|
|
|
/* Internal method to return a client to handle callbacks if the protocol
|
|
* is initialised without one.
|
|
*/
|
|
+ (id<NSURLProtocolClient>) _ProtocolClient
|
|
{
|
|
return nil;
|
|
}
|
|
@end
|
|
|
|
@implementation NSURLProtocol (Subclassing)
|
|
|
|
+ (BOOL) canInitWithRequest: (NSURLRequest *)request
|
|
{
|
|
[self subclassResponsibility: _cmd];
|
|
return NO;
|
|
}
|
|
|
|
+ (BOOL) canInitWithTask: (NSURLSessionTask*)task
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
+ (NSURLRequest *) canonicalRequestForRequest: (NSURLRequest *)request
|
|
{
|
|
return request;
|
|
}
|
|
|
|
+ (BOOL) requestIsCacheEquivalent: (NSURLRequest *)a
|
|
toRequest: (NSURLRequest *)b
|
|
{
|
|
a = [self canonicalRequestForRequest: a];
|
|
b = [self canonicalRequestForRequest: b];
|
|
return [a isEqual: b];
|
|
}
|
|
|
|
- (void) startLoading
|
|
{
|
|
[self subclassResponsibility: _cmd];
|
|
}
|
|
|
|
- (void) stopLoading
|
|
{
|
|
[self subclassResponsibility: _cmd];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@implementation _NSHTTPURLProtocol
|
|
|
|
+ (BOOL) canInitWithRequest: (NSURLRequest*)request
|
|
{
|
|
return [[[request URL] scheme] isEqualToString: @"http"];
|
|
}
|
|
|
|
+ (NSURLRequest*) canonicalRequestForRequest: (NSURLRequest*)request
|
|
{
|
|
return request;
|
|
}
|
|
|
|
- (void) cancelAuthenticationChallenge: (NSURLAuthenticationChallenge*)c
|
|
{
|
|
if (c == _challenge)
|
|
{
|
|
DESTROY(_challenge); // We should cancel the download
|
|
}
|
|
}
|
|
|
|
- (void) continueWithoutCredentialForAuthenticationChallenge:
|
|
(NSURLAuthenticationChallenge*)c
|
|
{
|
|
if (c == _challenge)
|
|
{
|
|
DESTROY(_credential); // We download the challenge page
|
|
}
|
|
}
|
|
|
|
- (void) dealloc
|
|
{
|
|
[_parser release]; // received headers
|
|
[_body release]; // for sending the body
|
|
[_response release];
|
|
[_credential release];
|
|
DESTROY(_writeData);
|
|
DESTROY(_masked);
|
|
[super dealloc];
|
|
}
|
|
|
|
- (void) startLoading
|
|
{
|
|
static NSDictionary *methods = nil;
|
|
|
|
_debug = GSDebugSet(@"NSURLProtocol");
|
|
if (YES == [this->request _debug])
|
|
{
|
|
_debug = YES;
|
|
}
|
|
if (_debug)
|
|
{
|
|
_logDelegate = [this->request _debugLogDelegate];
|
|
}
|
|
|
|
if (methods == nil)
|
|
{
|
|
methods = [[NSDictionary alloc] initWithObjectsAndKeys:
|
|
self, @"HEAD",
|
|
self, @"GET",
|
|
self, @"POST",
|
|
self, @"PUT",
|
|
self, @"DELETE",
|
|
self, @"TRACE",
|
|
self, @"OPTIONS",
|
|
self, @"CONNECT",
|
|
nil];
|
|
}
|
|
if ([methods objectForKey: [this->request HTTPMethod]] == nil)
|
|
{
|
|
NSLog(@"Invalid HTTP Method: %@", this->request);
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self didFailWithError:
|
|
[NSError errorWithDomain: @"Invalid HTTP Method"
|
|
code: 0
|
|
userInfo: nil]];
|
|
return;
|
|
}
|
|
if (_isLoading == YES)
|
|
{
|
|
NSLog(@"startLoading when load in progress");
|
|
return;
|
|
}
|
|
|
|
_statusCode = 0; /* No status returned yet. */
|
|
_isLoading = YES;
|
|
_complete = NO;
|
|
|
|
/* Perform a redirect if the path is empty.
|
|
* As per MacOs-X documentation.
|
|
*/
|
|
if ([[[this->request URL] fullPath] length] == 0)
|
|
{
|
|
NSString *s = [[this->request URL] absoluteString];
|
|
NSURL *url;
|
|
|
|
if ([s rangeOfString: @"?"].length > 0)
|
|
{
|
|
s = [s stringByReplacingString: @"?" withString: @"/?"];
|
|
}
|
|
else if ([s rangeOfString: @"#"].length > 0)
|
|
{
|
|
s = [s stringByReplacingString: @"#" withString: @"/#"];
|
|
}
|
|
else
|
|
{
|
|
s = [s stringByAppendingString: @"/"];
|
|
}
|
|
url = [NSURL URLWithString: s];
|
|
if (url == nil)
|
|
{
|
|
NSError *e;
|
|
|
|
e = [NSError errorWithDomain: @"Invalid redirect request"
|
|
code: 0
|
|
userInfo: nil];
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self
|
|
didFailWithError: e];
|
|
}
|
|
else
|
|
{
|
|
NSMutableURLRequest *r;
|
|
|
|
r = [[this->request mutableCopy] autorelease];
|
|
[r setURL: url];
|
|
[this->client URLProtocol: self
|
|
wasRedirectedToRequest: r
|
|
redirectResponse: nil];
|
|
}
|
|
if (NO == _isLoading)
|
|
{
|
|
return; // Loading cancelled
|
|
}
|
|
if (nil != this->input)
|
|
{
|
|
return; // Following redirection
|
|
}
|
|
// Fall through to continue original connect.
|
|
}
|
|
|
|
if (0 && this->cachedResponse)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
NSURL *url = [this->request URL];
|
|
NSHost *host = [NSHost hostWithName: [url host]];
|
|
int port = [[url port] intValue];
|
|
|
|
_parseOffset = 0;
|
|
DESTROY(_parser);
|
|
|
|
if (host == nil)
|
|
{
|
|
host = [NSHost hostWithAddress: [url host]]; // try dotted notation
|
|
}
|
|
if (host == nil)
|
|
{
|
|
host = [NSHost hostWithAddress: @"127.0.0.1"]; // final default
|
|
}
|
|
if (port == 0)
|
|
{
|
|
// default if not specified
|
|
port = [[url scheme] isEqualToString: @"https"] ? 443 : 80;
|
|
}
|
|
|
|
[NSStream getStreamsToHost: host
|
|
port: port
|
|
inputStream: &this->input
|
|
outputStream: &this->output];
|
|
if (!this->input || !this->output)
|
|
{
|
|
if (_debug == YES)
|
|
{
|
|
NSLog(@"%@ did not create streams for %@:%@",
|
|
self, host, [url port]);
|
|
}
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self didFailWithError:
|
|
[NSError errorWithDomain: @"can't connect" code: 0 userInfo:
|
|
[NSDictionary dictionaryWithObjectsAndKeys:
|
|
url, @"NSErrorFailingURLKey",
|
|
host, @"NSErrorFailingURLStringKey",
|
|
@"can't find host", @"NSLocalizedDescription",
|
|
nil]]];
|
|
return;
|
|
}
|
|
[this->input retain];
|
|
[this->output retain];
|
|
if ([[url scheme] isEqualToString: @"https"] == YES)
|
|
{
|
|
static NSArray *keys;
|
|
NSUInteger count;
|
|
|
|
[this->input setProperty: NSStreamSocketSecurityLevelNegotiatedSSL
|
|
forKey: NSStreamSocketSecurityLevelKey];
|
|
[this->output setProperty: NSStreamSocketSecurityLevelNegotiatedSSL
|
|
forKey: NSStreamSocketSecurityLevelKey];
|
|
if (nil == keys)
|
|
{
|
|
keys = [[NSArray alloc] initWithObjects:
|
|
GSTLSCAFile,
|
|
GSTLSCertificateFile,
|
|
GSTLSCertificateKeyFile,
|
|
GSTLSCertificateKeyPassword,
|
|
GSTLSDebug,
|
|
GSTLSIssuers,
|
|
GSTLSOwners,
|
|
GSTLSPriority,
|
|
GSTLSRemoteHosts,
|
|
GSTLSRevokeFile,
|
|
GSTLSServerName,
|
|
GSTLSVerify,
|
|
nil];
|
|
}
|
|
count = [keys count];
|
|
while (count-- > 0)
|
|
{
|
|
NSString *key = [keys objectAtIndex: count];
|
|
NSString *str = [this->request _propertyForKey: key];
|
|
|
|
if (nil != str)
|
|
{
|
|
[this->output setProperty: str forKey: key];
|
|
}
|
|
}
|
|
/* If there is no value set for the server name, and the host in the
|
|
* URL is a domain name rather than an address, we use that.
|
|
*/
|
|
if (nil == [this->output propertyForKey: GSTLSServerName])
|
|
{
|
|
NSString *host = [url host];
|
|
unichar c;
|
|
|
|
c = [host length] == 0 ? 0 : [host characterAtIndex: 0];
|
|
if (c != 0 && c != ':' && !isdigit(c))
|
|
{
|
|
[this->output setProperty: host forKey: GSTLSServerName];
|
|
}
|
|
}
|
|
if (_debug) [this->output setProperty: @"YES" forKey: GSTLSDebug];
|
|
}
|
|
[this->input setDelegate: self];
|
|
[this->output setDelegate: self];
|
|
[this->input scheduleInRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->output scheduleInRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->input open];
|
|
[this->output open];
|
|
}
|
|
}
|
|
|
|
- (void) stopLoading
|
|
{
|
|
if (_debug == YES)
|
|
{
|
|
NSLog(@"%@ stopLoading", self);
|
|
}
|
|
_isLoading = NO;
|
|
DESTROY(_writeData);
|
|
DESTROY(_masked);
|
|
if (this->input != nil)
|
|
{
|
|
[this->input setDelegate: nil];
|
|
[this->output setDelegate: nil];
|
|
[this->input removeFromRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->output removeFromRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->input close];
|
|
[this->output close];
|
|
DESTROY(this->input);
|
|
DESTROY(this->output);
|
|
}
|
|
}
|
|
|
|
- (void) _didLoad: (NSData*)d
|
|
{
|
|
[this->client URLProtocol: self didLoadData: d];
|
|
}
|
|
|
|
- (void) _got: (NSStream*)stream
|
|
{
|
|
unsigned char buffer[BUFSIZ*64];
|
|
int readCount;
|
|
NSError *e;
|
|
NSData *d;
|
|
BOOL wasInHeaders = NO;
|
|
|
|
readCount = [(NSInputStream *)stream read: buffer
|
|
maxLength: sizeof(buffer)];
|
|
if (readCount < 0)
|
|
{
|
|
if ([stream streamStatus] == NSStreamStatusError)
|
|
{
|
|
e = [stream streamError];
|
|
if (_debug)
|
|
{
|
|
NSLog(@"%@ receive error %@", self, e);
|
|
}
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self didFailWithError: e];
|
|
}
|
|
return;
|
|
}
|
|
if (_debug)
|
|
{
|
|
if (NO == [_logDelegate getBytes: buffer
|
|
ofLength: readCount
|
|
byHandle: self])
|
|
{
|
|
debugRead(self, readCount, buffer);
|
|
}
|
|
}
|
|
|
|
if (_parser == nil)
|
|
{
|
|
_parser = [GSMimeParser new];
|
|
[_parser setIsHttp];
|
|
}
|
|
wasInHeaders = [_parser isInHeaders];
|
|
d = [NSData dataWithBytes: buffer length: readCount];
|
|
if ([_parser parse: d] == NO && (_complete = [_parser isComplete]) == NO)
|
|
{
|
|
if (_debug)
|
|
{
|
|
NSLog(@"%@ HTTP parse failure - %@", self, _parser);
|
|
}
|
|
e = [NSError errorWithDomain: @"parse error"
|
|
code: 0
|
|
userInfo: nil];
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self didFailWithError: e];
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
BOOL isInHeaders = [_parser isInHeaders];
|
|
GSMimeDocument *document = [_parser mimeDocument];
|
|
unsigned bodyLength;
|
|
|
|
_complete = [_parser isComplete];
|
|
if (YES == wasInHeaders && NO == isInHeaders)
|
|
{
|
|
GSMimeHeader *info;
|
|
int len = -1;
|
|
NSString *ct;
|
|
NSString *st;
|
|
NSString *s;
|
|
|
|
info = [document headerNamed: @"http"];
|
|
|
|
_version = [[info value] floatValue];
|
|
if (_version < 1.1)
|
|
{
|
|
_shouldClose = YES;
|
|
}
|
|
else if ((s = [[document headerNamed: @"connection"] value]) != nil
|
|
&& [s caseInsensitiveCompare: @"close"] == NSOrderedSame)
|
|
{
|
|
_shouldClose = YES;
|
|
}
|
|
else
|
|
{
|
|
_shouldClose = NO; // Keep connection alive.
|
|
}
|
|
|
|
s = [info objectForKey: NSHTTPPropertyStatusCodeKey];
|
|
_statusCode = [s intValue];
|
|
|
|
s = [[document headerNamed: @"content-length"] value];
|
|
if ([s length] > 0)
|
|
{
|
|
len = [s intValue];
|
|
}
|
|
|
|
s = [info objectForKey: NSHTTPPropertyStatusReasonKey];
|
|
|
|
/* Should use this?
|
|
NSString *enc;
|
|
enc = [[document headerNamed: @"content-transfer-encoding"] value];
|
|
if (enc == nil)
|
|
{
|
|
enc = [[document headerNamed: @"transfer-encoding"] value];
|
|
}
|
|
*/
|
|
|
|
info = [document headerNamed: @"content-type"];
|
|
ct = [document contentType];
|
|
st = [document contentSubtype];
|
|
if (ct && st)
|
|
{
|
|
ct = [ct stringByAppendingFormat: @"/%@", st];
|
|
}
|
|
else
|
|
{
|
|
ct = nil;
|
|
}
|
|
_response = [[NSHTTPURLResponse alloc]
|
|
initWithURL: [this->request URL]
|
|
MIMEType: ct
|
|
expectedContentLength: len
|
|
textEncodingName: [info parameterForKey: @"charset"]];
|
|
[_response _setStatusCode: _statusCode text: s];
|
|
[document deleteHeaderNamed: @"http"];
|
|
[_response _setHeaders: [document allHeaders]];
|
|
|
|
if (_statusCode == 204 || _statusCode == 304)
|
|
{
|
|
_complete = YES; // No body expected.
|
|
}
|
|
else if (_complete == NO && [d length] == 0)
|
|
{
|
|
_complete = YES; // Had EOF ... terminate
|
|
}
|
|
|
|
if (_statusCode == 401)
|
|
{
|
|
/* This is an authentication challenge, so we keep reading
|
|
* until the challenge is complete, then try to deal with it.
|
|
*/
|
|
}
|
|
else if (_statusCode >= 300 && _statusCode < 400)
|
|
{
|
|
NSURL *url;
|
|
|
|
NS_DURING
|
|
s = [[document headerNamed: @"location"] value];
|
|
url = [NSURL URLWithString: s];
|
|
NS_HANDLER
|
|
url = nil;
|
|
NS_ENDHANDLER
|
|
|
|
if (url == nil)
|
|
{
|
|
NSError *e;
|
|
|
|
e = [NSError errorWithDomain: @"Invalid redirect request"
|
|
code: 0
|
|
userInfo: nil];
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self
|
|
didFailWithError: e];
|
|
}
|
|
else
|
|
{
|
|
NSMutableURLRequest *r;
|
|
|
|
r = AUTORELEASE([this->request mutableCopy]);
|
|
[r setURL: url];
|
|
[this->client URLProtocol: self
|
|
wasRedirectedToRequest: r
|
|
redirectResponse: _response];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NSURLCacheStoragePolicy policy;
|
|
|
|
/* Get cookies from the response and accept them into
|
|
* shared storage if policy permits
|
|
*/
|
|
if ([this->request HTTPShouldHandleCookies] == YES
|
|
&& [_response isKindOfClass: [NSHTTPURLResponse class]] == YES)
|
|
{
|
|
NSDictionary *hdrs;
|
|
NSArray *cookies;
|
|
NSURL *url;
|
|
|
|
url = [_response URL];
|
|
hdrs = [_response allHeaderFields];
|
|
cookies = [NSHTTPCookie cookiesWithResponseHeaderFields: hdrs
|
|
forURL: url];
|
|
[[NSHTTPCookieStorage sharedHTTPCookieStorage]
|
|
setCookies: cookies
|
|
forURL: url
|
|
mainDocumentURL: [this->request mainDocumentURL]];
|
|
}
|
|
|
|
/* Tell the client that we have a response and how
|
|
* it should be cached.
|
|
*/
|
|
policy = [this->request cachePolicy];
|
|
if (policy
|
|
== (NSURLCacheStoragePolicy)NSURLRequestUseProtocolCachePolicy)
|
|
{
|
|
if ([self isKindOfClass: [_NSHTTPSURLProtocol class]] == YES)
|
|
{
|
|
/* For HTTPS we should not allow caching unless the
|
|
* request explicitly wants it.
|
|
*/
|
|
policy = NSURLCacheStorageNotAllowed;
|
|
}
|
|
else
|
|
{
|
|
/* For HTTP we allow caching unless the request
|
|
* specifically denies it.
|
|
*/
|
|
policy = NSURLCacheStorageAllowed;
|
|
}
|
|
}
|
|
[this->client URLProtocol: self
|
|
didReceiveResponse: _response
|
|
cacheStoragePolicy: policy];
|
|
}
|
|
|
|
#if USE_ZLIB
|
|
s = [[document headerNamed: @"content-encoding"] value];
|
|
if ([s isEqualToString: @"gzip"] || [s isEqualToString: @"x-gzip"])
|
|
{
|
|
this->decompressing = YES;
|
|
this->z.opaque = 0;
|
|
this->z.zalloc = zalloc;
|
|
this->z.zfree = zfree;
|
|
this->z.next_in = 0;
|
|
this->z.avail_in = 0;
|
|
inflateInit2(&this->z, 1); // FIXME
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if (_complete == YES)
|
|
{
|
|
if (_statusCode == 401)
|
|
{
|
|
NSURLProtectionSpace *space;
|
|
NSString *hdr;
|
|
NSURL *url;
|
|
int failures = 0;
|
|
|
|
/* This was an authentication challenge.
|
|
*/
|
|
hdr = [[document headerNamed: @"WWW-Authenticate"] value];
|
|
url = [this->request URL];
|
|
space = [GSHTTPAuthentication
|
|
protectionSpaceForAuthentication: hdr requestURL: url];
|
|
DESTROY(_credential);
|
|
if (space != nil)
|
|
{
|
|
/* Create credential from user and password
|
|
* stored in the URL.
|
|
* Returns nil if we have no username or password.
|
|
*/
|
|
_credential = [[NSURLCredential alloc]
|
|
initWithUser: [url user]
|
|
password: [url password]
|
|
persistence: NSURLCredentialPersistenceForSession];
|
|
if (_credential == nil)
|
|
{
|
|
/* No credential from the URL, so we try using the
|
|
* default credential for the protection space.
|
|
*/
|
|
ASSIGN(_credential,
|
|
[[NSURLCredentialStorage sharedCredentialStorage]
|
|
defaultCredentialForProtectionSpace: space]);
|
|
}
|
|
}
|
|
|
|
if (_challenge != nil)
|
|
{
|
|
/* The failure count is incremented if we have just
|
|
* tried a request in the same protection space.
|
|
*/
|
|
if (YES == [[_challenge protectionSpace] isEqual: space])
|
|
{
|
|
failures = [_challenge previousFailureCount] + 1;
|
|
}
|
|
}
|
|
else if ([this->request valueForHTTPHeaderField:@"Authorization"])
|
|
{
|
|
/* Our request had an authorization header, so we should
|
|
* count that as a failure or we wouldn't have been
|
|
* challenged.
|
|
*/
|
|
failures = 1;
|
|
}
|
|
DESTROY(_challenge);
|
|
|
|
_challenge = [[NSURLAuthenticationChallenge alloc]
|
|
initWithProtectionSpace: space
|
|
proposedCredential: _credential
|
|
previousFailureCount: failures
|
|
failureResponse: _response
|
|
error: nil
|
|
sender: self];
|
|
|
|
/* Allow the client to control the credential we send
|
|
* or whether we actually send at all.
|
|
*/
|
|
[this->client URLProtocol: self
|
|
didReceiveAuthenticationChallenge: _challenge];
|
|
|
|
if (_challenge == nil)
|
|
{
|
|
NSError *e;
|
|
|
|
/* The client cancelled the authentication challenge
|
|
* so we must cancel the download.
|
|
*/
|
|
e = [NSError errorWithDomain: @"Authentication cancelled"
|
|
code: 0
|
|
userInfo: nil];
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self
|
|
didFailWithError: e];
|
|
}
|
|
else
|
|
{
|
|
NSString *auth = nil;
|
|
|
|
if (_credential != nil)
|
|
{
|
|
GSHTTPAuthentication *authentication;
|
|
|
|
/* Get information about basic or
|
|
* digest authentication.
|
|
*/
|
|
authentication = [GSHTTPAuthentication
|
|
authenticationWithCredential: _credential
|
|
inProtectionSpace: space];
|
|
|
|
/* Generate authentication header value for the
|
|
* authentication type in the challenge.
|
|
*/
|
|
auth = [authentication
|
|
authorizationForAuthentication: hdr
|
|
method: [this->request HTTPMethod]
|
|
path: [url fullPath]];
|
|
}
|
|
|
|
if (auth == nil)
|
|
{
|
|
NSURLCacheStoragePolicy policy;
|
|
|
|
/* We have no authentication credentials so we
|
|
* treat this as a download of the challenge page.
|
|
*/
|
|
|
|
/* Tell the client that we have a response and how
|
|
* it should be cached.
|
|
*/
|
|
policy = [this->request cachePolicy];
|
|
if (policy == (NSURLCacheStoragePolicy)
|
|
NSURLRequestUseProtocolCachePolicy)
|
|
{
|
|
if ([self isKindOfClass: [_NSHTTPSURLProtocol class]])
|
|
{
|
|
/* For HTTPS we should not allow caching unless
|
|
* the request explicitly wants it.
|
|
*/
|
|
policy = NSURLCacheStorageNotAllowed;
|
|
}
|
|
else
|
|
{
|
|
/* For HTTP we allow caching unless the request
|
|
* specifically denies it.
|
|
*/
|
|
policy = NSURLCacheStorageAllowed;
|
|
}
|
|
}
|
|
[this->client URLProtocol: self
|
|
didReceiveResponse: _response
|
|
cacheStoragePolicy: policy];
|
|
/* Fall through to code providing page data.
|
|
*/
|
|
}
|
|
else
|
|
{
|
|
NSMutableURLRequest *r;
|
|
|
|
/* To answer the authentication challenge,
|
|
* we must retry with a modified request and
|
|
* with the cached response cleared.
|
|
*/
|
|
r = [this->request mutableCopy];
|
|
[r setValue: auth
|
|
forHTTPHeaderField: @"Authorization"];
|
|
[self stopLoading];
|
|
RELEASE(this->request);
|
|
this->request = r;
|
|
DESTROY(this->cachedResponse);
|
|
[self startLoading];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
[this->input removeFromRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->output removeFromRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
if (_shouldClose == YES)
|
|
{
|
|
[this->input setDelegate: nil];
|
|
[this->output setDelegate: nil];
|
|
[this->input close];
|
|
[this->output close];
|
|
DESTROY(this->input);
|
|
DESTROY(this->output);
|
|
}
|
|
|
|
/*
|
|
* Tell superclass that we have successfully loaded the data
|
|
* (as long as we haven't had the load terminated by the client).
|
|
*/
|
|
if (_isLoading == YES)
|
|
{
|
|
d = [_parser data];
|
|
bodyLength = [d length];
|
|
if (bodyLength > _parseOffset)
|
|
{
|
|
if (_parseOffset > 0)
|
|
{
|
|
d = [d subdataWithRange:
|
|
NSMakeRange(_parseOffset, bodyLength - _parseOffset)];
|
|
}
|
|
_parseOffset = bodyLength;
|
|
[self _didLoad: d];
|
|
}
|
|
|
|
/* Check again in case the client cancelled the load inside
|
|
* the URLProtocol:didLoadData: callback.
|
|
*/
|
|
if (_isLoading == YES)
|
|
{
|
|
_isLoading = NO;
|
|
[this->client URLProtocolDidFinishLoading: self];
|
|
}
|
|
}
|
|
}
|
|
else if (_isLoading == YES && _statusCode != 401)
|
|
{
|
|
/*
|
|
* Report partial data if possible.
|
|
*/
|
|
if ([_parser isInBody])
|
|
{
|
|
d = [_parser data];
|
|
bodyLength = [d length];
|
|
if (bodyLength > _parseOffset)
|
|
{
|
|
if (_parseOffset > 0)
|
|
{
|
|
d = [d subdataWithRange:
|
|
NSMakeRange(_parseOffset, [d length] - _parseOffset)];
|
|
}
|
|
_parseOffset = bodyLength;
|
|
[self _didLoad: d];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_complete == NO && readCount == 0 && _isLoading == YES)
|
|
{
|
|
/* The read failed ... dropped, but parsing is not complete.
|
|
* The request was sent, so we can't know whether it was
|
|
* lost in the network or the remote end received it and
|
|
* the response was lost.
|
|
*/
|
|
if (_debug)
|
|
{
|
|
NSLog(@"%@ HTTP response not received - %@", self, _parser);
|
|
}
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self didFailWithError:
|
|
[NSError errorWithDomain: @"receive incomplete"
|
|
code: 0
|
|
userInfo: nil]];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void) _put
|
|
{
|
|
BOOL sent = NO;
|
|
|
|
while ((_writeData || _body) && [this->output hasSpaceAvailable])
|
|
{
|
|
int written;
|
|
|
|
// FIXME: should also send out relevant Cookies
|
|
if (_writeData != nil)
|
|
{
|
|
const unsigned char *bytes = [_writeData bytes];
|
|
unsigned len = [_writeData length];
|
|
|
|
written = [this->output write: bytes + _writeOffset
|
|
maxLength: len - _writeOffset];
|
|
if (written > 0)
|
|
{
|
|
if (_debug)
|
|
{
|
|
const unsigned char *b = [_masked bytes];
|
|
|
|
if (NULL == b)
|
|
{
|
|
b = bytes;
|
|
}
|
|
if (NO == [_logDelegate putBytes: b + _writeOffset
|
|
ofLength: written
|
|
byHandle: self])
|
|
{
|
|
debugWrite(self, written, b + _writeOffset);
|
|
}
|
|
}
|
|
_writeOffset += written;
|
|
if (_writeOffset >= len)
|
|
{
|
|
DESTROY(_writeData);
|
|
DESTROY(_masked);
|
|
if (_body == nil)
|
|
{
|
|
_body = RETAIN([this->request HTTPBodyStream]);
|
|
if (_body == nil)
|
|
{
|
|
NSData *d = [this->request HTTPBody];
|
|
|
|
if (d != nil)
|
|
{
|
|
_body = [NSInputStream alloc];
|
|
_body = [_body initWithData: d];
|
|
[_body open];
|
|
}
|
|
else
|
|
{
|
|
sent = YES;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (_body != nil)
|
|
{
|
|
if ([_body hasBytesAvailable])
|
|
{
|
|
unsigned char buffer[BUFSIZ*64];
|
|
int len;
|
|
|
|
len = [_body read: buffer maxLength: sizeof(buffer)];
|
|
if (len < 0)
|
|
{
|
|
if (_debug)
|
|
{
|
|
NSLog(@"%@ error reading from HTTPBody stream %@",
|
|
self, [NSError _last]);
|
|
}
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self didFailWithError:
|
|
[NSError errorWithDomain: @"can't read body"
|
|
code: 0
|
|
userInfo: nil]];
|
|
return;
|
|
}
|
|
else if (len > 0)
|
|
{
|
|
written = [this->output write: buffer maxLength: len];
|
|
if (written > 0)
|
|
{
|
|
if (_debug)
|
|
{
|
|
if (NO == [_logDelegate putBytes: buffer
|
|
ofLength: written
|
|
byHandle: self])
|
|
{
|
|
debugWrite(self, written, buffer);
|
|
}
|
|
}
|
|
len -= written;
|
|
if (len > 0)
|
|
{
|
|
/* Couldn't write it all now, save and try
|
|
* again later.
|
|
*/
|
|
_writeData = [[NSData alloc] initWithBytes:
|
|
buffer + written length: len];
|
|
_writeOffset = 0;
|
|
}
|
|
else if (len == 0 && ![_body hasBytesAvailable])
|
|
{
|
|
/* all _body's bytes are read and written
|
|
* so we shouldn't wait for another
|
|
* opportunity to close _body and set
|
|
* the flag 'sent'.
|
|
*/
|
|
[_body close];
|
|
DESTROY(_body);
|
|
sent = YES;
|
|
}
|
|
}
|
|
else if ([this->output streamStatus]
|
|
== NSStreamStatusWriting)
|
|
{
|
|
/* Couldn't write it all now, save and try
|
|
* again later.
|
|
*/
|
|
_writeData = [[NSData alloc] initWithBytes:
|
|
buffer length: len];
|
|
_writeOffset = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[_body close];
|
|
DESTROY(_body);
|
|
sent = YES;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[_body close];
|
|
DESTROY(_body);
|
|
sent = YES;
|
|
}
|
|
}
|
|
}
|
|
if (sent)
|
|
{
|
|
if (_shouldClose)
|
|
{
|
|
if (_debug)
|
|
{
|
|
NSLog(@"%@ request sent ... closing", self);
|
|
}
|
|
[this->output setDelegate: nil];
|
|
[this->output removeFromRunLoop:
|
|
[NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->output close];
|
|
DESTROY(this->output);
|
|
}
|
|
else if (_debug)
|
|
{
|
|
NSLog(@"%@ request sent", self);
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void) stream: (NSStream*)stream handleEvent: (NSStreamEvent)event
|
|
{
|
|
/* Make sure no action triggered by anything else destroys us prematurely.
|
|
*/
|
|
IF_NO_ARC([[self retain] autorelease];)
|
|
|
|
#if 0
|
|
NSLog(@"stream: %@ handleEvent: %x for: %@ (ip %p, op %p)",
|
|
stream, event, self, this->input, this->output);
|
|
#endif
|
|
|
|
if (stream == this->input)
|
|
{
|
|
switch(event)
|
|
{
|
|
case NSStreamEventHasBytesAvailable:
|
|
case NSStreamEventEndEncountered:
|
|
[self _got: stream];
|
|
return;
|
|
|
|
case NSStreamEventOpenCompleted:
|
|
if (_debug)
|
|
{
|
|
NSLog(@"%@ HTTP input stream opened", self);
|
|
}
|
|
return;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else if (stream == this->output)
|
|
{
|
|
switch (event)
|
|
{
|
|
case NSStreamEventOpenCompleted:
|
|
{
|
|
NSMutableData *m;
|
|
NSMutableData *mm = nil;
|
|
NSDictionary *d;
|
|
NSEnumerator *e;
|
|
NSString *s;
|
|
NSURL *u;
|
|
int l;
|
|
NSData *bytes;
|
|
|
|
if (_debug)
|
|
{
|
|
NSLog(@"%@ HTTP output stream opened", self);
|
|
}
|
|
this->in = [[NSString alloc]
|
|
initWithFormat: @"(%@:%@ <-- %@:%@)",
|
|
[stream propertyForKey: GSStreamLocalAddressKey],
|
|
[stream propertyForKey: GSStreamLocalPortKey],
|
|
[stream propertyForKey: GSStreamRemoteAddressKey],
|
|
[stream propertyForKey: GSStreamRemotePortKey]];
|
|
this->out = [[NSString alloc]
|
|
initWithFormat: @"(%@:%@ --> %@:%@)",
|
|
[stream propertyForKey: GSStreamLocalAddressKey],
|
|
[stream propertyForKey: GSStreamLocalPortKey],
|
|
[stream propertyForKey: GSStreamRemoteAddressKey],
|
|
[stream propertyForKey: GSStreamRemotePortKey]];
|
|
DESTROY(_writeData);
|
|
DESTROY(_masked);
|
|
_writeOffset = 0;
|
|
if ([this->request HTTPBodyStream] == nil)
|
|
{
|
|
// Not streaming
|
|
l = [[this->request HTTPBody] length];
|
|
_version = 1.1;
|
|
}
|
|
else
|
|
{
|
|
// Stream and close
|
|
l = -1;
|
|
_version = 1.0;
|
|
_shouldClose = YES;
|
|
}
|
|
|
|
m = [[NSMutableData alloc] initWithCapacity: 1024];
|
|
|
|
/* The request line is of the form:
|
|
* method /path?query HTTP/version
|
|
* where the query part may be missing
|
|
*/
|
|
[m appendData: [[this->request HTTPMethod]
|
|
dataUsingEncoding: NSASCIIStringEncoding]];
|
|
[m appendBytes: " " length: 1];
|
|
u = [this->request URL];
|
|
s = [[u fullPath] stringByAddingPercentEscapesUsingEncoding:
|
|
NSUTF8StringEncoding];
|
|
if ([s hasPrefix: @"/"] == NO)
|
|
{
|
|
[m appendBytes: "/" length: 1];
|
|
}
|
|
[m appendData: [s dataUsingEncoding: NSASCIIStringEncoding]];
|
|
s = [u query];
|
|
if ([s length] > 0)
|
|
{
|
|
[m appendBytes: "?" length: 1];
|
|
[m appendData: [s dataUsingEncoding: NSASCIIStringEncoding]];
|
|
}
|
|
s = [NSString stringWithFormat: @" HTTP/%0.1f\r\n", _version];
|
|
[m appendData: [s dataUsingEncoding: NSASCIIStringEncoding]];
|
|
|
|
d = [this->request allHTTPHeaderFields];
|
|
e = [d keyEnumerator];
|
|
while ((s = [e nextObject]) != nil)
|
|
{
|
|
GSMimeHeader *h;
|
|
|
|
h = [[GSMimeHeader alloc] initWithName: s
|
|
value: [d objectForKey: s]
|
|
parameters: nil];
|
|
if (_debug || mm)
|
|
{
|
|
[h addToBuffer: m masking: &mm];
|
|
}
|
|
else
|
|
{
|
|
[h addToBuffer: m masking: NULL];
|
|
}
|
|
RELEASE(h);
|
|
}
|
|
|
|
/* Use valueForHTTPHeaderField: to check for content-type
|
|
* header as that does a case insensitive comparison and
|
|
* we therefore won't end up adding a second header by
|
|
* accident because the two header names differ in case.
|
|
*/
|
|
if ([[this->request HTTPMethod] isEqual: @"POST"]
|
|
&& [this->request valueForHTTPHeaderField:
|
|
@"Content-Type"] == nil)
|
|
{
|
|
/* On MacOSX, this is automatically added to POST methods */
|
|
static char *ct
|
|
= "Content-Type: application/x-www-form-urlencoded\r\n";
|
|
[m appendBytes: ct length: strlen(ct)];
|
|
if (mm)
|
|
{
|
|
[mm appendBytes: ct length: strlen(ct)];
|
|
}
|
|
}
|
|
if ([this->request valueForHTTPHeaderField: @"Host"] == nil)
|
|
{
|
|
NSString *s = [u scheme];
|
|
id p = [u port];
|
|
id h = [u host];
|
|
|
|
if (h == nil)
|
|
{
|
|
h = @""; // Must send an empty host header
|
|
}
|
|
if (([s isEqualToString: @"http"] && [p intValue] == 80)
|
|
|| ([s isEqualToString: @"https"] && [p intValue] == 443))
|
|
{
|
|
/* Some buggy systems object to the port being in
|
|
* the Host header when it's the default (optional)
|
|
* value.
|
|
* To keep them happy let's omit it in those cases.
|
|
*/
|
|
p = nil;
|
|
}
|
|
if (nil == p)
|
|
{
|
|
s = [NSString stringWithFormat: @"Host: %@\r\n", h];
|
|
}
|
|
else
|
|
{
|
|
s = [NSString stringWithFormat: @"Host: %@:%@\r\n", h, p];
|
|
}
|
|
bytes = [s dataUsingEncoding: NSASCIIStringEncoding];
|
|
[m appendData: bytes];
|
|
if (mm)
|
|
{
|
|
[mm appendData: bytes];
|
|
}
|
|
}
|
|
if (l >= 0 && [this->request
|
|
valueForHTTPHeaderField: @"Content-Length"] == nil)
|
|
{
|
|
s = [NSString stringWithFormat: @"Content-Length: %d\r\n", l];
|
|
bytes = [s dataUsingEncoding: NSASCIIStringEncoding];
|
|
[m appendData: bytes];
|
|
if (mm)
|
|
{
|
|
[mm appendData: bytes];
|
|
}
|
|
}
|
|
[m appendBytes: "\r\n" length: 2]; // End of headers
|
|
if (mm)
|
|
{
|
|
[mm appendBytes: "\r\n" length: 2];
|
|
}
|
|
_writeData = m;
|
|
ASSIGN(_masked, mm);
|
|
} // Fall through to do the write
|
|
|
|
case NSStreamEventHasSpaceAvailable:
|
|
[self _put];
|
|
return;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NSLog(@"Unexpected event %"PRIuPTR
|
|
" occurred on stream %@ not being used by %@",
|
|
event, stream, self);
|
|
}
|
|
if (event == NSStreamEventErrorOccurred)
|
|
{
|
|
NSError *error = [[[stream streamError] retain] autorelease];
|
|
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self didFailWithError: error];
|
|
}
|
|
else
|
|
{
|
|
NSLog(@"Unexpected event %"PRIuPTR" ignored on stream %@ of %@",
|
|
event, stream, self);
|
|
}
|
|
}
|
|
|
|
- (void) useCredential: (NSURLCredential*)credential
|
|
forAuthenticationChallenge: (NSURLAuthenticationChallenge*)challenge
|
|
{
|
|
if (challenge == _challenge)
|
|
{
|
|
ASSIGN(_credential, credential);
|
|
}
|
|
}
|
|
@end
|
|
|
|
@implementation _NSHTTPSURLProtocol
|
|
|
|
+ (BOOL) canInitWithRequest: (NSURLRequest*)request
|
|
{
|
|
return [[[request URL] scheme] isEqualToString: @"https"];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation _NSFTPURLProtocol
|
|
|
|
+ (BOOL) canInitWithRequest: (NSURLRequest*)request
|
|
{
|
|
return [[[request URL] scheme] isEqualToString: @"ftp"];
|
|
}
|
|
|
|
+ (NSURLRequest*) canonicalRequestForRequest: (NSURLRequest*)request
|
|
{
|
|
return request;
|
|
}
|
|
|
|
- (void) startLoading
|
|
{
|
|
if (this->cachedResponse)
|
|
{ // handle from cache
|
|
}
|
|
else
|
|
{
|
|
NSURL *url = [this->request URL];
|
|
NSHost *host = [NSHost hostWithName: [url host]];
|
|
|
|
if (host == nil)
|
|
{
|
|
host = [NSHost hostWithAddress: [url host]];
|
|
}
|
|
[NSStream getStreamsToHost: host
|
|
port: [[url port] intValue]
|
|
inputStream: &this->input
|
|
outputStream: &this->output];
|
|
if (this->input == nil || this->output == nil)
|
|
{
|
|
[this->client URLProtocol: self didFailWithError:
|
|
[NSError errorWithDomain: @"can't connect"
|
|
code: 0
|
|
userInfo: nil]];
|
|
return;
|
|
}
|
|
[this->input retain];
|
|
[this->output retain];
|
|
if ([[url scheme] isEqualToString: @"https"] == YES)
|
|
{
|
|
[this->input setProperty: NSStreamSocketSecurityLevelNegotiatedSSL
|
|
forKey: NSStreamSocketSecurityLevelKey];
|
|
[this->output setProperty: NSStreamSocketSecurityLevelNegotiatedSSL
|
|
forKey: NSStreamSocketSecurityLevelKey];
|
|
}
|
|
[this->input setDelegate: self];
|
|
[this->output setDelegate: self];
|
|
[this->input scheduleInRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->output scheduleInRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
// set socket options for ftps requests
|
|
[this->input open];
|
|
[this->output open];
|
|
}
|
|
}
|
|
|
|
- (void) stopLoading
|
|
{
|
|
if (this->input)
|
|
{
|
|
[this->input setDelegate: nil];
|
|
[this->output setDelegate: nil];
|
|
[this->input removeFromRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->output removeFromRunLoop: [NSRunLoop currentRunLoop]
|
|
forMode: NSDefaultRunLoopMode];
|
|
[this->input close];
|
|
[this->output close];
|
|
DESTROY(this->input);
|
|
DESTROY(this->output);
|
|
}
|
|
}
|
|
|
|
- (void) stream: (NSStream *) stream handleEvent: (NSStreamEvent) event
|
|
{
|
|
if (stream == this->input)
|
|
{
|
|
switch(event)
|
|
{
|
|
case NSStreamEventHasBytesAvailable:
|
|
{
|
|
NSLog(@"FTP input stream has bytes available");
|
|
// implement FTP protocol
|
|
// [this->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");
|
|
[this->client URLProtocolDidFinishLoading: self];
|
|
return;
|
|
case NSStreamEventOpenCompleted:
|
|
// prepare to receive header
|
|
NSLog(@"FTP input stream opened");
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else if (stream == this->output)
|
|
{
|
|
NSLog(@"An event occurred on the output stream.");
|
|
// if successfully opened, send out FTP request header
|
|
}
|
|
else
|
|
{
|
|
NSLog(@"Unexpected event %"PRIuPTR
|
|
" occurred on stream %@ not being used by %@",
|
|
event, stream, self);
|
|
}
|
|
if (event == NSStreamEventErrorOccurred)
|
|
{
|
|
NSLog(@"An error %@ occurred on stream %@ of %@",
|
|
[stream streamError], stream, self);
|
|
[self stopLoading];
|
|
[this->client URLProtocol: self didFailWithError: [stream streamError]];
|
|
}
|
|
else
|
|
{
|
|
NSLog(@"Unexpected event %"PRIuPTR" ignored on stream %@ of %@",
|
|
event, stream, self);
|
|
}
|
|
}
|
|
|
|
@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
|
|
NSData *data;
|
|
NSURLResponse *r;
|
|
|
|
data = [NSData dataWithContentsOfFile: [[this->request URL] path]
|
|
/* options: error: - don't use that because it is based on self */];
|
|
if (data == nil)
|
|
{
|
|
[this->client URLProtocol: self didFailWithError:
|
|
[NSError errorWithDomain: @"can't load file" code: 0 userInfo:
|
|
[NSDictionary dictionaryWithObjectsAndKeys:
|
|
[this->request URL], @"URL",
|
|
[[this->request URL] path], @"path",
|
|
nil]]];
|
|
return;
|
|
}
|
|
|
|
/* FIXME ... maybe should infer MIME type and encoding from extension or BOM
|
|
*/
|
|
r = [[NSURLResponse alloc] initWithURL: [this->request URL]
|
|
MIMEType: @"text/html"
|
|
expectedContentLength: [data length]
|
|
textEncodingName: @"unknown"];
|
|
[this->client URLProtocol: self
|
|
didReceiveResponse: r
|
|
cacheStoragePolicy: NSURLRequestUseProtocolCachePolicy];
|
|
[this->client URLProtocol: self didLoadData: data];
|
|
[this->client URLProtocolDidFinishLoading: self];
|
|
RELEASE(r);
|
|
}
|
|
|
|
- (void) stopLoading
|
|
{
|
|
return;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation _NSDataURLProtocol
|
|
|
|
+ (BOOL) canInitWithRequest: (NSURLRequest*)request
|
|
{
|
|
return [[[request URL] scheme] isEqualToString: @"data"];
|
|
}
|
|
|
|
+ (NSURLRequest*) canonicalRequestForRequest: (NSURLRequest*)request
|
|
{
|
|
return request;
|
|
}
|
|
|
|
- (void) startLoading
|
|
{
|
|
NSURLResponse *r;
|
|
NSString *mime = @"text/plain";
|
|
NSString *encoding = @"US-ASCII";
|
|
NSData *data;
|
|
NSString *spec = [[this->request URL] resourceSpecifier];
|
|
NSRange comma = [spec rangeOfString:@","];
|
|
NSEnumerator *types;
|
|
NSString *type;
|
|
BOOL base64 = NO;
|
|
|
|
if (comma.location == NSNotFound)
|
|
{
|
|
NSDictionary *ui;
|
|
NSError *error;
|
|
|
|
ui = [NSDictionary dictionaryWithObjectsAndKeys:
|
|
[this->request URL], @"URL",
|
|
[[this->request URL] path], @"path",
|
|
nil];
|
|
error = [NSError errorWithDomain: @"can't load data"
|
|
code: 0
|
|
userInfo: ui];
|
|
[this->client URLProtocol: self didFailWithError: error];
|
|
return;
|
|
}
|
|
types = [[[spec substringToIndex: comma.location]
|
|
componentsSeparatedByString: @";"] objectEnumerator];
|
|
while (nil != (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;
|
|
}
|
|
}
|
|
spec = [spec substringFromIndex: comma.location + 1];
|
|
if (YES == base64)
|
|
{
|
|
data = [GSMimeDocument decodeBase64:
|
|
[spec dataUsingEncoding: NSUTF8StringEncoding]];
|
|
}
|
|
else
|
|
{
|
|
data = [[spec stringByReplacingPercentEscapesUsingEncoding:
|
|
NSUTF8StringEncoding] dataUsingEncoding: NSUTF8StringEncoding];
|
|
}
|
|
r = [[NSURLResponse alloc] initWithURL: [this->request URL]
|
|
MIMEType: mime
|
|
expectedContentLength: [data length]
|
|
textEncodingName: encoding];
|
|
|
|
[this->client URLProtocol: self
|
|
didReceiveResponse: r
|
|
cacheStoragePolicy: NSURLRequestUseProtocolCachePolicy];
|
|
[this->client URLProtocol: self didLoadData: data];
|
|
[this->client URLProtocolDidFinishLoading: self];
|
|
RELEASE(r);
|
|
}
|
|
|
|
- (void) stopLoading
|
|
{
|
|
return;
|
|
}
|
|
|
|
@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]; // no data
|
|
|
|
// we could pass different content depending on the url path
|
|
r = [[NSURLResponse alloc] initWithURL: [this->request URL]
|
|
MIMEType: @"text/html"
|
|
expectedContentLength: 0
|
|
textEncodingName: @"utf-8"];
|
|
[this->client URLProtocol: self
|
|
didReceiveResponse: r
|
|
cacheStoragePolicy: NSURLRequestUseProtocolCachePolicy];
|
|
[this->client URLProtocol: self didLoadData: data];
|
|
[this->client URLProtocolDidFinishLoading: self];
|
|
RELEASE(r);
|
|
}
|
|
|
|
- (void) stopLoading
|
|
{
|
|
return;
|
|
}
|
|
|
|
@end
|