Skip to content

Initial implementation of HTTPCookieStorage #672

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Foundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
0383A1751D2E558A0052E5D1 /* TestNSStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0383A1741D2E558A0052E5D1 /* TestNSStream.swift */; };
1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1520469A1D8AEABE00D02E36 /* HTTPServer.swift */; };
159884921DCC877700E3314C /* TestNSHTTPCookieStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159884911DCC877700E3314C /* TestNSHTTPCookieStorage.swift */; };
231503DB1D8AEE5D0061694D /* TestNSDecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 231503DA1D8AEE5D0061694D /* TestNSDecimal.swift */; };
294E3C1D1CC5E19300E4F44C /* TestNSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */; };
2EBE67A51C77BF0E006583D5 /* TestNSDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBE67A31C77BF05006583D5 /* TestNSDateFormatter.swift */; };
Expand Down Expand Up @@ -466,6 +467,7 @@
/* Begin PBXFileReference section */
0383A1741D2E558A0052E5D1 /* TestNSStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSStream.swift; sourceTree = "<group>"; };
1520469A1D8AEABE00D02E36 /* HTTPServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPServer.swift; sourceTree = "<group>"; };
159884911DCC877700E3314C /* TestNSHTTPCookieStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNSHTTPCookieStorage.swift; sourceTree = "<group>"; };
22B9C1E01C165D7A00DECFF9 /* TestNSDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSDate.swift; sourceTree = "<group>"; };
231503DA1D8AEE5D0061694D /* TestNSDecimal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSDecimal.swift; sourceTree = "<group>"; };
294E3C1C1CC5E19300E4F44C /* TestNSAttributedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSAttributedString.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1338,6 +1340,7 @@
EA66F65A1BF1976100136161 /* Tests */ = {
isa = PBXGroup;
children = (
159884911DCC877700E3314C /* TestNSHTTPCookieStorage.swift */,
D4FE895A1D703D1100DA7986 /* TestURLRequest.swift */,
C93559281C12C49F009FD6A9 /* TestNSAffineTransform.swift */,
EA66F63C1BF1619600136161 /* TestNSArray.swift */,
Expand Down Expand Up @@ -2235,6 +2238,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
159884921DCC877700E3314C /* TestNSHTTPCookieStorage.swift in Sources */,
5FE52C951D147D1C00F7D270 /* TestNSTextCheckingResult.swift in Sources */,
5B13B3451C582D4C00651CE2 /* TestNSString.swift in Sources */,
1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */,
Expand Down
200 changes: 186 additions & 14 deletions Foundation/NSHTTPCookieStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//

import Dispatch

/*!
@enum NSHTTPCookieAcceptPolicy
Expand All @@ -18,7 +18,6 @@
*/
extension HTTPCookie {
public enum AcceptPolicy : UInt {

case always
case never
case onlyFromMainDocumentDomain
Expand All @@ -35,11 +34,58 @@ extension HTTPCookie {
generate cookie-related HTTP header fields.
*/
open class HTTPCookieStorage: NSObject {

public override init() { NSUnimplemented() }


private static var sharedStorage: HTTPCookieStorage?
private static var sharedCookieStorages: [String: HTTPCookieStorage] = [:] //for group storage containers

private var cookieFilePath: String!
private let workQueue: DispatchQueue = DispatchQueue(label: "HTTPCookieStorage.workqueue")
var allCookies: [String: HTTPCookie]

private init(cookieStorageName: String) {
allCookies = [:]
cookieAcceptPolicy = .always
super.init()
//TODO: cookieFilePath = filePath(path: _CFXDGCreateConfigHomePath()._swiftObject, fileName: "/.cookies." + cookieStorageName)
cookieFilePath = filePath(path: NSHomeDirectory() + "/.config", fileName: "/.cookies." + cookieStorageName)
loadPersistedCookies()
}

private func loadPersistedCookies() {
guard let cookies = NSMutableDictionary(contentsOfFile: cookieFilePath) else { return }
var cookies0 = _SwiftValue.fetch(cookies) as? [String: [String: Any]] ?? [:]
for key in cookies0.keys {
if let cookie = createCookie(cookies0[key]!) {
allCookies[key] = cookie
}
}
}

private func directory(with path: String) -> Bool {
guard !FileManager.default.fileExists(atPath: path) else { return true }

do {
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
return true
} catch {
return false
}
}

private func filePath(path: String, fileName: String) -> String {
if directory(with: path) {
return path + fileName
}
//if we were unable to create the desired directory, create the cookie file in the `pwd`
return fileName
}

open var cookies: [HTTPCookie]? {
NSUnimplemented()
var theCookies: [HTTPCookie]?
workQueue.sync {
theCookies = Array(self.allCookies.values)
}
return theCookies
}

/*!
Expand All @@ -49,7 +95,14 @@ open class HTTPCookieStorage: NSObject {
@discussion Starting in OS X 10.11, each app has its own sharedHTTPCookieStorage singleton,
which will not be shared with other applications.
*/
class var shared: HTTPCookieStorage { get { NSUnimplemented() } }
open class var shared: HTTPCookieStorage {
get {
if sharedStorage == nil {
sharedStorage = HTTPCookieStorage(cookieStorageName: "shared")
}
return sharedStorage!
}
}

/*!
@method sharedCookieStorageForGroupContainerIdentifier:
Expand All @@ -62,28 +115,100 @@ open class HTTPCookieStorage: NSObject {
shared among all applications and extensions with access to the same application group. Subsequent calls to this
method with the same identifier will return the same cookie storage instance.
*/
class func sharedCookieStorage(forGroupContainerIdentifier identifier: String) -> HTTPCookieStorage { NSUnimplemented() }
open class func sharedCookieStorage(forGroupContainerIdentifier identifier: String) -> HTTPCookieStorage {
guard let cookieStorage = sharedCookieStorages[identifier] else {
let newCookieStorage = HTTPCookieStorage(cookieStorageName: identifier)
sharedCookieStorages[identifier] = newCookieStorage
return newCookieStorage
}
return cookieStorage
}


/*!
@method setCookie:
@abstract Set a cookie
@discussion The cookie will override an existing cookie with the
same name, domain and path, if any.
*/
open func setCookie(_ cookie: HTTPCookie) { NSUnimplemented() }
open func setCookie(_ cookie: HTTPCookie) {
workQueue.sync {
guard cookieAcceptPolicy != .never else { return }

//add or override
let key = cookie.domain + cookie.path + cookie.name
if let _ = allCookies.index(forKey: key) {
allCookies.updateValue(cookie, forKey: key)
} else {
allCookies[key] = cookie
}

//remove stale cookies, these may include the one we just added
let expired = allCookies.filter { (_, value) in value.expiresDate != nil && value.expiresDate!.timeIntervalSinceNow < 0 }
for (key,_) in expired {
self.allCookies.removeValue(forKey: key)
}

updatePersistentStore()
}
}

private func createCookie(_ properties: [String: Any]) -> HTTPCookie? {
var cookieProperties: [HTTPCookiePropertyKey: Any] = [:]
for (key, value) in properties {
if key == "Expires" {
guard let timestamp = value as? NSNumber else { continue }
cookieProperties[HTTPCookiePropertyKey(rawValue: key)] = Date(timeIntervalSince1970: timestamp.doubleValue)
} else {
cookieProperties[HTTPCookiePropertyKey(rawValue: key)] = properties[key]
}
}
return HTTPCookie(properties: cookieProperties)
}

private func updatePersistentStore() {
//persist cookies
var persistDictionary: [String : [String : Any]] = [:]
let persistable = allCookies.filter { (_, value) in
value.expiresDate != nil &&
value.isSessionOnly == false &&
value.expiresDate!.timeIntervalSinceNow > 0
}

for (key,cookie) in persistable {
persistDictionary[key] = cookie.persistableDictionary()
}

let nsdict = _SwiftValue.store(persistDictionary) as! NSDictionary
_ = nsdict.write(toFile: cookieFilePath, atomically: true)
}

/*!
@method deleteCookie:
@abstract Delete the specified cookie
*/
open func deleteCookie(_ cookie: HTTPCookie) { NSUnimplemented() }
open func deleteCookie(_ cookie: HTTPCookie) {
let key = cookie.domain + cookie.path + cookie.name
workQueue.sync {
self.allCookies.removeValue(forKey: key)
updatePersistentStore()
}
}

/*!
@method removeCookiesSince:
@abstract Delete all cookies from the cookie storage since the provided date.
*/
open func removeCookies(since date: Date) { NSUnimplemented() }

open func removeCookies(since date: Date) {
let cookiesSinceDate = allCookies.values.filter {
$0.properties![.created] as! Double > date.timeIntervalSinceReferenceDate
}
for cookie in cookiesSinceDate {
deleteCookie(cookie)
}
updatePersistentStore()
}

/*!
@method cookiesForURL:
@abstract Returns an array of cookies to send to the given URL.
Expand All @@ -94,7 +219,14 @@ open class HTTPCookieStorage: NSObject {
<tt>+[NSCookie requestHeaderFieldsWithCookies:]</tt> to turn this array
into a set of header fields to add to a request.
*/
open func cookies(for url: URL) -> [HTTPCookie]? { NSUnimplemented() }
open func cookies(for url: URL) -> [HTTPCookie]? {
var cookies: [HTTPCookie]?
guard let host = url.host else { return nil }
workQueue.sync {
cookies = Array(allCookies.values.filter{ $0.domain == host })
}
return cookies
}

/*!
@method setCookies:forURL:mainDocumentURL:
Expand All @@ -113,7 +245,26 @@ open class HTTPCookieStorage: NSObject {
dictionary and then use this method to store the resulting cookies
in accordance with policy settings.
*/
open func setCookies(_ cookies: [HTTPCookie], for url: URL?, mainDocumentURL: URL?) { NSUnimplemented() }
open func setCookies(_ cookies: [HTTPCookie], for url: URL?, mainDocumentURL: URL?) {
//if the cookieAcceptPolicy is `never` we don't have anything to do
guard cookieAcceptPolicy != .never else { return }

//if the urls don't have a host, we cannot do anything
guard let urlHost = url?.host else { return }

if mainDocumentURL != nil && cookieAcceptPolicy == .onlyFromMainDocumentDomain {
guard let mainDocumentHost = mainDocumentURL?.host else { return }

//the url.host must be a suffix of manDocumentURL.host, this is based on Darwin's behaviour
guard mainDocumentHost.hasSuffix(urlHost) else { return }
}

//save only those cookies whose domain matches with the url.host
let validCookies = cookies.filter { urlHost == $0.domain }
for cookie in validCookies {
setCookie(cookie)
}
}

/*!
@method cookieAcceptPolicy
Expand All @@ -138,3 +289,24 @@ public extension Notification.Name {
*/
public static let NSHTTPCookieManagerCookiesChanged = Notification.Name(rawValue: "NSHTTPCookieManagerCookiesChangedNotification")
}

extension HTTPCookie {
internal func persistableDictionary() -> [String: Any] {
var properties: [String: Any] = [:]
properties[HTTPCookiePropertyKey.name.rawValue] = name
properties[HTTPCookiePropertyKey.path.rawValue] = path
properties[HTTPCookiePropertyKey.value.rawValue] = _value
properties[HTTPCookiePropertyKey.secure.rawValue] = _secure
properties[HTTPCookiePropertyKey.version.rawValue] = _version
properties[HTTPCookiePropertyKey.expires.rawValue] = _expiresDate?.timeIntervalSince1970 ?? Date().timeIntervalSince1970 //OK?
properties[HTTPCookiePropertyKey.domain.rawValue] = _domain
if let commentURL = _commentURL {
properties[HTTPCookiePropertyKey.commentURL.rawValue] = commentURL.absoluteString
}
if let comment = _comment {
properties[HTTPCookiePropertyKey.comment.rawValue] = comment
}
properties[HTTPCookiePropertyKey.port.rawValue] = portList
return properties
}
}
Loading