httpclient-jgraichen 2.3.4.2

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