http_tools 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +80 -0
- data/bench/parser/request_bench.rb +59 -0
- data/bench/parser/response_bench.rb +21 -0
- data/example/http_client.rb +132 -0
- data/lib/http_tools.rb +110 -0
- data/lib/http_tools/builder.rb +49 -0
- data/lib/http_tools/encoding.rb +169 -0
- data/lib/http_tools/errors.rb +5 -0
- data/lib/http_tools/parser.rb +478 -0
- data/profile/parser/request_profile.rb +12 -0
- data/profile/parser/response_profile.rb +12 -0
- data/test/builder/request_test.rb +26 -0
- data/test/builder/response_test.rb +32 -0
- data/test/cover.rb +28 -0
- data/test/encoding/transfer_encoding_chunked_test.rb +141 -0
- data/test/encoding/url_encoding_test.rb +37 -0
- data/test/encoding/www_form_test.rb +42 -0
- data/test/parser/request_test.rb +481 -0
- data/test/parser/response_test.rb +446 -0
- data/test/runner.rb +1 -0
- metadata +89 -0
data/README.rdoc
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
= HTTPTools
|
2
|
+
|
3
|
+
HTTPTools is a collection of lower level utilities to aid working with HTTP,
|
4
|
+
including a fast-as-possible pure Ruby HTTP parser.
|
5
|
+
|
6
|
+
* rdoc[http://sourcetagsandcodes.com/http_tools/doc/]
|
7
|
+
* source[https://github.com/matsadler/http_tools]
|
8
|
+
|
9
|
+
== HTTPTools::Parser
|
10
|
+
|
11
|
+
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
|
+
Despite being just Ruby, every effort has been made to ensure it is as fast as
|
15
|
+
possible.
|
16
|
+
|
17
|
+
=== Example
|
18
|
+
|
19
|
+
parser = HTTPTools::Parser.new
|
20
|
+
parser.on(:status) {|status, message| puts "#{status} #{message}"}
|
21
|
+
parser.on(:headers) {|headers| puts headers.inspect}
|
22
|
+
parser.on(:body) {|body| puts body}
|
23
|
+
|
24
|
+
parser << "HTTP/1.1 200 OK\r\n"
|
25
|
+
parser << "Content-Length: 20\r\n\r\n"
|
26
|
+
parser << "<h1>Hello world</h1>"
|
27
|
+
|
28
|
+
Prints:
|
29
|
+
200 OK
|
30
|
+
{"Content-Length" => "20"}
|
31
|
+
<h1>Hello world</h1>
|
32
|
+
|
33
|
+
== HTTPTools::Encoding
|
34
|
+
|
35
|
+
HTTPTools::Encoding provides methods to deal with several HTTP related encodings
|
36
|
+
including url, www-form, and chunked transfer encoding. It can be used as a
|
37
|
+
mixin or class methods on HTTPTools::Encoding.
|
38
|
+
|
39
|
+
=== Example
|
40
|
+
|
41
|
+
HTTPTools::Encoding.www_form_encode({"query" => "fish", "lang" => "en"})
|
42
|
+
#=> "lang=en&query=fish"
|
43
|
+
|
44
|
+
include HTTPTools::Encoding
|
45
|
+
www_form_decode("lang=en&query=fish")
|
46
|
+
#=> {"query" => "fish", "lang" => "en"}
|
47
|
+
|
48
|
+
== HTTPTools::Builder
|
49
|
+
|
50
|
+
HTTPTools::Builder is a provides a simple interface to build HTTP requests &
|
51
|
+
responses. It can be used as a mixin or class methods on HTTPTools::Builder.
|
52
|
+
|
53
|
+
=== Example
|
54
|
+
|
55
|
+
Builder.request(:get, "example.com")\
|
56
|
+
#=> "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
57
|
+
|
58
|
+
== Licence
|
59
|
+
|
60
|
+
(The MIT License)
|
61
|
+
|
62
|
+
Copyright (c) 2011 Matthew Sadler
|
63
|
+
|
64
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
65
|
+
of this software and associated documentation files (the "Software"), to deal
|
66
|
+
in the Software without restriction, including without limitation the rights
|
67
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
68
|
+
copies of the Software, and to permit persons to whom the Software is
|
69
|
+
furnished to do so, subject to the following conditions:
|
70
|
+
|
71
|
+
The above copyright notice and this permission notice shall be included in
|
72
|
+
all copies or substantial portions of the Software.
|
73
|
+
|
74
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
75
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
76
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
77
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
78
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
79
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
80
|
+
THE SOFTWARE.
|
@@ -0,0 +1,59 @@
|
|
1
|
+
base = File.expand_path(File.dirname(__FILE__) + '/../../lib')
|
2
|
+
require base + '/http_tools'
|
3
|
+
require 'benchmark'
|
4
|
+
|
5
|
+
request = "GET / HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_8; en-gb) AppleWebKit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16\r\nAccept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5\r\nAccept-Language: en-gb\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\n\r\n"
|
6
|
+
|
7
|
+
Benchmark.bm(41) do |x|
|
8
|
+
x.report("HTTPTools::Parser") do
|
9
|
+
10_000.times do
|
10
|
+
HTTPTools::Parser.new << request
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
x.report("HTTPTools::Parser (reset)") do
|
15
|
+
parser = HTTPTools::Parser.new
|
16
|
+
10_000.times do
|
17
|
+
parser << request
|
18
|
+
parser.reset
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
x.report("HTTPTools::Parser (reset, with callbacks)") do
|
23
|
+
parser = HTTPTools::Parser.new
|
24
|
+
parser.on(:method) {|arg|}
|
25
|
+
parser.on(:path) {|arg, arg2|}
|
26
|
+
parser.on(:headers) {|arg|}
|
27
|
+
10_000.times do
|
28
|
+
parser << request
|
29
|
+
parser.reset
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
x.report("HTTPTools::Parser (reset, with delegate)") do
|
34
|
+
class TestDelegate
|
35
|
+
def on_method(arg)
|
36
|
+
end
|
37
|
+
def on_path(arg, arg2)
|
38
|
+
end
|
39
|
+
def on_headers(arg)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
parser = HTTPTools::Parser.new(TestDelegate.new)
|
43
|
+
10_000.times do
|
44
|
+
parser << request
|
45
|
+
parser.reset
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
begin
|
50
|
+
require 'rubygems'
|
51
|
+
require 'http11'
|
52
|
+
x.report("Mongrel::HttpParser") do
|
53
|
+
10_000.times do
|
54
|
+
Mongrel::HttpParser.new.execute({}, request.dup, 0)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
rescue LoadError
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
base = File.expand_path(File.dirname(__FILE__) + '/../../lib')
|
2
|
+
require base + '/http_tools'
|
3
|
+
require 'benchmark'
|
4
|
+
|
5
|
+
response = "HTTP/1.1 200 OK\r\nServer: Apache/2.2.3 (CentOS)\r\nLast-Modified: Thu, 03 Jun 2010 17:40:12 GMT\r\nETag: \"4d2c-23e-48823b2cf3700\"\r\nAccept-Ranges: bytes\r\nContent-Type: text/html; charset=UTF-8\r\nConnection: Keep-Alive\r\nDate: Wed, 21 Jul 2010 16:26:04 GMT\r\nAge: 7985 \r\nContent-Length: 574\r\n\r\n<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\r\n<HTML>\r\n<HEAD>\r\n <META http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n <TITLE>Example Web Page</TITLE>\r\n</HEAD> \r\n<body> \r\n<p>You have reached this web page by typing "example.com",\r\n"example.net",\r\n or "example.org" into your web browser.</p>\r\n<p>These domain names are reserved for use in documentation and are not available \r\n for registration. See <a href=\"http://www.rfc-editor.org/rfc/rfc2606.txt\">RFC \r\n 2606</a>, Section 3.</p>\r\n</BODY>\r\n</HTML>\r\n\r\n"
|
6
|
+
|
7
|
+
Benchmark.bm(25) do |x|
|
8
|
+
x.report("HTTPTools::Parser") do
|
9
|
+
10_000.times do
|
10
|
+
HTTPTools::Parser.new << response
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
x.report("HTTPTools::Parser (reset)") do
|
15
|
+
parser = HTTPTools::Parser.new
|
16
|
+
10_000.times do
|
17
|
+
parser << response
|
18
|
+
parser.reset
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'socket'
|
3
|
+
require 'stringio'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'http_tools'
|
6
|
+
|
7
|
+
# Usage:
|
8
|
+
# uri = URI.parse("http://example.com/")
|
9
|
+
# client = HTTP::Client.new(uri.host, uri.port)
|
10
|
+
# response = client.get(uri.path)
|
11
|
+
#
|
12
|
+
# puts "#{response.status} #{response.message}"
|
13
|
+
# puts response.headers.inspect
|
14
|
+
# puts response.body
|
15
|
+
#
|
16
|
+
# Streaming response:
|
17
|
+
# client.get(uri.path) do |response|
|
18
|
+
# puts "#{response.status} #{response.message}"
|
19
|
+
# response.stream do |chunk|
|
20
|
+
# print chunk
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
module HTTP
|
25
|
+
class Client
|
26
|
+
include HTTPTools::Encoding
|
27
|
+
|
28
|
+
CONTENT_TYPE = "Content-Type".freeze
|
29
|
+
CONTENT_LENGTH = "Content-Length".freeze
|
30
|
+
WWW_FORM = "application/x-www-form-urlencoded".freeze
|
31
|
+
|
32
|
+
def initialize(host, port=80)
|
33
|
+
@host = host
|
34
|
+
@port = port
|
35
|
+
end
|
36
|
+
|
37
|
+
def socket
|
38
|
+
@socket ||= TCPSocket.new(@host, @port)
|
39
|
+
end
|
40
|
+
|
41
|
+
def head(path, headers={})
|
42
|
+
request(:head, path, nil, headers, false)
|
43
|
+
end
|
44
|
+
|
45
|
+
def get(path, headers={}, &block)
|
46
|
+
request(:get, path, nil, headers, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def post(path, body="", headers={}, &block)
|
50
|
+
headers[CONTENT_TYPE] ||= WWW_FORM
|
51
|
+
unless body.respond_to?(:read)
|
52
|
+
if headers[CONTENT_TYPE] == WWW_FORM && body.respond_to?(:map) &&
|
53
|
+
!body.kind_of?(String)
|
54
|
+
body = www_form_encode(body)
|
55
|
+
end
|
56
|
+
body = StringIO.new(body.to_s)
|
57
|
+
end
|
58
|
+
if headers[CONTENT_LENGTH]
|
59
|
+
# ok
|
60
|
+
elsif body.respond_to?(:length)
|
61
|
+
headers[CONTENT_LENGTH] ||= body.length
|
62
|
+
elsif body.respond_to?(:stat)
|
63
|
+
headers[CONTENT_LENGTH] ||= body.stat.size
|
64
|
+
else
|
65
|
+
raise "Content-Length must be supplied"
|
66
|
+
end
|
67
|
+
|
68
|
+
request(:post, path, body, headers, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def request(method, path, request_body=nil, request_headers={}, response_has_body=true, &block)
|
73
|
+
parser = HTTPTools::Parser.new
|
74
|
+
parser.force_no_body = !response_has_body
|
75
|
+
response = nil
|
76
|
+
|
77
|
+
parser.add_listener(:status) {|s, m| response = Response.new(s, m)}
|
78
|
+
parser.add_listener(:headers) do |headers|
|
79
|
+
response.headers = headers
|
80
|
+
if block
|
81
|
+
response.parser = parser
|
82
|
+
block.call(response)
|
83
|
+
response.parser = nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
parser.add_listener(:body) {|body| response.body = body} unless block
|
87
|
+
|
88
|
+
socket << HTTPTools::Builder.request(method, @host, path, request_headers)
|
89
|
+
if request_body
|
90
|
+
socket << request_body.read(1024 * 16) until request_body.eof?
|
91
|
+
end
|
92
|
+
|
93
|
+
until parser.finished?
|
94
|
+
begin
|
95
|
+
readable, = select([socket], nil, nil)
|
96
|
+
parser << socket.read_nonblock(1024 * 16) if readable.any?
|
97
|
+
rescue EOFError
|
98
|
+
parser.finish
|
99
|
+
break
|
100
|
+
end
|
101
|
+
end
|
102
|
+
response
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class Response
|
107
|
+
attr_reader :status, :message
|
108
|
+
attr_accessor :headers, :body
|
109
|
+
attr_accessor :parser # :nodoc:
|
110
|
+
|
111
|
+
def initialize(status, message, headers={}, body=nil)
|
112
|
+
@status = status
|
113
|
+
@message = message
|
114
|
+
@headers = headers
|
115
|
+
@body = body
|
116
|
+
end
|
117
|
+
|
118
|
+
def stream(&block)
|
119
|
+
if parser
|
120
|
+
parser.add_listener(:stream, block)
|
121
|
+
else
|
122
|
+
block.call(body)
|
123
|
+
end
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def inspect
|
128
|
+
bytesize = body.respond_to?(:bytesize) ? body.bytesize : body.to_s.length
|
129
|
+
"#<Response #{status} #{message}: #{bytesize} bytes>"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/http_tools.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
module HTTPTools
|
2
|
+
STATUS_CODES = {
|
3
|
+
:continue => 100,
|
4
|
+
:switching_protocols => 101,
|
5
|
+
:ok => 200,
|
6
|
+
:created => 201,
|
7
|
+
:accepted => 202,
|
8
|
+
:non_authoritative_information => 203,
|
9
|
+
:no_content => 204,
|
10
|
+
:reset_content => 205,
|
11
|
+
:partial_content => 206,
|
12
|
+
:multiple_choices => 300,
|
13
|
+
:moved_permanently => 301,
|
14
|
+
:found => 302,
|
15
|
+
:see_other => 303,
|
16
|
+
:not_modified => 304,
|
17
|
+
:use_proxy => 305,
|
18
|
+
:temporary_redirect => 307,
|
19
|
+
:bad_request => 400,
|
20
|
+
:unauthorized => 401,
|
21
|
+
:payment_required => 402,
|
22
|
+
:forbidden => 403,
|
23
|
+
:not_found => 404,
|
24
|
+
:method_not_allowed => 405,
|
25
|
+
:not_acceptable => 406,
|
26
|
+
:proxy_authentication_required => 407,
|
27
|
+
:request_timeout => 408,
|
28
|
+
:conflict => 409,
|
29
|
+
:gone => 410,
|
30
|
+
:length_required => 411,
|
31
|
+
:precondition_failed => 412,
|
32
|
+
:request_entity_too_large => 413,
|
33
|
+
:request_uri_too_long => 414,
|
34
|
+
:unsupported_media_type => 415,
|
35
|
+
:requested_range_not_satisfiable => 416,
|
36
|
+
:expectation_failed => 417,
|
37
|
+
:internal_server_error => 500,
|
38
|
+
:not_implemented => 501,
|
39
|
+
:bad_gateway => 502,
|
40
|
+
:service_unavailable => 503,
|
41
|
+
:gateway_timeout => 504,
|
42
|
+
:http_version_not_supported => 505}.freeze
|
43
|
+
|
44
|
+
STATUS_DESCRIPTIONS = {
|
45
|
+
100 => "Continue",
|
46
|
+
101 => "Switching Protocols",
|
47
|
+
200 => "OK",
|
48
|
+
201 => "Created",
|
49
|
+
202 => "Accepted",
|
50
|
+
203 => "Non-Authoritative Information",
|
51
|
+
204 => "No Content",
|
52
|
+
205 => "Reset Content",
|
53
|
+
206 => "Partial Content",
|
54
|
+
300 => "Multiple Choices",
|
55
|
+
301 => "Moved Permanently",
|
56
|
+
302 => "Found",
|
57
|
+
303 => "See Other",
|
58
|
+
304 => "Not Modified",
|
59
|
+
305 => "Use Proxy",
|
60
|
+
307 => "Temporary Redirect",
|
61
|
+
400 => "Bad Request",
|
62
|
+
401 => "Unauthorized",
|
63
|
+
402 => "Payment Required",
|
64
|
+
403 => "Forbidden",
|
65
|
+
404 => "Not Found",
|
66
|
+
405 => "Method Not Allowed",
|
67
|
+
406 => "Not Acceptable",
|
68
|
+
407 => "Proxy Authentication Required",
|
69
|
+
408 => "Request Timeout",
|
70
|
+
409 => "Conflict",
|
71
|
+
410 => "Gone",
|
72
|
+
411 => "Length Required",
|
73
|
+
412 => "Precondition Failed",
|
74
|
+
413 => "Request Entity Too Large",
|
75
|
+
414 => "Request-URI Too Long",
|
76
|
+
415 => "Unsupported Media Type",
|
77
|
+
416 => "Requested Range Not Satisfiable",
|
78
|
+
417 => "Expectation Failed",
|
79
|
+
500 => "Internal Server Error",
|
80
|
+
501 => "Not Implemented",
|
81
|
+
502 => "Bad Gateway",
|
82
|
+
503 => "Service Unavailable",
|
83
|
+
504 => "Gateway Timeout",
|
84
|
+
505 => "HTTP Version Not Supported"}.freeze
|
85
|
+
STATUS_DESCRIPTIONS.values.each {|val| val.freeze}
|
86
|
+
|
87
|
+
STATUS_LINES = Hash.new do |hash, key|
|
88
|
+
code = if key.kind_of?(Integer) then key else STATUS_CODES[key] end
|
89
|
+
description = STATUS_DESCRIPTIONS[code]
|
90
|
+
hash[key] = "#{code} #{description}"
|
91
|
+
end
|
92
|
+
|
93
|
+
METHODS = %W{GET POST HEAD PUT DELETE OPTIONS TRACE CONNECT}.freeze
|
94
|
+
|
95
|
+
NO_BODY = Hash.new {|hash, key| hash[key] = false}
|
96
|
+
NO_BODY.merge!(204 => true, 304 => true, nil => false)
|
97
|
+
100.upto(199) {|status_code| NO_BODY[status_code] = true}
|
98
|
+
|
99
|
+
CRLF = "\r\n".freeze
|
100
|
+
SPACE = " ".freeze
|
101
|
+
|
102
|
+
require_base = File.dirname(__FILE__) + '/http_tools/'
|
103
|
+
autoload :Encoding, require_base + 'encoding'
|
104
|
+
autoload :Parser, require_base + 'parser'
|
105
|
+
autoload :Builder, require_base + 'builder'
|
106
|
+
autoload :ParseError, require_base + 'errors'
|
107
|
+
autoload :EndOfMessageError, require_base + 'errors'
|
108
|
+
autoload :MessageIncompleteError, require_base + 'errors'
|
109
|
+
|
110
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module HTTPTools
|
2
|
+
|
3
|
+
# HTTPTools::Builder is a provides a simple interface to build HTTP requests &
|
4
|
+
# responses. It can be used as a mixin or class methods on HTTPTools::Builder.
|
5
|
+
#
|
6
|
+
module Builder
|
7
|
+
KEY_VALUE = "%s: %s\r\n".freeze
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# :call-seq: Builder.response(status, headers={}) -> string
|
12
|
+
#
|
13
|
+
# Returns a HTTP status line and headers. Status can be a HTTP status code
|
14
|
+
# as an integer, or a HTTP status message as a lowercase, underscored
|
15
|
+
# symbol.
|
16
|
+
# Builder.response(200, "Content-Type" => "text/html")\
|
17
|
+
#=> "HTTP/1.1 200 ok\r\nContent-Type: text/html\r\n\r\n"
|
18
|
+
#
|
19
|
+
# Builder.response(:internal_server_error)\
|
20
|
+
#=> "HTTP/1.1 500 Internal Server Error\r\n\r\n"
|
21
|
+
#
|
22
|
+
def response(status, headers={})
|
23
|
+
"HTTP/1.1 #{STATUS_LINES[status]}\r\n#{format_headers(headers)}\r\n"
|
24
|
+
end
|
25
|
+
|
26
|
+
# :call-seq: Builder.request(method, host, path="/", headers={}) -> string
|
27
|
+
#
|
28
|
+
# Returns a HTTP request line and headers.
|
29
|
+
# Builder.request(:get, "example.com")\
|
30
|
+
#=> "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
|
31
|
+
#
|
32
|
+
# Builder.request(:post, "example.com", "/form", "Accept" => "text/html")\
|
33
|
+
#=> "POST" /form HTTP/1.1\r\nHost: example.com\r\nAccept: text/html\r\n\r\n"
|
34
|
+
#
|
35
|
+
def request(method, host, path="/", headers={})
|
36
|
+
"#{method.to_s.upcase} #{path} HTTP/1.1\r\nHost: #{host}\r\n#{
|
37
|
+
format_headers(headers)}\r\n"
|
38
|
+
end
|
39
|
+
|
40
|
+
def format_headers(headers)
|
41
|
+
headers.inject("") {|buffer, kv| buffer << KEY_VALUE % kv}
|
42
|
+
end
|
43
|
+
private :format_headers
|
44
|
+
class << self
|
45
|
+
private :format_headers
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|