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