22
22
Boston, MA 02111 USA.
25
#include "GSURLPrivate.h"
26
#include "Foundation/NSSet.h"
28
NSString * const NSHTTPCookieComment = @"NSHTTPCookieComment";
29
NSString * const NSHTTPCookieCommentURL = @"NSHTTPCookieCommentURL";
30
NSString * const NSHTTPCookieDiscard = @"NSHTTPCookieDiscard";
31
NSString * const NSHTTPCookieDomain = @"NSHTTPCookieDomain";
32
NSString * const NSHTTPCookieExpires = @"NSHTTPCookieExpires";
33
NSString * const NSHTTPCookieMaximumAge = @"NSHTTPCookieMaximumAge";
34
NSString * const NSHTTPCookieName = @"NSHTTPCookieName";
35
NSString * const NSHTTPCookieOriginURL = @"NSHTTPCookieOriginURL";
36
NSString * const NSHTTPCookiePath = @"NSHTTPCookiePath";
37
NSString * const NSHTTPCookiePort = @"NSHTTPCookiePort";
38
NSString * const NSHTTPCookieSecure = @"NSHTTPCookieSecure";
39
NSString * const NSHTTPCookieValue = @"NSHTTPCookieValue";
40
NSString * const NSHTTPCookieVersion = @"NSHTTPCookieVersion";
26
Try to handle cookies via the original Netscape specification
27
(http://web.archive.org/web/20070805052634/http://wp.netscape.com/newsref/std/cookie_spec.html)
28
and the official RFC2965 (http://tools.ietf.org/html/rfc2965).
30
Header fields named "Set-Cookie" are processed using either the original
31
spec or RFC2965. "Set-Cookie2" fields use the RFC spec. There are some
32
crazy things to be aware of though. Multiple cookies can be specified in the
33
same header and are separated by a comma. However, cookies themselves can
34
also contain commas, most notably in the Expires field (which is not quoted
35
and can contain spaces as well). The last key/value does not have to have a
36
semi-colon, so this can be tricky to parse if another cookie occurs
37
after this (See GSRangeOfCookie).
41
#define EXPOSE_NSHTTPCookie_IVARS 1
42
#import "GSURLPrivate.h"
43
#import "Foundation/NSSet.h"
44
#import "Foundation/NSValue.h"
45
#import "Foundation/NSString.h"
46
#import "Foundation/NSCalendarDate.h"
47
#import "GNUstepBase/Unicode.h"
48
#import "GNUstepBase/NSObject+GNUstepBase.h"
50
NSString * const NSHTTPCookieComment = @"Comment";
51
NSString * const NSHTTPCookieCommentURL = @"CommentURL";
52
NSString * const NSHTTPCookieDiscard = @"Discard";
53
NSString * const NSHTTPCookieDomain = @"Domain";
54
NSString * const NSHTTPCookieExpires = @"Expires";
55
NSString * const NSHTTPCookieMaximumAge = @"MaximumAge";
56
NSString * const NSHTTPCookieName = @"Name";
57
NSString * const NSHTTPCookieOriginURL = @"OriginURL";
58
NSString * const NSHTTPCookiePath = @"Path";
59
NSString * const NSHTTPCookiePort = @"Port";
60
NSString * const NSHTTPCookieSecure = @"Secure";
61
NSString * const NSHTTPCookieValue = @"Value";
62
NSString * const NSHTTPCookieVersion = @"Version";
42
64
// Internal data storage
44
66
NSDictionary *_properties;
50
#define this ((Internal*)(((priv*)self)->_NSHTTPCookieInternal))
51
#define inst ((Internal*)(((priv*)o)->_NSHTTPCookieInternal))
69
#define this ((Internal*)(self->_NSHTTPCookieInternal))
70
#define inst ((Internal*)(o->_NSHTTPCookieInternal))
72
/* Bitmap of characters considered white space if in an old style property
73
* list. This is the same as the set given by the isspace() function in the
74
* POSIX locale, but (for cross-locale portability of property list files)
75
* is fixed, rather than locale dependent.
77
static const unsigned char whitespace[32] = {
112
#define IS_BIT_SET(a,i) ((((a) & (1<<(i)))) > 0)
114
#define GS_IS_WHITESPACE(X) IS_BIT_SET(whitespace[(X)/8], (X) % 8)
116
static id GSPropertyListFromCookieFormat(NSString *string);
117
static NSRange GSRangeOfCookie(NSString *string);
53
119
@implementation NSHTTPCookie
71
137
return AUTORELEASE(o);
140
+ (NSMutableArray *) _parseField: (NSString *)field
141
forHeader: (NSString *)header
146
NSString *defaultPath, *defaultDomain;
147
NSMutableArray *a = [NSMutableArray array];
149
if ([header isEqual: @"Set-Cookie"])
151
else if ([header isEqual: @"Set-Cookie2"])
156
defaultDomain = [url host];
157
defaultPath = [url path];
158
if ([[url absoluteString] hasSuffix: @"/"] == NO)
159
defaultPath = [defaultPath stringByDeletingLastPathComponent];
161
/* We could use an NSScanner here, but this string could contain all
162
sorts of odd stuff. It's not quite a property list either - it has
163
dates and also could have tokens without values. */
167
NSHTTPCookie *cookie;
168
NSMutableDictionary *dict;
170
NSRange range = GSRangeOfCookie(field);
171
if (range.location == NSNotFound)
173
onecookie = [field substringFromRange: range];
175
dict = GSPropertyListFromCookieFormat(onecookie);
181
if ([dict objectForKey: NSHTTPCookiePath] == nil)
182
[dict setObject: defaultPath forKey: NSHTTPCookiePath];
183
if ([dict objectForKey: NSHTTPCookieDomain] == nil)
184
[dict setObject: defaultDomain forKey: NSHTTPCookieDomain];
185
cookie = [NSHTTPCookie cookieWithProperties: dict];
187
[a addObject: cookie];
189
if ([field length] <= NSMaxRange(range))
191
field = [field substringFromIndex: NSMaxRange(range)+1];
74
196
+ (NSArray *) cookiesWithResponseHeaderFields: (NSDictionary *)headerFields
75
197
forURL: (NSURL *)URL
77
NSMutableArray *a = nil;
79
[self notImplemented: _cmd]; // FIXME
199
NSEnumerator *henum = [headerFields keyEnumerator];
200
NSMutableArray *a = [NSMutableArray array];
202
while ((header = [henum nextObject]))
205
= [self _parseField: [headerFields objectForKey: header]
206
forHeader: header andURL: URL];
208
[a addObjectsFromArray: suba];
83
214
+ (NSDictionary *) requestHeaderFieldsWithCookies: (NSArray *)cookies
85
NSMutableDictionary *d = nil;
87
[self notImplemented: _cmd]; // FIXME
219
NSEnumerator *ckenum = [cookies objectEnumerator];
221
if ([cookies count] == 0)
223
NSLog(@"NSHTTPCookie requestHeaderFieldWithCookies: empty array");
226
/* Assume these cookies all came from the same URL so we format based
227
on the version of the first. */
229
version = [(NSHTTPCookie *)[cookies objectAtIndex: 0] version];
231
field = @"$Version=\"1\"";
232
while ((ck = [ckenum nextObject]))
235
str = [NSString stringWithFormat: @"%@=%@", [ck name], [ck value]];
237
field = [field stringByAppendingFormat: @"; %@", str];
240
if (version && [ck path])
241
field = [field stringByAppendingFormat: @"; $Path=\"%@\"", [ck path]];
244
return [NSDictionary dictionaryWithObject: field forKey: @"Cookie"];
91
247
- (NSString *) comment
164
346
return [this->_properties objectForKey: NSHTTPCookieValue];
169
return [[this->_properties objectForKey: NSHTTPCookieVersion] intValue];
349
- (NSUInteger) version
351
return [[this->_properties objectForKey: NSHTTPCookieVersion] integerValue];
354
- (NSString *) description
356
return [NSString stringWithFormat: @"<NSHTTPCookie %p: %@=%@>", self,
357
[self name], [self value]];
363
#define inrange(ch,min,max) ((ch)>=(min) && (ch)<=(max))
364
#define char2num(ch) \
365
inrange(ch,'0','9') \
367
: (inrange(ch,'a','f') \
368
? ((ch)-0x57) : ((ch)-0x37))
371
const unsigned char *ptr;
382
* Returns YES if there is any non-whitespace text remaining.
384
static BOOL skipSpace(pldata *pld)
388
while (pld->pos < pld->end)
390
c = pld->ptr[pld->pos];
392
if (GS_IS_WHITESPACE(c) == NO)
402
pld->err = @"reached end of string";
406
static inline id parseQuotedString(pldata* pld)
408
unsigned start = ++pld->pos;
409
unsigned escaped = 0;
414
while (pld->pos < pld->end)
416
unsigned char c = pld->ptr[pld->pos];
420
if (escaped == 1 && c >= '0' && c <= '7')
425
else if (escaped == 1 && (c == 'u' || c == 'U'))
430
else if (escaped > 1)
432
if (hex && isxdigit(c))
441
else if (c >= '0' && c <= '7')
477
if (pld->pos >= pld->end)
479
pld->err = @"reached end of string while parsing quoted string";
482
if (pld->pos - start - shrink == 0)
490
unichar *temp = NULL;
491
unsigned int temp_length = 0;
495
if (!GSToUnicode(&temp, &temp_length, &pld->ptr[start],
496
pld->pos - start, NSUTF8StringEncoding,
497
NSDefaultMallocZone(), 0))
499
pld->err = @"invalid utf8 data while parsing quoted string";
502
length = temp_length - shrink;
503
chars = NSAllocateCollectable(sizeof(unichar) * length, 0);
506
for (j = 0, k = 0; j < temp_length; j++)
512
if (escaped == 1 && c >= '0' && c <= '7')
518
else if (escaped == 1 && (c == 'u' || c == 'U'))
524
else if (escaped > 1)
526
if (hex && isxdigit(c))
529
chars[k] |= char2num(c);
537
else if (c >= '0' && c <= '7')
540
chars[k] |= (c - '0');
560
case 'a' : chars[k] = '\a'; break;
561
case 'b' : chars[k] = '\b'; break;
562
case 't' : chars[k] = '\t'; break;
563
case 'r' : chars[k] = '\r'; break;
564
case 'n' : chars[k] = '\n'; break;
565
case 'v' : chars[k] = '\v'; break;
566
case 'f' : chars[k] = '\f'; break;
567
default : chars[k] = c; break;
586
NSZoneFree(NSDefaultMallocZone(), temp);
589
obj = [NSString alloc];
590
obj = [obj initWithCharactersNoCopy: chars
598
/* In cookies, keys are terminated by '=' and values are terminated by ';'
600
static inline id parseUnquotedString(pldata *pld, char endChar)
602
unsigned start = pld->pos;
608
while (pld->pos < pld->end)
610
if ((pld->ptr[pld->pos]) == endChar)
615
length = pld->pos - start;
616
chars = NSAllocateCollectable(sizeof(unichar) * length, 0);
617
for (i = 0; i < length; i++)
619
chars[i] = pld->ptr[start + i];
623
obj = [NSString alloc];
624
obj = [obj initWithCharactersNoCopy: chars
632
_setCookieKey(NSMutableDictionary *dict, NSString *key, NSString *value)
634
if ([dict count] == 0)
636
/* This must be the name=value pair */
637
if ([value length] == 0)
639
[dict setObject: key forKey: NSHTTPCookieName];
640
[dict setObject: value forKey: NSHTTPCookieValue];
643
if ([[key lowercaseString] isEqual: @"comment"])
644
[dict setObject: value forKey: NSHTTPCookieComment];
645
else if ([[key lowercaseString] isEqual: @"commenturl"])
646
[dict setObject: value forKey: NSHTTPCookieCommentURL];
647
else if ([[key lowercaseString] isEqual: @"discard"])
648
[dict setObject: [NSNumber numberWithBool: YES]
649
forKey: NSHTTPCookieDiscard];
650
else if ([[key lowercaseString] isEqual: @"domain"])
651
[dict setObject: value forKey: NSHTTPCookieDomain];
652
else if ([[key lowercaseString] isEqual: @"expires"])
655
expireDate = [NSCalendarDate dateWithString: value
656
calendarFormat: @"%a, %d-%b-%Y %I:%M:%S %Z"];
658
[dict setObject: expireDate forKey: NSHTTPCookieExpires];
660
else if ([[key lowercaseString] isEqual: @"max-age"])
661
[dict setObject: value forKey: NSHTTPCookieMaximumAge];
662
else if ([[key lowercaseString] isEqual: @"originurl"])
663
[dict setObject: value forKey: NSHTTPCookieOriginURL];
664
else if ([[key lowercaseString] isEqual: @"path"])
665
[dict setObject: value forKey: NSHTTPCookiePath];
666
else if ([[key lowercaseString] isEqual: @"port"])
667
[dict setObject: value forKey: NSHTTPCookiePort];
668
else if ([[key lowercaseString] isEqual: @"secure"])
669
[dict setObject: [NSNumber numberWithBool: YES]
670
forKey: NSHTTPCookieSecure];
671
else if ([[key lowercaseString] isEqual: @"version"])
672
[dict setObject: value forKey: NSHTTPCookieVersion];
677
GSPropertyListFromCookieFormat(NSString *string)
679
NSMutableDictionary *dict;
686
* An empty string is a nil property list.
688
if ([string length] == 0)
693
d = [string dataUsingEncoding: NSUTF8StringEncoding];
694
NSCAssert(d, @"Couldn't get utf8 data from string.");
695
_pld.ptr = (unsigned char*)[d bytes];
697
_pld.end = [d length];
702
_pld.old = YES; // OpenStep style
704
dict = [[NSMutableDictionary allocWithZone: NSDefaultMallocZone()]
705
initWithCapacity: 0];
706
while (skipSpace(pld) == YES)
711
if (pld->ptr[pld->pos] == '"')
713
key = parseQuotedString(pld);
717
key = parseUnquotedString(pld, '=');
724
moreCharacters = skipSpace(pld);
725
if (moreCharacters == NO || pld->ptr[pld->pos] == ';')
728
if (_setCookieKey(dict, key, @"") == NO)
730
pld->err = @"invalid cookie pair";
735
else if (pld->ptr[pld->pos] == '=')
738
if (skipSpace(pld) == NO)
744
if (pld->ptr[pld->pos] == '"')
746
val = parseQuotedString(pld);
750
val = parseUnquotedString(pld, ';');
758
moreCharacters = skipSpace(pld);
759
if (_setCookieKey(dict, key, val) == NO)
761
pld->err = @"invalid cookie pair";
766
if (pld->ptr[pld->pos] == ';')
777
pld->err = @"unexpected character (wanted '=' or ';')";
783
if (dict == nil && _pld.err != nil)
786
[NSException raise: NSGenericException
787
format: @"Parse failed at line %d (char %d) - %@",
788
_pld.lin + 1, _pld.pos + 1, _pld.err];
790
return AUTORELEASE(dict);
793
/* Look for the comma that separates cookies. Commas can also occur in
794
date strings, like "expires", but perhaps it can occur other places.
795
For instance, the key/value pair key=value1,value2 is not really
796
valid, but should we handle it anyway? Definitely we should handle the
797
perfectly normal case of:
799
Set-Cookie: domain=test.com; expires=Thu, 12-Sep-2109 14:58:04 GMT;
803
which gets concatenated into something like:
805
Set-Cookie: domain=test.com; expires=Thu, 12-Sep-2109 14:58:04 GMT;
810
GSRangeOfCookie(NSString *string)
818
* An empty string is a nil property list.
820
range = NSMakeRange(NSNotFound, NSNotFound);
821
if ([string length] == 0)
826
d = [string dataUsingEncoding: NSUTF8StringEncoding];
827
NSCAssert(d, @"Couldn't get utf8 data from string.");
828
_pld.ptr = (unsigned char*)[d bytes];
830
_pld.end = [d length];
835
_pld.old = YES; // OpenStep style
837
while (skipSpace(pld) == YES)
839
if (pld->ptr[pld->pos] == ',')
841
/* Look ahead for something that will tell us if this is a
842
separate cookie or not */
843
unsigned saved_pos = pld->pos;
844
while (pld->ptr[pld->pos] != '=' && pld->ptr[pld->pos] != ';'
845
&& pld->ptr[pld->pos] != ',' && pld->pos < pld->end )
847
if (pld->ptr[pld->pos] == '=')
849
/* Separate comment */
850
range = NSMakeRange(0, saved_pos-1);
853
pld->pos = saved_pos;
857
if (range.location == NSNotFound)
858
range = NSMakeRange(0, [string length]);