httpclient-fixcerts 2.8.5

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