http_tools 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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