http-parser 1.0.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 +7 -0
- data/LICENSE +20 -0
- data/README.md +70 -0
- data/Rakefile +0 -0
- data/ext/Rakefile +9 -0
- data/ext/http-parser/http_parser.c +2175 -0
- data/ext/http-parser/http_parser.h +305 -0
- data/http-parser.gemspec +32 -0
- data/lib/http-parser.rb +9 -0
- data/lib/http-parser/errors.rb +74 -0
- data/lib/http-parser/ext.rb +7 -0
- data/lib/http-parser/parser.rb +198 -0
- data/lib/http-parser/types.rb +312 -0
- data/lib/http-parser/version.rb +3 -0
- data/spec/error_spec.rb +45 -0
- data/spec/instance_spec.rb +98 -0
- data/spec/parser_spec.rb +274 -0
- metadata +123 -0
@@ -0,0 +1,312 @@
|
|
1
|
+
|
2
|
+
module HttpParser
|
3
|
+
HTTP_MAX_HEADER_SIZE = (80 * 1024)
|
4
|
+
|
5
|
+
#
|
6
|
+
# These share a byte of data as a bitmap
|
7
|
+
#
|
8
|
+
TYPES = enum :http_parser_type, [
|
9
|
+
:request, 0,
|
10
|
+
:response,
|
11
|
+
:both
|
12
|
+
]
|
13
|
+
FLAG = {
|
14
|
+
:CHUNKED => 1 << 2,
|
15
|
+
:CONNECTION_KEEP_ALIVE => 1 << 3,
|
16
|
+
:CONNECTION_CLOSE => 1 << 4,
|
17
|
+
:TRAILING => 1 << 5,
|
18
|
+
:UPGRADE => 1 << 6,
|
19
|
+
:SKIPBODY => 1 << 7
|
20
|
+
}
|
21
|
+
|
22
|
+
#
|
23
|
+
# Request Methods
|
24
|
+
#
|
25
|
+
METHODS = enum :http_method, [
|
26
|
+
:DELETE, 0,
|
27
|
+
:GET,
|
28
|
+
:HEAD,
|
29
|
+
:POST,
|
30
|
+
:PUT,
|
31
|
+
# pathological
|
32
|
+
:CONNECT,
|
33
|
+
:OPTIONS,
|
34
|
+
:TRACE,
|
35
|
+
# webdav
|
36
|
+
:COPY,
|
37
|
+
:LOCK,
|
38
|
+
:MKCOL,
|
39
|
+
:MOVE,
|
40
|
+
:PROPFIND,
|
41
|
+
:PROPPATCH,
|
42
|
+
:SEARCH,
|
43
|
+
:UNLOCK,
|
44
|
+
# subversion
|
45
|
+
:REPORT,
|
46
|
+
:MKACTIVITY,
|
47
|
+
:CHECKOUT,
|
48
|
+
:MERGE,
|
49
|
+
# upnp
|
50
|
+
:MSEARCH,
|
51
|
+
:NOTIFY,
|
52
|
+
:SUBSCRIBE,
|
53
|
+
:UNSUBSCRIBE,
|
54
|
+
# RFC-5789
|
55
|
+
:PATCH,
|
56
|
+
:PURGE
|
57
|
+
]
|
58
|
+
|
59
|
+
|
60
|
+
UrlFields = enum :http_parser_url_fields, [
|
61
|
+
:SCHEMA, 0,
|
62
|
+
:HOST,
|
63
|
+
:PORT,
|
64
|
+
:PATH,
|
65
|
+
:QUERY,
|
66
|
+
:FRAGMENT,
|
67
|
+
:USERINFO,
|
68
|
+
:MAX
|
69
|
+
]
|
70
|
+
|
71
|
+
|
72
|
+
#
|
73
|
+
# Effectively this represents a request instance
|
74
|
+
#
|
75
|
+
class Instance < FFI::Struct
|
76
|
+
layout :type_flags, :uchar,
|
77
|
+
:state, :uchar,
|
78
|
+
:header_state, :uchar,
|
79
|
+
:index, :uchar,
|
80
|
+
|
81
|
+
:nread, :uint32,
|
82
|
+
:content_length, :int64,
|
83
|
+
|
84
|
+
# READ-ONLY
|
85
|
+
:http_major, :ushort,
|
86
|
+
:http_minor, :ushort,
|
87
|
+
:status_code, :ushort, # responses only
|
88
|
+
:method, :uchar, # requests only
|
89
|
+
:error_upgrade, :uchar, # errno = first 7bits, upgrade = last bit
|
90
|
+
|
91
|
+
# PUBLIC
|
92
|
+
:data, :pointer
|
93
|
+
|
94
|
+
|
95
|
+
def initialize(ptr = nil)
|
96
|
+
if ptr then super(ptr)
|
97
|
+
else
|
98
|
+
super()
|
99
|
+
self.type = :both
|
100
|
+
end
|
101
|
+
|
102
|
+
yield self if block_given?
|
103
|
+
|
104
|
+
::HttpParser.http_parser_init(self, self.type) unless ptr
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Resets the parser.
|
109
|
+
#
|
110
|
+
# @param [:request, :response, :both] new_type
|
111
|
+
# The new type for the parser.
|
112
|
+
#
|
113
|
+
def reset!(new_type = type)
|
114
|
+
::HttpParser.http_parser_init(self, new_type)
|
115
|
+
end
|
116
|
+
alias reset reset!
|
117
|
+
|
118
|
+
#
|
119
|
+
# The type of the parser.
|
120
|
+
#
|
121
|
+
# @return [:request, :response, :both]
|
122
|
+
# The parser type.
|
123
|
+
#
|
124
|
+
def type
|
125
|
+
TYPES[self[:type_flags] & 0x3]
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Sets the type of the parser.
|
130
|
+
#
|
131
|
+
# @param [:request, :response, :both] new_type
|
132
|
+
# The new parser type.
|
133
|
+
#
|
134
|
+
def type=(new_type)
|
135
|
+
self[:type_flags] = (flags | TYPES[new_type])
|
136
|
+
end
|
137
|
+
|
138
|
+
#
|
139
|
+
# Flags for the parser.
|
140
|
+
#
|
141
|
+
# @return [Integer]
|
142
|
+
# Parser flags.
|
143
|
+
#
|
144
|
+
def flags
|
145
|
+
(self[:type_flags] & 0xfc)
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# The parsed HTTP major version number.
|
150
|
+
#
|
151
|
+
# @return [Integer]
|
152
|
+
# The HTTP major version number.
|
153
|
+
#
|
154
|
+
def http_major
|
155
|
+
self[:http_major]
|
156
|
+
end
|
157
|
+
|
158
|
+
#
|
159
|
+
# The parsed HTTP minor version number.
|
160
|
+
#
|
161
|
+
# @return [Integer]
|
162
|
+
# The HTTP minor version number.
|
163
|
+
#
|
164
|
+
def http_minor
|
165
|
+
self[:http_minor]
|
166
|
+
end
|
167
|
+
|
168
|
+
#
|
169
|
+
# The parsed HTTP version.
|
170
|
+
#
|
171
|
+
# @return [String]
|
172
|
+
# The HTTP version.
|
173
|
+
#
|
174
|
+
def http_version
|
175
|
+
"%d.%d" % [self[:http_major], self[:http_minor]]
|
176
|
+
end
|
177
|
+
|
178
|
+
#
|
179
|
+
# The parsed HTTP response Status Code.
|
180
|
+
#
|
181
|
+
# @return [Integer]
|
182
|
+
# The HTTP Status Code.
|
183
|
+
#
|
184
|
+
# @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
|
185
|
+
#
|
186
|
+
def http_status
|
187
|
+
self[:status_code]
|
188
|
+
end
|
189
|
+
|
190
|
+
#
|
191
|
+
# The parsed HTTP Method.
|
192
|
+
#
|
193
|
+
# @return [Symbol]
|
194
|
+
# The HTTP Method name.
|
195
|
+
#
|
196
|
+
# @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.1
|
197
|
+
#
|
198
|
+
def http_method
|
199
|
+
METHODS[self[:method]]
|
200
|
+
end
|
201
|
+
|
202
|
+
#
|
203
|
+
# Determines whether the `Upgrade` header has been parsed.
|
204
|
+
#
|
205
|
+
# @return [Boolean]
|
206
|
+
# Specifies whether the `Upgrade` header has been seen.
|
207
|
+
#
|
208
|
+
# @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.42
|
209
|
+
#
|
210
|
+
def upgrade?
|
211
|
+
(self[:error_upgrade] & 0b10000000) > 0
|
212
|
+
end
|
213
|
+
|
214
|
+
#
|
215
|
+
# Determines whether an error occurred during processing.
|
216
|
+
#
|
217
|
+
# @return [Boolean]
|
218
|
+
# Did a parsing error occur with the request?
|
219
|
+
#
|
220
|
+
def error?
|
221
|
+
error = (self[:error_upgrade] & 0b1111111)
|
222
|
+
return error != 0
|
223
|
+
end
|
224
|
+
|
225
|
+
#
|
226
|
+
# Returns the error that occurred during processing.
|
227
|
+
#
|
228
|
+
# @return [StandarError]
|
229
|
+
# Returns the error that occurred.
|
230
|
+
#
|
231
|
+
def error
|
232
|
+
error = (self[:error_upgrade] & 0b1111111)
|
233
|
+
return nil if error == 0
|
234
|
+
|
235
|
+
err = ::HttpParser.err_name(error)[4..-1] # HPE_ is at the start of all these errors
|
236
|
+
klass = ERRORS[err.to_sym]
|
237
|
+
err = "#{::HttpParser.err_desc(error)} (#{err})"
|
238
|
+
return klass.nil? ? Error::UNKNOWN.new(err) : klass.new(err)
|
239
|
+
end
|
240
|
+
|
241
|
+
#
|
242
|
+
# Additional data attached to the parser.
|
243
|
+
#
|
244
|
+
# @return [FFI::Pointer]
|
245
|
+
# Pointer to the additional data.
|
246
|
+
#
|
247
|
+
def data
|
248
|
+
self[:data]
|
249
|
+
end
|
250
|
+
|
251
|
+
#
|
252
|
+
# Determines whether the `Connection: keep-alive` header has been
|
253
|
+
# parsed.
|
254
|
+
#
|
255
|
+
# @return [Boolean]
|
256
|
+
# Specifies whether the Connection should be kept alive.
|
257
|
+
#
|
258
|
+
# @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.10
|
259
|
+
#
|
260
|
+
def keep_alive?
|
261
|
+
::HttpParser.http_should_keep_alive(self) > 0
|
262
|
+
end
|
263
|
+
|
264
|
+
#
|
265
|
+
# Halts the parser if called in a callback
|
266
|
+
#
|
267
|
+
def stop!
|
268
|
+
throw :return, 1
|
269
|
+
end
|
270
|
+
|
271
|
+
#
|
272
|
+
# Indicates an error has occurred when called in a callback
|
273
|
+
#
|
274
|
+
def error!
|
275
|
+
throw :return, -1
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
class FieldData < FFI::Struct
|
280
|
+
layout :off, :uint16,
|
281
|
+
:len, :uint16
|
282
|
+
end
|
283
|
+
|
284
|
+
class HttpParserUrl < FFI::Struct
|
285
|
+
layout :field_set, :uint16,
|
286
|
+
:port, :uint16,
|
287
|
+
:field_data, [FieldData, UrlFields[:MAX]]
|
288
|
+
end
|
289
|
+
|
290
|
+
|
291
|
+
callback :http_data_cb, [Instance.ptr, :pointer, :size_t], :int
|
292
|
+
callback :http_cb, [Instance.ptr], :int
|
293
|
+
|
294
|
+
|
295
|
+
class Settings < FFI::Struct
|
296
|
+
layout :on_message_begin, :http_cb,
|
297
|
+
:on_url, :http_data_cb,
|
298
|
+
:on_status_complete, :http_cb,
|
299
|
+
:on_header_field, :http_data_cb,
|
300
|
+
:on_header_value, :http_data_cb,
|
301
|
+
:on_headers_complete, :http_cb,
|
302
|
+
:on_body, :http_data_cb,
|
303
|
+
:on_message_complete, :http_cb
|
304
|
+
end
|
305
|
+
|
306
|
+
|
307
|
+
attach_function :http_parser_init, [Instance.by_ref, :http_parser_type], :void, :blocking => true
|
308
|
+
attach_function :http_parser_execute, [Instance.by_ref, Settings.by_ref, :pointer, :size_t], :size_t, :blocking => true
|
309
|
+
|
310
|
+
attach_function :http_should_keep_alive, [Instance.by_ref], :int, :blocking => true
|
311
|
+
attach_function :http_method_str, [:http_method], :string, :blocking => true
|
312
|
+
end
|
data/spec/error_spec.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'http-parser'
|
2
|
+
|
3
|
+
describe HttpParser::Parser, "#initialize" do
|
4
|
+
before :each do
|
5
|
+
@inst = HttpParser::Parser.new_instance
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should return true when no error" do
|
9
|
+
subject.parse(@inst, "GET / HTTP/1.1\r\n").should be_true
|
10
|
+
@inst.error?.should be_false
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should return false on error" do
|
14
|
+
subject.parse(@inst, "GETS / HTTP/1.1\r\n").should be_false
|
15
|
+
@inst.error?.should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "the error should be inspectable" do
|
19
|
+
subject.parse(@inst, "GETS / HTTP/1.1\r\n").should be_false
|
20
|
+
@inst.error.should be_kind_of(::HttpParser::Error::INVALID_METHOD)
|
21
|
+
@inst.error?.should be_true
|
22
|
+
end
|
23
|
+
|
24
|
+
it "raises different error types depending on the error" do
|
25
|
+
subject.parse(@inst, "GET / HTTP/23\r\n").should be_false
|
26
|
+
@inst.error.should be_kind_of(::HttpParser::Error::INVALID_VERSION)
|
27
|
+
@inst.error?.should be_true
|
28
|
+
end
|
29
|
+
|
30
|
+
context "callback errors" do
|
31
|
+
subject do
|
32
|
+
described_class.new do |parser|
|
33
|
+
parser.on_url { |inst, data| raise 'unhandled' }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should handle unhandled errors gracefully" do
|
38
|
+
subject.parse(@inst, "GET /foo?q=1 HTTP/1.1").should be_false
|
39
|
+
|
40
|
+
@inst.error?.should be_true
|
41
|
+
@inst.error.should be_kind_of(::HttpParser::Error::CALLBACK)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'http-parser'
|
2
|
+
|
3
|
+
describe ::HttpParser::Instance, "#initialize" do
|
4
|
+
context "when initialized from a pointer" do
|
5
|
+
it "should not call http_parser_init" do
|
6
|
+
ptr = described_class.new.to_ptr
|
7
|
+
|
8
|
+
::HttpParser.should_not_receive(:http_parser_init)
|
9
|
+
|
10
|
+
described_class.new(ptr)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "when given a block" do
|
15
|
+
it "should yield the new Instance" do
|
16
|
+
expected = nil
|
17
|
+
|
18
|
+
described_class.new { |inst| expected = inst }
|
19
|
+
|
20
|
+
expected.should be_kind_of(described_class)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should allow changing the parser type" do
|
24
|
+
inst = described_class.new do |inst|
|
25
|
+
inst.type = :request
|
26
|
+
end
|
27
|
+
|
28
|
+
inst.type.should == :request
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#type" do
|
33
|
+
it "should default to :both" do
|
34
|
+
subject.type.should == :both
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should convert the type to a Symbol" do
|
38
|
+
subject[:type_flags] = ::HttpParser::TYPES[:request]
|
39
|
+
|
40
|
+
subject.type.should == :request
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should extract the type from the type_flags field" do
|
44
|
+
subject[:type_flags] = ((0xff & ~0x3) | ::HttpParser::TYPES[:response])
|
45
|
+
|
46
|
+
subject.type.should == :response
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#type=" do
|
51
|
+
it "should set the type" do
|
52
|
+
subject.type = :response
|
53
|
+
|
54
|
+
subject.type.should == :response
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should not change flags" do
|
58
|
+
flags = (0xff & ~0x3)
|
59
|
+
subject[:type_flags] = flags
|
60
|
+
|
61
|
+
subject.type = :request
|
62
|
+
|
63
|
+
subject[:type_flags].should == (flags | ::HttpParser::TYPES[:request])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#stop!" do
|
68
|
+
it "should throw :return, 1" do
|
69
|
+
lambda { subject.stop! }.should throw_symbol(:return,1)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#error!" do
|
74
|
+
it "should throw :return, -1" do
|
75
|
+
lambda { subject.error! }.should throw_symbol(:return,-1)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#reset!" do
|
80
|
+
it "should call http_parser_init" do
|
81
|
+
inst = described_class.new
|
82
|
+
|
83
|
+
::HttpParser.should_receive(:http_parser_init)
|
84
|
+
|
85
|
+
inst.reset!
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should not change the type" do
|
90
|
+
inst = described_class.new do |inst|
|
91
|
+
inst.type = :request
|
92
|
+
end
|
93
|
+
|
94
|
+
inst.reset!
|
95
|
+
inst.type.should == :request
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|