httpx 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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