protocol-http1 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.

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.