httpx 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +48 -0
  3. data/README.md +2 -0
  4. data/doc/release_notes/0_10_0.md +66 -0
  5. data/lib/httpx.rb +2 -0
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/chainable.rb +2 -2
  8. data/lib/httpx/connection.rb +3 -9
  9. data/lib/httpx/connection/http1.rb +1 -1
  10. data/lib/httpx/domain_name.rb +440 -0
  11. data/lib/httpx/errors.rb +1 -0
  12. data/lib/httpx/extensions.rb +21 -1
  13. data/lib/httpx/io/ssl.rb +0 -1
  14. data/lib/httpx/io/tcp.rb +6 -5
  15. data/lib/httpx/io/udp.rb +4 -1
  16. data/lib/httpx/options.rb +2 -0
  17. data/lib/httpx/parser/http1.rb +14 -17
  18. data/lib/httpx/plugins/compression.rb +28 -63
  19. data/lib/httpx/plugins/compression/brotli.rb +10 -14
  20. data/lib/httpx/plugins/compression/deflate.rb +7 -6
  21. data/lib/httpx/plugins/compression/gzip.rb +23 -5
  22. data/lib/httpx/plugins/cookies.rb +21 -60
  23. data/lib/httpx/plugins/cookies/cookie.rb +173 -0
  24. data/lib/httpx/plugins/cookies/jar.rb +74 -0
  25. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
  26. data/lib/httpx/plugins/expect.rb +3 -5
  27. data/lib/httpx/plugins/follow_redirects.rb +20 -2
  28. data/lib/httpx/plugins/h2c.rb +1 -1
  29. data/lib/httpx/plugins/multipart.rb +0 -8
  30. data/lib/httpx/plugins/persistent.rb +6 -1
  31. data/lib/httpx/plugins/proxy/socks4.rb +3 -1
  32. data/lib/httpx/plugins/rate_limiter.rb +51 -0
  33. data/lib/httpx/plugins/retries.rb +3 -2
  34. data/lib/httpx/plugins/stream.rb +109 -13
  35. data/lib/httpx/pool.rb +6 -6
  36. data/lib/httpx/request.rb +7 -19
  37. data/lib/httpx/resolver/https.rb +7 -2
  38. data/lib/httpx/resolver/native.rb +7 -3
  39. data/lib/httpx/response.rb +16 -23
  40. data/lib/httpx/selector.rb +2 -4
  41. data/lib/httpx/session.rb +17 -11
  42. data/lib/httpx/transcoder/chunker.rb +0 -2
  43. data/lib/httpx/transcoder/form.rb +0 -6
  44. data/lib/httpx/transcoder/json.rb +0 -4
  45. data/lib/httpx/utils.rb +45 -0
  46. data/lib/httpx/version.rb +1 -1
  47. data/sig/buffer.rbs +24 -0
  48. data/sig/callbacks.rbs +14 -0
  49. data/sig/chainable.rbs +37 -0
  50. data/sig/connection.rbs +2 -0
  51. data/sig/connection/http2.rbs +4 -0
  52. data/sig/domain_name.rbs +17 -0
  53. data/sig/errors.rbs +3 -0
  54. data/sig/headers.rbs +42 -0
  55. data/sig/httpx.rbs +14 -0
  56. data/sig/loggable.rbs +11 -0
  57. data/sig/missing.rbs +12 -0
  58. data/sig/options.rbs +118 -0
  59. data/sig/parser/http1.rbs +50 -0
  60. data/sig/plugins/authentication.rbs +11 -0
  61. data/sig/plugins/basic_authentication.rbs +13 -0
  62. data/sig/plugins/compression.rbs +55 -0
  63. data/sig/plugins/compression/brotli.rbs +21 -0
  64. data/sig/plugins/compression/deflate.rbs +17 -0
  65. data/sig/plugins/compression/gzip.rbs +29 -0
  66. data/sig/plugins/cookies.rbs +26 -0
  67. data/sig/plugins/cookies/cookie.rbs +50 -0
  68. data/sig/plugins/cookies/jar.rbs +27 -0
  69. data/sig/plugins/digest_authentication.rbs +33 -0
  70. data/sig/plugins/expect.rbs +19 -0
  71. data/sig/plugins/follow_redirects.rbs +37 -0
  72. data/sig/plugins/h2c.rbs +26 -0
  73. data/sig/plugins/multipart.rbs +19 -0
  74. data/sig/plugins/persistent.rbs +17 -0
  75. data/sig/plugins/proxy.rbs +47 -0
  76. data/sig/plugins/proxy/http.rbs +14 -0
  77. data/sig/plugins/proxy/socks4.rbs +33 -0
  78. data/sig/plugins/proxy/socks5.rbs +36 -0
  79. data/sig/plugins/proxy/ssh.rbs +18 -0
  80. data/sig/plugins/push_promise.rbs +22 -0
  81. data/sig/plugins/rate_limiter.rbs +11 -0
  82. data/sig/plugins/retries.rbs +48 -0
  83. data/sig/plugins/stream.rbs +39 -0
  84. data/sig/pool.rbs +2 -0
  85. data/sig/registry.rbs +9 -0
  86. data/sig/request.rbs +61 -0
  87. data/sig/response.rbs +87 -0
  88. data/sig/session.rbs +49 -0
  89. data/sig/test.rbs +9 -0
  90. data/sig/timeout.rbs +29 -0
  91. data/sig/transcoder.rbs +16 -0
  92. data/sig/transcoder/body.rbs +18 -0
  93. data/sig/transcoder/chunker.rbs +32 -0
  94. data/sig/transcoder/form.rbs +16 -0
  95. data/sig/transcoder/json.rbs +14 -0
  96. metadata +60 -17
@@ -12,93 +12,55 @@ module HTTPX
12
12
  # https://gitlab.com/honeyryderchuck/httpx/wikis/Cookies
13
13
  #
14
14
  module Cookies
15
- using URIExtensions
15
+ def self.load_dependencies(*)
16
+ require "httpx/plugins/cookies/jar"
17
+ require "httpx/plugins/cookies/cookie"
18
+ require "httpx/plugins/cookies/set_cookie_parser"
19
+ end
16
20
 
17
21
  def self.extra_options(options)
18
22
  Class.new(options.class) do
19
23
  def_option(:cookies) do |cookies|
20
- if cookies.is_a?(Store)
24
+ if cookies.is_a?(Jar)
21
25
  cookies
22
26
  else
23
- Store.new(cookies)
27
+ Jar.new(cookies)
24
28
  end
25
29
  end
26
30
  end.new(options)
27
31
  end
28
32
 
29
- class Store
30
- def self.new(cookies = nil)
31
- return cookies if cookies.is_a?(self)
32
-
33
- super
34
- end
35
-
36
- def initialize(cookies = nil)
37
- @store = Hash.new { |hash, origin| hash[origin] = HTTP::CookieJar.new }
38
- return unless cookies
39
-
40
- cookies = cookies.split(/ *; */) if cookies.is_a?(String)
41
- @default_cookies = cookies.map do |cookie, v|
42
- if cookie.is_a?(HTTP::Cookie)
43
- cookie
44
- else
45
- HTTP::Cookie.new(cookie.to_s, v.to_s)
46
- end
47
- end
48
- end
49
-
50
- def set(origin, cookies)
51
- return unless cookies
52
-
53
- @store[origin].parse(cookies, origin)
54
- end
55
-
56
- def [](uri)
57
- store = @store[uri.origin]
58
- @default_cookies.each do |cookie|
59
- c = cookie.dup
60
- c.domain ||= uri.authority
61
- c.path ||= uri.path
62
- store.add(c)
63
- end if @default_cookies
64
- store
65
- end
66
-
67
- def ==(other)
68
- @store == other.instance_variable_get(:@store)
69
- end
70
- end
71
-
72
- def self.load_dependencies(*)
73
- require "http/cookie"
74
- end
75
-
76
33
  module InstanceMethods
77
34
  extend Forwardable
78
35
 
79
36
  def_delegator :@options, :cookies
80
37
 
81
38
  def initialize(options = {}, &blk)
82
- super({ cookies: Store.new }.merge(options), &blk)
39
+ super({ cookies: Jar.new }.merge(options), &blk)
83
40
  end
84
41
 
85
42
  def wrap
86
43
  return super unless block_given?
87
44
 
88
45
  super do |session|
89
- old_cookies_store = @options.cookies.dup
46
+ old_cookies_jar = @options.cookies.dup
90
47
  begin
91
48
  yield session
92
49
  ensure
93
- @options = @options.with(cookies: old_cookies_store)
50
+ @options = @options.merge(cookies: old_cookies_jar)
94
51
  end
95
52
  end
96
53
  end
97
54
 
98
55
  private
99
56
 
100
- def on_response(request, response)
101
- @options.cookies.set(request.origin, response.headers["set-cookie"]) if response.respond_to?(:headers)
57
+ def on_response(reuest, response)
58
+ if response && response.respond_to?(:headers) && (set_cookie = response.headers["set-cookie"])
59
+
60
+ log { "cookies: set-cookie is over #{Cookie::MAX_LENGTH}" } if set_cookie.bytesize > Cookie::MAX_LENGTH
61
+
62
+ @options.cookies.parse(set_cookie)
63
+ end
102
64
 
103
65
  super
104
66
  end
@@ -111,13 +73,12 @@ module HTTPX
111
73
  end
112
74
 
113
75
  module HeadersMethods
114
- def set_cookie(jar)
115
- return unless jar
76
+ def set_cookie(cookies)
77
+ return if cookies.empty?
116
78
 
117
- cookie_value = HTTP::Cookie.cookie_value(jar.cookies)
118
- return if cookie_value.empty?
79
+ header_value = cookies.sort.join("; ")
119
80
 
120
- add("cookie", cookie_value)
81
+ add("cookie", header_value)
121
82
  end
122
83
  end
123
84
  end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins::Cookies
5
+ # The HTTP Cookie.
6
+ #
7
+ # Contains the single cookie info: name, value and attributes.
8
+ class Cookie
9
+ include Comparable
10
+ # Maximum number of bytes per cookie (RFC 6265 6.1 requires 4096 at
11
+ # least)
12
+ MAX_LENGTH = 4096
13
+
14
+ attr_reader :domain, :path, :name, :value, :created_at
15
+
16
+ def path=(path)
17
+ path = String(path)
18
+ @path = path.start_with?("/") ? path : "/"
19
+ end
20
+
21
+ # See #domain.
22
+ def domain=(domain)
23
+ domain = String(domain)
24
+
25
+ if domain.start_with?(".")
26
+ @for_domain = true
27
+ domain = domain[1..-1]
28
+ end
29
+
30
+ return if domain.empty?
31
+
32
+ @domain_name = DomainName.new(domain)
33
+ # RFC 6265 5.3 5.
34
+ @for_domain = false if @domain_name.domain.nil? # a public suffix or IP address
35
+
36
+ @domain = @domain_name.hostname
37
+ end
38
+
39
+ # Compares the cookie with another. When there are many cookies with
40
+ # the same name for a URL, the value of the smallest must be used.
41
+ def <=>(other)
42
+ # RFC 6265 5.4
43
+ # Precedence: 1. longer path 2. older creation
44
+ (@name <=> other.name).nonzero? ||
45
+ (other.path.length <=> @path.length).nonzero? ||
46
+ (@created_at <=> other.created_at).nonzero? ||
47
+ @value <=> other.value
48
+ end
49
+
50
+ class << self
51
+ def new(cookie, *args)
52
+ return cookie if cookie.is_a?(self)
53
+
54
+ super
55
+ end
56
+
57
+ # Tests if +target_path+ is under +base_path+ as described in RFC
58
+ # 6265 5.1.4. +base_path+ must be an absolute path.
59
+ # +target_path+ may be empty, in which case it is treated as the
60
+ # root path.
61
+ #
62
+ # e.g.
63
+ #
64
+ # path_match?('/admin/', '/admin/index') == true
65
+ # path_match?('/admin/', '/Admin/index') == false
66
+ # path_match?('/admin/', '/admin/') == true
67
+ # path_match?('/admin/', '/admin') == false
68
+ #
69
+ # path_match?('/admin', '/admin') == true
70
+ # path_match?('/admin', '/Admin') == false
71
+ # path_match?('/admin', '/admins') == false
72
+ # path_match?('/admin', '/admin/') == true
73
+ # path_match?('/admin', '/admin/index') == true
74
+ def path_match?(base_path, target_path)
75
+ base_path.start_with?("/") || (return false)
76
+ # RFC 6265 5.1.4
77
+ bsize = base_path.size
78
+ tsize = target_path.size
79
+ return bsize == 1 if tsize.zero? # treat empty target_path as "/"
80
+ return false unless target_path.start_with?(base_path)
81
+ return true if bsize == tsize || base_path.end_with?("/")
82
+
83
+ target_path[bsize] == "/"
84
+ end
85
+ end
86
+
87
+ def initialize(arg, *attrs)
88
+ @created_at = Time.now
89
+
90
+ if attrs.empty?
91
+ attr_hash = Hash.try_convert(arg)
92
+ else
93
+ @name = arg
94
+ @value, attr_hash = attrs
95
+ attr_hash = Hash.try_convert(attr_hash)
96
+ end
97
+
98
+ attr_hash.each do |key, val|
99
+ key = key.downcase.tr("-", "_").to_sym unless key.is_a?(Symbol)
100
+
101
+ case key
102
+ when :domain, :path
103
+ __send__(:"#{key}=", val)
104
+ else
105
+ instance_variable_set(:"@#{key}", val)
106
+ end
107
+ end if attr_hash
108
+
109
+ @path ||= "/"
110
+ raise ArgumentError, "name must be specified" if @name.nil?
111
+ end
112
+
113
+ def expires
114
+ @expires || (@created_at && @max_age ? @created_at + @max_age : nil)
115
+ end
116
+
117
+ def expired?(time = Time.now)
118
+ return false unless expires
119
+
120
+ expires <= time
121
+ end
122
+
123
+ # Returns a string for use in the Cookie header, i.e. `name=value`
124
+ # or `name="value"`.
125
+ def cookie_value
126
+ "#{@name}=#{Scanner.quote(@value)}"
127
+ end
128
+ alias_method :to_s, :cookie_value
129
+
130
+ # Tests if it is OK to send this cookie to a given `uri`. A
131
+ # RuntimeError is raised if the cookie's domain is unknown.
132
+ def valid_for_uri?(uri)
133
+ uri = URI(uri)
134
+ # RFC 6265 5.4
135
+
136
+ return false if @secure && uri.scheme != "https"
137
+
138
+ acceptable_from_uri?(uri) && Cookie.path_match?(@path, uri.path)
139
+ end
140
+
141
+ private
142
+
143
+ # Tests if it is OK to accept this cookie if it is sent from a given
144
+ # URI/URL, `uri`.
145
+ def acceptable_from_uri?(uri)
146
+ uri = URI(uri)
147
+
148
+ host = DomainName.new(uri.host)
149
+
150
+ # RFC 6265 5.3
151
+ if host.hostname == @domain
152
+ true
153
+ elsif @for_domain # !host-only-flag
154
+ host.cookie_domain?(@domain_name)
155
+ else
156
+ @domain.nil?
157
+ end
158
+ end
159
+
160
+ module Scanner
161
+ RE_BAD_CHAR = /([\x00-\x20\x7F",;\\])/.freeze
162
+
163
+ module_function
164
+
165
+ def quote(s)
166
+ return s unless s.match(RE_BAD_CHAR)
167
+
168
+ "\"#{s.gsub(/([\\"])/, "\\\\\\1")}\""
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins::Cookies
5
+ # The Cookie Jar
6
+ #
7
+ # It holds a bunch of cookies.
8
+ class Jar
9
+ using URIExtensions
10
+
11
+ include Enumerable
12
+
13
+ def initialize_dup(orig)
14
+ super
15
+ @cookies = orig.instance_variable_get(:@cookies).dup
16
+ end
17
+
18
+ def initialize(cookies = nil)
19
+ @cookies = []
20
+
21
+ cookies.each do |elem|
22
+ cookie = case elem
23
+ when Cookie
24
+ elem
25
+ when Array
26
+ Cookie.new(*elem)
27
+ else
28
+ Cookie.new(elem)
29
+ end
30
+
31
+ @cookies << cookie
32
+ end if cookies
33
+ end
34
+
35
+ def parse(set_cookie)
36
+ SetCookieParser.call(set_cookie) do |name, value, attrs|
37
+ add(Cookie.new(name, value, attrs))
38
+ end
39
+ end
40
+
41
+ def add(cookie, path = nil)
42
+ c = cookie.dup
43
+
44
+ c.path = path if path && c.path == "/"
45
+
46
+ @cookies << c
47
+ end
48
+
49
+ def [](uri)
50
+ each(uri).sort
51
+ end
52
+
53
+ def each(uri = nil, &blk)
54
+ return enum_for(__method__, uri) unless block_given?
55
+
56
+ return @store.each(&blk) unless uri
57
+
58
+ uri = URI(uri)
59
+
60
+ now = Time.now
61
+ tpath = uri.path
62
+
63
+ @cookies.delete_if do |cookie|
64
+ if cookie.expired?(now)
65
+ true
66
+ else
67
+ yield cookie if cookie.valid_for_uri?(uri) && Cookie.path_match?(cookie.path, tpath)
68
+ false
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+ require "time"
5
+
6
+ module HTTPX
7
+ module Plugins::Cookies
8
+ module SetCookieParser
9
+ using(RegexpExtensions) unless Regexp.method_defined?(:match?)
10
+
11
+ # Whitespace.
12
+ RE_WSP = /[ \t]+/.freeze
13
+
14
+ # A pattern that matches a cookie name or attribute name which may
15
+ # be empty, capturing trailing whitespace.
16
+ RE_NAME = /(?!#{RE_WSP})[^,;\\"=]*/.freeze
17
+
18
+ RE_BAD_CHAR = /([\x00-\x20\x7F",;\\])/.freeze
19
+
20
+ # A pattern that matches the comma in a (typically date) value.
21
+ RE_COOKIE_COMMA = /,(?=#{RE_WSP}?#{RE_NAME}=)/.freeze
22
+
23
+ module_function
24
+
25
+ def scan_dquoted(scanner)
26
+ s = +""
27
+
28
+ until scanner.eos?
29
+ break if scanner.skip(/"/)
30
+
31
+ if scanner.skip(/\\/)
32
+ s << scanner.getch
33
+ elsif scanner.scan(/[^"\\]+/)
34
+ s << scanner.matched
35
+ end
36
+ end
37
+
38
+ s
39
+ end
40
+
41
+ def scan_value(scanner, comma_as_separator = false)
42
+ value = +""
43
+
44
+ until scanner.eos?
45
+ if scanner.scan(/[^,;"]+/)
46
+ value << scanner.matched
47
+ elsif scanner.skip(/"/)
48
+ # RFC 6265 2.2
49
+ # A cookie-value may be DQUOTE'd.
50
+ value << scan_dquoted(scanner)
51
+ elsif scanner.check(/;/)
52
+ break
53
+ elsif comma_as_separator && scanner.check(RE_COOKIE_COMMA)
54
+ break
55
+ else
56
+ value << scanner.getch
57
+ end
58
+ end
59
+
60
+ value.rstrip!
61
+ value
62
+ end
63
+
64
+ def scan_name_value(scanner, comma_as_separator = false)
65
+ name = scanner.scan(RE_NAME)
66
+ name.rstrip! if name
67
+
68
+ if scanner.skip(/=/)
69
+ value = scan_value(scanner, comma_as_separator)
70
+ else
71
+ scan_value(scanner, comma_as_separator)
72
+ value = nil
73
+ end
74
+ [name, value]
75
+ end
76
+
77
+ def call(set_cookie)
78
+ scanner = StringScanner.new(set_cookie)
79
+
80
+ # RFC 6265 4.1.1 & 5.2
81
+ until scanner.eos?
82
+ start = scanner.pos
83
+ len = nil
84
+
85
+ scanner.skip(RE_WSP)
86
+
87
+ name, value = scan_name_value(scanner, true)
88
+ value = nil if name.empty?
89
+
90
+ attrs = {}
91
+
92
+ until scanner.eos?
93
+ if scanner.skip(/,/)
94
+ # The comma is used as separator for concatenating multiple
95
+ # values of a header.
96
+ len = (scanner.pos - 1) - start
97
+ break
98
+ elsif scanner.skip(/;/)
99
+ scanner.skip(RE_WSP)
100
+
101
+ aname, avalue = scan_name_value(scanner, true)
102
+
103
+ next if aname.empty? || value.nil?
104
+
105
+ aname.downcase!
106
+
107
+ case aname
108
+ when "expires"
109
+ # RFC 6265 5.2.1
110
+ (avalue &&= Time.httpdate(avalue)) || next
111
+ when "max-age"
112
+ # RFC 6265 5.2.2
113
+ next unless /\A-?\d+\z/.match?(avalue)
114
+
115
+ avalue = Integer(avalue)
116
+ when "domain"
117
+ # RFC 6265 5.2.3
118
+ # An empty value SHOULD be ignored.
119
+ next if avalue.nil? || avalue.empty?
120
+ when "path"
121
+ # RFC 6265 5.2.4
122
+ # A relative path must be ignored rather than normalizing it
123
+ # to "/".
124
+ next unless avalue.start_with?("/")
125
+ when "secure", "httponly"
126
+ # RFC 6265 5.2.5, 5.2.6
127
+ avalue = true
128
+ end
129
+ attrs[aname] = avalue
130
+ end
131
+ end
132
+
133
+ len ||= scanner.pos - start
134
+
135
+ next if len > Cookie::MAX_LENGTH
136
+
137
+ yield(name, value, attrs) if name && !name.empty? && value
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end