async-http 0.69.0 → 0.70.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f4a2ceef3a210332512cbe39f7bcac65cecaf23308f151534cfeb955fccb3e1
4
- data.tar.gz: a44a6b0f61b2bdbf81b868e1e05bfe7e45855bc92a3efdc266534688d9f84fd8
3
+ metadata.gz: f049c61bc6afdaebb893a682209ac35e761efa9a4ef6aea5759e276153a4da80
4
+ data.tar.gz: 0334a7f914b12caafa97e7ab00ea3e02845c8451c52c159bbd625ecc9a8f9d51
5
5
  SHA512:
6
- metadata.gz: 22f65ce662e632e0d7836f13c12db69f86305ecae5e88ccf97ace1e0dc3c861e5af4fce7877dd489fec2be48031dfef99b1b697c1c0153df9670f095061465e4
7
- data.tar.gz: 9cfb68209dcff74007c239d94176e7a3b9ac76a6667066e55e1d230b54e77663389cb5f81743ca2ac0f40b5f832e2f4720d65fa55c69962f808a9860a1018bd8
6
+ metadata.gz: 93188a2d18c3991ac8e1b7306dc26ea552b7094b1683c3c6434ee2b76951c4fa725a280438026cd410ccd1229efd36bbf337173241688e61552a8c9c23f5d07c
7
+ data.tar.gz: e0cc2b44db653d9770facff7958a512bb56da5483b07348bc12af17b76fec7cfc22e95a2d3d5fdcb608edac47591304c04e6ab558e5ae07fff816d125d3e0379
checksums.yaml.gz.sig CHANGED
Binary file
@@ -18,6 +18,13 @@ module Async
18
18
  module HTTP
19
19
  # Represents a way to connect to a remote HTTP server.
20
20
  class Endpoint < ::IO::Endpoint::Generic
21
+ SCHEMES = {
22
+ 'http' => URI::HTTP,
23
+ 'https' => URI::HTTPS,
24
+ 'ws' => URI::WS,
25
+ 'wss' => URI::WSS,
26
+ }
27
+
21
28
  def self.parse(string, endpoint = nil, **options)
22
29
  url = URI.parse(string).normalize
23
30
 
@@ -25,9 +32,15 @@ module Async
25
32
  end
26
33
 
27
34
  # Construct an endpoint with a specified scheme, hostname, optional path, and options.
35
+ #
36
+ # @parameter scheme [String] The scheme to use, e.g. "http" or "https".
37
+ # @parameter hostname [String] The hostname to connect to (or bind to).
38
+ # @parameter *options [Hash] Additional options, passed to {#initialize}.
28
39
  def self.for(scheme, hostname, path = "/", **options)
29
40
  # TODO: Consider using URI.for once it becomes available:
30
- uri_klass = URI.scheme_list[scheme.upcase] || URI::HTTP
41
+ uri_klass = SCHEMES.fetch(scheme.downcase) do
42
+ raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
43
+ end
31
44
 
32
45
  self.new(
33
46
  uri_klass.new(scheme, nil, hostname, nil, nil, path, nil, nil, nil).normalize,
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2021-2023, by Samuel Williams.
4
+ # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
6
  require_relative '../internet'
7
7
 
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2018-2023, by Samuel Williams.
5
+ # Copyright, 2019-2020, by Brian Morearty.
6
+
7
+ require_relative '../reference'
8
+
9
+ require 'protocol/http/middleware'
10
+ require 'protocol/http/body/rewindable'
11
+
12
+ module Async
13
+ module HTTP
14
+ module Middleware
15
+ # A client wrapper which transparently handles redirects to a given maximum number of hops.
16
+ #
17
+ # The default implementation will only follow relative locations (i.e. those without a scheme) and will switch to GET if the original request was not a GET.
18
+ #
19
+ # The best reference for these semantics is defined by the [Fetch specification](https://fetch.spec.whatwg.org/#http-redirect-fetch).
20
+ #
21
+ # | Redirect using GET | Permanent | Temporary |
22
+ # |:-----------------------------------------:|:---------:|:---------:|
23
+ # | Allowed | 301 | 302 |
24
+ # | Preserve original method | 308 | 307 |
25
+ #
26
+ # For the specific details of the redirect handling, see:
27
+ # - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-2> 301 Moved Permanently.
28
+ # - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-3> 302 Found.
29
+ # - <https://datatracker.ietf.org/doc/html/rfc7538 308 Permanent Redirect.
30
+ # - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-7> 307 Temporary Redirect.
31
+ #
32
+ class LocationRedirector < ::Protocol::HTTP::Middleware
33
+ class TooManyRedirects < StandardError
34
+ end
35
+
36
+ # Header keys which should be deleted when changing a request from a POST to a GET as defined by <https://fetch.spec.whatwg.org/#request-body-header-name>.
37
+ PROHIBITED_GET_HEADERS = [
38
+ 'content-encoding',
39
+ 'content-language',
40
+ 'content-location',
41
+ 'content-type',
42
+ ]
43
+
44
+ # maximum_hops is the max number of redirects. Set to 0 to allow 1 request with no redirects.
45
+ def initialize(app, maximum_hops = 3)
46
+ super(app)
47
+
48
+ @maximum_hops = maximum_hops
49
+ end
50
+
51
+ # The maximum number of hops which will limit the number of redirects until an error is thrown.
52
+ attr :maximum_hops
53
+
54
+ def redirect_with_get?(request, response)
55
+ # We only want to switch to GET if the request method is something other than get, e.g. POST.
56
+ if request.method != GET
57
+ # According to the RFC, we should only switch to GET if the response is a 301 or 302:
58
+ return response.status == 301 || response.status == 302
59
+ end
60
+ end
61
+
62
+ # Handle a redirect to a relative location.
63
+ #
64
+ # @parameter request [Protocol::HTTP::Request] The original request, which you can modify if you want to handle the redirect.
65
+ # @parameter location [String] The relative location to redirect to.
66
+ # @returns [Boolean] True if the redirect was handled, false if it was not.
67
+ def handle_redirect(request, location)
68
+ uri = URI.parse(location)
69
+
70
+ if uri.absolute?
71
+ return false
72
+ end
73
+
74
+ # Update the path of the request:
75
+ request.path = Reference[request.path] + location
76
+
77
+ # Follow the redirect:
78
+ return true
79
+ end
80
+
81
+ def call(request)
82
+ # We don't want to follow redirects for HEAD requests:
83
+ return super if request.head?
84
+
85
+ if body = request.body
86
+ if body.respond_to?(:rewind)
87
+ # The request body was already rewindable, so use it as is:
88
+ body = request.body
89
+ else
90
+ # The request body was not rewindable, and we might need to resubmit it if we get a response status of 307 or 308, so make it rewindable:
91
+ body = ::Protocol::HTTP::Body::Rewindable.new(body)
92
+ request.body = body
93
+ end
94
+ end
95
+
96
+ hops = 0
97
+
98
+ while hops <= @maximum_hops
99
+ response = super(request)
100
+
101
+ if response.redirection?
102
+ hops += 1
103
+
104
+ # Get the redirect location:
105
+ unless location = response.headers['location']
106
+ return response
107
+ end
108
+
109
+ response.finish
110
+
111
+ unless handle_redirect(request, location)
112
+ return response
113
+ end
114
+
115
+ # Ensure the request (body) is finished and set to nil before we manipulate the request:
116
+ request.finish
117
+
118
+ if request.method == GET or response.preserve_method?
119
+ # We (might) need to rewind the body so that it can be submitted again:
120
+ body&.rewind
121
+ request.body = body
122
+ else
123
+ # We are changing the method to GET:
124
+ request.method = GET
125
+
126
+ # We will no longer be submitting the body:
127
+ body = nil
128
+
129
+ # Remove any headers which are not allowed in a GET request:
130
+ PROHIBITED_GET_HEADERS.each do |header|
131
+ request.headers.delete(header)
132
+ end
133
+ end
134
+ else
135
+ return response
136
+ end
137
+ end
138
+
139
+ raise TooManyRedirects, "Redirected #{hops} times, exceeded maximum!"
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -1,24 +1,7 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- # THE SOFTWARE.
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
22
5
 
23
6
  require_relative '../protocol'
24
7
 
@@ -1,23 +1,6 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- # THE SOFTWARE.
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
22
5
 
23
6
  require_relative 'mock/endpoint'
@@ -31,7 +31,8 @@ module Async
31
31
  @connection = connection
32
32
  @reason = reason
33
33
 
34
- protocol = headers.delete(UPGRADE)
34
+ # Technically, there should never be more than one value for the upgrade header, but we'll just take the first one to avoid complexity.
35
+ protocol = headers.delete(UPGRADE)&.first
35
36
 
36
37
  super(version, status, headers, body, protocol)
37
38
  end
@@ -60,9 +60,9 @@ module Async
60
60
  # If a response was generated, send it:
61
61
  if response
62
62
  trailer = response.headers.trailer!
63
-
63
+
64
64
  write_response(@version, response.status, response.headers)
65
-
65
+
66
66
  # Some operations in this method are long running, that is, it's expected that `body.call(stream)` could literally run indefinitely. In order to facilitate garbage collection, we want to nullify as many local variables before calling the streaming body. This ensures that the garbage collection can clean up as much state as possible during the long running operation, so we don't retain objects that are no longer needed.
67
67
 
68
68
  if body and protocol = response.protocol
@@ -89,7 +89,7 @@ module Async
89
89
 
90
90
  write_body(version, body, head, trailer)
91
91
  end
92
-
92
+
93
93
  # We are done with the body, you shouldn't need to call close on it:
94
94
  body = nil
95
95
  else
@@ -4,116 +4,15 @@
4
4
  # Copyright, 2018-2023, by Samuel Williams.
5
5
  # Copyright, 2019-2020, by Brian Morearty.
6
6
 
7
- require_relative 'client'
8
- require_relative 'endpoint'
9
- require_relative 'reference'
7
+ require_relative 'middleware/location_redirector'
10
8
 
11
- require 'protocol/http/middleware'
12
- require 'protocol/http/body/rewindable'
9
+ warn "`Async::HTTP::RelativeLocation` is deprecated and will be removed in the next release. Please use `Async::HTTP::Middleware::LocationRedirector` instead.", uplevel: 1
13
10
 
14
11
  module Async
15
12
  module HTTP
16
- class TooManyRedirects < StandardError
17
- end
18
-
19
- # A client wrapper which transparently handles both relative and absolute redirects to a given maximum number of hops.
20
- #
21
- # The best reference for these semantics is defined by the [Fetch specification](https://fetch.spec.whatwg.org/#http-redirect-fetch).
22
- #
23
- # | Redirect using GET | Permanent | Temporary |
24
- # |:-----------------------------------------:|:---------:|:---------:|
25
- # | Allowed | 301 | 302 |
26
- # | Preserve original method | 308 | 307 |
27
- #
28
- # For the specific details of the redirect handling, see:
29
- # - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-2> 301 Moved Permanently.
30
- # - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-3> 302 Found.
31
- # - <https://datatracker.ietf.org/doc/html/rfc7538 308 Permanent Redirect.
32
- # - <https://datatracker.ietf.org/doc/html/rfc7231#section-6-4-7> 307 Temporary Redirect.
33
- #
34
- class RelativeLocation < ::Protocol::HTTP::Middleware
35
- # Header keys which should be deleted when changing a request from a POST to a GET as defined by <https://fetch.spec.whatwg.org/#request-body-header-name>.
36
- PROHIBITED_GET_HEADERS = [
37
- 'content-encoding',
38
- 'content-language',
39
- 'content-location',
40
- 'content-type',
41
- ]
42
-
43
- # maximum_hops is the max number of redirects. Set to 0 to allow 1 request with no redirects.
44
- def initialize(app, maximum_hops = 3)
45
- super(app)
46
-
47
- @maximum_hops = maximum_hops
48
- end
49
-
50
- # The maximum number of hops which will limit the number of redirects until an error is thrown.
51
- attr :maximum_hops
52
-
53
- def redirect_with_get?(request, response)
54
- # We only want to switch to GET if the request method is something other than get, e.g. POST.
55
- if request.method != GET
56
- # According to the RFC, we should only switch to GET if the response is a 301 or 302:
57
- return response.status == 301 || response.status == 302
58
- end
59
- end
60
-
61
- def call(request)
62
- # We don't want to follow redirects for HEAD requests:
63
- return super if request.head?
64
-
65
- if body = request.body
66
- # We need to cache the body as it might be submitted multiple times if we get a response status of 307 or 308:
67
- body = ::Protocol::HTTP::Body::Rewindable.new(body)
68
- request.body = body
69
- end
70
-
71
- hops = 0
72
-
73
- while hops <= @maximum_hops
74
- response = super(request)
75
-
76
- if response.redirection?
77
- hops += 1
78
-
79
- # Get the redirect location:
80
- unless location = response.headers['location']
81
- return response
82
- end
83
-
84
- response.finish
85
-
86
- uri = URI.parse(location)
87
-
88
- if uri.absolute?
89
- return response
90
- else
91
- request.path = Reference[request.path] + location
92
- end
93
-
94
- if request.method == GET or response.preserve_method?
95
- # We (might) need to rewind the body so that it can be submitted again:
96
- body&.rewind
97
- else
98
- # We are changing the method to GET:
99
- request.method = GET
100
-
101
- # Clear the request body:
102
- request.finish
103
- body = nil
104
-
105
- # Remove any headers which are not allowed in a GET request:
106
- PROHIBITED_GET_HEADERS.each do |header|
107
- request.headers.delete(header)
108
- end
109
- end
110
- else
111
- return response
112
- end
113
- end
114
-
115
- raise TooManyRedirects, "Redirected #{hops} times, exceeded maximum!"
116
- end
13
+ module Middleware
14
+ RelativeLocation = Middleware::LocationRedirector
15
+ TooManyRedirects = RelativeLocation::TooManyRedirects
117
16
  end
118
17
  end
119
18
  end
@@ -64,7 +64,7 @@ module Async
64
64
  connection&.close
65
65
  end
66
66
 
67
- # @returns [Array(Async::Task)] The task that is running the server.
67
+ # @returns [Async::Task] The task that is running the server.
68
68
  def run
69
69
  Async do |task|
70
70
  @endpoint.accept(&self.method(:accept))
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module HTTP
8
- VERSION = "0.69.0"
8
+ VERSION = "0.70.0"
9
9
  end
10
10
  end
data/lib/async/http.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2023, by Samuel Williams.
4
+ # Copyright, 2017-2024, by Samuel Williams.
5
5
 
6
6
  require_relative 'http/version'
7
7
 
data/readme.md CHANGED
@@ -22,14 +22,6 @@ We welcome contributions to this project.
22
22
  4. Push to the branch (`git push origin my-new-feature`).
23
23
  5. Create new Pull Request.
24
24
 
25
- ### Developer Certificate of Origin
26
-
27
- This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
28
-
29
- ### Contributor Covenant
30
-
31
- This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
32
-
33
25
  ## See Also
34
26
 
35
27
  - [benchmark-http](https://github.com/socketry/benchmark-http) — A benchmarking tool to report on web server concurrency.
@@ -37,3 +29,11 @@ This project is governed by the [Contributor Covenant](https://www.contributor-c
37
29
  - [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets.
38
30
  - [async-rest](https://github.com/socketry/async-rest) — A RESTful resource layer built on top of `async-http`.
39
31
  - [async-http-faraday](https://github.com/socketry/async-http-faraday) — A faraday adapter to use `async-http`.
32
+
33
+ ### Developer Certificate of Origin
34
+
35
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
36
+
37
+ ### Community Guidelines
38
+
39
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.69.0
4
+ version: 0.70.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -58,7 +58,7 @@ cert_chain:
58
58
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
59
59
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
60
60
  -----END CERTIFICATE-----
61
- date: 2024-06-24 00:00:00.000000000 Z
61
+ date: 2024-08-14 00:00:00.000000000 Z
62
62
  dependencies:
63
63
  - !ruby/object:Gem::Dependency
64
64
  name: async
@@ -122,14 +122,14 @@ dependencies:
122
122
  requirements:
123
123
  - - "~>"
124
124
  - !ruby/object:Gem::Version
125
- version: '0.26'
125
+ version: '0.28'
126
126
  type: :runtime
127
127
  prerelease: false
128
128
  version_requirements: !ruby/object:Gem::Requirement
129
129
  requirements:
130
130
  - - "~>"
131
131
  - !ruby/object:Gem::Version
132
- version: '0.26'
132
+ version: '0.28'
133
133
  - !ruby/object:Gem::Dependency
134
134
  name: protocol-http1
135
135
  requirement: !ruby/object:Gem::Requirement
@@ -191,6 +191,7 @@ files:
191
191
  - lib/async/http/endpoint.rb
192
192
  - lib/async/http/internet.rb
193
193
  - lib/async/http/internet/instance.rb
194
+ - lib/async/http/middleware/location_redirector.rb
194
195
  - lib/async/http/mock.rb
195
196
  - lib/async/http/mock/endpoint.rb
196
197
  - lib/async/http/protocol.rb
metadata.gz.sig CHANGED
Binary file