vmc 0.0.4 → 0.0.5

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.
@@ -0,0 +1,867 @@
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
+ private
289
+
290
+ def request_line
291
+ path = create_query_uri(@request_uri, @request_query)
292
+ if @request_via_proxy
293
+ path = "#{ @request_uri.scheme }://#{ @request_uri.host }:#{ @request_uri.port }#{ path }"
294
+ end
295
+ "#{ @request_method } #{ path } HTTP/#{ @http_version }#{ CRLF }"
296
+ end
297
+
298
+ def response_status_line
299
+ if defined?(Apache)
300
+ "HTTP/#{ @http_version } #{ @status_code } #{ @reason_phrase }#{ CRLF }"
301
+ else
302
+ "Status: #{ @status_code } #{ @reason_phrase }#{ CRLF }"
303
+ end
304
+ end
305
+
306
+ def set_header
307
+ if @is_request
308
+ set_request_header
309
+ else
310
+ set_response_header
311
+ end
312
+ end
313
+
314
+ def set_request_header
315
+ return if @dumped
316
+ @dumped = true
317
+ keep_alive = Message.keep_alive_enabled?(@http_version)
318
+ if !keep_alive and @request_method != 'CONNECT'
319
+ set('Connection', 'close')
320
+ end
321
+ if @chunked
322
+ set('Transfer-Encoding', 'chunked')
323
+ elsif @body_size and (keep_alive or @body_size != 0)
324
+ set('Content-Length', @body_size.to_s)
325
+ end
326
+ if @http_version >= 1.1
327
+ if @request_uri.port == @request_uri.default_port
328
+ # GFE/1.3 dislikes default port number (returns 404)
329
+ set('Host', "#{@request_uri.host}")
330
+ else
331
+ set('Host', "#{@request_uri.host}:#{@request_uri.port}")
332
+ end
333
+ end
334
+ end
335
+
336
+ def set_response_header
337
+ return if @dumped
338
+ @dumped = true
339
+ if defined?(Apache) && self['Date'].empty?
340
+ set('Date', Time.now.httpdate)
341
+ end
342
+ keep_alive = Message.keep_alive_enabled?(@http_version)
343
+ if @chunked
344
+ set('Transfer-Encoding', 'chunked')
345
+ else
346
+ if keep_alive or @body_size != 0
347
+ set('Content-Length', @body_size.to_s)
348
+ end
349
+ end
350
+ if @body_date
351
+ set('Last-Modified', @body_date.httpdate)
352
+ end
353
+ if self['Content-Type'].empty?
354
+ set('Content-Type', "#{ @body_type || 'text/html' }; charset=#{ charset_label(@body_charset || $KCODE) }")
355
+ end
356
+ end
357
+
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}"
381
+ end
382
+ path
383
+ end
384
+ end
385
+
386
+
387
+ # Represents HTTP message body.
388
+ class Body
389
+ # Size of body. nil when size is unknown (e.g. chunked response).
390
+ attr_reader :size
391
+ # maxbytes of IO#read for streaming request. See DEFAULT_CHUNK_SIZE.
392
+ attr_accessor :chunk_size
393
+
394
+ # Default value for chunk_size
395
+ DEFAULT_CHUNK_SIZE = 1024 * 16
396
+
397
+ # Creates a Message::Body. Use init_request or init_response
398
+ # for acutual initialize.
399
+ def initialize
400
+ @body = nil
401
+ @size = nil
402
+ @positions = nil
403
+ @chunk_size = nil
404
+ end
405
+
406
+ # Initialize this instance as a request.
407
+ def init_request(body = nil, boundary = nil)
408
+ @boundary = boundary
409
+ @positions = {}
410
+ set_content(body, boundary)
411
+ @chunk_size = DEFAULT_CHUNK_SIZE
412
+ end
413
+
414
+ # Initialize this instance as a response.
415
+ def init_response(body = nil)
416
+ @body = body
417
+ if @body.respond_to?(:size)
418
+ @size = @body.size
419
+ else
420
+ @size = nil
421
+ end
422
+ end
423
+
424
+ # Dumps message body to given dev.
425
+ # dev needs to respond to <<.
426
+ #
427
+ # Message header must be given as the first argument for performance
428
+ # reason. (header is dumped to dev, too)
429
+ # If no dev (the second argument) given, this method returns a dumped
430
+ # String.
431
+ def dump(header = '', dev = '')
432
+ if @body.is_a?(Parts)
433
+ dev << header
434
+ buf = ''
435
+ @body.parts.each do |part|
436
+ if Message.file?(part)
437
+ reset_pos(part)
438
+ while !part.read(@chunk_size, buf).nil?
439
+ dev << buf
440
+ end
441
+ else
442
+ dev << part
443
+ end
444
+ end
445
+ elsif @body
446
+ dev << header + @body
447
+ else
448
+ dev << header
449
+ end
450
+ dev
451
+ end
452
+
453
+ # Dumps message body with chunked encoding to given dev.
454
+ # dev needs to respond to <<.
455
+ #
456
+ # Message header must be given as the first argument for performance
457
+ # reason. (header is dumped to dev, too)
458
+ # If no dev (the second argument) given, this method returns a dumped
459
+ # String.
460
+ def dump_chunked(header = '', dev = '')
461
+ dev << header
462
+ if @body.is_a?(Parts)
463
+ @body.parts.each do |part|
464
+ if Message.file?(part)
465
+ reset_pos(part)
466
+ dump_chunks(part, dev)
467
+ else
468
+ dev << dump_chunk(part)
469
+ end
470
+ end
471
+ dev << (dump_last_chunk + CRLF)
472
+ elsif @body
473
+ reset_pos(@body)
474
+ dump_chunks(@body, dev)
475
+ dev << (dump_last_chunk + CRLF)
476
+ end
477
+ dev
478
+ end
479
+
480
+ # Returns a message body itself.
481
+ def content
482
+ @body
483
+ end
484
+
485
+ private
486
+
487
+ 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.
491
+ @body = body
492
+ remember_pos(@body)
493
+ @size = nil
494
+ elsif boundary and Message.multiparam_query?(body)
495
+ @body = build_query_multipart_str(body, boundary)
496
+ @size = @body.size
497
+ else
498
+ @body = Message.create_query_part_str(body)
499
+ @size = @body.size
500
+ end
501
+ end
502
+
503
+ def remember_pos(io)
504
+ # IO may not support it (ex. IO.pipe)
505
+ @positions[io] = io.pos rescue nil
506
+ end
507
+
508
+ def reset_pos(io)
509
+ io.pos = @positions[io] if @positions.key?(io)
510
+ end
511
+
512
+ def dump_chunks(io, dev)
513
+ buf = ''
514
+ while !io.read(@chunk_size, buf).nil?
515
+ dev << dump_chunk(buf)
516
+ end
517
+ end
518
+
519
+ def dump_chunk(str)
520
+ dump_chunk_size(str.size) + (str + CRLF)
521
+ end
522
+
523
+ def dump_last_chunk
524
+ dump_chunk_size(0)
525
+ end
526
+
527
+ def dump_chunk_size(size)
528
+ sprintf("%x", size) + CRLF
529
+ end
530
+
531
+ class Parts
532
+ attr_reader :size
533
+
534
+ def initialize
535
+ @body = []
536
+ @size = 0
537
+ @as_stream = false
538
+ end
539
+
540
+ def add(part)
541
+ if Message.file?(part)
542
+ @as_stream = true
543
+ @body << part
544
+ if part.respond_to?(:size)
545
+ if sz = part.size
546
+ @size += sz
547
+ else
548
+ @size = nil
549
+ end
550
+ elsif part.respond_to?(:lstat)
551
+ @size += part.lstat.size
552
+ else
553
+ # use chunked upload
554
+ @size = nil
555
+ end
556
+ elsif @body[-1].is_a?(String)
557
+ @body[-1] += part.to_s
558
+ @size += part.to_s.size if @size
559
+ else
560
+ @body << part.to_s
561
+ @size += part.to_s.size if @size
562
+ end
563
+ end
564
+
565
+ def parts
566
+ if @as_stream
567
+ @body
568
+ else
569
+ [@body.join]
570
+ end
571
+ end
572
+ end
573
+
574
+ def build_query_multipart_str(query, boundary)
575
+ parts = Parts.new
576
+ query.each do |attr, value|
577
+ value ||= ''
578
+ headers = ["--#{boundary}"]
579
+ if Message.file?(value)
580
+ remember_pos(value)
581
+ param_str = params_from_file(value).collect { |k, v|
582
+ "#{k}=\"#{v}\""
583
+ }.join("; ")
584
+ if value.respond_to?(:mime_type)
585
+ content_type = value.mime_type
586
+ else
587
+ content_type = Message.mime_type(value.path)
588
+ end
589
+ headers << %{Content-Disposition: form-data; name="#{attr}"; #{param_str}}
590
+ headers << %{Content-Type: #{content_type}}
591
+ else
592
+ headers << %{Content-Disposition: form-data; name="#{attr}"}
593
+ end
594
+ parts.add(headers.join(CRLF) + CRLF + CRLF)
595
+ parts.add(value)
596
+ parts.add(CRLF)
597
+ end
598
+ parts.add("--#{boundary}--" + CRLF + CRLF) # empty epilogue
599
+ parts
600
+ end
601
+
602
+ def params_from_file(value)
603
+ params = {}
604
+ params['filename'] = File.basename(value.path || '')
605
+ # Creation time is not available from File::Stat
606
+ if value.respond_to?(:mtime)
607
+ params['modification-date'] = value.mtime.rfc822
608
+ end
609
+ if value.respond_to?(:atime)
610
+ params['read-date'] = value.atime.rfc822
611
+ end
612
+ params
613
+ end
614
+ end
615
+
616
+
617
+ class << self
618
+ private :new
619
+
620
+ # Creates a Message instance of 'CONNECT' request.
621
+ # 'CONNECT' request does not have Body.
622
+ # uri:: an URI that need to connect. Only uri.host and uri.port are used.
623
+ def new_connect_request(uri)
624
+ m = new
625
+ m.header.init_connect_request(uri)
626
+ m.header.body_size = nil
627
+ m
628
+ end
629
+
630
+ # Creates a Message instance of general request.
631
+ # method:: HTTP method String.
632
+ # uri:: an URI object which represents an URL of web resource.
633
+ # query:: a Hash or an Array of query part of URL.
634
+ # e.g. { "a" => "b" } => 'http://host/part?a=b'
635
+ # Give an array to pass multiple value like
636
+ # [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'
637
+ # body:: a Hash or an Array of body part.
638
+ # e.g. { "a" => "b" } => 'a=b'.
639
+ # Give an array to pass multiple value like
640
+ # [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
641
+ # boundary:: When the boundary given, it is sent as
642
+ # a multipart/form-data using this boundary String.
643
+ def new_request(method, uri, query = nil, body = nil, boundary = nil)
644
+ m = new
645
+ m.header.init_request(method, uri, query)
646
+ m.body = Body.new
647
+ m.body.init_request(body || '', boundary)
648
+ if body
649
+ m.header.body_size = m.body.size
650
+ m.header.chunked = true if m.body.size.nil?
651
+ else
652
+ m.header.body_size = nil
653
+ end
654
+ m
655
+ end
656
+
657
+ # Creates a Message instance of response.
658
+ # body:: a String or an IO of response message body.
659
+ def new_response(body)
660
+ 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
665
+ m
666
+ end
667
+
668
+ @@mime_type_handler = nil
669
+
670
+ # Sets MIME type handler.
671
+ #
672
+ # handler must respond to :call with a single argument :path and returns
673
+ # a MIME type String e.g. 'text/html'.
674
+ # When the handler returns nil or an empty String,
675
+ # 'application/octet-stream' is used.
676
+ #
677
+ # When you set nil to the handler, internal_mime_type is used instead.
678
+ # The handler is nil by default.
679
+ def mime_type_handler=(handler)
680
+ @@mime_type_handler = handler
681
+ end
682
+
683
+ # Returns MIME type handler.
684
+ def mime_type_handler
685
+ @@mime_type_handler
686
+ end
687
+
688
+ # For backward compatibility.
689
+ alias set_mime_type_func mime_type_handler=
690
+ alias get_mime_type_func mime_type_handler
691
+
692
+ def mime_type(path) # :nodoc:
693
+ if @@mime_type_handler
694
+ res = @@mime_type_handler.call(path)
695
+ if !res || res.to_s == ''
696
+ return 'application/octet-stream'
697
+ else
698
+ return res
699
+ end
700
+ else
701
+ internal_mime_type(path)
702
+ end
703
+ end
704
+
705
+ # Default MIME type handler.
706
+ # See mime_type_handler=.
707
+ def internal_mime_type(path)
708
+ case path
709
+ when /\.txt$/i
710
+ 'text/plain'
711
+ when /\.(htm|html)$/i
712
+ 'text/html'
713
+ when /\.doc$/i
714
+ 'application/msword'
715
+ when /\.png$/i
716
+ 'image/png'
717
+ when /\.gif$/i
718
+ 'image/gif'
719
+ when /\.(jpg|jpeg)$/i
720
+ 'image/jpeg'
721
+ else
722
+ 'application/octet-stream'
723
+ end
724
+ end
725
+
726
+ # Returns true if the given HTTP version allows keep alive connection.
727
+ # version:: Float
728
+ def keep_alive_enabled?(version)
729
+ version >= 1.1
730
+ end
731
+
732
+ # Returns true if the given query (or body) has a multiple parameter.
733
+ def multiparam_query?(query)
734
+ query.is_a?(Array) or query.is_a?(Hash)
735
+ end
736
+
737
+ # Returns true if the given object is a File. In HTTPClient, a file is;
738
+ # * must respond to :read for retrieving String chunks.
739
+ # * must respond to :path and returns a path for Content-Disposition.
740
+ # * must respond to :pos and :pos= to rewind for reading.
741
+ # Rewinding is only needed for following HTTP redirect. Some IO impl
742
+ # defines :pos= but raises an Exception for pos= such as StringIO
743
+ # but there's no problem as far as using it for non-following methods
744
+ # (get/post/etc.)
745
+ def file?(obj)
746
+ obj.respond_to?(:read) and obj.respond_to?(:path) and
747
+ obj.respond_to?(:pos) and obj.respond_to?(:pos=)
748
+ end
749
+
750
+ def create_query_part_str(query) # :nodoc:
751
+ if multiparam_query?(query)
752
+ escape_query(query)
753
+ elsif query.respond_to?(:read)
754
+ query = query.read
755
+ else
756
+ query.to_s
757
+ end
758
+ end
759
+
760
+ def escape_query(query) # :nodoc:
761
+ query.collect { |attr, value|
762
+ if value.respond_to?(:read)
763
+ value = value.read
764
+ end
765
+ escape(attr.to_s) << '=' << escape(value.to_s)
766
+ }.join('&')
767
+ end
768
+
769
+ # 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(' ', '+')
774
+ end
775
+ end
776
+
777
+
778
+ # HTTP::Message::Headers:: message header.
779
+ attr_accessor :header
780
+
781
+ # HTTP::Message::Body:: message body.
782
+ attr_reader :body
783
+
784
+ # OpenSSL::X509::Certificate:: response only. server certificate which is
785
+ # used for retrieving the response.
786
+ attr_accessor :peer_cert
787
+
788
+ # Creates a Message. This method should be used internally.
789
+ # Use Message.new_connect_request, Message.new_request or
790
+ # Message.new_response instead.
791
+ def initialize # :nodoc:
792
+ @header = Headers.new
793
+ @body = @peer_cert = nil
794
+ end
795
+
796
+ # Dumps message (header and body) to given dev.
797
+ # dev needs to respond to <<.
798
+ 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)
804
+ else
805
+ dev << str
806
+ end
807
+ dev
808
+ end
809
+
810
+ # 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
814
+ end
815
+
816
+ # Returns HTTP version in a HTTP header. Float.
817
+ def version
818
+ @header.http_version
819
+ end
820
+
821
+ # Sets HTTP version in a HTTP header. Float.
822
+ def version=(version)
823
+ @header.http_version = version
824
+ end
825
+
826
+ # Returns HTTP status code in response. Integer.
827
+ def status
828
+ @header.status_code
829
+ end
830
+
831
+ alias code status
832
+ alias status_code status
833
+
834
+ # Sets HTTP status code of response. Integer.
835
+ # Reason phrase is updated, too.
836
+ def status=(status)
837
+ @header.status_code = status
838
+ end
839
+
840
+ # Returns HTTP status reason phrase in response. String.
841
+ def reason
842
+ @header.reason_phrase
843
+ end
844
+
845
+ # Sets HTTP status reason phrase of response. String.
846
+ def reason=(reason)
847
+ @header.reason_phrase = reason
848
+ end
849
+
850
+ # Sets 'Content-Type' header value. Overrides if already exists.
851
+ def contenttype
852
+ @header.contenttype
853
+ end
854
+
855
+ # Returns 'Content-Type' header value.
856
+ def contenttype=(contenttype)
857
+ @header.contenttype = contenttype
858
+ end
859
+
860
+ # Returns a content of message body. A String or an IO.
861
+ def content
862
+ @body.content
863
+ end
864
+ end
865
+
866
+
867
+ end