http 5.3.1 → 6.0.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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +110 -13
  5. data/UPGRADING.md +491 -0
  6. data/http.gemspec +32 -29
  7. data/lib/http/base64.rb +11 -1
  8. data/lib/http/chainable/helpers.rb +62 -0
  9. data/lib/http/chainable/verbs.rb +136 -0
  10. data/lib/http/chainable.rb +232 -136
  11. data/lib/http/client.rb +158 -127
  12. data/lib/http/connection/internals.rb +141 -0
  13. data/lib/http/connection.rb +126 -97
  14. data/lib/http/content_type.rb +61 -6
  15. data/lib/http/errors.rb +25 -1
  16. data/lib/http/feature.rb +65 -5
  17. data/lib/http/features/auto_deflate.rb +124 -17
  18. data/lib/http/features/auto_inflate.rb +38 -15
  19. data/lib/http/features/caching/entry.rb +178 -0
  20. data/lib/http/features/caching/in_memory_store.rb +63 -0
  21. data/lib/http/features/caching.rb +216 -0
  22. data/lib/http/features/digest_auth.rb +234 -0
  23. data/lib/http/features/instrumentation.rb +97 -17
  24. data/lib/http/features/logging.rb +183 -5
  25. data/lib/http/features/normalize_uri.rb +17 -0
  26. data/lib/http/features/raise_error.rb +18 -3
  27. data/lib/http/form_data/composite_io.rb +106 -0
  28. data/lib/http/form_data/file.rb +95 -0
  29. data/lib/http/form_data/multipart/param.rb +62 -0
  30. data/lib/http/form_data/multipart.rb +106 -0
  31. data/lib/http/form_data/part.rb +52 -0
  32. data/lib/http/form_data/readable.rb +58 -0
  33. data/lib/http/form_data/urlencoded.rb +175 -0
  34. data/lib/http/form_data/version.rb +8 -0
  35. data/lib/http/form_data.rb +102 -0
  36. data/lib/http/headers/known.rb +3 -0
  37. data/lib/http/headers/normalizer.rb +17 -36
  38. data/lib/http/headers.rb +172 -65
  39. data/lib/http/mime_type/adapter.rb +24 -9
  40. data/lib/http/mime_type/json.rb +19 -4
  41. data/lib/http/mime_type.rb +21 -3
  42. data/lib/http/options/definitions.rb +189 -0
  43. data/lib/http/options.rb +172 -125
  44. data/lib/http/redirector.rb +80 -75
  45. data/lib/http/request/body.rb +87 -6
  46. data/lib/http/request/builder.rb +184 -0
  47. data/lib/http/request/proxy.rb +83 -0
  48. data/lib/http/request/writer.rb +76 -16
  49. data/lib/http/request.rb +214 -98
  50. data/lib/http/response/body.rb +103 -18
  51. data/lib/http/response/inflater.rb +35 -7
  52. data/lib/http/response/parser.rb +98 -4
  53. data/lib/http/response/status/reasons.rb +2 -4
  54. data/lib/http/response/status.rb +141 -31
  55. data/lib/http/response.rb +219 -61
  56. data/lib/http/retriable/delay_calculator.rb +38 -11
  57. data/lib/http/retriable/errors.rb +21 -0
  58. data/lib/http/retriable/performer.rb +82 -38
  59. data/lib/http/session.rb +280 -0
  60. data/lib/http/timeout/global.rb +147 -34
  61. data/lib/http/timeout/null.rb +155 -9
  62. data/lib/http/timeout/per_operation.rb +139 -18
  63. data/lib/http/uri/normalizer.rb +82 -0
  64. data/lib/http/uri/parsing.rb +182 -0
  65. data/lib/http/uri.rb +289 -124
  66. data/lib/http/version.rb +2 -1
  67. data/lib/http.rb +11 -2
  68. data/sig/deps.rbs +122 -0
  69. data/sig/http.rbs +1619 -0
  70. data/test/http/base64_test.rb +28 -0
  71. data/test/http/client_test.rb +739 -0
  72. data/test/http/connection_test.rb +1533 -0
  73. data/test/http/content_type_test.rb +190 -0
  74. data/test/http/errors_test.rb +28 -0
  75. data/test/http/feature_test.rb +49 -0
  76. data/test/http/features/auto_deflate_test.rb +317 -0
  77. data/test/http/features/auto_inflate_test.rb +213 -0
  78. data/test/http/features/caching_test.rb +942 -0
  79. data/test/http/features/digest_auth_test.rb +996 -0
  80. data/test/http/features/instrumentation_test.rb +246 -0
  81. data/test/http/features/logging_test.rb +654 -0
  82. data/test/http/features/normalize_uri_test.rb +41 -0
  83. data/test/http/features/raise_error_test.rb +77 -0
  84. data/test/http/form_data/composite_io_test.rb +215 -0
  85. data/test/http/form_data/file_test.rb +255 -0
  86. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  87. data/test/http/form_data/multipart_test.rb +303 -0
  88. data/test/http/form_data/part_test.rb +90 -0
  89. data/test/http/form_data/urlencoded_test.rb +164 -0
  90. data/test/http/form_data_test.rb +232 -0
  91. data/test/http/headers/normalizer_test.rb +93 -0
  92. data/test/http/headers_test.rb +888 -0
  93. data/test/http/mime_type/json_test.rb +39 -0
  94. data/test/http/mime_type_test.rb +150 -0
  95. data/test/http/options/base_uri_test.rb +148 -0
  96. data/test/http/options/body_test.rb +21 -0
  97. data/test/http/options/features_test.rb +38 -0
  98. data/test/http/options/form_test.rb +21 -0
  99. data/test/http/options/headers_test.rb +32 -0
  100. data/test/http/options/json_test.rb +21 -0
  101. data/test/http/options/merge_test.rb +78 -0
  102. data/test/http/options/new_test.rb +37 -0
  103. data/test/http/options/proxy_test.rb +32 -0
  104. data/test/http/options_test.rb +575 -0
  105. data/test/http/redirector_test.rb +639 -0
  106. data/test/http/request/body_test.rb +318 -0
  107. data/test/http/request/builder_test.rb +623 -0
  108. data/test/http/request/writer_test.rb +391 -0
  109. data/test/http/request_test.rb +1733 -0
  110. data/test/http/response/body_test.rb +292 -0
  111. data/test/http/response/parser_test.rb +105 -0
  112. data/test/http/response/status_test.rb +322 -0
  113. data/test/http/response_test.rb +502 -0
  114. data/test/http/retriable/delay_calculator_test.rb +194 -0
  115. data/test/http/retriable/errors_test.rb +71 -0
  116. data/test/http/retriable/performer_test.rb +551 -0
  117. data/test/http/session_test.rb +424 -0
  118. data/test/http/timeout/global_test.rb +239 -0
  119. data/test/http/timeout/null_test.rb +218 -0
  120. data/test/http/timeout/per_operation_test.rb +220 -0
  121. data/test/http/uri/normalizer_test.rb +89 -0
  122. data/test/http/uri_test.rb +1140 -0
  123. data/test/http/version_test.rb +15 -0
  124. data/test/http_test.rb +818 -0
  125. data/test/regression_tests.rb +27 -0
  126. data/test/support/dummy_server/encoding_routes.rb +47 -0
  127. data/test/support/dummy_server/routes.rb +201 -0
  128. data/test/support/dummy_server/servlet.rb +81 -0
  129. data/test/support/dummy_server.rb +200 -0
  130. data/{spec → test}/support/fakeio.rb +2 -2
  131. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  132. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  133. data/test/support/http_handling_shared.rb +11 -0
  134. data/test/support/proxy_server.rb +207 -0
  135. data/test/support/servers/runner.rb +67 -0
  136. data/{spec → test}/support/simplecov.rb +11 -2
  137. data/test/support/ssl_helper.rb +108 -0
  138. data/test/test_helper.rb +38 -0
  139. metadata +108 -168
  140. data/.github/workflows/ci.yml +0 -67
  141. data/.gitignore +0 -15
  142. data/.rspec +0 -1
  143. data/.rubocop/layout.yml +0 -8
  144. data/.rubocop/metrics.yml +0 -4
  145. data/.rubocop/rspec.yml +0 -9
  146. data/.rubocop/style.yml +0 -32
  147. data/.rubocop.yml +0 -11
  148. data/.rubocop_todo.yml +0 -219
  149. data/.yardopts +0 -2
  150. data/CHANGES_OLD.md +0 -1002
  151. data/Gemfile +0 -51
  152. data/Guardfile +0 -18
  153. data/Rakefile +0 -64
  154. data/lib/http/headers/mixin.rb +0 -34
  155. data/lib/http/retriable/client.rb +0 -37
  156. data/logo.png +0 -0
  157. data/spec/lib/http/client_spec.rb +0 -556
  158. data/spec/lib/http/connection_spec.rb +0 -88
  159. data/spec/lib/http/content_type_spec.rb +0 -47
  160. data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
  161. data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
  162. data/spec/lib/http/features/instrumentation_spec.rb +0 -81
  163. data/spec/lib/http/features/logging_spec.rb +0 -65
  164. data/spec/lib/http/features/raise_error_spec.rb +0 -62
  165. data/spec/lib/http/headers/mixin_spec.rb +0 -36
  166. data/spec/lib/http/headers/normalizer_spec.rb +0 -52
  167. data/spec/lib/http/headers_spec.rb +0 -527
  168. data/spec/lib/http/options/body_spec.rb +0 -15
  169. data/spec/lib/http/options/features_spec.rb +0 -33
  170. data/spec/lib/http/options/form_spec.rb +0 -15
  171. data/spec/lib/http/options/headers_spec.rb +0 -24
  172. data/spec/lib/http/options/json_spec.rb +0 -15
  173. data/spec/lib/http/options/merge_spec.rb +0 -68
  174. data/spec/lib/http/options/new_spec.rb +0 -30
  175. data/spec/lib/http/options/proxy_spec.rb +0 -20
  176. data/spec/lib/http/options_spec.rb +0 -13
  177. data/spec/lib/http/redirector_spec.rb +0 -530
  178. data/spec/lib/http/request/body_spec.rb +0 -211
  179. data/spec/lib/http/request/writer_spec.rb +0 -121
  180. data/spec/lib/http/request_spec.rb +0 -234
  181. data/spec/lib/http/response/body_spec.rb +0 -85
  182. data/spec/lib/http/response/parser_spec.rb +0 -74
  183. data/spec/lib/http/response/status_spec.rb +0 -253
  184. data/spec/lib/http/response_spec.rb +0 -262
  185. data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
  186. data/spec/lib/http/retriable/performer_spec.rb +0 -302
  187. data/spec/lib/http/uri/normalizer_spec.rb +0 -95
  188. data/spec/lib/http/uri_spec.rb +0 -71
  189. data/spec/lib/http_spec.rb +0 -535
  190. data/spec/regression_specs.rb +0 -24
  191. data/spec/spec_helper.rb +0 -89
  192. data/spec/support/black_hole.rb +0 -13
  193. data/spec/support/dummy_server/servlet.rb +0 -203
  194. data/spec/support/dummy_server.rb +0 -44
  195. data/spec/support/fuubar.rb +0 -21
  196. data/spec/support/http_handling_shared.rb +0 -190
  197. data/spec/support/proxy_server.rb +0 -39
  198. data/spec/support/servers/config.rb +0 -11
  199. data/spec/support/servers/runner.rb +0 -19
  200. data/spec/support/ssl_helper.rb +0 -104
  201. /data/{spec → test}/support/capture_warning.rb +0 -0
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  require "http/headers"
6
4
 
7
5
  module HTTP
6
+ # Follows HTTP redirects according to configured policy
8
7
  class Redirector
9
8
  # Notifies that we reached max allowed redirect hops
10
9
  class TooManyRedirectsError < ResponseError; end
@@ -26,117 +25,123 @@ module HTTP
26
25
  # Verbs which will remain unchanged upon See Other response.
27
26
  SEE_OTHER_ALLOWED_VERBS = %i[get head].to_set.freeze
28
27
 
29
- # @!attribute [r] strict
30
- # Returns redirector policy.
31
- # @return [Boolean]
28
+ # Returns redirector policy
29
+ #
30
+ # @example
31
+ # redirector.strict # => true
32
+ #
33
+ # @return [Boolean]
34
+ # @api public
32
35
  attr_reader :strict
33
36
 
34
- # @!attribute [r] max_hops
35
- # Returns maximum allowed hops.
36
- # @return [Fixnum]
37
+ # Returns maximum allowed hops
38
+ #
39
+ # @example
40
+ # redirector.max_hops # => 5
41
+ #
42
+ # @return [Fixnum]
43
+ # @api public
37
44
  attr_reader :max_hops
38
45
 
39
- # @param [Hash] opts
40
- # @option opts [Boolean] :strict (true) redirector hops policy
41
- # @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
42
- def initialize(opts = {})
43
- @strict = opts.fetch(:strict, true)
44
- @max_hops = opts.fetch(:max_hops, 5).to_i
45
- @on_redirect = opts.fetch(:on_redirect, nil)
46
+ # Initializes a new Redirector
47
+ #
48
+ # @example
49
+ # HTTP::Redirector.new(strict: true, max_hops: 5)
50
+ #
51
+ # @param [Boolean] strict (true) redirector hops policy
52
+ # @param [#to_i] max_hops (5) maximum allowed amount of hops
53
+ # @param [#call, nil] on_redirect optional redirect callback
54
+ # @api public
55
+ # @return [HTTP::Redirector]
56
+ def initialize(strict: true, max_hops: 5, on_redirect: nil)
57
+ @strict = strict
58
+ @max_hops = Integer(max_hops)
59
+ @on_redirect = on_redirect
46
60
  end
47
61
 
48
62
  # Follows redirects until non-redirect response found
49
- def perform(request, response)
63
+ #
64
+ # @example
65
+ # redirector.perform(request, response) { |req| client.perform(req) }
66
+ #
67
+ # @param [HTTP::Request] request
68
+ # @param [HTTP::Response] response
69
+ # @api public
70
+ # @return [HTTP::Response]
71
+ def perform(request, response, &)
50
72
  @request = request
51
73
  @response = response
52
74
  @visited = []
53
- collect_cookies_from_request
54
- collect_cookies_from_response
55
-
56
- while REDIRECT_CODES.include? @response.status.code
57
- @visited << "#{@request.verb} #{@request.uri}"
58
-
59
- raise TooManyRedirectsError if too_many_hops?
60
- raise EndlessRedirectError if endless_loop?
61
75
 
62
- @response.flush
63
-
64
- # XXX(ixti): using `Array#inject` to return `nil` if no Location header.
65
- @request = redirect_to(@response.headers.get(Headers::LOCATION).inject(:+))
66
- unless cookie_jar.empty?
67
- @request.headers.set(Headers::COOKIE, cookie_jar.cookies.map { |c| "#{c.name}=#{c.value}" }.join("; "))
68
- end
69
- @on_redirect.call @response, @request if @on_redirect.respond_to?(:call)
70
- @response = yield @request
71
- collect_cookies_from_response
72
- end
76
+ follow_redirects(&) while REDIRECT_CODES.include?(@response.code)
73
77
 
74
78
  @response
75
79
  end
76
80
 
77
81
  private
78
82
 
79
- # All known cookies. On the original request, this is only the original cookies, but after that,
80
- # Set-Cookie headers can add, set or delete cookies.
81
- def cookie_jar
82
- # it seems that @response.cookies instance is reused between responses, so we have to "clone"
83
- @cookie_jar ||= HTTP::CookieJar.new
84
- end
83
+ # Perform a single redirect step
84
+ #
85
+ # @api private
86
+ # @return [void]
87
+ def follow_redirects
88
+ @visited << visit_key
85
89
 
86
- def collect_cookies_from_request
87
- request_cookie_header = @request.headers["Cookie"]
88
- cookies =
89
- if request_cookie_header
90
- HTTP::Cookie.cookie_value_to_hash(request_cookie_header)
91
- else
92
- {}
93
- end
94
-
95
- cookies.each do |key, value|
96
- cookie_jar.add(HTTP::Cookie.new(key, value, :path => @request.uri.path, :domain => @request.host))
97
- end
90
+ raise TooManyRedirectsError if too_many_hops?
91
+ raise EndlessRedirectError if endless_loop?
92
+
93
+ @response.flush
94
+
95
+ @request = redirect_to(redirect_uri)
96
+ @on_redirect&.call @response, @request
97
+ @response = yield @request
98
98
  end
99
99
 
100
- # Carry cookies from one response to the next. Carrying cookies to the next response ends up
101
- # carrying them to the next request as well.
100
+ # Extracts the redirect URI from the Location header
102
101
  #
103
- # Note that this isn't part of the IETF standard, but all major browsers support setting cookies
104
- # on redirect: https://blog.dubbelboer.com/2012/11/25/302-cookie.html
105
- def collect_cookies_from_response
106
- # Overwrite previous cookies
107
- @response.cookies.each do |cookie|
108
- if cookie.value == ""
109
- cookie_jar.delete(cookie)
110
- else
111
- cookie_jar.add(cookie)
112
- end
113
- end
114
-
115
- # I wish we could just do @response.cookes = cookie_jar
116
- cookie_jar.each do |cookie|
117
- @response.cookies.add(cookie)
118
- end
102
+ # @api private
103
+ # @return [String, nil] URI string or nil if no Location header
104
+ def redirect_uri
105
+ location = @response.headers.get(Headers::LOCATION)
106
+ location.join unless location.empty?
119
107
  end
120
108
 
121
109
  # Check if we reached max amount of redirect hops
110
+ #
111
+ # @api private
122
112
  # @return [Boolean]
123
113
  def too_many_hops?
124
- 1 <= @max_hops && @max_hops < @visited.count
114
+ @max_hops.positive? && @visited.length > @max_hops
125
115
  end
126
116
 
127
117
  # Check if we got into an endless loop
118
+ #
119
+ # @api private
128
120
  # @return [Boolean]
129
121
  def endless_loop?
130
- 2 <= @visited.count(@visited.last)
122
+ @visited.count(@visited.last) > 1
123
+ end
124
+
125
+ # Build a visit key for the current request
126
+ #
127
+ # Includes verb, URI, and Cookie header so that requests to the same URL
128
+ # with different cookies are not falsely detected as an endless loop.
129
+ #
130
+ # @api private
131
+ # @return [String]
132
+ def visit_key
133
+ "#{@request.verb} #{@request.uri} #{@request.headers[Headers::COOKIE]}"
131
134
  end
132
135
 
133
136
  # Redirect policy for follow
137
+ #
138
+ # @api private
134
139
  # @return [Request]
135
140
  def redirect_to(uri)
136
141
  raise StateError, "no Location header in redirect" unless uri
137
142
 
138
143
  verb = @request.verb
139
- code = @response.status.code
144
+ code = @response.code
140
145
 
141
146
  if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
142
147
  raise StateError, "can't follow #{@response.status} redirect" if @strict
@@ -144,7 +149,7 @@ module HTTP
144
149
  verb = :get
145
150
  end
146
151
 
147
- verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && 303 == code
152
+ verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && code.eql?(303)
148
153
 
149
154
  @request.redirect(uri, verb)
150
155
  end
@@ -2,18 +2,66 @@
2
2
 
3
3
  module HTTP
4
4
  class Request
5
+ # Represents an HTTP request body with streaming support
5
6
  class Body
7
+ # The source data for this body
8
+ #
9
+ # @example
10
+ # body.source # => "hello world"
11
+ #
12
+ # @return [String, Enumerable, IO, nil]
13
+ # @api public
6
14
  attr_reader :source
7
15
 
16
+ # Initialize a new request body
17
+ #
18
+ # @example
19
+ # Body.new("hello world")
20
+ #
21
+ # @return [HTTP::Request::Body]
22
+ # @api public
8
23
  def initialize(source)
9
24
  @source = source
10
25
 
11
26
  validate_source_type!
12
27
  end
13
28
 
14
- # Returns size which should be used for the "Content-Length" header.
29
+ # Whether the body is empty
30
+ #
31
+ # @example
32
+ # body.empty? # => true
33
+ #
34
+ # @return [Boolean]
35
+ # @api public
36
+ def empty?
37
+ @source.nil?
38
+ end
39
+
40
+ # Whether the body content can be accessed for logging
41
+ #
42
+ # Returns true for String sources (the content can be inspected).
43
+ # Returns false for IO streams and Enumerables (which cannot be
44
+ # read without consuming them), and for nil bodies.
45
+ #
46
+ # The logging feature checks the string encoding separately to
47
+ # decide whether to log the content as text or format it as binary.
48
+ #
49
+ # @example
50
+ # body.loggable? # => true
51
+ #
52
+ # @return [Boolean]
53
+ # @api public
54
+ def loggable?
55
+ @source.is_a?(String)
56
+ end
57
+
58
+ # Returns size for the "Content-Length" header
59
+ #
60
+ # @example
61
+ # body.size
15
62
  #
16
63
  # @return [Integer]
64
+ # @api public
17
65
  def size
18
66
  if @source.is_a?(String)
19
67
  @source.bytesize
@@ -24,33 +72,49 @@ module HTTP
24
72
  elsif @source.nil?
25
73
  0
26
74
  else
27
- raise RequestError, "cannot determine size of body: #{@source.inspect}"
75
+ raise RequestError,
76
+ "cannot determine size of body: #{@source.inspect}; " \
77
+ "set the Content-Length header explicitly or use chunked Transfer-Encoding"
28
78
  end
29
79
  end
30
80
 
31
- # Yields chunks of content to be streamed to the request body.
81
+ # Yields chunks of content to be streamed
82
+ #
83
+ # @example
84
+ # body.each { |chunk| socket.write(chunk) }
32
85
  #
33
86
  # @yieldparam [String]
87
+ # @return [self]
88
+ # @api public
34
89
  def each(&block)
35
90
  if @source.is_a?(String)
36
91
  yield @source
37
92
  elsif @source.respond_to?(:read)
38
93
  IO.copy_stream(@source, ProcIO.new(block))
39
94
  rewind(@source)
40
- elsif @source.is_a?(Enumerable)
95
+ elsif @source
41
96
  @source.each(&block)
42
97
  end
43
98
 
44
99
  self
45
100
  end
46
101
 
47
- # Request bodies are equivalent when they have the same source.
102
+ # Check equality based on source
103
+ #
104
+ # @example
105
+ # body == other_body
106
+ #
107
+ # @return [Boolean]
108
+ # @api public
48
109
  def ==(other)
49
- self.class == other.class && self.source == other.source # rubocop:disable Style/RedundantSelf
110
+ other.is_a?(self.class) && source == other.source
50
111
  end
51
112
 
52
113
  private
53
114
 
115
+ # Rewind an IO source if possible
116
+ # @return [void]
117
+ # @api private
54
118
  def rewind(io)
55
119
  io.rewind if io.respond_to? :rewind
56
120
  rescue Errno::ESPIPE, Errno::EPIPE
@@ -73,6 +137,9 @@ module HTTP
73
137
  nil
74
138
  end
75
139
 
140
+ # Validate that source is a supported type
141
+ # @return [void]
142
+ # @api private
76
143
  def validate_source_type!
77
144
  return if @source.is_a?(String)
78
145
  return if @source.respond_to?(:read)
@@ -86,10 +153,24 @@ module HTTP
86
153
  # #write simply calling the proc, which we can pass in as the
87
154
  # "destination IO" in IO.copy_stream.
88
155
  class ProcIO
156
+ # Initialize a new ProcIO wrapper
157
+ #
158
+ # @example
159
+ # ProcIO.new(block)
160
+ #
161
+ # @return [ProcIO]
162
+ # @api public
89
163
  def initialize(block)
90
164
  @block = block
91
165
  end
92
166
 
167
+ # Write data by calling the wrapped proc
168
+ #
169
+ # @example
170
+ # proc_io.write("hello")
171
+ #
172
+ # @return [Integer]
173
+ # @api public
93
174
  def write(data)
94
175
  @block.call(data)
95
176
  data.bytesize
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ require "http/form_data"
6
+ require "http/headers"
7
+ require "http/connection"
8
+ require "http/uri"
9
+
10
+ module HTTP
11
+ class Request
12
+ # Builds HTTP::Request objects from resolved options
13
+ #
14
+ # @example Build a request from options
15
+ # options = HTTP::Options.new(headers: {"Accept" => "application/json"})
16
+ # builder = HTTP::Request::Builder.new(options)
17
+ # request = builder.build(:get, "https://example.com")
18
+ #
19
+ # @see Options
20
+ class Builder
21
+ # Pattern matching HTTP or HTTPS URI schemes
22
+ HTTP_OR_HTTPS_RE = %r{\Ahttps?://}i
23
+
24
+ # Initialize a new Request Builder
25
+ #
26
+ # @example
27
+ # HTTP::Request::Builder.new(HTTP::Options.new)
28
+ #
29
+ # @param options [HTTP::Options] resolved request options
30
+ # @return [HTTP::Request::Builder]
31
+ # @api public
32
+ def initialize(options)
33
+ @options = options
34
+ end
35
+
36
+ # Build an HTTP request
37
+ #
38
+ # @example
39
+ # builder.build(:get, "https://example.com")
40
+ #
41
+ # @param verb [Symbol] the HTTP method
42
+ # @param uri [#to_s] the URI to request
43
+ # @return [HTTP::Request] the built request object
44
+ # @api public
45
+ def build(verb, uri)
46
+ uri = make_request_uri(uri)
47
+ headers = make_request_headers
48
+ body = make_request_body(headers)
49
+
50
+ req = HTTP::Request.new(
51
+ verb: verb,
52
+ uri: uri,
53
+ uri_normalizer: @options.feature(:normalize_uri)&.normalizer,
54
+ proxy: @options.proxy,
55
+ headers: headers,
56
+ body: body
57
+ )
58
+
59
+ wrap(req)
60
+ end
61
+
62
+ # Wrap a request through feature middleware
63
+ #
64
+ # @example
65
+ # builder.wrap(redirect_request)
66
+ #
67
+ # @param request [HTTP::Request] the request to wrap
68
+ # @return [HTTP::Request] the wrapped request
69
+ # @api public
70
+ def wrap(request)
71
+ @options.features.inject(request) do |req, (_name, feature)|
72
+ feature.wrap_request(req)
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ # Merges query params if needed
79
+ #
80
+ # @param uri [#to_s] the URI to process
81
+ # @return [HTTP::URI] the constructed URI
82
+ # @api private
83
+ def make_request_uri(uri)
84
+ uri = uri.to_s
85
+
86
+ if @options.base_uri? && uri !~ HTTP_OR_HTTPS_RE
87
+ uri = resolve_against_base(uri)
88
+ elsif @options.persistent? && uri !~ HTTP_OR_HTTPS_RE
89
+ uri = "#{@options.persistent}#{uri}"
90
+ end
91
+
92
+ uri = HTTP::URI.parse uri
93
+
94
+ merge_query_params!(uri)
95
+
96
+ # Some proxies (seen on WEBrick) fail if URL has
97
+ # empty path (e.g. `http://example.com`) while it's RFC-compliant:
98
+ # http://tools.ietf.org/html/rfc1738#section-3.1
99
+ uri.path = "/" if uri.path.empty?
100
+
101
+ uri
102
+ end
103
+
104
+ # Resolve a relative URI against the configured base URI
105
+ #
106
+ # Ensures the base URI path has a trailing slash so that relative
107
+ # paths are appended rather than replacing the last path segment,
108
+ # per the convention described in RFC 3986 Section 5.
109
+ #
110
+ # @param uri [String] the relative URI to resolve
111
+ # @return [String] the resolved absolute URI
112
+ # @api private
113
+ def resolve_against_base(uri)
114
+ base = @options.base_uri or raise Error, "base_uri is not set"
115
+
116
+ unless base.path.end_with?("/")
117
+ base = base.dup
118
+ base.path = "#{base.path}/"
119
+ end
120
+
121
+ String(base.join(uri))
122
+ end
123
+
124
+ # Merge query parameters into URI
125
+ #
126
+ # @return [void]
127
+ # @api private
128
+ def merge_query_params!(uri)
129
+ return unless @options.params && !@options.params.empty?
130
+
131
+ existing = ::URI.decode_www_form(uri.query || "")
132
+ uri.query = ::URI.encode_www_form(existing.concat(@options.params.to_a))
133
+ end
134
+
135
+ # Creates request headers
136
+ #
137
+ # @return [HTTP::Headers] the constructed headers
138
+ # @api private
139
+ def make_request_headers
140
+ headers = @options.headers
141
+
142
+ # Tell the server to keep the conn open
143
+ headers[Headers::CONNECTION] = @options.persistent? ? Connection::KEEP_ALIVE : Connection::CLOSE
144
+
145
+ headers
146
+ end
147
+
148
+ # Create the request body object to send
149
+ #
150
+ # @return [String, HTTP::FormData, nil] the request body
151
+ # @api private
152
+ def make_request_body(headers)
153
+ if @options.body
154
+ @options.body
155
+ elsif @options.form
156
+ form = make_form_data(@options.form)
157
+ headers[Headers::CONTENT_TYPE] ||= form.content_type
158
+ form
159
+ elsif @options.json
160
+ make_json_body(@options.json, headers)
161
+ end
162
+ end
163
+
164
+ # Encode JSON body and set content type header
165
+ # @return [String] the encoded JSON body
166
+ # @api private
167
+ def make_json_body(data, headers)
168
+ body = MimeType[:json].encode data
169
+ headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name.downcase}"
170
+ body
171
+ end
172
+
173
+ # Coerce form data into an HTTP::FormData object
174
+ # @return [HTTP::FormData::Multipart, HTTP::FormData::Urlencoded] form data
175
+ # @api private
176
+ def make_form_data(form)
177
+ return form if form.is_a? FormData::Multipart
178
+ return form if form.is_a? FormData::Urlencoded
179
+
180
+ FormData.create(form)
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ class Request
5
+ # Proxy-related methods for HTTP requests
6
+ module Proxy
7
+ # Merges proxy headers into the request headers
8
+ #
9
+ # @example
10
+ # request.include_proxy_headers
11
+ #
12
+ # @return [void]
13
+ # @api public
14
+ def include_proxy_headers
15
+ headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers)
16
+ include_proxy_authorization_header if using_authenticated_proxy?
17
+ end
18
+
19
+ # Compute and add the Proxy-Authorization header
20
+ #
21
+ # @example
22
+ # request.include_proxy_authorization_header
23
+ #
24
+ # @return [void]
25
+ # @api public
26
+ def include_proxy_authorization_header
27
+ headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header
28
+ end
29
+
30
+ # Build the Proxy-Authorization header value
31
+ #
32
+ # @example
33
+ # request.proxy_authorization_header
34
+ #
35
+ # @return [String]
36
+ # @api public
37
+ def proxy_authorization_header
38
+ digest = encode64(format("%s:%s", proxy.fetch(:proxy_username), proxy.fetch(:proxy_password)))
39
+ "Basic #{digest}"
40
+ end
41
+
42
+ # Setup tunnel through proxy for SSL request
43
+ #
44
+ # @example
45
+ # request.connect_using_proxy(socket)
46
+ #
47
+ # @return [void]
48
+ # @api public
49
+ def connect_using_proxy(socket)
50
+ Writer.new(socket, nil, proxy_connect_headers, proxy_connect_header).connect_through_proxy
51
+ end
52
+
53
+ # Compute HTTP request header SSL proxy connection
54
+ #
55
+ # @example
56
+ # request.proxy_connect_header
57
+ #
58
+ # @return [String]
59
+ # @api public
60
+ def proxy_connect_header
61
+ "CONNECT #{host}:#{port} HTTP/#{version}"
62
+ end
63
+
64
+ # Headers to send with proxy connect request
65
+ #
66
+ # @example
67
+ # request.proxy_connect_headers
68
+ #
69
+ # @return [HTTP::Headers]
70
+ # @api public
71
+ def proxy_connect_headers
72
+ connect_headers = Headers.coerce(
73
+ Headers::HOST => headers[Headers::HOST],
74
+ Headers::USER_AGENT => headers[Headers::USER_AGENT]
75
+ )
76
+
77
+ connect_headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header if using_authenticated_proxy?
78
+ connect_headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers)
79
+ connect_headers
80
+ end
81
+ end
82
+ end
83
+ end