protocol-http 0.54.0 → 0.55.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c76db95baeca082a769340ab2a29ee5948bb837894e55039c082653453aa4f4f
4
- data.tar.gz: 868cdcc4a21786c640c8c06e7892bfd6ef19f81670a90274692cfadef10a0c39
3
+ metadata.gz: 4c016217fd5d9ddfac87278d8a629d47ec338d02441d0f27ed25102962e927b9
4
+ data.tar.gz: 51d479e7f25046ca76d08ed60676b7bf4afe5ffa3e234bbb4501478d3fe1db2c
5
5
  SHA512:
6
- metadata.gz: acc4afb3f7caba7ec2d2611ab72dce9c954e88e8dfe72645645e442d8116909c3f228b22c2d460633d429510ffff5f6f4713bfff751f18d4873db167e773fcbe
7
- data.tar.gz: ef0e92bac69dba33417d074c24ce6d2f2009e19e1d81d317eecaf945817f3c7fb0775052048ca24df32b3f39c15f8de83ca45054669e2d89b5bf3b5b445c7cf1
6
+ metadata.gz: a49eb6993da937df85d263f3b505dc700066c796b102c2e4290b9893700fd9f82852bb17f330ef2bf350e11dbf0782e206ae1d29cc4be3fb422d8f6642f1720b
7
+ data.tar.gz: 2680c120c9d72c6ee7c35929984b67e248d26496145dc08700e4a8a5f60568e6dfe2600b5179042d92793d5784ee46e270a59e76f6d272c094c47ccf1c97c0b3
checksums.yaml.gz.sig CHANGED
Binary file
data/context/index.yaml CHANGED
@@ -20,14 +20,6 @@ files:
20
20
  - path: middleware.md
21
21
  title: Middleware
22
22
  description: This guide explains how to build and use HTTP middleware with `Protocol::HTTP::Middleware`.
23
- - path: hypertext-references.md
24
- title: Hypertext References
25
- description: This guide explains how to use `Protocol::HTTP::Reference` for constructing
26
- and manipulating hypertext references (URLs with parameters).
27
- - path: url-parsing.md
28
- title: URL Parsing
29
- description: This guide explains how to use `Protocol::HTTP::URL` for parsing and
30
- manipulating URL components, particularly query strings and parameters.
31
23
  - path: streaming.md
32
24
  title: Streaming
33
25
  description: This guide gives an overview of how to implement streaming requests
@@ -3,6 +3,7 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019-2025, by Samuel Williams.
5
5
  # Copyright, 2023, by Genki Takiuchi.
6
+ # Copyright, 2025, by William T. Nelson.
6
7
 
7
8
  require_relative "buffered"
8
9
 
@@ -315,7 +316,7 @@ module Protocol
315
316
  end
316
317
 
317
318
  # Write data to the stream using {write}.
318
- def <<(buffer)
319
+ def << buffer
319
320
  write(buffer)
320
321
  end
321
322
 
@@ -4,59 +4,66 @@
4
4
  # Copyright, 2019-2025, by Samuel Williams.
5
5
  # Copyright, 2022, by Herrick Fang.
6
6
 
7
- require_relative "url"
7
+ require_relative "quoted_string"
8
8
 
9
9
  module Protocol
10
10
  module HTTP
11
11
  # Represents an individual cookie key-value pair.
12
12
  class Cookie
13
+ # Valid cookie name characters according to RFC 6265.
14
+ # cookie-name = token (RFC 2616 defines token)
15
+ VALID_COOKIE_KEY = /\A#{TOKEN}\z/.freeze
16
+
17
+ # Valid cookie value characters according to RFC 6265.
18
+ # cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
19
+ # cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
20
+ # Excludes control chars, whitespace, DQUOTE, comma, semicolon, and backslash
21
+ VALID_COOKIE_VALUE = /\A[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*\z/.freeze
22
+
13
23
  # Initialize the cookie with the given name, value, and directives.
14
24
  #
15
- # @parameter name [String] The name of the cookiel, e.g. "session_id".
25
+ # @parameter name [String] The name of the cookie, e.g. "session_id".
16
26
  # @parameter value [String] The value of the cookie, e.g. "1234".
17
27
  # @parameter directives [Hash] The directives of the cookie, e.g. `{"path" => "/"}`.
18
- def initialize(name, value, directives)
28
+ # @raises [ArgumentError] If the name or value contains invalid characters.
29
+ def initialize(name, value, directives = nil)
30
+ unless VALID_COOKIE_KEY.match?(name)
31
+ raise ArgumentError, "Invalid cookie name: #{name.inspect}"
32
+ end
33
+
34
+ if value && !VALID_COOKIE_VALUE.match?(value)
35
+ raise ArgumentError, "Invalid cookie value: #{value.inspect}"
36
+ end
37
+
19
38
  @name = name
20
39
  @value = value
21
40
  @directives = directives
22
41
  end
23
42
 
24
43
  # @attribute [String] The name of the cookie.
25
- attr :name
44
+ attr_accessor :name
26
45
 
27
46
  # @attribute [String] The value of the cookie.
28
- attr :value
47
+ attr_accessor :value
29
48
 
30
49
  # @attribute [Hash] The directives of the cookie.
31
- attr :directives
32
-
33
- # Encode the name of the cookie.
34
- def encoded_name
35
- URL.escape(@name)
36
- end
37
-
38
- # Encode the value of the cookie.
39
- def encoded_value
40
- URL.escape(@value)
41
- end
50
+ attr_accessor :directives
42
51
 
43
52
  # Convert the cookie to a string.
44
53
  #
45
54
  # @returns [String] The string representation of the cookie.
46
55
  def to_s
47
- buffer = String.new.b
56
+ buffer = String.new
48
57
 
49
- buffer << encoded_name << "=" << encoded_value
58
+ buffer << @name << "=" << @value
50
59
 
51
60
  if @directives
52
- @directives.collect do |key, value|
61
+ @directives.each do |key, value|
53
62
  buffer << ";"
63
+ buffer << key
54
64
 
55
- case value
56
- when String
57
- buffer << key << "=" << value
58
- when TrueClass
59
- buffer << key
65
+ if value != true
66
+ buffer << "=" << value.to_s
60
67
  end
61
68
  end
62
69
  end
@@ -74,11 +81,7 @@ module Protocol
74
81
  key, value = head.split("=", 2)
75
82
  directives = self.parse_directives(directives)
76
83
 
77
- self.new(
78
- URL.unescape(key),
79
- URL.unescape(value),
80
- directives,
81
- )
84
+ self.new(key, value, directives)
82
85
  end
83
86
 
84
87
  # Parse a list of strings into a hash of directives.
@@ -2,9 +2,10 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2025, by Samuel Williams.
5
+ # Copyright, 2025, by William T. Nelson.
5
6
 
6
7
  require_relative "split"
7
- require_relative "quoted_string"
8
+ require_relative "../quoted_string"
8
9
  require_relative "../error"
9
10
 
10
11
  module Protocol
@@ -81,7 +82,7 @@ module Protocol
81
82
  # The input string is split into distinct entries and appended to the array.
82
83
  #
83
84
  # @parameter value [String] the value or values to add, separated by commas.
84
- def << (value)
85
+ def << value
85
86
  self.concat(value.scan(SEPARATOR).map(&:strip))
86
87
  end
87
88
 
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require_relative "split"
7
- require_relative "quoted_string"
7
+ require_relative "../quoted_string"
8
8
  require_relative "../error"
9
9
 
10
10
  module Protocol
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require_relative "split"
7
- require_relative "quoted_string"
7
+ require_relative "../quoted_string"
8
8
  require_relative "../error"
9
9
 
10
10
  module Protocol
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require_relative "split"
7
- require_relative "quoted_string"
7
+ require_relative "../quoted_string"
8
8
  require_relative "../error"
9
9
 
10
10
  module Protocol
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require_relative "split"
7
- require_relative "quoted_string"
7
+ require_relative "../quoted_string"
8
8
  require_relative "../error"
9
9
 
10
10
  module Protocol
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require_relative "split"
7
- require_relative "quoted_string"
7
+ require_relative "../quoted_string"
8
8
  require_relative "../error"
9
9
 
10
10
  module Protocol
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require_relative "split"
7
- require_relative "quoted_string"
7
+ require_relative "../quoted_string"
8
8
  require_relative "../error"
9
9
 
10
10
  module Protocol
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Protocol
7
+ module HTTP
8
+ # According to https://tools.ietf.org/html/rfc7231#appendix-C
9
+ TOKEN = /[!#$%&'*+\-.^_`|~0-9A-Z]+/i
10
+
11
+ QUOTED_STRING = /"(?:.(?!(?<!\\)"))*.?"/
12
+
13
+ # https://tools.ietf.org/html/rfc7231#section-5.3.1
14
+ QVALUE = /0(\.[0-9]{0,3})?|1(\.[0]{0,3})?/
15
+
16
+ # Handling of HTTP quoted strings.
17
+ module QuotedString
18
+ # Unquote a "quoted-string" value according to <https://tools.ietf.org/html/rfc7230#section-3.2.6>. It should already match the QUOTED_STRING pattern above by the parser.
19
+ def self.unquote(value, normalize_whitespace = true)
20
+ value = value[1...-1]
21
+
22
+ value.gsub!(/\\(.)/, '\1')
23
+
24
+ if normalize_whitespace
25
+ # LWS = [CRLF] 1*( SP | HT )
26
+ value.gsub!(/[\r\n]+\s+/, " ")
27
+ end
28
+
29
+ return value
30
+ end
31
+
32
+ QUOTES_REQUIRED = /[()<>@,;:\\"\/\[\]?={} \t]/
33
+
34
+ # Quote a string for HTTP header values if required.
35
+ #
36
+ # @raises [ArgumentError] if the value contains invalid characters like control characters or newlines.
37
+ def self.quote(value, force = false)
38
+ # Check if quoting is required:
39
+ if value =~ QUOTES_REQUIRED or force
40
+ "\"#{value.gsub(/["\\]/, '\\\\\0')}\""
41
+ else
42
+ value
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP
8
- VERSION = "0.54.0"
8
+ VERSION = "0.55.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -22,10 +22,6 @@ Please see the [project documentation](https://socketry.github.io/protocol-http/
22
22
 
23
23
  - [Middleware](https://socketry.github.io/protocol-http/guides/middleware/index) - This guide explains how to build and use HTTP middleware with `Protocol::HTTP::Middleware`.
24
24
 
25
- - [Hypertext References](https://socketry.github.io/protocol-http/guides/hypertext-references/index) - This guide explains how to use `Protocol::HTTP::Reference` for constructing and manipulating hypertext references (URLs with parameters).
26
-
27
- - [URL Parsing](https://socketry.github.io/protocol-http/guides/url-parsing/index) - This guide explains how to use `Protocol::HTTP::URL` for parsing and manipulating URL components, particularly query strings and parameters.
28
-
29
25
  - [Streaming](https://socketry.github.io/protocol-http/guides/streaming/index) - This guide gives an overview of how to implement streaming requests and responses.
30
26
 
31
27
  - [Design Overview](https://socketry.github.io/protocol-http/guides/design-overview/index) - This guide explains the high level design of `protocol-http` in the context of wider design patterns that can be used to implement HTTP clients and servers.
@@ -34,6 +30,15 @@ Please see the [project documentation](https://socketry.github.io/protocol-http/
34
30
 
35
31
  Please see the [project releases](https://socketry.github.io/protocol-http/releases/index) for all releases.
36
32
 
33
+ ### v0.55.0
34
+
35
+ - **Breaking**: Move `Protocol::HTTP::Header::QuotedString` to `Protocol::HTTP::QuotedString` for better reusability.
36
+ - **Breaking**: Handle cookie key/value pairs using `QuotedString` as per RFC 6265.
37
+ - Don't use URL encoding for cookie key/value.
38
+ - **Breaking**: Remove `Protocol::HTTP::URL` and `Protocol::HTTP::Reference` – replaced by `Protocol::URL` gem.
39
+ - `Protocol::HTTP::URL` -\> `Protocol::URL::Encoding`.
40
+ - `Protocol::HTTP::Reference` -\> `Protocol::URL::Reference`.
41
+
37
42
  ### v0.54.0
38
43
 
39
44
  - Introduce rich support for `Header::Digest`, `Header::ServerTiming`, `Header::TE`, `Header::Trailer` and `Header::TransferEncoding`.
@@ -77,17 +82,13 @@ Please see the [project releases](https://socketry.github.io/protocol-http/relea
77
82
 
78
83
  - Ensure chunks are flushed if required, when streaming.
79
84
 
80
- ### v0.30.0
81
-
82
- - [`Request[]` and `Response[]` Keyword Arguments](https://socketry.github.io/protocol-http/releases/index#request[]-and-response[]-keyword-arguments)
83
- - [Interim Response Handling](https://socketry.github.io/protocol-http/releases/index#interim-response-handling)
84
-
85
85
  ## See Also
86
86
 
87
87
  - [protocol-http1](https://github.com/socketry/protocol-http1) — HTTP/1 client/server implementation using this
88
88
  interface.
89
89
  - [protocol-http2](https://github.com/socketry/protocol-http2) — HTTP/2 client/server implementation using this
90
90
  interface.
91
+ - [protocol-url](https://github.com/socketry/protocol-url) — URL parsing and manipulation library.
91
92
  - [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client and server, supporting multiple HTTP
92
93
  protocols & TLS.
93
94
  - [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server WebSockets.
data/releases.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Releases
2
2
 
3
+ ## v0.55.0
4
+
5
+ - **Breaking**: Move `Protocol::HTTP::Header::QuotedString` to `Protocol::HTTP::QuotedString` for better reusability.
6
+ - **Breaking**: Handle cookie key/value pairs using `QuotedString` as per RFC 6265.
7
+ - Don't use URL encoding for cookie key/value.
8
+ - **Breaking**: Remove `Protocol::HTTP::URL` and `Protocol::HTTP::Reference` – replaced by `Protocol::URL` gem.
9
+ - `Protocol::HTTP::URL` -\> `Protocol::URL::Encoding`.
10
+ - `Protocol::HTTP::Reference` -\> `Protocol::URL::Reference`.
11
+
3
12
  ## v0.54.0
4
13
 
5
14
  - Introduce rich support for `Header::Digest`, `Header::ServerTiming`, `Header::TE`, `Header::Trailer` and `Header::TransferEncoding`.
@@ -125,3 +134,160 @@ def call(request)
125
134
  # ...
126
135
  end
127
136
  ```
137
+
138
+ ## v0.29.0
139
+
140
+ - Introduce `rewind` and `rewindable?` methods for body rewinding capabilities.
141
+ - Add support for output buffer in `read_partial`/`readpartial` methods.
142
+ - `Reader#buffered!` now returns `self` for method chaining.
143
+
144
+ ## v0.28.0
145
+
146
+ - Add convenient `Reader#buffered!` method to buffer the body.
147
+ - Modernize gem infrastructure with RuboCop integration.
148
+
149
+ ## v0.27.0
150
+
151
+ - Expand stream interface to support `gets`/`puts` operations.
152
+ - Skip empty key/value pairs in header processing.
153
+ - Prefer lowercase method names for consistency.
154
+ - Add `as_json` support to avoid default Rails implementation.
155
+ - Use `@callback` to track invocation state.
156
+ - Drop `base64` gem dependency.
157
+
158
+ ## v0.26.0
159
+
160
+ - Prefer connection `close` over `keep-alive` when both are present.
161
+ - Add support for `#readpartial` method.
162
+ - Add `base64` dependency.
163
+
164
+ ## v0.25.0
165
+
166
+ - Introduce explicit support for informational responses (1xx status codes).
167
+ - Add `cache-control` support for `must-revalidate`, `proxy-revalidate`, and `s-maxage` directives.
168
+ - Add `#strong_match?` and `#weak_match?` methods to `ETags` header.
169
+ - Fix `last-modified`, `if-modified-since` and `if-unmodified-since` headers to use proper `Date` parsing.
170
+ - Improve date/expires header parsing.
171
+ - Add tests for `Stream#close_read`.
172
+ - Check if input is closed before raising `IOError`.
173
+ - Ensure saved files truncate existing file by default.
174
+
175
+ ## v0.24.0
176
+
177
+ - Add output stream `#<<` as alias for `#write`.
178
+ - Add support for `Headers#include?` and `#key?` methods.
179
+ - Fix URL unescape functionality.
180
+ - Fix cookie parsing issues.
181
+ - Fix superclass mismatch in `Protocol::HTTP::Middleware::Builder`.
182
+ - Allow trailers without explicit `trailer` header.
183
+ - Fix cookie handling and Ruby 2 keyword arguments.
184
+
185
+ ## v0.23.0
186
+
187
+ - Improve argument handling.
188
+ - Rename `path` parameter to `target` to better match RFCs.
189
+
190
+ ## v0.22.0
191
+
192
+ - Rename `trailers` to `trailer` for consistency.
193
+
194
+ ## v0.21.0
195
+
196
+ - Streaming interface improvements.
197
+ - Rename `Streamable` to `Completable`.
198
+
199
+ ## v0.20.0
200
+
201
+ - Improve `Authorization` header implementation.
202
+
203
+ ## v0.19.0
204
+
205
+ - Expose `Body#ready?` for more efficient response handling.
206
+
207
+ ## v0.18.0
208
+
209
+ - Add `#trailers` method which enumerates trailers without marking tail.
210
+ - Don't clear trailers in `#dup`, move functionality to `flatten!`.
211
+ - All requests and responses must have mutable headers instance.
212
+
213
+ ## v0.17.0
214
+
215
+ - Remove deferred headers due to complexity.
216
+ - Remove deprecated `Headers#slice!`.
217
+ - Add support for static, dynamic and streaming content to `cache-control` model.
218
+ - Initial support for trailers.
219
+ - Add support for `Response#not_modified?`.
220
+
221
+ ## v0.16.0
222
+
223
+ - Add support for `if-match` and `if-none-match` headers.
224
+ - Revert `Request#target` change for HTTP/2 compatibility.
225
+
226
+ ## v0.15.0
227
+
228
+ - Prefer `Request#target` over `Request#path`.
229
+ - Add body implementation to support HEAD requests.
230
+ - Add support for computing digest on buffered body.
231
+ - Add `Headers#set(key, value)` to replace existing values.
232
+ - Add support for `vary` header.
233
+ - Add support for `no-cache` & `no-store` cache directives.
234
+
235
+ ## v0.14.0
236
+
237
+ - Add `Cacheable` body for buffering and caching responses.
238
+ - Add support for `cache-control` header.
239
+
240
+ ## v0.13.0
241
+
242
+ - Add support for `connection` header.
243
+ - Fix handling of keyword arguments.
244
+
245
+ ## v0.12.0
246
+
247
+ - Improved handling of `cookie` header.
248
+ - Add `Headers#clear` method.
249
+
250
+ ## v0.11.0
251
+
252
+ - Ensure `Body#call` invokes `stream.close` when done.
253
+
254
+ ## v0.10.0
255
+
256
+ - Allow user to specify size for character devices.
257
+
258
+ ## v0.9.1
259
+
260
+ - Add support for `authorization` header.
261
+
262
+ ## v0.8.0
263
+
264
+ - Remove `reason` from `Response`.
265
+
266
+ ## v0.7.0
267
+
268
+ - Explicit path handling in `Reference#with`.
269
+
270
+ ## v0.6.0
271
+
272
+ - Initial version with basic HTTP protocol support.
273
+
274
+ ## v0.5.1
275
+
276
+ - Fix path splitting behavior when path is empty.
277
+ - Add `connect` method.
278
+ - Support protocol in `[]` constructor.
279
+ - Incorporate middleware functionality.
280
+
281
+ ## v0.4.0
282
+
283
+ - Add `Request`, `Response` and `Body` classes from `async-http`.
284
+ - Allow deletion of non-existent header fields.
285
+
286
+ ## v0.3.0
287
+
288
+ - **Initial release** of `protocol-http` gem.
289
+ - Initial implementation of HTTP/2 flow control.
290
+ - Support for connection preface and settings frames.
291
+ - Initial headers support.
292
+ - Implementation of `Connection`, `Client` & `Server` classes.
293
+ - HTTP/2 protocol framing and headers.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,20 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.54.0
4
+ version: 0.55.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  - Thomas Morgan
9
9
  - Bruno Sutic
10
10
  - Herrick Fang
11
+ - William T. Nelson
11
12
  - Bryan Powell
12
13
  - Dan Olson
13
14
  - Earlopain
14
15
  - Genki Takiuchi
15
16
  - Marcelo Junior
16
17
  - Olle Jonsson
17
- - William T. Nelson
18
18
  - Yuta Iwama
19
19
  bindir: bin
20
20
  cert_chain:
@@ -96,7 +96,6 @@ files:
96
96
  - lib/protocol/http/header/etags.rb
97
97
  - lib/protocol/http/header/multiple.rb
98
98
  - lib/protocol/http/header/priority.rb
99
- - lib/protocol/http/header/quoted_string.rb
100
99
  - lib/protocol/http/header/server_timing.rb
101
100
  - lib/protocol/http/header/split.rb
102
101
  - lib/protocol/http/header/te.rb
@@ -108,10 +107,9 @@ files:
108
107
  - lib/protocol/http/middleware.rb
109
108
  - lib/protocol/http/middleware/builder.rb
110
109
  - lib/protocol/http/peer.rb
111
- - lib/protocol/http/reference.rb
110
+ - lib/protocol/http/quoted_string.rb
112
111
  - lib/protocol/http/request.rb
113
112
  - lib/protocol/http/response.rb
114
- - lib/protocol/http/url.rb
115
113
  - lib/protocol/http/version.rb
116
114
  - license.md
117
115
  - readme.md
@@ -136,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
134
  - !ruby/object:Gem::Version
137
135
  version: '0'
138
136
  requirements: []
139
- rubygems_version: 3.6.9
137
+ rubygems_version: 3.7.2
140
138
  specification_version: 4
141
139
  summary: Provides abstractions to handle HTTP protocols.
142
140
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
5
-
6
- module Protocol
7
- module HTTP
8
- module Header
9
- # According to https://tools.ietf.org/html/rfc7231#appendix-C
10
- TOKEN = /[!#$%&'*+\-.^_`|~0-9A-Z]+/i
11
-
12
- QUOTED_STRING = /"(?:.(?!(?<!\\)"))*.?"/
13
-
14
- # https://tools.ietf.org/html/rfc7231#section-5.3.1
15
- QVALUE = /0(\.[0-9]{0,3})?|1(\.[0]{0,3})?/
16
-
17
- # Handling of HTTP quoted strings.
18
- module QuotedString
19
- # Unquote a "quoted-string" value according to <https://tools.ietf.org/html/rfc7230#section-3.2.6>. It should already match the QUOTED_STRING pattern above by the parser.
20
- def self.unquote(value, normalize_whitespace = true)
21
- value = value[1...-1]
22
-
23
- value.gsub!(/\\(.)/, '\1')
24
-
25
- if normalize_whitespace
26
- # LWS = [CRLF] 1*( SP | HT )
27
- value.gsub!(/[\r\n]+\s+/, " ")
28
- end
29
-
30
- return value
31
- end
32
-
33
- QUOTES_REQUIRED = /[()<>@,;:\\"\/\[\]?={} \t]/
34
-
35
- # Quote a string for HTTP header values if required.
36
- #
37
- # @raises [ArgumentError] if the value contains invalid characters like control characters or newlines.
38
- def self.quote(value, force = false)
39
- # Check if quoting is required:
40
- if value =~ QUOTES_REQUIRED or force
41
- "\"#{value.gsub(/["\\]/, '\\\\\0')}\""
42
- else
43
- value
44
- end
45
- end
46
- end
47
- end
48
- end
49
- end
@@ -1,253 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2018-2025, by Samuel Williams.
5
-
6
- require_relative "url"
7
-
8
- module Protocol
9
- module HTTP
10
- # A relative reference, excluding any authority. The path part of an HTTP request.
11
- class Reference
12
- include Comparable
13
-
14
- # Generate a reference from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`.
15
- def self.parse(path = "/", parameters = nil)
16
- base, fragment = path.split("#", 2)
17
- path, query = base.split("?", 2)
18
-
19
- self.new(path, query, fragment, parameters)
20
- end
21
-
22
- # Initialize the reference.
23
- #
24
- # @parameter path [String] The path component, e.g. `/foo/bar/index.html`.
25
- # @parameter query [String | Nil] The un-parsed query string, e.g. 'x=10&y=20'.
26
- # @parameter fragment [String | Nil] The fragment, the part after the '#'.
27
- # @parameter parameters [Hash | Nil] User supplied parameters that will be appended to the query part.
28
- def initialize(path = "/", query = nil, fragment = nil, parameters = nil)
29
- @path = path
30
- @query = query
31
- @fragment = fragment
32
- @parameters = parameters
33
- end
34
-
35
- # @attribute [String] The path component, e.g. `/foo/bar/index.html`.
36
- attr_accessor :path
37
-
38
- # @attribute [String] The un-parsed query string, e.g. 'x=10&y=20'.
39
- attr_accessor :query
40
-
41
- # @attribute [String] The fragment, the part after the '#'.
42
- attr_accessor :fragment
43
-
44
- # @attribute [Hash] User supplied parameters that will be appended to the query part.
45
- attr_accessor :parameters
46
-
47
- # Freeze the reference.
48
- #
49
- # @returns [Reference] The frozen reference.
50
- def freeze
51
- return self if frozen?
52
-
53
- @path.freeze
54
- @query.freeze
55
- @fragment.freeze
56
- @parameters.freeze
57
-
58
- super
59
- end
60
-
61
- # Implicit conversion to an array.
62
- #
63
- # @returns [Array] The reference as an array, `[path, query, fragment, parameters]`.
64
- def to_ary
65
- [@path, @query, @fragment, @parameters]
66
- end
67
-
68
- # Compare two references.
69
- #
70
- # @parameter other [Reference] The other reference to compare.
71
- # @returns [Integer] -1, 0, 1 if the reference is less than, equal to, or greater than the other reference.
72
- def <=> other
73
- to_ary <=> other.to_ary
74
- end
75
-
76
- # Type-cast a reference.
77
- #
78
- # @parameter reference [Reference | String] The reference to type-cast.
79
- # @returns [Reference] The type-casted reference.
80
- def self.[] reference
81
- if reference.is_a? self
82
- return reference
83
- else
84
- return self.parse(reference)
85
- end
86
- end
87
-
88
- # @returns [Boolean] Whether the reference has parameters.
89
- def parameters?
90
- @parameters and !@parameters.empty?
91
- end
92
-
93
- # @returns [Boolean] Whether the reference has a query string.
94
- def query?
95
- @query and !@query.empty?
96
- end
97
-
98
- # @returns [Boolean] Whether the reference has a fragment.
99
- def fragment?
100
- @fragment and !@fragment.empty?
101
- end
102
-
103
- # Append the reference to the given buffer.
104
- def append(buffer = String.new)
105
- if query?
106
- buffer << URL.escape_path(@path) << "?" << @query
107
- buffer << "&" << URL.encode(@parameters) if parameters?
108
- else
109
- buffer << URL.escape_path(@path)
110
- buffer << "?" << URL.encode(@parameters) if parameters?
111
- end
112
-
113
- if fragment?
114
- buffer << "#" << URL.escape(@fragment)
115
- end
116
-
117
- return buffer
118
- end
119
-
120
- # Convert the reference to a string, e.g. `/foo/bar/index.html?x=10&y=20#section`
121
- #
122
- # @returns [String] The reference as a string.
123
- def to_s
124
- append
125
- end
126
-
127
- # Merges two references as specified by RFC2396, similar to `URI.join`.
128
- def + other
129
- other = self.class[other]
130
-
131
- self.class.new(
132
- expand_path(self.path, other.path, true),
133
- other.query,
134
- other.fragment,
135
- other.parameters,
136
- )
137
- end
138
-
139
- # Just the base path, without any query string, parameters or fragment.
140
- def base
141
- self.class.new(@path, nil, nil, nil)
142
- end
143
-
144
- # Update the reference with the given path, parameters and fragment.
145
- #
146
- # @parameter path [String] Append the string to this reference similar to `File.join`.
147
- # @parameter parameters [Hash] Append the parameters to this reference.
148
- # @parameter fragment [String] Set the fragment to this value.
149
- # @parameter pop [Boolean] If the path contains a trailing filename, pop the last component of the path before appending the new path.
150
- # @parameter merge [Boolean] If the parameters are specified, merge them with the existing parameters, otherwise replace them (including query string).
151
- def with(path: nil, parameters: false, fragment: @fragment, pop: false, merge: true)
152
- if merge
153
- # Merge mode: combine new parameters with existing, keep query:
154
- # parameters = (@parameters || {}).merge(parameters || {})
155
- if @parameters
156
- if parameters
157
- parameters = @parameters.merge(parameters)
158
- else
159
- parameters = @parameters
160
- end
161
- elsif !parameters
162
- parameters = @parameters
163
- end
164
-
165
- query = @query
166
- else
167
- # Replace mode: use new parameters if provided, clear query when replacing:
168
- if parameters == false
169
- # No new parameters provided, keep existing:
170
- parameters = @parameters
171
- query = @query
172
- else
173
- # New parameters provided, replace and clear query:
174
- # parameters = parameters
175
- query = nil
176
- end
177
- end
178
-
179
- if path
180
- path = expand_path(@path, path, pop)
181
- else
182
- path = @path
183
- end
184
-
185
- self.class.new(path, query, fragment, parameters)
186
- end
187
-
188
- private
189
-
190
- def split(path)
191
- if path.empty?
192
- [path]
193
- else
194
- path.split("/", -1)
195
- end
196
- end
197
-
198
- def expand_absolute_path(path, parts)
199
- parts.each do |part|
200
- if part == ".."
201
- path.pop
202
- elsif part == "."
203
- # Do nothing.
204
- else
205
- path << part
206
- end
207
- end
208
-
209
- if path.first != ""
210
- path.unshift("")
211
- end
212
- end
213
-
214
- def expand_relative_path(path, parts)
215
- parts.each do |part|
216
- if part == ".." and path.any?
217
- path.pop
218
- elsif part == "."
219
- # Do nothing.
220
- else
221
- path << part
222
- end
223
- end
224
- end
225
-
226
- # @param pop [Boolean] whether to remove the last path component of the base path, to conform to URI merging behaviour, as defined by RFC2396.
227
- def expand_path(base, relative, pop = true)
228
- if relative.start_with? "/"
229
- return relative
230
- end
231
-
232
- path = split(base)
233
-
234
- # RFC2396 Section 5.2:
235
- # 6) a) All but the last segment of the base URI's path component is
236
- # copied to the buffer. In other words, any characters after the
237
- # last (right-most) slash character, if any, are excluded.
238
- path.pop if pop or path.last == ""
239
-
240
- parts = split(relative)
241
-
242
- # Absolute path:
243
- if path.first == ""
244
- expand_absolute_path(path, parts)
245
- else
246
- expand_relative_path(path, parts)
247
- end
248
-
249
- return path.join("/")
250
- end
251
- end
252
- end
253
- end
@@ -1,149 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
5
- # Copyright, 2022, by Herrick Fang.
6
-
7
- module Protocol
8
- module HTTP
9
- # Helpers for working with URLs.
10
- module URL
11
- # Escapes a string using percent encoding, e.g. `a b` -> `a%20b`.
12
- #
13
- # @parameter string [String] The string to escape.
14
- # @returns [String] The escaped string.
15
- def self.escape(string, encoding = string.encoding)
16
- string.b.gsub(/([^a-zA-Z0-9_.\-]+)/) do |m|
17
- "%" + m.unpack("H2" * m.bytesize).join("%").upcase
18
- end.force_encoding(encoding)
19
- end
20
-
21
- # Unescapes a percent encoded string, e.g. `a%20b` -> `a b`.
22
- #
23
- # @parameter string [String] The string to unescape.
24
- # @returns [String] The unescaped string.
25
- def self.unescape(string, encoding = string.encoding)
26
- string.b.gsub(/%(\h\h)/) do |hex|
27
- Integer($1, 16).chr
28
- end.force_encoding(encoding)
29
- end
30
-
31
- # Matches characters that are not allowed in a URI path segment. According to RFC 3986 Section 3.3 (https://tools.ietf.org/html/rfc3986#section-3.3), a valid path segment consists of "pchar" characters. This pattern identifies characters that must be percent-encoded when included in a URI path segment.
32
- NON_PATH_CHARACTER_PATTERN = /([^a-zA-Z0-9_\-\.~!$&'()*+,;=:@\/]+)/.freeze
33
-
34
- # Escapes non-path characters using percent encoding. In other words, this method escapes characters that are not allowed in a URI path segment. According to RFC 3986 Section 3.3 (https://tools.ietf.org/html/rfc3986#section-3.3), a valid path segment consists of "pchar" characters. This method percent-encodes characters that are not "pchar" characters.
35
- #
36
- # @parameter path [String] The path to escape.
37
- # @returns [String] The escaped path.
38
- def self.escape_path(path)
39
- encoding = path.encoding
40
- path.b.gsub(NON_PATH_CHARACTER_PATTERN) do |m|
41
- "%" + m.unpack("H2" * m.bytesize).join("%").upcase
42
- end.force_encoding(encoding)
43
- end
44
-
45
- # Encodes a hash or array into a query string. This method is used to encode query parameters in a URL. For example, `{"a" => 1, "b" => 2}` is encoded as `a=1&b=2`.
46
- #
47
- # @parameter value [Hash | Array | Nil] The value to encode.
48
- # @parameter prefix [String] The prefix to use for keys.
49
- def self.encode(value, prefix = nil)
50
- case value
51
- when Array
52
- return value.map {|v|
53
- self.encode(v, "#{prefix}[]")
54
- }.join("&")
55
- when Hash
56
- return value.map {|k, v|
57
- self.encode(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s))
58
- }.reject(&:empty?).join("&")
59
- when nil
60
- return prefix
61
- else
62
- raise ArgumentError, "value must be a Hash" if prefix.nil?
63
-
64
- return "#{prefix}=#{escape(value.to_s)}"
65
- end
66
- end
67
-
68
- # Scan a string for URL-encoded key/value pairs.
69
- # @yields {|key, value| ...}
70
- # @parameter key [String] The unescaped key.
71
- # @parameter value [String] The unescaped key.
72
- def self.scan(string)
73
- string.split("&") do |assignment|
74
- next if assignment.empty?
75
-
76
- key, value = assignment.split("=", 2)
77
-
78
- yield unescape(key), value.nil? ? value : unescape(value)
79
- end
80
- end
81
-
82
- # Split a key into parts, e.g. `a[b][c]` -> `["a", "b", "c"]`.
83
- #
84
- # @parameter name [String] The key to split.
85
- # @returns [Array(String)] The parts of the key.
86
- def self.split(name)
87
- name.scan(/([^\[]+)|(?:\[(.*?)\])/)&.tap do |parts|
88
- parts.flatten!
89
- parts.compact!
90
- end
91
- end
92
-
93
- # Assign a value to a nested hash.
94
- #
95
- # @parameter keys [Array(String)] The parts of the key.
96
- # @parameter value [Object] The value to assign.
97
- # @parameter parent [Hash] The parent hash.
98
- def self.assign(keys, value, parent)
99
- top, *middle = keys
100
-
101
- middle.each_with_index do |key, index|
102
- if key.nil? or key.empty?
103
- parent = (parent[top] ||= Array.new)
104
- top = parent.size
105
-
106
- if nested = middle[index+1] and last = parent.last
107
- top -= 1 unless last.include?(nested)
108
- end
109
- else
110
- parent = (parent[top] ||= Hash.new)
111
- top = key
112
- end
113
- end
114
-
115
- parent[top] = value
116
- end
117
-
118
- # Decode a URL-encoded query string into a hash.
119
- #
120
- # @parameter string [String] The query string to decode.
121
- # @parameter maximum [Integer] The maximum number of keys in a path.
122
- # @parameter symbolize_keys [Boolean] Whether to symbolize keys.
123
- # @returns [Hash] The decoded query string.
124
- def self.decode(string, maximum = 8, symbolize_keys: false)
125
- parameters = {}
126
-
127
- self.scan(string) do |name, value|
128
- keys = self.split(name)
129
-
130
- if keys.empty?
131
- raise ArgumentError, "Invalid key path: #{name.inspect}!"
132
- end
133
-
134
- if keys.size > maximum
135
- raise ArgumentError, "Key length exceeded limit!"
136
- end
137
-
138
- if symbolize_keys
139
- keys.collect!{|key| key.empty? ? nil : key.to_sym}
140
- end
141
-
142
- self.assign(keys, value, parameters)
143
- end
144
-
145
- return parameters
146
- end
147
- end
148
- end
149
- end