http-protocol 0.3.2 → 0.4.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.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/http/protocol/headers.rb +4 -0
- data/lib/http/protocol/http10/connection.rb +53 -0
- data/lib/http/protocol/http11/connection.rb +395 -0
- data/lib/http/protocol/http2/continuation_frame.rb +4 -4
- data/lib/http/protocol/http2/frame.rb +15 -15
- data/lib/http/protocol/http2/framer.rb +9 -11
- data/lib/http/protocol/http2/ping_frame.rb +1 -1
- data/lib/http/protocol/http2/priority_frame.rb +1 -1
- data/lib/http/protocol/http2/reset_stream_frame.rb +1 -1
- data/lib/http/protocol/http2/settings_frame.rb +1 -1
- data/lib/http/protocol/reference.rb +188 -0
- data/lib/http/protocol/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 671f05047ee2dfd7365a45589f83f27a0f097c6e0cb787b61e3569863c0c5db4
|
4
|
+
data.tar.gz: fb4f399f497fd3480398522d2b7256800c04e99c944d0a2e00400335811c53b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b1c95fdac9a51fbbd9a285dd0619e7d463f668e85e5a5a4329e0eee5abd18e96cb911ff9311796d14ac36e8515f3803acd4758e73112e4f857cc60bd73b9e60
|
7
|
+
data.tar.gz: ac491a301fe51695e6ac1efae9fe1e73fe13d4b9ad8eac87f43753d60107fbd57e20e5589422e25a7496849d793205cab37db4e501657a7cd82b3deca26496fc
|
data/README.md
CHANGED
@@ -23,11 +23,11 @@ Or install it yourself as:
|
|
23
23
|
### HTTP2
|
24
24
|
|
25
25
|
```ruby
|
26
|
-
framer = HTTP::Protocol::HTTP2::Framer.new(
|
26
|
+
framer = HTTP::Protocol::HTTP2::Framer.new(stream)
|
27
27
|
|
28
28
|
frame = framer.read_frame
|
29
29
|
|
30
|
-
frame.write(
|
30
|
+
frame.write(stream)
|
31
31
|
```
|
32
32
|
|
33
33
|
## Contributing
|
@@ -0,0 +1,53 @@
|
|
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_relative '../http11/connection'
|
22
|
+
|
23
|
+
module HTTP
|
24
|
+
module Protocol
|
25
|
+
module HTTP10
|
26
|
+
class Connection < HTTP11::Connection
|
27
|
+
KEEP_ALIVE = 'keep-alive'.freeze
|
28
|
+
VERSION = "HTTP/1.0".freeze
|
29
|
+
|
30
|
+
def version
|
31
|
+
VERSION
|
32
|
+
end
|
33
|
+
|
34
|
+
def persistent?(headers)
|
35
|
+
if connection = headers[CONNECTION]
|
36
|
+
return connection.include?(KEEP_ALIVE)
|
37
|
+
else
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def write_persistent_header
|
43
|
+
@stream.write("connection: keep-alive\r\n") if @persistent
|
44
|
+
end
|
45
|
+
|
46
|
+
def write_body(body, chunked = false)
|
47
|
+
# We don't support chunked encoding by default.
|
48
|
+
super(body, chunked)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,395 @@
|
|
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_relative '../error'
|
22
|
+
|
23
|
+
module HTTP
|
24
|
+
module Protocol
|
25
|
+
module HTTP11
|
26
|
+
class Connection
|
27
|
+
CRLF = "\r\n".freeze
|
28
|
+
CONNECTION = 'connection'.freeze
|
29
|
+
HOST = 'host'.freeze
|
30
|
+
CLOSE = 'close'.freeze
|
31
|
+
VERSION = "HTTP/1.1".freeze
|
32
|
+
|
33
|
+
def initialize(stream, persistent = true)
|
34
|
+
@stream = stream
|
35
|
+
|
36
|
+
@persistent = persistent
|
37
|
+
|
38
|
+
@count = 0
|
39
|
+
end
|
40
|
+
|
41
|
+
attr :stream
|
42
|
+
attr :persistent
|
43
|
+
attr :count
|
44
|
+
|
45
|
+
def version
|
46
|
+
VERSION
|
47
|
+
end
|
48
|
+
|
49
|
+
def persistent?(headers)
|
50
|
+
if connection = headers[CONNECTION]
|
51
|
+
return !connection.include?(CLOSE)
|
52
|
+
else
|
53
|
+
return true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Effectively close the connection and return the underlying IO.
|
58
|
+
# @return [IO] the underlying non-blocking IO.
|
59
|
+
def hijack
|
60
|
+
@persistent = false
|
61
|
+
|
62
|
+
@stream.flush
|
63
|
+
|
64
|
+
return @stream
|
65
|
+
end
|
66
|
+
|
67
|
+
# Write the appropriate header for connection persistence.
|
68
|
+
def write_persistent_header
|
69
|
+
@stream.write("connection: keep-alive\r\n") if @persistent
|
70
|
+
end
|
71
|
+
|
72
|
+
# Close the connection and underlying stream.
|
73
|
+
def close
|
74
|
+
@stream.close
|
75
|
+
end
|
76
|
+
|
77
|
+
def write_request(authority, method, path, version, headers)
|
78
|
+
@stream.write("#{method} #{path} #{version}\r\n")
|
79
|
+
@stream.write("host: #{authority}\r\n")
|
80
|
+
|
81
|
+
write_headers(headers)
|
82
|
+
|
83
|
+
@stream.flush
|
84
|
+
end
|
85
|
+
|
86
|
+
def write_response(version, status, headers, body = nil, head = false)
|
87
|
+
@stream.write("#{version} #{status}\r\n")
|
88
|
+
write_headers(headers)
|
89
|
+
|
90
|
+
if head
|
91
|
+
write_body_head(body)
|
92
|
+
else
|
93
|
+
write_body(body)
|
94
|
+
end
|
95
|
+
|
96
|
+
@stream.flush
|
97
|
+
end
|
98
|
+
|
99
|
+
def write_headers(headers)
|
100
|
+
headers.each do |name, value|
|
101
|
+
@stream.write("#{name}: #{value}\r\n")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def each_line
|
106
|
+
while line = read_line
|
107
|
+
yield line
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def read_line
|
112
|
+
# To support Ruby 2.3, we do the following which is pretty inefficient. Ruby 2.4+ can do the following:
|
113
|
+
# @stream.gets(CRLF, chomp: true) or raise EOFError
|
114
|
+
if line = @stream.gets(CRLF)
|
115
|
+
return line.chomp!(CRLF)
|
116
|
+
else
|
117
|
+
raise EOFError
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def read_request
|
122
|
+
method, path, version = read_line.split(/\s+/, 3)
|
123
|
+
headers = read_headers
|
124
|
+
|
125
|
+
@persistent = persistent?(headers)
|
126
|
+
|
127
|
+
body = read_request_body(headers)
|
128
|
+
|
129
|
+
@count += 1
|
130
|
+
|
131
|
+
return headers[HOST], method, path, version, headers, body
|
132
|
+
end
|
133
|
+
|
134
|
+
def read_response(method)
|
135
|
+
version, status, reason = read_line.split(/\s+/, 3)
|
136
|
+
Async.logger.debug(self) {"#{version} #{status} #{reason}"}
|
137
|
+
|
138
|
+
headers = read_headers
|
139
|
+
|
140
|
+
@persistent = persistent?(headers)
|
141
|
+
|
142
|
+
body = read_response_body(method, status, headers)
|
143
|
+
|
144
|
+
@count += 1
|
145
|
+
|
146
|
+
return version, Integer(status), reason, headers, body
|
147
|
+
end
|
148
|
+
|
149
|
+
def read_headers
|
150
|
+
fields = []
|
151
|
+
|
152
|
+
self.each_line do |line|
|
153
|
+
if line =~ /^([a-zA-Z\-\d]+):\s*(.+?)\s*$/
|
154
|
+
fields << [$1, $2]
|
155
|
+
else
|
156
|
+
break
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
return Headers.new(fields)
|
161
|
+
end
|
162
|
+
|
163
|
+
def read_chunk
|
164
|
+
length = self.read_line.to_i(16)
|
165
|
+
|
166
|
+
if length == 0
|
167
|
+
self.read_line
|
168
|
+
|
169
|
+
return nil
|
170
|
+
end
|
171
|
+
|
172
|
+
# Read the data:
|
173
|
+
chunk = @stream.read(length)
|
174
|
+
|
175
|
+
# Consume the trailing CRLF:
|
176
|
+
crlf = @stream.read(2)
|
177
|
+
|
178
|
+
return chunk
|
179
|
+
end
|
180
|
+
|
181
|
+
def write_chunk(chunk)
|
182
|
+
if chunk.nil?
|
183
|
+
@stream.write("0\r\n\r\n")
|
184
|
+
elsif !chunk.empty?
|
185
|
+
@stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n")
|
186
|
+
@stream.write(chunk)
|
187
|
+
@stream.write(CRLF)
|
188
|
+
@stream.flush
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def write_empty_body(body)
|
193
|
+
# Write empty body:
|
194
|
+
write_persistent_header
|
195
|
+
@stream.write("content-length: 0\r\n\r\n")
|
196
|
+
|
197
|
+
body.read if body
|
198
|
+
|
199
|
+
@stream.flush
|
200
|
+
end
|
201
|
+
|
202
|
+
def write_fixed_length_body(body, length)
|
203
|
+
write_persistent_header
|
204
|
+
@stream.write("content-length: #{length}\r\n\r\n")
|
205
|
+
|
206
|
+
chunk_length = 0
|
207
|
+
body.each do |chunk|
|
208
|
+
chunk_length += chunk.bytesize
|
209
|
+
|
210
|
+
if chunk_length > length
|
211
|
+
raise ArgumentError, "Trying to write #{chunk_length} bytes, but content length was #{length} bytes!"
|
212
|
+
end
|
213
|
+
|
214
|
+
@stream.write(chunk)
|
215
|
+
end
|
216
|
+
|
217
|
+
@stream.flush
|
218
|
+
|
219
|
+
if chunk_length != length
|
220
|
+
raise ArgumentError, "Wrote #{chunk_length} bytes, but content length was #{length} bytes!"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def write_chunked_body(body)
|
225
|
+
write_persistent_header
|
226
|
+
@stream.write("transfer-encoding: chunked\r\n\r\n")
|
227
|
+
|
228
|
+
body.each do |chunk|
|
229
|
+
next if chunk.size == 0
|
230
|
+
|
231
|
+
@stream.write("#{chunk.bytesize.to_s(16).upcase}\r\n")
|
232
|
+
@stream.write(chunk)
|
233
|
+
@stream.write(CRLF)
|
234
|
+
@stream.flush
|
235
|
+
end
|
236
|
+
|
237
|
+
@stream.write("0\r\n\r\n")
|
238
|
+
@stream.flush
|
239
|
+
end
|
240
|
+
|
241
|
+
def write_body_and_close(body)
|
242
|
+
# We can't be persistent because we don't know the data length:
|
243
|
+
@persistent = false
|
244
|
+
write_persistent_header
|
245
|
+
|
246
|
+
@stream.write("\r\n")
|
247
|
+
|
248
|
+
body.each do |chunk|
|
249
|
+
@stream.write(chunk)
|
250
|
+
@stream.flush
|
251
|
+
end
|
252
|
+
|
253
|
+
@stream.stream.close_write
|
254
|
+
end
|
255
|
+
|
256
|
+
def write_body(body, chunked = true)
|
257
|
+
if body.nil? or body.empty?
|
258
|
+
write_empty_body(body)
|
259
|
+
elsif length = body.length
|
260
|
+
write_fixed_length_body(body, length)
|
261
|
+
elsif chunked
|
262
|
+
write_chunked_body(body)
|
263
|
+
else
|
264
|
+
write_body_and_close(body)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def write_body_head(body)
|
269
|
+
write_persistent_header
|
270
|
+
|
271
|
+
if body.nil? or body.empty?
|
272
|
+
@stream.write("content-length: 0\r\n\r\n")
|
273
|
+
elsif length = body.length
|
274
|
+
@stream.write("content-length: #{length}\r\n\r\n")
|
275
|
+
else
|
276
|
+
@stream.write("\r\n")
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def read_chunked_body
|
281
|
+
buffer = String.new.b
|
282
|
+
|
283
|
+
while chunk = read_chunk
|
284
|
+
buffer << chunk
|
285
|
+
chunk.clear
|
286
|
+
end
|
287
|
+
|
288
|
+
return buffer
|
289
|
+
end
|
290
|
+
|
291
|
+
def read_fixed_body(length)
|
292
|
+
@stream.read(length)
|
293
|
+
end
|
294
|
+
|
295
|
+
def read_tunnel_body
|
296
|
+
read_remainder_body
|
297
|
+
end
|
298
|
+
|
299
|
+
def read_remainder_body
|
300
|
+
@stream.read
|
301
|
+
end
|
302
|
+
|
303
|
+
HEAD = "HEAD".freeze
|
304
|
+
CONNECT = "CONNECT".freeze
|
305
|
+
|
306
|
+
def read_response_body(method, status, headers)
|
307
|
+
# RFC 7230 3.3.3
|
308
|
+
# 1. Any response to a HEAD request and any response with a 1xx
|
309
|
+
# (Informational), 204 (No Content), or 304 (Not Modified) status
|
310
|
+
# code is always terminated by the first empty line after the
|
311
|
+
# header fields, regardless of the header fields present in the
|
312
|
+
# message, and thus cannot contain a message body.
|
313
|
+
if method == "HEAD" or status == 204 or status == 304
|
314
|
+
return nil
|
315
|
+
end
|
316
|
+
|
317
|
+
# 2. Any 2xx (Successful) response to a CONNECT request implies that
|
318
|
+
# the connection will become a tunnel immediately after the empty
|
319
|
+
# line that concludes the header fields. A client MUST ignore any
|
320
|
+
# Content-Length or Transfer-Encoding header fields received in
|
321
|
+
# such a message.
|
322
|
+
if method == "CONNECT" and status == 200
|
323
|
+
return read_tunnel_body
|
324
|
+
end
|
325
|
+
|
326
|
+
if body = read_body(headers)
|
327
|
+
return body
|
328
|
+
else
|
329
|
+
# 7. Otherwise, this is a response message without a declared message
|
330
|
+
# body length, so the message body length is determined by the
|
331
|
+
# number of octets received prior to the server closing the
|
332
|
+
# connection.
|
333
|
+
return read_remainder_body
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def read_request_body(headers)
|
338
|
+
# 6. If this is a request message and none of the above are true, then
|
339
|
+
# the message body length is zero (no message body is present).
|
340
|
+
if body = read_body(headers)
|
341
|
+
return body
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def read_body(headers)
|
346
|
+
# 3. If a Transfer-Encoding header field is present and the chunked
|
347
|
+
# transfer coding (Section 4.1) is the final encoding, the message
|
348
|
+
# body length is determined by reading and decoding the chunked
|
349
|
+
# data until the transfer coding indicates the data is complete.
|
350
|
+
if transfer_encoding = headers[TRANSFER_ENCODING]
|
351
|
+
# If a message is received with both a Transfer-Encoding and a
|
352
|
+
# Content-Length header field, the Transfer-Encoding overrides the
|
353
|
+
# Content-Length. Such a message might indicate an attempt to
|
354
|
+
# perform request smuggling (Section 9.5) or response splitting
|
355
|
+
# (Section 9.4) and ought to be handled as an error. A sender MUST
|
356
|
+
# remove the received Content-Length field prior to forwarding such
|
357
|
+
# a message downstream.
|
358
|
+
if headers[CONTENT_LENGTH]
|
359
|
+
raise BadRequest, "Message contains both transfer encoding and content length!"
|
360
|
+
end
|
361
|
+
|
362
|
+
if transfer_encoding.last == CHUNKED
|
363
|
+
return read_chunked_body
|
364
|
+
else
|
365
|
+
# If a Transfer-Encoding header field is present in a response and
|
366
|
+
# the chunked transfer coding is not the final encoding, the
|
367
|
+
# message body length is determined by reading the connection until
|
368
|
+
# it is closed by the server. If a Transfer-Encoding header field
|
369
|
+
# is present in a request and the chunked transfer coding is not
|
370
|
+
# the final encoding, the message body length cannot be determined
|
371
|
+
# reliably; the server MUST respond with the 400 (Bad Request)
|
372
|
+
# status code and then close the connection.
|
373
|
+
return read_body_remainder
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
# 5. If a valid Content-Length header field is present without
|
378
|
+
# Transfer-Encoding, its decimal value defines the expected message
|
379
|
+
# body length in octets. If the sender closes the connection or
|
380
|
+
# the recipient times out before the indicated number of octets are
|
381
|
+
# received, the recipient MUST consider the message to be
|
382
|
+
# incomplete and close the connection.
|
383
|
+
if content_length = headers[CONTENT_LENGTH]
|
384
|
+
length = Integer(content_length)
|
385
|
+
if length >= 0
|
386
|
+
return read_fixed_body(length)
|
387
|
+
else
|
388
|
+
raise BadRequest, "Invalid content length: #{content_length}"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
@@ -34,21 +34,21 @@ module HTTP
|
|
34
34
|
flag_set?(END_HEADERS)
|
35
35
|
end
|
36
36
|
|
37
|
-
def read(
|
37
|
+
def read(stream, maximum_frame_size)
|
38
38
|
super
|
39
39
|
|
40
40
|
unless end_headers?
|
41
41
|
@continuation = ContinuationFrame.new
|
42
42
|
|
43
|
-
@continuation.read(
|
43
|
+
@continuation.read(stream, maximum_frame_size)
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
def write(
|
47
|
+
def write(stream)
|
48
48
|
super
|
49
49
|
|
50
50
|
if continuation = self.continuation
|
51
|
-
continuation.write(
|
51
|
+
continuation.write(stream)
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
@@ -36,7 +36,7 @@ module HTTP
|
|
36
36
|
include Comparable
|
37
37
|
|
38
38
|
# Stream Identifier cannot be bigger than this:
|
39
|
-
# https://http2.github.
|
39
|
+
# https://http2.github.stream/http2-spec/#rfc.section.4.1
|
40
40
|
VALID_STREAM_ID = 0..0x7fffffff
|
41
41
|
|
42
42
|
# The absolute maximum bounds for the length field:
|
@@ -151,39 +151,39 @@ module HTTP
|
|
151
151
|
return length, type, flags, stream_id
|
152
152
|
end
|
153
153
|
|
154
|
-
def read_header(
|
155
|
-
@length, @type, @flags, @stream_id = Frame.parse_header(
|
154
|
+
def read_header(stream)
|
155
|
+
@length, @type, @flags, @stream_id = Frame.parse_header(stream.read(9))
|
156
156
|
end
|
157
157
|
|
158
|
-
def read_payload(
|
159
|
-
@payload =
|
158
|
+
def read_payload(stream)
|
159
|
+
@payload = stream.read(@length)
|
160
160
|
end
|
161
161
|
|
162
|
-
def read(
|
163
|
-
read_header(
|
162
|
+
def read(stream, maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
|
163
|
+
read_header(stream) unless @length
|
164
164
|
|
165
165
|
if @length > maximum_frame_size
|
166
166
|
raise FrameSizeError, "Frame length #{@length} exceeds maximum frame size #{maximum_frame_size}!"
|
167
167
|
end
|
168
168
|
|
169
|
-
read_payload(
|
169
|
+
read_payload(stream)
|
170
170
|
end
|
171
171
|
|
172
|
-
def write_header(
|
173
|
-
|
172
|
+
def write_header(stream)
|
173
|
+
stream.write self.header
|
174
174
|
end
|
175
175
|
|
176
|
-
def write_payload(
|
177
|
-
|
176
|
+
def write_payload(stream)
|
177
|
+
stream.write(@payload) if @payload
|
178
178
|
end
|
179
179
|
|
180
|
-
def write(
|
180
|
+
def write(stream)
|
181
181
|
if @payload and @length != @payload.bytesize
|
182
182
|
raise ProtocolError, "Invalid payload size: #{@length} != #{@payload.bytesize}"
|
183
183
|
end
|
184
184
|
|
185
|
-
self.write_header(
|
186
|
-
self.write_payload(
|
185
|
+
self.write_header(stream)
|
186
|
+
self.write_payload(stream)
|
187
187
|
end
|
188
188
|
|
189
189
|
def apply(connection)
|
@@ -53,23 +53,21 @@ module HTTP
|
|
53
53
|
CONNECTION_PREFACE_MAGIC = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".freeze
|
54
54
|
|
55
55
|
class Framer
|
56
|
-
def initialize(
|
57
|
-
@
|
56
|
+
def initialize(stream, frames = FRAMES)
|
57
|
+
@stream = stream
|
58
58
|
@frames = frames
|
59
|
-
|
60
|
-
@buffer = String.new.b
|
61
59
|
end
|
62
60
|
|
63
61
|
def close
|
64
|
-
@
|
62
|
+
@stream.close
|
65
63
|
end
|
66
64
|
|
67
65
|
def write_connection_preface
|
68
|
-
@
|
66
|
+
@stream.write(CONNECTION_PREFACE_MAGIC)
|
69
67
|
end
|
70
68
|
|
71
69
|
def read_connection_preface
|
72
|
-
string = @
|
70
|
+
string = @stream.read(CONNECTION_PREFACE_MAGIC.bytesize)
|
73
71
|
|
74
72
|
unless string == CONNECTION_PREFACE_MAGIC
|
75
73
|
raise ProtocolError, "Invalid connection preface: #{string.inspect}"
|
@@ -89,20 +87,20 @@ module HTTP
|
|
89
87
|
frame = klass.new(stream_id, flags, type, length)
|
90
88
|
|
91
89
|
# Read the payload:
|
92
|
-
frame.read(@
|
90
|
+
frame.read(@stream, maximum_frame_size)
|
93
91
|
|
94
92
|
return frame
|
95
93
|
end
|
96
94
|
|
97
95
|
def write_frame(frame)
|
98
96
|
# puts "framer: write_frame #{frame.inspect}"
|
99
|
-
frame.write(@
|
97
|
+
frame.write(@stream)
|
100
98
|
|
101
|
-
@
|
99
|
+
@stream.flush
|
102
100
|
end
|
103
101
|
|
104
102
|
def read_header
|
105
|
-
if buffer = @
|
103
|
+
if buffer = @stream.read(9)
|
106
104
|
return Frame.parse_header(buffer)
|
107
105
|
else
|
108
106
|
# TODO: Is this necessary? I thought the IO would throw this.
|
@@ -0,0 +1,188 @@
|
|
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
|
+
module HTTP
|
22
|
+
module Protocol
|
23
|
+
# A relative reference, excluding any authority.
|
24
|
+
class Reference
|
25
|
+
def initialize(path, query_string, fragment, parameters)
|
26
|
+
@path = path
|
27
|
+
@query_string = query_string
|
28
|
+
@fragment = fragment
|
29
|
+
@parameters = parameters
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.[] reference
|
33
|
+
if reference.is_a? self
|
34
|
+
return reference
|
35
|
+
else
|
36
|
+
return self.parse(reference)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Generate a reference from a path and user parameters. The path may contain a `#fragment` or `?query=parameters`.
|
41
|
+
def self.parse(path = '/', parameters = nil)
|
42
|
+
base, fragment = path.split('#', 2)
|
43
|
+
path, query_string = base.split('?', 2)
|
44
|
+
|
45
|
+
self.new(path, query_string, fragment, parameters)
|
46
|
+
end
|
47
|
+
|
48
|
+
# The path component, e.g. /foo/bar/index.html
|
49
|
+
attr :path
|
50
|
+
|
51
|
+
# The un-parsed query string, e.g. 'x=10&y=20'
|
52
|
+
attr :query_string
|
53
|
+
|
54
|
+
# A fragment, the part after the '#'
|
55
|
+
attr :fragment
|
56
|
+
|
57
|
+
# User supplied parameters that will be appended to the query part.
|
58
|
+
attr :parameters
|
59
|
+
|
60
|
+
def parameters?
|
61
|
+
@parameters and !@parameters.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def query_string?
|
65
|
+
@query_string and !@query_string.empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
def fragment?
|
69
|
+
@fragment and !@fragment.empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
def append(buffer)
|
73
|
+
if query_string?
|
74
|
+
buffer << escape_path(@path) << '?' << @query_string
|
75
|
+
buffer << '&' << encode(@parameters) if parameters?
|
76
|
+
else
|
77
|
+
buffer << escape_path(@path)
|
78
|
+
buffer << '?' << encode(@parameters) if parameters?
|
79
|
+
end
|
80
|
+
|
81
|
+
if fragment?
|
82
|
+
buffer << '#' << escape(@fragment)
|
83
|
+
end
|
84
|
+
|
85
|
+
return buffer
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_str
|
89
|
+
append(String.new)
|
90
|
+
end
|
91
|
+
|
92
|
+
alias to_s to_str
|
93
|
+
|
94
|
+
def + other
|
95
|
+
other = self.class[other]
|
96
|
+
|
97
|
+
self.class.new(
|
98
|
+
expand_path(self.path, other.path),
|
99
|
+
other.query_string,
|
100
|
+
other.fragment,
|
101
|
+
other.parameters,
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
def [] parameters
|
106
|
+
self.dup(nil, parameters)
|
107
|
+
end
|
108
|
+
|
109
|
+
def dup(path = nil, parameters = nil)
|
110
|
+
if @parameters
|
111
|
+
if parameters
|
112
|
+
parameters = @parameters.merge(parameters)
|
113
|
+
else
|
114
|
+
parameters = @parameters
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
if path
|
119
|
+
path = @path + '/' + path
|
120
|
+
else
|
121
|
+
path = @path
|
122
|
+
end
|
123
|
+
|
124
|
+
self.class.new(path, @query_string, @fragment, parameters)
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def expand_path(base, relative)
|
130
|
+
if relative.start_with? '/'
|
131
|
+
return relative
|
132
|
+
else
|
133
|
+
path = base.split('/')
|
134
|
+
parts = relative.split('/')
|
135
|
+
|
136
|
+
parts.each do |part|
|
137
|
+
if part == '..'
|
138
|
+
path.pop
|
139
|
+
else
|
140
|
+
path << part
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
return path.join('/')
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Escapes a generic string, using percent encoding.
|
149
|
+
def escape(string)
|
150
|
+
encoding = string.encoding
|
151
|
+
string.b.gsub(/([^a-zA-Z0-9_.\-]+)/) do |m|
|
152
|
+
'%' + m.unpack('H2' * m.bytesize).join('%').upcase
|
153
|
+
end.force_encoding(encoding)
|
154
|
+
end
|
155
|
+
|
156
|
+
# According to https://tools.ietf.org/html/rfc3986#section-3.3, we escape non-pchar.
|
157
|
+
NON_PCHAR = /([^a-zA-Z0-9_\-\.~!$&'()*+,;=:@\/]+)/.freeze
|
158
|
+
|
159
|
+
# Escapes a path
|
160
|
+
def escape_path(path)
|
161
|
+
encoding = path.encoding
|
162
|
+
path.b.gsub(NON_PCHAR) do |m|
|
163
|
+
'%' + m.unpack('H2' * m.bytesize).join('%').upcase
|
164
|
+
end.force_encoding(encoding)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Encodes a hash or array into a query string
|
168
|
+
def encode(value, prefix = nil)
|
169
|
+
case value
|
170
|
+
when Array
|
171
|
+
return value.map {|v|
|
172
|
+
encode(v, "#{prefix}[]")
|
173
|
+
}.join("&")
|
174
|
+
when Hash
|
175
|
+
return value.map {|k, v|
|
176
|
+
encode(v, prefix ? "#{prefix}[#{escape(k.to_s)}]" : escape(k.to_s))
|
177
|
+
}.reject(&:empty?).join('&')
|
178
|
+
when nil
|
179
|
+
return prefix
|
180
|
+
else
|
181
|
+
raise ArgumentError, "value must be a Hash" if prefix.nil?
|
182
|
+
|
183
|
+
return "#{prefix}=#{escape(value.to_s)}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: http-protocol
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.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: 2018-
|
11
|
+
date: 2018-09-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http-hpack
|
@@ -83,6 +83,8 @@ files:
|
|
83
83
|
- lib/http/protocol.rb
|
84
84
|
- lib/http/protocol/error.rb
|
85
85
|
- lib/http/protocol/headers.rb
|
86
|
+
- lib/http/protocol/http10/connection.rb
|
87
|
+
- lib/http/protocol/http11/connection.rb
|
86
88
|
- lib/http/protocol/http2/client.rb
|
87
89
|
- lib/http/protocol/http2/connection.rb
|
88
90
|
- lib/http/protocol/http2/continuation_frame.rb
|
@@ -101,6 +103,7 @@ files:
|
|
101
103
|
- lib/http/protocol/http2/settings_frame.rb
|
102
104
|
- lib/http/protocol/http2/stream.rb
|
103
105
|
- lib/http/protocol/http2/window_update_frame.rb
|
106
|
+
- lib/http/protocol/reference.rb
|
104
107
|
- lib/http/protocol/version.rb
|
105
108
|
homepage: https://github.com/socketry/http-protocol
|
106
109
|
licenses: []
|
@@ -121,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
124
|
version: '0'
|
122
125
|
requirements: []
|
123
126
|
rubyforge_project:
|
124
|
-
rubygems_version: 2.7.
|
127
|
+
rubygems_version: 2.7.7
|
125
128
|
signing_key:
|
126
129
|
specification_version: 4
|
127
130
|
summary: Provides abstractions to handle HTTP1 and HTTP2 protocols.
|