File: cookie_jar.rb

package info (click to toggle)
ruby-rack-test 2.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 308 kB
  • sloc: ruby: 2,269; makefile: 6
file content (251 lines) | stat: -rw-r--r-- 7,373 bytes parent folder | download
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