protocol-http1 0.23.0 → 0.24.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: 5883f7878058a83c35d40fe91ea7e673a6abc1895d47e91dfefd57e1849861d5
4
- data.tar.gz: f01fbad8aa91e32dddd541fa6a3ab184d8204a5761b6a0b2d5bd7eafd5aab717
3
+ metadata.gz: '0854252b3602c1271818e28024b62e1a4373521052b75ba0aa879a1f77a6e84e'
4
+ data.tar.gz: 2ad1603fc177c4b8493f304806a1f8baf4dc147b0f1707d463f534fb1e740a62
5
5
  SHA512:
6
- metadata.gz: de91d508479206879dfded7ef4aca6ec5b211372ae8aa9d938afb97c4cb5d1804e9aefbea42d021979ce3c67ad6cf96c28f661be62e6c191219204f9ba96267e
7
- data.tar.gz: 3c556ecfd10f3b20424629898b4684e6f8d56ad7415c94e3fedcac7485d5fb389141fd2fb9a9bbadd67a20d05a51286e338fc656febe145551b491781c052776
6
+ metadata.gz: 213525d129854f2198cb1be6be3d99a74d7add2e4a8dc57ed72d3bd1854b29535256120dfcbd735f1823d91bb6bf27fee6571d93d8e85f0031a404be99b39e01
7
+ data.tar.gz: 32f7a63f81f830a77839fa154b69bfa5a8dc316d579cc48e10d2a786e92806960c36821b3f53da67c3b1e90ba29394264ae76c621d7608c62a0d56224dabec1f
checksums.yaml.gz.sig CHANGED
Binary file
@@ -12,8 +12,8 @@ module Protocol
12
12
  class Chunked < HTTP::Body::Readable
13
13
  CRLF = "\r\n"
14
14
 
15
- def initialize(stream, headers)
16
- @stream = stream
15
+ def initialize(connection, headers)
16
+ @connection = connection
17
17
  @finished = false
18
18
 
19
19
  @headers = headers
@@ -23,16 +23,16 @@ module Protocol
23
23
  end
24
24
 
25
25
  def empty?
26
- @stream.nil?
26
+ @connection.nil?
27
27
  end
28
28
 
29
29
  def discard
30
- if stream = @stream
31
- @stream = nil
30
+ if connection = @connection
31
+ @connection = nil
32
32
 
33
33
  # We only close the connection if we haven't completed reading the entire body:
34
34
  unless @finished
35
- stream.close_read
35
+ connection.close_read
36
36
  end
37
37
  end
38
38
  end
@@ -48,8 +48,8 @@ module Protocol
48
48
  # Follows the procedure outlined in https://tools.ietf.org/html/rfc7230#section-4.1.3
49
49
  def read
50
50
  if !@finished
51
- if @stream
52
- length, _extensions = read_line.split(";", 2)
51
+ if @connection
52
+ length, _extensions = @connection.read_line.split(";", 2)
53
53
 
54
54
  unless length =~ VALID_CHUNK_LENGTH
55
55
  raise BadRequest, "Invalid chunk length: #{length.inspect}"
@@ -61,15 +61,16 @@ module Protocol
61
61
  if length == 0
62
62
  read_trailer
63
63
 
64
- # The final chunk has been read and the stream is now closed:
65
- @stream = nil
64
+ # The final chunk has been read and the connection is now closed:
65
+ @connection.receive_end_stream!
66
+ @connection = nil
66
67
  @finished = true
67
68
 
68
69
  return nil
69
70
  end
70
71
 
71
72
  # Read trailing CRLF:
72
- chunk = @stream.read(length + 2)
73
+ chunk = @connection.read(length + 2)
73
74
 
74
75
  if chunk.bytesize == length + 2
75
76
  # ...and chomp it off:
@@ -80,13 +81,13 @@ module Protocol
80
81
 
81
82
  return chunk
82
83
  else
83
- # The stream has been closed before we have read the requested length:
84
+ # The connection has been closed before we have read the requested length:
84
85
  self.discard
85
86
  end
86
87
  end
87
88
 
88
- # If the stream has been closed before we have read the final chunk, raise an error:
89
- raise EOFError, "Stream closed before expected length was read!"
89
+ # If the connection has been closed before we have read the final chunk, raise an error:
90
+ raise EOFError, "connection closed before expected length was read!"
90
91
  end
91
92
  end
92
93
 
@@ -96,16 +97,8 @@ module Protocol
96
97
 
97
98
  private
98
99
 
99
- def read_line?
100
- @stream.gets(CRLF, chomp: true)
101
- end
102
-
103
- def read_line
104
- read_line? or raise EOFError
105
- end
106
-
107
100
  def read_trailer
108
- while line = read_line?
101
+ while line = @connection.read_line?
109
102
  # Empty line indicates end of trailer:
110
103
  break if line.empty?
111
104
 
@@ -9,8 +9,8 @@ module Protocol
9
9
  module HTTP1
10
10
  module Body
11
11
  class Fixed < HTTP::Body::Readable
12
- def initialize(stream, length)
13
- @stream = stream
12
+ def initialize(connection, length)
13
+ @connection = connection
14
14
 
15
15
  @length = length
16
16
  @remaining = length
@@ -20,15 +20,15 @@ module Protocol
20
20
  attr :remaining
21
21
 
22
22
  def empty?
23
- @stream.nil? or @remaining == 0
23
+ @connection.nil? or @remaining == 0
24
24
  end
25
25
 
26
26
  def discard
27
- if stream = @stream
28
- @stream = nil
27
+ if connection = @connection
28
+ @connection = nil
29
29
 
30
30
  if @remaining != 0
31
- stream.close_read
31
+ connection.close_read
32
32
  end
33
33
  end
34
34
  end
@@ -39,25 +39,30 @@ module Protocol
39
39
  super
40
40
  end
41
41
 
42
- # @raises EOFError if the stream is closed before the expected length is read.
42
+ # @raises EOFError if the connection is closed before the expected length is read.
43
43
  def read
44
44
  if @remaining > 0
45
- if @stream
46
- # `readpartial` will raise `EOFError` if the stream is finished, or `IOError` if the stream is closed.
47
- chunk = @stream.readpartial(@remaining)
45
+ if @connection
46
+ # `readpartial` will raise `EOFError` if the connection is finished, or `IOError` if the connection is closed.
47
+ chunk = @connection.readpartial(@remaining)
48
48
 
49
49
  @remaining -= chunk.bytesize
50
50
 
51
+ if @remaining == 0
52
+ @connection.receive_end_stream!
53
+ @connection = nil
54
+ end
55
+
51
56
  return chunk
52
57
  end
53
58
 
54
- # If the stream has been closed before we have read the expected length, raise an error:
55
- raise EOFError, "Stream closed before expected length was read!"
59
+ # If the connection has been closed before we have read the expected length, raise an error:
60
+ raise EOFError, "connection closed before expected length was read!"
56
61
  end
57
62
  end
58
63
 
59
64
  def inspect
60
- "\#<#{self.class} length=#{@length} remaining=#{@remaining} state=#{@stream ? 'open' : 'closed'}>"
65
+ "\#<#{self.class} length=#{@length} remaining=#{@remaining} state=#{@connection ? 'open' : 'closed'}>"
61
66
  end
62
67
  end
63
68
  end
@@ -8,23 +8,25 @@ require "protocol/http/body/readable"
8
8
  module Protocol
9
9
  module HTTP1
10
10
  module Body
11
- # A body that reads all remaining data from the stream.
11
+ # A body that reads all remaining data from the connection.
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 stream.
16
- def initialize(stream)
17
- @stream = stream
15
+ # block_size may be removed in the future. It is better managed by connection.
16
+ def initialize(connection)
17
+ @connection = connection
18
18
  end
19
19
 
20
20
  def empty?
21
- @stream.nil?
21
+ @connection.nil?
22
22
  end
23
23
 
24
24
  def discard
25
- if stream = @stream
26
- @stream = nil
27
- stream.close_read
25
+ if connection = @connection
26
+ @connection = nil
27
+
28
+ # Ensure no further requests can be read from the connection, as we are discarding the body which may not be fully read:
29
+ connection.close_read
28
30
  end
29
31
  end
30
32
 
@@ -35,15 +37,14 @@ module Protocol
35
37
  end
36
38
 
37
39
  def read
38
- @stream&.readpartial(BLOCK_SIZE)
39
- rescue EOFError, IOError
40
- @stream = nil
41
- # I noticed that in some cases you will get EOFError, and in other cases IOError!?
42
- return nil
40
+ @connection&.readpartial(BLOCK_SIZE)
41
+ rescue EOFError
42
+ @connection.receive_end_stream!
43
+ @connection = nil
43
44
  end
44
45
 
45
46
  def inspect
46
- "\#<#{self.class} state=#{@stream ? 'open' : 'closed'}>"
47
+ "\#<#{self.class} state=#{@connection ? 'open' : 'closed'}>"
47
48
  end
48
49
  end
49
50
  end
@@ -50,10 +50,11 @@ module Protocol
50
50
  HTTP10 = "HTTP/1.0"
51
51
  HTTP11 = "HTTP/1.1"
52
52
 
53
- def initialize(stream, persistent = true)
53
+ def initialize(stream, persistent: true, state: :idle)
54
54
  @stream = stream
55
55
 
56
56
  @persistent = persistent
57
+ @state = state
57
58
 
58
59
  @count = 0
59
60
  end
@@ -70,6 +71,64 @@ module Protocol
70
71
  # depending on the response semantics, may be reset to false anyway.
71
72
  attr_accessor :persistent
72
73
 
74
+ # The current state of the connection.
75
+ #
76
+ # ```
77
+ # ┌────────┐
78
+ # │ │
79
+ # ┌───────────────────────►│ idle │
80
+ # │ │ │
81
+ # │ └───┬────┘
82
+ # │ │
83
+ # │ │ send request /
84
+ # │ │ receive request
85
+ # │ │
86
+ # │ ▼
87
+ # │ ┌────────┐
88
+ # │ recv ES │ │ send ES
89
+ # │ ┌────────────┤ open ├────────────┐
90
+ # │ │ │ │ │
91
+ # │ ▼ └───┬────┘ ▼
92
+ # │ ┌──────────┐ │ ┌──────────┐
93
+ # │ │ half │ │ │ half │
94
+ # │ │ closed │ │ │ closed │
95
+ # │ │ (remote) │ │ │ (local) │
96
+ # │ └────┬─────┘ │ └─────┬────┘
97
+ # │ │ │ │
98
+ # │ │ send ES / │ recv ES / │
99
+ # │ │ close ▼ close │
100
+ # │ │ ┌────────┐ │
101
+ # │ └───────────►│ │◄───────────┘
102
+ # │ │ closed │
103
+ # └────────────────────────┤ │
104
+ # persistent └────────┘
105
+ # ```
106
+ #
107
+ # - `ES`: the body was fully received or sent (end of stream)
108
+ #
109
+ # State transition methods use a trailing "!".
110
+ attr_accessor :state
111
+
112
+ def idle?
113
+ @state == :idle
114
+ end
115
+
116
+ def open?
117
+ @state == :open
118
+ end
119
+
120
+ def half_closed_local?
121
+ @state == :half_closed_local
122
+ end
123
+
124
+ def half_closed_remote?
125
+ @state == :half_closed_remote
126
+ end
127
+
128
+ def closed?
129
+ @state == :closed
130
+ end
131
+
73
132
  # The number of requests processed.
74
133
  attr :count
75
134
 
@@ -130,7 +189,15 @@ module Protocol
130
189
  @stream&.close
131
190
  end
132
191
 
192
+ def open!
193
+ raise ProtocolError, "Cannot write request in #{@state}!" unless @state == :idle
194
+
195
+ @state = :open
196
+ end
197
+
133
198
  def write_request(authority, method, path, version, headers)
199
+ open!
200
+
134
201
  @stream.write("#{method} #{path} #{version}\r\n")
135
202
  @stream.write("host: #{authority}\r\n")
136
203
 
@@ -138,6 +205,10 @@ module Protocol
138
205
  end
139
206
 
140
207
  def write_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
208
+ unless @state == :open or @state == :half_closed_remote
209
+ raise ProtocolError, "Cannot write response in #{@state}!"
210
+ end
211
+
141
212
  # Safari WebSockets break if no reason is given:
142
213
  @stream.write("#{version} #{status} #{reason}\r\n")
143
214
 
@@ -145,6 +216,10 @@ module Protocol
145
216
  end
146
217
 
147
218
  def write_interim_response(version, status, headers, reason = Reason::DESCRIPTIONS[status])
219
+ unless @state == :open or @state == :half_closed_remote
220
+ raise ProtocolError, "Cannot write interim response in #{@state}!"
221
+ end
222
+
148
223
  @stream.write("#{version} #{status} #{reason}\r\n")
149
224
 
150
225
  write_headers(headers)
@@ -173,6 +248,14 @@ module Protocol
173
248
  end
174
249
  end
175
250
 
251
+ def readpartial(length)
252
+ @stream.readpartial(length)
253
+ end
254
+
255
+ def read(length)
256
+ @stream.read(length)
257
+ end
258
+
176
259
  def read_line?
177
260
  @stream.gets(CRLF, chomp: true)
178
261
  end
@@ -181,6 +264,10 @@ module Protocol
181
264
  read_line? or raise EOFError
182
265
  end
183
266
 
267
+ def close_read
268
+ @stream.close_read
269
+ end
270
+
184
271
  def read_request_line
185
272
  return unless line = read_line?
186
273
 
@@ -194,6 +281,8 @@ module Protocol
194
281
  end
195
282
 
196
283
  def read_request
284
+ open!
285
+
197
286
  method, path, version = read_request_line
198
287
  return unless method
199
288
 
@@ -203,6 +292,10 @@ module Protocol
203
292
 
204
293
  body = read_request_body(method, headers)
205
294
 
295
+ unless body
296
+ self.receive_end_stream!
297
+ end
298
+
206
299
  @count += 1
207
300
 
208
301
  return headers.delete(HOST), method, path, version, headers, body
@@ -216,16 +309,30 @@ module Protocol
216
309
  return version, status, reason
217
310
  end
218
311
 
312
+ private def interim_status?(status)
313
+ status != 101 and status >= 100 and status < 200
314
+ end
315
+
219
316
  def read_response(method)
317
+ unless @state == :open or @state == :half_closed_local
318
+ raise ProtocolError, "Cannot read response in #{@state}!"
319
+ end
320
+
220
321
  version, status, reason = read_response_line
221
322
 
222
323
  headers = read_headers
223
324
 
224
325
  @persistent = persistent?(version, method, headers)
225
326
 
226
- body = read_response_body(method, status, headers)
227
-
228
- @count += 1
327
+ unless interim_status?(status)
328
+ body = read_response_body(method, status, headers)
329
+
330
+ unless body
331
+ self.receive_end_stream!
332
+ end
333
+
334
+ @count += 1
335
+ end
229
336
 
230
337
  return version, status, reason, headers, body
231
338
  end
@@ -383,6 +490,32 @@ module Protocol
383
490
  @stream.close_write
384
491
  end
385
492
 
493
+ def idle!
494
+ @state = :idle
495
+ end
496
+
497
+ def closed!
498
+ unless @state == :half_closed_local or @state == :half_closed_remote
499
+ raise ProtocolError, "Cannot close in #{@state}!"
500
+ end
501
+
502
+ if @persistent
503
+ self.idle!
504
+ else
505
+ @state = :closed
506
+ end
507
+ end
508
+
509
+ def send_end_stream!
510
+ if @state == :open
511
+ @state = :half_closed_local
512
+ elsif @state == :half_closed_remote
513
+ self.closed!
514
+ else
515
+ raise ProtocolError, "Cannot send end stream in #{@state}!"
516
+ end
517
+ end
518
+
386
519
  def write_body(version, body, head = false, trailer = nil)
387
520
  # HTTP/1.0 cannot in any case handle trailers.
388
521
  if version == HTTP10 # or te: trailers was not present (strictly speaking not required.)
@@ -412,22 +545,37 @@ module Protocol
412
545
  write_connection_header(version)
413
546
  write_body_and_close(body, head)
414
547
  end
548
+ ensure
549
+ send_end_stream!
550
+ end
551
+
552
+ def receive_end_stream!
553
+ if @state == :open
554
+ @state = :half_closed_remote
555
+ elsif @state == :half_closed_local
556
+ self.closed!
557
+ else
558
+ raise ProtocolError, "Cannot receive end stream in #{@state}!"
559
+ end
415
560
  end
416
561
 
417
562
  def read_chunked_body(headers)
418
- Body::Chunked.new(@stream, headers)
563
+ Body::Chunked.new(self, headers)
419
564
  end
420
565
 
421
566
  def read_fixed_body(length)
422
- Body::Fixed.new(@stream, length)
567
+ Body::Fixed.new(self, length)
423
568
  end
424
569
 
425
570
  def read_remainder_body
426
571
  @persistent = false
427
- Body::Remainder.new(@stream)
572
+ Body::Remainder.new(self)
428
573
  end
429
574
 
430
575
  def read_head_body(length)
576
+ # We are not receiving any body:
577
+ self.receive_end_stream!
578
+
431
579
  Protocol::HTTP::Body::Head.new(length)
432
580
  end
433
581
 
@@ -10,6 +10,10 @@ module Protocol
10
10
  class Error < HTTP::Error
11
11
  end
12
12
 
13
+ # The protocol was violated in some way, e.g. trying to write a request while reading a response.
14
+ class ProtocolError < Error
15
+ end
16
+
13
17
  # The request was not able to be parsed correctly, or failed some kind of validation.
14
18
  class BadRequest < Error
15
19
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP1
8
- VERSION = "0.23.0"
8
+ VERSION = "0.24.0"
9
9
  end
10
10
  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.23.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -42,7 +42,7 @@ cert_chain:
42
42
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
43
43
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
44
44
  -----END CERTIFICATE-----
45
- date: 2024-09-17 00:00:00.000000000 Z
45
+ date: 2024-09-18 00:00:00.000000000 Z
46
46
  dependencies:
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: protocol-http
metadata.gz.sig CHANGED
Binary file