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.
@@ -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(:status) {|status, message| puts "#{status} #{message}"}
21
- parser.on(:headers) {|headers| puts headers.inspect}
22
- parser.on(:body) {|body| puts body}
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(:method) {|arg|}
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
- 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)
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
- parser = HTTPTools::Parser.new(TestDelegate.new)
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
@@ -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] ||= body.length
64
+ headers[CONTENT_LENGTH] = body.length
62
65
  elsif body.respond_to?(:stat)
63
- headers[CONTENT_LENGTH] ||= body.stat.size
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, request_body=nil, request_headers={}, response_has_body=true, &block)
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.force_no_body = !response_has_body
75
- response = nil
112
+ parser.allow_html_without_header = true
113
+ responses = []
76
114
 
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
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.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?
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
- 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
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
- response
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=nil)
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
- if parser
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
@@ -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
@@ -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
- unless scanner.is_a?(StringScanner)
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
- chunk = scanner.rest.slice(0, chunk_length)
151
- begin
152
- scanner.pos += chunk_length
153
- separator = scanner.scan(/\n|\r\n/)
154
- rescue RangeError
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 separator && chunk.length == chunk_length
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
@@ -2,4 +2,5 @@ module HTTPTools
2
2
  class ParseError < StandardError; end
3
3
  class EndOfMessageError < ParseError; end
4
4
  class MessageIncompleteError < EndOfMessageError; end
5
+ class EmptyMessageError < MessageIncompleteError; end
5
6
  end