httpclient-jgraichen 2.3.4.2

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