44# Copyright, 2019-2025, by Samuel Williams.
55# Copyright, 2022, by Herrick Fang.
66
7- require_relative "url "
7+ require_relative "quoted_string "
88
99module Protocol
1010 module HTTP
1111 # Represents an individual cookie key-value pair.
1212 class Cookie
13+ # Valid cookie name characters according to RFC 6265.
14+ # cookie-name = token (RFC 2616 defines token)
15+ VALID_COOKIE_KEY = /\A #{ TOKEN } \z / . freeze
16+
17+ # Valid cookie value characters according to RFC 6265.
18+ # cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
19+ # cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
20+ # Excludes control chars, whitespace, DQUOTE, comma, semicolon, and backslash
21+ VALID_COOKIE_VALUE = /\A [\x21 \x23 -\x2B \x2D -\x3A \x3C -\x5B \x5D -\x7E ]*\z / . freeze
22+
1323 # Initialize the cookie with the given name, value, and directives.
1424 #
15- # @parameter name [String] The name of the cookiel , e.g. "session_id".
25+ # @parameter name [String] The name of the cookie , e.g. "session_id".
1626 # @parameter value [String] The value of the cookie, e.g. "1234".
1727 # @parameter directives [Hash] The directives of the cookie, e.g. `{"path" => "/"}`.
18- def initialize ( name , value , directives )
28+ # @raises [ArgumentError] If the name or value contains invalid characters.
29+ def initialize ( name , value , directives = nil )
30+ unless VALID_COOKIE_KEY . match? ( name )
31+ raise ArgumentError , "Invalid cookie name: #{ name . inspect } "
32+ end
33+
34+ if value && !VALID_COOKIE_VALUE . match? ( value )
35+ raise ArgumentError , "Invalid cookie value: #{ value . inspect } "
36+ end
37+
1938 @name = name
2039 @value = value
2140 @directives = directives
@@ -30,41 +49,37 @@ def initialize(name, value, directives)
3049 # @attribute [Hash] The directives of the cookie.
3150 attr :directives
3251
33- # Encode the name of the cookie.
34- def encoded_name
35- URL . escape ( @name )
36- end
37-
38- # Encode the value of the cookie.
39- def encoded_value
40- URL . escape ( @value )
52+ # Encode a string for use in a cookie, escaping characters that are not allowed .
53+ #
54+ # @parameter string [String] The string to encode.
55+ # @returns [String] The encoded string.
56+ def self . encode ( string )
57+ string . b . gsub ( /([^!#$%&'*+ \- \. \/ 0-9A-Z \^ _`a-z|~]+)/ ) do | match |
58+ "%" + match . unpack ( "H2" * match . bytesize ) . join ( "%" ) . upcase
59+ end
4160 end
4261
4362 # Convert the cookie to a string.
4463 #
4564 # @returns [String] The string representation of the cookie.
46- def to_s
47- buffer = String . new . b
48-
49- buffer << encoded_name << "=" << encoded_value
50-
51- if @directives
52- @directives . collect do |key , value |
53- buffer << ";"
54-
55- case value
56- when String
57- buffer << key << "=" << value
58- when TrueClass
59- buffer << key
60- end
65+ def to_s
66+ buffer = String . new
67+
68+ buffer << @name << "=" << @value
69+
70+ if @directives
71+ @directives . each do |key , value |
72+ buffer << ";"
73+ buffer << key
74+
75+ if value != true
76+ buffer << "=" << value . to_s
6177 end
6278 end
63-
64- return buffer
6579 end
6680
67- # Parse a string into a cookie.
81+ return buffer
82+ end # Parse a string into a cookie.
6883 #
6984 # @parameter string [String] The string to parse.
7085 # @returns [Cookie] The parsed cookie.
@@ -74,11 +89,7 @@ def self.parse(string)
7489 key , value = head . split ( "=" , 2 )
7590 directives = self . parse_directives ( directives )
7691
77- self . new (
78- URL . unescape ( key ) ,
79- URL . unescape ( value ) ,
80- directives ,
81- )
92+ self . new ( key , value , directives )
8293 end
8394
8495 # Parse a list of strings into a hash of directives.
0 commit comments