Class: CookieJar::Jar

Inherits:
Object
  • Object
show all
Defined in:
lib/cookiejar/jar.rb

Overview

A cookie store for client side usage.

  • Enforces cookie validity rules

  • Returns just the cookies valid for a given URI

  • Handles expiration of cookies

  • Allows for persistence of cookie data (with or without session)

Internal format:

Internally, the data structure is a set of nested hashes. Domain Level: At the domain level, the hashes are of individual domains, down-cased and without any leading period. For instance, imagine cookies for .foo.com, .bar.com, and .auth.bar.com:

{
  "foo.com"      : (host data),
  "bar.com"      : (host data),
  "auth.bar.com" : (host data)
}

Lookups are done both for the matching entry, and for an entry without the first segment up to the dot, ie. for /^.?[^.]+.(.*)$/. A lookup of auth.bar.com would match both bar.com and auth.bar.com, but not entries for com or www.auth.bar.com.

Host Level: Entries are in an hash, with keys of the path and values of a hash of cookie names to cookie object

{
  "/" : {"session" : (Cookie object), "cart_id" : (Cookie object)}
  "/protected" : {"authentication" : (Cookie Object)}
}

Paths are given a straight prefix string comparison to match. Further filters <secure, http only, ports> are not represented in this hierarchy.

Cookies returned are ordered solely by specificity (length) of the path.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeJar

Create a new empty Jar


48
49
50
# File 'lib/cookiejar/jar.rb', line 48

def initialize
  @domains = {}
end

Class Method Details

.from_a(cookies) ⇒ CookieJar

Create a new Jar from an array of Cookie objects. Expired cookies will still be added to the archive, and conflicting cookies will be overwritten by the last cookie in the array.

Parameters:

  • cookies (Array<Cookie>)

    array of cookie objects

Returns:


178
179
180
181
182
183
184
# File 'lib/cookiejar/jar.rb', line 178

def self.from_a(cookies)
  jar = new
  cookies.each do |cookie|
    jar.add_cookie cookie
  end
  jar
end

.json_create(o) ⇒ CookieJar

Create a new Jar from a JSON-backed hash

Parameters:

  • o (Hash)

    the expanded JSON object

Returns:


163
164
165
166
167
168
169
170
# File 'lib/cookiejar/jar.rb', line 163

def self.json_create(o)
  o = JSON.parse(o) if o.is_a? String
  o = o['cookies'] if o.is_a? Hash
  cookies = o.inject([]) do |result, cookie_json|
    result << (Cookie.json_create cookie_json)
  end
  from_a cookies
end

Instance Method Details

Add a pre-existing cookie object to the jar.

Parameters:

  • cookie (Cookie)

    a pre-existing cookie object

Returns:

  • (Cookie)

    the cookie added to the store


126
127
128
129
130
# File 'lib/cookiejar/jar.rb', line 126

def add_cookie(cookie)
  domain_paths = find_or_add_domain_for_cookie cookie
  add_cookie_to_path domain_paths, cookie
  cookie
end

#expire_cookies(session = false) ⇒ Object

Look through the jar for any cookies which have passed their expiration date, or session cookies from a previous session

Parameters:

  • session (Boolean) (defaults to: false)

    whether session cookies should be expired, or just cookies past their expiration date.


191
192
193
194
195
196
197
198
199
200
201
# File 'lib/cookiejar/jar.rb', line 191

def expire_cookies(session = false)
  @domains.delete_if do |_domain, paths|
    paths.delete_if do |_path, cookies|
      cookies.delete_if do |_cookie_name, cookie|
        cookie.expired? || (session && cookie.session?)
      end
      cookies.empty?
    end
    paths.empty?
  end
end

Given a request URI, return a string Cookie header.Cookies will be in order per RFC 2965 - sorted by longest path length, but otherwise unordered.

Parameters:

  • request_uri (String, URI)

    the address the HTTP request will be sent to

  • opts (Hash) (defaults to: {})

    options controlling returned cookies

Options Hash (opts):

  • :script (Boolean) — default: false

    Cookies marked HTTP-only will be ignored if true

Returns:

  • String value of the Cookie header which should be sent on the HTTP request


254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/cookiejar/jar.rb', line 254

def get_cookie_header(request_uri, opts = {})
  cookies = get_cookies request_uri, opts
  ver = [[], []]
  cookies.each do |cookie|
    ver[cookie.version] << cookie
  end
  if ver[1].empty?
    # can do a netscape-style cookie header, relish the opportunity
    cookies.map(&:to_s).join ';'
  else
    # build a RFC 2965-style cookie header. Split the cookies into
    # version 0 and 1 groups so that we can reuse the '$Version' header
    result = ''
    unless ver[0].empty?
      result << '$Version=0;'
      result << ver[0].map do |cookie|
        (cookie.to_s 1, false)
      end.join(';')
      # separate version 0 and 1 with a comma
      result << ','
    end
    result << '$Version=1;'
    ver[1].map do |cookie|
      result << (cookie.to_s 1, false)
    end
    result
  end
end

#get_cookies(request_uri, opts = {}) ⇒ Array<Cookie>

Given a request URI, return a sorted list of Cookie objects. Cookies will be in order per RFC 2965 - sorted by longest path length, but otherwise unordered.

Parameters:

  • request_uri (String, URI)

    the address the HTTP request will be sent to. This must be a full URI, i.e. must include the protocol, if you pass digi.ninja it will fail to find the domain, you must pass digi.ninja

  • opts (Hash) (defaults to: {})

    options controlling returned cookies

Options Hash (opts):

  • :script (Boolean) — default: false

    Cookies marked HTTP-only will be ignored if true

Returns:

  • (Array<Cookie>)

    cookies which should be sent in the HTTP request


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
# File 'lib/cookiejar/jar.rb', line 215

def get_cookies(request_uri, opts = {})
  uri = to_uri request_uri
  hosts = Cookie.compute_search_domains uri

  return [] if hosts.nil?

  path = if uri.path == ''
           '/'
         else
           uri.path
         end

  results = []
  hosts.each do |host|
    domain = find_domain host
    domain.each do |apath, cookies|
      next unless path.start_with? apath
      results += cookies.values.select do |cookie|
        cookie.should_send? uri, opts[:script]
      end
    end
  end
  # Sort by path length, longest first
  results.sort do |lhs, rhs|
    rhs.path.length <=> lhs.path.length
  end
end

Given a request URI and a literal Set-Cookie header value, attempt to add the cookie(s) to the cookie store.

Parameters:

  • request_uri (String, URI)

    the resource returning the header

  • cookie_header_value (String)

    the contents of the Set-Cookie

Returns:

  • (Cookie)

    which was created and stored

Raises:


59
60
61
62
63
64
# File 'lib/cookiejar/jar.rb', line 59

def set_cookie(request_uri, cookie_header_values)
  cookie_header_values.split(/, (?=[\w]+=)/).each do |cookie_header_value|
    cookie = Cookie.from_set_cookie request_uri, cookie_header_value
    add_cookie cookie
  end
end

#set_cookie2(request_uri, cookie_header_value) ⇒ Cookie

Given a request URI and a literal Set-Cookie2 header value, attempt to add the cookie to the cookie store.

Parameters:

  • request_uri (String, URI)

    the resource returning the header

  • cookie_header_value (String)

    the contents of the Set-Cookie2

Returns:

  • (Cookie)

    which was created and stored

Raises:


73
74
75
76
# File 'lib/cookiejar/jar.rb', line 73

def set_cookie2(request_uri, cookie_header_value)
  cookie = Cookie.from_set_cookie2 request_uri, cookie_header_value
  add_cookie cookie
end

#set_cookies_from_headers(request_uri, http_headers) ⇒ Array<Cookie>?

Given a request URI and some HTTP headers, attempt to add the cookie(s) (from Set-Cookie or Set-Cookie2 headers) to the cookie store. If a cookie is defined (by equivalent name, domain, and path) via Set-Cookie and Set-Cookie2, the Set-Cookie version is ignored.

Parameters:

  • request_uri (String, URI)

    the resource returning the header

  • http_headers (Hash<String,[String,Array<String>]>)

    a Hash which may have a key of “Set-Cookie” or “Set-Cookie2”, and values of either strings or arrays of strings

Returns:

  • (Array<Cookie>, nil)

    the cookies created, or nil if none found.

Raises:

  • (InvalidCookieError)

    if one of the cookie headers contained invalid formatting or data


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
# File 'lib/cookiejar/jar.rb', line 90

def set_cookies_from_headers(request_uri, http_headers)
  set_cookie_key = http_headers.keys.detect { |k| /\ASet-Cookie\Z/i.match k }
  cookies = gather_header_values http_headers[set_cookie_key] do |value|
    begin
      Cookie.from_set_cookie request_uri, value
    rescue InvalidCookieError
    end
  end

  set_cookie2_key = http_headers.keys.detect { |k| /\ASet-Cookie2\Z/i.match k }
  cookies += gather_header_values(http_headers[set_cookie2_key]) do |value|
    begin
      Cookie.from_set_cookie2 request_uri, value
    rescue InvalidCookieError
    end
  end

  # build the list of cookies, using a Jar. Since Set-Cookie2 values
  # come second, they will replace the Set-Cookie versions.
  jar = Jar.new
  cookies.each do |cookie|
    jar.add_cookie cookie
  end
  cookies = jar.to_a

  # now add them all to our own store.
  cookies.each do |cookie|
    add_cookie cookie
  end
  cookies
end

#to_aArray<Cookie>

Return an array of all cookie objects in the jar

which have not yet been removed with expire_cookies

Returns:

  • (Array<Cookie>)

    all cookies. Includes any expired cookies


136
137
138
139
140
141
142
143
144
# File 'lib/cookiejar/jar.rb', line 136

def to_a
  result = []
  @domains.values.each do |paths|
    paths.values.each do |cookies|
      cookies.values.inject result, :<<
    end
  end
  result
end

#to_json(*a) ⇒ String

Return a JSON 'object' for the various data values. Allows for persistence of the cookie information

Parameters:

  • a (Array)

    options controlling output JSON text (usually a State and a depth)

Returns:

  • (String)

    JSON representation of object data


152
153
154
155
156
157
# File 'lib/cookiejar/jar.rb', line 152

def to_json(*a)
  {
    'json_class' => self.class.name,
    'cookies' => to_a.to_json(*a)
  }.to_json(*a)
end