1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
|
# frozen_string_literal: true
require 'uri'
require 'time'
module Rack
module Test
# Represents individual cookies in the cookie jar. This is considered private
# API and behavior of this class can change at any time.
class Cookie # :nodoc:
include Rack::Utils
# The name of the cookie, will be a string
attr_reader :name
# The value of the cookie, will be a string or nil if there is no value.
attr_reader :value
# The raw string for the cookie, without options. Will generally be in
# name=value format is name and value are provided.
attr_reader :raw
def initialize(raw, uri = nil, default_host = DEFAULT_HOST)
@default_host = default_host
uri ||= default_uri
# separate the name / value pair from the cookie options
@raw, options = raw.split(/[;,] */n, 2)
@name, @value = parse_query(@raw, ';').to_a.first
@options = Hash[parse_query(options, ';').map { |k, v| [k.downcase, v] }]
if domain = @options['domain']
@exact_domain_match = false
domain[0] = '' if domain[0] == '.'
else
# If the domain attribute is not present in the cookie,
# the domain must match exactly.
@exact_domain_match = true
@options['domain'] = (uri.host || default_host)
end
# Set the path for the cookie to the directory containing
# the request if it isn't set.
@options['path'] ||= uri.path.sub(/\/[^\/]*\Z/, '')
end
# Wether the given cookie can replace the current cookie in the cookie jar.
def replaces?(other)
[name.downcase, domain, path] == [other.name.downcase, other.domain, other.path]
end
# Whether the cookie has a value.
def empty?
@value.nil? || @value.empty?
end
# The explicit or implicit domain for the cookie.
def domain
@options['domain']
end
# Whether the cookie has the secure flag, indicating it can only be sent over
# an encrypted connection.
def secure?
@options.key?('secure')
end
# Whether the cookie has the httponly flag, indicating it is not available via
# a javascript API.
def http_only?
@options.key?('httponly')
end
# The explicit or implicit path for the cookie.
def path
([*@options['path']].first.split(',').first || '/').strip
end
# A Time value for when the cookie expires, if the expires option is set.
def expires
Time.parse(@options['expires']) if @options['expires']
end
# Whether the cookie is currently expired.
def expired?
expires && expires < Time.now
end
# Whether the cookie is valid for the given URI.
def valid?(uri)
uri ||= default_uri
uri.host = @default_host if uri.host.nil?
!!((!secure? || (secure? && uri.scheme == 'https')) &&
uri.host =~ Regexp.new("#{'^' if @exact_domain_match}#{Regexp.escape(domain)}$", Regexp::IGNORECASE))
end
# Cookies that do not match the URI will not be sent in requests to the URI.
def matches?(uri)
!expired? && valid?(uri) && uri.path.start_with?(path)
end
# Order cookies by name, path, and domain.
def <=>(other)
[name, path, domain.reverse] <=> [other.name, other.path, other.domain.reverse]
end
# A hash of cookie options, including the cookie value, but excluding the cookie name.
def to_h
hash = @options.merge(
'value' => @value,
'HttpOnly' => http_only?,
'secure' => secure?
)
hash.delete('httponly')
hash
end
alias to_hash to_h
private
# The default URI to use for the cookie, including just the host.
def default_uri
URI.parse('//' + @default_host + '/')
end
end
# Represents all cookies for a session, handling adding and
# removing cookies, and finding which cookies apply to a given
# request. This is considered private API and behavior of this
# class can change at any time.
class CookieJar # :nodoc:
DELIMITER = '; '.freeze
def initialize(cookies = [], default_host = DEFAULT_HOST)
@default_host = default_host
@cookies = cookies.sort!
end
# Ensure the copy uses a distinct cookies array.
def initialize_copy(other)
super
@cookies = @cookies.dup
end
# Return the value for first cookie with the given name, or nil
# if no such cookie exists.
def [](name)
name = name.to_s
@cookies.each do |cookie|
return cookie.value if cookie.name == name
end
nil
end
# Set a cookie with the given name and value in the
# cookie jar.
def []=(name, value)
merge("#{name}=#{Rack::Utils.escape(value)}")
end
# Return the first cookie with the given name, or nil if
# no such cookie exists.
def get_cookie(name)
@cookies.each do |cookie|
return cookie if cookie.name == name
end
nil
end
# Delete all cookies with the given name from the cookie jar.
def delete(name)
@cookies.reject! do |cookie|
cookie.name == name
end
nil
end
# Add a string of raw cookie information to the cookie jar,
# if the cookie is valid for the given URI.
# Cookies should be separated with a newline.
def merge(raw_cookies, uri = nil)
return unless raw_cookies
raw_cookies = raw_cookies.split("\n") if raw_cookies.is_a? String
raw_cookies.each do |raw_cookie|
next if raw_cookie.empty?
cookie = Cookie.new(raw_cookie, uri, @default_host)
self << cookie if cookie.valid?(uri)
end
end
# Add a Cookie to the cookie jar.
def <<(new_cookie)
@cookies.reject! do |existing_cookie|
new_cookie.replaces?(existing_cookie)
end
@cookies << new_cookie
@cookies.sort!
end
# Return a raw cookie string for the cookie header to
# use for the given URI.
def for(uri)
buf = String.new
delimiter = nil
each_cookie_for(uri) do |cookie|
if delimiter
buf << delimiter
else
delimiter = DELIMITER
end
buf << cookie.raw
end
buf
end
# Return a hash cookie names and cookie values for cookies in the jar.
def to_hash
cookies = {}
@cookies.each do |cookie|
cookies[cookie.name] = cookie.value
end
cookies
end
private
# Yield each cookie that matches for the URI.
#
# The cookies are sorted by most specific first. So, we loop through
# all the cookies in order and add it to a hash by cookie name if
# the cookie can be sent to the current URI. It's added to the hash
# so that when we are done, the cookies will be unique by name and
# we'll have grabbed the most specific to the URI.
def each_cookie_for(uri)
@cookies.each do |cookie|
yield cookie if !uri || cookie.matches?(uri)
end
end
end
end
end
|