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,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ module PushPromise
6
+ PUSH_OPTIONS = { http2_settings: { settings_enable_push: 1 },
7
+ max_concurrent_requests: 1 }.freeze
8
+
9
+ module ResponseMethods
10
+ def pushed?
11
+ @__pushed
12
+ end
13
+
14
+ def mark_as_pushed!
15
+ @__pushed = true
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ def initialize(opts = {})
21
+ super(PUSH_OPTIONS.merge(opts))
22
+ @promise_headers = {}
23
+ end
24
+
25
+ private
26
+
27
+ def on_promise(parser, stream)
28
+ stream.on(:promise_headers) do |h|
29
+ __on_promise_request(parser, stream, h)
30
+ end
31
+ stream.on(:headers) do |h|
32
+ __on_promise_response(parser, stream, h)
33
+ end
34
+ end
35
+
36
+ def __on_promise_request(parser, stream, h)
37
+ log(1, "#{stream.id}: ") do
38
+ h.map { |k, v| "-> PROMISE HEADER: #{k}: #{v}" }.join("\n")
39
+ end
40
+ headers = @options.headers_class.new(h)
41
+ path = headers[":path"]
42
+ authority = headers[":authority"]
43
+ request = parser.pending.find { |r| r.authority == authority && r.path == path }
44
+ if request
45
+ request.merge_headers(headers)
46
+ @promise_headers[stream] = request
47
+ parser.pending.delete(request)
48
+ else
49
+ stream.refuse
50
+ end
51
+ end
52
+
53
+ def __on_promise_response(parser, stream, h)
54
+ request = @promise_headers.delete(stream)
55
+ return unless request
56
+ parser.__send__(:on_stream_headers, stream, request, h)
57
+ request.transition(:done)
58
+ response = request.response
59
+ response.mark_as_pushed!
60
+ stream.on(:data, &parser.method(:on_stream_data).curry[stream, request])
61
+ stream.on(:close, &parser.method(:on_stream_close).curry[stream, request])
62
+ end
63
+ end
64
+ end
65
+ register_plugin(:push_promise, PushPromise)
66
+ end
67
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ module Stream
6
+ module InstanceMethods
7
+ def stream
8
+ headers("accept" => "text/event-stream",
9
+ "cache-control" => "no-cache")
10
+ end
11
+ end
12
+
13
+ module ResponseMethods
14
+ def complete?
15
+ super ||
16
+ stream? &&
17
+ @stream_complete
18
+ end
19
+
20
+ def stream?
21
+ @headers["content-type"].start_with?("text/event-stream")
22
+ end
23
+
24
+ def <<(data)
25
+ res = super
26
+ @stream_complete = true if String(data).end_with?("\n\n")
27
+ res
28
+ end
29
+ end
30
+ end
31
+ register_plugin :stream, Stream
32
+ end
33
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ # Adds a general-purpose registry API to a class. It is designed to be a
5
+ # configuration-level API, i.e. the registry is global to the class and
6
+ # should be set on **boot time**.
7
+ #
8
+ # It is used internally to associate tags with handlers.
9
+ #
10
+ # ## Register/Fetch
11
+ #
12
+ # One is strongly advised to register handlers when creating the class.
13
+ #
14
+ # There is an instance-level method to retrieve from the registry based
15
+ # on the tag:
16
+ #
17
+ # class Server
18
+ # include HTTPX::Registry
19
+ #
20
+ # register "tcp", TCPHandler
21
+ # register "ssl", SSLHandlers
22
+ # ...
23
+ #
24
+ #
25
+ # def handle(uri)
26
+ # scheme = uri.scheme
27
+ # handler = registry(scheme) #=> TCPHandler
28
+ # handler.handle
29
+ # end
30
+ # end
31
+ #
32
+ module Registry
33
+ # Base Registry Error
34
+ Error = Class.new(Error)
35
+
36
+ def self.extended(klass)
37
+ super
38
+ klass.extend(ClassMethods)
39
+ end
40
+
41
+ def self.included(klass)
42
+ super
43
+ klass.extend(ClassMethods)
44
+ klass.__send__(:include, InstanceMethods)
45
+ end
46
+
47
+ # Class Methods
48
+ module ClassMethods
49
+ def inherited(klass)
50
+ super
51
+ klass.instance_variable_set(:@registry, @registry.dup)
52
+ end
53
+
54
+ # @param [Object] tag the handler identifier in the registry
55
+ # @return [Symbol, String, Object] the corresponding handler (if Symbol or String,
56
+ # will assume it referes to an autoloaded module, and will load-and-return it).
57
+ #
58
+ def registry(tag = nil)
59
+ @registry ||= {}
60
+ return @registry if tag.nil?
61
+ handler = @registry.fetch(tag)
62
+ raise(Error, "#{tag} is not registered in #{self}") unless handler
63
+ case handler
64
+ when Symbol, String
65
+ const_get(handler)
66
+ else
67
+ handler
68
+ end
69
+ end
70
+
71
+ # @param [Object] tag the identifier for the handler in the registry
72
+ # @return [Symbol, String, Object] the handler (if Symbol or String, it is
73
+ # assumed to be an autoloaded module, to be loaded later)
74
+ #
75
+ def register(tag, handler)
76
+ registry[tag] = handler
77
+ end
78
+ end
79
+
80
+ # Instance Methods
81
+ module InstanceMethods
82
+ # delegates to HTTPX::Registry#registry
83
+ def registry(tag)
84
+ self.class.registry(tag)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module HTTPX
6
+ class Request
7
+ extend Forwardable
8
+
9
+ METHODS = [
10
+ # RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
11
+ :options, :get, :head, :post, :put, :delete, :trace, :connect,
12
+
13
+ # RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV
14
+ :propfind, :proppatch, :mkcol, :copy, :move, :lock, :unlock,
15
+
16
+ # RFC 3648: WebDAV Ordered Collections Protocol
17
+ :orderpatch,
18
+
19
+ # RFC 3744: WebDAV Access Control Protocol
20
+ :acl,
21
+
22
+ # RFC 6352: vCard Extensions to WebDAV -- CardDAV
23
+ :report,
24
+
25
+ # RFC 5789: PATCH Method for HTTP
26
+ :patch,
27
+
28
+ # draft-reschke-webdav-search: WebDAV Search
29
+ :search
30
+ ].freeze
31
+
32
+ USER_AGENT = "httpx.rb/#{VERSION}"
33
+
34
+ attr_reader :verb, :uri, :headers, :body, :state
35
+
36
+ attr_accessor :response
37
+
38
+ def_delegator :@body, :<<
39
+
40
+ def_delegator :@body, :empty?
41
+
42
+ def_delegator :@body, :chunk!
43
+
44
+ def initialize(verb, uri, options = {})
45
+ @verb = verb.to_s.downcase.to_sym
46
+ @uri = URI(uri)
47
+ @options = Options.new(options)
48
+
49
+ raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
50
+
51
+ @headers = @options.headers_class.new(@options.headers)
52
+ @headers["user-agent"] ||= USER_AGENT
53
+ @headers["accept"] ||= "*/*"
54
+
55
+ @body = @options.request_body_class.new(@headers, @options)
56
+ @state = :idle
57
+ end
58
+
59
+ def merge_headers(h)
60
+ @headers = @headers.merge(h)
61
+ end
62
+
63
+ def scheme
64
+ @uri.scheme
65
+ end
66
+
67
+ def path
68
+ path = uri.path
69
+ path << "/" if path.empty?
70
+ path << "?#{query}" unless query.empty?
71
+ path
72
+ end
73
+
74
+ def authority
75
+ host = @uri.host
76
+ port_string = @uri.port == @uri.default_port ? nil : ":#{@uri.port}"
77
+ "#{host}#{port_string}"
78
+ end
79
+
80
+ def query
81
+ return @query if defined?(@query)
82
+ query = []
83
+ if (q = @options.params)
84
+ query << URI.encode_www_form(q)
85
+ end
86
+ query << @uri.query if @uri.query
87
+ @query = query.join("&")
88
+ end
89
+
90
+ def drain_body
91
+ return nil if @body.nil?
92
+ @drainer ||= @body.each
93
+ chunk = @drainer.next
94
+ chunk.dup
95
+ rescue StopIteration
96
+ nil
97
+ end
98
+
99
+ def inspect
100
+ "#<Request #{@verb.to_s.upcase} #{path} @headers=#{@headers.to_hash} @body=#{@body}>"
101
+ end
102
+
103
+ class Body
104
+ class << self
105
+ def new(*, options)
106
+ return options.body if options.body.is_a?(self)
107
+ super
108
+ end
109
+ end
110
+
111
+ def initialize(headers, options)
112
+ @headers = headers
113
+ @body = if options.body
114
+ Transcoder.registry("body").encode(options.body)
115
+ elsif options.form
116
+ Transcoder.registry("form").encode(options.form)
117
+ elsif options.json
118
+ Transcoder.registry("json").encode(options.json)
119
+ end
120
+ return if @body.nil?
121
+ @headers["content-type"] ||= @body.content_type
122
+ @headers["content-length"] = @body.bytesize unless unbounded_body?
123
+ end
124
+
125
+ def each(&block)
126
+ return enum_for(__method__) unless block_given?
127
+ return if @body.nil?
128
+ body = stream(@body)
129
+ if body.respond_to?(:read)
130
+ ::IO.copy_stream(body, ProcIO.new(block))
131
+ elsif body.respond_to?(:each)
132
+ body.each(&block)
133
+ else
134
+ block[body.to_s]
135
+ end
136
+ end
137
+
138
+ def empty?
139
+ return true if @body.nil?
140
+ return false if chunked?
141
+ bytesize.zero?
142
+ end
143
+
144
+ def bytesize
145
+ return 0 if @body.nil?
146
+ if @body.respond_to?(:bytesize)
147
+ @body.bytesize
148
+ elsif @body.respond_to?(:size)
149
+ @body.size
150
+ else
151
+ raise Error, "cannot determine size of body: #{@body.inspect}"
152
+ end
153
+ end
154
+
155
+ def stream(body)
156
+ encoded = body
157
+ encoded = Transcoder.registry("chunker").encode(body) if chunked?
158
+ encoded
159
+ end
160
+
161
+ def unbounded_body?
162
+ chunked? || @body.bytesize == Float::INFINITY
163
+ end
164
+
165
+ def chunked?
166
+ @headers["transfer-encoding"] == "chunked"
167
+ end
168
+
169
+ def chunk!
170
+ @headers.add("transfer-encoding", "chunked")
171
+ end
172
+ end
173
+
174
+ def transition(nextstate)
175
+ case nextstate
176
+ when :idle
177
+ @response = nil
178
+ when :headers
179
+ return unless @state == :idle
180
+ when :body
181
+ return unless @state == :headers ||
182
+ @state == :expect
183
+
184
+ if @headers.key?("expect")
185
+ unless @response
186
+ @state = :expect
187
+ return
188
+ end
189
+
190
+ case @response.status
191
+ when 100
192
+ # deallocate
193
+ @response = nil
194
+ when 417
195
+ @response = ErrorResponse.new("Expectation Failed", 0)
196
+ return
197
+ end
198
+ end
199
+ when :done
200
+ return if @state == :expect
201
+ end
202
+ @state = nextstate
203
+ nil
204
+ end
205
+
206
+ def expects?
207
+ @headers["expect"] == "100-continue" &&
208
+ @response && @response.status == 100
209
+ end
210
+
211
+ class ProcIO
212
+ def initialize(block)
213
+ @block = block
214
+ end
215
+
216
+ def write(data)
217
+ @block.call(data)
218
+ data.bytesize
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "tempfile"
5
+ require "fileutils"
6
+ require "forwardable"
7
+
8
+ module HTTPX
9
+ class Response
10
+ extend Forwardable
11
+
12
+ attr_reader :status, :headers, :body, :version
13
+
14
+ def_delegator :@body, :to_s
15
+
16
+ def_delegator :@body, :read
17
+
18
+ def_delegator :@body, :copy_to
19
+
20
+ def_delegator :@body, :close
21
+
22
+ def_delegator :@request, :uri
23
+
24
+ def initialize(request, status, version, headers, options = {})
25
+ @options = Options.new(options)
26
+ @version = version
27
+ @request = request
28
+ @status = Integer(status)
29
+ @headers = @options.headers_class.new(headers)
30
+ @body = @options.response_body_class.new(self, threshold_size: @options.body_threshold_size,
31
+ window_size: @options.window_size)
32
+ end
33
+
34
+ def merge_headers(h)
35
+ @headers = @headers.merge(h)
36
+ end
37
+
38
+ def <<(data)
39
+ @body.write(data)
40
+ end
41
+
42
+ def bodyless?
43
+ @request.verb == :head ||
44
+ @status < 200 ||
45
+ @status == 201 ||
46
+ @status == 204 ||
47
+ @status == 205 ||
48
+ @status == 304
49
+ end
50
+
51
+ def content_type
52
+ ContentType.parse(@headers["content-type"])
53
+ end
54
+
55
+ def complete?
56
+ bodyless? || (@request.verb == :connect && @status == 200)
57
+ end
58
+
59
+ def inspect
60
+ "#<Response:#{object_id} @status=#{@status} @headers=#{@headers}>"
61
+ end
62
+
63
+ class Body
64
+ def initialize(response, threshold_size:, window_size: 1 << 14)
65
+ @response = response
66
+ @headers = response.headers
67
+ @threshold_size = threshold_size
68
+ @window_size = window_size
69
+ @encoding = response.content_type.charset || Encoding::BINARY
70
+ @length = 0
71
+ @buffer = nil
72
+ @state = :idle
73
+ end
74
+
75
+ def write(chunk)
76
+ @length += chunk.bytesize
77
+ transition
78
+ @buffer.write(chunk)
79
+ end
80
+
81
+ def read(*args)
82
+ return unless @buffer
83
+ @buffer.read(*args)
84
+ end
85
+
86
+ def bytesize
87
+ @length
88
+ end
89
+
90
+ def each
91
+ return enum_for(__method__) unless block_given?
92
+ begin
93
+ unless @state == :idle
94
+ rewind
95
+ while (chunk = @buffer.read(@window_size))
96
+ yield(chunk)
97
+ end
98
+ end
99
+ ensure
100
+ close
101
+ end
102
+ end
103
+
104
+ def to_s
105
+ rewind
106
+ return @buffer.read.force_encoding(@encoding) if @buffer
107
+ ""
108
+ ensure
109
+ close
110
+ end
111
+ alias_method :to_str, :to_s
112
+
113
+ def empty?
114
+ @length.zero?
115
+ end
116
+
117
+ def copy_to(dest)
118
+ return unless @buffer
119
+ if dest.respond_to?(:path) && @buffer.respond_to?(:path)
120
+ FileUtils.mv(@buffer.path, dest.path)
121
+ else
122
+ @buffer.rewind
123
+ ::IO.copy_stream(@buffer, dest)
124
+ end
125
+ end
126
+
127
+ # closes/cleans the buffer, resets everything
128
+ def close
129
+ return if @state == :idle
130
+ @buffer.close
131
+ @buffer.unlink if @buffer.respond_to?(:unlink)
132
+ @buffer = nil
133
+ @length = 0
134
+ @state = :idle
135
+ end
136
+
137
+ def ==(other)
138
+ to_s == other.to_s
139
+ end
140
+
141
+ private
142
+
143
+ def rewind
144
+ return if @state == :idle
145
+ @buffer.rewind
146
+ end
147
+
148
+ def transition
149
+ case @state
150
+ when :idle
151
+ if @length > @threshold_size
152
+ @state = :buffer
153
+ @buffer = Tempfile.new("httpx", encoding: @encoding, mode: File::RDWR)
154
+ else
155
+ @state = :memory
156
+ @buffer = StringIO.new("".b, File::RDWR)
157
+ end
158
+ when :memory
159
+ if @length > @threshold_size
160
+ aux = @buffer
161
+ @buffer = Tempfile.new("palanca", encoding: @encoding, mode: File::RDWR)
162
+ aux.rewind
163
+ ::IO.copy_stream(aux, @buffer)
164
+ # TODO: remove this if/when minor ruby is 2.3
165
+ # (this looks like a bug from older versions)
166
+ @buffer.pos = aux.pos #######################
167
+ #############################################
168
+ aux.close
169
+ @state = :buffer
170
+ end
171
+ end
172
+
173
+ return unless %i[memory buffer].include?(@state)
174
+ end
175
+ end
176
+ end
177
+
178
+ class ContentType
179
+ MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
180
+ CHARSET_RE = /;\s*charset=([^;]+)/i
181
+
182
+ attr_reader :mime_type, :charset
183
+
184
+ def initialize(mime_type, charset)
185
+ @mime_type = mime_type
186
+ @charset = charset
187
+ end
188
+
189
+ class << self
190
+ # Parse string and return ContentType struct
191
+ def parse(str)
192
+ new(mime_type(str), charset(str))
193
+ end
194
+
195
+ private
196
+
197
+ # :nodoc:
198
+ def mime_type(str)
199
+ m = str.to_s[MIME_TYPE_RE, 1]
200
+ m && m.strip.downcase
201
+ end
202
+
203
+ # :nodoc:
204
+ def charset(str)
205
+ m = str.to_s[CHARSET_RE, 1]
206
+ m && m.strip.delete('"')
207
+ end
208
+ end
209
+ end
210
+
211
+ class ErrorResponse
212
+ attr_reader :error, :retries
213
+
214
+ alias_method :status, :error
215
+
216
+ def initialize(error, retries)
217
+ @error = error
218
+ @retries = retries
219
+ end
220
+
221
+ def retryable?
222
+ @retries.positive?
223
+ end
224
+ end
225
+ end