httpclient-fixcerts 2.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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