protocol-http1 0.5.0 → 0.6.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.

Potentially problematic release.


This version of protocol-http1 might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12c9729522d079c4f935b879740c9b34b83ec7f1473cc84ece34c4ffb869a78c
4
- data.tar.gz: 2e2f7259a77564235aaf3b99ab9aac1078f464a901f1371a7afb3b30a4773250
3
+ metadata.gz: 750f0922f84bc1790af1f042ffe720f1c2d7ffd50b3492522d3ace0362447d96
4
+ data.tar.gz: 5a4dc4316888e0ba157c4360c871945ea5f4533f86098486fbe0682865b9b2e8
5
5
  SHA512:
6
- metadata.gz: 25e1bc8406ce40cf8093b4b4f5e966f3c615d0097767111ab2f2b1daed0a0e226d8a5911d4c3cad8b5f584305a7c47398ff6a0d523467416466241d7f78b3187
7
- data.tar.gz: '0868212e349a7f2c060748ec315a147b056b660c664f4b4c8a79a5da16d3c542bfc4e7fa1b27b6d09185d9f55e51e13b83b4052479d24a0368b469c19f0e4250'
6
+ metadata.gz: 3d347f96ce603c2721abe056862c07d2cca962735b150027177d1bca5fc1d869209f5840e55f241312c633c83c57a47c380e07cc4d815cba10224ecfcbf7962a
7
+ data.tar.gz: 858cb2dae8d2e93e3150ef8361e247cf7dfe2bba43dcd52e2cf8917da3e211f779446decca233f101666a5e4188b4a31764df03c229ff86f902eb90e3ce185ca
data/.gitignore CHANGED
@@ -10,3 +10,4 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
  Gemfile.lock
13
+ .covered.db
data/Gemfile CHANGED
@@ -2,3 +2,8 @@ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in protocol-http1.gemspec
4
4
  gemspec
5
+
6
+ group :test do
7
+ gem 'async-io'
8
+ gem 'async-rspec'
9
+ end
data/README.md CHANGED
@@ -27,11 +27,11 @@ Here is a basic HTTP/1.1 client:
27
27
  ```ruby
28
28
  require 'async'
29
29
  require 'async/io/stream'
30
- require 'async/http/url_endpoint'
30
+ require 'async/http/endpoint'
31
31
  require 'protocol/http1/connection'
32
32
 
33
33
  Async.run do
34
- endpoint = Async::HTTP::URLEndpoint.parse("https://www.google.com/search?q=kittens", alpn_protocols: ["http/1.1"])
34
+ endpoint = Async::HTTP::Endpoint.parse("https://www.google.com/search?q=kittens", alpn_protocols: ["http/1.1"])
35
35
 
36
36
  peer = endpoint.connect
37
37
 
@@ -3,12 +3,12 @@ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
3
3
 
4
4
  require 'async'
5
5
  require 'async/io/stream'
6
- require 'async/http/url_endpoint'
6
+ require 'async/http/endpoint'
7
7
  require 'protocol/http1/connection'
8
8
  require 'pry'
9
9
 
10
10
  Async.run do
11
- endpoint = Async::HTTP::URLEndpoint.parse("https://www.google.com/search?q=kittens", alpn_protocols: ["http/1.1"])
11
+ endpoint = Async::HTTP::Endpoint.parse("https://www.google.com/search?q=kittens", alpn_protocols: ["http/1.1"])
12
12
 
13
13
  peer = endpoint.connect
14
14
 
@@ -0,0 +1,77 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'protocol/http/body/readable'
22
+
23
+ module Protocol
24
+ module HTTP1
25
+ module Body
26
+ class Chunked < HTTP::Body::Readable
27
+ # TODO maybe this should take a stream rather than a connection?
28
+ def initialize(connection)
29
+ @connection = connection
30
+ @finished = false
31
+
32
+ @length = 0
33
+ @count = 0
34
+ end
35
+
36
+ def empty?
37
+ @finished
38
+ end
39
+
40
+ def close(error = nil)
41
+ # We only close the connection if we haven't completed reading the entire body:
42
+ unless @finished
43
+ @connection.close
44
+ @finished = true
45
+ end
46
+
47
+ super
48
+ end
49
+
50
+ def read
51
+ return nil if @finished
52
+
53
+ length = @connection.read_line.to_i(16)
54
+
55
+ if length == 0
56
+ @finished = true
57
+ @connection.read_line
58
+
59
+ return nil
60
+ end
61
+
62
+ chunk = @connection.stream.read(length)
63
+ @connection.read_line # Consume the trailing CRLF
64
+
65
+ @length += length
66
+ @count += 1
67
+
68
+ return chunk
69
+ end
70
+
71
+ def inspect
72
+ "\#<#{self.class} #{@length} bytes read in #{@count} chunks>"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,72 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'protocol/http/body/readable'
22
+
23
+ module Protocol
24
+ module HTTP1
25
+ module Body
26
+ class Fixed < HTTP::Body::Readable
27
+ def initialize(stream, length)
28
+ @stream = stream
29
+ @length = length
30
+ @remaining = length
31
+ end
32
+
33
+ attr :length
34
+ attr :remaining
35
+
36
+ def empty?
37
+ @remaining == 0
38
+ end
39
+
40
+ def close(error = nil)
41
+ if @remaining != 0
42
+ @stream.close
43
+ end
44
+
45
+ super
46
+ end
47
+
48
+ def read
49
+ if @remaining > 0
50
+ if chunk = @stream.read_partial(@remaining)
51
+ @remaining -= chunk.bytesize
52
+
53
+ return chunk
54
+ end
55
+ end
56
+ end
57
+
58
+ def join
59
+ buffer = @stream.read(@remaining)
60
+
61
+ @remaining = 0
62
+
63
+ return buffer
64
+ end
65
+
66
+ def inspect
67
+ "\#<#{self.class} length=#{@length} remaining=#{@remaining}>"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,64 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'protocol/http/body/readable'
22
+
23
+ module Protocol
24
+ module HTTP1
25
+ module Body
26
+ class Remainder < HTTP::Body::Readable
27
+ def initialize(stream)
28
+ @stream = stream
29
+ end
30
+
31
+ def empty?
32
+ @stream.closed?
33
+ end
34
+
35
+ def close(error = nil)
36
+ # We can't really do anything in this case except close the connection.
37
+ @stream.close
38
+
39
+ super
40
+ end
41
+
42
+ def read
43
+ @stream.read_partial
44
+ end
45
+
46
+ def call(stream)
47
+ self.each do |chunk|
48
+ stream.write(chunk)
49
+ end
50
+
51
+ stream.flush
52
+ end
53
+
54
+ def join
55
+ read
56
+ end
57
+
58
+ def inspect
59
+ "\#<#{self.class} #{@stream.closed? ? 'closed' : 'open'}>"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -22,6 +22,10 @@ require 'protocol/http/headers'
22
22
 
23
23
  require_relative 'error'
24
24
 
25
+ require_relative 'body/chunked'
26
+ require_relative 'body/fixed'
27
+ require_relative 'body/remainder'
28
+
25
29
  module Protocol
26
30
  module HTTP1
27
31
  CONTENT_LENGTH = 'content-length'.freeze
@@ -45,7 +49,6 @@ module Protocol
45
49
  @stream = stream
46
50
 
47
51
  @persistent = persistent
48
- @upgrade = nil
49
52
 
50
53
  @count = 0
51
54
  end
@@ -55,15 +58,12 @@ module Protocol
55
58
  # Whether the connection is persistent.
56
59
  attr :persistent
57
60
 
58
- # Whether the connection has been upgraded, and to what.
59
- attr :upgrade
60
-
61
61
  # The number of requests processed.
62
62
  attr :count
63
63
 
64
64
  def upgrade?(headers)
65
65
  if upgrade = headers[UPGRADE]
66
- return upgrade.first
66
+ return upgrade
67
67
  end
68
68
  end
69
69
 
@@ -83,27 +83,19 @@ module Protocol
83
83
  end
84
84
  end
85
85
 
86
- def upgrade!(protocol)
87
- @persistent = false
88
-
89
- @upgrade = protocol
90
-
91
- return @stream
92
- end
93
-
94
86
  # Write the appropriate header for connection persistence.
95
87
  def write_connection_header(version)
96
- if @upgrade
97
- @stream.write("connection: upgrade\r\nupgrade: #{@upgrade}\r\n")
88
+ if version == HTTP10
89
+ @stream.write("connection: keep-alive\r\n") if @persistent
98
90
  else
99
- if version == HTTP10
100
- @stream.write("connection: keep-alive\r\n") if @persistent
101
- else
102
- @stream.write("connection: close\r\n") unless @persistent
103
- end
91
+ @stream.write("connection: close\r\n") unless @persistent
104
92
  end
105
93
  end
106
94
 
95
+ def write_upgrade_header(upgrade)
96
+ @stream.write("connection: upgrade\r\nupgrade: #{upgrade}\r\n")
97
+ end
98
+
107
99
  # Effectively close the connection and return the underlying IO.
108
100
  # @return [IO] the underlying non-blocking IO.
109
101
  def hijack!
@@ -124,15 +116,13 @@ module Protocol
124
116
  @stream.write("host: #{authority}\r\n")
125
117
 
126
118
  write_headers(headers)
127
- write_connection_header(version)
128
119
  end
129
120
 
130
- def write_response(version, status, headers, body = nil, head = false)
131
- @stream.write("#{version} #{status}\r\n")
121
+ def write_response(version, status, headers, reason = "With Honour.")
122
+ # Safari WebSockets break if no reason is given.
123
+ @stream.write("#{version} #{status} #{reason}\r\n")
132
124
 
133
125
  write_headers(headers)
134
- write_connection_header(version)
135
- write_body(body, version == HTTP11, head)
136
126
  end
137
127
 
138
128
  def write_headers(headers)
@@ -156,7 +146,6 @@ module Protocol
156
146
  headers = read_headers
157
147
 
158
148
  @persistent = persistent?(version, headers)
159
- @upgrade = upgrade?(headers)
160
149
 
161
150
  body = read_request_body(headers)
162
151
 
@@ -195,32 +184,20 @@ module Protocol
195
184
  return HTTP::Headers.new(fields)
196
185
  end
197
186
 
198
- def read_chunk
199
- length = self.read_line.to_i(16)
200
-
201
- if length == 0
202
- self.read_line
203
-
204
- return nil
205
- end
206
-
207
- # Read the data:
208
- chunk = @stream.read(length)
209
-
210
- # Consume the trailing CRLF:
211
- @stream.read(2)
212
-
213
- return chunk
214
- end
215
-
216
- def write_upgrade_body(body = nil)
217
- # Any time we are potentially closing the stream, we should ensure no further requests are processed:
187
+ # @param protocol [String] the protocol to upgrade to.
188
+ def write_upgrade_body(protocol, body = nil)
189
+ # Once we upgrade the connection, it can no longer handle other requests:
218
190
  @persistent = false
219
191
 
192
+ write_upgrade_header(protocol)
193
+
220
194
  @stream.write("\r\n")
221
- @stream.flush
195
+ @stream.flush # Don't remove me!
222
196
 
223
- body.call(@stream) if body
197
+ body&.each do |chunk|
198
+ @stream.write(chunk)
199
+ @stream.flush
200
+ end
224
201
 
225
202
  return @stream
226
203
  end
@@ -229,9 +206,7 @@ module Protocol
229
206
  @stream.write("content-length: 0\r\n\r\n")
230
207
  @stream.flush
231
208
 
232
- if body
233
- body.close if body.respond_to?(:close)
234
- end
209
+ body&.close
235
210
  end
236
211
 
237
212
  def write_fixed_length_body(body, length, head)
@@ -239,7 +214,7 @@ module Protocol
239
214
  @stream.flush
240
215
 
241
216
  if head
242
- body.close if body.respond_to?(:close)
217
+ body.close
243
218
 
244
219
  return
245
220
  end
@@ -267,7 +242,7 @@ module Protocol
267
242
  @stream.flush
268
243
 
269
244
  if head
270
- body.close if body.respond_to?(:close)
245
+ body.close
271
246
 
272
247
  return
273
248
  end
@@ -288,11 +263,12 @@ module Protocol
288
263
  def write_body_and_close(body, head)
289
264
  # We can't be persistent because we don't know the data length:
290
265
  @persistent = false
266
+
291
267
  @stream.write("\r\n")
292
268
  @stream.flush
293
269
 
294
270
  if head
295
- body.close if body.respond_to?(:close)
271
+ body.close
296
272
  else
297
273
  body.each do |chunk|
298
274
  @stream.write(chunk)
@@ -303,48 +279,42 @@ module Protocol
303
279
  @stream.close_write
304
280
  end
305
281
 
306
- def write_body(body, chunked = true, head = false)
282
+ def write_body(version, body, head = false)
307
283
  if body.nil? or body.empty?
284
+ write_connection_header(version)
308
285
  write_empty_body(body)
309
- elsif body.respond_to?(:call)
310
- write_upgrade_body(body)
311
286
  elsif length = body.length
287
+ write_connection_header(version)
312
288
  write_fixed_length_body(body, length, head)
313
- elsif @persistent and chunked
289
+ elsif @persistent and version == HTTP11
290
+ write_connection_header(version)
314
291
  # We specifically ensure that non-persistent connections do not use chunked response, so that hijacking works as expected.
315
292
  write_chunked_body(body, head)
316
293
  else
294
+ @persistent = false
295
+ write_connection_header(version)
317
296
  write_body_and_close(body, head)
318
297
  end
319
-
320
- @stream.flush
321
298
  end
322
299
 
323
300
  def read_chunked_body
324
- buffer = String.new.b
325
-
326
- while chunk = read_chunk
327
- buffer << chunk
328
- chunk.clear
329
- end
330
-
331
- return buffer
301
+ Body::Chunked.new(self)
332
302
  end
333
303
 
334
304
  def read_fixed_body(length)
335
- @stream.read(length)
305
+ Body::Fixed.new(@stream, length)
336
306
  end
337
307
 
338
- def read_tunnel_body
339
- read_remainder_body
308
+ def read_remainder_body
309
+ Body::Remainder.new(@stream)
340
310
  end
341
311
 
342
- def read_upgrade_body
312
+ def read_tunnel_body
343
313
  read_remainder_body
344
314
  end
345
315
 
346
- def read_remainder_body
347
- @stream.read
316
+ def read_upgrade_body(protocol)
317
+ read_remainder_body
348
318
  end
349
319
 
350
320
  HEAD = "HEAD".freeze
@@ -428,11 +398,7 @@ module Protocol
428
398
  end
429
399
  end
430
400
 
431
- if upgrade = upgrade?(headers)
432
- @upgrade = upgrade
433
- return read_upgrade_body
434
- end
435
-
401
+ # http://tools.ietf.org/html/rfc2068#section-19.7.1.1
436
402
  if remainder
437
403
  # 7. Otherwise, this is a response message without a declared message
438
404
  # body length, so the message body length is determined by the
@@ -1,5 +1,25 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
1
21
  module Protocol
2
22
  module HTTP1
3
- VERSION = "0.5.0"
23
+ VERSION = "0.6.0"
4
24
  end
5
25
  end
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "protocol-http", "~> 0.2"
21
+ spec.add_dependency "protocol-http", "~> 0.5"
22
22
 
23
23
  spec.add_development_dependency "covered"
24
24
  spec.add_development_dependency "bundler"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http1
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-12 00:00:00.000000000 Z
11
+ date: 2019-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: protocol-http
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.2'
19
+ version: '0.5'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.2'
26
+ version: '0.5'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: covered
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -96,6 +96,9 @@ files:
96
96
  - Rakefile
97
97
  - examples/http1/request.rb
98
98
  - lib/protocol/http1.rb
99
+ - lib/protocol/http1/body/chunked.rb
100
+ - lib/protocol/http1/body/fixed.rb
101
+ - lib/protocol/http1/body/remainder.rb
99
102
  - lib/protocol/http1/connection.rb
100
103
  - lib/protocol/http1/error.rb
101
104
  - lib/protocol/http1/version.rb
@@ -119,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
122
  - !ruby/object:Gem::Version
120
123
  version: '0'
121
124
  requirements: []
122
- rubygems_version: 3.0.3
125
+ rubygems_version: 3.0.2
123
126
  signing_key:
124
127
  specification_version: 4
125
128
  summary: A low level implementation of the HTTP/1 protocol.