diff --git a/lib/protocol/http/header/cookie.rb b/lib/protocol/http/header/cookie.rb index f86c97f..427f701 100644 --- a/lib/protocol/http/header/cookie.rb +++ b/lib/protocol/http/header/cookie.rb @@ -12,7 +12,40 @@ module Header # The `cookie` header contains stored HTTP cookies previously sent by the server with the `set-cookie` header. # # It is used by clients to send key-value pairs representing stored cookies back to the server. - class Cookie < Multiple + # Multiple cookies within a single `Cookie` header are joined with `"; "` per RFC 6265. + class Cookie < Array + # Parses a raw header value. + # + # @parameter value [String] a single raw header value. + # @returns [Cookie] a new instance containing the parsed value. + def self.parse(value) + self.new([value]) + end + + # Coerces a value into a parsed header object. + # + # @parameter value [String | Array] the value to coerce. + # @returns [Cookie] a parsed header object. + def self.coerce(value) + case value + when Array + self.new(value.map(&:to_s)) + else + self.parse(value.to_s) + end + end + + # Initializes the cookie header with the given values. + # + # @parameter value [Array | Nil] an array of cookie strings, or `nil` for an empty header. + def initialize(value = nil) + super() + + if value + self.concat(value) + end + end + # Parses the `cookie` header into a hash of cookie names and their corresponding cookie objects. # # @returns [Hash(String, HTTP::Cookie)] a hash where keys are cookie names and values are {HTTP::Cookie} objects. @@ -20,15 +53,15 @@ def to_h cookies = self.collect do |string| HTTP::Cookie.parse(string) end - + cookies.map{|cookie| [cookie.name, cookie]}.to_h end - - # Serializes the `cookie` header by joining individual cookie strings with semicolons. + + # Serializes the `cookie` header by joining individual cookie strings with `"; "` per RFC 6265. def to_s - join(";") + join("; ") end - + # Whether this header is acceptable in HTTP trailers. # Cookie headers should not appear in trailers as they contain state information needed early in processing. # @returns [Boolean] `false`, as cookie headers are needed during initial request processing. @@ -36,11 +69,29 @@ def self.trailer? false end end - + # The `set-cookie` header sends cookies from the server to the user agent. # - # It is used to store cookies on the client side, which are then sent back to the server in subsequent requests using the `cookie` header. - class SetCookie < Cookie + # Each `Set-Cookie` header must be a separate header field — they cannot be combined. + # It is used to store cookies on the client side, which are then sent back to the server + # in subsequent requests using the `cookie` header. + class SetCookie < Multiple + # Parses the `set-cookie` headers into a hash of cookie names and their corresponding cookie objects. + # + # @returns [Hash(String, HTTP::Cookie)] a hash where keys are cookie names and values are {HTTP::Cookie} objects. + def to_h + cookies = self.collect do |string| + HTTP::Cookie.parse(string) + end + + cookies.map{|cookie| [cookie.name, cookie]}.to_h + end + + # Whether this header is acceptable in HTTP trailers. + # @returns [Boolean] `false`, as set-cookie headers are needed during initial response processing. + def self.trailer? + false + end end end end diff --git a/lib/protocol/http/headers.rb b/lib/protocol/http/headers.rb index 76dcb5a..bc00ac9 100644 --- a/lib/protocol/http/headers.rb +++ b/lib/protocol/http/headers.rb @@ -299,8 +299,14 @@ def []=(key, value) if @indexed @indexed[key] = value end - - @fields << [key, value.to_s] + + if value.is_a?(Multiple) + value.each do |v| + @fields << [key, v.to_s] + end + else + @fields << [key, value.to_s] + end end # Get the value of the specified header key. diff --git a/test/protocol/http/header/cookie.rb b/test/protocol/http/header/cookie.rb index 7b108dc..a866ffa 100644 --- a/test/protocol/http/header/cookie.rb +++ b/test/protocol/http/header/cookie.rb @@ -56,8 +56,8 @@ cookie end - it "joins cookies with semicolons without spaces" do - expect(header.to_s).to be == "session=abc123;user_id=42;token=xyz789" + it "joins cookies with semicolons and spaces per RFC 6265" do + expect(header.to_s).to be == "session=abc123; user_id=42; token=xyz789" end end end diff --git a/test/protocol/http/headers.rb b/test/protocol/http/headers.rb index e5bb9b2..69de97b 100644 --- a/test/protocol/http/headers.rb +++ b/test/protocol/http/headers.rb @@ -600,7 +600,7 @@ def self.trailer? with "set-cookie" do it "can extract parsed cookies" do - expect(headers["set-cookie"]).to be_a(Protocol::HTTP::Header::Cookie) + expect(headers["set-cookie"]).to be_a(Protocol::HTTP::Header::SetCookie) end end