http_tools 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,9 +12,12 @@ Written purely in Ruby, with no dependencies outside of the standard library, it
12
12
  should run across all Ruby implementations compatible with 1.8 or later, and
13
13
  install in environments without a compiler available.
14
14
 
15
- Tests are currently run on travis-ci[http://travis-ci.org/] against Ruby 1.8.6, 1.8.7, 1.9.2, 1.9.3, JRuby, Rubinius, Rubinius 2, and Ruby Enterprise Edition. Additionally tests are run against MacRuby 0.10
15
+ Tests are currently run on travis-ci[http://travis-ci.org/matsadler/http_tools]
16
+ against Ruby 1.8.6, 1.8.7, 1.9.2, 1.9.3, JRuby, Rubinius, Rubinius 2, and Ruby
17
+ Enterprise Edition. Additionally tests are run against MacRuby 0.10
16
18
 
17
- Performance tuning is mainly aimed at Ruby 1.9, with Ruby 1.8 and JRuby taken in to consideration. JRuby is generally fastest.
19
+ Performance tuning is mainly aimed at Ruby 1.9, with Ruby 1.8 and JRuby taken in
20
+ to consideration. JRuby is generally fastest.
18
21
 
19
22
  == HTTPTools::Parser
20
23
 
@@ -62,7 +65,7 @@ responses. It can be used as a mixin or class methods on HTTPTools::Builder.
62
65
 
63
66
  === Example
64
67
 
65
- Builder.request(:get, "example.com")\
68
+ Builder.request(:get, "example.com")
66
69
  #=> "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
67
70
 
68
71
  == Licence
@@ -31,9 +31,8 @@ module HTTPTools
31
31
  # Builder.request(:get, "example.com")
32
32
  # #=> "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
33
33
  #
34
- # Builder.request(:post, "example.com", "/form", "Accept" => "text/html")
35
- #\
36
- #=> "POST" /form HTTP/1.1\r\nHost: example.com\r\nAccept: text/html\r\n\r\n"
34
+ # Builder.request(:post, "example.com", "/s", "Accept" => "text/html")
35
+ # #=> "POST /s HTTP/1.1\r\nHost: example.com\r\nAccept: text/html\r\n\r\n"
37
36
  #
38
37
  def request(method, host, path="/", headers={})
39
38
  "#{method.to_s.upcase} #{path} HTTP/1.1\r\nHost: #{host}\r\n#{
@@ -109,27 +109,27 @@ module HTTPTools
109
109
  # the decoded response and nil.
110
110
  # Example:
111
111
  # encoded_string = "3\r\nfoo\r\n3\r\nbar\r\n0\r\n"
112
- # Encoding.transfer_encoding_chunked_decode(encoded_string)\
113
- # => ["foobar", nil]
112
+ # Encoding.transfer_encoding_chunked_decode(encoded_string)
113
+ # #=> ["foobar", nil]
114
114
  #
115
115
  # Decoding a partial response will return an array of the response decoded
116
116
  # so far, and the remainder of the encoded string.
117
117
  # Example
118
118
  # encoded_string = "3\r\nfoo\r\n3\r\nba"
119
- # Encoding.transfer_encoding_chunked_decode(encoded_string)\
120
- # => ["foo", "3\r\nba"]
119
+ # Encoding.transfer_encoding_chunked_decode(encoded_string)
120
+ # #=> ["foo", "3\r\nba"]
121
121
  #
122
122
  # If the chunks are complete, but there is no empty terminating chunk, the
123
123
  # second element in the array will be an empty string.
124
124
  # encoded_string = "3\r\nfoo\r\n3\r\nbar"
125
- # Encoding.transfer_encoding_chunked_decode(encoded_string)\
126
- # => ["foobar", ""]
125
+ # Encoding.transfer_encoding_chunked_decode(encoded_string)
126
+ # #=> ["foobar", ""]
127
127
  #
128
128
  # If nothing can be decoded the first element in the array will be nil and
129
129
  # the second the remainder
130
130
  # encoded_string = "3\r\nfo"
131
- # Encoding.transfer_encoding_chunked_decode(encoded_string)\
132
- # => [nil, "3\r\nfo"]
131
+ # Encoding.transfer_encoding_chunked_decode(encoded_string)
132
+ # #=> [nil, "3\r\nfo"]
133
133
  #
134
134
  # Example use:
135
135
  # include Encoding
@@ -31,6 +31,7 @@ module HTTPTools
31
31
  # :stopdoc:
32
32
  EMPTY = "".freeze
33
33
  COLON = ":".freeze
34
+ SPACE = " ".freeze
34
35
  KEY_TERMINATOR = ": ".freeze
35
36
  CONTENT_LENGTH = "Content-Length".freeze
36
37
  TRANSFER_ENCODING = "Transfer-Encoding".freeze
@@ -124,10 +125,15 @@ module HTTPTools
124
125
  #
125
126
  # Returns a Rack compatible environment hash. Will return nil if called
126
127
  # before headers are complete.
128
+ #
129
+ # "SERVER_NAME" and "SERVER_PORT" are only supplied if they can be
130
+ # determined from the request (eg, they are present in the "Host" header).
127
131
  #
128
132
  # "rack.input" is only supplied if #env is called after parsing the request
129
- # has finsished, and no listener is set for the `stream` event, otherwise
130
- # you must add it yourself to make the environment hash fully Rack compliant
133
+ # has finsished, and no listener is set for the `stream` event
134
+ #
135
+ # If not supplied, you must ensure "SERVER_NAME", "SERVER_PORT", and
136
+ # "rack.input" are present to make the environment hash fully Rack compliant
131
137
  #
132
138
  def env
133
139
  return unless @header_complete
@@ -141,8 +147,8 @@ module HTTPTools
141
147
  env[upper_key.freeze] = value
142
148
  end
143
149
  host, port = env[HTTP_HOST].split(COLON) if env.key?(HTTP_HOST)
144
- env[SERVER_NAME] = host
145
- env[SERVER_PORT] = port || "80"
150
+ env[SERVER_NAME] = host if host
151
+ env[SERVER_PORT] = port if port
146
152
  @trailer.each {|k, val| env[HTTP_ + k.tr(LOWERCASE, UPPERCASE)] = val}
147
153
  if @body || @stream_callback == method(:setup_stream_callback)
148
154
  env[RACK_INPUT] = StringIO.new(@body || "")
@@ -409,7 +415,7 @@ module HTTPTools
409
415
  def value
410
416
  value = @buffer.scan(/[^\x00\n\x7f]*\n/i)
411
417
  if value
412
- value.chop!
418
+ value.strip!
413
419
  if @header.key?(@last_key)
414
420
  @header[@last_key] << "\n#{value}"
415
421
  else
@@ -420,7 +426,7 @@ module HTTPTools
420
426
  elsif TRANSFER_ENCODING.casecmp(@last_key) == 0
421
427
  @chunked = CHUNKED.casecmp(value) == 0
422
428
  end
423
- key_or_newline
429
+ value_extention
424
430
  elsif @buffer.eos? || @buffer.check(/[^\x00\n\x7f]+\Z/i)
425
431
  :value
426
432
  else
@@ -428,6 +434,19 @@ module HTTPTools
428
434
  end
429
435
  end
430
436
 
437
+ def value_extention
438
+ if @buffer.check(/[^ \t]/)
439
+ key_or_newline
440
+ elsif value_extra = @buffer.scan(/[ \t]+[^\x00\n\x7f]*\n/)
441
+ value_extra.sub!(/^[ \t]+/, SPACE)
442
+ value_extra.chop!
443
+ (@header[@last_key] << value_extra).strip!
444
+ value_extention
445
+ else
446
+ :value_extention
447
+ end
448
+ end
449
+
431
450
  def start_body
432
451
  if @request_method && !(@content_left || @chunked) ||
433
452
  NO_BODY.key?(@status_code) || @force_no_body
@@ -532,13 +551,13 @@ module HTTPTools
532
551
  def trailer_value
533
552
  value = @buffer.scan(/[^\000\n\177]+\n/i)
534
553
  if value
535
- value.chop!
554
+ value.strip!
536
555
  if @trailer.key?(@last_key)
537
556
  @trailer[@last_key] << "\n#{value}"
538
557
  else
539
558
  @trailer[@last_key.freeze] = value
540
559
  end
541
- trailer_key_or_newline
560
+ trailer_value_extention
542
561
  elsif @buffer.eos? || @buffer.check(/[^\x00\n\x7f]+\Z/i)
543
562
  :trailer_value
544
563
  else
@@ -546,6 +565,19 @@ module HTTPTools
546
565
  end
547
566
  end
548
567
 
568
+ def trailer_value_extention
569
+ if @buffer.check(/[^ \t]/)
570
+ trailer_key_or_newline
571
+ elsif value_extra = @buffer.scan(/[ \t]+[^\x00\n\x7f]*\n/)
572
+ value_extra.sub!(/^[ \t]+/, SPACE)
573
+ value_extra.chop!
574
+ (@trailer[@last_key] << value_extra).strip!
575
+ trailer_value_extention
576
+ else
577
+ :trailer_value_extention
578
+ end
579
+ end
580
+
549
581
  def end_of_message
550
582
  raise EndOfMessageError.new("Message ended") if @state == :end_of_message
551
583
  @finish_callback.call if @finish_callback
@@ -589,7 +589,7 @@ class ParserRequestTest < Test::Unit::TestCase
589
589
  assert_equal("/test", env["PATH_INFO"])
590
590
  assert_equal("q=foo", env["QUERY_STRING"])
591
591
  assert_equal("www.example.com", env["SERVER_NAME"])
592
- assert_equal("80", env["SERVER_PORT"])
592
+ assert(!env.key?("SERVER_PORT"), "env must not contain SERVER_PORT")
593
593
  assert_equal("www.example.com", env["HTTP_HOST"])
594
594
  assert_equal("text/html", env["HTTP_ACCEPT"])
595
595
 
@@ -623,7 +623,7 @@ class ParserRequestTest < Test::Unit::TestCase
623
623
  assert_equal("/submit", env["PATH_INFO"])
624
624
  assert_equal("", env["QUERY_STRING"])
625
625
  assert_equal("www.example.com", env["SERVER_NAME"])
626
- assert_equal("80", env["SERVER_PORT"])
626
+ assert(!env.key?("SERVER_PORT"), "env must not contain SERVER_PORT")
627
627
  assert_equal("www.example.com", env["HTTP_HOST"])
628
628
  assert_equal("chunked", env["HTTP_TRANSFER_ENCODING"])
629
629
  assert_equal("X-Checksum", env["HTTP_TRAILER"])
@@ -703,15 +703,16 @@ class ParserRequestTest < Test::Unit::TestCase
703
703
 
704
704
  def test_env_no_host
705
705
  parser = HTTPTools::Parser.new
706
+ env = nil
706
707
 
707
708
  parser << "GET /test?q=foo HTTP/1.1\r\n\r\n"
708
709
 
709
710
  assert_nothing_raised(NoMethodError) do
710
- parser.env
711
+ env = parser.env
711
712
  end
712
- assert_equal(nil, parser.env["HTTP_HOST"])
713
- assert_equal(nil, parser.env["SERVER_NAME"])
714
- assert_equal("80", parser.env["SERVER_PORT"])
713
+ assert(!env.key?("HTTP_HOST"), "env must not contain HTTP_HOST")
714
+ assert(!env.key?("SERVER_NAME"), "env must not contain SERVER_NAME")
715
+ assert(!env.key?("SERVER_PORT"), "env must not contain SERVER_PORT")
715
716
  end
716
717
 
717
718
  def test_inspect
@@ -235,6 +235,73 @@ class ParserResponseTest < Test::Unit::TestCase
235
235
  assert_equal({"Set-Cookie" => "foo=bar\nbaz=qux"}, headers)
236
236
  end
237
237
 
238
+ def test_multi_line_header_value
239
+ parser = HTTPTools::Parser.new
240
+ headers = nil
241
+
242
+ parser.add_listener(:header) {headers = parser.header}
243
+
244
+ parser << "HTTP/1.1 200 OK\r\n"
245
+ parser << "Content-Type: text/html;\r\n"
246
+ parser << " charset=utf-8\r\n\r\n"
247
+
248
+ assert_equal({"Content-Type" => "text/html; charset=utf-8"}, headers)
249
+ end
250
+
251
+ def test_multi_line_header_value_in_one_chunk
252
+ parser = HTTPTools::Parser.new
253
+ headers = nil
254
+
255
+ parser.add_listener(:header) {headers = parser.header}
256
+
257
+ parser << "HTTP/1.1 200 OK\r\n"
258
+ parser << "Content-Type: text/html;\r\n charset=utf-8\r\n\r\n"
259
+
260
+ assert_equal({"Content-Type" => "text/html; charset=utf-8"}, headers)
261
+ end
262
+
263
+ def test_multi_line_header_value_sub_line_chunks
264
+ parser = HTTPTools::Parser.new
265
+ headers = nil
266
+
267
+ parser.add_listener(:header) {headers = parser.header}
268
+
269
+ parser << "HTTP/1.1 200 OK\r\n"
270
+ parser << "Content-Type:"
271
+ parser << " text/"
272
+ parser << "html;\r\n "
273
+ parser << "charset="
274
+ parser << "utf-8\r\n\r\n"
275
+
276
+ assert_equal({"Content-Type" => "text/html; charset=utf-8"}, headers)
277
+ end
278
+
279
+ def test_header_value_separated_by_newline
280
+ parser = HTTPTools::Parser.new
281
+ headers = nil
282
+
283
+ parser.add_listener(:header) {headers = parser.header}
284
+
285
+ parser << "HTTP/1.1 200 OK\r\n"
286
+ parser << "Content-Type:\r\n"
287
+ parser << "\t text/html;\r\n"
288
+ parser << "\t charset=utf-8\r\n\r\n"
289
+
290
+ assert_equal({"Content-Type" => "text/html; charset=utf-8"}, headers)
291
+ end
292
+
293
+ def test_header_value_leading_and_trailing_whitespace_is_stripped
294
+ parser = HTTPTools::Parser.new
295
+ headers = nil
296
+
297
+ parser.add_listener(:header) {headers = parser.header}
298
+
299
+ parser << "HTTP/1.1 200 OK\r\n"
300
+ parser << "Content-Type:\t text/html \t\r\n\r\n"
301
+
302
+ assert_equal({"Content-Type" => "text/html"}, headers)
303
+ end
304
+
238
305
  def test_skip_junk_headers_at_end
239
306
  parser = HTTPTools::Parser.new
240
307
  code, message, headers = nil
@@ -526,7 +593,7 @@ class ParserResponseTest < Test::Unit::TestCase
526
593
  parser << "\r\n"
527
594
  parser << "<h1>Hello world</h1>"
528
595
 
529
- assert_equal({"Page-Completion-Status" => "Normal\r", "Content-Length" => "20"}, headers)
596
+ assert_equal({"Page-Completion-Status" => "Normal", "Content-Length" => "20"}, headers)
530
597
  assert_equal("<h1>Hello world</h1>", body)
531
598
  assert(parser.finished?, "parser should be finished")
532
599
  end
@@ -1021,6 +1088,88 @@ class ParserResponseTest < Test::Unit::TestCase
1021
1088
  assert_equal({"X-Test" => "1\n2"}, trailer)
1022
1089
  end
1023
1090
 
1091
+ def test_multi_line_trailer_value
1092
+ parser = HTTPTools::Parser.new
1093
+ trailer = nil
1094
+
1095
+ parser.add_listener(:trailer) {trailer = parser.trailer}
1096
+
1097
+ parser << "HTTP/1.1 200 OK\r\n"
1098
+ parser << "Transfer-Encoding: chunked\r\n"
1099
+ parser << "Trailer: X-Test\r\n\r\n"
1100
+ parser << "14\r\n<h1>Hello world</h1>\r\n0\r\n"
1101
+ parser << "X-Test: one\r\n"
1102
+ parser << " two\r\n\r\n"
1103
+
1104
+ assert_equal({"X-Test" => "one two"}, trailer)
1105
+ end
1106
+
1107
+ def test_multi_line_trailer_value_in_one_chunk
1108
+ parser = HTTPTools::Parser.new
1109
+ trailer = nil
1110
+
1111
+ parser.add_listener(:trailer) {trailer = parser.trailer}
1112
+
1113
+ parser << "HTTP/1.1 200 OK\r\n"
1114
+ parser << "Transfer-Encoding: chunked\r\n"
1115
+ parser << "Trailer: X-Test\r\n\r\n"
1116
+ parser << "14\r\n<h1>Hello world</h1>\r\n0\r\n"
1117
+ parser << "X-Test: one\r\n two\r\n\r\n"
1118
+
1119
+ assert_equal({"X-Test" => "one two"}, trailer)
1120
+ end
1121
+
1122
+ def test_multi_line_trailer_value_sub_line_chunks
1123
+ parser = HTTPTools::Parser.new
1124
+ trailer = nil
1125
+
1126
+ parser.add_listener(:trailer) {trailer = parser.trailer}
1127
+
1128
+ parser << "HTTP/1.1 200 OK\r\n"
1129
+ parser << "Transfer-Encoding: chunked\r\n"
1130
+ parser << "Trailer: X-Test\r\n\r\n"
1131
+ parser << "14\r\n<h1>Hello world</h1>\r\n0\r\n"
1132
+ parser << "X-Test:"
1133
+ parser << " on"
1134
+ parser << "e\r\n "
1135
+ parser << "t"
1136
+ parser << "wo\r\n\r\n"
1137
+
1138
+ assert_equal({"X-Test" => "one two"}, trailer)
1139
+ end
1140
+
1141
+ def test_trailer_value_separated_by_newline
1142
+ parser = HTTPTools::Parser.new
1143
+ trailer = nil
1144
+
1145
+ parser.add_listener(:trailer) {trailer = parser.trailer}
1146
+
1147
+ parser << "HTTP/1.1 200 OK\r\n"
1148
+ parser << "Transfer-Encoding: chunked\r\n"
1149
+ parser << "Trailer: X-Test\r\n\r\n"
1150
+ parser << "14\r\n<h1>Hello world</h1>\r\n0\r\n"
1151
+ parser << "X-Test:\r\n"
1152
+ parser << "\t one\r\n"
1153
+ parser << "\t two\r\n\r\n"
1154
+
1155
+ assert_equal({"X-Test" => "one two"}, trailer)
1156
+ end
1157
+
1158
+ def test_trailer_value_leading_and_trailing_whitespace_is_stripped
1159
+ parser = HTTPTools::Parser.new
1160
+ trailer = nil
1161
+
1162
+ parser.add_listener(:trailer) {trailer = parser.trailer}
1163
+
1164
+ parser << "HTTP/1.1 200 OK\r\n"
1165
+ parser << "Transfer-Encoding: chunked\r\n"
1166
+ parser << "Trailer: X-Test\r\n\r\n"
1167
+ parser << "14\r\n<h1>Hello world</h1>\r\n0\r\n"
1168
+ parser << "X-Test:\t one \t\r\n\r\n"
1169
+
1170
+ assert_equal({"X-Test" => "one"}, trailer)
1171
+ end
1172
+
1024
1173
  def test_messed_up_iis_header_style_trailer_1
1025
1174
  parser = HTTPTools::Parser.new
1026
1175
  trailer = nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-09-25 00:00:00.000000000 Z
12
+ date: 2011-10-09 00:00:00.000000000Z
13
13
  dependencies: []
14
14
  description: A fast-as-possible pure Ruby HTTP parser plus associated lower level
15
15
  utilities to aid working with HTTP and the web.