ffi-http-parser 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.
- data/.document +3 -0
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +11 -0
- data/LICENSE.txt +20 -0
- data/README.md +66 -0
- data/Rakefile +40 -0
- data/ffi-http-parser.gemspec +60 -0
- data/gemspec.yml +17 -0
- data/lib/ffi/http/parser.rb +2 -0
- data/lib/ffi/http/parser/instance.rb +398 -0
- data/lib/ffi/http/parser/parser.rb +31 -0
- data/lib/ffi/http/parser/settings.rb +22 -0
- data/lib/ffi/http/parser/types.rb +50 -0
- data/lib/ffi/http/parser/version.rb +10 -0
- data/spec/callback_examples.rb +26 -0
- data/spec/instance_spec.rb +458 -0
- data/spec/parser_spec.rb +24 -0
- data/spec/spec_helper.rb +5 -0
- metadata +133 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'ffi/http/parser/types'
|
2
|
+
require 'ffi/http/parser/instance'
|
3
|
+
|
4
|
+
module FFI
|
5
|
+
module HTTP
|
6
|
+
module Parser
|
7
|
+
extend FFI::Library
|
8
|
+
|
9
|
+
ffi_lib ['http_parser', 'http_parser.so.1']
|
10
|
+
|
11
|
+
attach_function :http_parser_init, [:pointer, :http_parser_type], :void
|
12
|
+
attach_function :http_parser_execute, [:pointer, :pointer, :pointer, :size_t], :size_t
|
13
|
+
|
14
|
+
attach_function :http_should_keep_alive, [:pointer], :int
|
15
|
+
attach_function :http_method_str, [:http_method], :string
|
16
|
+
|
17
|
+
#
|
18
|
+
# Creates a new Parser.
|
19
|
+
#
|
20
|
+
# @return [Instance]
|
21
|
+
# A new parser instance.
|
22
|
+
#
|
23
|
+
# @see Instance
|
24
|
+
#
|
25
|
+
def self.new(&block)
|
26
|
+
Instance.new(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'ffi/http/parser/types'
|
2
|
+
|
3
|
+
module FFI
|
4
|
+
module HTTP
|
5
|
+
module Parser
|
6
|
+
class Settings < FFI::Struct
|
7
|
+
|
8
|
+
layout :on_message_begin, :http_cb,
|
9
|
+
:on_path, :http_data_cb,
|
10
|
+
:on_query_string, :http_data_cb,
|
11
|
+
:on_url, :http_data_cb,
|
12
|
+
:on_fragment, :http_data_cb,
|
13
|
+
:on_header_field, :http_data_cb,
|
14
|
+
:on_header_value, :http_data_cb,
|
15
|
+
:on_headers_complete, :http_cb,
|
16
|
+
:on_body, :http_data_cb,
|
17
|
+
:on_message_complete, :http_cb
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
|
3
|
+
module FFI
|
4
|
+
module HTTP
|
5
|
+
module Parser
|
6
|
+
extend FFI::Library
|
7
|
+
|
8
|
+
# Maximum header size
|
9
|
+
HTTP_MAX_HEADER_SIZE = (80 * 1024)
|
10
|
+
|
11
|
+
callback :http_data_cb, [:pointer, :pointer, :size_t], :int
|
12
|
+
callback :http_cb, [:pointer], :int
|
13
|
+
|
14
|
+
# HTTP Methods
|
15
|
+
METHODS = enum :http_method, [
|
16
|
+
:DELETE,
|
17
|
+
:GET,
|
18
|
+
:HEAD,
|
19
|
+
:POST,
|
20
|
+
:PUT,
|
21
|
+
# pathological
|
22
|
+
:CONNECT,
|
23
|
+
:OPTIONS,
|
24
|
+
:TRACE,
|
25
|
+
# webdav
|
26
|
+
:COPY,
|
27
|
+
:LOCK,
|
28
|
+
:MKCOL,
|
29
|
+
:MOVE,
|
30
|
+
:PROPFIND,
|
31
|
+
:PROPPATCH,
|
32
|
+
:UNLOCK,
|
33
|
+
# subversion
|
34
|
+
:REPORT,
|
35
|
+
:MKACTIVITY,
|
36
|
+
:CHECKOUT,
|
37
|
+
:MERGE,
|
38
|
+
# upnp
|
39
|
+
:MSEARCH,
|
40
|
+
:NOTIFY,
|
41
|
+
:SUBSCRIBE,
|
42
|
+
:UNSUBSCRIBE
|
43
|
+
]
|
44
|
+
|
45
|
+
# HTTP Parser types
|
46
|
+
TYPES = enum :http_parser_type, [:request, :response, :both]
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
shared_examples_for "callback" do |callback_pair|
|
2
|
+
context "when it returns :error" do
|
3
|
+
subject do
|
4
|
+
callback, next_callback = callback_pair.to_a.first
|
5
|
+
|
6
|
+
described_class.new do |parser|
|
7
|
+
parser.send(callback) { |*args| :error }
|
8
|
+
|
9
|
+
parser.send(next_callback) { @called = true }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should stop the parser" do
|
14
|
+
subject << "POST /path?q=1#fragment HTTP/1.1\r\n"
|
15
|
+
subject << "Transfer-Encoding: chunked\r\n"
|
16
|
+
subject << "\r\n"
|
17
|
+
|
18
|
+
subject << "4\r\n"
|
19
|
+
subject << "Body\r\n"
|
20
|
+
|
21
|
+
subject << "0\r\n"
|
22
|
+
|
23
|
+
@called.should_not be_true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,458 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'callback_examples'
|
3
|
+
|
4
|
+
require 'ffi/http/parser/instance'
|
5
|
+
|
6
|
+
describe Instance do
|
7
|
+
describe "#initialize" do
|
8
|
+
context "when initialized from a pointer" do
|
9
|
+
it "should not call http_parser_init" do
|
10
|
+
ptr = described_class.new.to_ptr
|
11
|
+
|
12
|
+
FFI::HTTP::Parser.should_not_receive(:http_parser_init)
|
13
|
+
|
14
|
+
described_class.new(ptr)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "when given a block" do
|
19
|
+
it "should yield the new Instance" do
|
20
|
+
expected = nil
|
21
|
+
|
22
|
+
described_class.new { |parser| expected = parser }
|
23
|
+
|
24
|
+
expected.should be_kind_of(described_class)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should allow changing the parser type" do
|
28
|
+
parser = described_class.new do |parser|
|
29
|
+
parser.type = :both
|
30
|
+
end
|
31
|
+
|
32
|
+
parser.type.should == :both
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#type" do
|
38
|
+
it "should default to :request" do
|
39
|
+
subject.type.should == :request
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should convert the type to a Symbol" do
|
43
|
+
subject[:type_flags] = TYPES[:both]
|
44
|
+
|
45
|
+
subject.type.should == :both
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should extract the type from the type_flags field" do
|
49
|
+
subject[:type_flags] = ((0xff & ~0x3) | TYPES[:both])
|
50
|
+
|
51
|
+
subject.type.should == :both
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#type=" do
|
56
|
+
it "should set the type" do
|
57
|
+
subject.type = :both
|
58
|
+
|
59
|
+
subject.type.should == :both
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should not change flags" do
|
63
|
+
flags = (0xff & ~0x3)
|
64
|
+
subject[:type_flags] = flags
|
65
|
+
|
66
|
+
subject.type = :both
|
67
|
+
|
68
|
+
subject[:type_flags].should == (flags | TYPES[:both])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#<<" do
|
73
|
+
it "should call http_parser_execute" do
|
74
|
+
FFI::HTTP::Parser.should_receive(:http_parser_execute)
|
75
|
+
|
76
|
+
subject << "GET / HTTP/1.1\r\n"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "callbacks" do
|
81
|
+
describe "on_message_begin" do
|
82
|
+
include_examples "callback", {:on_message_begin => :on_path}
|
83
|
+
|
84
|
+
subject do
|
85
|
+
described_class.new do |parser|
|
86
|
+
parser.on_message_begin { @begun = true }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should trigger on a new request" do
|
91
|
+
subject << "GET / HTTP/1.1"
|
92
|
+
|
93
|
+
@begun.should be_true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "on_path" do
|
98
|
+
include_examples "callback", {:on_path => :on_query_string}
|
99
|
+
|
100
|
+
let(:expected) { '/foo' }
|
101
|
+
|
102
|
+
subject do
|
103
|
+
described_class.new do |parser|
|
104
|
+
parser.on_path { |data| @path = data }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should pass the recognized path" do
|
109
|
+
subject << "GET "
|
110
|
+
|
111
|
+
@path.should be_nil
|
112
|
+
|
113
|
+
subject << "#{expected} HTTP/1.1"
|
114
|
+
|
115
|
+
@path.should == expected
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "on_query_string" do
|
120
|
+
include_examples "callback", {:on_query_string => :on_fragment}
|
121
|
+
|
122
|
+
let(:expected) { 'x=1&y=2' }
|
123
|
+
|
124
|
+
subject do
|
125
|
+
described_class.new do |parser|
|
126
|
+
parser.on_query_string { |data| @query_string = data }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should pass the recognized query_string" do
|
131
|
+
subject << "GET /foo"
|
132
|
+
|
133
|
+
@query_string.should be_nil
|
134
|
+
|
135
|
+
subject << "?#{expected} HTTP/1.1"
|
136
|
+
|
137
|
+
@query_string.should == expected
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "on_fragment" do
|
142
|
+
include_examples "callback", {:on_fragment => :on_header_field}
|
143
|
+
|
144
|
+
let(:expected) { 'bar' }
|
145
|
+
|
146
|
+
subject do
|
147
|
+
described_class.new do |parser|
|
148
|
+
parser.on_fragment { |data| @fragment = data }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should pass the recognized fragment" do
|
153
|
+
subject << "GET /foo"
|
154
|
+
|
155
|
+
@fragment.should be_nil
|
156
|
+
|
157
|
+
subject << "##{expected} HTTP/1.1"
|
158
|
+
|
159
|
+
@fragment.should == expected
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe "on_url" do
|
164
|
+
include_examples "callback", {:on_url => :on_header_field}
|
165
|
+
|
166
|
+
let(:expected) { '/foo?q=1' }
|
167
|
+
|
168
|
+
subject do
|
169
|
+
described_class.new do |parser|
|
170
|
+
parser.on_url { |data| @url = data }
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should pass the recognized url" do
|
175
|
+
subject << "GET "
|
176
|
+
|
177
|
+
@url.should be_nil
|
178
|
+
|
179
|
+
subject << "#{expected} HTTP/1.1"
|
180
|
+
|
181
|
+
@url.should == expected
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
describe "on_header_field" do
|
186
|
+
include_examples "callback", {:on_header_field => :on_header_value}
|
187
|
+
|
188
|
+
let(:expected) { 'Host' }
|
189
|
+
|
190
|
+
subject do
|
191
|
+
described_class.new do |parser|
|
192
|
+
parser.on_header_field { |data| @header_field = data }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should pass the recognized header-name" do
|
197
|
+
subject << "GET /foo HTTP/1.1\r\n"
|
198
|
+
|
199
|
+
@header_field.should be_nil
|
200
|
+
|
201
|
+
subject << "#{expected}: example.com\r\n"
|
202
|
+
|
203
|
+
@header_field.should == expected
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "on_header_value" do
|
208
|
+
include_examples "callback", {:on_header_value => :on_body}
|
209
|
+
|
210
|
+
let(:expected) { 'example.com' }
|
211
|
+
|
212
|
+
subject do
|
213
|
+
described_class.new do |parser|
|
214
|
+
parser.on_header_value { |data| @header_value = data }
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
it "should pass the recognized header-value" do
|
219
|
+
subject << "GET /foo HTTP/1.1\r\n"
|
220
|
+
|
221
|
+
@header_value.should be_nil
|
222
|
+
|
223
|
+
subject << "Host: #{expected}\r\n"
|
224
|
+
|
225
|
+
@header_value.should == expected
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
describe "on_headers_complete" do
|
230
|
+
include_examples "callback", {:on_headers_complete => :on_body}
|
231
|
+
|
232
|
+
subject do
|
233
|
+
described_class.new do |parser|
|
234
|
+
parser.on_headers_complete { @header_complete = true }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
it "should trigger on the last header" do
|
239
|
+
subject << "GET / HTTP/1.1\r\n"
|
240
|
+
subject << "Host: example.com\r\n"
|
241
|
+
|
242
|
+
@header_complete.should be_nil
|
243
|
+
|
244
|
+
subject << "\r\n"
|
245
|
+
|
246
|
+
@header_complete.should be_true
|
247
|
+
end
|
248
|
+
|
249
|
+
context "when the callback returns :stop" do
|
250
|
+
subject do
|
251
|
+
described_class.new do |parser|
|
252
|
+
parser.on_headers_complete { :stop }
|
253
|
+
|
254
|
+
parser.on_body { |data| @body = data }
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
it "should indicate there is no request body to parse" do
|
259
|
+
subject << "GET / HTTP/1.1\r\n"
|
260
|
+
subject << "Host: example.com\r\n"
|
261
|
+
subject << "\r\n"
|
262
|
+
subject << "Body"
|
263
|
+
|
264
|
+
@body.should be_nil
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
describe "on_body" do
|
270
|
+
include_examples "callback", {:on_body => :on_message_complete}
|
271
|
+
|
272
|
+
let(:expected) { "Body" }
|
273
|
+
|
274
|
+
subject do
|
275
|
+
described_class.new do |parser|
|
276
|
+
parser.on_body { |data| @body = data }
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
it "should trigger on the body" do
|
281
|
+
subject << "POST / HTTP/1.1\r\n"
|
282
|
+
subject << "Transfer-Encoding: chunked\r\n"
|
283
|
+
subject << "\r\n"
|
284
|
+
|
285
|
+
@body.should be_nil
|
286
|
+
|
287
|
+
subject << "#{"%x" % expected.length}\r\n"
|
288
|
+
subject << expected
|
289
|
+
|
290
|
+
@body.should == expected
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
describe "on_message_complete" do
|
295
|
+
subject do
|
296
|
+
described_class.new do |parser|
|
297
|
+
parser.on_message_complete { @message_complete = true }
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
it "should trigger at the end of the message" do
|
302
|
+
subject << "GET / HTTP/1.1\r\n"
|
303
|
+
|
304
|
+
@message_complete.should be_nil
|
305
|
+
|
306
|
+
subject << "Host: example.com\r\n\r\n"
|
307
|
+
|
308
|
+
@message_complete.should be_true
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
describe "#reset!" do
|
314
|
+
it "should call http_parser_init" do
|
315
|
+
parser = described_class.new
|
316
|
+
|
317
|
+
FFI::HTTP::Parser.should_receive(:http_parser_init)
|
318
|
+
|
319
|
+
parser.reset!
|
320
|
+
end
|
321
|
+
|
322
|
+
it "should not change the type" do
|
323
|
+
parser = described_class.new do |parser|
|
324
|
+
parser.type = :both
|
325
|
+
end
|
326
|
+
|
327
|
+
parser.reset!
|
328
|
+
|
329
|
+
parser.type.should == :both
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
describe "#http_method" do
|
334
|
+
let(:expected) { :POST }
|
335
|
+
|
336
|
+
it "should set the http_method field" do
|
337
|
+
subject << "#{expected} / HTTP/1.1\r\n"
|
338
|
+
|
339
|
+
subject.http_method.should == expected
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
describe "#http_major" do
|
344
|
+
let(:expected) { 1 }
|
345
|
+
|
346
|
+
context "when parsing requests" do
|
347
|
+
it "should set the http_major field" do
|
348
|
+
subject << "GET / HTTP/#{expected}."
|
349
|
+
|
350
|
+
subject.http_major.should == expected
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
context "when parsing responses" do
|
355
|
+
subject do
|
356
|
+
described_class.new do |parser|
|
357
|
+
parser.type = :response
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
it "should set the http_major field" do
|
362
|
+
subject << "HTTP/#{expected}."
|
363
|
+
|
364
|
+
subject.http_major.should == expected
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
describe "#http_minor" do
|
370
|
+
let(:expected) { 2 }
|
371
|
+
|
372
|
+
context "when parsing requests" do
|
373
|
+
it "should set the http_minor field" do
|
374
|
+
subject << "GET / HTTP/1.#{expected}\r\n"
|
375
|
+
|
376
|
+
subject.http_minor.should == expected
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
context "when parsing responses" do
|
381
|
+
subject do
|
382
|
+
described_class.new do |parser|
|
383
|
+
parser.type = :response
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
it "should set the http_major field" do
|
388
|
+
subject << "HTTP/1.#{expected} "
|
389
|
+
|
390
|
+
subject.http_minor.should == expected
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
describe "#http_version" do
|
396
|
+
let(:expected) { '1.1' }
|
397
|
+
|
398
|
+
before do
|
399
|
+
subject << "GET / HTTP/#{expected}\r\n"
|
400
|
+
end
|
401
|
+
|
402
|
+
it "should combine #http_major and #http_minor" do
|
403
|
+
subject.http_version.should == expected
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
describe "#http_status" do
|
408
|
+
context "when parsing requests" do
|
409
|
+
before do
|
410
|
+
subject << "GET / HTTP/1.1\r\n"
|
411
|
+
subject << "Host: example.com\r\n"
|
412
|
+
subject << "\r\n"
|
413
|
+
end
|
414
|
+
|
415
|
+
it "should not be set" do
|
416
|
+
subject.http_status.should be_zero
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
context "when parsing responses" do
|
421
|
+
let(:expected) { 200 }
|
422
|
+
|
423
|
+
subject do
|
424
|
+
described_class.new do |parser|
|
425
|
+
parser.type = :response
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
before do
|
430
|
+
subject << "HTTP/1.1 #{expected} OK\r\n"
|
431
|
+
subject << "Location: http://example.com/\r\n"
|
432
|
+
subject << "\r\n"
|
433
|
+
end
|
434
|
+
|
435
|
+
it "should set the http_status field" do
|
436
|
+
subject.http_status.should == expected
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
describe "#upgrade?" do
|
442
|
+
let(:upgrade) { 'WebSocket' }
|
443
|
+
|
444
|
+
before do
|
445
|
+
subject << "GET /demo HTTP/1.1\r\n"
|
446
|
+
subject << "Upgrade: #{upgrade}\r\n"
|
447
|
+
subject << "Connection: Upgrade\r\n"
|
448
|
+
subject << "Host: example.com\r\n"
|
449
|
+
subject << "Origin: http://example.com\r\n"
|
450
|
+
subject << "WebSocket-Protocol: sample\r\n"
|
451
|
+
subject << "\r\n"
|
452
|
+
end
|
453
|
+
|
454
|
+
it "should return true if the Upgrade header was set" do
|
455
|
+
subject.upgrade?.should be_true
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|