Skip to content

Commit 29f9550

Browse files
committed
HTTPCookie: parse domain according to RFC
When the domain field is specified, the cookie is intended for the domain and subdomains, so the value of domain has to be prefixed by a dot. Added tests to check for this behaviour and modified the incorrect tests.
1 parent 000c62b commit 29f9550

File tree

2 files changed

+76
-4
lines changed

2 files changed

+76
-4
lines changed

Foundation/HTTPCookie.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ open class HTTPCookie : NSObject {
445445

446446
//Bake a cookie
447447
private class func createHttpCookie(url: URL, pairs: ArraySlice<String>) -> HTTPCookie? {
448-
var properties: [HTTPCookiePropertyKey : Any] = [:]
448+
var properties: [HTTPCookiePropertyKey : String] = [:]
449449
for pair in pairs {
450450
let name = pair.components(separatedBy: "=")[0]
451451
var value = pair.components(separatedBy: "\(name)=")[1] //a value can have an "="
@@ -455,10 +455,22 @@ open class HTTPCookie : NSObject {
455455
properties[canonicalize(name)] = value
456456
}
457457

458-
// If domain wasn't provided, extract it from the URL
459-
if properties[.domain] == nil {
458+
if let domain = properties[.domain] {
459+
// The provided domain string has to be prepended with a dot,
460+
// because the domain field indicates that it can be sent
461+
// subdomains of the domain (but only if it is not an IP address).
462+
if (!domain.hasPrefix(".") && !isIPv4Address(domain)) {
463+
properties[.domain] = ".\(domain)"
464+
}
465+
} else {
466+
// If domain wasn't provided, extract it from the URL. No dots in
467+
// this case, only exact matching.
460468
properties[.domain] = url.host
461469
}
470+
// Always lowercase the domain.
471+
if let domain = properties[.domain] {
472+
properties[.domain] = domain.lowercased()
473+
}
462474

463475
//the default Path is "/"
464476
if properties[.path] == nil {
@@ -476,6 +488,11 @@ open class HTTPCookie : NSObject {
476488
return value
477489
}
478490

491+
private class func isIPv4Address(_ string: String) -> Bool {
492+
var x = in_addr()
493+
return inet_pton(AF_INET, string, &x) == 1
494+
}
495+
479496
//These cookie attributes are defined in RFC 6265 and 2965(which is obsolete)
480497
//HTTPCookie supports these
481498
private class func isCookieAttribute(_ string: String) -> Bool {

TestFoundation/TestHTTPCookie.swift

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class TestHTTPCookie: XCTestCase {
1212
static var allTests: [(String, (TestHTTPCookie) -> () throws -> Void)] {
1313
return [
1414
("test_BasicConstruction", test_BasicConstruction),
15+
("test_cookieDomainCanonicalization", test_cookieDomainCanonicalization),
1516
("test_RequestHeaderFields", test_RequestHeaderFields),
1617
("test_cookiesWithResponseHeader1cookie", test_cookiesWithResponseHeader1cookie),
1718
("test_cookiesWithResponseHeader0cookies", test_cookiesWithResponseHeader0cookies),
@@ -171,6 +172,53 @@ class TestHTTPCookie: XCTestCase {
171172
XCTAssertEqual(cookies[0].path, "/")
172173
}
173174

175+
func test_cookieDomainCanonicalization() throws {
176+
do {
177+
let headers = [
178+
"Set-Cookie": "PREF=a=b; expires=\(formattedCookieTime(sinceNow: 100))); path=/; domain=eXample.com"
179+
]
180+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: try URL(string: "http://eXample.com").unwrapped())
181+
XCTAssertEqual(cookies.count, 1)
182+
XCTAssertEqual(cookies.first?.domain, ".example.com")
183+
}
184+
185+
do {
186+
let headers = [
187+
"Set-Cookie": "PREF=a=b; expires=\(formattedCookieTime(sinceNow: 100))); path=/; domain=.eXample.com"
188+
]
189+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: try URL(string: "http://eXample.com").unwrapped())
190+
XCTAssertEqual(cookies.count, 1)
191+
XCTAssertEqual(cookies.first?.domain, ".example.com")
192+
}
193+
194+
do {
195+
let headers = [
196+
"Set-Cookie": "PREF=a=b; expires=\(formattedCookieTime(sinceNow: 100))); path=/; domain=a.eXample.com"
197+
]
198+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: try URL(string: "http://a.eXample.com").unwrapped())
199+
XCTAssertEqual(cookies.count, 1)
200+
XCTAssertEqual(cookies.first?.domain, ".a.example.com")
201+
}
202+
203+
do {
204+
let headers = [
205+
"Set-Cookie": "PREF=a=b; expires=\(formattedCookieTime(sinceNow: 100))); path=/"
206+
]
207+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: try URL(string: "http://a.eXample.com").unwrapped())
208+
XCTAssertEqual(cookies.count, 1)
209+
XCTAssertEqual(cookies.first?.domain, "a.example.com")
210+
}
211+
212+
do {
213+
let headers = [
214+
"Set-Cookie": "PREF=a=b; expires=\(formattedCookieTime(sinceNow: 100))); path=/; domain=1.2.3.4"
215+
]
216+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: try URL(string: "http://eXample.com").unwrapped())
217+
XCTAssertEqual(cookies.count, 1)
218+
XCTAssertEqual(cookies.first?.domain, "1.2.3.4")
219+
}
220+
}
221+
174222
func test_cookieExpiresDateFormats() {
175223
let testDate = Date(timeIntervalSince1970: 1577881800)
176224
let cookieString =
@@ -188,7 +236,7 @@ class TestHTTPCookie: XCTestCase {
188236
XCTAssertEqual(cookies.count, 3)
189237
cookies.forEach { cookie in
190238
XCTAssertEqual(cookie.expiresDate, testDate)
191-
XCTAssertEqual(cookie.domain, "swift.org")
239+
XCTAssertEqual(cookie.domain, ".swift.org")
192240
XCTAssertEqual(cookie.path, "/")
193241
}
194242
}
@@ -201,4 +249,11 @@ class TestHTTPCookie: XCTestCase {
201249
XCTFail("Unable to create cookie with substring")
202250
}
203251
}
252+
253+
private func formattedCookieTime(sinceNow seconds: TimeInterval) -> String {
254+
let f = DateFormatter()
255+
f.timeZone = TimeZone(abbreviation: "GMT")
256+
f.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
257+
return f.string(from: Date(timeIntervalSinceNow: seconds))
258+
}
204259
}

0 commit comments

Comments
 (0)