/* * Copyright (c) 2008-2024 Jonathan Schleifer <js@nil.im> * * All rights reserved. * * This file is part of ObjFW. It may be distributed under the terms of the * Q Public License 1.0, which can be found in the file LICENSE.QPL included in * the packaging of this file. * * Alternatively, it may be distributed under the terms of the GNU General * Public License, either version 2 or 3, which can be found in the file * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this * file. */ #include "config.h" #import "OFHTTPCookie.h" #import "OFArray.h" #import "OFDate.h" #import "OFDictionary.h" #import "OFIRI.h" #import "OFInvalidFormatException.h" static void handleAttribute(OFHTTPCookie *cookie, OFString *name, OFString *value) { OFString *lowercaseName = name.lowercaseString; if (value != nil) { if ([lowercaseName isEqual: @"expires"]) { OFDate *date = [OFDate dateWithDateString: value format: @"%a, %d %b %Y %H:%M:%S %z"]; cookie.expires = date; } else if ([lowercaseName isEqual: @"max-age"]) { OFDate *date = [OFDate dateWithTimeIntervalSinceNow: value.unsignedLongLongValue]; cookie.expires = date; } else if ([lowercaseName isEqual: @"domain"]) cookie.domain = value; else if ([lowercaseName isEqual: @"path"]) cookie.path = value; else [cookie.extensions addObject: [OFString stringWithFormat: @"%@=%@", name, value]]; } else { if ([lowercaseName isEqual: @"secure"]) cookie.secure = true; else if ([lowercaseName isEqual: @"httponly"]) cookie.HTTPOnly = true; else if (name.length > 0) [cookie.extensions addObject: name]; } } @implementation OFHTTPCookie @synthesize name = _name, value = _value, domain = _domain, path = _path; @synthesize expires = _expires, secure = _secure, HTTPOnly = _HTTPOnly; @synthesize extensions = _extensions; + (OFArray OF_GENERIC(OFHTTPCookie *) *)cookiesWithResponseHeaderFields: (OFDictionary OF_GENERIC(OFString *, OFString *) *)headerFields forIRI: (OFIRI *)IRI { OFMutableArray OF_GENERIC(OFHTTPCookie *) *ret = [OFMutableArray array]; void *pool = objc_autoreleasePoolPush(); OFString *string = [headerFields objectForKey: @"Set-Cookie"]; OFString *domain = IRI.IRIByAddingPercentEncodingForUnicodeCharacters .host; const OFUnichar *characters = string.characters; size_t length = string.length, last = 0; enum { statePreName, stateName, stateExpectValue, stateValue, stateQuotedValue, statePostQuotedValue, statePreAttrName, stateAttrName, stateAttrValue } state = statePreName; OFString *name = nil, *value = nil; for (size_t i = 0; i < length; i++) { switch (state) { case statePreName: if (characters[i] != ' ') { state = stateName; last = i; i--; } break; case stateName: if (characters[i] == '=') { name = [string substringWithRange: OFMakeRange(last, i - last)]; state = stateExpectValue; } break; case stateExpectValue: if (characters[i] == '"') { state = stateQuotedValue; last = i + 1; } else { state = stateValue; last = i; } i--; break; case stateValue: if (characters[i] == ';' || characters[i] == ',') { value = [string substringWithRange: OFMakeRange(last, i - last)]; [ret addObject: [OFHTTPCookie cookieWithName: name value: value domain: domain]]; state = (characters[i] == ';' ? statePreAttrName : statePreName); } break; case stateQuotedValue: if (characters[i] == '"') { value = [string substringWithRange: OFMakeRange(last, i - last)]; [ret addObject: [OFHTTPCookie cookieWithName: name value: value domain: domain]]; state = statePostQuotedValue; } break; case statePostQuotedValue: if (characters[i] == ';') state = statePreAttrName; else if (characters[i] == ',') state = statePreName; else @throw [OFInvalidFormatException exception]; break; case statePreAttrName: if (characters[i] != ' ') { state = stateAttrName; last = i; i--; } break; case stateAttrName: if (characters[i] == '=') { name = [string substringWithRange: OFMakeRange(last, i - last)]; state = stateAttrValue; last = i + 1; } else if (characters[i] == ';' || characters[i] == ',') { name = [string substringWithRange: OFMakeRange(last, i - last)]; handleAttribute(ret.lastObject, name, nil); state = (characters[i] == ';' ? statePreAttrName : statePreName); } break; case stateAttrValue: if (characters[i] == ';' || characters[i] == ',') { value = [string substringWithRange: OFMakeRange(last, i - last)]; /* * Expires often contains a comma, even though * the comma is used as a separator for * concatenating headers as per RFC 2616, * meaning RFC 6265 contradicts RFC 2616. * Solve this by special casing this. */ if (characters[i] == ',' && [name caseInsensitiveCompare: @"expires"] == OFOrderedSame && value.length == 3 && ([value isEqual: @"Mon"] || [value isEqual: @"Tue"] || [value isEqual: @"Wed"] || [value isEqual: @"Thu"] || [value isEqual: @"Fri"] || [value isEqual: @"Sat"] || [value isEqual: @"Sun"])) break; handleAttribute(ret.lastObject, name, value); state = (characters[i] == ';' ? statePreAttrName : statePreName); } break; } } switch (state) { case statePreName: case statePostQuotedValue: case statePreAttrName: break; case stateName: case stateQuotedValue: @throw [OFInvalidFormatException exception]; break; case stateValue: value = [string substringWithRange: OFMakeRange(last, length - last)]; [ret addObject: [OFHTTPCookie cookieWithName: name value: value domain: domain]]; break; /* We end up here if the cookie is just foo= */ case stateExpectValue: [ret addObject: [OFHTTPCookie cookieWithName: name value: @"" domain: domain]]; break; case stateAttrName: if (last != length) { name = [string substringWithRange: OFMakeRange(last, length - last)]; handleAttribute(ret.lastObject, name, nil); } break; case stateAttrValue: value = [string substringWithRange: OFMakeRange(last, length - last)]; handleAttribute(ret.lastObject, name, value); break; } objc_autoreleasePoolPop(pool); return ret; } + (OFDictionary *)requestHeaderFieldsWithCookies: (OFArray OF_GENERIC(OFHTTPCookie *) *)cookies { OFDictionary OF_GENERIC(OFString *, OFString *) *ret; void *pool; OFMutableString *cookieString; bool first = true; if (cookies.count == 0) return [OFDictionary dictionary]; pool = objc_autoreleasePoolPush(); cookieString = [OFMutableString string]; for (OFHTTPCookie *cookie in cookies) { if OF_UNLIKELY (first) first = false; else [cookieString appendString: @"; "]; [cookieString appendString: cookie.name]; [cookieString appendString: @"="]; [cookieString appendString: cookie.value]; } ret = [[OFDictionary alloc] initWithObject: cookieString forKey: @"Cookie"]; objc_autoreleasePoolPop(pool); return [ret autorelease]; } + (instancetype)cookieWithName: (OFString *)name value: (OFString *)value domain: (OFString *)domain { return [[[self alloc] initWithName: name value: value domain: domain] autorelease]; } - (instancetype)init { OF_INVALID_INIT_METHOD } - (instancetype)initWithName: (OFString *)name value: (OFString *)value domain: (OFString *)domain { self = [super init]; @try { _name = [name copy]; _value = [value copy]; _domain = [domain copy]; _path = @"/"; _extensions = [[OFMutableArray alloc] init]; } @catch (id e) { [self release]; @throw e; } return self; } - (void)dealloc { [_name release]; [_value release]; [_domain release]; [_path release]; [_expires release]; [_extensions release]; [super dealloc]; } - (bool)isEqual: (id)object { OFHTTPCookie *cookie; if (object == self) return true; if (![object isKindOfClass: [OFHTTPCookie class]]) return false; cookie = object; if (![cookie->_name isEqual: _name]) return false; if (![cookie->_value isEqual: _value]) return false; if (cookie->_domain != _domain && ![cookie->_domain isEqual: _domain]) return false; if (cookie->_path != _path && ![cookie->_path isEqual: _path]) return false; if (cookie->_expires != _expires && ![cookie->_expires isEqual: _expires]) return false; if (cookie->_secure != _secure) return false; if (cookie->_HTTPOnly != _HTTPOnly) return false; if (cookie->_extensions != _extensions && ![cookie->_extensions isEqual: _extensions]) return false; return true; } - (unsigned long)hash { unsigned long hash; OFHashInit(&hash); OFHashAddHash(&hash, _name.hash); OFHashAddHash(&hash, _value.hash); OFHashAddHash(&hash, _domain.hash); OFHashAddHash(&hash, _path.hash); OFHashAddHash(&hash, _expires.hash); OFHashAddByte(&hash, _secure); OFHashAddByte(&hash, _HTTPOnly); OFHashAddHash(&hash, _extensions.hash); OFHashFinalize(&hash); return hash; } - (id)copy { OFHTTPCookie *copy = [[OFHTTPCookie alloc] initWithName: _name value: _value domain: _domain]; @try { copy->_path = [_path copy]; copy->_expires = [_expires copy]; copy->_secure = _secure; copy->_HTTPOnly = _HTTPOnly; [copy->_extensions addObjectsFromArray: _extensions]; } @catch (id e) { [copy release]; @throw e; } return copy; } - (OFString *)description { OFMutableString *ret = [OFMutableString stringWithFormat: @"%@=%@", _name, _value]; void *pool = objc_autoreleasePoolPush(); [ret appendFormat: @"; Domain=%@; Path=%@", _domain, _path]; if (_expires != nil) [ret appendString: [_expires dateStringWithFormat: @"; Expires=%a, %d %b %Y " @"%H:%M:%S +0000"]]; if (_secure) [ret appendString: @"; Secure"]; if (_HTTPOnly) [ret appendString: @"; HTTPOnly"]; if (_extensions.count > 0) [ret appendFormat: @"; %@", [_extensions componentsJoinedByString: @"; "]]; objc_autoreleasePoolPop(pool); [ret makeImmutable]; return ret; } @end