http_tools 0.3.0 → 0.4.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.
@@ -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"]}