httpx 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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