How to set cookieAcceptPolicy for ephemeral NSURLSession

1.9k views Asked by At

I am trying to use ephemeral NSURLSessions to provide separate cookie handling for several tasks within my app. These tasks are not directly bound to a UI. Problem: Whatever I do, the cookieAcceptPolicy of the ephemeral NSHTTPCookieStorage remains NSHTTPCookieAcceptPolicyNever.

Here's my code:

// use a pure in-memory configuration with its own private cache, cookie, and credential store
__block NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration];

// do anything to accept all cookies
config.HTTPShouldSetCookies = YES;
config.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyAlways;
config.HTTPCookieStorage.cookieAcceptPolicy = NSHTTPCookieAcceptPolicyAlways;

__block NSURLSession* session = [NSURLSession sessionWithConfiguration:config];

NSURLSessionDataTask* task = [session dataTaskWithURL:[NSURL URLWithString:@"https://test.cgmlife.com/Catalogs"]
                                    completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                        NSHTTPCookieStorage* cookies = session.configuration.HTTPCookieStorage;
                                        NSLog(@"%@", cookies);
                                        NSLog(@"%lu", cookies.cookieAcceptPolicy);
                                    }];
[task resume];

The output from NSLog is always:

Ephemeral <NSHTTPCookieStorage cookies count:0>
1

where 1 is the value for NSHTTPCookieAcceptPolicyNever (expected 0 for NSHTTPCookieAcceptPolicyAlways). The headers in the response are there.

What can I do to have the NSHTTPCookieStorage remember my cookies while the session is alive? I don't need and don't want any persistence. I just want to keep cookies in memory so that they are reused for further requests in the same session.

2

There are 2 answers

0
Heath Borders On BEST ANSWER

It looks like ephemeral sessions don't store cookies ever. eskimo1 says on the devforums:

ISTR that ephemeral session configurations don't work the way that folks expect based on the documentation. I never got around to looking at this in detail but it seems like you have. You should file a bug about this; the implementation and documentation are clearly out of sync, so one of them needs to be fixed.

0
Carl Lindberg On

It looks like the iOS9 ephemeralSessionConfiguration works as expected. On iOS8 though, it seems like the cookie storage returns Never for its policy and it can't be reset. The implementation appears to be no-storage despite the private class name, instead of memory-only.

For iOS8, I was able to substitute a rudimentary implementation though, and it seems to work (at least in the simulator with light testing). Implementing the new methods that take task objects was essential.

#import <Foundation/Foundation.h>
@interface MemoryCookieStorage : NSHTTPCookieStorage
@property (nonatomic, strong) NSMutableArray *internalCookies;
@property (atomic, assign) NSHTTPCookieAcceptPolicy policy;
@end

@implementation MemoryCookieStorage

- (id)init
{
    if (self = [super init]) {
        _internalCookies = [NSMutableArray new];
        _policy = NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain;
    }
    return self;
}

- (NSHTTPCookieAcceptPolicy)cookieAcceptPolicy {
    return self.policy;
}
- (void)setCookieAcceptPolicy:(NSHTTPCookieAcceptPolicy)cookieAcceptPolicy {
    self.policy = cookieAcceptPolicy;
}

- (NSUInteger)_indexOfCookie:(NSHTTPCookie *)target
{
    return [_internalCookies indexOfObjectPassingTest:^BOOL(NSHTTPCookie *cookie, NSUInteger idx, BOOL *stop) {
        return ([target.name caseInsensitiveCompare:cookie.name] == NSOrderedSame &&
                [target.domain caseInsensitiveCompare:cookie.domain] == NSOrderedSame &&
                (target.path == cookie.path || [target.path isEqual:cookie.path]));
    }];
}

- (void)setCookie:(NSHTTPCookie *)cookie
{
    if (self.cookieAcceptPolicy != NSHTTPCookieAcceptPolicyNever)
    {
        @synchronized(_internalCookies) {
            NSInteger idx = [self _indexOfCookie:cookie];
            if (idx == NSNotFound)
                [_internalCookies addObject:cookie];
            else
                [_internalCookies replaceObjectAtIndex:idx withObject:cookie];
        }
    }
}

- (void)deleteCookie:(NSHTTPCookie *)cookie
{
    @synchronized(_internalCookies) {
        NSInteger idx = [self _indexOfCookie:cookie];
        if (idx != NSNotFound)
            [_internalCookies removeObjectAtIndex:idx];
    }
}

- (NSArray *)cookies
{
    @synchronized(_internalCookies) {
        return [_internalCookies copy];
    }
}

static BOOL HasCaseSuffix(NSString *string, NSString *suffix)
{
    return [string rangeOfString:suffix options:NSCaseInsensitiveSearch|NSAnchoredSearch|NSBackwardsSearch].length > 0;
}
static BOOL IsDomainOK(NSString *cookieDomain, NSString *host)
{
    return ([cookieDomain caseInsensitiveCompare:host] == NSOrderedSame ||
            ([cookieDomain hasPrefix:@"."] && HasCaseSuffix(host, cookieDomain)) ||
            (cookieDomain && HasCaseSuffix(host, [@"." stringByAppendingString:cookieDomain])));
}
- (NSArray *)cookiesForURL:(NSURL *)URL
{
    NSMutableArray *array = [NSMutableArray new];
    NSString *host = URL.host;
    NSString *path = URL.path;

    @synchronized(_internalCookies)
    {
        for (NSHTTPCookie *cookie in _internalCookies)
        {
            if (!IsDomainOK(cookie.domain, host))
                continue;

            BOOL pathOK = cookie.path.length == 0 || [cookie.path isEqual:@"/"] || [path hasPrefix:cookie.path];
            if (!pathOK)
                continue;

            if (cookie.isSecure && [URL.scheme caseInsensitiveCompare:@"https"] != NSOrderedSame)
                continue;

            if ([cookie.expiresDate timeIntervalSinceNow] > 0)
                continue;

            [array addObject:cookie];
        }
    }

    array = (id)[array sortedArrayUsingComparator:^NSComparisonResult(NSHTTPCookie *c1, NSHTTPCookie *c2) {
        /* More specific cookies, i.e. matching the longest portion of the path, come first */
        NSInteger path1 = c1.path.length;
        NSInteger path2 = c2.path.length;
        if (path1 > path2)
            return NSOrderedAscending;
        if (path2 > path1)
            return NSOrderedDescending;
        return [c1.name caseInsensitiveCompare:c2.name];
    }];

    return array;
}

- (NSArray *)sortedCookiesUsingDescriptors:(NSArray *)sortOrder
{
    return [[self cookies] sortedArrayUsingDescriptors:sortOrder];
}

- (void)getCookiesForTask:(NSURLSessionTask *)task completionHandler:(void (^) (NSArray *taskCookies))completionHandler
{
    NSArray *urlCookies = [self cookiesForURL:task.currentRequest.URL];
    completionHandler(urlCookies);
}

- (void)setCookies:(NSArray *)newCookies forURL:(NSURL *)URL mainDocumentURL:(NSURL *)mainDocumentURL
{
    NSString *host = mainDocumentURL.host;
    for (NSHTTPCookie *cookie in newCookies)
    {
        switch (self.cookieAcceptPolicy)
        {
            case NSHTTPCookieAcceptPolicyAlways:
                [self setCookie:cookie];
                break;
            case NSHTTPCookieAcceptPolicyNever:
                break;
            case NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain:
                if (IsDomainOK(cookie.domain, host))
                    [self setCookie:cookie];
                break;
        }
    }
}

- (void)storeCookies:(NSArray *)taskCookies forTask:(NSURLSessionTask *)task
{
    NSURL *mainURL = task.currentRequest.mainDocumentURL ?: task.originalRequest.mainDocumentURL ?: task.originalRequest.URL;
    [self setCookies:taskCookies forURL:task.currentRequest.URL mainDocumentURL:mainURL];
}

@end

It should be possible to test sessionConfiguration.HTTPCookieStorage.cookieAcceptPolicy == NSHTTPCookieAcceptPolicyNever after creating the ephemeral session to see if the HTTPCookieStorage needs to be replaced with an instance of the above class (shouldn't need it on iOS9). There are probably some bugs... I just needed this for a demo and it worked well enough for that. But they shouldn't be too hard to fix if any come up.