Skip to content
This repository has been archived by the owner on Apr 7, 2022. It is now read-only.

Commit

Permalink
refactor HTTPCookieValue
Browse files Browse the repository at this point in the history
  • Loading branch information
tanner0101 committed Apr 17, 2018
1 parent 29606de commit d8bf032
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 97 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Expand Up @@ -11,7 +11,7 @@ let package = Package(
.package(url: "https://github.com/vapor/core.git", from: "3.0.0"),

// Event-driven network application framework for high performance protocol servers & clients, non-blocking.
.package(url: "https://github.com/apple/swift-nio.git", from: "1.2.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "1.4.0"),

// Bindings to OpenSSL-compatible libraries for TLS support in SwiftNIO
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "1.0.1"),
Expand Down
@@ -1,27 +1,27 @@
/// A single cookie (key/value pair).
public struct HTTPCookie {
public struct HTTPCookieValue: ExpressibleByStringLiteral {
/// Parses an individual `HTTPCookie` from a `String`.
///
/// let cookie = HTTPCookie.parse("sessionID=123; HTTPOnly")
///
/// - parameters:
/// - data: `LosslessDataConvertible` to parse the cookie from.
/// - returns: `HTTPCookie` or `nil` if the data is invalid.
public static func parse(_ data: LosslessDataConvertible) -> HTTPCookie? {
public static func parse(_ data: LosslessDataConvertible) -> (String, HTTPCookieValue)? {
/// Parse `HeaderValue` or return nil.
guard let header = HeaderValue.parse(data) else {
return nil
}

/// Fetch name and value.
var name: String
var value: String
var string: String

let parts = header.value.split(separator: "=", maxSplits: 1)
switch parts.count {
case 2:
name = String(parts[0]).trimmingCharacters(in: .whitespaces)
value = String(parts[1]).trimmingCharacters(in: .whitespaces)
string = String(parts[1]).trimmingCharacters(in: .whitespaces)
default: return nil
}

Expand All @@ -47,9 +47,8 @@ public struct HTTPCookie {
}
}

return HTTPCookie(
name: name,
value: value,
let value = HTTPCookieValue(
string: string,
expires: expires,
maxAge: maxAge,
domain: domain,
Expand All @@ -58,47 +57,50 @@ public struct HTTPCookie {
isHTTPOnly: httpOnly,
sameSite: sameSite
)
return (name, value)
}

/// The cookie's key.
public var name: String

/// The cookie's value.
public var value: String
public var string: String

/// The `Cookie`'s expiration date
/// The cookie's expiration date
public var expires: Date?

/// The maximum `Cookie` age in seconds
/// The maximum cookie age in seconds.
public var maxAge: Int?

/// The affected domain at which the `Cookie` is active
/// The affected domain at which the cookie is active.
public var domain: String?

/// The path at which the `Cookie` is active
/// The path at which the cookie is active.
public var path: String?

/// Limits the cookie to secure connections
/// Limits the cookie to secure connections.
public var isSecure: Bool

/// Does not expose the `Cookie` over non-HTTP channels
/// Does not expose the cookie over non-HTTP channels.
public var isHTTPOnly: Bool

/// A cookie which can only be sent in requests originating from the same origin as the target domain.
///
/// This restriction mitigates attacks such as cross-site request forgery (XSRF).
public var sameSite: HTTPSameSitePolicy?

/// Creates a new `HTTPCookie`.
/// Creates a new `HTTPCookieValue`.
///
/// let cookie = HTTPCookie(name: "sessionID", value: "123")
/// let cookie = HTTPCookieValue(string: "123")
///
/// - parameters:
/// - named: Key for this cookie.
/// - value: Value for this cookie.
/// - expires: The cookie's expiration date. Defaults to `nil`.
/// - maxAge: The maximum cookie age in seconds. Defaults to `nil`.
/// - domain: The affected domain at which the cookie is active. Defaults to `nil`.
/// - path: The path at which the cookie is active. Defaults to `"/"`.
/// - isSecure: Limits the cookie to secure connections. Defaults to `false`.
/// - isHTTPOnly: Does not expose the cookie over non-HTTP channels. Defaults to `false`.
/// - sameSite: See `HTTPSameSitePolicy`. Defaults to `nil`.
public init(
name: String,
value: String,
string: String,
expires: Date? = nil,
maxAge: Int? = nil,
domain: String? = nil,
Expand All @@ -107,8 +109,7 @@ public struct HTTPCookie {
isHTTPOnly: Bool = false,
sameSite: HTTPSameSitePolicy? = nil
) {
self.name = name
self.value = value
self.string = string
self.expires = expires
self.maxAge = maxAge
self.domain = domain
Expand All @@ -118,9 +119,14 @@ public struct HTTPCookie {
self.sameSite = sameSite
}

/// See `ExpressibleByStringLiteral`.
public init(stringLiteral value: String) {
self.init(string: value)
}

/// Seriaizes an `HTTPCookie` to a `String`.
public func serialize() -> String {
var serialized = "\(name)=\(value)"
public func serialize(name: String) -> String {
var serialized = "\(name)=\(string)"

if let expires = self.expires {
serialized += "; Expires=\(expires.rfc1123)"
Expand Down
88 changes: 25 additions & 63 deletions Sources/HTTP/Cookies/HTTPCookies.swift
@@ -1,65 +1,59 @@
/// A collection of `HTTPCookie`s.
public struct HTTPCookies: ExpressibleByArrayLiteral, Sequence {
public struct HTTPCookies: ExpressibleByDictionaryLiteral {
/// Internal storage.
private var cookies: [HTTPCookie]
private var cookies: [String: HTTPCookieValue]

/// Creates an empty `HTTPCookies`
public init() {
self.cookies = []
self.cookies = [:]
}

// MARK: Parse

/// Parses a `Request` cookie
public static func parse(cookieHeader: String) -> HTTPCookies? {
var cookies: HTTPCookies = []
var cookies: HTTPCookies = [:]

// cookies are sent separated by semicolons
let tokens = cookieHeader.components(separatedBy: ";")

for token in tokens {
// If a single deserialization fails, the cookies are malformed
guard let cookie = HTTPCookie.parse(token) else {
guard let (name, value) = HTTPCookieValue.parse(token) else {
return nil
}

cookies.add(cookie)
cookies[name] = value
}

return cookies
}

/// Parses a `Response` cookie
public static func parse(setCookieHeaders: [String]) -> HTTPCookies? {
var cookies: HTTPCookies = []
var cookies: HTTPCookies = [:]

for token in setCookieHeaders {
// If a single deserialization fails, the cookies are malformed
guard let cookie = HTTPCookie.parse(token) else {
guard let (name, value) = HTTPCookieValue.parse(token) else {
return nil
}

cookies.add(cookie)
cookies[name] = value
}

return cookies
}

/// Creates a `Cookies` from the contents of a `Cookie` Sequence
public init<C>(cookies: C) where C.Iterator.Element == HTTPCookie, C: Sequence {
self.cookies = Array(cookies)
}

/// See `ExpressibleByArrayLiteral`.
public init(arrayLiteral elements: HTTPCookie...) {
self.cookies = elements
}

/// Access `HTTPCookies` by name
public subscript(name: String) -> [HTTPCookie] {
return cookies.filter { $0.name == name }
/// See `ExpressibleByDictionaryLiteral`.
public init(dictionaryLiteral elements: (String, HTTPCookieValue)...) {
var cookies: [String: HTTPCookieValue] = [:]
for (name, value) in elements {
cookies[name] = value
}
self.cookies = cookies
}

// MARK: Serialize

/// Seriaizes the `Cookies` for a `Request`
Expand All @@ -69,8 +63,8 @@ public struct HTTPCookies: ExpressibleByArrayLiteral, Sequence {
return
}

let cookie: String = map { cookie in
return cookie.serialize()
let cookie: String = cookies.map { (name, value) in
return value.serialize(name: name)
}.joined(separator: "; ")

request.headers.replaceOrAdd(name: .cookie, value: cookie)
Expand All @@ -83,48 +77,16 @@ public struct HTTPCookies: ExpressibleByArrayLiteral, Sequence {
return
}

for cookie in self {
response.headers.add(name: .setCookie, value: cookie.serialize())
for (name, value) in cookies {
response.headers.add(name: .setCookie, value: value.serialize(name: name))
}
}

// MARK: Access

/// Fetches the first `HTTPCookie` with matching name.
public func firstCookie(named name: String) -> HTTPCookie? {
for cookie in cookies {
if cookie.name == name {
return cookie
}
}
return nil
}

/// Adds a new `HTTPCookie`, removing all existing cookies with the same name
/// if any exist.
///
/// - parameters:
/// - cookie: New `HTTPCookie` to add.
public mutating func replaceOrAdd(_ cookie: HTTPCookie) {
remove(name: cookie.name)
add(cookie)
}

/// Removes all `HTTPCookie`s with the supplied name.
public mutating func remove(name: String) {
cookies = cookies.filter { $0.name != name }
}

/// Adds a new `HTTPCookie`, even if one with the same name already exists.
///
/// - parameters:
/// - cookie: New `HTTPCookie` to add.
public mutating func add(_ cookie: HTTPCookie) {
cookies.append(cookie)
}

/// See `Sequence`.
public func makeIterator() -> IndexingIterator<[HTTPCookie]> {
return cookies.makeIterator()
/// Access `HTTPCookies` by name
public subscript(name: String) -> HTTPCookieValue? {
get { return cookies[name] }
set { cookies[name] = newValue }
}
}
2 changes: 1 addition & 1 deletion Sources/HTTP/Message/HTTPRequest.swift
Expand Up @@ -47,7 +47,7 @@ public struct HTTPRequest: HTTPMessage {
/// Get and set `HTTPCookies` for this `HTTPRequest`
/// This accesses the `"Cookie"` header.
public var cookies: HTTPCookies {
get { return headers.firstValue(name: .cookie).flatMap(HTTPCookies.parse) ?? [] }
get { return headers.firstValue(name: .cookie).flatMap(HTTPCookies.parse) ?? [:] }
set { newValue.serialize(into: &self) }
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/HTTP/Message/HTTPResponse.swift
Expand Up @@ -32,7 +32,7 @@ public struct HTTPResponse: HTTPMessage {
/// Get and set `HTTPCookies` for this `HTTPResponse`
/// This accesses the `"Set-Cookie"` header.
public var cookies: HTTPCookies {
get { return HTTPCookies.parse(setCookieHeaders: headers[.setCookie]) ?? [] }
get { return HTTPCookies.parse(setCookieHeaders: headers[.setCookie]) ?? [:] }
set { newValue.serialize(into: &self) }
}

Expand Down
10 changes: 5 additions & 5 deletions Tests/HTTPTests/HTTPTests.swift
Expand Up @@ -4,14 +4,14 @@ import XCTest
class HTTPTests: XCTestCase {
func testCookieParse() throws {
/// from https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
guard let cookie = HTTPCookie.parse("id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly") else {
guard let (name, value) = HTTPCookieValue.parse("id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly") else {
throw HTTPError(identifier: "cookie", reason: "Could not parse test cookie")
}

XCTAssertEqual(cookie.name, "id")
XCTAssertEqual(cookie.expires, Date(rfc1123: "Wed, 21 Oct 2015 07:28:00 GMT"))
XCTAssertEqual(cookie.isSecure, true)
XCTAssertEqual(cookie.isHTTPOnly, true)
XCTAssertEqual(name, "id")
XCTAssertEqual(value.expires, Date(rfc1123: "Wed, 21 Oct 2015 07:28:00 GMT"))
XCTAssertEqual(value.isSecure, true)
XCTAssertEqual(value.isHTTPOnly, true)
}

func testAcceptHeader() throws {
Expand Down

0 comments on commit d8bf032

Please sign in to comment.