protocol-http1 0.22.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: 550b6e9b8f04f40fbba30a9260ead61bc1921130b069f8d4f6d7005077da0e61
4
- data.tar.gz: 54a8009f974a0292b1500c21f7e8047d5af4d815e7b844fa46ec0d6f6d356fee
3
+ metadata.gz: '0854252b3602c1271818e28024b62e1a4373521052b75ba0aa879a1f77a6e84e'
4
+ data.tar.gz: 2ad1603fc177c4b8493f304806a1f8baf4dc147b0f1707d463f534fb1e740a62
5
5
  SHA512:
6
- metadata.gz: e44973d7e04933d1e6d3e8d50c98dcc57603fc4cd7491a1ab9ed490ef659551bbf1e0eb425222cc2e61f38b9fc5af864198970505a42750dad8c97bdb68331e7
7
- data.tar.gz: 6fba9bb8ac29bc13966bbebed00ca72b60d9214ba1943185e44760abb72ad5b1fa8254f4222951e5bf983263fef015a78ecdca6e6c8396c3c0e9548bade51576
6
+ metadata.gz: 213525d129854f2198cb1be6be3d99a74d7add2e4a8dc57ed72d3bd1854b29535256120dfcbd735f1823d91bb6bf27fee6571d93d8e85f0031a404be99b39e01
7
+ data.tar.gz: 32f7a63f81f830a77839fa154b69bfa5a8dc316d579cc48e10d2a786e92806960c36821b3f53da67c3b1e90ba29394264ae76c621d7608c62a0d56224dabec1f
checksums.yaml.gz.sig CHANGED
Binary file
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2019-2024, by Samuel Williams.
5
5
  # Copyright, 2023, by Thomas Morgan.
6
6
 
7
- require 'protocol/http/body/readable'
7
+ require "protocol/http/body/readable"
8
8
 
9
9
  module Protocol
10
10
  module HTTP1
@@ -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,18 +23,22 @@ module Protocol
23
23
  end
24
24
 
25
25
  def empty?
26
- @stream.nil?
26
+ @connection.nil?
27
27
  end
28
28
 
29
- def close(error = nil)
30
- if @stream
29
+ def discard
30
+ if connection = @connection
31
+ @connection = nil
32
+
31
33
  # We only close the connection if we haven't completed reading the entire body:
32
34
  unless @finished
33
- @stream.close_read
35
+ connection.close_read
34
36
  end
35
-
36
- @stream = nil
37
37
  end
38
+ end
39
+
40
+ def close(error = nil)
41
+ self.discard
38
42
 
39
43
  super
40
44
  end
@@ -44,8 +48,8 @@ module Protocol
44
48
  # Follows the procedure outlined in https://tools.ietf.org/html/rfc7230#section-4.1.3
45
49
  def read
46
50
  if !@finished
47
- if @stream
48
- length, _extensions = read_line.split(";", 2)
51
+ if @connection
52
+ length, _extensions = @connection.read_line.split(";", 2)
49
53
 
50
54
  unless length =~ VALID_CHUNK_LENGTH
51
55
  raise BadRequest, "Invalid chunk length: #{length.inspect}"
@@ -57,27 +61,33 @@ module Protocol
57
61
  if length == 0
58
62
  read_trailer
59
63
 
60
- # The final chunk has been read and the stream is now closed:
61
- @stream = nil
64
+ # The final chunk has been read and the connection is now closed:
65
+ @connection.receive_end_stream!
66
+ @connection = nil
62
67
  @finished = true
63
68
 
64
69
  return nil
65
70
  end
66
71
 
67
72
  # Read trailing CRLF:
68
- chunk = @stream.read(length + 2)
69
-
70
- # ...and chomp it off:
71
- chunk.chomp!(CRLF)
72
-
73
- @length += length
74
- @count += 1
73
+ chunk = @connection.read(length + 2)
75
74
 
76
- return chunk
75
+ if chunk.bytesize == length + 2
76
+ # ...and chomp it off:
77
+ chunk.chomp!(CRLF)
78
+
79
+ @length += length
80
+ @count += 1
81
+
82
+ return chunk
83
+ else
84
+ # The connection has been closed before we have read the requested length:
85
+ self.discard
86
+ end
77
87
  end
78
88
 
79
- # If the stream has been closed before we have read the final chunk, raise an error:
80
- 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!"
81
91
  end
82
92
  end
83
93
 
@@ -87,16 +97,8 @@ module Protocol
87
97
 
88
98
  private
89
99
 
90
- def read_line?
91
- @stream.gets(CRLF, chomp: true)
92
- end
93
-
94
- def read_line
95
- read_line? or raise EOFError
96
- end
97
-
98
100
  def read_trailer
99
- while line = read_line?
101
+ while line = @connection.read_line?
100
102
  # Empty line indicates end of trailer:
101
103
  break if line.empty?
102
104
 
@@ -3,14 +3,14 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require 'protocol/http/body/readable'
6
+ require "protocol/http/body/readable"
7
7
 
8
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,41 +20,49 @@ 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
- def close(error = nil)
27
- if @stream
28
- # If we are closing the body without fully reading it, the underlying connection is now in an undefined state.
26
+ def discard
27
+ if connection = @connection
28
+ @connection = nil
29
+
29
30
  if @remaining != 0
30
- @stream.close_read
31
+ connection.close_read
31
32
  end
32
-
33
- @stream = nil
34
33
  end
34
+ end
35
+
36
+ def close(error = nil)
37
+ self.discard
35
38
 
36
39
  super
37
40
  end
38
41
 
39
- # @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.
40
43
  def read
41
44
  if @remaining > 0
42
- if @stream
43
- # `readpartial` will raise `EOFError` if the stream is finished, or `IOError` if the stream is closed.
44
- if chunk = @stream.readpartial(@remaining)
45
- @remaining -= chunk.bytesize
46
-
47
- return chunk
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
+
49
+ @remaining -= chunk.bytesize
50
+
51
+ if @remaining == 0
52
+ @connection.receive_end_stream!
53
+ @connection = nil
48
54
  end
55
+
56
+ return chunk
49
57
  end
50
58
 
51
- # If the stream has been closed before we have read the expected length, raise an error:
52
- 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!"
53
61
  end
54
62
  end
55
63
 
56
64
  def inspect
57
- "\#<#{self.class} length=#{@length} remaining=#{@remaining} state=#{@stream ? 'open' : 'closed'}>"
65
+ "\#<#{self.class} length=#{@length} remaining=#{@remaining} state=#{@connection ? 'open' : 'closed'}>"
58
66
  end
59
67
  end
60
68
  end
@@ -3,44 +3,48 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require 'protocol/http/body/readable'
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 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
- def close(error = nil)
25
- if @stream
26
- # We can't really do anything in this case except close the connection.
27
- @stream.close_read
28
- @stream = nil
24
+ def discard
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
29
30
  end
31
+ end
32
+
33
+ def close(error = nil)
34
+ self.discard
30
35
 
31
36
  super
32
37
  end
33
38
 
34
39
  def read
35
- @stream&.readpartial(BLOCK_SIZE)
36
- rescue EOFError, IOError
37
- @stream = nil
38
- # I noticed that in some cases you will get EOFError, and in other cases IOError!?
39
- return nil
40
+ @connection&.readpartial(BLOCK_SIZE)
41
+ rescue EOFError
42
+ @connection.receive_end_stream!
43
+ @connection = nil
40
44
  end
41
45
 
42
46
  def inspect
43
- "\#<#{self.class} state=#{@stream ? 'open' : 'closed'}>"
47
+ "\#<#{self.class} state=#{@connection ? 'open' : 'closed'}>"
44
48
  end
45
49
  end
46
50
  end
@@ -7,31 +7,31 @@
7
7
  # Copyright, 2023-2024, by Thomas Morgan.
8
8
  # Copyright, 2024, by Anton Zhuravsky.
9
9
 
10
- require 'protocol/http/headers'
10
+ require "protocol/http/headers"
11
11
 
12
- require_relative 'reason'
13
- require_relative 'error'
12
+ require_relative "reason"
13
+ require_relative "error"
14
14
 
15
- require_relative 'body/chunked'
16
- require_relative 'body/fixed'
17
- require_relative 'body/remainder'
18
- require 'protocol/http/body/head'
15
+ require_relative "body/chunked"
16
+ require_relative "body/fixed"
17
+ require_relative "body/remainder"
18
+ require "protocol/http/body/head"
19
19
 
20
- require 'protocol/http/methods'
20
+ require "protocol/http/methods"
21
21
 
22
22
  module Protocol
23
23
  module HTTP1
24
- CONTENT_LENGTH = 'content-length'
24
+ CONTENT_LENGTH = "content-length"
25
25
 
26
- TRANSFER_ENCODING = 'transfer-encoding'
27
- CHUNKED = 'chunked'
26
+ TRANSFER_ENCODING = "transfer-encoding"
27
+ CHUNKED = "chunked"
28
28
 
29
- CONNECTION = 'connection'
30
- CLOSE = 'close'
31
- KEEP_ALIVE = 'keep-alive'
29
+ CONNECTION = "connection"
30
+ CLOSE = "close"
31
+ KEEP_ALIVE = "keep-alive"
32
32
 
33
- HOST = 'host'
34
- UPGRADE = 'upgrade'
33
+ HOST = "host"
34
+ UPGRADE = "upgrade"
35
35
 
36
36
  # HTTP/1.x request line parser:
37
37
  TOKEN = /[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+/.freeze
@@ -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
 
@@ -3,13 +3,17 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require 'protocol/http/error'
6
+ require "protocol/http/error"
7
7
 
8
8
  module Protocol
9
9
  module HTTP1
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
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require 'protocol/http/error'
6
+ require "protocol/http/error"
7
7
 
8
8
  module Protocol
9
9
  module HTTP1
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP1
8
- VERSION = "0.22.0"
8
+ VERSION = "0.24.0"
9
9
  end
10
10
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2022, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
- require_relative 'http1/version'
7
- require_relative 'http1/connection'
6
+ require_relative "http1/version"
7
+ require_relative "http1/connection"
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.22.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-05 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