protocol-http1 0.23.0 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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