File: cookie.rb

package info (click to toggle)
ruby-httpx 1.7.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,816 kB
  • sloc: ruby: 12,209; makefile: 4
file content (175 lines) | stat: -rw-r--r-- 4,957 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
# frozen_string_literal: true

module HTTPX
  module Plugins::Cookies
    # The HTTP Cookie.
    #
    # Contains the single cookie info: name, value and attributes.
    class Cookie
      include Comparable

      # Maximum number of bytes per cookie (RFC 6265 6.1 requires 4096 at
      # least)
      MAX_LENGTH = 4096

      attr_reader :domain, :path, :name, :value, :created_at

      def path=(path)
        path = String(path)
        @path = path.start_with?("/") ? path : "/"
      end

      # See #domain.
      def domain=(domain)
        domain = String(domain)

        if domain.start_with?(".")
          @for_domain = true
          domain = domain[1..-1]
        end

        return if domain.empty?

        @domain_name = DomainName.new(domain)
        # RFC 6265 5.3 5.
        @for_domain = false if @domain_name.domain.nil? # a public suffix or IP address

        @domain = @domain_name.hostname
      end

      # Compares the cookie with another.  When there are many cookies with
      # the same name for a URL, the value of the smallest must be used.
      def <=>(other)
        # RFC 6265 5.4
        # Precedence: 1. longer path  2. older creation
        (@name <=> other.name).nonzero? ||
          (other.path.length <=> @path.length).nonzero? ||
          (@created_at <=> other.created_at).nonzero? || 0
      end

      class << self
        def new(cookie, *args)
          return cookie if cookie.is_a?(self)

          super
        end

        # Tests if +target_path+ is under +base_path+ as described in RFC
        # 6265 5.1.4.  +base_path+ must be an absolute path.
        # +target_path+ may be empty, in which case it is treated as the
        # root path.
        #
        # e.g.
        #
        #         path_match?('/admin/', '/admin/index') == true
        #         path_match?('/admin/', '/Admin/index') == false
        #         path_match?('/admin/', '/admin/') == true
        #         path_match?('/admin/', '/admin') == false
        #
        #         path_match?('/admin', '/admin') == true
        #         path_match?('/admin', '/Admin') == false
        #         path_match?('/admin', '/admins') == false
        #         path_match?('/admin', '/admin/') == true
        #         path_match?('/admin', '/admin/index') == true
        def path_match?(base_path, target_path)
          base_path.start_with?("/") || (return false)
          # RFC 6265 5.1.4
          bsize = base_path.size
          tsize = target_path.size
          return bsize == 1 if tsize.zero? # treat empty target_path as "/"
          return false unless target_path.start_with?(base_path)
          return true if bsize == tsize || base_path.end_with?("/")

          target_path[bsize] == "/"
        end
      end

      def initialize(arg, *attrs)
        @created_at = Time.now

        if attrs.empty?
          attr_hash = Hash.try_convert(arg)
        else
          @name = arg
          @value, attr_hash = attrs
          attr_hash = Hash.try_convert(attr_hash)
        end

        attr_hash.each do |key, val|
          key = key.downcase.tr("-", "_").to_sym unless key.is_a?(Symbol)

          case key
          when :domain, :path
            __send__(:"#{key}=", val)
          else
            instance_variable_set(:"@#{key}", val)
          end
        end if attr_hash

        @path ||= "/"
        raise ArgumentError, "name must be specified" if @name.nil?

        @name = @name.to_s
      end

      def expires
        @expires || (@created_at && @max_age ? @created_at + @max_age : nil)
      end

      def expired?(time = Time.now)
        return false unless expires

        expires <= time
      end

      # Returns a string for use in the Cookie header, i.e. `name=value`
      # or `name="value"`.
      def cookie_value
        "#{@name}=#{Scanner.quote(@value.to_s)}"
      end
      alias_method :to_s, :cookie_value

      # Tests if it is OK to send this cookie to a given `uri`.  A
      # RuntimeError is raised if the cookie's domain is unknown.
      def valid_for_uri?(uri)
        uri = URI(uri)
        # RFC 6265 5.4

        return false if @secure && uri.scheme != "https"

        acceptable_from_uri?(uri) && Cookie.path_match?(@path, uri.path)
      end

      private

      # Tests if it is OK to accept this cookie if it is sent from a given
      # URI/URL, `uri`.
      def acceptable_from_uri?(uri)
        uri = URI(uri)

        host = DomainName.new(uri.host)

        # RFC 6265 5.3
        if host.hostname == @domain
          true
        elsif @for_domain # !host-only-flag
          host.cookie_domain?(@domain_name)
        else
          @domain.nil?
        end
      end

      module Scanner
        RE_BAD_CHAR = /([\x00-\x20\x7F",;\\])/.freeze

        module_function

        def quote(s)
          return s unless s.match(RE_BAD_CHAR)

          "\"#{s.gsub(/([\\"])/, "\\\\\\1")}\""
        end
      end
    end
  end
end