httpclient 2.1.5 → 2.8.3

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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +85 -0
  3. data/bin/httpclient +77 -0
  4. data/bin/jsonclient +85 -0
  5. data/lib/hexdump.rb +50 -0
  6. data/lib/http-access2.rb +6 -4
  7. data/lib/httpclient/auth.rb +575 -173
  8. data/lib/httpclient/cacert.pem +3952 -0
  9. data/lib/httpclient/cacert1024.pem +3866 -0
  10. data/lib/httpclient/connection.rb +6 -2
  11. data/lib/httpclient/cookie.rb +162 -504
  12. data/lib/httpclient/http.rb +334 -119
  13. data/lib/httpclient/include_client.rb +85 -0
  14. data/lib/httpclient/jruby_ssl_socket.rb +588 -0
  15. data/lib/httpclient/session.rb +385 -288
  16. data/lib/httpclient/ssl_config.rb +195 -155
  17. data/lib/httpclient/ssl_socket.rb +150 -0
  18. data/lib/httpclient/timeout.rb +14 -10
  19. data/lib/httpclient/util.rb +142 -6
  20. data/lib/httpclient/version.rb +3 -0
  21. data/lib/httpclient/webagent-cookie.rb +459 -0
  22. data/lib/httpclient.rb +509 -202
  23. data/lib/jsonclient.rb +63 -0
  24. data/lib/oauthclient.rb +111 -0
  25. data/sample/async.rb +8 -0
  26. data/sample/auth.rb +11 -0
  27. data/sample/cookie.rb +18 -0
  28. data/sample/dav.rb +103 -0
  29. data/sample/howto.rb +49 -0
  30. data/sample/jsonclient.rb +67 -0
  31. data/sample/oauth_buzz.rb +57 -0
  32. data/sample/oauth_friendfeed.rb +59 -0
  33. data/sample/oauth_twitter.rb +61 -0
  34. data/sample/ssl/0cert.pem +22 -0
  35. data/sample/ssl/0key.pem +30 -0
  36. data/sample/ssl/1000cert.pem +19 -0
  37. data/sample/ssl/1000key.pem +18 -0
  38. data/sample/ssl/htdocs/index.html +10 -0
  39. data/sample/ssl/ssl_client.rb +22 -0
  40. data/sample/ssl/webrick_httpsd.rb +29 -0
  41. data/sample/stream.rb +21 -0
  42. data/sample/thread.rb +27 -0
  43. data/sample/wcat.rb +21 -0
  44. data/test/ca-chain.pem +44 -0
  45. data/test/ca.cert +23 -0
  46. data/test/client-pass.key +18 -0
  47. data/test/client.cert +19 -0
  48. data/test/client.key +15 -0
  49. data/test/helper.rb +131 -0
  50. data/test/htdigest +1 -0
  51. data/test/htpasswd +2 -0
  52. data/test/jruby_ssl_socket/test_pemutils.rb +32 -0
  53. data/test/runner.rb +2 -0
  54. data/test/server.cert +19 -0
  55. data/test/server.key +15 -0
  56. data/test/sslsvr.rb +65 -0
  57. data/test/subca.cert +21 -0
  58. data/test/test_auth.rb +492 -0
  59. data/test/test_cookie.rb +309 -0
  60. data/test/test_hexdump.rb +14 -0
  61. data/test/test_http-access2.rb +508 -0
  62. data/test/test_httpclient.rb +2145 -0
  63. data/test/test_include_client.rb +52 -0
  64. data/test/test_jsonclient.rb +80 -0
  65. data/test/test_ssl.rb +559 -0
  66. data/test/test_webagent-cookie.rb +465 -0
  67. metadata +85 -44
  68. data/lib/httpclient/auth.rb.orig +0 -513
  69. data/lib/httpclient/cacert.p7s +0 -1579
  70. data/lib/httpclient.rb.orig +0 -1020
  71. data/lib/tags +0 -908
@@ -1,5 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  # HTTPClient - HTTP client library.
2
- # Copyright (C) 2000-2009 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
4
+ # Copyright (C) 2000-2015 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
3
5
  #
4
6
  # This program is copyrighted free software by NAKAMURA, Hiroshi. You can
5
7
  # redistribute it and/or modify it under the same terms of Ruby's license;
@@ -7,6 +9,10 @@
7
9
 
8
10
 
9
11
  require 'time'
12
+ if defined?(Encoding::ASCII_8BIT)
13
+ require 'open-uri' # for encoding
14
+ end
15
+ require 'httpclient/util'
10
16
 
11
17
 
12
18
  # A namespace module for HTTP Message definitions used by HTTPClient.
@@ -90,12 +96,13 @@ module HTTP
90
96
  # p res.header['last-modified'].first
91
97
  #
92
98
  class Message
99
+ include HTTPClient::Util
93
100
 
94
101
  CRLF = "\r\n"
95
102
 
96
103
  # Represents HTTP message header.
97
104
  class Headers
98
- # HTTP version in a HTTP header. Float.
105
+ # HTTP version in a HTTP header. String.
99
106
  attr_accessor :http_version
100
107
  # Size of body. nil when size is unknown (e.g. chunked response).
101
108
  attr_reader :body_size
@@ -109,7 +116,7 @@ module HTTP
109
116
  # Request only. Requested query.
110
117
  attr_accessor :request_query
111
118
  # Request only. Requested via proxy or not.
112
- attr_accessor :request_via_proxy
119
+ attr_accessor :request_absolute_uri
113
120
 
114
121
  # Response only. HTTP status
115
122
  attr_reader :status_code
@@ -122,6 +129,8 @@ module HTTP
122
129
  attr_accessor :body_charset # :nodoc:
123
130
  # Used for dumping response.
124
131
  attr_accessor :body_date # :nodoc:
132
+ # Used for keeping content encoding.
133
+ attr_reader :body_encoding # :nodoc:
125
134
 
126
135
  # HTTP response status code to reason phrase mapping definition.
127
136
  STATUS_CODE_MAP = {
@@ -151,14 +160,14 @@ module HTTP
151
160
  # Creates a Message::Headers. Use init_request, init_response, or
152
161
  # init_connect_request for acutual initialize.
153
162
  def initialize
154
- @http_version = 1.1
163
+ @http_version = '1.1'
155
164
  @body_size = nil
156
165
  @chunked = false
157
166
 
158
167
  @request_method = nil
159
168
  @request_uri = nil
160
169
  @request_query = nil
161
- @request_via_proxy = nil
170
+ @request_absolute_uri = nil
162
171
 
163
172
  @status_code = nil
164
173
  @reason_phrase = nil
@@ -166,6 +175,7 @@ module HTTP
166
175
  @body_type = nil
167
176
  @body_charset = nil
168
177
  @body_date = nil
178
+ @body_encoding = nil
169
179
 
170
180
  @is_request = nil
171
181
  @header_item = []
@@ -178,24 +188,31 @@ module HTTP
178
188
  @request_method = 'CONNECT'
179
189
  @request_uri = uri
180
190
  @request_query = nil
181
- @http_version = 1.0
191
+ @http_version = '1.0'
182
192
  end
183
193
 
184
194
  # Placeholder URI object for nil uri.
185
- NIL_URI = URI.parse('http://nil-uri-given/')
195
+ NIL_URI = HTTPClient::Util.urify('http://nil-uri-given/')
186
196
  # Initialize this instance as a general request.
187
197
  def init_request(method, uri, query = nil)
188
198
  @is_request = true
189
199
  @request_method = method
190
200
  @request_uri = uri || NIL_URI
191
201
  @request_query = query
192
- @request_via_proxy = false
202
+ @request_absolute_uri = false
203
+ self
193
204
  end
194
205
 
195
206
  # Initialize this instance as a response.
196
- def init_response(status_code)
207
+ def init_response(status_code, req = nil)
197
208
  @is_request = false
198
209
  self.status_code = status_code
210
+ if req
211
+ @request_method = req.request_method
212
+ @request_uri = req.request_uri
213
+ @request_query = req.request_query
214
+ end
215
+ self
199
216
  end
200
217
 
201
218
  # Sets status code and reason phrase.
@@ -205,14 +222,31 @@ module HTTP
205
222
  end
206
223
 
207
224
  # Returns 'Content-Type' header value.
208
- def contenttype
225
+ def content_type
209
226
  self['Content-Type'][0]
210
227
  end
211
228
 
212
229
  # Sets 'Content-Type' header value. Overrides if already exists.
213
- def contenttype=(contenttype)
230
+ def content_type=(content_type)
214
231
  delete('Content-Type')
215
- self['Content-Type'] = contenttype
232
+ self['Content-Type'] = content_type
233
+ end
234
+
235
+ alias contenttype content_type
236
+ alias contenttype= content_type=
237
+
238
+ if defined?(Encoding::ASCII_8BIT)
239
+ def set_body_encoding
240
+ if type = self.content_type
241
+ OpenURI::Meta.init(o = '')
242
+ o.meta_add_field('content-type', type)
243
+ @body_encoding = o.encoding
244
+ end
245
+ end
246
+ else
247
+ def set_body_encoding
248
+ @body_encoding = nil
249
+ end
216
250
  end
217
251
 
218
252
  # Sets byte size of message body.
@@ -235,6 +269,11 @@ module HTTP
235
269
  }.join
236
270
  end
237
271
 
272
+ # Set Date header
273
+ def set_date_header
274
+ set('Date', Time.now.httpdate)
275
+ end
276
+
238
277
  # Adds a header. Addition order is preserved.
239
278
  def add(key, value)
240
279
  if value.is_a?(Array)
@@ -285,11 +324,45 @@ module HTTP
285
324
  get(key).collect { |item| item[1] }
286
325
  end
287
326
 
327
+ def set_headers(headers)
328
+ headers.each do |key, value|
329
+ add(key, value)
330
+ end
331
+ set_body_encoding
332
+ end
333
+
334
+ def create_query_uri()
335
+ if @request_method == 'CONNECT'
336
+ return "#{@request_uri.host}:#{@request_uri.port}"
337
+ end
338
+ path = @request_uri.path
339
+ path = '/' if path.nil? or path.empty?
340
+ if query_str = create_query_part()
341
+ path += "?#{query_str}"
342
+ end
343
+ path
344
+ end
345
+
346
+ def create_query_part()
347
+ query_str = nil
348
+ if @request_uri.query
349
+ query_str = @request_uri.query
350
+ end
351
+ if @request_query
352
+ if query_str
353
+ query_str += "&#{Message.create_query_part_str(@request_query)}"
354
+ else
355
+ query_str = Message.create_query_part_str(@request_query)
356
+ end
357
+ end
358
+ query_str
359
+ end
360
+
288
361
  private
289
362
 
290
363
  def request_line
291
- path = create_query_uri(@request_uri, @request_query)
292
- if @request_via_proxy
364
+ path = create_query_uri()
365
+ if @request_absolute_uri
293
366
  path = "#{ @request_uri.scheme }://#{ @request_uri.host }:#{ @request_uri.port }#{ path }"
294
367
  end
295
368
  "#{ @request_method } #{ path } HTTP/#{ @http_version }#{ CRLF }"
@@ -323,12 +396,12 @@ module HTTP
323
396
  elsif @body_size and (keep_alive or @body_size != 0)
324
397
  set('Content-Length', @body_size.to_s)
325
398
  end
326
- if @http_version >= 1.1
399
+ if @http_version >= '1.1' and get('Host').empty?
327
400
  if @request_uri.port == @request_uri.default_port
328
401
  # GFE/1.3 dislikes default port number (returns 404)
329
- set('Host', "#{@request_uri.host}")
402
+ set('Host', "#{@request_uri.hostname}")
330
403
  else
331
- set('Host', "#{@request_uri.host}:#{@request_uri.port}")
404
+ set('Host', "#{@request_uri.hostname}:#{@request_uri.port}")
332
405
  end
333
406
  end
334
407
  end
@@ -337,7 +410,7 @@ module HTTP
337
410
  return if @dumped
338
411
  @dumped = true
339
412
  if defined?(Apache) && self['Date'].empty?
340
- set('Date', Time.now.httpdate)
413
+ set_date_header
341
414
  end
342
415
  keep_alive = Message.keep_alive_enabled?(@http_version)
343
416
  if @chunked
@@ -351,35 +424,17 @@ module HTTP
351
424
  set('Last-Modified', @body_date.httpdate)
352
425
  end
353
426
  if self['Content-Type'].empty?
354
- set('Content-Type', "#{ @body_type || 'text/html' }; charset=#{ charset_label(@body_charset || $KCODE) }")
427
+ set('Content-Type', "#{ @body_type || 'text/html' }; charset=#{ charset_label }")
355
428
  end
356
429
  end
357
430
 
358
- def charset_label(charset)
359
- CHARSET_MAP[charset] || 'us-ascii'
360
- end
361
-
362
- def create_query_uri(uri, query)
363
- if @request_method == 'CONNECT'
364
- return "#{uri.host}:#{uri.port}"
365
- end
366
- path = uri.path
367
- path = '/' if path.nil? or path.empty?
368
- query_str = nil
369
- if uri.query
370
- query_str = uri.query
371
- end
372
- if query
373
- if query_str
374
- query_str += "&#{Message.create_query_part_str(query)}"
375
- else
376
- query_str = Message.create_query_part_str(query)
377
- end
378
- end
379
- if query_str
380
- path += "?#{query_str}"
431
+ def charset_label
432
+ # TODO: should handle response encoding for 1.9 correctly.
433
+ if RUBY_VERSION > "1.9"
434
+ CHARSET_MAP[@body_charset] || 'us-ascii'
435
+ else
436
+ CHARSET_MAP[@body_charset || $KCODE] || 'us-ascii'
381
437
  end
382
- path
383
438
  end
384
439
  end
385
440
 
@@ -390,6 +445,8 @@ module HTTP
390
445
  attr_reader :size
391
446
  # maxbytes of IO#read for streaming request. See DEFAULT_CHUNK_SIZE.
392
447
  attr_accessor :chunk_size
448
+ # Hash that keeps IO positions
449
+ attr_accessor :positions
393
450
 
394
451
  # Default value for chunk_size
395
452
  DEFAULT_CHUNK_SIZE = 1024 * 16
@@ -409,16 +466,20 @@ module HTTP
409
466
  @positions = {}
410
467
  set_content(body, boundary)
411
468
  @chunk_size = DEFAULT_CHUNK_SIZE
469
+ self
412
470
  end
413
471
 
414
472
  # Initialize this instance as a response.
415
473
  def init_response(body = nil)
416
474
  @body = body
417
- if @body.respond_to?(:size)
475
+ if @body.respond_to?(:bytesize)
476
+ @size = @body.bytesize
477
+ elsif @body.respond_to?(:size)
418
478
  @size = @body.size
419
479
  else
420
480
  @size = nil
421
481
  end
482
+ self
422
483
  end
423
484
 
424
485
  # Dumps message body to given dev.
@@ -428,20 +489,23 @@ module HTTP
428
489
  # reason. (header is dumped to dev, too)
429
490
  # If no dev (the second argument) given, this method returns a dumped
430
491
  # String.
492
+ #
493
+ # assert: @size is not nil
431
494
  def dump(header = '', dev = '')
432
495
  if @body.is_a?(Parts)
433
496
  dev << header
434
- buf = ''
435
497
  @body.parts.each do |part|
436
498
  if Message.file?(part)
437
499
  reset_pos(part)
438
- while !part.read(@chunk_size, buf).nil?
439
- dev << buf
440
- end
500
+ dump_file(part, dev, @body.sizes[part])
441
501
  else
442
502
  dev << part
443
503
  end
444
504
  end
505
+ elsif Message.file?(@body)
506
+ dev << header
507
+ reset_pos(@body)
508
+ dump_file(@body, dev, @size)
445
509
  elsif @body
446
510
  dev << header + @body
447
511
  else
@@ -485,30 +549,41 @@ module HTTP
485
549
  private
486
550
 
487
551
  def set_content(body, boundary = nil)
488
- if body.respond_to?(:read)
489
- # uses Transfer-Encoding: chunked. bear in mind that server may not
490
- # support it. at least ruby's CGI doesn't.
552
+ if Message.file?(body)
553
+ # uses Transfer-Encoding: chunked if body does not respond to :size.
554
+ # bear in mind that server may not support it. at least ruby's CGI doesn't.
491
555
  @body = body
492
556
  remember_pos(@body)
493
- @size = nil
557
+ @size = body.respond_to?(:size) ? body.size - body.pos : nil
494
558
  elsif boundary and Message.multiparam_query?(body)
495
559
  @body = build_query_multipart_str(body, boundary)
496
560
  @size = @body.size
497
561
  else
498
562
  @body = Message.create_query_part_str(body)
499
- @size = @body.size
563
+ @size = @body.bytesize
500
564
  end
501
565
  end
502
566
 
503
567
  def remember_pos(io)
504
568
  # IO may not support it (ex. IO.pipe)
505
- @positions[io] = io.pos rescue nil
569
+ @positions[io] = io.pos if io.respond_to?(:pos)
506
570
  end
507
571
 
508
572
  def reset_pos(io)
509
573
  io.pos = @positions[io] if @positions.key?(io)
510
574
  end
511
575
 
576
+ def dump_file(io, dev, sz)
577
+ buf = ''
578
+ rest = sz
579
+ while rest > 0
580
+ n = io.read([rest, @chunk_size].min, buf)
581
+ raise ArgumentError.new("Illegal size value: #size returns #{sz} but cannot read") if n.nil?
582
+ dev << buf
583
+ rest -= n.bytesize
584
+ end
585
+ end
586
+
512
587
  def dump_chunks(io, dev)
513
588
  buf = ''
514
589
  while !io.read(@chunk_size, buf).nil?
@@ -517,7 +592,7 @@ module HTTP
517
592
  end
518
593
 
519
594
  def dump_chunk(str)
520
- dump_chunk_size(str.size) + (str + CRLF)
595
+ dump_chunk_size(str.bytesize) + (str + CRLF)
521
596
  end
522
597
 
523
598
  def dump_last_chunk
@@ -530,10 +605,12 @@ module HTTP
530
605
 
531
606
  class Parts
532
607
  attr_reader :size
608
+ attr_reader :sizes
533
609
 
534
610
  def initialize
535
611
  @body = []
536
- @size = 0
612
+ @sizes = {}
613
+ @size = 0 # total
537
614
  @as_stream = false
538
615
  end
539
616
 
@@ -541,24 +618,27 @@ module HTTP
541
618
  if Message.file?(part)
542
619
  @as_stream = true
543
620
  @body << part
544
- if part.respond_to?(:size)
621
+ if part.respond_to?(:lstat)
622
+ sz = part.lstat.size
623
+ add_size(part, sz)
624
+ elsif part.respond_to?(:size)
545
625
  if sz = part.size
546
- @size += sz
626
+ add_size(part, sz)
547
627
  else
628
+ @sizes.clear
548
629
  @size = nil
549
630
  end
550
- elsif part.respond_to?(:lstat)
551
- @size += part.lstat.size
552
631
  else
553
632
  # use chunked upload
633
+ @sizes.clear
554
634
  @size = nil
555
635
  end
556
636
  elsif @body[-1].is_a?(String)
557
637
  @body[-1] += part.to_s
558
- @size += part.to_s.size if @size
638
+ @size += part.to_s.bytesize if @size
559
639
  else
560
640
  @body << part.to_s
561
- @size += part.to_s.size if @size
641
+ @size += part.to_s.bytesize if @size
562
642
  end
563
643
  end
564
644
 
@@ -569,12 +649,20 @@ module HTTP
569
649
  [@body.join]
570
650
  end
571
651
  end
652
+
653
+ private
654
+
655
+ def add_size(part, sz)
656
+ if @size
657
+ @sizes[part] = sz
658
+ @size += sz
659
+ end
660
+ end
572
661
  end
573
662
 
574
663
  def build_query_multipart_str(query, boundary)
575
664
  parts = Parts.new
576
665
  query.each do |attr, value|
577
- value ||= ''
578
666
  headers = ["--#{boundary}"]
579
667
  if Message.file?(value)
580
668
  remember_pos(value)
@@ -583,13 +671,24 @@ module HTTP
583
671
  }.join("; ")
584
672
  if value.respond_to?(:mime_type)
585
673
  content_type = value.mime_type
674
+ elsif value.respond_to?(:content_type)
675
+ content_type = value.content_type
586
676
  else
587
- content_type = Message.mime_type(value.path)
677
+ path = value.respond_to?(:path) ? value.path : nil
678
+ content_type = Message.mime_type(path)
588
679
  end
589
680
  headers << %{Content-Disposition: form-data; name="#{attr}"; #{param_str}}
590
681
  headers << %{Content-Type: #{content_type}}
682
+ elsif attr.is_a?(Hash)
683
+ h = attr
684
+ value = h[:content]
685
+ h.each do |h_key, h_val|
686
+ headers << %{#{h_key}: #{h_val}} if h_key != :content
687
+ end
688
+ remember_pos(value) if Message.file?(value)
591
689
  else
592
690
  headers << %{Content-Disposition: form-data; name="#{attr}"}
691
+ value = value.to_s
593
692
  end
594
693
  parts.add(headers.join(CRLF) + CRLF + CRLF)
595
694
  parts.add(value)
@@ -601,7 +700,9 @@ module HTTP
601
700
 
602
701
  def params_from_file(value)
603
702
  params = {}
604
- params['filename'] = File.basename(value.path || '')
703
+ original_filename = value.respond_to?(:original_filename) ? value.original_filename : nil
704
+ path = value.respond_to?(:path) ? value.path : nil
705
+ params['filename'] = original_filename || File.basename(path || '')
605
706
  # Creation time is not available from File::Stat
606
707
  if value.respond_to?(:mtime)
607
708
  params['modification-date'] = value.mtime.rfc822
@@ -622,8 +723,8 @@ module HTTP
622
723
  # uri:: an URI that need to connect. Only uri.host and uri.port are used.
623
724
  def new_connect_request(uri)
624
725
  m = new
625
- m.header.init_connect_request(uri)
626
- m.header.body_size = nil
726
+ m.http_header.init_connect_request(uri)
727
+ m.http_header.body_size = nil
627
728
  m
628
729
  end
629
730
 
@@ -642,26 +743,26 @@ module HTTP
642
743
  # a multipart/form-data using this boundary String.
643
744
  def new_request(method, uri, query = nil, body = nil, boundary = nil)
644
745
  m = new
645
- m.header.init_request(method, uri, query)
646
- m.body = Body.new
647
- m.body.init_request(body || '', boundary)
746
+ m.http_header.init_request(method, uri, query)
747
+ m.http_body = Body.new
748
+ m.http_body.init_request(body || '', boundary)
648
749
  if body
649
- m.header.body_size = m.body.size
650
- m.header.chunked = true if m.body.size.nil?
750
+ m.http_header.body_size = m.http_body.size
751
+ m.http_header.chunked = true if m.http_body.size.nil?
651
752
  else
652
- m.header.body_size = nil
753
+ m.http_header.body_size = nil
653
754
  end
654
755
  m
655
756
  end
656
757
 
657
758
  # Creates a Message instance of response.
658
759
  # body:: a String or an IO of response message body.
659
- def new_response(body)
760
+ def new_response(body, req = nil)
660
761
  m = new
661
- m.header.init_response(Status::OK)
662
- m.body = Body.new
663
- m.body.init_response(body)
664
- m.header.body_size = m.body.size || 0
762
+ m.http_header.init_response(Status::OK, req)
763
+ m.http_body = Body.new
764
+ m.http_body.init_response(body)
765
+ m.http_header.body_size = m.http_body.size || 0
665
766
  m
666
767
  end
667
768
 
@@ -708,6 +809,8 @@ module HTTP
708
809
  case path
709
810
  when /\.txt$/i
710
811
  'text/plain'
812
+ when /\.xml$/i
813
+ 'text/xml'
711
814
  when /\.(htm|html)$/i
712
815
  'text/html'
713
816
  when /\.doc$/i
@@ -724,9 +827,9 @@ module HTTP
724
827
  end
725
828
 
726
829
  # Returns true if the given HTTP version allows keep alive connection.
727
- # version:: Float
830
+ # version:: String
728
831
  def keep_alive_enabled?(version)
729
- version >= 1.1
832
+ version >= '1.1'
730
833
  end
731
834
 
732
835
  # Returns true if the given query (or body) has a multiple parameter.
@@ -736,15 +839,14 @@ module HTTP
736
839
 
737
840
  # Returns true if the given object is a File. In HTTPClient, a file is;
738
841
  # * must respond to :read for retrieving String chunks.
739
- # * must respond to :path and returns a path for Content-Disposition.
740
842
  # * must respond to :pos and :pos= to rewind for reading.
741
843
  # Rewinding is only needed for following HTTP redirect. Some IO impl
742
844
  # defines :pos= but raises an Exception for pos= such as StringIO
743
845
  # but there's no problem as far as using it for non-following methods
744
846
  # (get/post/etc.)
745
847
  def file?(obj)
746
- obj.respond_to?(:read) and obj.respond_to?(:path) and
747
- obj.respond_to?(:pos) and obj.respond_to?(:pos=)
848
+ obj.respond_to?(:read) and obj.respond_to?(:pos) and
849
+ obj.respond_to?(:pos=)
748
850
  end
749
851
 
750
852
  def create_query_part_str(query) # :nodoc:
@@ -757,50 +859,107 @@ module HTTP
757
859
  end
758
860
  end
759
861
 
862
+ def Array.try_convert(value)
863
+ return value if value.instance_of?(Array)
864
+ return nil if !value.respond_to?(:to_ary)
865
+ converted = value.to_ary
866
+ return converted if converted.instance_of?(Array)
867
+
868
+ cname = value.class.name
869
+ raise TypeError, "can't convert %s to %s (%s#%s gives %s)" %
870
+ [cname, Array.name, cname, :to_ary, converted.class.name]
871
+ end unless Array.respond_to?(:try_convert)
872
+
760
873
  def escape_query(query) # :nodoc:
761
- query.collect { |attr, value|
762
- if value.respond_to?(:read)
763
- value = value.read
874
+ pairs = []
875
+ query.each { |attr, value|
876
+ left = escape(attr.to_s) << '='
877
+ if values = Array.try_convert(value)
878
+ values.each { |v|
879
+ if v.respond_to?(:read)
880
+ v = v.read
881
+ end
882
+ pairs.push(left + escape(v.to_s))
883
+ }
884
+ else
885
+ if value.respond_to?(:read)
886
+ value = value.read
887
+ end
888
+ pairs.push(left << escape(value.to_s))
764
889
  end
765
- escape(attr.to_s) << '=' << escape(value.to_s)
766
- }.join('&')
890
+ }
891
+ pairs.join('&')
767
892
  end
768
893
 
769
894
  # from CGI.escape
770
- def escape(str) # :nodoc:
771
- str.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
772
- '%' + $1.unpack('H2' * $1.size).join('%').upcase
773
- }.tr(' ', '+')
895
+ if defined?(Encoding::ASCII_8BIT)
896
+ def escape(str) # :nodoc:
897
+ str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(/([^ a-zA-Z0-9_.-]+)/) {
898
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
899
+ }.tr(' ', '+')
900
+ end
901
+ else
902
+ def escape(str) # :nodoc:
903
+ str.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
904
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
905
+ }.tr(' ', '+')
906
+ end
907
+ end
908
+
909
+ # from CGI.parse
910
+ def parse(query)
911
+ params = Hash.new([].freeze)
912
+ query.split(/[&;]/n).each do |pairs|
913
+ key, value = pairs.split('=',2).collect{|v| unescape(v) }
914
+ if params.has_key?(key)
915
+ params[key].push(value)
916
+ else
917
+ params[key] = [value]
918
+ end
919
+ end
920
+ params
921
+ end
922
+
923
+ # from CGI.unescape
924
+ def unescape(string)
925
+ string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
926
+ [$1.delete('%')].pack('H*')
927
+ end
774
928
  end
775
929
  end
776
930
 
777
931
 
778
932
  # HTTP::Message::Headers:: message header.
779
- attr_accessor :header
933
+ attr_accessor :http_header
780
934
 
781
935
  # HTTP::Message::Body:: message body.
782
- attr_reader :body
936
+ attr_reader :http_body
783
937
 
784
938
  # OpenSSL::X509::Certificate:: response only. server certificate which is
785
939
  # used for retrieving the response.
786
940
  attr_accessor :peer_cert
787
941
 
942
+ # The other Message object when this Message is generated instead of
943
+ # the Message because of redirection, negotiation, or format conversion.
944
+ attr_accessor :previous
945
+
788
946
  # Creates a Message. This method should be used internally.
789
947
  # Use Message.new_connect_request, Message.new_request or
790
948
  # Message.new_response instead.
791
949
  def initialize # :nodoc:
792
- @header = Headers.new
793
- @body = @peer_cert = nil
950
+ @http_header = Headers.new
951
+ @http_body = @peer_cert = nil
952
+ @previous = nil
794
953
  end
795
954
 
796
955
  # Dumps message (header and body) to given dev.
797
956
  # dev needs to respond to <<.
798
957
  def dump(dev = '')
799
- str = header.dump + CRLF
800
- if header.chunked
801
- dev = body.dump_chunked(str, dev)
802
- elsif body
803
- dev = body.dump(str, dev)
958
+ str = @http_header.dump + CRLF
959
+ if @http_header.chunked
960
+ dev = @http_body.dump_chunked(str, dev)
961
+ elsif @http_body
962
+ dev = @http_body.dump(str, dev)
804
963
  else
805
964
  dev << str
806
965
  end
@@ -808,24 +967,36 @@ module HTTP
808
967
  end
809
968
 
810
969
  # Sets a new body. header.body_size is updated with new body.size.
811
- def body=(body)
812
- @body = body
813
- @header.body_size = @body.size if @header
970
+ def http_body=(body)
971
+ @http_body = body
972
+ @http_header.body_size = @http_body.size if @http_header
973
+ end
974
+ alias body= http_body=
975
+
976
+ # Returns HTTP version in a HTTP header. String.
977
+ def http_version
978
+ @http_header.http_version
979
+ end
980
+
981
+ # Sets HTTP version in a HTTP header. String.
982
+ def http_version=(http_version)
983
+ @http_header.http_version = http_version
814
984
  end
815
985
 
816
- # Returns HTTP version in a HTTP header. Float.
986
+ VERSION_WARNING = 'Message#version (Float) is deprecated. Use Message#http_version (String) instead.'
817
987
  def version
818
- @header.http_version
988
+ warning(VERSION_WARNING)
989
+ @http_header.http_version.to_f
819
990
  end
820
991
 
821
- # Sets HTTP version in a HTTP header. Float.
822
992
  def version=(version)
823
- @header.http_version = version
993
+ warning(VERSION_WARNING)
994
+ @http_header.http_version = version
824
995
  end
825
996
 
826
997
  # Returns HTTP status code in response. Integer.
827
998
  def status
828
- @header.status_code
999
+ @http_header.status_code
829
1000
  end
830
1001
 
831
1002
  alias code status
@@ -834,34 +1005,78 @@ module HTTP
834
1005
  # Sets HTTP status code of response. Integer.
835
1006
  # Reason phrase is updated, too.
836
1007
  def status=(status)
837
- @header.status_code = status
1008
+ @http_header.status_code = status
838
1009
  end
839
1010
 
840
1011
  # Returns HTTP status reason phrase in response. String.
841
1012
  def reason
842
- @header.reason_phrase
1013
+ @http_header.reason_phrase
843
1014
  end
844
1015
 
845
1016
  # Sets HTTP status reason phrase of response. String.
846
1017
  def reason=(reason)
847
- @header.reason_phrase = reason
1018
+ @http_header.reason_phrase = reason
1019
+ end
1020
+
1021
+ # Returns 'Content-Type' header value.
1022
+ def content_type
1023
+ @http_header.content_type
848
1024
  end
849
1025
 
850
1026
  # Sets 'Content-Type' header value. Overrides if already exists.
851
- def contenttype
852
- @header.contenttype
1027
+ def content_type=(content_type)
1028
+ @http_header.content_type = content_type
853
1029
  end
1030
+ alias contenttype content_type
1031
+ alias contenttype= content_type=
854
1032
 
855
- # Returns 'Content-Type' header value.
856
- def contenttype=(contenttype)
857
- @header.contenttype = contenttype
1033
+ # Returns content encoding
1034
+ def body_encoding
1035
+ @http_header.body_encoding
858
1036
  end
859
1037
 
860
1038
  # Returns a content of message body. A String or an IO.
861
1039
  def content
862
- @body.content
1040
+ @http_body.content
863
1041
  end
864
- end
865
1042
 
1043
+ alias header http_header
1044
+ alias body content
1045
+
1046
+ # Returns Hash of header. key and value are both String. Each key has a
1047
+ # single value so you can't extract exact value when a message has multiple
1048
+ # headers like 'Set-Cookie'. Use header['Set-Cookie'] for that purpose.
1049
+ # (It returns an Array always)
1050
+ def headers
1051
+ Hash[*http_header.all.flatten]
1052
+ end
1053
+
1054
+ # Extracts cookies from 'Set-Cookie' header.
1055
+ # Supports 'Set-Cookie' in response header only.
1056
+ # Do we need 'Cookie' support in request header?
1057
+ def cookies
1058
+ set_cookies = http_header['set-cookie']
1059
+ unless set_cookies.empty?
1060
+ uri = http_header.request_uri
1061
+ set_cookies.map { |str|
1062
+ WebAgent::Cookie.parse(str, uri)
1063
+ }.flatten
1064
+ end
1065
+ end
1066
+
1067
+ # Convenience method to return boolean of whether we had a successful request
1068
+ def ok?
1069
+ HTTP::Status.successful?(status)
1070
+ end
1071
+
1072
+ def redirect?
1073
+ HTTP::Status.redirect?(status)
1074
+ end
1075
+
1076
+ # SEE_OTHER is a redirect, but it should sent as GET
1077
+ def see_other?
1078
+ status == HTTP::Status::SEE_OTHER
1079
+ end
1080
+ end
866
1081
 
867
1082
  end