httpclient 2.1.5 → 2.8.3

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