http_tools 0.1.0 → 0.2.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/README.rdoc +11 -5
- data/bench/parser/request_bench.rb +11 -15
- data/bench/parser/response_bench.rb +12 -0
- data/bench/transfer_encoding_chunked_bench.rb +26 -0
- data/example/http_client.rb +81 -32
- data/example/http_server.rb +82 -0
- data/lib/http_tools.rb +4 -0
- data/lib/http_tools/encoding.rb +25 -24
- data/lib/http_tools/errors.rb +1 -0
- data/lib/http_tools/parser.rb +170 -157
- data/test/encoding/transfer_encoding_chunked_test.rb +22 -0
- data/test/parser/request_test.rb +169 -126
- data/test/parser/response_test.rb +520 -52
- metadata +7 -5
data/README.rdoc
CHANGED
@@ -6,20 +6,26 @@ including a fast-as-possible pure Ruby HTTP parser.
|
|
6
6
|
* rdoc[http://sourcetagsandcodes.com/http_tools/doc/]
|
7
7
|
* source[https://github.com/matsadler/http_tools]
|
8
8
|
|
9
|
+
== Platform Support
|
10
|
+
|
11
|
+
Written purely in Ruby, with no dependencies outside of the standard library, it
|
12
|
+
should run across all Ruby implementations compatible with 1.8 or later, and
|
13
|
+
install in environments without a compiler available.
|
14
|
+
|
9
15
|
== HTTPTools::Parser
|
10
16
|
|
11
17
|
HTTPTools::Parser is a HTTP request & response parser with an evented API.
|
12
|
-
Written purely in Ruby, with no dependencies, it should run across all Ruby
|
13
|
-
implementations, and install in environments without a compiler available.
|
14
18
|
Despite being just Ruby, every effort has been made to ensure it is as fast as
|
15
19
|
possible.
|
16
20
|
|
17
21
|
=== Example
|
18
22
|
|
19
23
|
parser = HTTPTools::Parser.new
|
20
|
-
parser.on(:
|
21
|
-
|
22
|
-
|
24
|
+
parser.on(:header) do |header|
|
25
|
+
puts parser.status_code + " " + parser.method
|
26
|
+
puts parser.header.inspect
|
27
|
+
end
|
28
|
+
parser.on(:stream) {|chunk| print chunk}
|
23
29
|
|
24
30
|
parser << "HTTP/1.1 200 OK\r\n"
|
25
31
|
parser << "Content-Length: 20\r\n\r\n"
|
@@ -21,29 +21,25 @@ Benchmark.bm(41) do |x|
|
|
21
21
|
|
22
22
|
x.report("HTTPTools::Parser (reset, with callbacks)") do
|
23
23
|
parser = HTTPTools::Parser.new
|
24
|
-
parser.on(:
|
25
|
-
parser.on(:path) {|arg, arg2|}
|
26
|
-
parser.on(:headers) {|arg|}
|
24
|
+
parser.on(:headers) {}
|
27
25
|
10_000.times do
|
28
26
|
parser << request
|
29
27
|
parser.reset
|
30
28
|
end
|
31
29
|
end
|
32
30
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
31
|
+
begin
|
32
|
+
require 'rubygems'
|
33
|
+
require 'http/parser'
|
34
|
+
x.report("Http::Parser") do
|
35
|
+
10_000.times do
|
36
|
+
parser = Http::Parser.new
|
37
|
+
parser.on_headers_complete = Proc.new {}
|
38
|
+
parser.on_message_complete = Proc.new {}
|
39
|
+
parser << request
|
40
40
|
end
|
41
41
|
end
|
42
|
-
|
43
|
-
10_000.times do
|
44
|
-
parser << request
|
45
|
-
parser.reset
|
46
|
-
end
|
42
|
+
rescue LoadError
|
47
43
|
end
|
48
44
|
|
49
45
|
begin
|
@@ -18,4 +18,16 @@ Benchmark.bm(25) do |x|
|
|
18
18
|
parser.reset
|
19
19
|
end
|
20
20
|
end
|
21
|
+
|
22
|
+
begin
|
23
|
+
require 'rubygems'
|
24
|
+
require 'http/parser'
|
25
|
+
x.report("Http::Parser") do
|
26
|
+
10_000.times do
|
27
|
+
parser = Http::Parser.new
|
28
|
+
parser << response
|
29
|
+
end
|
30
|
+
end
|
31
|
+
rescue LoadError
|
32
|
+
end
|
21
33
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
base = File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
require base + '/http_tools'
|
3
|
+
require 'benchmark'
|
4
|
+
|
5
|
+
Benchmark.bm(36) do |x|
|
6
|
+
x.report("lots of very short chunks") do
|
7
|
+
encoded = "1\r\na\r\n" * 100 + "0\r\n"
|
8
|
+
1_000.times do
|
9
|
+
HTTPTools::Encoding.transfer_encoding_chunked_decode(encoded)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
x.report("slightly less slightly longer chunks") do
|
14
|
+
encoded = "16\r\n<h1>Hello world</h1>\r\n\r\n12\r\n<p>Lorem ipsum</p>\r\n" * 50 + "0\r\n"
|
15
|
+
1_000.times do
|
16
|
+
HTTPTools::Encoding.transfer_encoding_chunked_decode(encoded)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
x.report("a couple of big chunks") do
|
21
|
+
encoded = "2710\r\n#{"a" * 10000}\r\n" * 2 + "0\r\n"
|
22
|
+
1_000.times do
|
23
|
+
HTTPTools::Encoding.transfer_encoding_chunked_decode(encoded)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/example/http_client.rb
CHANGED
@@ -29,9 +29,12 @@ module HTTP
|
|
29
29
|
CONTENT_LENGTH = "Content-Length".freeze
|
30
30
|
WWW_FORM = "application/x-www-form-urlencoded".freeze
|
31
31
|
|
32
|
+
attr_writer :keepalive
|
33
|
+
|
32
34
|
def initialize(host, port=80)
|
33
35
|
@host = host
|
34
36
|
@port = port
|
37
|
+
@pipeline = []
|
35
38
|
end
|
36
39
|
|
37
40
|
def socket
|
@@ -58,9 +61,9 @@ module HTTP
|
|
58
61
|
if headers[CONTENT_LENGTH]
|
59
62
|
# ok
|
60
63
|
elsif body.respond_to?(:length)
|
61
|
-
headers[CONTENT_LENGTH]
|
64
|
+
headers[CONTENT_LENGTH] = body.length
|
62
65
|
elsif body.respond_to?(:stat)
|
63
|
-
headers[CONTENT_LENGTH]
|
66
|
+
headers[CONTENT_LENGTH] = body.stat.size
|
64
67
|
else
|
65
68
|
raise "Content-Length must be supplied"
|
66
69
|
end
|
@@ -68,47 +71,88 @@ module HTTP
|
|
68
71
|
request(:post, path, body, headers, &block)
|
69
72
|
end
|
70
73
|
|
74
|
+
def pipeline
|
75
|
+
@pipelining = true
|
76
|
+
yield self
|
77
|
+
pipeline_requests(@pipeline)
|
78
|
+
ensure
|
79
|
+
@pipelining = false
|
80
|
+
end
|
81
|
+
|
82
|
+
def keepalive?
|
83
|
+
@keepalive
|
84
|
+
end
|
85
|
+
|
86
|
+
def keepalive
|
87
|
+
self.keepalive, original = true, keepalive?
|
88
|
+
yield self
|
89
|
+
ensure
|
90
|
+
self.keepalive = original
|
91
|
+
end
|
92
|
+
|
71
93
|
private
|
72
|
-
def request(method, path,
|
94
|
+
def request(method, path, body=nil, headers={}, response_has_body=true, &b)
|
95
|
+
request = {
|
96
|
+
:method => method,
|
97
|
+
:path => path,
|
98
|
+
:body => body,
|
99
|
+
:headers => headers,
|
100
|
+
:response_has_body => response_has_body,
|
101
|
+
:block => b}
|
102
|
+
if @pipelining
|
103
|
+
@pipeline << request
|
104
|
+
nil
|
105
|
+
else
|
106
|
+
pipeline_requests([request]).first
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def pipeline_requests(requests)
|
73
111
|
parser = HTTPTools::Parser.new
|
74
|
-
parser.
|
75
|
-
|
112
|
+
parser.allow_html_without_header = true
|
113
|
+
responses = []
|
76
114
|
|
77
|
-
parser.
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
block.call(response)
|
83
|
-
response.parser = nil
|
115
|
+
parser.on(:finish) do |remainder|
|
116
|
+
if responses.length < requests.length
|
117
|
+
parser.reset
|
118
|
+
parser << remainder.lstrip if remainder
|
119
|
+
throw :reset
|
84
120
|
end
|
85
121
|
end
|
86
|
-
parser.
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
122
|
+
parser.on(:header) do
|
123
|
+
request = requests[responses.length]
|
124
|
+
parser.force_no_body = !request[:response_has_body]
|
125
|
+
response = Response.new(parser.status_code, parser.message)
|
126
|
+
response.headers = parser.header
|
127
|
+
parser.on(:stream) {|chunk| response.receive_chunk(chunk)}
|
128
|
+
responses.push(response)
|
91
129
|
end
|
92
130
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
rescue EOFError
|
98
|
-
parser.finish
|
99
|
-
break
|
131
|
+
requests.each do |r|
|
132
|
+
socket << HTTPTools::Builder.request(r[:method], @host, r[:path], r[:headers])
|
133
|
+
if body = r[:body]
|
134
|
+
socket << body.read(1024 * 16) until body.eof?
|
100
135
|
end
|
101
136
|
end
|
102
|
-
|
137
|
+
|
138
|
+
begin
|
139
|
+
catch(:reset) {parser << socket.sysread(1024 * 16)}
|
140
|
+
rescue EOFError
|
141
|
+
@socket = nil
|
142
|
+
parser.finish
|
143
|
+
break
|
144
|
+
end until parser.finished?
|
145
|
+
|
146
|
+
@socket = nil unless keepalive?
|
147
|
+
responses
|
103
148
|
end
|
104
149
|
end
|
105
150
|
|
106
151
|
class Response
|
107
152
|
attr_reader :status, :message
|
108
153
|
attr_accessor :headers, :body
|
109
|
-
attr_accessor :parser # :nodoc:
|
110
154
|
|
111
|
-
def initialize(status, message, headers={}, body=
|
155
|
+
def initialize(status, message, headers={}, body="")
|
112
156
|
@status = status
|
113
157
|
@message = message
|
114
158
|
@headers = headers
|
@@ -116,17 +160,22 @@ module HTTP
|
|
116
160
|
end
|
117
161
|
|
118
162
|
def stream(&block)
|
119
|
-
|
120
|
-
parser.add_listener(:stream, block)
|
121
|
-
else
|
122
|
-
block.call(body)
|
123
|
-
end
|
163
|
+
@stream_callback = block
|
124
164
|
nil
|
125
165
|
end
|
126
166
|
|
167
|
+
def receive_chunk(chunk) # :nodoc:
|
168
|
+
body << chunk
|
169
|
+
@stream_callback.call(chunk) if @stream_callback
|
170
|
+
end
|
171
|
+
|
127
172
|
def inspect
|
128
173
|
bytesize = body.respond_to?(:bytesize) ? body.bytesize : body.to_s.length
|
129
174
|
"#<Response #{status} #{message}: #{bytesize} bytes>"
|
130
175
|
end
|
176
|
+
|
177
|
+
def to_s
|
178
|
+
body.to_s
|
179
|
+
end
|
131
180
|
end
|
132
181
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'stringio'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'http_tools'
|
5
|
+
|
6
|
+
module HTTP
|
7
|
+
class Server
|
8
|
+
RACK_INPUT = "rack.input".freeze
|
9
|
+
NO_BODY = {"GET" => true, "HEAD" => true}
|
10
|
+
CONNECTION = "Connection".freeze
|
11
|
+
KEEP_ALIVE = "Keep-Alive".freeze
|
12
|
+
CLOSE = "close".freeze
|
13
|
+
ONE_ONE = "1.1".freeze
|
14
|
+
|
15
|
+
def initialize(app, options={})
|
16
|
+
host = options[:host] || options[:Host] || "0.0.0.0"
|
17
|
+
port = (options[:port] || options[:Port] || 9292).to_s
|
18
|
+
@app = app
|
19
|
+
@instance_env = {"SERVER_NAME" => host, "SERVER_PORT" => port,
|
20
|
+
"rack.multithread" => true}
|
21
|
+
@server = TCPServer.new(host, port)
|
22
|
+
@server.listen(1024)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.run(app, options={})
|
26
|
+
new(app, options).listen
|
27
|
+
end
|
28
|
+
|
29
|
+
def listen
|
30
|
+
while socket = @server.accept
|
31
|
+
Thread.new do
|
32
|
+
begin
|
33
|
+
on_connection(socket)
|
34
|
+
rescue StandardError, LoadError, SyntaxError => e
|
35
|
+
STDERR.puts("#{e.class}: #{e.message} #{e.backtrace.join("\n")}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def on_connection(socket)
|
43
|
+
parser = HTTPTools::Parser.new
|
44
|
+
env, input = nil
|
45
|
+
|
46
|
+
parser.on(:header) do
|
47
|
+
parser.force_no_body = NO_BODY[parser.request_method]
|
48
|
+
input = StringIO.new
|
49
|
+
env = parser.env.merge!(RACK_INPUT => input).merge!(@instance_env)
|
50
|
+
end
|
51
|
+
parser.on(:stream) {|chunk| input << chunk}
|
52
|
+
parser.on(:finish) do |remainder|
|
53
|
+
input.rewind
|
54
|
+
status, header, body = @app.call(env)
|
55
|
+
keep_alive = keep_alive?(parser.version, parser.header[CONNECTION])
|
56
|
+
header[CONNECTION] = keep_alive ? KEEP_ALIVE : CLOSE
|
57
|
+
socket << HTTPTools::Builder.response(status, header)
|
58
|
+
body.each {|chunk| socket << chunk}
|
59
|
+
body.close if body.respond_to?(:close)
|
60
|
+
if keep_alive
|
61
|
+
parser.reset
|
62
|
+
parser << remainder.lstrip if remainder
|
63
|
+
throw :reset
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
begin
|
68
|
+
readable, = select([socket], nil, nil, 30)
|
69
|
+
break unless readable
|
70
|
+
catch(:reset) {parser << socket.read_nonblock(1024 * 16)}
|
71
|
+
rescue EOFError
|
72
|
+
break
|
73
|
+
end until parser.finished?
|
74
|
+
socket.close
|
75
|
+
end
|
76
|
+
|
77
|
+
def keep_alive?(http_version, connection)
|
78
|
+
http_version == ONE_ONE && connection != CLOSE || connection == KEEP_ALIVE
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
data/lib/http_tools.rb
CHANGED
@@ -96,6 +96,9 @@ module HTTPTools
|
|
96
96
|
NO_BODY.merge!(204 => true, 304 => true, nil => false)
|
97
97
|
100.upto(199) {|status_code| NO_BODY[status_code] = true}
|
98
98
|
|
99
|
+
ARRAY_VALUE_HEADERS = Hash.new {|hash, key| hash[key] = false}
|
100
|
+
ARRAY_VALUE_HEADERS.merge!("Set-Cookie" => true)
|
101
|
+
|
99
102
|
CRLF = "\r\n".freeze
|
100
103
|
SPACE = " ".freeze
|
101
104
|
|
@@ -106,5 +109,6 @@ module HTTPTools
|
|
106
109
|
autoload :ParseError, require_base + 'errors'
|
107
110
|
autoload :EndOfMessageError, require_base + 'errors'
|
108
111
|
autoload :MessageIncompleteError, require_base + 'errors'
|
112
|
+
autoload :EmptyMessageError, require_base + 'errors'
|
109
113
|
|
110
114
|
end
|
data/lib/http_tools/encoding.rb
CHANGED
@@ -134,35 +134,36 @@ module HTTPTools
|
|
134
134
|
# while remainder
|
135
135
|
# remainder << get_data
|
136
136
|
# chunk, remainder = transfer_encoding_chunked_decode(remainder)
|
137
|
-
# decoded << chunk
|
137
|
+
# decoded << chunk if chunk
|
138
138
|
# end
|
139
139
|
#
|
140
|
-
def transfer_encoding_chunked_decode(scanner)
|
141
|
-
|
142
|
-
scanner = StringScanner.new(scanner.dup)
|
143
|
-
end
|
144
|
-
hex_chunk_length = scanner.scan(/[0-9a-fA-F]+\r?\n/)
|
145
|
-
return [nil, scanner.string] unless hex_chunk_length
|
146
|
-
|
147
|
-
chunk_length = hex_chunk_length.to_i(16)
|
148
|
-
return [nil, nil] if chunk_length == 0
|
140
|
+
def transfer_encoding_chunked_decode(str, scanner=StringScanner.new(str))
|
141
|
+
decoded = ""
|
149
142
|
|
150
|
-
|
151
|
-
|
152
|
-
scanner.
|
153
|
-
|
154
|
-
|
143
|
+
remainder = while true
|
144
|
+
start_pos = scanner.pos
|
145
|
+
hex_chunk_length = scanner.scan(/[0-9a-f]+ *\r?\n/i)
|
146
|
+
break scanner.rest unless hex_chunk_length
|
147
|
+
|
148
|
+
chunk_length = hex_chunk_length.to_i(16)
|
149
|
+
break nil if chunk_length == 0
|
150
|
+
|
151
|
+
begin
|
152
|
+
chunk = scanner.rest.slice(0, chunk_length)
|
153
|
+
scanner.pos += chunk_length
|
154
|
+
if chunk && scanner.skip(/\r?\n/i)
|
155
|
+
decoded << chunk
|
156
|
+
else
|
157
|
+
scanner.pos = start_pos
|
158
|
+
break scanner.rest
|
159
|
+
end
|
160
|
+
rescue RangeError
|
161
|
+
scanner.pos = start_pos
|
162
|
+
break scanner.rest
|
163
|
+
end
|
155
164
|
end
|
156
165
|
|
157
|
-
if
|
158
|
-
scanner.string.replace(scanner.rest)
|
159
|
-
scanner.reset
|
160
|
-
rest, remainder = transfer_encoding_chunked_decode(scanner)
|
161
|
-
chunk << rest if rest
|
162
|
-
[chunk, remainder]
|
163
|
-
else
|
164
|
-
[nil, scanner.string]
|
165
|
-
end
|
166
|
+
[(decoded if decoded.length > 0), remainder]
|
166
167
|
end
|
167
168
|
|
168
169
|
end
|