protocol-http1 0.1.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 +7 -0
- data/.editorconfig +6 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +19 -0
- data/Gemfile +4 -0
- data/README.md +92 -0
- data/Rakefile +6 -0
- data/examples/http1/request.rb +38 -0
- data/lib/protocol/http1.rb +22 -0
- data/lib/protocol/http1/connection.rb +437 -0
- data/lib/protocol/http1/error.rb +31 -0
- data/lib/protocol/http1/version.rb +5 -0
- data/protocol-http1.gemspec +29 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4a739005ea471dd8601ea3844e71a6437981bcd5e4cb3e7d5c85f7eb699d55a8
|
4
|
+
data.tar.gz: 31c5dc038dceaa79552fb76bb56667f85e7e61ac568f06c564d426a9f6606784
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7aebf8675b398d210cc4fb457d498ea4a682112a93f4d298ced3ed24709dd3d1532eaa6ba56ceeaeb1c521545b23a48f557ba8616aec2f6d3fb2a57937e5ccb7
|
7
|
+
data.tar.gz: fcac06a0408e7bafede84eb1ab675bddfec3cc17605690fdc6f6111dc626412a65ff0f0dbefcd908d1c6b14068c4ac2adc03d0f9c717fb8ef9fef2b7f5d62f0e
|
data/.editorconfig
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
language: ruby
|
2
|
+
dist: xenial
|
3
|
+
cache: bundler
|
4
|
+
|
5
|
+
matrix:
|
6
|
+
include:
|
7
|
+
- rvm: 2.4
|
8
|
+
- rvm: 2.5
|
9
|
+
- rvm: 2.6
|
10
|
+
- rvm: 2.6
|
11
|
+
env: COVERAGE=PartialSummary,Coveralls
|
12
|
+
- rvm: truffleruby
|
13
|
+
- rvm: jruby-head
|
14
|
+
env: JRUBY_OPTS="--debug -X+O"
|
15
|
+
- rvm: ruby-head
|
16
|
+
allow_failures:
|
17
|
+
- rvm: truffleruby
|
18
|
+
- rvm: ruby-head
|
19
|
+
- rvm: jruby-head
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Protocol::HTTP1
|
2
|
+
|
3
|
+
Provides a low-level implementation of the HTTP/1 protocol.
|
4
|
+
|
5
|
+
[](http://travis-ci.com/socketry/protocol-http1)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'protocol-http1'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install protocol-http1
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Here is a basic HTTP/1.1 client:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
require 'async'
|
29
|
+
require 'async/io/stream'
|
30
|
+
require 'async/http/url_endpoint'
|
31
|
+
require 'protocol/http1/connection'
|
32
|
+
|
33
|
+
Async.run do
|
34
|
+
endpoint = Async::HTTP::URLEndpoint.parse("https://www.google.com/search?q=kittens", alpn_protocols: ["http/1.1"])
|
35
|
+
|
36
|
+
peer = endpoint.connect
|
37
|
+
|
38
|
+
puts "Connected to #{peer} #{peer.remote_address.inspect}"
|
39
|
+
|
40
|
+
# IO Buffering...
|
41
|
+
stream = Async::IO::Stream.new(peer)
|
42
|
+
client = Protocol::HTTP1::Connection.new(stream)
|
43
|
+
|
44
|
+
def client.read_line
|
45
|
+
@stream.read_until(Protocol::HTTP1::Connection::CRLF) or raise EOFError
|
46
|
+
end
|
47
|
+
|
48
|
+
puts "Writing request..."
|
49
|
+
client.write_request("www.google.com", "GET", "/search?q=kittens", "HTTP/1.1", [["Accept", "*/*"]])
|
50
|
+
client.write_body(nil)
|
51
|
+
|
52
|
+
puts "Reading response..."
|
53
|
+
response = client.read_response("GET")
|
54
|
+
|
55
|
+
puts "Got response: #{response.inspect}"
|
56
|
+
|
57
|
+
puts "Closing client..."
|
58
|
+
client.close
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
## Contributing
|
63
|
+
|
64
|
+
1. Fork it
|
65
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
66
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
67
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
68
|
+
5. Create new Pull Request
|
69
|
+
|
70
|
+
## License
|
71
|
+
|
72
|
+
Released under the MIT license.
|
73
|
+
|
74
|
+
Copyright, 2019, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
|
75
|
+
|
76
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
77
|
+
of this software and associated documentation files (the "Software"), to deal
|
78
|
+
in the Software without restriction, including without limitation the rights
|
79
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
80
|
+
copies of the Software, and to permit persons to whom the Software is
|
81
|
+
furnished to do so, subject to the following conditions:
|
82
|
+
|
83
|
+
The above copyright notice and this permission notice shall be included in
|
84
|
+
all copies or substantial portions of the Software.
|
85
|
+
|
86
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
87
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
88
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
89
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
90
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
91
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
92
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
|
3
|
+
|
4
|
+
require 'async'
|
5
|
+
require 'async/io/stream'
|
6
|
+
require 'async/http/url_endpoint'
|
7
|
+
require 'protocol/http1/connection'
|
8
|
+
require 'pry'
|
9
|
+
|
10
|
+
Async.run do
|
11
|
+
endpoint = Async::HTTP::URLEndpoint.parse("https://www.google.com/search?q=kittens", alpn_protocols: ["http/1.1"])
|
12
|
+
|
13
|
+
peer = endpoint.connect
|
14
|
+
|
15
|
+
puts "Connected to #{peer} #{peer.remote_address.inspect}"
|
16
|
+
|
17
|
+
# IO Buffering...
|
18
|
+
stream = Async::IO::Stream.new(peer)
|
19
|
+
client = Protocol::HTTP1::Connection.new(stream)
|
20
|
+
|
21
|
+
def client.read_line
|
22
|
+
@stream.read_until(Protocol::HTTP1::Connection::CRLF) or raise EOFError
|
23
|
+
end
|
24
|
+
|
25
|
+
puts "Writing request..."
|
26
|
+
client.write_request("www.google.com", "GET", "/search?q=kittens", "HTTP/1.1", [["Accept", "*/*"]])
|
27
|
+
client.write_body(nil)
|
28
|
+
|
29
|
+
puts "Reading response..."
|
30
|
+
response = client.read_response("GET")
|
31
|
+
|
32
|
+
puts "Got response: #{response.inspect}"
|
33
|
+
|
34
|
+
puts "Closing client..."
|
35
|
+
client.close
|
36
|
+
end
|
37
|
+
|
38
|
+
puts "Exiting."
|
@@ -0,0 +1,22 @@
|
|
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
|
+
|
21
|
+
require_relative 'http1/version'
|
22
|
+
require_relative 'http1/connection'
|
@@ -0,0 +1,437 @@
|
|
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/headers'
|
22
|
+
|
23
|
+
require_relative 'error'
|
24
|
+
|
25
|
+
module Protocol
|
26
|
+
module HTTP1
|
27
|
+
CONTENT_LENGTH = 'content-length'.freeze
|
28
|
+
|
29
|
+
TRANSFER_ENCODING = 'transfer-encoding'.freeze
|
30
|
+
CHUNKED = 'chunked'.freeze
|
31
|
+
|
32
|
+
CONNECTION = 'connection'.freeze
|
33
|
+
CLOSE = 'close'.freeze
|
34
|
+
KEEP_ALIVE = 'keep-alive'.freeze
|
35
|
+
|
36
|
+
HOST = 'host'.freeze
|
37
|
+
UPGRADE = 'upgrade'.freeze
|
38
|
+
|
39
|
+
class Connection
|
40
|
+
CRLF = "\r\n".freeze
|
41
|
+
HTTP10 = "HTTP/1.0".freeze
|
42
|
+
HTTP11 = "HTTP/1.1".freeze
|
43
|
+
|
44
|
+
def initialize(stream, persistent = true)
|
45
|
+
@stream = stream
|
46
|
+
|
47
|
+
@persistent = persistent
|
48
|
+
@upgrade = nil
|
49
|
+
|
50
|
+
@count = 0
|
51
|
+
end
|
52
|
+
|
53
|
+
attr :stream
|
54
|
+
|
55
|
+
# Whether the connection is persistent.
|
56
|
+
attr :persistent
|
57
|
+
|
58
|
+
# Whether the connection has been upgraded, and to what.
|
59
|
+
attr :upgrade
|
60
|
+
|
61
|
+
# The number of requests processed.
|
62
|
+
attr :count
|
63
|
+
|
64
|
+
def upgrade?(headers)
|
65
|
+
if upgrade = headers[UPGRADE]
|
66
|
+
return upgrade.first
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def persistent?(version, headers)
|
71
|
+
if version == HTTP10
|
72
|
+
if connection = headers[CONNECTION]
|
73
|
+
return connection.include?(KEEP_ALIVE)
|
74
|
+
else
|
75
|
+
return false
|
76
|
+
end
|
77
|
+
else
|
78
|
+
if connection = headers[CONNECTION]
|
79
|
+
return !connection.include?(CLOSE)
|
80
|
+
else
|
81
|
+
return true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def upgrade!(protocol)
|
87
|
+
@upgrade = protocol
|
88
|
+
@persistent = false
|
89
|
+
|
90
|
+
return @stream
|
91
|
+
end
|
92
|
+
|
93
|
+
# Write the appropriate header for connection persistence.
|
94
|
+
def write_connection_header(version)
|
95
|
+
if @upgrade
|
96
|
+
@stream.write("connection: upgrade\r\nupgrade: #{@upgrade}\r\n")
|
97
|
+
else
|
98
|
+
if version == HTTP10
|
99
|
+
@stream.write("connection: keep-alive\r\n") if @persistent
|
100
|
+
else
|
101
|
+
@stream.write("connection: close\r\n") unless @persistent
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Effectively close the connection and return the underlying IO.
|
107
|
+
# @return [IO] the underlying non-blocking IO.
|
108
|
+
def hijack!
|
109
|
+
@persistent = false
|
110
|
+
|
111
|
+
@stream.flush
|
112
|
+
|
113
|
+
return @stream
|
114
|
+
end
|
115
|
+
|
116
|
+
# Close the connection and underlying stream.
|
117
|
+
def close
|
118
|
+
@stream.close
|
119
|
+
end
|
120
|
+
|
121
|
+
def write_request(authority, method, path, version, headers)
|
122
|
+
@stream.write("#{method} #{path} #{version}\r\n")
|
123
|
+
@stream.write("host: #{authority}\r\n")
|
124
|
+
|
125
|
+
write_headers(headers)
|
126
|
+
write_connection_header(version)
|
127
|
+
end
|
128
|
+
|
129
|
+
def write_response(version, status, headers, body = nil, head = false)
|
130
|
+
@stream.write("#{version} #{status}\r\n")
|
131
|
+
|
132
|
+
write_headers(headers)
|
133
|
+
write_connection_header(version)
|
134
|
+
write_body(body, version == HTTP11, head)
|
135
|
+
end
|
136
|
+
|
137
|
+
def write_headers(headers)
|
138
|
+
headers.each do |name, value|
|
139
|
+
@stream.write("#{name}: #{value}\r\n")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def each_line
|
144
|
+
while line = read_line
|
145
|
+
yield line
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def read_line
|
150
|
+
@stream.gets(CRLF, chomp: true) or raise EOFError
|
151
|
+
end
|
152
|
+
|
153
|
+
def read_request
|
154
|
+
method, path, version = read_line.split(/\s+/, 3)
|
155
|
+
headers = read_headers
|
156
|
+
|
157
|
+
@persistent = persistent?(version, headers)
|
158
|
+
@upgrade = upgrade?(headers)
|
159
|
+
|
160
|
+
body = read_request_body(headers)
|
161
|
+
|
162
|
+
@count += 1
|
163
|
+
|
164
|
+
return headers.delete(HOST), method, path, version, headers, body
|
165
|
+
end
|
166
|
+
|
167
|
+
def read_response(method)
|
168
|
+
version, status, reason = read_line.split(/\s+/, 3)
|
169
|
+
|
170
|
+
status = Integer(status)
|
171
|
+
|
172
|
+
headers = read_headers
|
173
|
+
|
174
|
+
@persistent = persistent?(version, headers)
|
175
|
+
|
176
|
+
body = read_response_body(method, status, headers)
|
177
|
+
|
178
|
+
@count += 1
|
179
|
+
|
180
|
+
return version, status, reason, headers, body
|
181
|
+
end
|
182
|
+
|
183
|
+
def read_headers
|
184
|
+
fields = []
|
185
|
+
|
186
|
+
self.each_line do |line|
|
187
|
+
if line =~ /^([a-zA-Z\-\d]+):\s*(.+?)\s*$/
|
188
|
+
fields << [$1, $2]
|
189
|
+
else
|
190
|
+
break
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
return HTTP::Headers.new(fields)
|
195
|
+
end
|
196
|
+
|
197
|
+
def read_chunk
|
198
|
+
length = self.read_line.to_i(16)
|
199
|
+
|
200
|
+
if length == 0
|
201
|
+
self.read_line
|
202
|
+
|
203
|
+
return nil
|
204
|
+
end
|
205
|
+
|
206
|
+
# Read the data:
|
207
|
+
chunk = @stream.read(length)
|
208
|
+
|
209
|
+
# Consume the trailing CRLF:
|
210
|
+
@stream.read(2)
|
211
|
+
|
212
|
+
return chunk
|
213
|
+
end
|
214
|
+
|
215
|
+
def write_upgrade_body
|
216
|
+
@stream.write("\r\n")
|
217
|
+
@stream.flush
|
218
|
+
|
219
|
+
return @stream unless block_given?
|
220
|
+
|
221
|
+
begin
|
222
|
+
yield @stream
|
223
|
+
rescue
|
224
|
+
@stream.close_write
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def write_empty_body(body)
|
229
|
+
@stream.write("content-length: 0\r\n\r\n")
|
230
|
+
@stream.flush
|
231
|
+
|
232
|
+
if body
|
233
|
+
body.close if body.respond_to?(:close)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def write_fixed_length_body(body, length, head)
|
238
|
+
@stream.write("content-length: #{length}\r\n\r\n")
|
239
|
+
@stream.flush
|
240
|
+
|
241
|
+
if head
|
242
|
+
body.close if body.respond_to?(:close)
|
243
|
+
|
244
|
+
return
|
245
|
+
end
|
246
|
+
|
247
|
+
chunk_length = 0
|
248
|
+
body.each do |chunk|
|
249
|
+
chunk_length += chunk.bytesize
|
250
|
+
|
251
|
+
if chunk_length > length
|
252
|
+
raise ProtocolError, "Trying to write #{chunk_length} bytes, but content length was #{length} bytes!"
|
253
|
+
end
|
254
|
+
|
255
|
+
@stream.write(chunk)
|
256
|
+
end
|
257
|
+
|
258
|
+
@stream.flush
|
259
|
+
|
260
|
+
if chunk_length != length
|
261
|
+
raise ProtocolError, "Wrote #{chunk_length} bytes, but content length was #{length} bytes!"
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def write_chunked_body(body, head)
|
266
|
+
@stream.write("transfer-encoding: chunked\r\n\r\n")
|
267
|
+
@stream.flush
|
268
|
+
|
269
|
+
if head
|
270
|
+
body.close if body.respond_to?(:close)
|
271
|
+
|
272
|
+
return
|
273
|
+
end
|
274
|
+
|
275
|
+
body.each do |chunk|
|
276
|
+
next if chunk.size == 0
|
277
|
+
|
278
|
+
@stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n")
|
279
|
+
@stream.write(chunk)
|
280
|
+
@stream.write(CRLF)
|
281
|
+
@stream.flush
|
282
|
+
end
|
283
|
+
|
284
|
+
@stream.write("0\r\n\r\n")
|
285
|
+
@stream.flush
|
286
|
+
end
|
287
|
+
|
288
|
+
def write_body_and_close(body, head)
|
289
|
+
# We can't be persistent because we don't know the data length:
|
290
|
+
@persistent = false
|
291
|
+
@stream.write("\r\n")
|
292
|
+
@stream.flush
|
293
|
+
|
294
|
+
if head
|
295
|
+
body.close if body.respond_to?(:close)
|
296
|
+
else
|
297
|
+
body.each do |chunk|
|
298
|
+
@stream.write(chunk)
|
299
|
+
@stream.flush
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
@stream.close_write
|
304
|
+
end
|
305
|
+
|
306
|
+
def write_body(body, chunked = true, head = false)
|
307
|
+
if body.nil? or body.empty?
|
308
|
+
write_empty_body(body)
|
309
|
+
elsif body.respond_to?(:call)
|
310
|
+
write_upgrade_body(&body)
|
311
|
+
elsif length = body.length
|
312
|
+
write_fixed_length_body(body, length, head)
|
313
|
+
elsif @persistent and chunked
|
314
|
+
# We specifically ensure that non-persistent connections do not use chunked response, so that hijacking works as expected.
|
315
|
+
write_chunked_body(body, head)
|
316
|
+
else
|
317
|
+
write_body_and_close(body, head)
|
318
|
+
end
|
319
|
+
|
320
|
+
@stream.flush
|
321
|
+
end
|
322
|
+
|
323
|
+
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
|
332
|
+
end
|
333
|
+
|
334
|
+
def read_fixed_body(length)
|
335
|
+
@stream.read(length)
|
336
|
+
end
|
337
|
+
|
338
|
+
def read_tunnel_body
|
339
|
+
read_remainder_body
|
340
|
+
end
|
341
|
+
|
342
|
+
def read_remainder_body
|
343
|
+
@stream.read
|
344
|
+
end
|
345
|
+
|
346
|
+
HEAD = "HEAD".freeze
|
347
|
+
CONNECT = "CONNECT".freeze
|
348
|
+
|
349
|
+
def read_response_body(method, status, headers)
|
350
|
+
# RFC 7230 3.3.3
|
351
|
+
# 1. Any response to a HEAD request and any response with a 1xx
|
352
|
+
# (Informational), 204 (No Content), or 304 (Not Modified) status
|
353
|
+
# code is always terminated by the first empty line after the
|
354
|
+
# header fields, regardless of the header fields present in the
|
355
|
+
# message, and thus cannot contain a message body.
|
356
|
+
if method == "HEAD" or (status >= 100 and status < 200) or status == 204 or status == 304
|
357
|
+
return nil
|
358
|
+
end
|
359
|
+
|
360
|
+
# 2. Any 2xx (Successful) response to a CONNECT request implies that
|
361
|
+
# the connection will become a tunnel immediately after the empty
|
362
|
+
# line that concludes the header fields. A client MUST ignore any
|
363
|
+
# Content-Length or Transfer-Encoding header fields received in
|
364
|
+
# such a message.
|
365
|
+
if method == "CONNECT" and status == 200
|
366
|
+
return read_tunnel_body
|
367
|
+
end
|
368
|
+
|
369
|
+
return read_body(headers, true)
|
370
|
+
end
|
371
|
+
|
372
|
+
def read_request_body(headers)
|
373
|
+
# 6. If this is a request message and none of the above are true, then
|
374
|
+
# the message body length is zero (no message body is present).
|
375
|
+
return read_body(headers)
|
376
|
+
end
|
377
|
+
|
378
|
+
def read_body(headers, remainder = false)
|
379
|
+
# 3. If a Transfer-Encoding header field is present and the chunked
|
380
|
+
# transfer coding (Section 4.1) is the final encoding, the message
|
381
|
+
# body length is determined by reading and decoding the chunked
|
382
|
+
# data until the transfer coding indicates the data is complete.
|
383
|
+
if transfer_encoding = headers.delete(TRANSFER_ENCODING)
|
384
|
+
# If a message is received with both a Transfer-Encoding and a
|
385
|
+
# Content-Length header field, the Transfer-Encoding overrides the
|
386
|
+
# Content-Length. Such a message might indicate an attempt to
|
387
|
+
# perform request smuggling (Section 9.5) or response splitting
|
388
|
+
# (Section 9.4) and ought to be handled as an error. A sender MUST
|
389
|
+
# remove the received Content-Length field prior to forwarding such
|
390
|
+
# a message downstream.
|
391
|
+
if headers[CONTENT_LENGTH]
|
392
|
+
raise BadRequest, "Message contains both transfer encoding and content length!"
|
393
|
+
end
|
394
|
+
|
395
|
+
if transfer_encoding.last == CHUNKED
|
396
|
+
return read_chunked_body
|
397
|
+
else
|
398
|
+
# If a Transfer-Encoding header field is present in a response and
|
399
|
+
# the chunked transfer coding is not the final encoding, the
|
400
|
+
# message body length is determined by reading the connection until
|
401
|
+
# it is closed by the server. If a Transfer-Encoding header field
|
402
|
+
# is present in a request and the chunked transfer coding is not
|
403
|
+
# the final encoding, the message body length cannot be determined
|
404
|
+
# reliably; the server MUST respond with the 400 (Bad Request)
|
405
|
+
# status code and then close the connection.
|
406
|
+
return read_remainder_body
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# 5. If a valid Content-Length header field is present without
|
411
|
+
# Transfer-Encoding, its decimal value defines the expected message
|
412
|
+
# body length in octets. If the sender closes the connection or
|
413
|
+
# the recipient times out before the indicated number of octets are
|
414
|
+
# received, the recipient MUST consider the message to be
|
415
|
+
# incomplete and close the connection.
|
416
|
+
if content_length = headers.delete(CONTENT_LENGTH)
|
417
|
+
length = Integer(content_length)
|
418
|
+
if length > 0
|
419
|
+
return read_fixed_body(length)
|
420
|
+
elsif length == 0
|
421
|
+
return nil
|
422
|
+
else
|
423
|
+
raise BadRequest, "Invalid content length: #{content_length}"
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
if remainder
|
428
|
+
# 7. Otherwise, this is a response message without a declared message
|
429
|
+
# body length, so the message body length is determined by the
|
430
|
+
# number of octets received prior to the server closing the
|
431
|
+
# connection.
|
432
|
+
return read_remainder_body
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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
|
+
|
21
|
+
require 'protocol/http/error'
|
22
|
+
|
23
|
+
module Protocol
|
24
|
+
module HTTP1
|
25
|
+
class BadRequest < HTTP::BadRequest
|
26
|
+
end
|
27
|
+
|
28
|
+
class ProtocolError < HTTP::ProtocolError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
require_relative 'lib/protocol/http1/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "protocol-http1"
|
6
|
+
spec.version = Protocol::HTTP1::VERSION
|
7
|
+
spec.authors = ["Samuel Williams"]
|
8
|
+
spec.email = ["samuel.williams@oriontransfer.co.nz"]
|
9
|
+
|
10
|
+
spec.summary = "A low level implementation of the HTTP/1 protocol."
|
11
|
+
spec.homepage = "https://github.com/socketry/protocol-http1"
|
12
|
+
spec.license = "MIT"
|
13
|
+
|
14
|
+
# Specify which files should be added to the gem when it is released.
|
15
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
16
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
17
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
end
|
19
|
+
|
20
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency "protocol-http"
|
24
|
+
|
25
|
+
spec.add_development_dependency "covered"
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.17"
|
27
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
28
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: protocol-http1
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Samuel Williams
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-05-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: protocol-http
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: covered
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.17'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.17'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- samuel.williams@oriontransfer.co.nz
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".editorconfig"
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- ".travis.yml"
|
94
|
+
- Gemfile
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- examples/http1/request.rb
|
98
|
+
- lib/protocol/http1.rb
|
99
|
+
- lib/protocol/http1/connection.rb
|
100
|
+
- lib/protocol/http1/error.rb
|
101
|
+
- lib/protocol/http1/version.rb
|
102
|
+
- protocol-http1.gemspec
|
103
|
+
homepage: https://github.com/socketry/protocol-http1
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubygems_version: 3.0.2
|
123
|
+
signing_key:
|
124
|
+
specification_version: 4
|
125
|
+
summary: A low level implementation of the HTTP/1 protocol.
|
126
|
+
test_files: []
|