protocol-http1 0.30.0 → 0.32.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: a8e19e671a7c4596842f4bcc2a8b82e395a24ed3754c8205625747223fd574f5
4
- data.tar.gz: 5a911681a3193eebc65d5a329045d413e2508ef2b20f28a4abe52bf6b09aad57
3
+ metadata.gz: 1908561ca0c3707f9e64cbb12c5014c03858f2655dbb4fab1fe18cd31113272c
4
+ data.tar.gz: 113d3d3b14b2e4258c4bbcb401ff0fefe07921d85edd231c395d0121f9c8c026
5
5
  SHA512:
6
- metadata.gz: a40371b329727872dd04be0a0902e771c7a3231a4262380c84c4abfa96065a73d4088b0f3fc2c0b25b8f1882cd03ba6e888d786018934b1a5230a4c3676fbb54
7
- data.tar.gz: 7fa713254161dad9802b8783c45f290f30993f1957ce64d4d2dd3718e699d8195a6e56dc9991aeba06c2c773be83e672b7f522e093ee3de30187ec8a1b0198bb
6
+ metadata.gz: 47d9919aa52376eb982b92a08dc33038e8fd6f201efee3c6b4da0b9f9b4acf2d7dee5bc6ae3023ab28067ec377ac302fc63d679f7113819e907272b5234b8b93
7
+ data.tar.gz: 47e13c906b80c14b9bb85abfc367daef71392454c1f7d1b2629afa8b1fbbdf51cf0c80d3e1d65599d25e46cc2cac517c00580b7ae7b8a55a09e5fe9521b0534f
checksums.yaml.gz.sig CHANGED
Binary file
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
  # Copyright, 2023, by Thomas Morgan.
6
6
 
7
7
  require "protocol/http/body/readable"
@@ -9,9 +9,16 @@ require "protocol/http/body/readable"
9
9
  module Protocol
10
10
  module HTTP1
11
11
  module Body
12
+ # Represents a chunked body, which is a series of chunks, each with a length prefix.
13
+ #
14
+ # See https://tools.ietf.org/html/rfc7230#section-4.1 for more details on the chunked transfer encoding.
12
15
  class Chunked < HTTP::Body::Readable
13
16
  CRLF = "\r\n"
14
17
 
18
+ # Initialize the chunked body.
19
+ #
20
+ # @parameter connection [Protocol::HTTP1::Connection] the connection to read the body from.
21
+ # @parameter headers [Protocol::HTTP::Headers] the headers to read the trailer into, if any.
15
22
  def initialize(connection, headers)
16
23
  @connection = connection
17
24
  @finished = false
@@ -22,19 +29,25 @@ module Protocol
22
29
  @count = 0
23
30
  end
24
31
 
32
+ # @attribute [Integer] the number of chunks read so far.
25
33
  attr :count
26
34
 
35
+ # @attribute [Integer] the length of the body if known.
27
36
  def length
28
- # We only know the length once we've read everything. This is because the length is not known until the final chunk is read.
37
+ # We only know the length once we've read the final chunk:
29
38
  if @finished
30
39
  @length
31
40
  end
32
41
  end
33
42
 
43
+ # @returns [Boolean] true if the body is empty, in other words {read} will return `nil`.
34
44
  def empty?
35
45
  @connection.nil?
36
46
  end
37
47
 
48
+ # Close the connection and mark the body as finished.
49
+ #
50
+ # @parameter error [Exception | Nil] the error that caused the body to be closed, if any.
38
51
  def close(error = nil)
39
52
  if connection = @connection
40
53
  @connection = nil
@@ -49,7 +62,12 @@ module Protocol
49
62
 
50
63
  VALID_CHUNK_LENGTH = /\A[0-9a-fA-F]+\z/
51
64
 
65
+ # Read a chunk of data.
66
+ #
52
67
  # Follows the procedure outlined in https://tools.ietf.org/html/rfc7230#section-4.1.3
68
+ #
69
+ # @returns [String | Nil] the next chunk of data, or `nil` if the body is finished.
70
+ # @raises [EOFError] if the connection is closed before the expected length is read.
53
71
  def read
54
72
  if !@finished
55
73
  if @connection
@@ -96,12 +114,14 @@ module Protocol
96
114
  end
97
115
  end
98
116
 
117
+ # @returns [String] a human-readable representation of the body.
99
118
  def inspect
100
119
  "\#<#{self.class} #{@length} bytes read in #{@count} chunks>"
101
120
  end
102
121
 
103
122
  private
104
123
 
124
+ # Read the trailer from the connection, and add any headers to the trailer.
105
125
  def read_trailer
106
126
  while line = @connection.read_line?
107
127
  # Empty line indicates end of trailer:
@@ -1,14 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http/body/readable"
7
7
 
8
8
  module Protocol
9
9
  module HTTP1
10
10
  module Body
11
+ # Represents a fixed length body.
11
12
  class Fixed < HTTP::Body::Readable
13
+ # Initialize the body with the given connection and length.
14
+ #
15
+ # @parameter connection [Protocol::HTTP1::Connection] the connection to read the body from.
16
+ # @parameter length [Integer] the length of the body.
12
17
  def initialize(connection, length)
13
18
  @connection = connection
14
19
 
@@ -16,13 +21,20 @@ module Protocol
16
21
  @remaining = length
17
22
  end
18
23
 
24
+ # @attribute [Integer] the length of the body.
19
25
  attr :length
26
+
27
+ # @attribute [Integer] the remaining bytes to read.
20
28
  attr :remaining
21
29
 
30
+ # @returns [Boolean] true if the body is empty.
22
31
  def empty?
23
32
  @connection.nil? or @remaining == 0
24
33
  end
25
34
 
35
+ # Close the connection.
36
+ #
37
+ # @parameter error [Exception | Nil] the error that caused the connection to be closed, if any.
26
38
  def close(error = nil)
27
39
  if connection = @connection
28
40
  @connection = nil
@@ -35,7 +47,10 @@ module Protocol
35
47
  super
36
48
  end
37
49
 
38
- # @raises EOFError if the connection is closed before the expected length is read.
50
+ # Read a chunk of data.
51
+ #
52
+ # @returns [String | Nil] the next chunk of data.
53
+ # @raises [EOFError] if the connection is closed before the expected length is read.
39
54
  def read
40
55
  if @remaining > 0
41
56
  if @connection
@@ -57,6 +72,7 @@ module Protocol
57
72
  end
58
73
  end
59
74
 
75
+ # @returns [String] a human-readable representation of the body.
60
76
  def inspect
61
77
  "\#<#{self.class} length=#{@length} remaining=#{@remaining} state=#{@connection ? 'open' : 'closed'}>"
62
78
  end
@@ -1,26 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http/body/readable"
7
7
 
8
8
  module Protocol
9
9
  module HTTP1
10
10
  module Body
11
- # A body that reads all remaining data from the connection.
11
+ # Represents the remainder of the body, which reads all the data from the connection until it is finished.
12
12
  class Remainder < HTTP::Body::Readable
13
13
  BLOCK_SIZE = 1024 * 64
14
14
 
15
- # block_size may be removed in the future. It is better managed by connection.
16
- def initialize(connection)
15
+ # Initialize the body with the given connection.
16
+ #
17
+ # @parameter connection [Protocol::HTTP1::Connection] the connection to read the body from.
18
+ def initialize(connection, block_size: BLOCK_SIZE)
17
19
  @connection = connection
20
+ @block_size = block_size
18
21
  end
19
22
 
23
+ # @returns [Boolean] true if the body is empty.
20
24
  def empty?
21
25
  @connection.nil?
22
26
  end
23
27
 
28
+ # Discard the body, which will close the connection and prevent further reads.
24
29
  def discard
25
30
  if connection = @connection
26
31
  @connection = nil
@@ -30,14 +35,20 @@ module Protocol
30
35
  end
31
36
  end
32
37
 
38
+ # Close the connection.
39
+ #
40
+ # @parameter error [Exception | Nil] the error that caused the connection to be closed, if any.
33
41
  def close(error = nil)
34
42
  self.discard
35
43
 
36
44
  super
37
45
  end
38
46
 
47
+ # Read a chunk of data.
48
+ #
49
+ # @returns [String | Nil] the next chunk of data.
39
50
  def read
40
- @connection&.readpartial(BLOCK_SIZE)
51
+ @connection&.readpartial(@block_size)
41
52
  rescue EOFError
42
53
  if connection = @connection
43
54
  @connection = nil
@@ -47,6 +58,7 @@ module Protocol
47
58
  return nil
48
59
  end
49
60
 
61
+ # @returns [String] a human-readable representation of the body.
50
62
  def inspect
51
63
  "\#<#{self.class} state=#{@connection ? 'open' : 'closed'}>"
52
64
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "body/chunked"
7
+ require_relative "body/fixed"
8
+ require_relative "body/remainder"
9
+
10
+ module Protocol
11
+ module HTTP1
12
+ # A collection of classes for handling HTTP/1.1 request and response bodies.
13
+ module Body
14
+ end
15
+ end
16
+ end
@@ -11,10 +11,8 @@ require "protocol/http/headers"
11
11
 
12
12
  require_relative "reason"
13
13
  require_relative "error"
14
+ require_relative "body"
14
15
 
15
- require_relative "body/chunked"
16
- require_relative "body/fixed"
17
- require_relative "body/remainder"
18
16
  require "protocol/http/body/head"
19
17
 
20
18
  require "protocol/http/methods"
@@ -39,19 +37,32 @@ module Protocol
39
37
 
40
38
  # HTTP/1.x header parser:
41
39
  FIELD_NAME = TOKEN
42
- FIELD_VALUE = /[^\000-\037]*/.freeze
43
- HEADER = /\A(#{FIELD_NAME}):\s*(#{FIELD_VALUE})\s*\z/.freeze
40
+ WS = /[ \t]/ # Whitespace.
41
+ OWS = /#{WS}*/ # Optional whitespace.
42
+ VCHAR = /[!-~]/ # Match visible characters from ASCII 33 to 126.
43
+ FIELD_VALUE = /#{VCHAR}+(?:#{WS}+#{VCHAR}+)*/.freeze
44
+ HEADER = /\A(#{FIELD_NAME}):#{OWS}(?:(#{FIELD_VALUE})#{OWS})?\z/.freeze
44
45
 
45
46
  VALID_FIELD_NAME = /\A#{FIELD_NAME}\z/.freeze
46
47
  VALID_FIELD_VALUE = /\A#{FIELD_VALUE}\z/.freeze
47
48
 
48
49
  DEFAULT_MAXIMUM_LINE_LENGTH = 8192
49
50
 
51
+ # Represents a single HTTP/1.x connection, which may be used to send and receive multiple requests and responses.
50
52
  class Connection
51
53
  CRLF = "\r\n"
54
+
55
+ # The HTTP/1.0 version string.
52
56
  HTTP10 = "HTTP/1.0"
57
+
58
+ # The HTTP/1.1 version string.
53
59
  HTTP11 = "HTTP/1.1"
54
60
 
61
+ # Initialize the connection with the given stream.
62
+ #
63
+ # @parameter stream [IO] the stream to read and write data from.
64
+ # @parameter persistent [Boolean] whether the connection is persistent.
65
+ # @parameter state [Symbol] the initial state of the connection, typically idle.
55
66
  def initialize(stream, persistent: true, state: :idle, maximum_line_length: DEFAULT_MAXIMUM_LINE_LENGTH)
56
67
  @stream = stream
57
68
 
@@ -63,16 +74,12 @@ module Protocol
63
74
  @maximum_line_length = maximum_line_length
64
75
  end
65
76
 
77
+ # The underlying IO stream.
66
78
  attr :stream
67
79
 
68
- # Whether the connection is persistent.
69
- # This determines what connection headers are sent in the response and whether
70
- # the connection can be reused after the response is sent.
71
- # This setting is automatically managed according to the nature of the request
72
- # and response.
73
- # Changing to false is safe.
74
- # Changing to true from outside this class should generally be avoided and,
75
- # depending on the response semantics, may be reset to false anyway.
80
+ # @attribute [Boolean] true if the connection is persistent.
81
+ #
82
+ # This determines what connection headers are sent in the response and whether the connection can be reused after the response is sent. This setting is automatically managed according to the nature of the request and response. Changing to false is safe. Changing to true from outside this class should generally be avoided and, depending on the response semantics, may be reset to false anyway.
76
83
  attr_accessor :persistent
77
84
 
78
85
  # The current state of the connection.
@@ -114,29 +121,40 @@ module Protocol
114
121
  # State transition methods use a trailing "!".
115
122
  attr_accessor :state
116
123
 
124
+ # @return [Boolean] whether the connection is in the idle state.
117
125
  def idle?
118
126
  @state == :idle
119
127
  end
120
128
 
129
+ # @return [Boolean] whether the connection is in the open state.
121
130
  def open?
122
131
  @state == :open
123
132
  end
124
133
 
134
+ # @return [Boolean] whether the connection is in the half-closed local state.
125
135
  def half_closed_local?
126
136
  @state == :half_closed_local
127
137
  end
128
138
 
139
+ # @return [Boolean] whether the connection is in the half-closed remote state.
129
140
  def half_closed_remote?
130
141
  @state == :half_closed_remote
131
142
  end
132
143
 
144
+ # @return [Boolean] whether the connection is in the closed state.
133
145
  def closed?
134
146
  @state == :closed
135
147
  end
136
148
 
137
- # The number of requests processed.
149
+ # @attribute [Integer] the number of requests and responses processed by this connection.
138
150
  attr :count
139
151
 
152
+ # Indicates whether the connection is persistent given the version, method, and headers.
153
+ #
154
+ # @parameter version [String] the HTTP version.
155
+ # @parameter method [String] the HTTP method.
156
+ # @parameter headers [Hash] the HTTP headers.
157
+ # @return [Boolean] whether the connection can be persistent.
140
158
  def persistent?(version, method, headers)
141
159
  if method == HTTP::Methods::CONNECT
142
160
  return false
@@ -148,7 +166,7 @@ module Protocol
148
166
  else
149
167
  return false
150
168
  end
151
- else
169
+ else # HTTP/1.1+
152
170
  if connection = headers[CONNECTION]
153
171
  return !connection.close?
154
172
  else
@@ -166,19 +184,21 @@ module Protocol
166
184
  end
167
185
  end
168
186
 
187
+ # Write the appropriate header for connection upgrade.
169
188
  def write_upgrade_header(upgrade)
170
189
  @stream.write("connection: upgrade\r\nupgrade: #{upgrade}\r\n")
171
190
  end
172
191
 
173
- # Indicates whether the connection has been hijacked meaning its
174
- # IO has been handed over and is not usable anymore.
175
- # @return [Boolean] hijack status
192
+ # Indicates whether the connection has been hijacked meaning its IO has been handed over and is not usable anymore.
193
+ #
194
+ # @returns [Boolean] hijack status
176
195
  def hijacked?
177
196
  @stream.nil?
178
197
  end
179
198
 
180
- # Effectively close the connection and return the underlying IO.
181
- # @return [IO] the underlying non-blocking IO.
199
+ # Hijack the connection - that is, take over the underlying IO and close the connection.
200
+ #
201
+ # @returns [IO | Nil] the underlying non-blocking IO.
182
202
  def hijack!
183
203
  @persistent = false
184
204
 
@@ -193,13 +213,14 @@ module Protocol
193
213
  end
194
214
  end
195
215
 
216
+ # Close the read end of the connection and transition to the half-closed remote state (or closed if already in the half-closed local state).
196
217
  def close_read
197
218
  @persistent = false
198
219
  @stream&.close_read
199
220
  self.receive_end_stream!
200
221
  end
201
222
 
202
- # Close the connection and underlying stream.
223
+ # Close the connection and underlying stream and transition to the closed state.
203
224
  def close(error = nil)
204
225
  @persistent = false
205
226
 
@@ -214,6 +235,9 @@ module Protocol
214
235
  end
215
236
  end
216
237
 
238
+ # Force a transition to the open state.
239
+ #
240
+ # @raises [ProtocolError] if the connection is not in the idle state.
217
241
  def open!
218
242
  unless @state == :idle
219
243
  raise ProtocolError, "Cannot open connection in state: #{@state}!"
@@ -224,6 +248,16 @@ module Protocol
224
248
  return self
225
249
  end
226
250
 
251
+ # Write a request to the connection. It is expected you will write the body after this method.
252
+ #
253
+ # Transitions to the open state.
254
+ #
255
+ # @parameter authority [String] the authority of the request.
256
+ # @parameter method [String] the HTTP method.
257
+ # @parameter target [String] the request target.
258
+ # @parameter version [String] the HTTP version.
259
+ # @parameter headers [Hash] the HTTP headers.
260
+ # @raises [ProtocolError] if the connection is not in the idle state.
227
261
  def write_request(authority, method, target, version, headers)
228
262
  open!
229
263
 
@@ -233,6 +267,12 @@ module Protocol
233
267
  write_headers(headers)
234
268
  end
235
269
 
270
+ # Write a response to the connection. It is expected you will write the body after this method.
271
+ #
272
+ # @parameter version [String] the HTTP version.
273
+ # @parameter status [Integer] the HTTP status code.
274
+ # @parameter headers [Hash] the HTTP headers.
275
+ # @parameter reason [String] the reason phrase, defaults to the standard reason phrase for the status code.
236
276
  def write_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
237
277
  unless @state == :open or @state == :half_closed_remote
238
278
  raise ProtocolError, "Cannot write response in state: #{@state}!"
@@ -244,6 +284,13 @@ module Protocol
244
284
  write_headers(headers)
245
285
  end
246
286
 
287
+ # Write an interim response to the connection. It is expected you will eventually write the final response after this method.
288
+ #
289
+ # @parameter version [String] the HTTP version.
290
+ # @parameter status [Integer] the HTTP status code.
291
+ # @parameter headers [Hash] the HTTP headers.
292
+ # @parameter reason [String] the reason phrase, defaults to the standard reason phrase for the status code.
293
+ # @raises [ProtocolError] if the connection is not in the open or half-closed remote state.
247
294
  def write_interim_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
248
295
  unless @state == :open or @state == :half_closed_remote
249
296
  raise ProtocolError, "Cannot write interim response in state: #{@state}!"
@@ -257,6 +304,10 @@ module Protocol
257
304
  @stream.flush
258
305
  end
259
306
 
307
+ # Write headers to the connection.
308
+ #
309
+ # @parameter headers [Hash] the headers to write.
310
+ # @raises [BadHeader] if the header name or value is invalid.
260
311
  def write_headers(headers)
261
312
  headers.each do |name, value|
262
313
  # Convert it to a string:
@@ -277,14 +328,25 @@ module Protocol
277
328
  end
278
329
  end
279
330
 
331
+ # Read some data from the connection.
332
+ #
333
+ # @parameter length [Integer] the maximum number of bytes to read.
280
334
  def readpartial(length)
281
335
  @stream.readpartial(length)
282
336
  end
283
337
 
338
+ # Read some data from the connection.
339
+ #
340
+ # @parameter length [Integer] the number of bytes to read.
284
341
  def read(length)
285
342
  @stream.read(length)
286
343
  end
287
344
 
345
+ # Read a line from the connection.
346
+ #
347
+ # @returns [String | Nil] the line read, or nil if the connection is closed.
348
+ # @raises [EOFError] if the connection is closed.
349
+ # @raises [LineLengthError] if the line is too long.
288
350
  def read_line?
289
351
  if line = @stream.gets(CRLF, @maximum_line_length)
290
352
  unless line.chomp!(CRLF)
@@ -296,10 +358,17 @@ module Protocol
296
358
  return line
297
359
  end
298
360
 
361
+ # Read a line from the connection.
362
+ #
363
+ # @raises [EOFError] if a line could not be read.
364
+ # @raises [LineLengthError] if the line is too long.
299
365
  def read_line
300
366
  read_line? or raise EOFError
301
367
  end
302
368
 
369
+ # Read a request line from the connection.
370
+ #
371
+ # @returns [Tuple(String, String, String) | Nil] the method, path, and version of the request, or nil if the connection is closed.
303
372
  def read_request_line
304
373
  return unless line = read_line?
305
374
 
@@ -312,6 +381,13 @@ module Protocol
312
381
  return method, path, version
313
382
  end
314
383
 
384
+ # Read a request from the connection, including the request line and request headers, and prepares to read the request body.
385
+ #
386
+ # Transitions to the open state.
387
+ #
388
+ # @yields {|host, method, path, version, headers, body| ...} if a block is given.
389
+ # @returns [Tuple(String, String, String, String, HTTP::Headers, Protocol::HTTP1::Body) | Nil] the host, method, path, version, headers, and body of the request, or `nil` if the connection is closed.
390
+ # @raises [ProtocolError] if the connection is not in the idle state.
315
391
  def read_request
316
392
  open!
317
393
 
@@ -320,7 +396,11 @@ module Protocol
320
396
 
321
397
  headers = read_headers
322
398
 
323
- @persistent = persistent?(version, method, headers)
399
+ # If we are not persistent, we can't become persistent even if the request might allow it:
400
+ if @persistent
401
+ # In other words, `@persistent` can only transition from true to false.
402
+ @persistent = persistent?(version, method, headers)
403
+ end
324
404
 
325
405
  body = read_request_body(method, headers)
326
406
 
@@ -337,6 +417,10 @@ module Protocol
337
417
  end
338
418
  end
339
419
 
420
+ # Read a response line from the connection.
421
+ #
422
+ # @returns [Tuple(String, Integer, String)] the version, status, and reason of the response.
423
+ # @raises [EOFError] if the connection is closed.
340
424
  def read_response_line
341
425
  version, status, reason = read_line.split(/\s+/, 3)
342
426
 
@@ -345,10 +429,21 @@ module Protocol
345
429
  return version, status, reason
346
430
  end
347
431
 
348
- private def interim_status?(status)
432
+ # Indicates whether the status code is an interim status code.
433
+ #
434
+ # @parameter status [Integer] the status code.
435
+ # @returns [Boolean] whether the status code is an interim status code.
436
+ def interim_status?(status)
349
437
  status != 101 and status >= 100 and status < 200
350
438
  end
351
439
 
440
+ # Read a response from the connection.
441
+ #
442
+ # @parameter method [String] the HTTP method.
443
+ # @yields {|version, status, reason, headers, body| ...} if a block is given.
444
+ # @returns [Tuple(String, Integer, String, HTTP::Headers, Protocol::HTTP1::Body)] the version, status, reason, headers, and body of the response.
445
+ # @raises [ProtocolError] if the connection is not in the open or half-closed local state.
446
+ # @raises [EOFError] if the connection is closed.
352
447
  def read_response(method)
353
448
  unless @state == :open or @state == :half_closed_local
354
449
  raise ProtocolError, "Cannot read response in state: #{@state}!"
@@ -358,7 +453,9 @@ module Protocol
358
453
 
359
454
  headers = read_headers
360
455
 
361
- @persistent = persistent?(version, method, headers)
456
+ if @persistent
457
+ @persistent = persistent?(version, method, headers)
458
+ end
362
459
 
363
460
  unless interim_status?(status)
364
461
  body = read_response_body(method, status, headers)
@@ -377,6 +474,11 @@ module Protocol
377
474
  end
378
475
  end
379
476
 
477
+ # Read headers from the connection until an empty line is encountered.
478
+ #
479
+ # @returns [HTTP::Headers] the headers read.
480
+ # @raises [EOFError] if the connection is closed.
481
+ # @raises [BadHeader] if a header could not be parsed.
380
482
  def read_headers
381
483
  fields = []
382
484
 
@@ -385,7 +487,7 @@ module Protocol
385
487
  break if line.empty?
386
488
 
387
489
  if match = line.match(HEADER)
388
- fields << [match[1], match[2]]
490
+ fields << [match[1], match[2] || ""]
389
491
  else
390
492
  raise BadHeader, "Could not parse header: #{line.inspect}"
391
493
  end
@@ -394,6 +496,11 @@ module Protocol
394
496
  return HTTP::Headers.new(fields)
395
497
  end
396
498
 
499
+ # Transition to the half-closed local state, in other words, the connection is closed for writing.
500
+ #
501
+ # If the connection is already in the half-closed remote state, it will transition to the closed state.
502
+ #
503
+ # @raises [ProtocolError] if the connection is not in the open state.
397
504
  def send_end_stream!
398
505
  if @state == :open
399
506
  @state = :half_closed_local
@@ -404,7 +511,15 @@ module Protocol
404
511
  end
405
512
  end
406
513
 
407
- # @param protocol [String] the protocol to upgrade to.
514
+ # Write an upgrade body to the connection.
515
+ #
516
+ # This writes the upgrade header and the body to the connection. If the body is `nil`, you should coordinate writing to the stream.
517
+ #
518
+ # The connection will not be persistent after this method is called.
519
+ #
520
+ # @parameter protocol [String] the protocol to upgrade to.
521
+ # @parameter body [Object | Nil] the body to write.
522
+ # @returns [IO] the underlying IO stream.
408
523
  def write_upgrade_body(protocol, body = nil)
409
524
  # Once we upgrade the connection, it can no longer handle other requests:
410
525
  @persistent = false
@@ -428,6 +543,15 @@ module Protocol
428
543
  self.send_end_stream!
429
544
  end
430
545
 
546
+ # Write a tunnel body to the connection.
547
+ #
548
+ # This writes the connection header and the body to the connection. If the body is `nil`, you should coordinate writing to the stream.
549
+ #
550
+ # The connection will not be persistent after this method is called.
551
+ #
552
+ # @parameter version [String] the HTTP version.
553
+ # @parameter body [Object | Nil] the body to write.
554
+ # @returns [IO] the underlying IO stream.
431
555
  def write_tunnel_body(version, body = nil)
432
556
  @persistent = false
433
557
 
@@ -450,7 +574,12 @@ module Protocol
450
574
  self.send_end_stream!
451
575
  end
452
576
 
453
- def write_empty_body(body)
577
+ # Write an empty body to the connection.
578
+ #
579
+ # If given, the body will be closed.
580
+ #
581
+ # @parameter body [Object | Nil] the body to write.
582
+ def write_empty_body(body = nil)
454
583
  @stream.write("content-length: 0\r\n\r\n")
455
584
  @stream.flush
456
585
 
@@ -459,6 +588,14 @@ module Protocol
459
588
  self.send_end_stream!
460
589
  end
461
590
 
591
+ # Write a fixed length body to the connection.
592
+ #
593
+ # If the request was a `HEAD` request, the body will be closed, and no data will be written.
594
+ #
595
+ # @parameter body [Object] the body to write.
596
+ # @parameter length [Integer] the length of the body.
597
+ # @parameter head [Boolean] whether the request was a `HEAD` request.
598
+ # @raises [ContentLengthError] if the body length does not match the content length specified.
462
599
  def write_fixed_length_body(body, length, head)
463
600
  @stream.write("content-length: #{length}\r\n\r\n")
464
601
 
@@ -493,6 +630,15 @@ module Protocol
493
630
  self.send_end_stream!
494
631
  end
495
632
 
633
+ # Write a chunked body to the connection.
634
+ #
635
+ # If the request was a `HEAD` request, the body will be closed, and no data will be written.
636
+ #
637
+ # If trailers are given, they will be written after the body.
638
+ #
639
+ # @parameter body [Object] the body to write.
640
+ # @parameter head [Boolean] whether the request was a `HEAD` request.
641
+ # @parameter trailer [Hash | Nil] the trailers to write.
496
642
  def write_chunked_body(body, head, trailer = nil)
497
643
  @stream.write("transfer-encoding: chunked\r\n\r\n")
498
644
 
@@ -529,6 +675,10 @@ module Protocol
529
675
  self.send_end_stream!
530
676
  end
531
677
 
678
+ # Write the body to the connection and close the connection.
679
+ #
680
+ # @parameter body [Object] the body to write.
681
+ # @parameter head [Boolean] whether the request was a `HEAD` request.
532
682
  def write_body_and_close(body, head)
533
683
  # We can't be persistent because we don't know the data length:
534
684
  @persistent = false
@@ -553,6 +703,10 @@ module Protocol
553
703
  end
554
704
 
555
705
  # The connection (stream) was closed. It may now be in the idle state.
706
+ #
707
+ # Sub-classes may override this method to perform additional cleanup.
708
+ #
709
+ # @parameter error [Exception | Nil] the error that caused the connection to be closed, if any.
556
710
  def closed(error = nil)
557
711
  end
558
712
 
@@ -572,6 +726,14 @@ module Protocol
572
726
  self.closed(error)
573
727
  end
574
728
 
729
+ # Write a body to the connection.
730
+ #
731
+ # The behavior of this method is determined by the HTTP version, the body, and the request method. We try to choose the best approach possible, given the constraints, connection persistence, whether the length is known, etc.
732
+ #
733
+ # @parameter version [String] the HTTP version.
734
+ # @parameter body [Object] the body to write.
735
+ # @parameter head [Boolean] whether the request was a `HEAD` request.
736
+ # @parameter trailer [Hash | Nil] the trailers to write.
575
737
  def write_body(version, body, head = false, trailer = nil)
576
738
  # HTTP/1.0 cannot in any case handle trailers.
577
739
  if version == HTTP10 # or te: trailers was not present (strictly speaking not required.)
@@ -603,6 +765,11 @@ module Protocol
603
765
  end
604
766
  end
605
767
 
768
+ # Indicate that the end of the stream (body) has been received.
769
+ #
770
+ # This will transition to the half-closed remote state if the connection is open, or the closed state if the connection is half-closed local.
771
+ #
772
+ # @raises [ProtocolError] if the connection is not in the open or half-closed remote state.
606
773
  def receive_end_stream!
607
774
  if @state == :open
608
775
  @state = :half_closed_remote
@@ -613,19 +780,34 @@ module Protocol
613
780
  end
614
781
  end
615
782
 
783
+ # Read the body, assuming it is using the chunked transfer encoding.
784
+ #
785
+ # @parameters headers [Hash] the headers of the request.
786
+ # @returns [Protocol::HTTP1::Body::Chunked] the body.
616
787
  def read_chunked_body(headers)
617
788
  Body::Chunked.new(self, headers)
618
789
  end
619
790
 
791
+ # Read the body, assuming it has a fixed length.
792
+ #
793
+ # @parameters length [Integer] the length of the body.
794
+ # @returns [Protocol::HTTP1::Body::Fixed] the body.
620
795
  def read_fixed_body(length)
621
796
  Body::Fixed.new(self, length)
622
797
  end
623
798
 
799
+ # Read the body, assuming that we read until the connection is closed.
800
+ #
801
+ # @returns [Protocol::HTTP1::Body::Remainder] the body.
624
802
  def read_remainder_body
625
803
  @persistent = false
626
804
  Body::Remainder.new(self)
627
805
  end
628
806
 
807
+ # Read the body, assuming that we are not receiving any actual data, but just the length.
808
+ #
809
+ # @parameters length [Integer] the length of the body.
810
+ # @returns [Protocol::HTTP::Body::Head] the body.
629
811
  def read_head_body(length)
630
812
  # We are not receiving any body:
631
813
  self.receive_end_stream!
@@ -633,21 +815,41 @@ module Protocol
633
815
  Protocol::HTTP::Body::Head.new(length)
634
816
  end
635
817
 
818
+ # Read the body, assuming it is a tunnel.
819
+ #
820
+ # Invokes {read_remainder_body}.
821
+ #
822
+ # @returns [Protocol::HTTP::Body::Remainder] the body.
636
823
  def read_tunnel_body
637
824
  read_remainder_body
638
825
  end
639
826
 
827
+ # Read the body, assuming it is an upgrade.
828
+ #
829
+ # Invokes {read_remainder_body}.
830
+ #
831
+ # @returns [Protocol::HTTP::Body::Remainder] the body.
640
832
  def read_upgrade_body
641
833
  # When you have an incoming upgrade request body, we must be extremely careful not to start reading it until the upgrade has been confirmed, otherwise if the upgrade was rejected and we started forwarding the incoming request body, it would desynchronize the connection (potential security issue).
642
834
  # We mitigate this issue by setting @persistent to false, which will prevent the connection from being reused, even if the upgrade fails (potential performance issue).
643
835
  read_remainder_body
644
836
  end
645
837
 
838
+ # The HTTP `HEAD` method.
646
839
  HEAD = "HEAD"
840
+
841
+ # The HTTP `CONNECT` method.
647
842
  CONNECT = "CONNECT"
648
843
 
844
+ # The pattern for valid content length values.
649
845
  VALID_CONTENT_LENGTH = /\A\d+\z/
650
846
 
847
+ # Extract the content length from the headers, if possible.
848
+ #
849
+ # @parameter headers [Hash] the headers.
850
+ # @yields {|length| ...} if a content length is found.
851
+ # @parameter length [Integer] the content length.
852
+ # @raises [BadRequest] if the content length is invalid.
651
853
  def extract_content_length(headers)
652
854
  if content_length = headers.delete(CONTENT_LENGTH)
653
855
  if content_length =~ VALID_CONTENT_LENGTH
@@ -658,6 +860,17 @@ module Protocol
658
860
  end
659
861
  end
660
862
 
863
+ # Read the body of the response.
864
+ #
865
+ # - The `HEAD` method is used to retrieve the headers of the response without the body, so {read_head_body} is invoked if there is a content length, otherwise nil is returned.
866
+ # - A 101 status code indicates that the connection will be upgraded, so {read_upgrade_body} is invoked.
867
+ # - Interim status codes (1xx), no content (204) and not modified (304) status codes do not have a body, so nil is returned.
868
+ # - The `CONNECT` method is used to establish a tunnel, so {read_tunnel_body} is invoked.
869
+ # - Otherwise, the body is read according to {read_body}.
870
+ #
871
+ # @parameter method [String] the HTTP method.
872
+ # @parameter status [Integer] the HTTP status code.
873
+ # @parameter headers [Hash] the headers of the response.
661
874
  def read_response_body(method, status, headers)
662
875
  # RFC 7230 3.3.3
663
876
  # 1. Any response to a HEAD request and any response with a 1xx
@@ -698,6 +911,14 @@ module Protocol
698
911
  return read_body(headers, true)
699
912
  end
700
913
 
914
+ # Read the body of the request.
915
+ #
916
+ # - The `CONNECT` method is used to establish a tunnel, so the body is read until the connection is closed.
917
+ # - The `UPGRADE` method is used to upgrade the connection to a different protocol (typically WebSockets), so the body is read until the connection is closed.
918
+ # - Otherwise, the body is read according to {read_body}.
919
+ #
920
+ # @parameter method [String] the HTTP method.
921
+ # @parameter headers [Hash] the headers of the request.
701
922
  def read_request_body(method, headers)
702
923
  # 2. Any 2xx (Successful) response to a CONNECT request implies that
703
924
  # the connection will become a tunnel immediately after the empty
@@ -718,6 +939,15 @@ module Protocol
718
939
  return read_body(headers)
719
940
  end
720
941
 
942
+ # Read the body of the message.
943
+ #
944
+ # - The `transfer-encoding` header is used to determine if the body is chunked.
945
+ # - Otherwise, if the `content-length` is present, the body is read until the content length is reached.
946
+ # - Otherwise, if `remainder` is true, the body is read until the connection is closed.
947
+ #
948
+ # @parameter headers [Hash] the headers of the message.
949
+ # @parameter remainder [Boolean] whether to read the remainder of the body.
950
+ # @returns [Object] the body.
721
951
  def read_body(headers, remainder = false)
722
952
  # 3. If a Transfer-Encoding header field is present and the chunked
723
953
  # transfer coding (Section 4.1) is the final encoding, the message
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http/error"
7
7
 
8
8
  module Protocol
9
9
  module HTTP1
10
+ # The base class for all HTTP/1.x errors.
10
11
  class Error < HTTP::Error
11
12
  end
12
13
 
@@ -14,6 +15,7 @@ module Protocol
14
15
  class ProtocolError < Error
15
16
  end
16
17
 
18
+ # The request line was too long.
17
19
  class LineLengthError < Error
18
20
  end
19
21
 
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http/error"
7
7
 
8
8
  module Protocol
9
9
  module HTTP1
10
+ # Reason phrases for HTTP status codes.
10
11
  module Reason
12
+ # Get the reason phrase for the given status code.
11
13
  DESCRIPTIONS = {
12
14
  100 => "Continue",
13
15
  101 => "Switching Protocols",
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP1
8
- VERSION = "0.30.0"
8
+ VERSION = "0.32.0"
9
9
  end
10
10
  end
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "http1/version"
7
7
  require_relative "http1/connection"
8
+
9
+ # @namespace
10
+ module Protocol
11
+ # @namespace
12
+ module HTTP1
13
+ end
14
+ end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http1
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.0
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -41,7 +41,7 @@ cert_chain:
41
41
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
42
42
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
43
43
  -----END CERTIFICATE-----
44
- date: 2025-01-29 00:00:00.000000000 Z
44
+ date: 2025-03-20 00:00:00.000000000 Z
45
45
  dependencies:
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: protocol-http
@@ -62,6 +62,7 @@ extensions: []
62
62
  extra_rdoc_files: []
63
63
  files:
64
64
  - lib/protocol/http1.rb
65
+ - lib/protocol/http1/body.rb
65
66
  - lib/protocol/http1/body/chunked.rb
66
67
  - lib/protocol/http1/body/fixed.rb
67
68
  - lib/protocol/http1/body/remainder.rb
metadata.gz.sig CHANGED
Binary file