http_tools 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -25,7 +25,7 @@ possible.
25
25
  puts parser.status_code + " " + parser.request_method
26
26
  puts parser.header.inspect
27
27
  end
28
- parser.on(:stream) {|chunk| print chunk}
28
+ parser.on(:finish) {print parser.body}
29
29
 
30
30
  parser << "HTTP/1.1 200 OK\r\n"
31
31
  parser << "Content-Length: 20\r\n\r\n"
@@ -45,11 +45,11 @@ mixin or class methods on HTTPTools::Encoding.
45
45
  === Example
46
46
 
47
47
  HTTPTools::Encoding.www_form_encode({"query" => "fish", "lang" => "en"})
48
- #=> "lang=en&query=fish"
48
+ #=> "query=fish&lang=en"
49
49
 
50
50
  include HTTPTools::Encoding
51
51
  www_form_decode("lang=en&query=fish")
52
- #=> {"query" => "fish", "lang" => "en"}
52
+ #=> {"lang" => "en", "query" => "fish"}
53
53
 
54
54
  == HTTPTools::Builder
55
55
 
@@ -0,0 +1,40 @@
1
+ base = File.expand_path(File.dirname(__FILE__) + '/../../lib')
2
+ require base + '/http_tools'
3
+ require 'benchmark'
4
+
5
+ Benchmark.bm(26) do |x|
6
+ body = "x" * 1024 * 1024
7
+ chunks = []
8
+ 64.times {|i| chunks << body[i * 64, body.length / 64]}
9
+
10
+ header = "HTTP/1.1 200 OK\r\nDate: Mon, 06 Jun 2011 14:55:51 GMT\r\nServer: Apache/2.2.17 (Unix) mod_ssl/2.2.17 OpenSSL/0.9.8l DAV/2 mod_fastcgi/2.4.2\r\nLast-Modified: Mon, 06 Jun 2011 14:55:49 GMT\r\nETag: \"3f18045-400-4a50c4c87c740\"\r\nAccept-Ranges: bytes\r\nContent-Length: #{body.length}\r\nContent-Type: text/plain\r\n\r\n"
11
+ x.report("content_length") do
12
+ 200.times do
13
+ parser = HTTPTools::Parser.new
14
+ parser << header
15
+ chunks.each {|chunk| parser << chunk}
16
+ end
17
+ end
18
+
19
+ header = "HTTP/1.1 200 OK\r\nDate: Mon, 06 Jun 2011 14:55:51 GMT\r\nServer: Apache/2.2.17 (Unix) mod_ssl/2.2.17 OpenSSL/0.9.8l DAV/2 mod_fastcgi/2.4.2\r\nLast-Modified: Mon, 06 Jun 2011 14:55:49 GMT\r\nETag: \"3f18045-400-4a50c4c87c740\"\r\nAccept-Ranges: bytes\r\nConnection: close\r\nContent-Type: text/plain\r\n\r\n"
20
+ x.report("close") do
21
+ 200.times do
22
+ parser = HTTPTools::Parser.new
23
+ parser << header
24
+ chunks.each {|chunk| parser << chunk}
25
+ parser.finish
26
+ end
27
+ end
28
+
29
+ header = "HTTP/1.1 200 OK\r\nDate: Mon, 06 Jun 2011 14:55:51 GMT\r\nServer: Apache/2.2.17 (Unix) mod_ssl/2.2.17 OpenSSL/0.9.8l DAV/2 mod_fastcgi/2.4.2\r\nLast-Modified: Mon, 06 Jun 2011 14:55:49 GMT\r\nETag: \"3f18045-400-4a50c4c87c740\"\r\nAccept-Ranges: bytes\r\nTransfer-Encoding: chunked\r\nContent-Type: text/plain\r\n\r\n"
30
+ chunks << nil
31
+ chunks.map!(&HTTPTools::Encoding.method(:transfer_encoding_chunked_encode))
32
+ x.report("chunked") do
33
+ 200.times do
34
+ parser = HTTPTools::Parser.new
35
+ parser << header
36
+ chunks.each {|chunk| parser << chunk}
37
+ end
38
+ end
39
+
40
+ end
@@ -3,24 +3,24 @@ require base + '/http_tools'
3
3
  require 'benchmark'
4
4
 
5
5
  Benchmark.bm(36) do |x|
6
+ encoded = "1\r\na\r\n" * 100 + "0\r\n"
6
7
  x.report("lots of very short chunks") do
7
- encoded = "1\r\na\r\n" * 100 + "0\r\n"
8
8
  1_000.times do
9
9
  HTTPTools::Encoding.transfer_encoding_chunked_decode(encoded)
10
10
  end
11
11
  end
12
12
 
13
+ encoded = "16\r\n<h1>Hello world</h1>\r\n\r\n12\r\n<p>Lorem ipsum</p>\r\n" * 50 + "0\r\n"
13
14
  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
15
  1_000.times do
16
16
  HTTPTools::Encoding.transfer_encoding_chunked_decode(encoded)
17
17
  end
18
18
  end
19
19
 
20
+ encoded = "2710\r\n#{"a" * 10000}\r\n" * 2 + "0\r\n"
20
21
  x.report("a couple of big chunks") do
21
- encoded = "2710\r\n#{"a" * 10000}\r\n" * 2 + "0\r\n"
22
22
  1_000.times do
23
23
  HTTPTools::Encoding.transfer_encoding_chunked_decode(encoded)
24
24
  end
25
25
  end
26
- end
26
+ end
@@ -1,6 +1,4 @@
1
- require 'uri'
2
1
  require 'socket'
3
- require 'stringio'
4
2
  require 'rubygems'
5
3
  require 'http_tools'
6
4
 
@@ -12,170 +10,57 @@ require 'http_tools'
12
10
  # puts "#{response.status} #{response.message}"
13
11
  # puts response.headers.inspect
14
12
  # 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
13
  #
24
14
  module HTTP
25
15
  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
- attr_writer :keepalive
16
+ Response = Struct.new(:status, :message, :headers, :body)
33
17
 
34
18
  def initialize(host, port=80)
35
- @host = host
36
- @port = port
37
- @pipeline = []
38
- end
39
-
40
- def socket
41
- @socket ||= TCPSocket.new(@host, @port)
19
+ @host, @port = host, port
42
20
  end
43
21
 
44
22
  def head(path, headers={})
45
23
  request(:head, path, nil, headers, false)
46
24
  end
47
25
 
48
- def get(path, headers={}, &block)
49
- request(:get, path, nil, headers, &block)
26
+ def get(path, headers={})
27
+ request(:get, path, nil, headers)
50
28
  end
51
29
 
52
- def post(path, body="", headers={}, &block)
53
- headers[CONTENT_TYPE] ||= WWW_FORM
54
- unless body.respond_to?(:read)
55
- if headers[CONTENT_TYPE] == WWW_FORM && body.respond_to?(:map) &&
56
- !body.kind_of?(String)
57
- body = www_form_encode(body)
58
- end
59
- body = StringIO.new(body.to_s)
60
- end
61
- if headers[CONTENT_LENGTH]
62
- # ok
63
- elsif body.respond_to?(:length)
64
- headers[CONTENT_LENGTH] = body.length
65
- elsif body.respond_to?(:stat)
66
- headers[CONTENT_LENGTH] = body.stat.size
67
- else
68
- raise "Content-Length must be supplied"
69
- end
70
-
71
- request(:post, path, body, headers, &block)
30
+ def post(path, body=nil, headers={})
31
+ request(:post, path, body, headers)
72
32
  end
73
33
 
74
- def pipeline
75
- @pipelining = true
76
- yield self
77
- pipeline_requests(@pipeline)
78
- ensure
79
- @pipelining = false
34
+ def put(path, body=nil, headers={})
35
+ request(:put, path, body, headers)
80
36
  end
81
37
 
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
38
+ def delete(path, headers={})
39
+ request(:delete, path, nil, headers)
91
40
  end
92
41
 
93
42
  private
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)
43
+ def request(method, path, body=nil, headers={}, response_has_body=true)
111
44
  parser = HTTPTools::Parser.new
112
- parser.allow_html_without_header = true
113
- responses = []
114
-
115
- parser.on(:finish) do |remainder|
116
- if responses.length < requests.length
117
- parser.reset
118
- parser << remainder.lstrip if remainder
119
- throw :reset
120
- end
121
- end
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)
129
- end
45
+ parser.force_no_body = !response_has_body
46
+ response = nil
130
47
 
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?
135
- end
48
+ parser.on(:finish) do
49
+ code, message = parser.status_code, parser.message
50
+ response = Response.new(code, message, parser.header, parser.body)
136
51
  end
137
52
 
53
+ socket = TCPSocket.new(@host, @port)
54
+ socket << HTTPTools::Builder.request(method, @host, path, headers)
55
+ socket << body if body
138
56
  begin
139
- catch(:reset) {parser << socket.sysread(1024 * 16)}
57
+ parser << socket.sysread(1024 * 16)
140
58
  rescue EOFError
141
- @socket = nil
142
- parser.finish
143
- break
59
+ break parser.finish
144
60
  end until parser.finished?
61
+ socket.close
145
62
 
146
- @socket = nil unless keepalive?
147
- responses
148
- end
149
- end
150
-
151
- class Response
152
- attr_reader :status, :message
153
- attr_accessor :headers, :body
154
-
155
- def initialize(status, message, headers={}, body="")
156
- @status = status
157
- @message = message
158
- @headers = headers
159
- @body = body
160
- end
161
-
162
- def stream(&block)
163
- @stream_callback = block
164
- nil
165
- end
166
-
167
- def receive_chunk(chunk) # :nodoc:
168
- body << chunk
169
- @stream_callback.call(chunk) if @stream_callback
170
- end
171
-
172
- def inspect
173
- bytesize = body.respond_to?(:bytesize) ? body.bytesize : body.to_s.length
174
- "#<Response #{status} #{message}: #{bytesize} bytes>"
175
- end
176
-
177
- def to_s
178
- body.to_s
63
+ response
179
64
  end
180
65
  end
181
- end
66
+ end
@@ -1,11 +1,9 @@
1
1
  require 'socket'
2
- require 'stringio'
3
2
  require 'rubygems'
4
3
  require 'http_tools'
5
4
 
6
5
  module HTTP
7
6
  class Server
8
- RACK_INPUT = "rack.input".freeze
9
7
  CONNECTION = "Connection".freeze
10
8
  KEEP_ALIVE = "Keep-Alive".freeze
11
9
  CLOSE = "close".freeze
@@ -39,16 +37,9 @@ module HTTP
39
37
  private
40
38
  def on_connection(socket)
41
39
  parser = HTTPTools::Parser.new
42
- env, input = nil
43
40
 
44
- parser.on(:header) do
45
- input = StringIO.new
46
- env = parser.env.merge!(RACK_INPUT => input).merge!(@instance_env)
47
- end
48
- parser.on(:stream) {|chunk| input << chunk}
49
41
  parser.on(:finish) do
50
- input.rewind
51
- status, header, body = @app.call(env)
42
+ status, header, body = @app.call(parser.env.merge!(@instance_env))
52
43
  keep_alive = keep_alive?(parser.version, parser.header[CONNECTION])
53
44
  header[CONNECTION] = keep_alive ? KEEP_ALIVE : CLOSE
54
45
  socket << HTTPTools::Builder.response(status, header)
@@ -74,7 +65,3 @@ module HTTP
74
65
 
75
66
  end
76
67
  end
77
-
78
- HTTP::Server.run(proc do |env|
79
- [200, {"Content-Length" => "5"}, "Hello"]
80
- end)
@@ -84,11 +84,13 @@ module HTTPTools
84
84
  505 => "HTTP Version Not Supported"}.freeze
85
85
  STATUS_DESCRIPTIONS.values.each {|val| val.freeze}
86
86
 
87
+ # :stopdoc: hide from rdoc as it makes a mess
87
88
  STATUS_LINES = Hash.new do |hash, key|
88
89
  code = if key.kind_of?(Integer) then key else STATUS_CODES[key] end
89
90
  description = STATUS_DESCRIPTIONS[code]
90
91
  hash[key] = "#{code} #{description}"
91
92
  end
93
+ # :startdoc:
92
94
 
93
95
  METHODS = %W{GET POST HEAD PUT DELETE OPTIONS TRACE CONNECT}.freeze
94
96
 
@@ -96,15 +98,15 @@ module HTTPTools
96
98
  100.upto(199) {|status_code| NO_BODY[status_code] = true}
97
99
  NO_BODY.freeze
98
100
 
99
- ARRAY_VALUE_HEADERS = {"Set-Cookie" => true} # presence of key tested, not val
101
+ Error = Class.new(StandardError)
102
+ ParseError = Class.new(Error)
103
+ EndOfMessageError = Class.new(ParseError)
104
+ MessageIncompleteError = Class.new(EndOfMessageError)
105
+ EmptyMessageError = Class.new(MessageIncompleteError)
100
106
 
101
107
  require_base = File.dirname(__FILE__) + '/http_tools/'
102
108
  autoload :Encoding, require_base + 'encoding'
103
109
  autoload :Parser, require_base + 'parser'
104
110
  autoload :Builder, require_base + 'builder'
105
- autoload :ParseError, require_base + 'errors'
106
- autoload :EndOfMessageError, require_base + 'errors'
107
- autoload :MessageIncompleteError, require_base + 'errors'
108
- autoload :EmptyMessageError, require_base + 'errors'
109
111
 
110
112
  end
@@ -4,8 +4,6 @@ module HTTPTools
4
4
  # responses. It can be used as a mixin or class methods on HTTPTools::Builder.
5
5
  #
6
6
  module Builder
7
- KEY_VALUE = "%s: %s\r\n".freeze
8
-
9
7
  module_function
10
8
 
11
9
  # :call-seq: Builder.response(status, headers={}) -> string
@@ -13,11 +11,15 @@ module HTTPTools
13
11
  # Returns a HTTP status line and headers. Status can be a HTTP status code
14
12
  # as an integer, or a HTTP status message as a lowercase, underscored
15
13
  # symbol.
16
- # Builder.response(200, "Content-Type" => "text/html")\
17
- #=> "HTTP/1.1 200 ok\r\nContent-Type: text/html\r\n\r\n"
14
+ # Builder.response(200, "Content-Type" => "text/html")
15
+ # #=> "HTTP/1.1 200 ok\r\nContent-Type: text/html\r\n\r\n"
16
+ #
17
+ # Builder.response(:internal_server_error)
18
+ # #=> "HTTP/1.1 500 Internal Server Error\r\n\r\n"
18
19
  #
19
- # Builder.response(:internal_server_error)\
20
- #=> "HTTP/1.1 500 Internal Server Error\r\n\r\n"
20
+ # To send multiple headers with the same name:
21
+ # Builder.response(:ok, "Set-Cookie" => ["a=b", "c=d"])
22
+ # Builder.response(:ok, "Set-Cookie" => "a=b\nc=d")
21
23
  #
22
24
  def response(status, headers={})
23
25
  "HTTP/1.1 #{STATUS_LINES[status]}\r\n#{format_headers(headers)}\r\n"
@@ -26,10 +28,11 @@ module HTTPTools
26
28
  # :call-seq: Builder.request(method, host, path="/", headers={}) -> string
27
29
  #
28
30
  # 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
+ # Builder.request(:get, "example.com")
32
+ # #=> "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
31
33
  #
32
- # Builder.request(:post, "example.com", "/form", "Accept" => "text/html")\
34
+ # Builder.request(:post, "example.com", "/form", "Accept" => "text/html")
35
+ #\
33
36
  #=> "POST" /form HTTP/1.1\r\nHost: example.com\r\nAccept: text/html\r\n\r\n"
34
37
  #
35
38
  def request(method, host, path="/", headers={})
@@ -37,8 +40,17 @@ module HTTPTools
37
40
  format_headers(headers)}\r\n"
38
41
  end
39
42
 
40
- def format_headers(headers)
41
- headers.inject("") {|buffer, kv| buffer << KEY_VALUE % kv}
43
+ def format_headers(headers, buffer="")
44
+ headers.each do |key, value|
45
+ if value.respond_to?(:each_line)
46
+ value.each_line {|val| buffer << "#{key}: #{val.chomp}\r\n"}
47
+ elsif value.respond_to?(:each)
48
+ value.each {|val| buffer << "#{key}: #{val}\r\n"}
49
+ else
50
+ buffer << "#{key}: #{value}\r\n"
51
+ end
52
+ end
53
+ buffer
42
54
  end
43
55
  private :format_headers
44
56
  class << self
@@ -7,6 +7,7 @@ module HTTPTools
7
7
  # used as a mixin or class methods on HTTPTools::Encoding.
8
8
  #
9
9
  module Encoding
10
+ # :stopdoc:
10
11
  HEX_BIG_ENDIAN_2_BYTES = "H2".freeze
11
12
  HEX_BIG_ENDIAN_REPEATING = "H*".freeze
12
13
  PERCENT = "%".freeze
@@ -15,6 +16,7 @@ module HTTPTools
15
16
  AMPERSAND = "&".freeze
16
17
  EQUALS = "=".freeze
17
18
  CHUNK_FORMAT = "%x\r\n%s\r\n".freeze
19
+ # :startdoc:
18
20
 
19
21
  module_function
20
22
 
@@ -44,7 +46,7 @@ module HTTPTools
44
46
  #
45
47
  # Takes a Hash and converts it to a String as if it was a HTML form being
46
48
  # submitted, eg
47
- # {"query" => "fish", "lang" => "en"} becomes "lang=en&query=fish"
49
+ # {"query" => "fish", "lang" => "en"} becomes "query=fish&lang=en"
48
50
  #
49
51
  # To get multiple key value pairs with the same key use an array as the
50
52
  # value, eg
@@ -64,7 +66,7 @@ module HTTPTools
64
66
  #
65
67
  # Takes a String resulting from a HTML form being submitted, and converts it
66
68
  # to a hash,
67
- # eg "lang=en&query=fish" becomes {"query" => "fish", "lang" => "en"}
69
+ # eg "lang=en&query=fish" becomes {"lang" => "en", "query" => "fish"}
68
70
  #
69
71
  # Multiple key value pairs with the same key will become a single key with
70
72
  # an array value, eg "lang=en&lang=fr" becomes {"lang" => ["en", "fr"]}