Skip to content

Commit 738a647

Browse files
authored
Merge pull request #2373 from drodriguez/cookies-canonical-domain
HTTPCookie: parse domain according to RFC
2 parents cee74f4 + 84d0b3a commit 738a647

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
@@ -70,7 +70,7 @@ extension HTTPCookiePropertyKey {
7070
}
7171

7272
internal extension HTTPCookiePropertyKey {
73-
internal static let httpOnly = HTTPCookiePropertyKey(rawValue: "HttpOnly")
73+
static let httpOnly = HTTPCookiePropertyKey(rawValue: "HttpOnly")
7474

7575
static private let _setCookieAttributes: [String: HTTPCookiePropertyKey] = {
7676
// Only some attributes are valid in the Set-Cookie header.
@@ -570,10 +570,22 @@ open class HTTPCookie : NSObject {
570570
}
571571
}
572572

573-
// If domain wasn't provided, extract it from the URL
574-
if properties[.domain] == nil {
573+
if let domain = properties[.domain] as? String {
574+
// The provided domain string has to be prepended with a dot,
575+
// because the domain field indicates that it can be sent
576+
// subdomains of the domain (but only if it is not an IP address).
577+
if (!domain.hasPrefix(".") && !isIPv4Address(domain)) {
578+
properties[.domain] = ".\(domain)"
579+
}
580+
} else {
581+
// If domain wasn't provided, extract it from the URL. No dots in
582+
// this case, only exact matching.
575583
properties[.domain] = url.host
576584
}
585+
// Always lowercase the domain.
586+
if let domain = properties[.domain] as? String {
587+
properties[.domain] = domain.lowercased()
588+
}
577589

578590
// the default Path is "/"
579591
if let path = properties[.path] as? String, path.first == "/" {
@@ -606,6 +618,11 @@ open class HTTPCookie : NSObject {
606618
return (name, value)
607619
}
608620

621+
private class func isIPv4Address(_ string: String) -> Bool {
622+
var x = in_addr()
623+
return inet_pton(AF_INET, string, &x) == 1
624+
}
625+
609626
/// Returns a dictionary representation of the receiver.
610627
///
611628
/// This method returns a dictionary representation of the

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),
@@ -303,6 +304,53 @@ class TestHTTPCookie: XCTestCase {
303304
}
304305
}
305306

307+
func test_cookieDomainCanonicalization() throws {
308+
do {
309+
let headers = [
310+
"Set-Cookie": "PREF=a=b; expires=\(formattedCookieTime(sinceNow: 100))); path=/; domain=eXample.com"
311+
]
312+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: try URL(string: "http://eXample.com").unwrapped())
313+
XCTAssertEqual(cookies.count, 1)
314+
XCTAssertEqual(cookies.first?.domain, ".example.com")
315+
}
316+
317+
do {
318+
let headers = [
319+
"Set-Cookie": "PREF=a=b; expires=\(formattedCookieTime(sinceNow: 100))); path=/; domain=.eXample.com"
320+
]
321+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: try URL(string: "http://eXample.com").unwrapped())
322+
XCTAssertEqual(cookies.count, 1)
323+
XCTAssertEqual(cookies.first?.domain, ".example.com")
324+
}
325+
326+
do {
327+
let headers = [
328+
"Set-Cookie": "PREF=a=b; expires=\(formattedCookieTime(sinceNow: 100))); path=/; domain=a.eXample.com"
329+
]
330+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: try URL(string: "http://a.eXample.com").unwrapped())
331+
XCTAssertEqual(cookies.count, 1)
332+
XCTAssertEqual(cookies.first?.domain, ".a.example.com")
333+
}
334+
335+
do {
336+
let headers = [
337+
"Set-Cookie": "PREF=a=b; expires=\(formattedCookieTime(sinceNow: 100))); path=/"
338+
]
339+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: try URL(string: "http://a.eXample.com").unwrapped())
340+
XCTAssertEqual(cookies.count, 1)
341+
XCTAssertEqual(cookies.first?.domain, "a.example.com")
342+
}
343+
344+
do {
345+
let headers = [
346+
"Set-Cookie": "PREF=a=b; expires=\(formattedCookieTime(sinceNow: 100))); path=/; domain=1.2.3.4"
347+
]
348+
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headers, for: try URL(string: "http://eXample.com").unwrapped())
349+
XCTAssertEqual(cookies.count, 1)
350+
XCTAssertEqual(cookies.first?.domain, "1.2.3.4")
351+
}
352+
}
353+
306354
func test_cookieExpiresDateFormats() {
307355
let testDate = Date(timeIntervalSince1970: 1577881800)
308356
let cookieString =
@@ -320,7 +368,7 @@ class TestHTTPCookie: XCTestCase {
320368
XCTAssertEqual(cookies.count, 3)
321369
cookies.forEach { cookie in
322370
XCTAssertEqual(cookie.expiresDate, testDate)
323-
XCTAssertEqual(cookie.domain, "swift.org")
371+
XCTAssertEqual(cookie.domain, ".swift.org")
324372
XCTAssertEqual(cookie.path, "/")
325373
}
326374
}
@@ -333,4 +381,11 @@ class TestHTTPCookie: XCTestCase {
333381
XCTFail("Unable to create cookie with substring")
334382
}
335383
}
384+
385+
private func formattedCookieTime(sinceNow seconds: TimeInterval) -> String {
386+
let f = DateFormatter()
387+
f.timeZone = TimeZone(abbreviation: "GMT")
388+
f.dateFormat = "EEEE',' dd'-'MMM'-'yy HH':'mm':'ss z"
389+
return f.string(from: Date(timeIntervalSinceNow: seconds))
390+
}
336391
}

0 commit comments

Comments
 (0)