http_tools 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/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 &quot;example.com&quot;,\r\n&quot;example.net&quot;,\r\n or &quot;example.org&quot; 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