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
|