h1p 0.1
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 +7 -0
- data/.github/workflows/test.yml +31 -0
- data/.gitignore +57 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +33 -0
- data/LICENSE +21 -0
- data/README.md +5 -0
- data/Rakefile +16 -0
- data/TODO.md +106 -0
- data/benchmarks/bm_http1_parser.rb +85 -0
- data/examples/http_server.rb +41 -0
- data/ext/h1p/extconf.rb +13 -0
- data/ext/h1p/h1p.c +860 -0
- data/ext/h1p/h1p.h +18 -0
- data/ext/h1p/limits.rb +10 -0
- data/h1p.gemspec +25 -0
- data/lib/h1p.rb +31 -0
- data/lib/h1p/version.rb +5 -0
- data/test/helper.rb +15 -0
- data/test/test_h1p.rb +584 -0
- metadata +125 -0
data/ext/h1p/h1p.h
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#ifndef H1P_H
|
2
|
+
#define H1P_H
|
3
|
+
|
4
|
+
#include "ruby.h"
|
5
|
+
|
6
|
+
// debugging
|
7
|
+
#define OBJ_ID(obj) (NUM2LONG(rb_funcall(obj, rb_intern("object_id"), 0)))
|
8
|
+
#define INSPECT(str, obj) { printf(str); VALUE s = rb_funcall(obj, rb_intern("inspect"), 0); printf(": %s\n", StringValueCStr(s)); }
|
9
|
+
#define TRACE_CALLER() { VALUE c = rb_funcall(rb_mKernel, rb_intern("caller"), 0); INSPECT("caller: ", c); }
|
10
|
+
#define TRACE_C_STACK() { \
|
11
|
+
void *entries[10]; \
|
12
|
+
size_t size = backtrace(entries, 10); \
|
13
|
+
char **strings = backtrace_symbols(entries, size); \
|
14
|
+
for (unsigned long i = 0; i < size; i++) printf("%s\n", strings[i]); \
|
15
|
+
free(strings); \
|
16
|
+
}
|
17
|
+
|
18
|
+
#endif /* H1P_H */
|
data/ext/h1p/limits.rb
ADDED
data/h1p.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative './lib/h1p/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'h1p'
|
5
|
+
s.version = H1P::VERSION
|
6
|
+
s.licenses = ['MIT']
|
7
|
+
s.summary = 'H1P is a blocking HTTP/1 parser for Ruby'
|
8
|
+
s.author = 'Sharon Rosner'
|
9
|
+
s.email = 'sharon@noteflakes.com'
|
10
|
+
s.files = `git ls-files`.split
|
11
|
+
s.homepage = 'http://github.com/digital-fabric/h1p'
|
12
|
+
s.metadata = {
|
13
|
+
"source_code_uri" => "https://github.com/digital-fabric/h1p"
|
14
|
+
}
|
15
|
+
s.rdoc_options = ["--title", "h1p", "--main", "README.md"]
|
16
|
+
s.extra_rdoc_files = ["README.md"]
|
17
|
+
s.extensions = ["ext/h1p/extconf.rb"]
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
s.required_ruby_version = '>= 2.6'
|
20
|
+
|
21
|
+
s.add_development_dependency 'rake-compiler', '1.1.1'
|
22
|
+
s.add_development_dependency 'rake', '~>12.3.3'
|
23
|
+
s.add_development_dependency 'minitest', '~>5.11.3'
|
24
|
+
s.add_development_dependency 'minitest-reporters', '~>1.4.2'
|
25
|
+
end
|
data/lib/h1p.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'h1p_ext'
|
4
|
+
|
5
|
+
unless Object.const_defined?('Polyphony')
|
6
|
+
class IO
|
7
|
+
def __parser_read_method__
|
8
|
+
:stock_readpartial
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'socket'
|
13
|
+
|
14
|
+
class Socket
|
15
|
+
def __parser_read_method__
|
16
|
+
:stock_readpartial
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class TCPSocket
|
21
|
+
def __parser_read_method__
|
22
|
+
:stock_readpartial
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class UNIXSocket
|
27
|
+
def __parser_read_method__
|
28
|
+
:stock_readpartial
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/h1p/version.rb
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
require 'minitest/autorun'
|
8
|
+
require 'minitest/reporters'
|
9
|
+
|
10
|
+
module Minitest::Assertions
|
11
|
+
def assert_in_range exp_range, act
|
12
|
+
msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
|
13
|
+
assert exp_range.include?(act), msg
|
14
|
+
end
|
15
|
+
end
|
data/test/test_h1p.rb
ADDED
@@ -0,0 +1,584 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helper'
|
4
|
+
require 'h1p'
|
5
|
+
require 'socket'
|
6
|
+
require_relative '../ext/h1p/limits'
|
7
|
+
|
8
|
+
class H1PTest < MiniTest::Test
|
9
|
+
Error = H1P::Error
|
10
|
+
|
11
|
+
def setup
|
12
|
+
super
|
13
|
+
@i, @o = IO.pipe
|
14
|
+
@parser = H1P::Parser.new(@i)
|
15
|
+
end
|
16
|
+
alias_method :reset_parser, :setup
|
17
|
+
|
18
|
+
def test_request_line
|
19
|
+
msg = "GET / HTTP/1.1\r\n\r\n"
|
20
|
+
@o << msg
|
21
|
+
headers = @parser.parse_headers
|
22
|
+
|
23
|
+
assert_equal(
|
24
|
+
{
|
25
|
+
':method' => 'get',
|
26
|
+
':path' => '/',
|
27
|
+
':protocol' => 'http/1.1',
|
28
|
+
':rx' => msg.bytesize
|
29
|
+
},
|
30
|
+
headers
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_request_line_whitespace
|
35
|
+
msg = "GET / HTTP/1.1\r\n\r\n"
|
36
|
+
@o << msg
|
37
|
+
headers = @parser.parse_headers
|
38
|
+
|
39
|
+
assert_equal(
|
40
|
+
{
|
41
|
+
':method' => 'get',
|
42
|
+
':path' => '/',
|
43
|
+
':protocol' => 'http/1.1',
|
44
|
+
':rx' => msg.bytesize
|
45
|
+
},
|
46
|
+
headers
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_eof
|
51
|
+
@o << "GET / HTTP/1.1"
|
52
|
+
@o.close
|
53
|
+
|
54
|
+
assert_nil @parser.parse_headers
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_method_case
|
58
|
+
@o << "GET / HTTP/1.1\r\n\r\n"
|
59
|
+
headers = @parser.parse_headers
|
60
|
+
assert_equal 'get', headers[':method']
|
61
|
+
|
62
|
+
reset_parser
|
63
|
+
@o << "post / HTTP/1.1\r\n\r\n"
|
64
|
+
headers = @parser.parse_headers
|
65
|
+
assert_equal 'post', headers[':method']
|
66
|
+
|
67
|
+
reset_parser
|
68
|
+
@o << "PoST / HTTP/1.1\r\n\r\n"
|
69
|
+
headers = @parser.parse_headers
|
70
|
+
assert_equal 'post', headers[':method']
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_bad_method
|
74
|
+
@o << " / HTTP/1.1\r\n\r\n"
|
75
|
+
@o.close
|
76
|
+
|
77
|
+
assert_raises(Error) { @parser.parse_headers }
|
78
|
+
|
79
|
+
max_length = H1P_LIMITS[:max_method_length]
|
80
|
+
|
81
|
+
reset_parser
|
82
|
+
@o << "#{'a' * max_length} / HTTP/1.1\r\n\r\n"
|
83
|
+
assert_equal 'a' * max_length, @parser.parse_headers[':method']
|
84
|
+
|
85
|
+
reset_parser
|
86
|
+
@o << "#{'a' * (max_length + 1)} / HTTP/1.1\r\n\r\n"
|
87
|
+
assert_raises(Error) { @parser.parse_headers }
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_path_characters
|
91
|
+
@o << "GET /äBçDé¤23~{@€ HTTP/1.1\r\n\r\n"
|
92
|
+
headers = @parser.parse_headers
|
93
|
+
assert_equal '/äBçDé¤23~{@€', headers[':path']
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_bad_path
|
97
|
+
@o << "GET HTTP/1.1\r\n\r\n"
|
98
|
+
assert_raises(Error) { @parser.parse_headers }
|
99
|
+
|
100
|
+
max_length = H1P_LIMITS[:max_path_length]
|
101
|
+
|
102
|
+
reset_parser
|
103
|
+
@o << "get #{'a' * max_length} HTTP/1.1\r\n\r\n"
|
104
|
+
assert_equal 'a' * max_length, @parser.parse_headers[':path']
|
105
|
+
|
106
|
+
reset_parser
|
107
|
+
@o << "get #{'a' * (max_length + 1)} HTTP/1.1\r\n\r\n"
|
108
|
+
assert_raises(Error) { @parser.parse_headers }
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_protocol
|
112
|
+
@o << "GET / http/1\r\n\r\n"
|
113
|
+
headers = @parser.parse_headers
|
114
|
+
assert_equal 'http/1', headers[':protocol']
|
115
|
+
|
116
|
+
reset_parser
|
117
|
+
@o << "GET / HTTP/1\r\n\r\n"
|
118
|
+
headers = @parser.parse_headers
|
119
|
+
assert_equal 'http/1', headers[':protocol']
|
120
|
+
|
121
|
+
reset_parser
|
122
|
+
@o << "GET / HTTP/1.0\r\n\r\n"
|
123
|
+
headers = @parser.parse_headers
|
124
|
+
assert_equal 'http/1.0', headers[':protocol']
|
125
|
+
|
126
|
+
@o << "GET / HttP/1.1\r\n\r\n"
|
127
|
+
headers = @parser.parse_headers
|
128
|
+
assert_equal 'http/1.1', headers[':protocol']
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_bad_protocol
|
132
|
+
@o << "GET / blah\r\n\r\n"
|
133
|
+
assert_raises(Error) { @parser.parse_headers }
|
134
|
+
|
135
|
+
reset_parser
|
136
|
+
@o << "GET / http\r\n\r\n"
|
137
|
+
assert_raises(Error) { @parser.parse_headers }
|
138
|
+
|
139
|
+
reset_parser
|
140
|
+
@o << "GET / http/2\r\n\r\n"
|
141
|
+
assert_raises(Error) { @parser.parse_headers }
|
142
|
+
|
143
|
+
reset_parser
|
144
|
+
@o << "GET / http/1.\r\n\r\n"
|
145
|
+
assert_raises(Error) { @parser.parse_headers }
|
146
|
+
|
147
|
+
reset_parser
|
148
|
+
@o << "GET / http/a.1\r\n\r\n"
|
149
|
+
assert_raises(Error) { @parser.parse_headers }
|
150
|
+
|
151
|
+
reset_parser
|
152
|
+
@o << "GET / http/1.1.1\r\n\r\n"
|
153
|
+
assert_raises(Error) { @parser.parse_headers }
|
154
|
+
end
|
155
|
+
|
156
|
+
def test_headers_eof
|
157
|
+
@o << "GET / HTTP/1.1\r\na"
|
158
|
+
@o.close
|
159
|
+
|
160
|
+
assert_nil @parser.parse_headers
|
161
|
+
|
162
|
+
reset_parser
|
163
|
+
@o << "GET / HTTP/1.1\r\na:"
|
164
|
+
@o.close
|
165
|
+
|
166
|
+
assert_nil @parser.parse_headers
|
167
|
+
|
168
|
+
reset_parser
|
169
|
+
@o << "GET / HTTP/1.1\r\na: "
|
170
|
+
@o.close
|
171
|
+
|
172
|
+
assert_nil @parser.parse_headers
|
173
|
+
end
|
174
|
+
|
175
|
+
def test_headers
|
176
|
+
@o << "GET / HTTP/1.1\r\nFoo: Bar\r\n\r\n"
|
177
|
+
headers = @parser.parse_headers
|
178
|
+
assert_equal [':method', ':path', ':protocol', 'foo', ':rx'], headers.keys
|
179
|
+
assert_equal 'Bar', headers['foo']
|
180
|
+
|
181
|
+
reset_parser
|
182
|
+
@o << "GET / HTTP/1.1\r\nFOO: baR\r\n\r\n"
|
183
|
+
headers = @parser.parse_headers
|
184
|
+
assert_equal 'baR', headers['foo']
|
185
|
+
|
186
|
+
reset_parser
|
187
|
+
@o << "GET / HTTP/1.1\r\na: bbb\r\nc: ddd\r\n\r\n"
|
188
|
+
headers = @parser.parse_headers
|
189
|
+
assert_equal 'bbb', headers['a']
|
190
|
+
assert_equal 'ddd', headers['c']
|
191
|
+
end
|
192
|
+
|
193
|
+
def test_headers_multiple_values
|
194
|
+
@o << "GET / HTTP/1.1\r\nFoo: Bar\r\nfoo: baz\r\n\r\n"
|
195
|
+
headers = @parser.parse_headers
|
196
|
+
assert_equal ['Bar', 'baz'], headers['foo']
|
197
|
+
end
|
198
|
+
|
199
|
+
def test_bad_headers
|
200
|
+
@o << "GET / http/1.1\r\n a: b\r\n\r\n"
|
201
|
+
assert_raises(Error) { @parser.parse_headers }
|
202
|
+
|
203
|
+
reset_parser
|
204
|
+
@o << "GET / http/1.1\r\na b\r\n\r\n"
|
205
|
+
assert_raises(Error) { @parser.parse_headers }
|
206
|
+
|
207
|
+
max_key_length = H1P_LIMITS[:max_header_key_length]
|
208
|
+
|
209
|
+
reset_parser
|
210
|
+
@o << "GET / http/1.1\r\n#{'a' * max_key_length}: b\r\n\r\n"
|
211
|
+
headers = @parser.parse_headers
|
212
|
+
assert_equal 'b', headers['a' * max_key_length]
|
213
|
+
|
214
|
+
reset_parser
|
215
|
+
@o << "GET / http/1.1\r\n#{'a' * (max_key_length + 1)}: b\r\n\r\n"
|
216
|
+
assert_raises(Error) { @parser.parse_headers }
|
217
|
+
|
218
|
+
max_value_length = H1P_LIMITS[:max_header_value_length]
|
219
|
+
|
220
|
+
reset_parser
|
221
|
+
@o << "GET / http/1.1\r\nfoo: #{'a' * max_value_length}\r\n\r\n"
|
222
|
+
headers = @parser.parse_headers
|
223
|
+
assert_equal 'a' * max_value_length, headers['foo']
|
224
|
+
|
225
|
+
reset_parser
|
226
|
+
@o << "GET / http/1.1\r\nfoo: #{'a' * (max_value_length + 1)}\r\n\r\n"
|
227
|
+
assert_raises(Error) { @parser.parse_headers }
|
228
|
+
|
229
|
+
max_header_count = H1P_LIMITS[:max_header_count]
|
230
|
+
|
231
|
+
reset_parser
|
232
|
+
hdrs = (1..max_header_count).map { |i| "foo#{i}: bar\r\n" }.join
|
233
|
+
@o << "GET / http/1.1\r\n#{hdrs}\r\n"
|
234
|
+
headers = @parser.parse_headers
|
235
|
+
assert_equal (max_header_count + 4), headers.size
|
236
|
+
|
237
|
+
reset_parser
|
238
|
+
hdrs = (1..(max_header_count + 1)).map { |i| "foo#{i}: bar\r\n" }.join
|
239
|
+
@o << "GET / http/1.1\r\n#{hdrs}\r\n"
|
240
|
+
assert_raises(Error) { @parser.parse_headers }
|
241
|
+
end
|
242
|
+
|
243
|
+
def test_request_without_cr
|
244
|
+
msg = "GET /foo HTTP/1.1\nBar: baz\n\n"
|
245
|
+
@o << msg
|
246
|
+
headers = @parser.parse_headers
|
247
|
+
assert_equal({
|
248
|
+
':method' => 'get',
|
249
|
+
':path' => '/foo',
|
250
|
+
':protocol' => 'http/1.1',
|
251
|
+
'bar' => 'baz',
|
252
|
+
':rx' => msg.bytesize
|
253
|
+
}, headers)
|
254
|
+
end
|
255
|
+
|
256
|
+
def test_read_body_with_content_length
|
257
|
+
10.times do
|
258
|
+
data = ' ' * rand(20..60000)
|
259
|
+
msg = "POST /foo HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
|
260
|
+
Thread.new { @o << msg }
|
261
|
+
|
262
|
+
headers = @parser.parse_headers
|
263
|
+
assert_equal data.bytesize.to_s, headers['content-length']
|
264
|
+
|
265
|
+
body = @parser.read_body
|
266
|
+
assert_equal data, body
|
267
|
+
assert_equal msg.bytesize, headers[':rx']
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def test_read_body_chunk_with_content_length
|
272
|
+
data = 'abc' * 20000
|
273
|
+
msg = "POST /foo HTTP/1.1\r\nContent-Length: #{data.bytesize}\r\n\r\n#{data}"
|
274
|
+
Thread.new { @o << msg }
|
275
|
+
headers = @parser.parse_headers
|
276
|
+
assert_equal data.bytesize.to_s, headers['content-length']
|
277
|
+
|
278
|
+
buf = +''
|
279
|
+
count = 0
|
280
|
+
while (chunk = @parser.read_body_chunk(false))
|
281
|
+
count += 1
|
282
|
+
buf += chunk
|
283
|
+
end
|
284
|
+
assert_equal data.bytesize, data.bytesize
|
285
|
+
assert_equal data, buf
|
286
|
+
assert_in_range 1..20, count
|
287
|
+
assert_equal msg.bytesize, headers[':rx']
|
288
|
+
end
|
289
|
+
|
290
|
+
def test_read_body_with_content_length_incomplete
|
291
|
+
data = ' ' * rand(20..60000)
|
292
|
+
Thread.new do
|
293
|
+
@o << "POST /foo HTTP/1.1\r\nContent-Length: #{data.bytesize + 1}\r\n\r\n#{data}"
|
294
|
+
@o.close # !!! otherwise the parser will keep waiting
|
295
|
+
end
|
296
|
+
headers = @parser.parse_headers
|
297
|
+
|
298
|
+
assert_raises(H1P::Error) { @parser.read_body }
|
299
|
+
end
|
300
|
+
|
301
|
+
def test_read_body_chunk_with_content_length_incomplete
|
302
|
+
data = 'abc' * 50
|
303
|
+
@o << "POST /foo HTTP/1.1\r\nContent-Length: #{data.bytesize + 1}\r\n\r\n#{data}"
|
304
|
+
@o.close
|
305
|
+
headers = @parser.parse_headers
|
306
|
+
|
307
|
+
assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
|
308
|
+
end
|
309
|
+
|
310
|
+
def test_read_body_with_chunked_encoding
|
311
|
+
chunks = []
|
312
|
+
total_sent = 0
|
313
|
+
Thread.new do
|
314
|
+
msg = "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
315
|
+
@o << msg
|
316
|
+
total_sent += msg.bytesize
|
317
|
+
rand(8..16).times do |i|
|
318
|
+
chunk = i.to_s * rand(200..360000)
|
319
|
+
msg = "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
320
|
+
@o << msg
|
321
|
+
chunks << chunk
|
322
|
+
total_sent += msg.bytesize
|
323
|
+
end
|
324
|
+
msg = "0\r\n\r\n"
|
325
|
+
@o << msg
|
326
|
+
total_sent += msg.bytesize
|
327
|
+
end
|
328
|
+
headers = @parser.parse_headers
|
329
|
+
assert_equal 'chunked', headers['transfer-encoding']
|
330
|
+
|
331
|
+
body = @parser.read_body
|
332
|
+
assert_equal chunks.join, body
|
333
|
+
assert_equal total_sent, headers[':rx']
|
334
|
+
end
|
335
|
+
|
336
|
+
def test_read_body_chunk_with_chunked_encoding
|
337
|
+
chunks = []
|
338
|
+
total_sent = 0
|
339
|
+
Thread.new do
|
340
|
+
msg = "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
341
|
+
@o << msg
|
342
|
+
total_sent += msg.bytesize
|
343
|
+
rand(8..16).times do |i|
|
344
|
+
chunk = i.to_s * rand(40000..360000)
|
345
|
+
msg = "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
346
|
+
@o << msg
|
347
|
+
total_sent += msg.bytesize
|
348
|
+
chunks << chunk
|
349
|
+
end
|
350
|
+
msg = "0\r\n\r\n"
|
351
|
+
@o << msg
|
352
|
+
total_sent += msg.bytesize
|
353
|
+
end
|
354
|
+
headers = @parser.parse_headers
|
355
|
+
assert_equal 'chunked', headers['transfer-encoding']
|
356
|
+
|
357
|
+
received = []
|
358
|
+
while (chunk = @parser.read_body_chunk(false))
|
359
|
+
received << chunk
|
360
|
+
end
|
361
|
+
assert_equal chunks, received
|
362
|
+
assert_equal total_sent, headers[':rx']
|
363
|
+
end
|
364
|
+
|
365
|
+
def test_read_body_with_chunked_encoding_malformed
|
366
|
+
Thread.new do
|
367
|
+
@o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
368
|
+
chunk = ' '.to_s * rand(40000..360000)
|
369
|
+
@o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n3"
|
370
|
+
@o << "0\r\n\r\n"
|
371
|
+
@o.close
|
372
|
+
end
|
373
|
+
headers = @parser.parse_headers
|
374
|
+
assert_raises(H1P::Error) { @parser.read_body }
|
375
|
+
|
376
|
+
reset_parser
|
377
|
+
# missing last empty chunk
|
378
|
+
Thread.new do
|
379
|
+
@o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
380
|
+
chunk = ' '.to_s * rand(40000..360000)
|
381
|
+
@o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
382
|
+
@o.close
|
383
|
+
end
|
384
|
+
headers = @parser.parse_headers
|
385
|
+
assert_raises(H1P::Error) { @parser.read_body }
|
386
|
+
|
387
|
+
reset_parser
|
388
|
+
# bad chunk size
|
389
|
+
Thread.new do
|
390
|
+
@o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
391
|
+
chunk = ' '.to_s * rand(40000..360000)
|
392
|
+
@o << "-#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
393
|
+
@o.close
|
394
|
+
end
|
395
|
+
headers = @parser.parse_headers
|
396
|
+
assert_raises(H1P::Error) { @parser.read_body }
|
397
|
+
end
|
398
|
+
|
399
|
+
def test_read_body_chunk_with_chunked_encoding_malformed
|
400
|
+
chunk = nil
|
401
|
+
Thread.new do
|
402
|
+
@o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
403
|
+
chunk = ' ' * rand(40000..360000)
|
404
|
+
@o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n3"
|
405
|
+
@o << "0\r\n\r\n"
|
406
|
+
@o.close
|
407
|
+
end
|
408
|
+
headers = @parser.parse_headers
|
409
|
+
read = @parser.read_body_chunk(false)
|
410
|
+
assert_equal chunk, read
|
411
|
+
assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
|
412
|
+
|
413
|
+
reset_parser
|
414
|
+
|
415
|
+
# missing last empty chunk
|
416
|
+
chunk = nil
|
417
|
+
Thread.new do
|
418
|
+
@o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
419
|
+
chunk = ' '.to_s * rand(20..1600)
|
420
|
+
@o << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
421
|
+
@o.close
|
422
|
+
end
|
423
|
+
headers = @parser.parse_headers
|
424
|
+
read = @parser.read_body_chunk(false)
|
425
|
+
assert_equal chunk, read
|
426
|
+
assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
|
427
|
+
|
428
|
+
reset_parser
|
429
|
+
|
430
|
+
# bad chunk size
|
431
|
+
Thread.new do
|
432
|
+
@o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
433
|
+
chunk = ' '.to_s * rand(20..1600)
|
434
|
+
@o << "-#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
435
|
+
@o.close
|
436
|
+
end
|
437
|
+
headers = @parser.parse_headers
|
438
|
+
assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
|
439
|
+
|
440
|
+
reset_parser
|
441
|
+
|
442
|
+
# missing body
|
443
|
+
@o << "POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
444
|
+
@o.close
|
445
|
+
headers = @parser.parse_headers
|
446
|
+
assert_raises(H1P::Error) { @parser.read_body_chunk(false) }
|
447
|
+
end
|
448
|
+
|
449
|
+
def test_complete?
|
450
|
+
@o << "GET / HTTP/1.1\r\n\r\n"
|
451
|
+
headers = @parser.parse_headers
|
452
|
+
assert_equal true, @parser.complete?
|
453
|
+
|
454
|
+
reset_parser
|
455
|
+
@o << "GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\n"
|
456
|
+
headers = @parser.parse_headers
|
457
|
+
assert_equal false, @parser.complete?
|
458
|
+
@o << 'foo'
|
459
|
+
body = @parser.read_body
|
460
|
+
assert_equal 'foo', body
|
461
|
+
assert_equal true, @parser.complete?
|
462
|
+
|
463
|
+
reset_parser
|
464
|
+
@o << "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
|
465
|
+
headers = @parser.parse_headers
|
466
|
+
assert_equal false, @parser.complete?
|
467
|
+
@o << "3\r\nfoo\r\n"
|
468
|
+
chunk = @parser.read_body_chunk(false)
|
469
|
+
assert_equal 'foo', chunk
|
470
|
+
assert_equal false, @parser.complete?
|
471
|
+
@o << "0\r\n\r\n"
|
472
|
+
chunk = @parser.read_body_chunk(false)
|
473
|
+
assert_nil chunk
|
474
|
+
assert_equal true, @parser.complete?
|
475
|
+
end
|
476
|
+
|
477
|
+
def test_buffered_body_chunk
|
478
|
+
@o << "GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\nfoo"
|
479
|
+
headers = @parser.parse_headers
|
480
|
+
assert_equal false, @parser.complete?
|
481
|
+
|
482
|
+
chunk = @parser.read_body_chunk(true)
|
483
|
+
assert_equal 'foo', chunk
|
484
|
+
assert_equal true, @parser.complete?
|
485
|
+
chunk = @parser.read_body_chunk(false)
|
486
|
+
assert_nil chunk
|
487
|
+
assert_equal true, @parser.complete?
|
488
|
+
|
489
|
+
reset_parser
|
490
|
+
@o << "GET / HTTP/1.1\r\nContent-Length: 6\r\n\r\nfoo"
|
491
|
+
headers = @parser.parse_headers
|
492
|
+
assert_equal false, @parser.complete?
|
493
|
+
|
494
|
+
chunk = @parser.read_body_chunk(true)
|
495
|
+
assert_equal 'foo', chunk
|
496
|
+
assert_equal false, @parser.complete?
|
497
|
+
@o << 'bar'
|
498
|
+
chunk = @parser.read_body_chunk(false)
|
499
|
+
assert_equal 'bar', chunk
|
500
|
+
assert_equal true, @parser.complete?
|
501
|
+
|
502
|
+
reset_parser
|
503
|
+
@o << "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n"
|
504
|
+
headers = @parser.parse_headers
|
505
|
+
assert_equal false, @parser.complete?
|
506
|
+
|
507
|
+
chunk = @parser.read_body_chunk(true)
|
508
|
+
assert_equal 'foo', chunk
|
509
|
+
assert_equal false, @parser.complete?
|
510
|
+
@o << "0\r\n\r\n"
|
511
|
+
chunk = @parser.read_body_chunk(true)
|
512
|
+
assert_nil chunk
|
513
|
+
assert_equal true, @parser.complete?
|
514
|
+
end
|
515
|
+
|
516
|
+
def test_parser_with_tcp_socket
|
517
|
+
port = rand(1234..5678)
|
518
|
+
server = TCPServer.new('127.0.0.1', port)
|
519
|
+
server_thread = Thread.new do
|
520
|
+
while (socket = server.accept)
|
521
|
+
Thread.new do
|
522
|
+
parser = H1P::Parser.new(socket)
|
523
|
+
headers = parser.parse_headers
|
524
|
+
socket << headers.inspect
|
525
|
+
socket.shutdown
|
526
|
+
socket.close
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
sleep 0.001
|
532
|
+
client = TCPSocket.new('127.0.0.1', port)
|
533
|
+
msg = "get /foo HTTP/1.1\r\nCookie: abc=def\r\n\r\n"
|
534
|
+
client << msg
|
535
|
+
reply = client.read
|
536
|
+
assert_equal({
|
537
|
+
':method' => 'get',
|
538
|
+
':path' => '/foo',
|
539
|
+
':protocol' => 'http/1.1',
|
540
|
+
'cookie' => 'abc=def',
|
541
|
+
':rx' => msg.bytesize,
|
542
|
+
}, eval(reply))
|
543
|
+
ensure
|
544
|
+
client.shutdown rescue nil
|
545
|
+
client&.close
|
546
|
+
server_thread&.kill
|
547
|
+
server_thread&.join
|
548
|
+
server&.close
|
549
|
+
end
|
550
|
+
|
551
|
+
def test_parser_with_callable
|
552
|
+
buf = []
|
553
|
+
request = +"GET /foo HTTP/1.1\r\nHost: bar\r\n\r\n"
|
554
|
+
callable = proc do |len|
|
555
|
+
buf << {len: len}
|
556
|
+
request
|
557
|
+
end
|
558
|
+
|
559
|
+
parser = H1P::Parser.new(callable)
|
560
|
+
|
561
|
+
headers = parser.parse_headers
|
562
|
+
assert_equal({
|
563
|
+
':method' => 'get',
|
564
|
+
':path' => '/foo',
|
565
|
+
':protocol' => 'http/1.1',
|
566
|
+
'host' => 'bar',
|
567
|
+
':rx' => request.bytesize,
|
568
|
+
|
569
|
+
}, headers)
|
570
|
+
assert_equal [{len: 4096}], buf
|
571
|
+
|
572
|
+
request = +"GET /bar HTTP/1.1\r\nHost: baz\r\n\r\n"
|
573
|
+
headers = parser.parse_headers
|
574
|
+
assert_equal({
|
575
|
+
':method' => 'get',
|
576
|
+
':path' => '/bar',
|
577
|
+
':protocol' => 'http/1.1',
|
578
|
+
'host' => 'baz',
|
579
|
+
':rx' => request.bytesize,
|
580
|
+
|
581
|
+
}, headers)
|
582
|
+
assert_equal [{len: 4096}, {len: 4096}], buf
|
583
|
+
end
|
584
|
+
end
|