maedana-httpclient 2.1.5.2.1

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,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