// Copyright 1997-2002 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 "OWAuthorizationCredential.h"
#import "OWAuthorizationRequest.h"
#import "OWContentCache.h"
#import "OWContentType.h"
#import "OWContentContainer.h"
#import "OWParameterizedContentType.h"
#import "OWCookieDomain.h"
#import "OWCookie.h"
#import "OWDataStream.h"
#import "OWHeaderDictionary.h"
#import "OWHTTPProcessor.h"
#import "OWHTTPSessionQueue.h"
#import "OWNetLocation.h"
#import "OWPipeline.h"
#import "OWSourceProcessor.h"
#import "OWTimeStamp.h"
#import "OWUnknownDataStreamProcessor.h"
#import "OWURL.h"
#import "OWWebPipeline.h"

#define DEBUG_TRANSACTIONS

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Processors.subproj/Protocols.subproj/HTTP.subproj/OWHTTPSession.m,v 1.117 2002/03/09 01:53:52 kc Exp $")

@interface OWHTTPSession (Private)

+ (void)_calculatePrimaryUserAgentInfo;
+ (NSString *)stringForHeader:(NSString *)aHeader value:(id)aValue;

// Generating request
- (NSString *)commandStringForAddress:(OWAddress *)anAddress;
- (NSString *)acceptHeadersStringForTarget:(id <OWTarget>)aTarget;
- (NSString *)acceptLanguageHeadersString;
- (NSString *)referrerHeaderStringForPipeline:(OWWebPipeline *)aPipeline;
- (NSString *)cacheControlHeaderStringForPipeline:(OWWebPipeline *)aPipeline;
- (NSString *)pragmaHeaderStringForPipeline:(OWWebPipeline *)aPipeline;
- (NSString *)hostHeaderStringForURL:(OWURL *)aURL;
- (NSString *)rangeStringForProcessor:(OWHTTPProcessor *)aProcessor;
- (NSString *)keepAliveString;
- (NSString *)cookiesForURL:(OWURL *)aURL;
- (NSString *)contentTypeHeaderStringForAddress:(OWAddress *)anAddress;
- (NSString *)contentLengthHeaderStringForAddress:(OWAddress *)anAddress;
- (NSString *)contentStringForAddress:(OWAddress *)anAddress;

// Reading results
- (BOOL)readResponseForProcessor:(OWHTTPProcessor *)processor inPipeline:(OWPipeline *)pipeline;
- (void)readBodyForPipeline:(OWPipeline *)pipeline processor:(OWHTTPProcessor *)processor ignore:(BOOL)ignoreThis;
- (BOOL)readHeadForProcessor:(OWHTTPProcessor *)processor inPipeline:(OWPipeline *)pipeline;
- (void)readTimeStamp;
- (void)readHeadersForPipeline:(OWPipeline *)pipeline;
- (unsigned int)intValueFromHexString:(NSString *)aString;
- (void)readChunkedBodyIntoStream:(OWDataStream *)dataStream forProcessor:(OWProcessor *)processor;
- (void)readStandardBodyIntoStream:(OWDataStream *)dataStream forProcessor:(OWProcessor *)processor;
- (void)readClosingBodyIntoStream:(OWDataStream *)dataStream forProcessor:(OWProcessor *)processor;

// Exception handling
- (void)notifyProcessor:(OWHTTPProcessor *)aProcessor ofSessionException:(NSException *)sessionException;

@end

@implementation OWHTTPSession

static OFPreference *_userAgentHeaderFormatPreference;


static BOOL OWHTTPDebug = NO;
static BOOL OWHTTPCredentialsDebug = NO;
static NSMutableDictionary *languageAbbreviationsDictionary;
static NSString *preferredDateFormat;
static NSString *acceptLanguageString = nil;
static NSString *http10VersionString;
static NSString *http11VersionString;
static NSString *endOfLineString;
static NSString *_primaryUserAgentInfo = nil;
static NSMutableArray *languageArray = nil;
static NSCharacterSet *spaceCharacterSet;
static NSCharacterSet *nonTokenCharacterSet;
static OWContentType *textPlainContentType;
static OWContentType *applicationOctetStreamContentType;

+ (void)initialize;
{
    NSMutableCharacterSet *tokenCharacterSet;
    
    OBINITIALIZE;

    http10VersionString = @"HTTP/1.0";
    http11VersionString = @"HTTP/1.1";
    endOfLineString = @"\r\n";
    languageAbbreviationsDictionary = [[NSMutableDictionary alloc] initWithCapacity:8];
    spaceCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:@" "] retain];
    textPlainContentType = [OWContentType contentTypeForString:@"text/plain"];
    applicationOctetStreamContentType = [OWContentType contentTypeForString:@"application/octet-stream"];

    // This is the character set allowed for tokens, [RFC2068 section 2.2]
    tokenCharacterSet = [[NSMutableCharacterSet alloc] init];
    // All US-ASCII chars except controls (0-31) and DEL (127)
    [tokenCharacterSet addCharactersInRange:NSMakeRange(32, (127 - 32))];
    // Remove 'tspecials'
    [tokenCharacterSet removeCharactersInString:@"()<>@,;:\\\"/[]?={} "];
    
    nonTokenCharacterSet = [[tokenCharacterSet invertedSet] retain];
    [tokenCharacterSet release];
}

// 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;
{
    [[OFController sharedController] addObserver:self];
}

+ (void)controllerDidInitialize:(OFController *)controller;
{
    [self _calculatePrimaryUserAgentInfo];
    _userAgentHeaderFormatPreference = [[OFPreference preferenceForKey:@"OWHTTPUserAgentHeaderFormat"] retain];
    [self readDefaults];
}

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

+ (int)defaultPort;
{
    return 80;
}

+ (void)readDefaults;
{
    NSUserDefaults *defaults;
    NSArray *systemLanguages;
    unsigned int systemLanguageIndex, systemLanguageCount;

    defaults = [NSUserDefaults standardUserDefaults];

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

    if (languageArray != nil)
        [languageArray release];
    if (acceptLanguageString != nil)
        [acceptLanguageString release];
    
    // OSX stores the user's language preferences in AppleLanguages, using the ISO abbreviations --- very convenient, but see below.
    systemLanguages = [[defaults stringArrayForKey:@"AppleLanguages"] copy];
    if (systemLanguages == nil) {
        // Fall back on NSLanguages (perhaps we should remove this code at some point)
        systemLanguages = [defaults stringArrayForKey:@"NSLanguages"];
    }

    systemLanguageCount = (systemLanguages != nil)? [systemLanguages count] : 0;
    languageArray = [[NSMutableArray alloc] initWithCapacity:systemLanguageCount];

    // Apple bug fix for 4K78: the system preference is normally in ISO format. However, if it isn't set, then the *default* value is in the old (pre-OSX) format, which we then have to convert to the IANA abbreviations. Bah! (This conversion is also necessary if we got language prefs from NSLanguages.)
    for (systemLanguageIndex = 0; systemLanguageIndex < systemLanguageCount; systemLanguageIndex++) {
        NSString *systemLanguage;
        NSString *languageAbbreviation;
        NSMutableString *mutableLanguageAbbreviation;
        NSString *immutableLanguageAbbreviation;

        systemLanguage = [systemLanguages objectAtIndex:systemLanguageIndex];
        languageAbbreviation = [languageAbbreviationsDictionary objectForKey:systemLanguage];
        if (languageAbbreviation == nil)
            languageAbbreviation = systemLanguage;

        // Note:  the system preference separates subtypes from languages with underscores (e.g. en_US for us-english). RFC2068 [3.10] specifies that subtypes are separated with hyphens (e.g. en-us).
        mutableLanguageAbbreviation = [[languageAbbreviation lowercaseString] mutableCopy];
        [mutableLanguageAbbreviation replaceAllOccurrencesOfString:@"_" withString:@"-"];
        immutableLanguageAbbreviation = [mutableLanguageAbbreviation copy];
        [mutableLanguageAbbreviation release];
        mutableLanguageAbbreviation = nil;

        [languageArray addObject:immutableLanguageAbbreviation];
        [immutableLanguageAbbreviation release];
    }

    OBASSERT(systemLanguageCount == [languageArray count]);
    if (systemLanguageCount > 0) {
        NSMutableArray *acceptLanguages;
        NSString *qualityFormatString;
        float acceptLanguageCount;

        if (systemLanguageCount < 10) {
            qualityFormatString = @"%@;q=%0.1f";
        } else if (systemLanguageCount < 100) {
            qualityFormatString = @"%@;q=%0.2f";
        } else {
            OBASSERT(systemLanguageCount < 1000); // If not, too bad!  According to RFC2616, a qvalue can only have three digits after the decimal point
            qualityFormatString = @"%@;q=%0.3f";
        }
        acceptLanguageCount = systemLanguageCount + 1.0; // system languages + "*"
        acceptLanguages = [[NSMutableArray alloc] initWithCapacity:systemLanguageCount];
        for (systemLanguageIndex = 0; systemLanguageIndex < systemLanguageCount; systemLanguageIndex++) {
            NSString *language;

            language = [languageArray objectAtIndex:systemLanguageIndex];
            if (systemLanguageIndex == 0) {
                [acceptLanguages addObject:language]; // q=1.0 is redundant
            } else {
                float quality;

                quality = (acceptLanguageCount - systemLanguageIndex) / acceptLanguageCount;
                [acceptLanguages addObject:[NSString stringWithFormat:qualityFormatString, language, quality]];
            }
        }
        [acceptLanguages addObject:[NSString stringWithFormat:qualityFormatString, @"*", 1.0 / acceptLanguageCount]]; // End with "*;q=0.01"
        acceptLanguageString = [[self stringForHeader:@"Accept-Language" value:[acceptLanguages componentsJoinedByString:@", "]] retain];
    } else
        acceptLanguageString = nil;
}

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

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

    defaults = [NSUserDefaults standardUserDefaults];

    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 *)userAgentHeaderFormatString;
{
    return [_userAgentHeaderFormatPreference stringValue];
}

+ (void)setUserHeaderAgentFormatString:(NSString *)userAgentHeaderFormatString;
{
    [_userAgentHeaderFormatPreference setStringValue: userAgentHeaderFormatString];
}

+ (NSString *)userAgentInfoForServerAtNetLocation:(OWNetLocation *)whatServer;
{
    NSString *userAgentHeaderFormatString;

    userAgentHeaderFormatString = [self userAgentHeaderFormatString];
    if ([userAgentHeaderFormatString containsString:@"%@"]) {
        // The user agent string has a spot for us to insert our true identity
        return [NSString stringWithFormat:userAgentHeaderFormatString, _primaryUserAgentInfo];
    } else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"OWHTTPHideTrueIdentity"]) {
        // We're supposed to hide our true identity, so return the user agent string exactly as is
        return userAgentHeaderFormatString;
    } else {
        // Our true identity wasn't included, so we'll just add it to the very end (as Netscape 6.1 does)
        return [NSString stringWithFormat:@"%@ %@", userAgentHeaderFormatString, _primaryUserAgentInfo];
    }
}

+ (NSString *)primaryUserAgentInfo;
{
    if (_primaryUserAgentInfo == nil)
        [self _calculatePrimaryUserAgentInfo];
    return _primaryUserAgentInfo;
}

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

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

+ (NSCharacterSet *)nonTokenCharacterSet
{
    return nonTokenCharacterSet;
}

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

    queue = aQueue;
    proxyURL = [anAddress proxyURL];
    realURL = [anAddress url];
    flags.connectingViaProxyServer = (proxyURL != realURL);
    proxyLocation = [[proxyURL parsedNetLocation] retain];
    processorQueue = [[NSMutableArray alloc] initWithCapacity:[queue maximumNumberOfRequestsToPipeline]];
    flags.pipeliningRequests = NO;
    failedRequests = 0;

    return self;
}

- (void)dealloc;
{
    [self disconnectAndRequeueProcessors];
    [proxyLocation release];
    proxyLocation = nil; // Why?  Thread-safety issues?  We should fix those instead--this isn't a reliable cure, since the other thread could access proxyLocation after we release but before we reset it.
    [processorQueue release];
    processorQueue = nil;
    [super dealloc];
}

- (void)runSession;
{
    do {
        NSException *sessionException = nil;
        OWHTTPProcessor *aProcessor;

        OMNI_POOL_START {
            NS_DURING {
                BOOL continueSession;

                do {
                    OMNI_POOL_START {
                        continueSession = [self sendRequest];
                        if (continueSession) {
                            aProcessor = [processorQueue objectAtIndex:0];
                            continueSession = [self fetchForProcessor:aProcessor inPipeline:[aProcessor pipeline]];
                            if (continueSession) {
                                [processorQueue removeObjectAtIndex:0];
                                if (!flags.pipeliningRequests)
                                    continueSession = NO;
                            }
                        }
                    } OMNI_POOL_END;
                } while (continueSession);
                [self disconnectAndRequeueProcessors];
            } NS_HANDLER {
                sessionException = localException;
            } NS_ENDHANDLER;
            if (sessionException != nil) {
                unsigned int processorIndex, processorCount;

                // Notify processors
                processorCount = [processorQueue count];
                for (processorIndex = 0; processorIndex < processorCount; processorIndex++) {
                    aProcessor = [processorQueue objectAtIndex:processorIndex];
                    [self notifyProcessor:aProcessor ofSessionException:sessionException];
                }
                [processorQueue removeAllObjects];
                [self disconnectAndRequeueProcessors];
                if (processorCount == 0) {
                    // -sendRequest raised an exception before we even started looking for processors (e.g., when trying to connect to a server which is down or doesn't exist).  Send the exception to all queued processors.
                    while ((aProcessor = [queue nextProcessor])) {
                        [self notifyProcessor:aProcessor ofSessionException:sessionException];
                    }
                }
            }
        } OMNI_POOL_END;
    } while (![queue sessionIsIdle:self]);
    
    // At this point we are idle. If we've gotten to this point without ever sending any requests (due to the race condition in the above loop), disconnect from the server. The reason for this is that HTTP/1.0 servers are not allowed to drop connections except after a response, and if we haven't sent any requests, the server does not know our version and must assume we are HTTP/1.0. 
    if (requestsSentThisConnection == 0)
        [self disconnectAndRequeueProcessors];
}

- (BOOL)prepareConnectionForProcessor:(OWProcessor *)aProcessor;
{
    // The HTTPS plug-in subclasses this method to support SSL-Tunneling
    return YES;
}

- (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 (fetchAddress)
        [debugDictionary setObject: fetchAddress forKey:@"fetchAddress"];
    if (socketStream)
        [debugDictionary setObject:socketStream forKey:@"socketStream"];
    if (headerDictionary)
        [debugDictionary setObject:headerDictionary forKey:@"headerDictionary"];

    return debugDictionary;
}

@end


@implementation OWHTTPSession (SubclassesOnly)

- (void)connect;
{
    NSString *port;
    ONInternetSocket *socket;
    ONHost *host;
    Class socketClass;
    NSBundle *myBundle = [OWHTTPSession bundle];
    
    requestsSentThisConnection = 0;
    
    [self setStatusFormat:NSLocalizedStringFromTableInBundle(@"Finding %@", @"OWF", myBundle, http session status), [proxyLocation shortDisplayString]];
    port = [proxyLocation port];
    host = [ONHost hostForHostname:[proxyLocation hostname]];
    [self setStatusFormat:NSLocalizedStringFromTableInBundle(@"Contacting %@", @"OWF", myBundle, http session status), [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.)
    socketClass = [isa socketClass];
    socket = (ONInternetSocket *)objc_msgSend(socketClass, @selector(socket));
    [socket setReadBufferSize:32 * 1024];

    OBASSERT(!socketStream);
    socketStream = [[ONSocketStream alloc] initWithSocket:socket];
    [socket connectToHost:host port:port ? [port intValue] : [isa defaultPort]];

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

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

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

    // If we have a socket stream, release it
    [socketStream release];
    socketStream = nil;
}

- (BOOL)fetchForProcessor:(OWHTTPProcessor *)aProcessor inPipeline:(OWPipeline *)aPipeline;
{
    NSException *sessionException = nil;
    BOOL finishedProcessing = NO;
     
    [aProcessor processBegin];
    
    fetchAddress = [[aPipeline lastAddress] retain];
    fetchURL = [[fetchAddress url] retain];
    headerDictionary = [[OWHeaderDictionary alloc] init];
    interruptedDataStream = [[aProcessor dataStream] retain];

    NS_DURING {
        if ([[fetchAddress methodString] isEqualToString:@"HEAD"])
            finishedProcessing = [self readHeadForProcessor:aProcessor inPipeline:aPipeline];
        else
            finishedProcessing = [self readResponseForProcessor:aProcessor inPipeline:aPipeline];
        failedRequests = 0;
    } NS_HANDLER {
#ifdef DEBUG_kc
        NSLog(@"%@(%@): Caught exception: %@", [fetchAddress addressString], OBShortObjectDescription(self), [localException reason]);
#endif
        if ([[localException name] isEqualToString:ONInternetSocketReadFailedExceptionName] && [localException posixErrorNumber] == ECONNRESET) {
            // Unable to read from socket: Connection reset by peer
            if (flags.pipeliningRequests) {
                // This HTTP 1.1 connection was reset by the server
                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 {
                // Our HTTP/1.0 connection appears to have been dropped:  overloaded server, perhaps?  Let's retry a few times.
                failedRequests++;
                if (interruptedDataStream != nil || failedRequests > 3) {
                    sessionException = [localException retain];
                    [interruptedDataStream dataAbort];
                } else if (failedRequests > 1) {
                    // If this isn't our first retry, give the server a slight rest before connecting again
                    [[NSDate dateWithTimeIntervalSinceNow:3.0] sleepUntilDate];
                }
            }
        } else {
            // Abort the data stream and reraise the exception
            sessionException = [localException retain];
            [interruptedDataStream dataAbort];
        }
    } NS_ENDHANDLER;            

    fetchFlags.aborting = NO;
    if (sessionException) {
        [self notifyProcessor:aProcessor ofSessionException:sessionException];
    } else if (finishedProcessing) {
        [aProcessor processEnd];
        [aProcessor retire];        
    } 
        

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

- (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 authorizationStringForAddress:anAddress processor:aProcessor]];
    [requestString appendString:[self cookiesForURL:aURL]];
    // TODO: For bookmarks refreshes, we should support If-Modified-Since:
    
    if ([requestMethod isEqualToString:@"POST"] ||
        [requestMethod isEqualToString:@"PUT"]) {
        NSDictionary *methodDictionary;
        NSData *headersAndData;
        
        methodDictionary = [anAddress methodDictionary];
        headersAndData = [methodDictionary objectForKey: OWAddressContentHeadersAndDataMethodKey];
        if (headersAndData) {
            // This case is used in the Netscape plugin support where the plugin wants to be able to append headers and data.  In this case, the plugin must supply the content type and content length (well, it should -- the iTools plugin does and I haven't seen this used anywhere else yet).
        } else {
            [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;
}

- (NSString *)authorizationStringForAddress:(OWAddress *)anAddress processor:(OWHTTPProcessor *)aProcessor;
{
    NSMutableArray *credentialsToSend;
    OWAuthorizationRequest *serverAuthorization, *proxyAuthorization;
    NSMutableString *buffer;
    unsigned credentialCount, credentialIndex;
    
    if (flags.connectingViaProxyServer && proxyCredentials == nil) {
        proxyAuthorization = [[[OWAuthorizationRequest authorizationRequestClass] alloc] initForType:OWAuth_HTTP_Proxy netLocation:proxyLocation defaultPort:[isa defaultPort] pipeline:[aProcessor pipeline] challenge:nil promptForMoreThan:nil];
    } else
        proxyAuthorization = nil;
    
    if ([aProcessor credentials] == nil) {
        serverAuthorization = [[[OWAuthorizationRequest authorizationRequestClass] alloc] initForType:OWAuth_HTTP netLocation:[[anAddress url] parsedNetLocation] defaultPort:[isa defaultPort] pipeline:[aProcessor pipeline] challenge:nil promptForMoreThan:nil];
    } else
        serverAuthorization = nil;
    
    // Create both AuthorizationRequests before asking either one for its credentials
    
    if (proxyAuthorization != nil) {
        OBASSERT(proxyCredentials == nil); // Or we would need to release them here
        proxyCredentials = [proxyAuthorization credentials];
        if (proxyCredentials && [proxyCredentials count] > 1)
            proxyCredentials = [proxyCredentials subarrayWithRange:NSMakeRange(0,1)];
        [proxyCredentials retain];
        [proxyAuthorization release];
    }
    
    if (serverAuthorization != nil) {
        NSArray *serverCredentials = [serverAuthorization credentials];
        if (serverCredentials && [serverCredentials count])
            [aProcessor addCredential:[serverCredentials objectAtIndex:0]];
        [serverAuthorization release];
    }
    
    credentialsToSend = [[NSMutableArray alloc] init];
    if (OWHTTPCredentialsDebug)
        NSLog(@"%@: proxy credentials = %@, using proxy = %d", [anAddress addressString], proxyCredentials, flags.connectingViaProxyServer);
    if (flags.connectingViaProxyServer && proxyCredentials != nil && [proxyCredentials count])
        [credentialsToSend addObject:[proxyCredentials objectAtIndex:0]];
    if (OWHTTPCredentialsDebug)
        NSLog(@"%@: server credentials = %@", [anAddress addressString], [aProcessor credentials]);
    if ([aProcessor credentials])
        [credentialsToSend addObject:[[aProcessor credentials] objectAtIndex:0]];
    
    credentialCount = [credentialsToSend count];
    if (credentialCount == 0) {
        [credentialsToSend release];
        return @"";
    }
    
    buffer = [[[NSMutableString alloc] init] autorelease];
    for(credentialIndex = 0; credentialIndex < credentialCount; credentialIndex++) {
        NSString *headerString;

        headerString = [[credentialsToSend objectAtIndex:credentialIndex] httpHeaderStringForProcessor:aProcessor];
        if (headerString) {
            [buffer appendString:headerString];
            [buffer appendString:endOfLineString];
        }
    }
    
    [credentialsToSend release];
    
    return buffer;
}

- (NSString *)userAgentHeaderStringForURL:(OWURL *)aURL;
{
    NSString *userAgent;

    userAgent = [isa userAgentInfoForServerAtNetLocation:[aURL parsedNetLocation]];
    if (userAgent == nil)
        return @"";
    else
        return [isa stringForHeader:@"User-Agent" value:userAgent];
}

- (BOOL)sendRequest;
{    
    if (![(ONInternetSocket *)[socketStream socket] isWritable]) {
        [self disconnectAndRequeueProcessors];
        [self connect];
    }
    NS_DURING {
        [self sendRequests];
    } NS_HANDLER {
        BOOL problemIsTransient = NO;

        if ([[localException name] isEqualToString:ONInternetSocketWriteFailedExceptionName]) {
            switch ([localException posixErrorNumber]) {
                case EPIPE:
                case ECONNRESET:
                    // We'll try again in a bit
                    problemIsTransient = YES;
                    break;
                default:
                    break;
            }
        }
        if (!problemIsTransient) {
            // Reraise the exception
            [localException raise];
        }
    } NS_ENDHANDLER;
    return ([processorQueue count] != 0);
}

- (BOOL)sendRequests;
{
    NSData *requestData;
    NSString *requestString;
    OWAddress *anAddress;
    unsigned int queueIndex, queueCount;
    BOOL shouldPipelineRequests;
    unsigned int maximumNumberOfRequestsToSend;

    // figure out how many requests to send
    shouldPipelineRequests = [queue shouldPipelineRequests];
    queueCount = [processorQueue count];
    // We've already sent requests for the processors in processorQueue
    queueIndex = queueCount;
    if (shouldPipelineRequests) {
        maximumNumberOfRequestsToSend = [queue maximumNumberOfRequestsToPipeline];
        flags.pipeliningRequests = YES;
    } else {
        maximumNumberOfRequestsToSend = 1;
    }
    // Fill our queue
    while (queueCount < maximumNumberOfRequestsToSend) {
        OWHTTPProcessor *aProcessor;

        aProcessor = [queue nextProcessor];
        if (aProcessor == nil)
            break;
        switch ([aProcessor status]) {
            case OWProcessorStarting:
            case OWProcessorQueued:
                [aProcessor processBegin];
                // Fall through to running state
            case OWProcessorRunning:
                [processorQueue addObject:aProcessor];
                [aProcessor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Preparing to request document from %@", @"OWF", [OWHTTPSession bundle], httpsession status), [proxyLocation shortDisplayString]];
                queueCount++;
                break;
            case OWProcessorAborting:
                [aProcessor retire];
                // Fall through to retired state
            case OWProcessorRetired:
                break;
        }
    }

    // Send requests for each new processor in the queue
    for (; queueIndex < queueCount; queueIndex++) {
        OWHTTPProcessor *aProcessor;

        aProcessor = [processorQueue objectAtIndex:queueIndex];
        if ([self prepareConnectionForProcessor:aProcessor]) {
            NSDictionary *methodDictionary;
            
            anAddress = (OWAddress *)[[aProcessor pipeline] lastAddress];
            requestString = [self requestStringForProcessor:aProcessor];
            if (OWHTTPDebug)
                NSLog(@"%@ Tx: %@", [[anAddress url] scheme], requestString);
            [aProcessor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Requesting document from %@", @"OWF", [OWHTTPSession bundle], httpsession status), [proxyLocation shortDisplayString]];
            [socketStream writeString:requestString];
            
            methodDictionary = [anAddress methodDictionary];
            requestData = [methodDictionary objectForKey: OWAddressContentHeadersAndDataMethodKey];
            if (!requestData)
                requestData = [methodDictionary objectForKey:OWAddressContentDataMethodKey];
                
            if (requestData) {
                if (OWHTTPDebug)
                    NSLog(@"Tx: %@", [NSString stringWithCString: [requestData bytes] length: [requestData length]]);
                [socketStream writeData:requestData];
            }
            
            requestsSentThisConnection++;
            
#ifdef DEBUG_TRANSACTIONS
            {
                OWContentContainer *txRequest = [OWContentContainer containerWithContent:requestString contentType:[OWContentType contentTypeForString:@"omni/request-string"]];
                [[[aProcessor pipeline] contentCacheForLastAddress] addContent:txRequest];
            }
#endif
        }
    }
    return queueIndex != 0;
}

@end

@implementation OWHTTPSession (Private)

+ (void)_calculatePrimaryUserAgentInfo;
{
    NSBundle *mainBundle;
    NSDictionary *mainBundleInfoDictionary;
    NSString *primaryAgentName, *primaryAgentVersion, *primaryAgentBuild, *primaryAgentCombinedVersion;

    mainBundle = [NSBundle mainBundle];
    mainBundleInfoDictionary = [mainBundle infoDictionary];
    primaryAgentName = [mainBundleInfoDictionary objectForKey:@"CFBundleName"];
    primaryAgentVersion = [mainBundleInfoDictionary objectForKey:@"CFBundleShortVersionString"];
    primaryAgentBuild = [mainBundleInfoDictionary objectForKey:@"CFBundleVersion"];
    if (![NSString isEmptyString:primaryAgentBuild])
        primaryAgentBuild = [@"v" stringByAppendingString:primaryAgentBuild]; // 319 -> v319
    if (![NSString isEmptyString:primaryAgentVersion]) {
        NSRange firstWordRange;
        NSString *firstWord;

        // Trim "4.0 beta 1" to "4.0".  (They can get full details by looking up the build number.)
        firstWordRange = [primaryAgentVersion rangeOfCharactersAtIndex:0 delimitedBy:[NSCharacterSet whitespaceCharacterSet]];
        firstWord = [primaryAgentVersion substringWithRange:firstWordRange];
        if (![NSString isEmptyString:primaryAgentBuild])
            primaryAgentCombinedVersion = [NSString stringWithFormat:@"%@-%@", firstWord, primaryAgentBuild];
        else
            primaryAgentCombinedVersion = firstWord;
    } else {
        primaryAgentCombinedVersion = primaryAgentBuild;
    }
    if (![NSString isEmptyString:primaryAgentCombinedVersion]) {
        _primaryUserAgentInfo = [[NSString alloc] initWithFormat:@"%@/%@", primaryAgentName, primaryAgentCombinedVersion];
    } else {
        _primaryUserAgentInfo = [primaryAgentName copy];
    }
}

+ (NSString *)stringForHeader:(NSString *)aHeader value:(id)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;
}

//
// Generating request
//

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

    aURL = [anAddress url];
    command = [NSMutableString stringWithCapacity:128];
    [command appendStrings:[anAddress methodString], @" ", nil];
    if (flags.connectingViaProxyServer)
        fetchPath = [aURL proxyFetchPath];
    else
        fetchPath = [aURL fetchPath];
    
    // Last-ditch quoting of the URL path. This is here to catch mistakes (e.g. unquoted spaces) and it is also here to support IURIs (URIs containing characters outside the ASCII range).
    if (fetchPath)
        fetchPath = [fetchPath fullyEncodeAsIURI];
    else
        fetchPath = @"*";  // see rfc2616 [5.1.2]

    [command appendStrings:fetchPath, @" ", (flags.pipeliningRequests ? http11VersionString : http10VersionString), nil];
    [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;
    NSString *userString;
    
    if ([[NSUserDefaults standardUserDefaults] 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

        wildcardContentTypeString = [[OWContentType wildcardContentType] contentTypeString];
        acceptsArray = [[NSMutableArray alloc] initWithCapacity:16];
        possibleTypesEnumerator = [[[aTarget targetContentType] indirectSourceContentTypes] objectEnumerator];
        while ((possibleType = [possibleTypesEnumerator nextObject])) {
            if ([possibleType isPublic] && [possibleType isInteresting]) {
                NSString *typeString;

                typeString = [possibleType contentTypeString];
                if (![typeString isEqualToString:wildcardContentTypeString])
                    [acceptsArray addObject:typeString];
            }
        }
        [acceptsArray sortUsingSelector:@selector(caseInsensitiveCompare:)];
        // TODO: Be clever about when we include the wildcard content type.  Also, should we have covers for the boring content types (image/*, maybe)?
        [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])) {
        if ([possibleType isPublic] && [possibleType isInteresting]) {
            NSString *encodingString;

            encodingString = [possibleType contentTypeString];
            OBASSERT([encodingString hasPrefix:@"encoding/"]);
            [acceptsArray addObject:[encodingString substringFromIndex:9]]; // remove the "encoding/" prefix
        }
    }
    if ([acceptsArray count] != 0) {
        [acceptsArray sortUsingSelector:@selector(caseInsensitiveCompare:)];
        headerString = [headerString stringByAppendingString:[isa stringForHeader:@"Accept-Encoding" value:[acceptsArray componentsJoinedByString:@", "]]];
    }
    [acceptsArray release];


    userString = [[NSUserDefaults standardUserDefaults] stringForKey:@"OWHTTPAcceptCharsetHeader"];
    if (userString && [userString length]) {
        headerString = [headerString stringByAppendingString:[isa stringForHeader:@"Accept-Charset" value:userString]];
    }

    return headerString;
}

- (NSString *)acceptLanguageHeadersString;
{
    // This can be nil if we didn't find any information on the user's language preferences; we'll return the empty string to avoid crashing in appendString:.
    return acceptLanguageString ? acceptLanguageString : @"";
}

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

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

- (NSString *)cacheControlHeaderStringForPipeline:(OWWebPipeline *)aPipeline;
{
    if ([aPipeline respondsToSelector:@selector(proxyCacheDisabled)] &&
        [aPipeline proxyCacheDisabled])
        return [isa stringForHeader:@"Cache-Control" value:@"no-cache"];
    else
        return @"";
}

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

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

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

- (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 @"";
}

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

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

    cookies = [OWCookieDomain cookiesForURL:aURL];
    cookieCount = cookies ? [cookies count] : 0;
    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]];
    }
    if (cookieString) {
        [cookieString appendString:endOfLineString];
        return cookieString;
    } else {
        return @"";
    }
}

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

    addressMethodDictionary = [anAddress methodDictionary];
    contentType = [addressMethodDictionary objectForKey:OWAddressContentTypeMethodKey];
    if (!contentType)
        return @"";
    valueString = [[NSMutableString alloc] initWithString:contentType];
    boundaryString = [addressMethodDictionary objectForKey:OWAddressBoundaryMethodKey];
    if (boundaryString)
        [valueString appendStrings:@"; boundary=", boundaryString, nil];

    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:OWAddressContentStringMethodKey] length] + [[addressMethodDictionary objectForKey:OWAddressContentDataMethodKey] length]]];
}

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

    methodContentString = [[anAddress methodDictionary] objectForKey:OWAddressContentStringMethodKey];
    if (!methodContentString)
        return nil;
    return [methodContentString stringByAppendingString:endOfLineString];
}

//
// Reading results
//

static void notifyCredentials(NSArray *credentials, BOOL success, OWHeaderDictionary *response)
{
    unsigned int credentialIndex, credentialCount;
    
    if (credentials == nil)
        return;
    credentialCount = [credentials count];
    // Only notify the first credential, since we only sent the first credential --- the second through Nth credentials are credentials we've tried and failed with, and are just keeping around so we know not to try them again.
    for (credentialIndex = 0; credentialIndex < credentialCount && credentialIndex < 1; credentialIndex++) {
        OWAuthorizationCredential *credential;

        credential = [credentials objectAtIndex:credentialIndex];
        [credential authorizationSucceeded:success response:response];
    }
}

- (BOOL)readResponseForProcessor:(OWHTTPProcessor *)processor inPipeline:(OWPipeline *)pipeline;
{
    NSString *line;
    NSScanner *scanner;
    float httpVersion;
    HTTPStatus httpStatus;
    NSString *commentString;
    OWAuthorizationRequest *authorizationRequest;
    NSArray *newCredentials, *oldCredentials;

    if (fetchFlags.aborting)
        [NSException raise:@"User Aborted" format:NSLocalizedStringFromTableInBundle(@"User Stopped", @"OWF", [OWHTTPSession bundle], httpsession error)];
        
    [processor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Awaiting document from %@", @"OWF", [OWHTTPSession bundle], httpsession status), [proxyLocation shortDisplayString]];

beginReadResponse:    
    
    line = [socketStream peekLine];
    while (line != nil && [line isEqualToString:@""]) {
        // Skipping past leading newlines in the response fixes a problem I was seeing talking to a SmallWebServer/2.0 (used in some bulletin boards like the one at Clan Fat, http://pub12.ezboard.com/bfat).  I think what might have happened is that they miscalculated their content length in an earlier request, and sent us an extra newline following the counted bytes.  The result was that every other request to the server would fail.
        // Note:  if we're actually talking to an HTTP 0.9 server, it's possible we're losing blank lines at the beginning of the content they're sending us.  But since I haven't seen any HTTP 0.9 servers in a long, long time...
        [socketStream readLine]; // Skip past the empty line
        line = [socketStream peekLine]; // And peek at the next one
    }
    if (line == nil)
        return NO;  // TODO: what to do here? shouldn't this actually be an exception?
    if (OWHTTPDebug)
        NSLog(@"%@ Rx: %@", [fetchURL scheme], line);
    scanner = [NSScanner scannerWithString:line];

    if (![scanner scanString:@"HTTP" intoString:NULL]) {
        // 0.9 server:  good luck!
        NSLog(@"%@ is ancient, good luck!", [proxyLocation shortDisplayString]);
        [processor setStatusFormat:NSLocalizedStringFromTableInBundle(@"%@ is ancient, good luck!", @"OWF", [OWHTTPSession bundle], httpsession status for HTTP/0.9 servers), [proxyLocation shortDisplayString]];
        [headerDictionary addString:@"www/unknown" forKey:@"content-type"];
        [pipeline setHeaderDictionary:headerDictionary fromURL:fetchURL];
        [OWCookieDomain registerCookiesFromPipeline:pipeline headerDictionary:headerDictionary];
        [self readBodyForPipeline:pipeline processor:processor ignore:NO];
        return YES;
    }

    [socketStream readLine]; // Skip past the line we're already parsing
    [scanner scanString:@"/" intoString:NULL];
    [scanner scanFloat:&httpVersion];
    if (OWHTTPDebug)
        NSLog(@"Rx: %@", [fetchAddress addressString]);
    if (httpVersion > 1.0) {
        [queue setServerUnderstandsPipelinedRequests];
    }

    [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 readHeadersForPipeline:pipeline];
            goto beginReadResponse;
            
        // 200 codes - Success: Got MIME object

        case HTTP_STATUS_OK:
        case HTTP_STATUS_PARTIAL_CONTENT:
            [self readHeadersForPipeline:pipeline];
            notifyCredentials(proxyCredentials, YES, headerDictionary);
            notifyCredentials([processor credentials], YES, headerDictionary);
            [self readBodyForPipeline:pipeline processor:processor ignore: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 readHeadersForPipeline:pipeline];
                newLocationString = [headerDictionary lastStringForKey:@"location"];
                if (newLocationString == nil)
                    [NSException raise:@"Redirect failure" format:NSLocalizedStringFromTableInBundle(@"Location header missing on redirect", @"OWF", [OWHTTPSession bundle], httpsession error - required header missing in response)];
                newLocation = [fetchAddress addressForRelativeString:newLocationString];
                if ([newLocation isEqual:fetchAddress])
                    [NSException raise:@"Redirect loop" format:NSLocalizedStringFromTableInBundle(@"This page redirects the browser back to itself", @"OWF", [OWHTTPSession bundle], httpsession error - document redirects to itself or indirectly to itself)];
                [processor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Redirected to %@", @"OWF", [OWHTTPSession bundle], httpsession status), newLocationString];
                if (httpStatus == HTTP_STATUS_MOVED_PERMANENTLY)
                    [pipeline permanentRedirection:newLocation];
                [pipeline 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:[[fetchAddress addressString] stringByAppendingString:@"/"]])
                    [pipeline cacheContent];
                [pipeline startProcessingContent];
                [self readBodyForPipeline:pipeline processor:processor ignore: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:
            [pipeline processor:processor hasErrorName:commentString reason:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Server returns \"%@\" (%d)", @"OWF", [OWHTTPSession bundle], httpsession 400-series error - string and numeric code from server), commentString, httpStatus]];
            [self readHeadersForPipeline:pipeline];
            [pipeline contentError];
            [self readBodyForPipeline:pipeline processor:processor ignore:NO];
            break;

        case HTTP_STATUS_UNAUTHORIZED:
        case HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED:
#define IsProxyAuth (httpStatus == HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED)
            [processor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Authorizing %@", @"OWF", [OWHTTPSession bundle], httpsession status), [proxyLocation shortDisplayString]];
            [self readHeadersForPipeline:pipeline];
            if (IsProxyAuth) {
                notifyCredentials(proxyCredentials, NO, headerDictionary);
            } else {
                notifyCredentials([processor credentials], NO, headerDictionary);
                notifyCredentials(proxyCredentials, YES, headerDictionary);
            }
            oldCredentials = (IsProxyAuth ? proxyCredentials : [processor credentials]);
            if (!oldCredentials) oldCredentials = [NSArray array];
            authorizationRequest = [[[OWAuthorizationRequest authorizationRequestClass] alloc]
                initForType:(IsProxyAuth ? OWAuth_HTTP_Proxy : OWAuth_HTTP)
                netLocation:(IsProxyAuth ? proxyLocation : [fetchURL parsedNetLocation])
                defaultPort:[isa defaultPort]
                pipeline:pipeline
                challenge:headerDictionary
                promptForMoreThan:oldCredentials];
            newCredentials = [authorizationRequest credentials];
            if (newCredentials == nil)
                [pipeline processor:processor hasErrorName:(IsProxyAuth ? @"ProxyAuthFailed" : @"AuthFailed") reason:[authorizationRequest errorString]];
            if (newCredentials == nil || ![newCredentials count]) {
                [authorizationRequest release];
                [pipeline contentError];
                [self readBodyForPipeline:pipeline processor:processor ignore:NO];
            } else {
                OWAuthorizationCredential *newCredential;
                // If we got several new credentials, only add the first (hopefully best) one to our list. If it fails, we'll try again with the others.
                newCredential = [newCredentials objectAtIndex:0];
                    
                // TODO: Instead of accumulating all the old, known-to-be-bad credentials here (and sending them again on every request!), we should keep track of which ones we've tried and failed with but not re-send them when we send the next set. Perhaps an OWAuthorizationStateManager class should be created to keep track of this (one for each OWHTTPProcessor, and one for each proxy server). [note wiml19feb02: we now send only the first (most recent) credential in the array each time, which has the desired result]
                if (IsProxyAuth) {
                    newCredentials = [[NSArray arrayWithObject:newCredential] arrayByAddingObjectsFromArray:proxyCredentials];
                    [proxyCredentials release];
                    proxyCredentials = [newCredentials retain];
                } else {
                    [processor addCredential:newCredential];
                }
                [authorizationRequest release];
                [self readBodyForPipeline:pipeline processor:processor ignore:YES];
                return NO;  // Try again
            }
#undef IsProxyAuth
            break;

        case HTTP_STATUS_REQUEST_TIMEOUT:
            return NO; // Try again

        // 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:
            [pipeline processor:processor hasErrorName:commentString reason:[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Server returns \"%@\" (%d)", @"OWF", [OWHTTPSession bundle], httpsession 500-series error - string and numeric code from server), commentString, httpStatus]];
            [self readHeadersForPipeline:pipeline];
            [pipeline contentError];
            [self readBodyForPipeline:pipeline processor:processor ignore: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;
}

- (void)readBodyForPipeline:(OWPipeline *)pipeline processor:(OWHTTPProcessor *)processor ignore:(BOOL)ignoreThis;
{
    OWParameterizedContentType *parameterizedContentType;

    if (ignoreThis) {
#warning The implementation of reading bodies currently requires that a data stream exist, so we create one that will just get thrown away. We could be more efficient by having separate methods for each type of body to read and ignore the results.    
        interruptedDataStream = [[OWDataStream alloc] init];
    } else if (!interruptedDataStream) {
        OWContentType *testContentType;
        NSString *contentLengthString;

        contentLengthString = [headerDictionary lastStringForKey:@"content-length"];
        if (contentLengthString != nil && [contentLengthString intValue] > 0)
            interruptedDataStream = [[OWDataStream alloc] initWithLength:[contentLengthString intValue]];
        else
            interruptedDataStream = [[OWDataStream alloc] init];
        parameterizedContentType = [headerDictionary parameterizedContentType];
        testContentType = [parameterizedContentType contentType];
        if (testContentType == textPlainContentType || testContentType == applicationOctetStreamContentType) {
            // Treat text/plain and application/octet-stream as www/unknown.  A lot of web servers out there use these as default content types, which means they end up claiming that, say, .gnutar.gz files are text/plain, or ReadMe is application/octet-stream.  So...if we see a suspicious content type, let's just run it through our data detector and see whether it really is what it claims to be.  (Some HTML pages are also served as text/plain, but IE displays them as HTML even though they're valid plain text.  This also makes us compatible with that behavior.)
            parameterizedContentType = [[[OWParameterizedContentType alloc] initWithContentType:[OWUnknownDataStreamProcessor unknownContentType]] autorelease];
        }
        [interruptedDataStream setFullContentType:parameterizedContentType];
        [interruptedDataStream setContentEncoding:[headerDictionary contentEncoding]];
        [pipeline addSourceContent:interruptedDataStream];
        [pipeline cacheContent];
        [pipeline 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.
            [processor setDataStream:interruptedDataStream];
        }
    }
    
    [processor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Reading document from %@", @"OWF", [OWHTTPSession bundle], httpsession status), [proxyLocation shortDisplayString]];

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

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

- (BOOL)readHeadForProcessor:(OWHTTPProcessor *)processor inPipeline:(OWPipeline *)pipeline;
{
    NSString *line;
    NSScanner *scanner;
    float httpVersion;
    HTTPStatus httpStatus;
    NSString *commentString;
    OWTimeStamp *stamp = nil;

    [processor setStatusFormat:NSLocalizedStringFromTableInBundle(@"Awaiting document info from %@", @"OWF", [OWHTTPSession bundle], httpsession status), [proxyLocation shortDisplayString]];
    line = [socketStream peekLine];
    while (line != nil && [line isEqualToString:@""]) {
        // Skipping past leading newlines in the response fixes a problem I was seeing talking to a SmallWebServer/2.0 (used in some bulletin boards like the one at Clan Fat, http://pub12.ezboard.com/bfat).  I think what might have happened is that they miscalculated their content length in an earlier request, and sent us an extra newline following the counted bytes.  The result was that every other request to the server would fail.
        // Note:  if we're actually talking to an HTTP 0.9 server, it's possible we're losing blank lines at the beginning of the content they're sending us.  But since I haven't seen any HTTP 0.9 servers in a long, long time...
        [socketStream readLine]; // Skip past the empty line
        line = [socketStream peekLine]; // And peek at the next one
    }
    if (line == nil)
        return NO;
    if (OWHTTPDebug)
        NSLog(@"%@ Rx: %@", [fetchURL scheme], line);
    scanner = [NSScanner scannerWithString:line];

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

    [socketStream readLine]; // Skip past the line we're already parsing
    [scanner scanString:@"/" intoString:NULL];
    [scanner scanFloat:&httpVersion];
    if (OWHTTPDebug)
        NSLog(@"Rx: %@", [fetchAddress addressString]);
    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 readHeadersForPipeline:pipeline];
            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 readHeadersForPipeline:pipeline];
                newLocation = [OWAddress addressForString:
                    [headerDictionary lastStringForKey:@"location"]];
                if (httpStatus == HTTP_STATUS_MOVED_PERMANENTLY)
                    [pipeline permanentRedirection:newLocation];
                [pipeline addSourceContent:newLocation];
                // TODO: Should we cacheContent even for temporary redirections? If not, maybe -cacheContent should do the permanent redirection stuff that -permanentRedirection: does.
                [pipeline cacheContent];
                [pipeline 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:fetchAddress];
            break;

        case HTTP_STATUS_REQUEST_TIMEOUT:
            return NO; // Try again

        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:fetchAddress];
            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;
    }
    [pipeline addContent:stamp];
    [pipeline 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:fetchAddress];
}

- (void)readHeadersForPipeline:(OWPipeline *)pipeline;
{
    [headerDictionary readRFC822HeadersFromSocketStream:socketStream];
    if (OWHTTPDebug)
        NSLog(@"Rx Headers:\n%@", headerDictionary);
    
#ifdef DEBUG_TRANSACTIONS
    {
        OWContentContainer *rxHeaders = [OWContentContainer containerWithContent:[headerDictionary  dictionaryCopy] contentType:[OWContentType contentTypeForString:@"omni/response-headers"]];
        [[pipeline contentCacheForLastAddress] addContent:rxHeaders];
    }
#endif

    [pipeline setHeaderDictionary:headerDictionary fromURL:fetchURL];
    [OWCookieDomain registerCookiesFromPipeline:pipeline headerDictionary:headerDictionary];
    [self readTimeStamp];
}

- (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 forProcessor:(OWProcessor *)processor;
{
    while (!fetchFlags.aborting) {
        NSAutoreleasePool *autoreleasePool = nil;
        unsigned int contentLength, bytesLeft;
        unsigned int byteCount, bytesInThisPool;

        autoreleasePool = [[NSAutoreleasePool alloc] init];
        contentLength = [self intValueFromHexString:[socketStream readLine]];
        if (contentLength == 0)
            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);
        [processor processedBytes:byteCount ofBytes:contentLength];

        while (!fetchFlags.aborting && bytesLeft != 0) {
            unsigned int dataStreamBytesAvailable, socketBytesWritten;
            void *dataStreamBuffer;

            dataStreamBytesAvailable = MIN([dataStream appendToUnderlyingBuffer:&dataStreamBuffer], bytesLeft);
            socketBytesWritten = [socketStream readBytesWithMaxLength:dataStreamBytesAvailable intoBuffer:dataStreamBuffer];
            if (socketBytesWritten == 0)
                break;
                
            byteCount += socketBytesWritten;
            bytesLeft -= socketBytesWritten;
            bytesInThisPool += socketBytesWritten;
            [processor processedBytes:byteCount ofBytes:contentLength];
            // NSLog(@"%@ readChunkedBody: processed bytes %d of %d for dataStream %@, bytesLeft = %d", OBShortObjectDescription(self), byteCount, contentLength, OBShortObjectDescription(dataStream), bytesLeft);
            [dataStream wroteBytesToUnderlyingBuffer:socketBytesWritten];
            // NSLog(@"%@ readChunkedBody: wrote data to %@", OBShortObjectDescription(self), OBShortObjectDescription(dataStream));
            if (bytesInThisPool > 64 * 1024) {
                [autoreleasePool release];
                autoreleasePool = [[NSAutoreleasePool alloc] init];
                bytesInThisPool = 0;
            }
        }
        [socketStream readLine];
        [autoreleasePool release];
    }
    if (fetchFlags.aborting)
        [NSException raise:@"User Aborted" format:NSLocalizedStringFromTableInBundle(@"User Stopped", @"OWF", [OWHTTPSession bundle], httpsession error)];
    [headerDictionary readRFC822HeadersFromSocketStream:socketStream];
}

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

    autoreleasePool = [[NSAutoreleasePool alloc] init];
    contentLength = [[headerDictionary lastStringForKey:@"content-length"] intValue];
    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);
    [processor processedBytes:byteCount ofBytes:contentLength];

    while (!fetchFlags.aborting && bytesLeft != 0) {
        void *dataStreamBuffer;
        unsigned int dataStreamBytesAvailable, socketBytesWritten;

        dataStreamBytesAvailable = MIN([dataStream appendToUnderlyingBuffer:&dataStreamBuffer], bytesLeft);
        socketBytesWritten = [socketStream readBytesWithMaxLength:dataStreamBytesAvailable intoBuffer:dataStreamBuffer];
        if (socketBytesWritten == 0)
            break;

        byteCount += socketBytesWritten;
        bytesLeft -= socketBytesWritten;
        bytesInThisPool += socketBytesWritten;
        [processor processedBytes:byteCount ofBytes:contentLength];
        // NSLog(@"%@ readStandardBody: processed bytes %d of %d for dataStream %@, bytesLeft = %d", OBShortObjectDescription(self), byteCount, contentLength, OBShortObjectDescription(dataStream), bytesLeft);
        [dataStream wroteBytesToUnderlyingBuffer:socketBytesWritten];
        // NSLog(@"%@ readStandardBody: wrote data to %@", OBShortObjectDescription(self), OBShortObjectDescription(dataStream));
        if (bytesInThisPool > 64 * 1024) {
            [autoreleasePool release];
            autoreleasePool = [[NSAutoreleasePool alloc] init];
            bytesInThisPool = 0;
        }
    }
    [autoreleasePool release];
    if (fetchFlags.aborting)
        [NSException raise:@"User Aborted" format:NSLocalizedStringFromTableInBundle(@"User Stopped", @"OWF", [OWHTTPSession bundle], httpsession error)];
}

- (void)readClosingBodyIntoStream:(OWDataStream *)dataStream forProcessor:(OWProcessor *)processor;
{
    NSAutoreleasePool *autoreleasePool = nil;
    void *dataStreamBuffer;
    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));
    [processor processedBytes:byteCount ofBytes:0];

    NS_DURING {
        while (!fetchFlags.aborting) {
            unsigned int dataStreamBytesAvailable, socketBytesWritten;

            dataStreamBytesAvailable = [dataStream appendToUnderlyingBuffer:&dataStreamBuffer];
            socketBytesWritten = [socketStream readBytesWithMaxLength:dataStreamBytesAvailable intoBuffer:dataStreamBuffer];
            if (socketBytesWritten == 0)
                break;

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

    if (fetchFlags.aborting)
        [NSException raise:@"User Aborted" format:NSLocalizedStringFromTableInBundle(@"User Stopped", @"OWF", [OWHTTPSession bundle], httpsession error)];
    [autoreleasePool release];
}

// Exception handling

- (void)notifyProcessor:(OWHTTPProcessor *)aProcessor ofSessionException:(NSException *)sessionException;
{
    NS_DURING {
        [aProcessor handleSessionException:sessionException];
        [aProcessor processEnd];
        [aProcessor retire];
    } NS_HANDLER {
        NSLog(@"Exception trying to notify processor of session exception: sessionException = %@, localException = %@", sessionException, localException);
    } NS_ENDHANDLER;
}

@end

@implementation OWHTTPSession (OWDeprecatedAPI)

// This is just here for backwards compatibility (specifically, so the HTTPS plug-in doesn't break).

- (void)disconnect; // deprecated
{
    [self disconnectAndRequeueProcessors];
}

@end
