maedana-httpclient 2.1.5.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,903 @@
1
+ # HTTPClient - HTTP client library.
2
+ # Copyright (C) 2000-2009 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
3
+ #
4
+ # This program is copyrighted free software by NAKAMURA, Hiroshi. You can
5
+ # redistribute it and/or modify it under the same terms of Ruby's license;
6
+ # either the dual license version in 2003, or any later version.
7
+
8
+
9
+ require 'time'
10
+
11
+
12
+ # A namespace module for HTTP Message definitions used by HTTPClient.
13
+ module HTTP
14
+
15
+
16
+ # Represents HTTP response status code. Defines constants for HTTP response
17
+ # and some conditional methods.
18
+ module Status
19
+ OK = 200
20
+ CREATED = 201
21
+ ACCEPTED = 202
22
+ NON_AUTHORITATIVE_INFORMATION = 203
23
+ NO_CONTENT = 204
24
+ RESET_CONTENT = 205
25
+ PARTIAL_CONTENT = 206
26
+ MOVED_PERMANENTLY = 301
27
+ FOUND = 302
28
+ SEE_OTHER = 303
29
+ TEMPORARY_REDIRECT = MOVED_TEMPORARILY = 307
30
+ BAD_REQUEST = 400
31
+ UNAUTHORIZED = 401
32
+ PROXY_AUTHENTICATE_REQUIRED = 407
33
+ INTERNAL = 500
34
+
35
+ # Status codes for successful HTTP response.
36
+ SUCCESSFUL_STATUS = [
37
+ OK, CREATED, ACCEPTED,
38
+ NON_AUTHORITATIVE_INFORMATION, NO_CONTENT,
39
+ RESET_CONTENT, PARTIAL_CONTENT
40
+ ]
41
+
42
+ # Status codes which is a redirect.
43
+ REDIRECT_STATUS = [
44
+ MOVED_PERMANENTLY, FOUND, SEE_OTHER,
45
+ TEMPORARY_REDIRECT, MOVED_TEMPORARILY
46
+ ]
47
+
48
+ # Returns true if the given status represents successful HTTP response.
49
+ # See also SUCCESSFUL_STATUS.
50
+ def self.successful?(status)
51
+ SUCCESSFUL_STATUS.include?(status)
52
+ end
53
+
54
+ # Returns true if the given status is thought to be redirect.
55
+ # See also REDIRECT_STATUS.
56
+ def self.redirect?(status)
57
+ REDIRECT_STATUS.include?(status)
58
+ end
59
+ end
60
+
61
+
62
+ # Represents a HTTP message. A message is for a request or a response.
63
+ #
64
+ # Request message is generated from given parameters internally so users
65
+ # don't need to care about it. Response message is the instance that
66
+ # methods of HTTPClient returns so users need to know how to extract
67
+ # HTTP response data from Message.
68
+ #
69
+ # Some attributes are only for a request or a response, not both.
70
+ #
71
+ # == How to use HTTP response message
72
+ #
73
+ # 1. Gets response message body.
74
+ #
75
+ # res = clnt.get(url)
76
+ # p res.content #=> String
77
+ #
78
+ # 2. Gets response status code.
79
+ #
80
+ # res = clnt.get(url)
81
+ # p res.status #=> 200, 501, etc. (Integer)
82
+ #
83
+ # 3. Gets response header.
84
+ #
85
+ # res = clnt.get(url)
86
+ # res.header['set-cookie'].each do |value|
87
+ # p value
88
+ # end
89
+ # assert_equal(1, res.header['last-modified'].size)
90
+ # p res.header['last-modified'].first
91
+ #
92
+ class Message
93
+
94
+ CRLF = "\r\n"
95
+
96
+ # Represents HTTP message header.
97
+ class Headers
98
+ # HTTP version in a HTTP header. Float.
99
+ attr_accessor :http_version
100
+ # Size of body. nil when size is unknown (e.g. chunked response).
101
+ attr_reader :body_size
102
+ # Request/Response is chunked or not.
103
+ attr_accessor :chunked
104
+
105
+ # Request only. Requested method.
106
+ attr_reader :request_method
107
+ # Request only. Requested URI.
108
+ attr_accessor :request_uri
109
+ # Request only. Requested query.
110
+ attr_accessor :request_query
111
+ # Request only. Requested via proxy or not.
112
+ attr_accessor :request_via_proxy
113
+
114
+ # Response only. HTTP status
115
+ attr_reader :status_code
116
+ # Response only. HTTP status reason phrase.
117
+ attr_accessor :reason_phrase
118
+
119
+ # Used for dumping response.
120
+ attr_accessor :body_type # :nodoc:
121
+ # Used for dumping response.
122
+ attr_accessor :body_charset # :nodoc:
123
+ # Used for dumping response.
124
+ attr_accessor :body_date # :nodoc:
125
+
126
+ # HTTP response status code to reason phrase mapping definition.
127
+ STATUS_CODE_MAP = {
128
+ Status::OK => 'OK',
129
+ Status::CREATED => "Created",
130
+ Status::NON_AUTHORITATIVE_INFORMATION => "Non-Authoritative Information",
131
+ Status::NO_CONTENT => "No Content",
132
+ Status::RESET_CONTENT => "Reset Content",
133
+ Status::PARTIAL_CONTENT => "Partial Content",
134
+ Status::MOVED_PERMANENTLY => 'Moved Permanently',
135
+ Status::FOUND => 'Found',
136
+ Status::SEE_OTHER => 'See Other',
137
+ Status::TEMPORARY_REDIRECT => 'Temporary Redirect',
138
+ Status::MOVED_TEMPORARILY => 'Temporary Redirect',
139
+ Status::BAD_REQUEST => 'Bad Request',
140
+ Status::INTERNAL => 'Internal Server Error',
141
+ }
142
+
143
+ # $KCODE to charset mapping definition.
144
+ CHARSET_MAP = {
145
+ 'NONE' => 'us-ascii',
146
+ 'EUC' => 'euc-jp',
147
+ 'SJIS' => 'shift_jis',
148
+ 'UTF8' => 'utf-8',
149
+ }
150
+
151
+ # Creates a Message::Headers. Use init_request, init_response, or
152
+ # init_connect_request for acutual initialize.
153
+ def initialize
154
+ @http_version = 1.1
155
+ @body_size = nil
156
+ @chunked = false
157
+
158
+ @request_method = nil
159
+ @request_uri = nil
160
+ @request_query = nil
161
+ @request_via_proxy = nil
162
+
163
+ @status_code = nil
164
+ @reason_phrase = nil
165
+
166
+ @body_type = nil
167
+ @body_charset = nil
168
+ @body_date = nil
169
+
170
+ @is_request = nil
171
+ @header_item = []
172
+ @dumped = false
173
+ end
174
+
175
+ # Initialize this instance as a CONNECT request.
176
+ def init_connect_request(uri)
177
+ @is_request = true
178
+ @request_method = 'CONNECT'
179
+ @request_uri = uri
180
+ @request_query = nil
181
+ @http_version = 1.0
182
+ end
183
+
184
+ # Placeholder URI object for nil uri.
185
+ NIL_URI = URI.parse('http://nil-uri-given/')
186
+ # Initialize this instance as a general request.
187
+ def init_request(method, uri, query = nil)
188
+ @is_request = true
189
+ @request_method = method
190
+ @request_uri = uri || NIL_URI
191
+ @request_query = query
192
+ @request_via_proxy = false
193
+ end
194
+
195
+ # Initialize this instance as a response.
196
+ def init_response(status_code)
197
+ @is_request = false
198
+ self.status_code = status_code
199
+ end
200
+
201
+ # Sets status code and reason phrase.
202
+ def status_code=(status_code)
203
+ @status_code = status_code
204
+ @reason_phrase = STATUS_CODE_MAP[@status_code]
205
+ end
206
+
207
+ # Returns 'Content-Type' header value.
208
+ def contenttype
209
+ self['Content-Type'][0]
210
+ end
211
+
212
+ # Sets 'Content-Type' header value. Overrides if already exists.
213
+ def contenttype=(contenttype)
214
+ delete('Content-Type')
215
+ self['Content-Type'] = contenttype
216
+ end
217
+
218
+ # Sets byte size of message body.
219
+ # body_size == nil means that the body is_a? IO
220
+ def body_size=(body_size)
221
+ @body_size = body_size
222
+ end
223
+
224
+ # Dumps message header part and returns a dumped String.
225
+ def dump
226
+ set_header
227
+ str = nil
228
+ if @is_request
229
+ str = request_line
230
+ else
231
+ str = response_status_line
232
+ end
233
+ str + @header_item.collect { |key, value|
234
+ "#{ key }: #{ value }#{ CRLF }"
235
+ }.join
236
+ end
237
+
238
+ # Adds a header. Addition order is preserved.
239
+ def add(key, value)
240
+ if value.is_a?(Array)
241
+ value.each do |v|
242
+ @header_item.push([key, v])
243
+ end
244
+ else
245
+ @header_item.push([key, value])
246
+ end
247
+ end
248
+
249
+ # Sets a header.
250
+ def set(key, value)
251
+ delete(key)
252
+ add(key, value)
253
+ end
254
+
255
+ # Returns an Array of headers for the given key. Each element is a pair
256
+ # of key and value. It returns an single element Array even if the only
257
+ # one header exists. If nil key given, it returns all headers.
258
+ def get(key = nil)
259
+ if key.nil?
260
+ all
261
+ else
262
+ key = key.upcase
263
+ @header_item.find_all { |k, v| k.upcase == key }
264
+ end
265
+ end
266
+
267
+ # Returns an Array of all headers.
268
+ def all
269
+ @header_item
270
+ end
271
+
272
+ # Deletes headers of the given key.
273
+ def delete(key)
274
+ key = key.upcase
275
+ @header_item.delete_if { |k, v| k.upcase == key }
276
+ end
277
+
278
+ # Adds a header. See set.
279
+ def []=(key, value)
280
+ set(key, value)
281
+ end
282
+
283
+ # Returns an Array of header values for the given key.
284
+ def [](key)
285
+ get(key).collect { |item| item[1] }
286
+ end
287
+
288
+ def create_query_uri()
289
+ if @request_method == 'CONNECT'
290
+ return "#{@request_uri.host}:#{@request_uri.port}"
291
+ end
292
+ path = @request_uri.path
293
+ path = '/' if path.nil? or path.empty?
294
+ if query_str = create_query_part()
295
+ path += "?#{query_str}"
296
+ end
297
+ path
298
+ end
299
+
300
+ def create_query_part()
301
+ query_str = nil
302
+ if @request_uri.query
303
+ query_str = @request_uri.query
304
+ end
305
+ if @request_query
306
+ if query_str
307
+ query_str += "&#{Message.create_query_part_str(@request_query)}"
308
+ else
309
+ query_str = Message.create_query_part_str(@request_query)
310
+ end
311
+ end
312
+ query_str
313
+ end
314
+
315
+ private
316
+
317
+ def request_line
318
+ path = create_query_uri()
319
+ if @request_via_proxy
320
+ path = "#{ @request_uri.scheme }://#{ @request_uri.host }:#{ @request_uri.port }#{ path }"
321
+ end
322
+ "#{ @request_method } #{ path } HTTP/#{ @http_version }#{ CRLF }"
323
+ end
324
+
325
+ def response_status_line
326
+ if defined?(Apache)
327
+ "HTTP/#{ @http_version } #{ @status_code } #{ @reason_phrase }#{ CRLF }"
328
+ else
329
+ "Status: #{ @status_code } #{ @reason_phrase }#{ CRLF }"
330
+ end
331
+ end
332
+
333
+ def set_header
334
+ if @is_request
335
+ set_request_header
336
+ else
337
+ set_response_header
338
+ end
339
+ end
340
+
341
+ def set_request_header
342
+ return if @dumped
343
+ @dumped = true
344
+ keep_alive = Message.keep_alive_enabled?(@http_version)
345
+ if !keep_alive and @request_method != 'CONNECT'
346
+ set('Connection', 'close')
347
+ end
348
+ if @chunked
349
+ set('Transfer-Encoding', 'chunked')
350
+ elsif @body_size and (keep_alive or @body_size != 0)
351
+ set('Content-Length', @body_size.to_s)
352
+ end
353
+ if @http_version >= 1.1 and get('Host').empty?
354
+ if @request_uri.port == @request_uri.default_port
355
+ # GFE/1.3 dislikes default port number (returns 404)
356
+ set('Host', "#{@request_uri.host}")
357
+ else
358
+ set('Host', "#{@request_uri.host}:#{@request_uri.port}")
359
+ end
360
+ end
361
+ end
362
+
363
+ def set_response_header
364
+ return if @dumped
365
+ @dumped = true
366
+ if defined?(Apache) && self['Date'].empty?
367
+ set('Date', Time.now.httpdate)
368
+ end
369
+ keep_alive = Message.keep_alive_enabled?(@http_version)
370
+ if @chunked
371
+ set('Transfer-Encoding', 'chunked')
372
+ else
373
+ if keep_alive or @body_size != 0
374
+ set('Content-Length', @body_size.to_s)
375
+ end
376
+ end
377
+ if @body_date
378
+ set('Last-Modified', @body_date.httpdate)
379
+ end
380
+ if self['Content-Type'].empty?
381
+ set('Content-Type', "#{ @body_type || 'text/html' }; charset=#{ charset_label(@body_charset || $KCODE) }")
382
+ end
383
+ end
384
+
385
+ def charset_label(charset)
386
+ CHARSET_MAP[charset] || 'us-ascii'
387
+ end
388
+ end
389
+
390
+
391
+ # Represents HTTP message body.
392
+ class Body
393
+ # Size of body. nil when size is unknown (e.g. chunked response).
394
+ attr_reader :size
395
+ # maxbytes of IO#read for streaming request. See DEFAULT_CHUNK_SIZE.
396
+ attr_accessor :chunk_size
397
+
398
+ # Default value for chunk_size
399
+ DEFAULT_CHUNK_SIZE = 1024 * 16
400
+
401
+ # Creates a Message::Body. Use init_request or init_response
402
+ # for acutual initialize.
403
+ def initialize
404
+ @body = nil
405
+ @size = nil
406
+ @positions = nil
407
+ @chunk_size = nil
408
+ end
409
+
410
+ # Initialize this instance as a request.
411
+ def init_request(body = nil, boundary = nil)
412
+ @boundary = boundary
413
+ @positions = {}
414
+ set_content(body, boundary)
415
+ @chunk_size = DEFAULT_CHUNK_SIZE
416
+ end
417
+
418
+ # Initialize this instance as a response.
419
+ def init_response(body = nil)
420
+ @body = body
421
+ if @body.respond_to?(:bytesize)
422
+ @size = @body.bytesize
423
+ elsif @body.respond_to?(:size)
424
+ @size = @body.size
425
+ else
426
+ @size = nil
427
+ end
428
+ end
429
+
430
+ # Dumps message body to given dev.
431
+ # dev needs to respond to <<.
432
+ #
433
+ # Message header must be given as the first argument for performance
434
+ # reason. (header is dumped to dev, too)
435
+ # If no dev (the second argument) given, this method returns a dumped
436
+ # String.
437
+ def dump(header = '', dev = '')
438
+ if @body.is_a?(Parts)
439
+ dev << header
440
+ buf = ''
441
+ @body.parts.each do |part|
442
+ if Message.file?(part)
443
+ reset_pos(part)
444
+ while !part.read(@chunk_size, buf).nil?
445
+ dev << buf
446
+ end
447
+ else
448
+ dev << part
449
+ end
450
+ end
451
+ elsif @body
452
+ dev << header + @body
453
+ else
454
+ dev << header
455
+ end
456
+ dev
457
+ end
458
+
459
+ # Dumps message body with chunked encoding to given dev.
460
+ # dev needs to respond to <<.
461
+ #
462
+ # Message header must be given as the first argument for performance
463
+ # reason. (header is dumped to dev, too)
464
+ # If no dev (the second argument) given, this method returns a dumped
465
+ # String.
466
+ def dump_chunked(header = '', dev = '')
467
+ dev << header
468
+ if @body.is_a?(Parts)
469
+ @body.parts.each do |part|
470
+ if Message.file?(part)
471
+ reset_pos(part)
472
+ dump_chunks(part, dev)
473
+ else
474
+ dev << dump_chunk(part)
475
+ end
476
+ end
477
+ dev << (dump_last_chunk + CRLF)
478
+ elsif @body
479
+ reset_pos(@body)
480
+ dump_chunks(@body, dev)
481
+ dev << (dump_last_chunk + CRLF)
482
+ end
483
+ dev
484
+ end
485
+
486
+ # Returns a message body itself.
487
+ def content
488
+ @body
489
+ end
490
+
491
+ private
492
+
493
+ def set_content(body, boundary = nil)
494
+ if body.respond_to?(:read)
495
+ # uses Transfer-Encoding: chunked. bear in mind that server may not
496
+ # support it. at least ruby's CGI doesn't.
497
+ @body = body
498
+ remember_pos(@body)
499
+ @size = nil
500
+ elsif boundary and Message.multiparam_query?(body)
501
+ @body = build_query_multipart_str(body, boundary)
502
+ @size = @body.size
503
+ else
504
+ @body = Message.create_query_part_str(body)
505
+ @size = @body.bytesize
506
+ end
507
+ end
508
+
509
+ def remember_pos(io)
510
+ # IO may not support it (ex. IO.pipe)
511
+ @positions[io] = io.pos rescue nil
512
+ end
513
+
514
+ def reset_pos(io)
515
+ io.pos = @positions[io] if @positions.key?(io)
516
+ end
517
+
518
+ def dump_chunks(io, dev)
519
+ buf = ''
520
+ while !io.read(@chunk_size, buf).nil?
521
+ dev << dump_chunk(buf)
522
+ end
523
+ end
524
+
525
+ def dump_chunk(str)
526
+ dump_chunk_size(str.bytesize) + (str + CRLF)
527
+ end
528
+
529
+ def dump_last_chunk
530
+ dump_chunk_size(0)
531
+ end
532
+
533
+ def dump_chunk_size(size)
534
+ sprintf("%x", size) + CRLF
535
+ end
536
+
537
+ class Parts
538
+ attr_reader :size
539
+
540
+ def initialize
541
+ @body = []
542
+ @size = 0
543
+ @as_stream = false
544
+ end
545
+
546
+ def add(part)
547
+ if Message.file?(part)
548
+ @as_stream = true
549
+ @body << part
550
+ if part.respond_to?(:size)
551
+ if sz = part.size
552
+ @size += sz
553
+ else
554
+ @size = nil
555
+ end
556
+ elsif part.respond_to?(:lstat)
557
+ @size += part.lstat.size
558
+ else
559
+ # use chunked upload
560
+ @size = nil
561
+ end
562
+ elsif @body[-1].is_a?(String)
563
+ @body[-1] += part.to_s
564
+ @size += part.to_s.bytesize if @size
565
+ else
566
+ @body << part.to_s
567
+ @size += part.to_s.bytesize if @size
568
+ end
569
+ end
570
+
571
+ def parts
572
+ if @as_stream
573
+ @body
574
+ else
575
+ [@body.join]
576
+ end
577
+ end
578
+ end
579
+
580
+ def build_query_multipart_str(query, boundary)
581
+ parts = Parts.new
582
+ query.each do |attr, value|
583
+ value ||= ''
584
+ headers = ["--#{boundary}"]
585
+ if Message.file?(value)
586
+ remember_pos(value)
587
+ param_str = params_from_file(value).collect { |k, v|
588
+ "#{k}=\"#{v}\""
589
+ }.join("; ")
590
+ if value.respond_to?(:mime_type)
591
+ content_type = value.mime_type
592
+ elsif value.respond_to?(:content_type)
593
+ content_type = value.content_type
594
+ else
595
+ path = value.respond_to?(:path) ? value.path : nil
596
+ content_type = Message.mime_type(path)
597
+ end
598
+ headers << %{Content-Disposition: form-data; name="#{attr}"; #{param_str}}
599
+ headers << %{Content-Type: #{content_type}}
600
+ else
601
+ headers << %{Content-Disposition: form-data; name="#{attr}"}
602
+ end
603
+ parts.add(headers.join(CRLF) + CRLF + CRLF)
604
+ parts.add(value)
605
+ parts.add(CRLF)
606
+ end
607
+ parts.add("--#{boundary}--" + CRLF + CRLF) # empty epilogue
608
+ parts
609
+ end
610
+
611
+ def params_from_file(value)
612
+ params = {}
613
+ path = value.respond_to?(:path) ? value.path : nil
614
+ params['filename'] = File.basename(path || '')
615
+ # Creation time is not available from File::Stat
616
+ if value.respond_to?(:mtime)
617
+ params['modification-date'] = value.mtime.rfc822
618
+ end
619
+ if value.respond_to?(:atime)
620
+ params['read-date'] = value.atime.rfc822
621
+ end
622
+ params
623
+ end
624
+ end
625
+
626
+
627
+ class << self
628
+ private :new
629
+
630
+ # Creates a Message instance of 'CONNECT' request.
631
+ # 'CONNECT' request does not have Body.
632
+ # uri:: an URI that need to connect. Only uri.host and uri.port are used.
633
+ def new_connect_request(uri)
634
+ m = new
635
+ m.header.init_connect_request(uri)
636
+ m.header.body_size = nil
637
+ m
638
+ end
639
+
640
+ # Creates a Message instance of general request.
641
+ # method:: HTTP method String.
642
+ # uri:: an URI object which represents an URL of web resource.
643
+ # query:: a Hash or an Array of query part of URL.
644
+ # e.g. { "a" => "b" } => 'http://host/part?a=b'
645
+ # Give an array to pass multiple value like
646
+ # [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'
647
+ # body:: a Hash or an Array of body part.
648
+ # e.g. { "a" => "b" } => 'a=b'.
649
+ # Give an array to pass multiple value like
650
+ # [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
651
+ # boundary:: When the boundary given, it is sent as
652
+ # a multipart/form-data using this boundary String.
653
+ def new_request(method, uri, query = nil, body = nil, boundary = nil)
654
+ m = new
655
+ m.header.init_request(method, uri, query)
656
+ m.body = Body.new
657
+ m.body.init_request(body || '', boundary)
658
+ if body
659
+ m.header.body_size = m.body.size
660
+ m.header.chunked = true if m.body.size.nil?
661
+ else
662
+ m.header.body_size = nil
663
+ end
664
+ m
665
+ end
666
+
667
+ # Creates a Message instance of response.
668
+ # body:: a String or an IO of response message body.
669
+ def new_response(body)
670
+ m = new
671
+ m.header.init_response(Status::OK)
672
+ m.body = Body.new
673
+ m.body.init_response(body)
674
+ m.header.body_size = m.body.size || 0
675
+ m
676
+ end
677
+
678
+ @@mime_type_handler = nil
679
+
680
+ # Sets MIME type handler.
681
+ #
682
+ # handler must respond to :call with a single argument :path and returns
683
+ # a MIME type String e.g. 'text/html'.
684
+ # When the handler returns nil or an empty String,
685
+ # 'application/octet-stream' is used.
686
+ #
687
+ # When you set nil to the handler, internal_mime_type is used instead.
688
+ # The handler is nil by default.
689
+ def mime_type_handler=(handler)
690
+ @@mime_type_handler = handler
691
+ end
692
+
693
+ # Returns MIME type handler.
694
+ def mime_type_handler
695
+ @@mime_type_handler
696
+ end
697
+
698
+ # For backward compatibility.
699
+ alias set_mime_type_func mime_type_handler=
700
+ alias get_mime_type_func mime_type_handler
701
+
702
+ def mime_type(path) # :nodoc:
703
+ if @@mime_type_handler
704
+ res = @@mime_type_handler.call(path)
705
+ if !res || res.to_s == ''
706
+ return 'application/octet-stream'
707
+ else
708
+ return res
709
+ end
710
+ else
711
+ internal_mime_type(path)
712
+ end
713
+ end
714
+
715
+ # Default MIME type handler.
716
+ # See mime_type_handler=.
717
+ def internal_mime_type(path)
718
+ case path
719
+ when /\.txt$/i
720
+ 'text/plain'
721
+ when /\.(htm|html)$/i
722
+ 'text/html'
723
+ when /\.doc$/i
724
+ 'application/msword'
725
+ when /\.png$/i
726
+ 'image/png'
727
+ when /\.gif$/i
728
+ 'image/gif'
729
+ when /\.(jpg|jpeg)$/i
730
+ 'image/jpeg'
731
+ else
732
+ 'application/octet-stream'
733
+ end
734
+ end
735
+
736
+ # Returns true if the given HTTP version allows keep alive connection.
737
+ # version:: Float
738
+ def keep_alive_enabled?(version)
739
+ version >= 1.1
740
+ end
741
+
742
+ # Returns true if the given query (or body) has a multiple parameter.
743
+ def multiparam_query?(query)
744
+ query.is_a?(Array) or query.is_a?(Hash)
745
+ end
746
+
747
+ # Returns true if the given object is a File. In HTTPClient, a file is;
748
+ # * must respond to :read for retrieving String chunks.
749
+ # * must respond to :pos and :pos= to rewind for reading.
750
+ # Rewinding is only needed for following HTTP redirect. Some IO impl
751
+ # defines :pos= but raises an Exception for pos= such as StringIO
752
+ # but there's no problem as far as using it for non-following methods
753
+ # (get/post/etc.)
754
+ def file?(obj)
755
+ obj.respond_to?(:read) and obj.respond_to?(:pos) and
756
+ obj.respond_to?(:pos=)
757
+ end
758
+
759
+ def create_query_part_str(query) # :nodoc:
760
+ if multiparam_query?(query)
761
+ escape_query(query)
762
+ elsif query.respond_to?(:read)
763
+ query = query.read
764
+ else
765
+ query.to_s
766
+ end
767
+ end
768
+
769
+ def escape_query(query) # :nodoc:
770
+ query.sort_by { |attr, value| attr.to_s }.collect { |attr, value|
771
+ if value.respond_to?(:read)
772
+ value = value.read
773
+ end
774
+ escape(attr.to_s) << '=' << escape(value.to_s)
775
+ }.join('&')
776
+ end
777
+
778
+ # from CGI.escape
779
+ def escape(str) # :nodoc:
780
+ if str.respond_to?(:force_encoding)
781
+ str.dup.force_encoding('BINARY').gsub(/([^ a-zA-Z0-9_.-]+)/) {
782
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
783
+ }.tr(' ', '+')
784
+ else
785
+ str.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
786
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
787
+ }.tr(' ', '+')
788
+ end
789
+ end
790
+
791
+ # from CGI.parse
792
+ def parse(query)
793
+ params = Hash.new([].freeze)
794
+ query.split(/[&;]/n).each do |pairs|
795
+ key, value = pairs.split('=',2).collect{|v| unescape(v) }
796
+ if params.has_key?(key)
797
+ params[key].push(value)
798
+ else
799
+ params[key] = [value]
800
+ end
801
+ end
802
+ params
803
+ end
804
+
805
+ # from CGI.unescape
806
+ def unescape(string)
807
+ string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
808
+ [$1.delete('%')].pack('H*')
809
+ end
810
+ end
811
+ end
812
+
813
+
814
+ # HTTP::Message::Headers:: message header.
815
+ attr_accessor :header
816
+
817
+ # HTTP::Message::Body:: message body.
818
+ attr_reader :body
819
+
820
+ # OpenSSL::X509::Certificate:: response only. server certificate which is
821
+ # used for retrieving the response.
822
+ attr_accessor :peer_cert
823
+
824
+ # Creates a Message. This method should be used internally.
825
+ # Use Message.new_connect_request, Message.new_request or
826
+ # Message.new_response instead.
827
+ def initialize # :nodoc:
828
+ @header = Headers.new
829
+ @body = @peer_cert = nil
830
+ end
831
+
832
+ # Dumps message (header and body) to given dev.
833
+ # dev needs to respond to <<.
834
+ def dump(dev = '')
835
+ str = header.dump + CRLF
836
+ if header.chunked
837
+ dev = body.dump_chunked(str, dev)
838
+ elsif body
839
+ dev = body.dump(str, dev)
840
+ else
841
+ dev << str
842
+ end
843
+ dev
844
+ end
845
+
846
+ # Sets a new body. header.body_size is updated with new body.size.
847
+ def body=(body)
848
+ @body = body
849
+ @header.body_size = @body.size if @header
850
+ end
851
+
852
+ # Returns HTTP version in a HTTP header. Float.
853
+ def version
854
+ @header.http_version
855
+ end
856
+
857
+ # Sets HTTP version in a HTTP header. Float.
858
+ def version=(version)
859
+ @header.http_version = version
860
+ end
861
+
862
+ # Returns HTTP status code in response. Integer.
863
+ def status
864
+ @header.status_code
865
+ end
866
+
867
+ alias code status
868
+ alias status_code status
869
+
870
+ # Sets HTTP status code of response. Integer.
871
+ # Reason phrase is updated, too.
872
+ def status=(status)
873
+ @header.status_code = status
874
+ end
875
+
876
+ # Returns HTTP status reason phrase in response. String.
877
+ def reason
878
+ @header.reason_phrase
879
+ end
880
+
881
+ # Sets HTTP status reason phrase of response. String.
882
+ def reason=(reason)
883
+ @header.reason_phrase = reason
884
+ end
885
+
886
+ # Sets 'Content-Type' header value. Overrides if already exists.
887
+ def contenttype
888
+ @header.contenttype
889
+ end
890
+
891
+ # Returns 'Content-Type' header value.
892
+ def contenttype=(contenttype)
893
+ @header.contenttype = contenttype
894
+ end
895
+
896
+ # Returns a content of message body. A String or an IO.
897
+ def content
898
+ @body.content
899
+ end
900
+ end
901
+
902
+
903
+ end