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 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