protocol-http1 0.31.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/protocol/http1/body/chunked.rb +22 -2
- data/lib/protocol/http1/body/fixed.rb +18 -2
- data/lib/protocol/http1/body/remainder.rb +17 -5
- data/lib/protocol/http1/body.rb +16 -0
- data/lib/protocol/http1/connection.rb +248 -24
- data/lib/protocol/http1/error.rb +3 -1
- data/lib/protocol/http1/reason.rb +3 -1
- data/lib/protocol/http1/version.rb +1 -1
- data/lib/protocol/http1.rb +8 -1
- data.tar.gz.sig +0 -0
- metadata +3 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1908561ca0c3707f9e64cbb12c5014c03858f2655dbb4fab1fe18cd31113272c
|
4
|
+
data.tar.gz: 113d3d3b14b2e4258c4bbcb401ff0fefe07921d85edd231c395d0121f9c8c026
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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-
|
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
|
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-
|
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
|
-
#
|
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-
|
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
|
-
#
|
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
|
-
#
|
16
|
-
|
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(
|
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
|
-
|
43
|
-
|
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
|
-
#
|
69
|
-
#
|
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
|
-
#
|
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
|
@@ -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
|
-
#
|
175
|
-
# @
|
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
|
-
#
|
181
|
-
#
|
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
|
|
@@ -341,6 +417,10 @@ module Protocol
|
|
341
417
|
end
|
342
418
|
end
|
343
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.
|
344
424
|
def read_response_line
|
345
425
|
version, status, reason = read_line.split(/\s+/, 3)
|
346
426
|
|
@@ -349,10 +429,21 @@ module Protocol
|
|
349
429
|
return version, status, reason
|
350
430
|
end
|
351
431
|
|
352
|
-
|
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)
|
353
437
|
status != 101 and status >= 100 and status < 200
|
354
438
|
end
|
355
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.
|
356
447
|
def read_response(method)
|
357
448
|
unless @state == :open or @state == :half_closed_local
|
358
449
|
raise ProtocolError, "Cannot read response in state: #{@state}!"
|
@@ -383,6 +474,11 @@ module Protocol
|
|
383
474
|
end
|
384
475
|
end
|
385
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.
|
386
482
|
def read_headers
|
387
483
|
fields = []
|
388
484
|
|
@@ -391,7 +487,7 @@ module Protocol
|
|
391
487
|
break if line.empty?
|
392
488
|
|
393
489
|
if match = line.match(HEADER)
|
394
|
-
fields << [match[1], match[2]]
|
490
|
+
fields << [match[1], match[2] || ""]
|
395
491
|
else
|
396
492
|
raise BadHeader, "Could not parse header: #{line.inspect}"
|
397
493
|
end
|
@@ -400,6 +496,11 @@ module Protocol
|
|
400
496
|
return HTTP::Headers.new(fields)
|
401
497
|
end
|
402
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.
|
403
504
|
def send_end_stream!
|
404
505
|
if @state == :open
|
405
506
|
@state = :half_closed_local
|
@@ -410,7 +511,15 @@ module Protocol
|
|
410
511
|
end
|
411
512
|
end
|
412
513
|
|
413
|
-
#
|
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.
|
414
523
|
def write_upgrade_body(protocol, body = nil)
|
415
524
|
# Once we upgrade the connection, it can no longer handle other requests:
|
416
525
|
@persistent = false
|
@@ -434,6 +543,15 @@ module Protocol
|
|
434
543
|
self.send_end_stream!
|
435
544
|
end
|
436
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.
|
437
555
|
def write_tunnel_body(version, body = nil)
|
438
556
|
@persistent = false
|
439
557
|
|
@@ -456,7 +574,12 @@ module Protocol
|
|
456
574
|
self.send_end_stream!
|
457
575
|
end
|
458
576
|
|
459
|
-
|
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)
|
460
583
|
@stream.write("content-length: 0\r\n\r\n")
|
461
584
|
@stream.flush
|
462
585
|
|
@@ -465,6 +588,14 @@ module Protocol
|
|
465
588
|
self.send_end_stream!
|
466
589
|
end
|
467
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.
|
468
599
|
def write_fixed_length_body(body, length, head)
|
469
600
|
@stream.write("content-length: #{length}\r\n\r\n")
|
470
601
|
|
@@ -499,6 +630,15 @@ module Protocol
|
|
499
630
|
self.send_end_stream!
|
500
631
|
end
|
501
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.
|
502
642
|
def write_chunked_body(body, head, trailer = nil)
|
503
643
|
@stream.write("transfer-encoding: chunked\r\n\r\n")
|
504
644
|
|
@@ -535,6 +675,10 @@ module Protocol
|
|
535
675
|
self.send_end_stream!
|
536
676
|
end
|
537
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.
|
538
682
|
def write_body_and_close(body, head)
|
539
683
|
# We can't be persistent because we don't know the data length:
|
540
684
|
@persistent = false
|
@@ -559,6 +703,10 @@ module Protocol
|
|
559
703
|
end
|
560
704
|
|
561
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.
|
562
710
|
def closed(error = nil)
|
563
711
|
end
|
564
712
|
|
@@ -578,6 +726,14 @@ module Protocol
|
|
578
726
|
self.closed(error)
|
579
727
|
end
|
580
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.
|
581
737
|
def write_body(version, body, head = false, trailer = nil)
|
582
738
|
# HTTP/1.0 cannot in any case handle trailers.
|
583
739
|
if version == HTTP10 # or te: trailers was not present (strictly speaking not required.)
|
@@ -609,6 +765,11 @@ module Protocol
|
|
609
765
|
end
|
610
766
|
end
|
611
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.
|
612
773
|
def receive_end_stream!
|
613
774
|
if @state == :open
|
614
775
|
@state = :half_closed_remote
|
@@ -619,19 +780,34 @@ module Protocol
|
|
619
780
|
end
|
620
781
|
end
|
621
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.
|
622
787
|
def read_chunked_body(headers)
|
623
788
|
Body::Chunked.new(self, headers)
|
624
789
|
end
|
625
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.
|
626
795
|
def read_fixed_body(length)
|
627
796
|
Body::Fixed.new(self, length)
|
628
797
|
end
|
629
798
|
|
799
|
+
# Read the body, assuming that we read until the connection is closed.
|
800
|
+
#
|
801
|
+
# @returns [Protocol::HTTP1::Body::Remainder] the body.
|
630
802
|
def read_remainder_body
|
631
803
|
@persistent = false
|
632
804
|
Body::Remainder.new(self)
|
633
805
|
end
|
634
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.
|
635
811
|
def read_head_body(length)
|
636
812
|
# We are not receiving any body:
|
637
813
|
self.receive_end_stream!
|
@@ -639,21 +815,41 @@ module Protocol
|
|
639
815
|
Protocol::HTTP::Body::Head.new(length)
|
640
816
|
end
|
641
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.
|
642
823
|
def read_tunnel_body
|
643
824
|
read_remainder_body
|
644
825
|
end
|
645
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.
|
646
832
|
def read_upgrade_body
|
647
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).
|
648
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).
|
649
835
|
read_remainder_body
|
650
836
|
end
|
651
837
|
|
838
|
+
# The HTTP `HEAD` method.
|
652
839
|
HEAD = "HEAD"
|
840
|
+
|
841
|
+
# The HTTP `CONNECT` method.
|
653
842
|
CONNECT = "CONNECT"
|
654
843
|
|
844
|
+
# The pattern for valid content length values.
|
655
845
|
VALID_CONTENT_LENGTH = /\A\d+\z/
|
656
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.
|
657
853
|
def extract_content_length(headers)
|
658
854
|
if content_length = headers.delete(CONTENT_LENGTH)
|
659
855
|
if content_length =~ VALID_CONTENT_LENGTH
|
@@ -664,6 +860,17 @@ module Protocol
|
|
664
860
|
end
|
665
861
|
end
|
666
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.
|
667
874
|
def read_response_body(method, status, headers)
|
668
875
|
# RFC 7230 3.3.3
|
669
876
|
# 1. Any response to a HEAD request and any response with a 1xx
|
@@ -704,6 +911,14 @@ module Protocol
|
|
704
911
|
return read_body(headers, true)
|
705
912
|
end
|
706
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.
|
707
922
|
def read_request_body(method, headers)
|
708
923
|
# 2. Any 2xx (Successful) response to a CONNECT request implies that
|
709
924
|
# the connection will become a tunnel immediately after the empty
|
@@ -724,6 +939,15 @@ module Protocol
|
|
724
939
|
return read_body(headers)
|
725
940
|
end
|
726
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.
|
727
951
|
def read_body(headers, remainder = false)
|
728
952
|
# 3. If a Transfer-Encoding header field is present and the chunked
|
729
953
|
# transfer coding (Section 4.1) is the final encoding, the message
|
data/lib/protocol/http1/error.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-
|
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-
|
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",
|
data/lib/protocol/http1.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-
|
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.
|
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-03-
|
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
|