httpx 0.0.1

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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +191 -0
  3. data/README.md +119 -0
  4. data/lib/httpx.rb +50 -0
  5. data/lib/httpx/buffer.rb +34 -0
  6. data/lib/httpx/callbacks.rb +32 -0
  7. data/lib/httpx/chainable.rb +51 -0
  8. data/lib/httpx/channel.rb +222 -0
  9. data/lib/httpx/channel/http1.rb +220 -0
  10. data/lib/httpx/channel/http2.rb +224 -0
  11. data/lib/httpx/client.rb +173 -0
  12. data/lib/httpx/connection.rb +74 -0
  13. data/lib/httpx/errors.rb +7 -0
  14. data/lib/httpx/extensions.rb +52 -0
  15. data/lib/httpx/headers.rb +152 -0
  16. data/lib/httpx/io.rb +240 -0
  17. data/lib/httpx/loggable.rb +11 -0
  18. data/lib/httpx/options.rb +138 -0
  19. data/lib/httpx/plugins/authentication.rb +14 -0
  20. data/lib/httpx/plugins/basic_authentication.rb +20 -0
  21. data/lib/httpx/plugins/compression.rb +123 -0
  22. data/lib/httpx/plugins/compression/brotli.rb +55 -0
  23. data/lib/httpx/plugins/compression/deflate.rb +50 -0
  24. data/lib/httpx/plugins/compression/gzip.rb +59 -0
  25. data/lib/httpx/plugins/cookies.rb +63 -0
  26. data/lib/httpx/plugins/digest_authentication.rb +141 -0
  27. data/lib/httpx/plugins/follow_redirects.rb +72 -0
  28. data/lib/httpx/plugins/h2c.rb +85 -0
  29. data/lib/httpx/plugins/proxy.rb +108 -0
  30. data/lib/httpx/plugins/proxy/http.rb +115 -0
  31. data/lib/httpx/plugins/proxy/socks4.rb +110 -0
  32. data/lib/httpx/plugins/proxy/socks5.rb +152 -0
  33. data/lib/httpx/plugins/push_promise.rb +67 -0
  34. data/lib/httpx/plugins/stream.rb +33 -0
  35. data/lib/httpx/registry.rb +88 -0
  36. data/lib/httpx/request.rb +222 -0
  37. data/lib/httpx/response.rb +225 -0
  38. data/lib/httpx/selector.rb +155 -0
  39. data/lib/httpx/timeout.rb +68 -0
  40. data/lib/httpx/transcoder.rb +12 -0
  41. data/lib/httpx/transcoder/body.rb +56 -0
  42. data/lib/httpx/transcoder/chunker.rb +38 -0
  43. data/lib/httpx/transcoder/form.rb +41 -0
  44. data/lib/httpx/transcoder/json.rb +36 -0
  45. data/lib/httpx/version.rb +5 -0
  46. metadata +150 -0
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ module Compression
6
+ module Brotli
7
+ def self.load_dependencies(klass, *)
8
+ klass.plugin(:compression)
9
+ require "brotli"
10
+ end
11
+
12
+ def self.configure(*)
13
+ Compression.register "br", self
14
+ end
15
+
16
+ module Encoder
17
+ module_function
18
+
19
+ def deflate(raw, buffer, chunk_size:)
20
+ while (chunk = raw.read(chunk_size))
21
+ compressed = ::Brotli.deflate(chunk)
22
+ buffer << compressed
23
+ yield compressed if block_given?
24
+ end
25
+ end
26
+ end
27
+
28
+ module BrotliWrapper
29
+ module_function
30
+
31
+ def inflate(text)
32
+ ::Brotli.inflate(text)
33
+ end
34
+
35
+ def close; end
36
+
37
+ def finish
38
+ ""
39
+ end
40
+ end
41
+
42
+ module_function
43
+
44
+ def encoder
45
+ Encoder
46
+ end
47
+
48
+ def decoder
49
+ Decoder.new(BrotliWrapper)
50
+ end
51
+ end
52
+ end
53
+ register_plugin :"compression/brotli", Compression::Brotli
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ module Compression
6
+ module Deflate
7
+ def self.load_dependencies(*)
8
+ require "stringio"
9
+ require "zlib"
10
+ end
11
+
12
+ def self.configure(*)
13
+ Compression.register "deflate", self
14
+ end
15
+
16
+ module Encoder
17
+ module_function
18
+
19
+ def deflate(raw, buffer, chunk_size:)
20
+ deflater = Zlib::Deflate.new(Zlib::BEST_COMPRESSION,
21
+ Zlib::MAX_WBITS,
22
+ Zlib::MAX_MEM_LEVEL,
23
+ Zlib::HUFFMAN_ONLY)
24
+ while (chunk = raw.read(chunk_size))
25
+ compressed = deflater.deflate(chunk)
26
+ buffer << compressed
27
+ yield compressed if block_given?
28
+ end
29
+ last = deflater.finish
30
+ buffer << last
31
+ yield last if block_given?
32
+ ensure
33
+ deflater.close
34
+ end
35
+ end
36
+
37
+ module_function
38
+
39
+ def encoder
40
+ Encoder
41
+ end
42
+
43
+ def decoder
44
+ Decoder.new(Zlib::Inflate.new(32 + Zlib::MAX_WBITS))
45
+ end
46
+ end
47
+ end
48
+ register_plugin :"compression/deflate", Compression::Deflate
49
+ end
50
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module HTTPX
6
+ module Plugins
7
+ module Compression
8
+ module GZIP
9
+ def self.load_dependencies(*)
10
+ require "zlib"
11
+ end
12
+
13
+ def self.configure(*)
14
+ Compression.register "gzip", self
15
+ end
16
+
17
+ class Encoder
18
+ def deflate(raw, buffer, chunk_size:)
19
+ gzip = Zlib::GzipWriter.new(self)
20
+
21
+ while (chunk = raw.read(chunk_size))
22
+ gzip.write(chunk)
23
+ gzip.flush
24
+ compressed = compressed_chunk
25
+ buffer << compressed
26
+ yield compressed if block_given?
27
+ end
28
+ ensure
29
+ gzip.close
30
+ end
31
+
32
+ private
33
+
34
+ def write(chunk)
35
+ @compressed_chunk = chunk
36
+ end
37
+
38
+ def compressed_chunk
39
+ compressed = @compressed_chunk
40
+ compressed
41
+ ensure
42
+ @compressed_chunk = nil
43
+ end
44
+ end
45
+
46
+ module_function
47
+
48
+ def encoder
49
+ Encoder.new
50
+ end
51
+
52
+ def decoder
53
+ Decoder.new(Zlib::Inflate.new(32 + Zlib::MAX_WBITS))
54
+ end
55
+ end
56
+ end
57
+ register_plugin :"compression/gzip", Compression::GZIP
58
+ end
59
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ module Cookies
6
+ def self.load_dependencies(*)
7
+ require "http/cookie"
8
+ end
9
+
10
+ module InstanceMethods
11
+ def cookies(cookies)
12
+ branch(default_options.with_cookies(cookies))
13
+ end
14
+ end
15
+
16
+ module RequestMethods
17
+ def initialize(*)
18
+ super
19
+ @headers.cookies(@options.cookies, self)
20
+ end
21
+ end
22
+
23
+ module HeadersMethods
24
+ def cookies(jar, request)
25
+ return unless jar
26
+ unless jar.is_a?(HTTP::CookieJar)
27
+ jar = jar.each_with_object(HTTP::CookieJar.new) do |(k, v), j|
28
+ cookie = k.is_a?(HTTP::Cookie) ? v : HTTP::Cookie.new(k.to_s, v.to_s)
29
+ cookie.domain = request.authority
30
+ cookie.path = request.path
31
+ j.add(cookie)
32
+ end
33
+ end
34
+ self["cookie"] = HTTP::Cookie.cookie_value(jar.cookies)
35
+ end
36
+ end
37
+
38
+ module ResponseMethods
39
+ def cookie_jar
40
+ return @cookies if defined?(@cookies)
41
+ return nil unless headers.key?("set-cookie")
42
+ @cookies ||= begin
43
+ jar = HTTP::CookieJar.new
44
+ jar.parse(headers["set-cookie"], @request.uri)
45
+ jar
46
+ end
47
+ end
48
+ alias_method :cookies, :cookie_jar
49
+ end
50
+
51
+ module OptionsMethods
52
+ def self.included(klass)
53
+ super
54
+ klass.def_option(:cookies) do |cookies|
55
+ cookies.split(/ *; */) if cookies.is_a?(String)
56
+ cookies
57
+ end
58
+ end
59
+ end
60
+ end
61
+ register_plugin :cookies, Cookies
62
+ end
63
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ module DigestAuthentication
6
+ DigestError = Class.new(Error)
7
+
8
+ def self.load_dependencies(*)
9
+ require "securerandom"
10
+ require "digest"
11
+ end
12
+
13
+ module InstanceMethods
14
+ def digest_authentication(user, password)
15
+ @_digest = Digest.new(user, password)
16
+ self
17
+ end
18
+ alias_method :digest_auth, :digest_authentication
19
+
20
+ def request(*args, keep_open: @keep_open, **options)
21
+ return super unless @_digest
22
+ begin
23
+ requests = __build_reqs(*args, **options)
24
+ probe_request = requests.first
25
+ prev_response = __send_reqs(*probe_request).first
26
+
27
+ unless prev_response.status == 401
28
+ raise Error, "request doesn't require authentication (status: #{prev_response})"
29
+ end
30
+
31
+ probe_request.transition(:idle)
32
+ responses = []
33
+
34
+ requests.each do |request|
35
+ token = @_digest.generate_header(request, prev_response)
36
+ request.headers["authorization"] = "Digest #{token}"
37
+ response = __send_reqs(*request).first
38
+ responses << response
39
+ prev_response = response
40
+ end
41
+ return responses.first if responses.size == 1
42
+ responses
43
+ ensure
44
+ close unless keep_open
45
+ end
46
+ end
47
+ end
48
+
49
+ class Digest
50
+ def initialize(user, password)
51
+ @user = user
52
+ @password = password
53
+ @nonce = 0
54
+ end
55
+
56
+ def generate_header(request, response, _iis = false)
57
+ method = request.verb.to_s.upcase
58
+ www = response.headers["www-authenticate"]
59
+
60
+ # TODO: assert if auth-type is Digest
61
+ auth_info = www[/^(\w+) (.*)/, 2]
62
+
63
+ uri = request.path
64
+
65
+ params = Hash[auth_info.scan(/(\w+)="(.*?)"/)]
66
+
67
+ nonce = params["nonce"]
68
+ nc = next_nonce
69
+
70
+ # verify qop
71
+ qop = params["qop"]
72
+
73
+ if params["algorithm"] =~ /(.*?)(-sess)?$/
74
+ algorithm = case Regexp.last_match(1)
75
+ when "MD5" then ::Digest::MD5
76
+ when "SHA1" then ::Digest::SHA1
77
+ when "SHA2" then ::Digest::SHA2
78
+ when "SHA256" then ::Digest::SHA256
79
+ when "SHA384" then ::Digest::SHA384
80
+ when "SHA512" then ::Digest::SHA512
81
+ when "RMD160" then ::Digest::RMD160
82
+ else raise DigestError, "unknown algorithm \"#{Regexp.last_match(1)}\""
83
+ end
84
+ sess = Regexp.last_match(2)
85
+ else
86
+ algorithm = ::Digest::MD5
87
+ end
88
+
89
+ if qop || sess
90
+ cnonce = make_cnonce
91
+ nc = format("%08x", nc)
92
+ end
93
+
94
+ a1 = if sess
95
+ [algorithm.hexdigest("#{@user}:#{params["realm"]}:#{@password}"),
96
+ nonce,
97
+ cnonce].join ":"
98
+ else
99
+ "#{@user}:#{params["realm"]}:#{@password}"
100
+ end
101
+
102
+ ha1 = algorithm.hexdigest(a1)
103
+ ha2 = algorithm.hexdigest("#{method}:#{uri}")
104
+ request_digest = [ha1, nonce]
105
+ request_digest.push(nc, cnonce, qop) if qop
106
+ request_digest << ha2
107
+ request_digest = request_digest.join(":")
108
+
109
+ header = [
110
+ %(username="#{@user}"),
111
+ %(nonce="#{nonce}"),
112
+ %(uri="#{uri}"),
113
+ %(response="#{algorithm.hexdigest(request_digest)}"),
114
+ ]
115
+ header << %(realm="#{params["realm"]}") if params.key?("realm")
116
+ header << %(algorithm=#{params["algorithm"]}") if params.key?("algorithm")
117
+ header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
118
+ header << %(cnonce="#{cnonce}") if cnonce
119
+ header << %(nc=#{nc})
120
+ header << %(qop=#{qop}) if qop
121
+ header.join ", "
122
+ end
123
+
124
+ private
125
+
126
+ def make_cnonce
127
+ ::Digest::MD5.hexdigest [
128
+ Time.now.to_i,
129
+ Process.pid,
130
+ SecureRandom.random_number(2**32),
131
+ ].join ":"
132
+ end
133
+
134
+ def next_nonce
135
+ @nonce += 1
136
+ end
137
+ end
138
+ end
139
+ register_plugin :digest_authentication, DigestAuthentication
140
+ end
141
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ module FollowRedirects
6
+ module InstanceMethods
7
+ MAX_REDIRECTS = 3
8
+ REDIRECT_STATUS = 300..399
9
+
10
+ def max_redirects(n)
11
+ branch(default_options.with_max_redirects(n.to_i))
12
+ end
13
+
14
+ def request(*args, **options)
15
+ # do not needlessly close channels
16
+ keep_open = @keep_open
17
+ @keep_open = true
18
+
19
+ max_redirects = @options.max_redirects || MAX_REDIRECTS
20
+ requests = __build_reqs(*args, **options)
21
+ responses = __send_reqs(*requests)
22
+
23
+ loop do
24
+ redirect_requests = []
25
+ indexes = responses.each_with_index.map do |response, index|
26
+ next unless REDIRECT_STATUS.include?(response.status)
27
+ request = requests[index]
28
+ retry_request = __build_redirect_req(request, response, options)
29
+ redirect_requests << retry_request
30
+ index
31
+ end.compact
32
+ break if redirect_requests.empty?
33
+ break if max_redirects <= 0
34
+ max_redirects -= 1
35
+
36
+ redirect_responses = __send_reqs(*redirect_requests)
37
+ indexes.each_with_index do |index, i2|
38
+ requests[index] = redirect_requests[i2]
39
+ responses[index] = redirect_responses[i2]
40
+ end
41
+ end
42
+
43
+ return responses.first if responses.size == 1
44
+ responses
45
+ ensure
46
+ @keep_open = keep_open
47
+ end
48
+
49
+ private
50
+
51
+ def __build_redirect_req(request, response, options)
52
+ redirect_uri = URI(response.headers["location"])
53
+ redirect_uri = response.uri.merge(redirect_uri) if redirect_uri.relative?
54
+
55
+ # TODO: integrate cookies in the next request
56
+ # redirects are **ALWAYS** GET
57
+ retry_options = options.merge(headers: request.headers,
58
+ body: request.body)
59
+ __build_req(:get, redirect_uri, retry_options)
60
+ end
61
+ end
62
+
63
+ module OptionsMethods
64
+ def self.included(klass)
65
+ super
66
+ klass.def_option(:max_redirects)
67
+ end
68
+ end
69
+ end
70
+ register_plugin :follow_redirects, FollowRedirects
71
+ end
72
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ module H2C
6
+ def self.load_dependencies(*)
7
+ require "base64"
8
+ end
9
+
10
+ module InstanceMethods
11
+ def request(*args, keep_open: @keep_open, **options)
12
+ return super if @_h2c_probed
13
+ begin
14
+ requests = __build_reqs(*args, **options)
15
+
16
+ upgrade_request = requests.first
17
+ return super unless valid_h2c_upgrade_request?(upgrade_request)
18
+ upgrade_request.headers["upgrade"] = "h2c"
19
+ upgrade_request.headers.add("connection", "upgrade")
20
+ upgrade_request.headers.add("connection", "http2-settings")
21
+ upgrade_request.headers["http2-settings"] = HTTP2::Client.settings_header(@options.http2_settings)
22
+ # TODO: validate!
23
+ upgrade_response = __send_reqs(*upgrade_request, **options).first
24
+
25
+ if upgrade_response.status == 101
26
+ channel = find_channel(upgrade_request)
27
+ parser = channel.upgrade_parser("h2")
28
+ parser.extend(UpgradeExtensions)
29
+ parser.upgrade(upgrade_request, upgrade_response, **options)
30
+ data = upgrade_response.to_s
31
+ parser << data
32
+ response = upgrade_request.response
33
+ if response.status == 200
34
+ requests.delete(upgrade_request)
35
+ return response if requests.empty?
36
+ end
37
+ responses = __send_reqs(*requests)
38
+ else
39
+ # proceed as usual
40
+ responses = [upgrade_response] + __send_reqs(*requests[1..-1])
41
+ end
42
+ return responses.first if responses.size == 1
43
+ responses
44
+ ensure
45
+ @_h2c_probed = true
46
+ close unless keep_open
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ VALID_H2C_METHODS = %i[get options head].freeze
53
+ private_constant :VALID_H2C_METHODS
54
+
55
+ def valid_h2c_upgrade_request?(request)
56
+ VALID_H2C_METHODS.include?(request.verb) &&
57
+ request.scheme == "http"
58
+ end
59
+ end
60
+
61
+ module UpgradeExtensions
62
+ def upgrade(request, _response, **)
63
+ @connection.send_connection_preface
64
+ # skip checks, it is assumed that this is the first
65
+ # request in the connection
66
+ stream = @connection.upgrade
67
+ handle_stream(stream, request)
68
+ @streams[request] = stream
69
+ end
70
+ end
71
+
72
+ module FrameBuilder
73
+ include HTTP2
74
+
75
+ module_function
76
+
77
+ def settings_value(settings)
78
+ frame = Framer.new.generate(type: :settings, stream: 0, payload: settings)
79
+ Base64.urlsafe_encode64(frame[9..-1])
80
+ end
81
+ end
82
+ end
83
+ register_plugin(:h2c, H2C)
84
+ end
85
+ end