// Copyright 1997-1999 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "OWHTTPSession.h"

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>
#import <OmniNetworking/OmniNetworking.h>

#import "NSDate-OWExtensions.h"
#import "OWAddress.h"
#import "OWAuthorizationServer.h"
#import "OWContentType.h"
#import "OWCookie.h"
#import "OWDataStream.h"
#import "OWHeaderDictionary.h"
#import "OWNetLocation.h"
#import "OWSourceProcessor.h"
#import "OWTimeStamp.h"
#import "OWURL.h"
#import "OWWebPipeline.h"
#import "OWHTTPProcessor.h"
#import "OWHTTPSessionQueue.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Processors.subproj/Protocols.subproj/HTTP.subproj/OWHTTPSession.m,v 1.32 1999/09/27 23:36:11 kc Exp $")

@interface OWHTTPSession (ActualProtocolStuff)

#warning Lots of strings here need to be made localizable

+ (NSString *)stringForHeader:(NSString *)aHeader value:aValue;
- (void)connect;
- (void)disconnect;

- (BOOL)sendRequest;

// Reading results
- (void)readHeaders;
- (BOOL)readResponse;
- (void)readBodyAndIgnore:(BOOL)ignoreThis;
- (BOOL)readHead;
- (void)readTimeStamp;

@end

@implementation OWHTTPSession

static BOOL OWHTTPDebug = NO;
static NSMutableDictionary *languageAbbreviationsDictionary;
static NSString *preferredDateFormat;
static NSString *acceptLanguageString;
static NSString *http10VersionString;
static NSString *http11VersionString;
static NSString *endOfLineString;
static NSString *userAgentInfo = nil;
static NSString *spoofUserAgentInfo = nil;
static NSString *spoofUserAgentHeaderFormat = nil;
static NSMutableArray *languageArray = nil;
static NSCharacterSet *spaceCharacterSet;

+ (void)initialize;
{
    static BOOL initialized = NO;

    [super initialize];
    if (initialized)
        return;
    initialized = YES;

    http10VersionString = @"HTTP/1.0";
    http11VersionString = @"HTTP/1.1";
    endOfLineString = @"\r\n";
    languageAbbreviationsDictionary = [[NSMutableDictionary alloc] initWithCapacity:8];
    spaceCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:@" "] retain];
}

// OFBundleRegistryTarget informal protocol

+ (void)registerItemName:(NSString *)itemName bundle:(NSBundle *)bundle description:(NSDictionary *)description;
{
    if ([itemName isEqualToString:@"languageAbbreviations"]) {
        [languageAbbreviationsDictionary addEntriesFromDictionary:description];
    } else if ([itemName isEqualToString:@"dateFormats"]) {
        preferredDateFormat = [description objectForKey:@"preferredDateFormat"];
    }
}

+ (void)didLoad;
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidInit:) name:OFControllerDidInitNotification object:nil];
}

+ (void)controllerDidInit:(NSNotification *)notification;
{
    NSDictionary *softwareVersionDictionary;
    NSString *software;
    NSString *version;
    NSMutableArray *softwareVersionArray;
    NSString *primaryAgentKey, *primaryAgentVersion;

    softwareVersionArray = [[NSMutableArray alloc] init];
    softwareVersionDictionary = [OFBundleRegistry softwareVersionDictionary];
    primaryAgentKey = [[NSProcessInfo processInfo] processName];
    primaryAgentVersion = [softwareVersionDictionary objectForKey:primaryAgentKey];
    if (primaryAgentVersion && [primaryAgentVersion length] != 0) {
        [softwareVersionArray addObject:[NSString stringWithFormat:@"%@/%@", primaryAgentKey, primaryAgentVersion]];
    } else {
        [softwareVersionArray addObject:primaryAgentKey];
    }

    // Look up the OWF framework version.  (We used to list the versions of all registered frameworks, but that made our user-agent header too long for some servers.)
    software = @"OWF";
    version = [softwareVersionDictionary objectForKey:software];
    if (version && [version length] != 0) {
        [softwareVersionArray addObject:[NSString stringWithFormat:@"%@/%@", software, version]];
    } else {
        [softwareVersionArray addObject:software];
    }

    userAgentInfo = [softwareVersionArray componentsJoinedByString:@" "];
    [userAgentInfo retain];
    [softwareVersionArray release];

    [self readDefaults];

    spoofUserAgentInfo = [[NSString stringWithFormat:spoofUserAgentHeaderFormat, userAgentInfo] retain];
}

+ (Class)socketClass;
{
    return [ONTCPSocket class];
}

+ (int)defaultPort;
{
    return 80;
}

+ (void)readDefaults;
{
    NSString *systemLanguages;
    NSArray *systemLanguagesArray = nil;
    unsigned int systemLanguageIndex;
    unsigned int systemLanguageCount;
    OFUserDefaults *defaults;

    defaults = [OFUserDefaults sharedUserDefaults];

    spoofUserAgentHeaderFormat = [defaults stringForKey:@"OWHTTPSpoofUserAgentHeaderFormat"];

    OWHTTPDebug = [defaults boolForKey:@"OWHTTPDebug"];
    [OWHeaderDictionary setDebug:OWHTTPDebug];

    // TODO: I don't know if the first attempt here ever works, or if it's a remnant from NEXTSTEP?  --wim
    systemLanguages = [[NSUserDefaults standardUserDefaults] objectForKey:@"Language"];
    if (systemLanguages && [systemLanguages length])
        systemLanguagesArray = [systemLanguages componentsSeparatedByString:@";"];

    // otherwise, use the NSLanguages default
    if (!systemLanguagesArray || [systemLanguagesArray count] == 0)
        systemLanguagesArray = [[NSUserDefaults standardUserDefaults] stringArrayForKey:@"NSLanguages"];

    // otherwise, fall back on English
    if (!systemLanguagesArray || [systemLanguagesArray count] == 0)
        systemLanguagesArray = [NSArray arrayWithObjects:@"English", nil];

    if (languageArray) [languageArray release];
    languageArray = [[NSMutableArray alloc]
            initWithCapacity:[systemLanguagesArray count]];
    systemLanguageCount = [systemLanguagesArray count];
    for (systemLanguageIndex = 0; systemLanguageIndex < systemLanguageCount; systemLanguageIndex++) {
        NSString *languageAbbreviation;

        languageAbbreviation = [languageAbbreviationsDictionary objectForKey:[systemLanguagesArray objectAtIndex:systemLanguageIndex]];
        if (languageAbbreviation)
            [languageArray addObject:languageAbbreviation];
    }

    acceptLanguageString = [[self stringForHeader:@"Accept-Language"
            value:[languageArray componentsJoinedByString:@", "]] retain];
}

+ (void)setDebug:(BOOL)shouldDebug;
{
    OWHTTPDebug = shouldDebug;
}

+ (BOOL)shouldSpoofNetLocation:(OWNetLocation *)whatServer;
{
    NSString *hostname;
    OFUserDefaults *defaults;
    NSArray *spoofServers;
    unsigned int spoofServerIndex, spoofServerCount;
    NSString *spoofDomain;

    defaults = [OFUserDefaults sharedUserDefaults];

    if ([defaults boolForKey:@"OWHTTPSpoofAllServers"])
        return YES;

    hostname = [whatServer hostname];
    spoofServers = [defaults arrayForKey:@"OWHTTPSpoofServers"];
    spoofServerCount = [spoofServers count];
    for (spoofServerIndex = 0; spoofServerIndex < spoofServerCount; spoofServerIndex++) {
        spoofDomain = [spoofServers objectAtIndex:spoofServerIndex];
        if ([hostname hasSuffix:spoofDomain])
            return YES;
    }
    return NO;
}

+ (NSString *)userAgentInfoForServerAtNetLocation:(OWNetLocation *)whatServer
{
    if ([self shouldSpoofNetLocation:whatServer])
        return spoofUserAgentInfo;
    else
        return userAgentInfo;
}

+ (NSString *)preferredDateFormat;
{
    return preferredDateFormat;
}

+ (NSArray *)acceptLanguages;
{
    return languageArray;
}

- initWithAddress:(OWAddress *)anAddress inQueue:(OWHTTPSessionQueue *)aQueue;
{
    OWURL *proxyURL, *realURL;
    
    if (![super init])
        return nil;

    queue = aQueue;
    proxyURL = [anAddress proxyURL];
    realURL = [anAddress url];
    flags.connectingViaProxyServer = (proxyURL != realURL);
    proxyLocation = [[proxyURL parsedNetLocation] retain];
    authorizationServer = [[OWAuthorizationServer serverForAddress:address] retain];
    if (flags.connectingViaProxyServer)
        proxyAuthorizationServer = [[OWAuthorizationServer serverForProxy:[proxyURL netLocation]] retain];
    processorQueue = [[NSMutableArray alloc] initWithCapacity:[queue maximumNumberOfRequestsToPipeline]];
    flags.pipeliningRequests = NO;
    failedRequests = 0;

    return self;
}

- (void)dealloc;
{
    [self disconnect];
    [proxyLocation release];               proxyLocation = nil;
    [processorQueue release];              processorQueue = nil;
    [authorizationServer release];         authorizationServer = nil;
    [proxyAuthorizationServer release];    proxyAuthorizationServer = nil;
    [super dealloc];
}

- (BOOL)fetchForProcessor:(OWHTTPProcessor *)aProcessor inPipeline:(OWPipeline *)aPipeline;
{
    NSException *exception = nil;
    BOOL finishedProcessing;
    
    nonretainedProcessor = aProcessor;
    nonretainedPipeline = aPipeline;
    [nonretainedProcessor processBegin];
    
    address = [[nonretainedPipeline lastAddress] retain];
    url = [[address url] retain];
    headerDictionary = [[OWHeaderDictionary alloc] init];
    interruptedDataStream = [[nonretainedProcessor dataStream] retain];

    NS_DURING {
        if ([[address methodString] isEqualToString:@"HEAD"])
            finishedProcessing = [self readHead];
        else
            finishedProcessing = [self readResponse];
        failedRequests = 0;
    } NS_HANDLER {
        if (flags.pipeliningRequests && [[localException reason] isEqualToString:@"Unable to read from socket: Connection reset by peer"]) {
            // This HTTP 1.1 connection was reset by the server
            finishedProcessing = NO;
            if ([interruptedDataStream bufferedDataLength] < 1024) {
                failedRequests++;
                if (failedRequests > 0) {
                    // We've been dropped by this server without getting much data:  let's try a traditional HTTP/1.0 connection instead.
                    // NSLog(@"%@: Switching to HTTP/1.0", OBShortObjectDescription(self));
                    [queue setServerCannotHandlePipelinedRequestsReliably];
                    failedRequests = 0;
                }
            } else {
                // Well, we got _some_ data...
                failedRequests = 0;
            }
        } else if (interruptedDataStream) {
            // Abort the data stream and reraise the exception
            exception = [localException retain];
            [interruptedDataStream dataAbort];
            finishedProcessing = YES;
        } else {
            // TODO: I'm not sure about this case
            NSLog(@"%@: Caught exception %@ but interruptedDataStream is nil", OBShortObjectDescription(self), [localException reason]);
            finishedProcessing = NO;
        }
    } NS_ENDHANDLER;            

    fetchFlags.aborting = NO;
    if (exception) {
        [nonretainedProcessor handleSessionException:exception];
        [exception release];
    }
    if (finishedProcessing) {
        [nonretainedProcessor processEnd];
        [nonretainedProcessor retire];        
    }

    // get rid of variables for this fetch
    [address release];
    address = nil;
    [url release];
    url = nil;
    [headerDictionary release];
    headerDictionary = nil;
    [interruptedDataStream release];
    interruptedDataStream = nil;
    return finishedProcessing;
}

- (void)runSession;
{
    OWHTTPProcessor *aProcessor;
    NSAutoreleasePool *pool;
    NSException *exception = nil;

    NS_DURING {
        do {
            while (1) {
                pool = [[NSAutoreleasePool alloc] init];
                if (![self sendRequest])
                    break;
                aProcessor = [processorQueue objectAtIndex:0];
                if ([self fetchForProcessor:aProcessor inPipeline:[aProcessor pipeline]]) {
                    [processorQueue removeObjectAtIndex:0];
                } else {
                    [self disconnect];
                }
                [pool release];
            }
            [pool release];
        } while (![queue sessionIsIdle:self]);
    } NS_HANDLER {
        exception = [localException retain];
    } NS_ENDHANDLER;

    if (exception) {
        [self disconnect];
        do {
            while ((aProcessor = [queue nextProcessor])) {
                [aProcessor processBegin];
                [aProcessor handleSessionException:exception];
                [aProcessor processEnd];
                [aProcessor retire];
            }
        } while (![queue sessionIsIdle:self]);
        [exception release];
    }
}

- (void)abortProcessingForProcessor:(OWProcessor *)aProcessor;
{
    // Does this really abort the right processor?
    if ([processorQueue containsObject:aProcessor]) {
        fetchFlags.aborting = YES;
        [(ONInternetSocket *)[socketStream socket] abortSocket];
    }
}

- (void)setStatusString:(NSString *)newStatus;
{
    unsigned int index, count;

    // Set the status for all processors in this session
    for (index = 0, count = [processorQueue count]; index < count; index++)
        [[processorQueue objectAtIndex:index] setStatusString:newStatus];
    // And for any processors waiting on us
    [queue session:self hasStatusString:newStatus];
}

- (void)setStatusFormat:(NSString *)aFormat, ...;
{
    NSString *newStatus;
    va_list argList;

    va_start(argList, aFormat);
    newStatus = [[NSString alloc] initWithFormat:aFormat arguments:argList];
    va_end(argList);
    [self setStatusString:newStatus];
    [newStatus release];
}

// OBObject subclass

- (NSMutableDictionary *)debugDictionary;
{
    NSMutableDictionary *debugDictionary;

    debugDictionary = [super debugDictionary];
    if (address)
        [debugDictionary setObject:address forKey:@"address"];
    if (socketStream)
        [debugDictionary setObject:socketStream forKey:@"socketStream"];
    if (headerDictionary)
        [debugDictionary setObject:headerDictionary forKey:@"headerDictionary"];
    if (authorizationServer)
        [debugDictionary setObject:authorizationServer forKey:@"authorizationServer"];
    if (proxyAuthorizationServer)
        [debugDictionary setObject:proxyAuthorizationServer forKey:@"proxyAuthorizationServer"];

    return debugDictionary;
}

@end


@implementation OWHTTPSession (ActualProtocolStuff)

+ (NSString *)stringForHeader:(NSString *)aHeader value:aValue;
{
    NSMutableString *header;
    NSString *value;

    value = [aValue description];
    header = [[NSMutableString alloc] initWithCapacity:[aHeader length] + 2 + [value length] + [endOfLineString length]];
    [header appendString:aHeader];
    [header appendString:@": "];
    [header appendString:value];
    [header appendString:endOfLineString];
    [header autorelease];
    return header;
}

- (void)connect;
{
    NSString *port;
    ONInternetSocket *socket;
    ONHost *host;

    [self setStatusFormat:@"Finding %@", [proxyLocation shortDisplayString]];
    port = [proxyLocation port];
    host = [ONHost hostForHostname:[proxyLocation hostname]];
    [self setStatusFormat:@"Contacting %@", [proxyLocation shortDisplayString]];

    // Sadly, we have two socket methods, +[ONInternetSocket socket] and -[ONSocketStream socket], and the compiler doesn't know which it's getting without the final cast.  (There's no way to cast +socketClass to disambiguate ahead of time.)
    socket = (ONInternetSocket *)[[isa socketClass] socket];
    [socket setReadBufferSize:32 * 1024];
    socketStream = [[ONSocketStream alloc] initWithSocket:socket];
    [socket connectToHost:host port:port ? [port intValue] : [isa defaultPort]];

    [self setStatusFormat:@"Contacted %@", [proxyLocation shortDisplayString]];
    if (OWHTTPDebug)
        NSLog(@"%@: Connected to %@", [isa description], [proxyLocation displayString]);
}

- (void)disconnect;
{
    unsigned int index, count;

    // drop all processors
    for (index = 0, count = [processorQueue count]; index < count; index++)
        [queue queueProcessor:[processorQueue objectAtIndex:index]];
    [processorQueue removeAllObjects];

    if (!socketStream)
        return;
    [socketStream release];
    socketStream = nil;
}

- (NSString *)commandStringForAddress:(OWAddress *)anAddress;
{
    NSMutableString *command;
    OWURL *aURL;
    NSString *fetchPath;

    aURL = [anAddress url];
    command = [NSMutableString stringWithCapacity:128];
    [command appendFormat:@"%@ ", [anAddress methodString]];
    if (flags.connectingViaProxyServer)
        fetchPath = [aURL proxyFetchPath];
    else
        fetchPath = [aURL fetchPath];
    if ([fetchPath containsCharacterInSet:spaceCharacterSet]) {
        // Don't send a malformed request just because someone forgot to quote the spaces in their URL
        fetchPath = [fetchPath stringByReplacingCharactersInSet:spaceCharacterSet withString:@"%20"];
    }
    [command appendString:fetchPath];
    [command appendFormat:@" %@", flags.pipeliningRequests ? http11VersionString : http10VersionString];
    [command appendString:endOfLineString];

    return command;
}

// Note --- this method uses no ivars.
- (NSString *)acceptHeadersStringForTarget:(id <OWTarget>)aTarget;
{
    NSString *headerString;
    NSMutableArray *acceptsArray;
    NSArray *encodings;
    NSEnumerator *possibleTypesEnumerator;
    OWContentType *possibleType;
    
    if ([[OFUserDefaults sharedUserDefaults] boolForKey:@"OWHTTPFakeAcceptHeader"]) {
        headerString = [isa stringForHeader:@"Accept" value:@"image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, image/tiff, multipart/x-mixed-replace, */*"];
    } else {
        NSString *wildcardContentTypeString;
        // Determine the real Accept header

        acceptsArray = [[NSMutableArray alloc] initWithCapacity:16];
        possibleTypesEnumerator = [[[aTarget targetContentType] indirectSourceContentTypes] objectEnumerator];
        while ((possibleType = [possibleTypesEnumerator nextObject]))
            if ([possibleType isPublic] && [possibleType isInteresting])
                [acceptsArray addObject:[possibleType contentTypeString]];

        // TODO: Be clever about when we include the wildcard content type
        // also: should we have covers for the boring content types (image/*, maybe)?
        wildcardContentTypeString = [[OWContentType wildcardContentType] contentTypeString];
        if ([acceptsArray indexOfObject:wildcardContentTypeString] == NSNotFound)
            [acceptsArray addObject:wildcardContentTypeString];

        headerString = [isa stringForHeader:@"Accept" value:[acceptsArray componentsJoinedByString:@", "]];
        [acceptsArray release];
    }

    // Determine the Accept-Encoding header if any
    encodings = [OWContentType contentEncodings];
    acceptsArray = [[NSMutableArray alloc] initWithCapacity:[encodings count]];
    possibleTypesEnumerator = [encodings objectEnumerator];
    while ((possibleType = [possibleTypesEnumerator nextObject])) {
        NSString *encodingString;
        if (![possibleType isPublic])
            continue;
        // TODO: Discard encodings we know about but can't handle (see comment above +contentEncodings)
//        if (![[possibleType links] count])
//            continue;
        encodingString = [possibleType contentTypeString];
        // TODO: More generic way to perform this test
        if ([encodingString isEqualToString:@"encoding/none"])
            continue;
        if (![encodingString hasPrefix:@"encoding/"])
            continue; // this should never happen
        [acceptsArray addObject:[encodingString substringFromIndex:9]]; // remove the "encoding/"
    }
    if ([acceptsArray count])
        headerString = [headerString stringByAppendingString:[isa stringForHeader:@"Accept-Encoding" value:[acceptsArray componentsJoinedByString:@", "]]];
    [acceptsArray release];

    return headerString;
}

- (NSString *)acceptLanguageHeadersString;
{
    return acceptLanguageString;
}

- (NSString *)referrerHeaderStringForPipeline:(OWWebPipeline *)aPipeline;
{
    OWAddress *referringAddress;
    NSString *referrerString;

    if ([aPipeline respondsToSelector:@selector(referringAddress)])
        referringAddress = [(OWWebPipeline *)aPipeline referringAddress];
    else
        return nil;
    
    referrerString = [referringAddress addressString];
    if (!referrerString)
        return nil;
    return [isa stringForHeader:@"Referer" /* [sic] */ value:referrerString];
}

- (NSString *)pragmaHeaderStringForPipeline:(OWWebPipeline *)aPipeline;
{
    if ([aPipeline proxyCacheDisabled])
        return [isa stringForHeader:@"Pragma" value:@"no-cache"];
    return nil;
}

- (NSString *)hostHeaderStringForURL:(OWURL *)aURL;
{
    NSString *netLocation;

    netLocation = [aURL netLocation];
    if (!netLocation)
        return nil;
    return [isa stringForHeader:@"Host" value:netLocation];
}

- (NSString *)keepAliveString;
{
    if (flags.connectingViaProxyServer)
        return @"";
    else
        return [isa stringForHeader:@"Connection" value:@"Keep-Alive"];
}

- (NSString *)userAgentHeaderStringForURL:(OWURL *)aURL;
{
    return [isa stringForHeader:@"User-Agent" value:[isa userAgentInfoForServerAtNetLocation:[aURL parsedNetLocation]]];
}

- (NSString *)authorizationStringForURL:(OWURL *)aURL;
{
    NSMutableArray *allCredentials;
    NSString *credentialString;

    flags.foundCredentials = NO;
    flags.foundProxyCredentials = NO;
    allCredentials = [[NSMutableArray alloc] init];

    if (authorizationServer) {
        NSArray *credentials;

        credentials = [authorizationServer credentialsForPath:[aURL path]];
        if (credentials && [credentials count] > 0) {
            [allCredentials insertObjectsFromArray:credentials atIndex:0];
            flags.foundCredentials = YES;
        }
    }

    if (proxyAuthorizationServer) {
        NSArray *credentials;

        credentials = [proxyAuthorizationServer credentialsForPath:[aURL path]];
        if (credentials && [credentials count] > 0) {
            [allCredentials insertObjectsFromArray:credentials atIndex:0];
            flags.foundProxyCredentials = YES;
        }
    }

    if ([allCredentials count] > 0) {
        credentialString = [[allCredentials componentsJoinedByString:endOfLineString] stringByAppendingString:endOfLineString];
    } else {
        credentialString = nil;
    }

    [allCredentials release];

    return credentialString;
}

- (NSString *)cookiesForURL:(OWURL *)aURL;
{
    NSMutableString *cookieString = nil;
    NSArray *cookies;
    unsigned int cookieIndex, cookieCount;

    cookies = [OWCookie cookiesForURL:aURL];
    if (!cookies)
        return nil;
    cookieCount = [cookies count];
    for (cookieIndex = 0; cookieIndex < cookieCount; cookieIndex++) {
        OWCookie *cookie;

        cookie = [cookies objectAtIndex:cookieIndex];
        if (!cookieString)
            cookieString = [[[NSMutableString alloc] initWithString:@"Cookie: "] autorelease];
        else
            [cookieString appendString:@"; "];
        [cookieString appendString:[cookie name]];
        [cookieString appendString:@"="];
        [cookieString appendString:[cookie value]];
    }
    [cookieString appendString:endOfLineString];
    return cookieString;
}

- (NSString *)contentTypeHeaderStringForAddress:(OWAddress *)anAddress;
{
    NSMutableString *valueString;
    NSDictionary *addressMethodDictionary;
    NSString *boundaryString;
    NSString *contentTypeHeaderString;
    NSString *contentType;

    addressMethodDictionary = [anAddress methodDictionary];
    contentType = [addressMethodDictionary objectForKey:@"Content-Type"];
    if (!contentType)
        return nil;
    valueString = [[NSMutableString alloc] initWithString:contentType];
    boundaryString = [addressMethodDictionary objectForKey:@"Boundary"];
    if (boundaryString)
        [valueString appendFormat:@"; boundary=%@", boundaryString];

    contentTypeHeaderString = [isa stringForHeader:@"Content-Type" value:valueString];
    [valueString release];
    return contentTypeHeaderString;
}

- (NSString *)contentLengthHeaderStringForAddress:(OWAddress *)anAddress;
{
    NSDictionary *addressMethodDictionary;

    addressMethodDictionary = [anAddress methodDictionary];
    return [isa stringForHeader:@"Content-Length" value:[NSNumber numberWithInt:[[addressMethodDictionary objectForKey:@"Content-String"] length] + [[addressMethodDictionary objectForKey:@"Content-Data"] length]]];
}

- (NSString *)contentStringForAddress:(OWAddress *)anAddress;
{
    NSString *methodContentString;

    methodContentString = [[anAddress methodDictionary] objectForKey:@"Content-String"];
    if (!methodContentString)
        return nil;
    return [methodContentString stringByAppendingString:endOfLineString];
}

- (NSString *)rangeStringForProcessor:(OWHTTPProcessor *)aProcessor;
{
    OWDataStream *dataStream;

    if ((dataStream = [aProcessor dataStream]) && [dataStream bufferedDataLength]) {
        OBASSERT([queue serverUnderstandsPipelinedRequests]);
        return [isa stringForHeader:@"Range" value:[NSString stringWithFormat:@"bytes=%d-", [dataStream bufferedDataLength]]];
    } else
        return nil;
}

- (NSString *)requestStringForProcessor:(OWHTTPProcessor *)aProcessor;
{
    NSMutableString *requestString;
    NSString *requestMethod;
    NSString *tempString;
    OWWebPipeline *aPipeline;
    OWAddress *anAddress;
    OWURL *aURL;

    aPipeline = (OWWebPipeline *)[aProcessor pipeline];
    anAddress = (OWAddress *)[aPipeline lastAddress];
    aURL = [anAddress url];
    requestMethod = [anAddress methodString];
    requestString = [NSMutableString stringWithCapacity:2048];
    [requestString appendString:[self commandStringForAddress:anAddress]];
    [requestString appendString:[self rangeStringForProcessor:aProcessor]];
    [requestString appendString:[self keepAliveString]];
    [requestString appendString:[self referrerHeaderStringForPipeline:aPipeline]];
    [requestString appendString:[self userAgentHeaderStringForURL:aURL]];
    [requestString appendString:[self pragmaHeaderStringForPipeline:aPipeline]];
    [requestString appendString:[self hostHeaderStringForURL:aURL]];
    [requestString appendString:[self acceptHeadersStringForTarget:[aPipeline target]]];
    [requestString appendString:[self acceptLanguageHeadersString]];
    [requestString appendString:[self authorizationStringForURL:aURL]];
    [requestString appendString:[self cookiesForURL:aURL]];
    // TODO: For bookmarks refreshes, we should support If-Modified-Since:
    if ([requestMethod isEqualToString:@"POST"] ||
        [requestMethod isEqualToString:@"PUT"]) {
        [requestString appendString:[self contentTypeHeaderStringForAddress:anAddress]];
        [requestString appendString:[self contentLengthHeaderStringForAddress:anAddress]];

        // Blank line signals end of headers
        [requestString appendString:endOfLineString];

        // We should probably make -requestData return this, and then this code wouldn't have to be here.  Which means that the above statement could be collapsed to be the same as the else clause.
        if ((tempString = [self contentStringForAddress:anAddress]))
            [requestString appendString:tempString];
    } else {
        // Blank line signals end of headers
        [requestString appendString:endOfLineString];
    }
    return requestString;
}

- (BOOL)sendRequests;
{
    NSData *requestData;
    NSString *requestString;
    OWAddress *anAddress;
    OWHTTPProcessor *aProcessor = nil;
    unsigned int index, count;

    // figure out how many requests to send
    flags.pipeliningRequests = [queue shouldPipelineRequests];
    count = [processorQueue count];
    // We've already sent requests for the processors in processorQueue
    index = count;
    if (flags.pipeliningRequests) {
        unsigned int maximumNumberOfRequestsToPipeline;

        maximumNumberOfRequestsToPipeline = [queue maximumNumberOfRequestsToPipeline];
        // Fill our queue
        while (count < maximumNumberOfRequestsToPipeline) {
            aProcessor = [queue nextProcessor];
            if (!aProcessor)
                break;
            [processorQueue addObject:aProcessor];
            [aProcessor setStatusFormat:@"Preparing to request document from %@", [proxyLocation shortDisplayString]];
            count++;
        }
    } else {
        if (count == 0) {
            if ((aProcessor = [queue nextProcessor])) {
                [processorQueue addObject:aProcessor];
                count++;
            }
        } else {
            // We still have pipelined requests, but don't want to pipeline any more (presumably because the server cannot handle pipelined requests reliably).
            flags.pipeliningRequests = YES;
        }
    }

    // send each
    for (; index < count; index++) {
        aProcessor = [processorQueue objectAtIndex:index];
        anAddress = (OWAddress *)[[aProcessor pipeline] lastAddress];
        requestString = [self requestStringForProcessor:aProcessor];
        if (OWHTTPDebug)
            NSLog(@"%@ Tx: %@", [[anAddress url] scheme], requestString);
        [aProcessor setStatusFormat:@"Requesting document from %@", [proxyLocation shortDisplayString]];
        [socketStream writeString:requestString];
        if ((requestData = [[anAddress methodDictionary] objectForKey:@"Content-Data"]))
            [socketStream writeData:requestData];
    }
    return (index != 0);
}

- (BOOL)sendRequest;
{    
    if (![(ONInternetSocket *)[socketStream socket] isWritable]) {
        [self disconnect];
        [self connect];
    }
    NS_DURING {
        [self sendRequests];
    } NS_HANDLER {
    } NS_ENDHANDLER;
    return ([processorQueue count] != 0);
}

- (void)readHeaders;
{
    [headerDictionary readRFC822HeadersFromSocketStream:socketStream];
    if (OWHTTPDebug)
        NSLog(@"Rx Headers:\n%@", headerDictionary);
    [nonretainedPipeline setHeaderDictionary:headerDictionary fromURL:url];
    [OWCookie registerCookiesFromURL:url headerDictionary:headerDictionary];
    [self readTimeStamp];
}

- (BOOL)readResponse;
{
    NSString *line;
    NSScanner *scanner;
    float httpVersion;
    HTTPStatus httpStatus;
    NSString *commentString;

    if (fetchFlags.aborting)
        [NSException raise:@"User Aborted" format:@"User Aborted"];
        
    [nonretainedProcessor setStatusFormat:@"Awaiting document from %@", [proxyLocation shortDisplayString]];

beginReadResponse:    
    
    if (!(line = [socketStream peekLine]))
        return NO;
    scanner = [NSScanner scannerWithString:line];

    if (![scanner scanString:@"HTTP" intoString:NULL]) {
        // Stinky 0.9 server:  good luck!
        [nonretainedProcessor setStatusFormat:@"%@ is ancient, good luck!", [proxyLocation shortDisplayString]];
        [headerDictionary addString:@"www/unknown" forKey:@"content-type"];
        [nonretainedPipeline setHeaderDictionary:headerDictionary fromURL:url];
        [OWCookie registerCookiesFromURL:url headerDictionary:headerDictionary];
        [self readBodyAndIgnore:NO];
        return YES;
    }

    if (OWHTTPDebug)
        NSLog(@"%@ Rx: %@", [url scheme], line);
    [socketStream readLine];
    [scanner scanString:@"/" intoString:NULL];
    [scanner scanFloat:&httpVersion];
    if (httpVersion > 1.0) {
        if (OWHTTPDebug) {
            NSLog(@"Rx: %@", [address addressString]);
        }
        [queue setServerUnderstandsPipelinedRequests];
    } else if (OWHTTPDebug)
        NSLog(@"Rx: %@", [address addressString]);

    [scanner scanInt:(int *)&httpStatus];
    if (![scanner scanUpToString:@"\n" intoString:&commentString])
        commentString = @"";

processStatus:
    switch (httpStatus) {

        // 100 codes - Informational
        
        case HTTP_STATUS_CONTINUE:
            // read the headers, ignore 'em, start over
            [self readHeaders];
            goto beginReadResponse;
            
        // 200 codes - Success: Got MIME object

        case HTTP_STATUS_OK:
        case HTTP_STATUS_PARTIAL_CONTENT:
            [self readHeaders];
            [self readBodyAndIgnore:NO];
            break;

        case HTTP_STATUS_NO_CONTENT:
            // Netscape 4.0 just ignores request if it returns "NO_CONTENT"
            // was [NSException raise:@"NoContent" format:@"Server returns no content"];
            break;		// Don't read headers and body

        // 300 codes - Temporary error (various forms of redirection)

#warning Should double-check our HTTP 1.1 handling
        case HTTP_STATUS_MULTIPLE_CHOICES:
            break;

        // TODO: Handle permanent vs. temporary redirection
        case HTTP_STATUS_MOVED_PERMANENTLY:
        case HTTP_STATUS_MOVED_TEMPORARILY:
            {
                NSString *newLocationString;
                OWAddress *newLocation;

                [self readHeaders];
                newLocationString =
                    [headerDictionary lastStringForKey:@"location"];
                if (!newLocationString)
                    [NSException raise:@"Redirect failure" format:@"Location header missing on redirect"];
                newLocation = [address addressForRelativeString:newLocationString];
                if ([newLocation isEqual:address])
                    [NSException raise:@"Redirect loop" format:@"Redirect loop"];
                [nonretainedProcessor setStatusFormat:@"Redirected to %@", newLocationString];
                if (httpStatus == HTTP_STATUS_MOVED_PERMANENTLY)
                    [nonretainedPipeline permanentRedirection:newLocation];
                [nonretainedPipeline addSourceContent:newLocation];
                // TODO: Should we cacheContent for permanent redirections? If not, maybe -cacheContent should do the permanent redirection stuff that -permanentRedirection: does.
                if ([[newLocation addressString] isEqualToString:[[address addressString] stringByAppendingString:@"/"]])
                    [nonretainedPipeline cacheContent];
                [nonretainedPipeline startProcessingContent];
                [self readBodyAndIgnore:YES];
            }
            break;

        case HTTP_STATUS_SEE_OTHER:
            break;

        case HTTP_STATUS_NOT_MODIFIED:
            break;

        case HTTP_STATUS_USE_PROXY:
            break;

        // 400 codes - Permanent error

        case HTTP_STATUS_BAD_REQUEST:
        case HTTP_STATUS_PAYMENT_REQUIRED:
        case HTTP_STATUS_FORBIDDEN:
        case HTTP_STATUS_NOT_FOUND:
            [nonretainedPipeline processor:nonretainedProcessor hasErrorName:commentString reason:[NSString stringWithFormat:@"Server returns \"%@\"", commentString]];
            [self readHeaders];
            [nonretainedPipeline contentError];
            [self readBodyAndIgnore:NO];
            break;

        case HTTP_STATUS_UNAUTHORIZED:
            [nonretainedProcessor setStatusFormat:@"Authorizing %@", [proxyLocation shortDisplayString]];
            [self readHeaders];
            if (!authorizationServer)
                authorizationServer = [[OWAuthorizationServer newServerForAddress:address] retain];
            NS_DURING {
                [authorizationServer generateCredentialsForChallenge:headerDictionary path:[url path] reprompt:flags.foundCredentials];
                [nonretainedPipeline startProcessingContent];
            } NS_HANDLER {
                [nonretainedPipeline processor:nonretainedProcessor hasErrorName:[localException displayName] reason:[localException reason]];
                [nonretainedPipeline contentError];
                [self readBodyAndIgnore:NO];
            } NS_ENDHANDLER;
            break;


        case HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED:
            [nonretainedProcessor setStatusFormat:@"Authorizing %@", [proxyLocation shortDisplayString]];
            [self readHeaders];
            if (!proxyAuthorizationServer)
                proxyAuthorizationServer = [[OWAuthorizationServer newServerForProxy:[proxyLocation displayString]] retain];
            NS_DURING {
                [proxyAuthorizationServer generateCredentialsForChallenge:headerDictionary path:[url path] reprompt:flags.foundProxyCredentials];
                [nonretainedPipeline startProcessingContent];
            } NS_HANDLER {
                [nonretainedPipeline processor:nonretainedProcessor hasErrorName:[localException displayName] reason:[localException reason]];
                [nonretainedPipeline contentError];
                [self readBodyAndIgnore:NO];
            } NS_ENDHANDLER;
            break;

        // 500 codes - Server error
        case HTTP_STATUS_INTERNAL_SERVER_ERROR:
        case HTTP_STATUS_NOT_IMPLEMENTED:
        case HTTP_STATUS_BAD_GATEWAY:
        case HTTP_STATUS_SERVICE_UNAVAILABLE:
        case HTTP_STATUS_GATEWAY_TIMEOUT:
            [nonretainedPipeline processor:nonretainedProcessor hasErrorName:commentString reason:[NSString stringWithFormat:@"Server returns \"%@\"", commentString]];
            [self readHeaders];
            [nonretainedPipeline contentError];
            [self readBodyAndIgnore:NO];
            break;

        // Unrecognized client code, treat as x00

        default:
            {
                HTTPStatus equivalentStatus;

                equivalentStatus = httpStatus - httpStatus % 100;
                if (equivalentStatus == httpStatus)
                    httpStatus = HTTP_STATUS_NOT_IMPLEMENTED;
                else
                    httpStatus = equivalentStatus;
            }
            goto processStatus;
    }
    return YES;
}

- (unsigned int)intValueFromHexString:(NSString *)aString;
{
    unsigned int addition, result = 0;
    unsigned int index, length = [aString length];
    unichar c;
    
    for (index = 0; index < length; index++) {
        c = [aString characterAtIndex:index];
        if ((c >= '0') && (c <= '9'))
            addition = c - '0';
        else if ((c >= 'a') && (c <= 'f'))
            addition = c - 'a' + 10;
        else if ((c >= 'A') && (c <= 'F'))
            addition = c - 'A' + 10;
        else
            break;
        result *= 16;
        result += addition;
    }
    return result;
}

- (void)readChunkedBodyIntoStream:(OWDataStream *)dataStream;
{
    unsigned int contentLength, bytesLeft;
    NSAutoreleasePool *autoreleasePool = nil;
    NSData *data;
    unsigned int byteCount, bytesInThisPool;

    while (1) {
        autoreleasePool = [[NSAutoreleasePool alloc] init];

        if (!(contentLength = [self intValueFromHexString:[socketStream readLine]]))
            break;

        bytesInThisPool = 0;
        byteCount = 0;
        bytesLeft = contentLength;
        // NSLog(@"%@ readChunkedBody: start: processed bytes %d of %d for dataStream %@, bytesLeft = %d", OBShortObjectDescription(self), byteCount, contentLength, OBShortObjectDescription(dataStream), bytesLeft);
        [nonretainedProcessor processedBytes:byteCount ofBytes:contentLength];

        while (bytesLeft && (data = [socketStream readDataWithMaxLength:bytesLeft])) {
            unsigned int dataLength;

            dataLength = [data length];
            byteCount += dataLength;
            bytesLeft -= dataLength;
            bytesInThisPool += dataLength;
            [nonretainedProcessor processedBytes:byteCount ofBytes:contentLength];
            // NSLog(@"%@ readChunkedBody: processed bytes %d of %d for dataStream %@, bytesLeft = %d", OBShortObjectDescription(self), byteCount, contentLength, OBShortObjectDescription(dataStream), bytesLeft);
            [dataStream writeData:data];
            // NSLog(@"%@ readChunkedBody: wrote data to %@", OBShortObjectDescription(self), OBShortObjectDescription(dataStream));
            if (bytesInThisPool > 64 * 1024) {
                [autoreleasePool release];
                autoreleasePool = [[NSAutoreleasePool alloc] init];
                bytesInThisPool = 0;
            }
            if (fetchFlags.aborting)
                [NSException raise:@"User Aborted" format:@"User Aborted"];
        }
        [socketStream readLine];
        [autoreleasePool release];
    }
    [headerDictionary readRFC822HeadersFromSocketStream:socketStream];
}

- (void)readStandardBodyIntoStream:(OWDataStream *)dataStream;
{
    unsigned int contentLength, bytesLeft;
    NSAutoreleasePool *autoreleasePool = nil;
    NSData *data;
    unsigned int byteCount, bytesInThisPool;

    contentLength = [[headerDictionary lastStringForKey:@"content-length"] intValue];

    autoreleasePool = [[NSAutoreleasePool alloc] init];
    bytesInThisPool = 0;
    byteCount = 0;
    bytesLeft = contentLength;
    // NSLog(@"%@ readStandardBody: start: processed bytes %d of %d for dataStream %@, bytesLeft = %d", OBShortObjectDescription(self), byteCount, contentLength, OBShortObjectDescription(dataStream), bytesLeft);
    [nonretainedProcessor processedBytes:byteCount ofBytes:contentLength];

    while (bytesLeft && (data = [socketStream readDataWithMaxLength:bytesLeft])) {
        unsigned int dataLength;

        dataLength = [data length];
        byteCount += dataLength;
        bytesLeft -= dataLength;
        bytesInThisPool += dataLength;
        [nonretainedProcessor processedBytes:byteCount ofBytes:contentLength];
        // NSLog(@"%@ readStandardBody: processed bytes %d of %d for dataStream %@, bytesLeft = %d", OBShortObjectDescription(self), byteCount, contentLength, OBShortObjectDescription(dataStream), bytesLeft);
        [dataStream writeData:data];
        // NSLog(@"%@ readStandardBody: wrote data to %@", OBShortObjectDescription(self), OBShortObjectDescription(dataStream));
        if (bytesInThisPool > 64 * 1024) {
            [autoreleasePool release];
            autoreleasePool = [[NSAutoreleasePool alloc] init];
            bytesInThisPool = 0;
        }
        if (fetchFlags.aborting)
            [NSException raise:@"User Aborted" format:@"User Aborted"];
    }
    [autoreleasePool release];
}

- (void)readClosingBodyIntoStream:(OWDataStream *)dataStream;
{
    NSAutoreleasePool *autoreleasePool = nil;
    NSData *data;
    unsigned int byteCount, bytesInThisPool;

    autoreleasePool = [[NSAutoreleasePool alloc] init];
    bytesInThisPool = 0;
    byteCount = 0;
    // NSLog(@"%@ readClosingBody: start: processed bytes %d for dataStream %@", OBShortObjectDescription(self), byteCount, OBShortObjectDescription(dataStream));
    [nonretainedProcessor processedBytes:byteCount ofBytes:0];

    NS_DURING {
        while ((data = [socketStream readData])) {
            unsigned int dataLength;

            dataLength = [data length];
            byteCount += dataLength;
            bytesInThisPool += dataLength;
            [nonretainedProcessor processedBytes:byteCount ofBytes:0];
            // NSLog(@"%@ readClosingBody: processed bytes %d for dataStream %@", OBShortObjectDescription(self), byteCount, OBShortObjectDescription(dataStream));
            [dataStream writeData:data];
            // NSLog(@"%@ readClosingBody: wrote data to %@", OBShortObjectDescription(self), OBShortObjectDescription(dataStream));
            if (bytesInThisPool > 64 * 1024) {
                [autoreleasePool release];
                autoreleasePool = [[NSAutoreleasePool alloc] init];
                bytesInThisPool = 0;
            }
            if (fetchFlags.aborting)
                break;
        }        
    } NS_HANDLER {
    } NS_ENDHANDLER;

    if (fetchFlags.aborting)
        [NSException raise:@"User Aborted" format:@"User Aborted"];
    [autoreleasePool release];
}

- (void)readBodyAndIgnore:(BOOL)ignoreThis;
{
    OWContentType *contentType;

    if (ignoreThis) {
        interruptedDataStream = nil;
    } else if (!interruptedDataStream) {
        interruptedDataStream = [[OWDataStream alloc] init];
        contentType = [headerDictionary contentType];
        [interruptedDataStream setContentType:contentType];
        [interruptedDataStream setContentEncoding:[headerDictionary contentEncoding]];
        [nonretainedPipeline addSourceContent:interruptedDataStream];
        [nonretainedPipeline cacheContent];
        [nonretainedPipeline startProcessingContent];
        if ([queue shouldPipelineRequests]) {
            // If it's not okay to pipeline requests, then we can't fetch partial ranges anyway, so we don't want to cache this data stream.
            [nonretainedProcessor setDataStream:interruptedDataStream];
        }
    }
    
    [nonretainedProcessor setStatusFormat:@"Reading document from %@", [proxyLocation shortDisplayString]];

    if ([[headerDictionary lastStringForKey:@"transfer-encoding"] isEqualToString:@"chunked"])
        [self readChunkedBodyIntoStream:interruptedDataStream];
    else if ([headerDictionary lastStringForKey:@"content-length"])
        [self readStandardBodyIntoStream:interruptedDataStream];
    else
        [self readClosingBodyIntoStream:interruptedDataStream];

    // NSLog(@"%@: ending data stream %@", OBShortObjectDescription(self), OBShortObjectDescription(interruptedDataStream));
    [interruptedDataStream dataEnd];
    // NSLog(@"%@: ended data stream %@", OBShortObjectDescription(self), OBShortObjectDescription(interruptedDataStream));
}

- (BOOL)readHead;
{
    NSString *line;
    NSScanner *scanner;
    float httpVersion;
    HTTPStatus httpStatus;
    NSString *commentString;
    OWTimeStamp *stamp = nil;

    [nonretainedProcessor setStatusFormat:@"Awaiting document info from %@", [proxyLocation shortDisplayString]];
    if (!(line = [socketStream peekLine]))
        return NO;
    scanner = [NSScanner scannerWithString:line];

    if (![scanner scanString:@"HTTP/" intoString:NULL]) {
        // Stinky 0.9 server, so can't know time
        stamp = [OWTimeStamp cacheDate:[NSDate distantPast] forAddress:address];
        [nonretainedPipeline addContent:stamp];
        [nonretainedPipeline startProcessingContent];
        return YES;
    }

    [socketStream readLine];
    [scanner scanFloat:&httpVersion];
    if (httpVersion > 1.0) {
        [queue setServerUnderstandsPipelinedRequests];
    }
    [scanner scanInt:(int *)&httpStatus];
    if (![scanner scanUpToString:@"\n" intoString:&commentString])
        commentString = @"";

processStatus:
    switch (httpStatus) {

        // 200 codes - Got MIME object

        case HTTP_STATUS_OK:
        case HTTP_STATUS_CREATED:
        case HTTP_STATUS_ACCEPTED:
        case HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION:
        case HTTP_STATUS_NO_CONTENT:
        case HTTP_STATUS_RESET_CONTENT:
        case HTTP_STATUS_PARTIAL_CONTENT:
            [self readHeaders];
            return YES;

        // 300 codes - Various forms of redirection

        case HTTP_STATUS_MULTIPLE_CHOICES:
            break;

        case HTTP_STATUS_MOVED_PERMANENTLY:
        case HTTP_STATUS_MOVED_TEMPORARILY:
            {
                OWAddress *newLocation;

                [self readHeaders];
                newLocation = [OWAddress addressForString:
                    [headerDictionary lastStringForKey:@"location"]];
                if (httpStatus == HTTP_STATUS_MOVED_PERMANENTLY)
                    [nonretainedPipeline permanentRedirection:newLocation];
                [nonretainedPipeline addSourceContent:newLocation];
                // TODO: Should we cacheContent even for temporary redirections? If not, maybe -cacheContent should do the permanent redirection stuff that -permanentRedirection: does.
                [nonretainedPipeline cacheContent];
                [nonretainedPipeline startProcessingContent];
            }
            break;

        case HTTP_STATUS_SEE_OTHER:
        case HTTP_STATUS_NOT_MODIFIED:
        case HTTP_STATUS_USE_PROXY:
            break;

        // 400 codes - Access Authorization problem

        case HTTP_STATUS_UNAUTHORIZED:
            // TODO: authorization

        case HTTP_STATUS_PAYMENT_REQUIRED:
        case HTTP_STATUS_FORBIDDEN:
        case HTTP_STATUS_NOT_FOUND:
            stamp = [OWTimeStamp cacheDate:[NSDate distantFuture] forAddress:address];
            break;

        case HTTP_STATUS_BAD_REQUEST:
            // fall through to 500 codes

        // 500 codes - Server error

        case HTTP_STATUS_INTERNAL_SERVER_ERROR:
        case HTTP_STATUS_BAD_GATEWAY:
        case HTTP_STATUS_NOT_IMPLEMENTED: 
        case HTTP_STATUS_SERVICE_UNAVAILABLE:
        case HTTP_STATUS_GATEWAY_TIMEOUT:
            // ignore it and try later?
            stamp = [OWTimeStamp cacheDate:[NSDate distantPast] forAddress:address];
            break;

        // Unrecognized client code, treat as x00

        default:
            {
                HTTPStatus equivalentStatus;

                equivalentStatus = httpStatus - httpStatus % 100;
                if (equivalentStatus == httpStatus)
                    httpStatus = HTTP_STATUS_NOT_IMPLEMENTED;
                else
                    httpStatus = equivalentStatus;
            }
            goto processStatus;
    }
    [nonretainedPipeline addContent:stamp];
    [nonretainedPipeline startProcessingContent];
    return YES;
}

- (void)readTimeStamp;
{
    NSDate *date = nil;
    NSString *string;
    OWTimeStamp *stamp;

    string = [headerDictionary lastStringForKey:@"last-modified"];
    if (string) {
        date = [NSDate dateWithHTTPDateString:string];
    }
    if (!date)
        date = [NSDate distantPast];
    stamp = [OWTimeStamp cacheDate:date forAddress:address];
}

@end
