solutious-stella 0.7.0.004 → 0.7.0.005

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/bin/stella +16 -20
  2. data/examples/essentials/logo.png +0 -0
  3. data/examples/{basic → essentials}/plan.rb +7 -3
  4. data/examples/{basic → essentials}/search_terms.csv +0 -0
  5. data/examples/example_webapp.rb +7 -4
  6. data/examples/example_webapp.ru +3 -0
  7. data/lib/stella.rb +18 -26
  8. data/lib/stella/cli.rb +4 -1
  9. data/lib/stella/client.rb +49 -26
  10. data/lib/stella/data.rb +35 -9
  11. data/lib/stella/data/http.rb +1 -1
  12. data/lib/stella/data/http/request.rb +3 -14
  13. data/lib/stella/engine.rb +10 -4
  14. data/lib/stella/engine/functional.rb +2 -4
  15. data/lib/stella/engine/load.rb +24 -21
  16. data/lib/stella/mixins.rb +1 -1
  17. data/lib/stella/stats.rb +17 -4
  18. data/lib/stella/testplan/usecase.rb +2 -2
  19. data/lib/stella/utils.rb +16 -1
  20. data/lib/stella/version.rb +1 -1
  21. data/stella.gemspec +17 -4
  22. data/vendor/httpclient-2.1.5.2/httpclient.rb +1025 -0
  23. data/vendor/httpclient-2.1.5.2/httpclient/auth.rb +522 -0
  24. data/vendor/httpclient-2.1.5.2/httpclient/cacert.p7s +1579 -0
  25. data/vendor/httpclient-2.1.5.2/httpclient/cacert_sha1.p7s +1579 -0
  26. data/vendor/httpclient-2.1.5.2/httpclient/connection.rb +84 -0
  27. data/vendor/httpclient-2.1.5.2/httpclient/cookie.rb +562 -0
  28. data/vendor/httpclient-2.1.5.2/httpclient/http.rb +867 -0
  29. data/vendor/httpclient-2.1.5.2/httpclient/session.rb +864 -0
  30. data/vendor/httpclient-2.1.5.2/httpclient/ssl_config.rb +417 -0
  31. data/vendor/httpclient-2.1.5.2/httpclient/stats.rb +90 -0
  32. data/vendor/httpclient-2.1.5.2/httpclient/timeout.rb +136 -0
  33. data/vendor/httpclient-2.1.5.2/httpclient/util.rb +86 -0
  34. metadata +17 -4
  35. data/lib/stella/dsl.rb +0 -5
@@ -0,0 +1,867 @@
1
+ # HTTPClient - HTTP client library.
2
+ # Copyright (C) 2000-2009 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
3
+ #
4
+ # This program is copyrighted free software by NAKAMURA, Hiroshi. You can
5
+ # redistribute it and/or modify it under the same terms of Ruby's license;
6
+ # either the dual license version in 2003, or any later version.
7
+
8
+
9
+ require 'time'
10
+
11
+
12
+ # A namespace module for HTTP Message definitions used by HTTPClient.
13
+ module HTTP
14
+
15
+
16
+ # Represents HTTP response status code. Defines constants for HTTP response
17
+ # and some conditional methods.
18
+ module Status
19
+ OK = 200
20
+ CREATED = 201
21
+ ACCEPTED = 202
22
+ NON_AUTHORITATIVE_INFORMATION = 203
23
+ NO_CONTENT = 204
24
+ RESET_CONTENT = 205
25
+ PARTIAL_CONTENT = 206
26
+ MOVED_PERMANENTLY = 301
27
+ FOUND = 302
28
+ SEE_OTHER = 303
29
+ TEMPORARY_REDIRECT = MOVED_TEMPORARILY = 307
30
+ BAD_REQUEST = 400
31
+ UNAUTHORIZED = 401
32
+ PROXY_AUTHENTICATE_REQUIRED = 407
33
+ INTERNAL = 500
34
+
35
+ # Status codes for successful HTTP response.
36
+ SUCCESSFUL_STATUS = [
37
+ OK, CREATED, ACCEPTED,
38
+ NON_AUTHORITATIVE_INFORMATION, NO_CONTENT,
39
+ RESET_CONTENT, PARTIAL_CONTENT
40
+ ]
41
+
42
+ # Status codes which is a redirect.
43
+ REDIRECT_STATUS = [
44
+ MOVED_PERMANENTLY, FOUND, SEE_OTHER,
45
+ TEMPORARY_REDIRECT, MOVED_TEMPORARILY
46
+ ]
47
+
48
+ # Returns true if the given status represents successful HTTP response.
49
+ # See also SUCCESSFUL_STATUS.
50
+ def self.successful?(status)
51
+ SUCCESSFUL_STATUS.include?(status)
52
+ end
53
+
54
+ # Returns true if the given status is thought to be redirect.
55
+ # See also REDIRECT_STATUS.
56
+ def self.redirect?(status)
57
+ REDIRECT_STATUS.include?(status)
58
+ end
59
+ end
60
+
61
+
62
+ # Represents a HTTP message. A message is for a request or a response.
63
+ #
64
+ # Request message is generated from given parameters internally so users
65
+ # don't need to care about it. Response message is the instance that
66
+ # methods of HTTPClient returns so users need to know how to extract
67
+ # HTTP response data from Message.
68
+ #
69
+ # Some attributes are only for a request or a response, not both.
70
+ #
71
+ # == How to use HTTP response message
72
+ #
73
+ # 1. Gets response message body.
74
+ #
75
+ # res = clnt.get(url)
76
+ # p res.content #=> String
77
+ #
78
+ # 2. Gets response status code.
79
+ #
80
+ # res = clnt.get(url)
81
+ # p res.status #=> 200, 501, etc. (Integer)
82
+ #
83
+ # 3. Gets response header.
84
+ #
85
+ # res = clnt.get(url)
86
+ # res.header['set-cookie'].each do |value|
87
+ # p value
88
+ # end
89
+ # assert_equal(1, res.header['last-modified'].size)
90
+ # p res.header['last-modified'].first
91
+ #
92
+ class Message
93
+
94
+ CRLF = "\r\n"
95
+
96
+ # Represents HTTP message header.
97
+ class Headers
98
+ # HTTP version in a HTTP header. Float.
99
+ attr_accessor :http_version
100
+ # Size of body. nil when size is unknown (e.g. chunked response).
101
+ attr_reader :body_size
102
+ # Request/Response is chunked or not.
103
+ attr_accessor :chunked
104
+
105
+ # Request only. Requested method.
106
+ attr_reader :request_method
107
+ # Request only. Requested URI.
108
+ attr_accessor :request_uri
109
+ # Request only. Requested query.
110
+ attr_accessor :request_query
111
+ # Request only. Requested via proxy or not.
112
+ attr_accessor :request_via_proxy
113
+
114
+ # Response only. HTTP status
115
+ attr_reader :status_code
116
+ # Response only. HTTP status reason phrase.
117
+ attr_accessor :reason_phrase
118
+
119
+ # Used for dumping response.
120
+ attr_accessor :body_type # :nodoc:
121
+ # Used for dumping response.
122
+ attr_accessor :body_charset # :nodoc:
123
+ # Used for dumping response.
124
+ attr_accessor :body_date # :nodoc:
125
+
126
+ # HTTP response status code to reason phrase mapping definition.
127
+ STATUS_CODE_MAP = {
128
+ Status::OK => 'OK',
129
+ Status::CREATED => "Created",
130
+ Status::NON_AUTHORITATIVE_INFORMATION => "Non-Authoritative Information",
131
+ Status::NO_CONTENT => "No Content",
132
+ Status::RESET_CONTENT => "Reset Content",
133
+ Status::PARTIAL_CONTENT => "Partial Content",
134
+ Status::MOVED_PERMANENTLY => 'Moved Permanently',
135
+ Status::FOUND => 'Found',
136
+ Status::SEE_OTHER => 'See Other',
137
+ Status::TEMPORARY_REDIRECT => 'Temporary Redirect',
138
+ Status::MOVED_TEMPORARILY => 'Temporary Redirect',
139
+ Status::BAD_REQUEST => 'Bad Request',
140
+ Status::INTERNAL => 'Internal Server Error',
141
+ }
142
+
143
+ # $KCODE to charset mapping definition.
144
+ CHARSET_MAP = {
145
+ 'NONE' => 'us-ascii',
146
+ 'EUC' => 'euc-jp',
147
+ 'SJIS' => 'shift_jis',
148
+ 'UTF8' => 'utf-8',
149
+ }
150
+
151
+ # Creates a Message::Headers. Use init_request, init_response, or
152
+ # init_connect_request for acutual initialize.
153
+ def initialize
154
+ @http_version = 1.1
155
+ @body_size = nil
156
+ @chunked = false
157
+
158
+ @request_method = nil
159
+ @request_uri = nil
160
+ @request_query = nil
161
+ @request_via_proxy = nil
162
+
163
+ @status_code = nil
164
+ @reason_phrase = nil
165
+
166
+ @body_type = nil
167
+ @body_charset = nil
168
+ @body_date = nil
169
+
170
+ @is_request = nil
171
+ @header_item = []
172
+ @dumped = false
173
+ end
174
+
175
+ # Initialize this instance as a CONNECT request.
176
+ def init_connect_request(uri)
177
+ @is_request = true
178
+ @request_method = 'CONNECT'
179
+ @request_uri = uri
180
+ @request_query = nil
181
+ @http_version = 1.0
182
+ end
183
+
184
+ # Placeholder URI object for nil uri.
185
+ NIL_URI = URI.parse('http://nil-uri-given/')
186
+ # Initialize this instance as a general request.
187
+ def init_request(method, uri, query = nil)
188
+ @is_request = true
189
+ @request_method = method
190
+ @request_uri = uri || NIL_URI
191
+ @request_query = query
192
+ @request_via_proxy = false
193
+ end
194
+
195
+ # Initialize this instance as a response.
196
+ def init_response(status_code)
197
+ @is_request = false
198
+ self.status_code = status_code
199
+ end
200
+
201
+ # Sets status code and reason phrase.
202
+ def status_code=(status_code)
203
+ @status_code = status_code
204
+ @reason_phrase = STATUS_CODE_MAP[@status_code]
205
+ end
206
+
207
+ # Returns 'Content-Type' header value.
208
+ def contenttype
209
+ self['Content-Type'][0]
210
+ end
211
+
212
+ # Sets 'Content-Type' header value. Overrides if already exists.
213
+ def contenttype=(contenttype)
214
+ delete('Content-Type')
215
+ self['Content-Type'] = contenttype
216
+ end
217
+
218
+ # Sets byte size of message body.
219
+ # body_size == nil means that the body is_a? IO
220
+ def body_size=(body_size)
221
+ @body_size = body_size
222
+ end
223
+
224
+ # Dumps message header part and returns a dumped String.
225
+ def dump
226
+ set_header
227
+ str = nil
228
+ if @is_request
229
+ str = request_line
230
+ else
231
+ str = response_status_line
232
+ end
233
+ str + @header_item.collect { |key, value|
234
+ "#{ key }: #{ value }#{ CRLF }"
235
+ }.join
236
+ end
237
+
238
+ # Adds a header. Addition order is preserved.
239
+ def add(key, value)
240
+ if value.is_a?(Array)
241
+ value.each do |v|
242
+ @header_item.push([key, v])
243
+ end
244
+ else
245
+ @header_item.push([key, value])
246
+ end
247
+ end
248
+
249
+ # Sets a header.
250
+ def set(key, value)
251
+ delete(key)
252
+ add(key, value)
253
+ end
254
+
255
+ # Returns an Array of headers for the given key. Each element is a pair
256
+ # of key and value. It returns an single element Array even if the only
257
+ # one header exists. If nil key given, it returns all headers.
258
+ def get(key = nil)
259
+ if key.nil?
260
+ all
261
+ else
262
+ key = key.upcase
263
+ @header_item.find_all { |k, v| k.upcase == key }
264
+ end
265
+ end
266
+
267
+ # Returns an Array of all headers.
268
+ def all
269
+ @header_item
270
+ end
271
+
272
+ # Deletes headers of the given key.
273
+ def delete(key)
274
+ key = key.upcase
275
+ @header_item.delete_if { |k, v| k.upcase == key }
276
+ end
277
+
278
+ # Adds a header. See set.
279
+ def []=(key, value)
280
+ set(key, value)
281
+ end
282
+
283
+ # Returns an Array of header values for the given key.
284
+ def [](key)
285
+ get(key).collect { |item| item[1] }
286
+ end
287
+
288
+ private
289
+
290
+ def request_line
291
+ path = create_query_uri(@request_uri, @request_query)
292
+ if @request_via_proxy
293
+ path = "#{ @request_uri.scheme }://#{ @request_uri.host }:#{ @request_uri.port }#{ path }"
294
+ end
295
+ "#{ @request_method } #{ path } HTTP/#{ @http_version }#{ CRLF }"
296
+ end
297
+
298
+ def response_status_line
299
+ if defined?(Apache)
300
+ "HTTP/#{ @http_version } #{ @status_code } #{ @reason_phrase }#{ CRLF }"
301
+ else
302
+ "Status: #{ @status_code } #{ @reason_phrase }#{ CRLF }"
303
+ end
304
+ end
305
+
306
+ def set_header
307
+ if @is_request
308
+ set_request_header
309
+ else
310
+ set_response_header
311
+ end
312
+ end
313
+
314
+ def set_request_header
315
+ return if @dumped
316
+ @dumped = true
317
+ keep_alive = Message.keep_alive_enabled?(@http_version)
318
+ if !keep_alive and @request_method != 'CONNECT'
319
+ set('Connection', 'close')
320
+ end
321
+ if @chunked
322
+ set('Transfer-Encoding', 'chunked')
323
+ elsif @body_size and (keep_alive or @body_size != 0)
324
+ set('Content-Length', @body_size.to_s)
325
+ end
326
+ if @http_version >= 1.1
327
+ if @request_uri.port == @request_uri.default_port
328
+ # GFE/1.3 dislikes default port number (returns 404)
329
+ set('Host', "#{@request_uri.host}")
330
+ else
331
+ set('Host', "#{@request_uri.host}:#{@request_uri.port}")
332
+ end
333
+ end
334
+ end
335
+
336
+ def set_response_header
337
+ return if @dumped
338
+ @dumped = true
339
+ if defined?(Apache) && self['Date'].empty?
340
+ set('Date', Time.now.httpdate)
341
+ end
342
+ keep_alive = Message.keep_alive_enabled?(@http_version)
343
+ if @chunked
344
+ set('Transfer-Encoding', 'chunked')
345
+ else
346
+ if keep_alive or @body_size != 0
347
+ set('Content-Length', @body_size.to_s)
348
+ end
349
+ end
350
+ if @body_date
351
+ set('Last-Modified', @body_date.httpdate)
352
+ end
353
+ if self['Content-Type'].empty?
354
+ set('Content-Type', "#{ @body_type || 'text/html' }; charset=#{ charset_label(@body_charset || $KCODE) }")
355
+ end
356
+ end
357
+
358
+ def charset_label(charset)
359
+ CHARSET_MAP[charset] || 'us-ascii'
360
+ end
361
+
362
+ def create_query_uri(uri, query)
363
+ if @request_method == 'CONNECT'
364
+ return "#{uri.host}:#{uri.port}"
365
+ end
366
+ path = uri.path
367
+ path = '/' if path.nil? or path.empty?
368
+ query_str = nil
369
+ if uri.query
370
+ query_str = uri.query
371
+ end
372
+ if query
373
+ if query_str
374
+ query_str += "&#{Message.create_query_part_str(query)}"
375
+ else
376
+ query_str = Message.create_query_part_str(query)
377
+ end
378
+ end
379
+ if query_str
380
+ path += "?#{query_str}"
381
+ end
382
+ path
383
+ end
384
+ end
385
+
386
+
387
+ # Represents HTTP message body.
388
+ class Body
389
+ # Size of body. nil when size is unknown (e.g. chunked response).
390
+ attr_reader :size
391
+ # maxbytes of IO#read for streaming request. See DEFAULT_CHUNK_SIZE.
392
+ attr_accessor :chunk_size
393
+
394
+ # Default value for chunk_size
395
+ DEFAULT_CHUNK_SIZE = 1024 * 16
396
+
397
+ # Creates a Message::Body. Use init_request or init_response
398
+ # for acutual initialize.
399
+ def initialize
400
+ @body = nil
401
+ @size = nil
402
+ @positions = nil
403
+ @chunk_size = nil
404
+ end
405
+
406
+ # Initialize this instance as a request.
407
+ def init_request(body = nil, boundary = nil)
408
+ @boundary = boundary
409
+ @positions = {}
410
+ set_content(body, boundary)
411
+ @chunk_size = DEFAULT_CHUNK_SIZE
412
+ end
413
+
414
+ # Initialize this instance as a response.
415
+ def init_response(body = nil)
416
+ @body = body
417
+ if @body.respond_to?(:size)
418
+ @size = @body.size
419
+ else
420
+ @size = nil
421
+ end
422
+ end
423
+
424
+ # Dumps message body to given dev.
425
+ # dev needs to respond to <<.
426
+ #
427
+ # Message header must be given as the first argument for performance
428
+ # reason. (header is dumped to dev, too)
429
+ # If no dev (the second argument) given, this method returns a dumped
430
+ # String.
431
+ def dump(header = '', dev = '')
432
+ if @body.is_a?(Parts)
433
+ dev << header
434
+ buf = ''
435
+ @body.parts.each do |part|
436
+ if Message.file?(part)
437
+ reset_pos(part)
438
+ while !part.read(@chunk_size, buf).nil?
439
+ dev << buf
440
+ end
441
+ else
442
+ dev << part
443
+ end
444
+ end
445
+ elsif @body
446
+ dev << header + @body
447
+ else
448
+ dev << header
449
+ end
450
+ dev
451
+ end
452
+
453
+ # Dumps message body with chunked encoding to given dev.
454
+ # dev needs to respond to <<.
455
+ #
456
+ # Message header must be given as the first argument for performance
457
+ # reason. (header is dumped to dev, too)
458
+ # If no dev (the second argument) given, this method returns a dumped
459
+ # String.
460
+ def dump_chunked(header = '', dev = '')
461
+ dev << header
462
+ if @body.is_a?(Parts)
463
+ @body.parts.each do |part|
464
+ if Message.file?(part)
465
+ reset_pos(part)
466
+ dump_chunks(part, dev)
467
+ else
468
+ dev << dump_chunk(part)
469
+ end
470
+ end
471
+ dev << (dump_last_chunk + CRLF)
472
+ elsif @body
473
+ reset_pos(@body)
474
+ dump_chunks(@body, dev)
475
+ dev << (dump_last_chunk + CRLF)
476
+ end
477
+ dev
478
+ end
479
+
480
+ # Returns a message body itself.
481
+ def content
482
+ @body
483
+ end
484
+
485
+ private
486
+
487
+ def set_content(body, boundary = nil)
488
+ if body.respond_to?(:read)
489
+ # uses Transfer-Encoding: chunked. bear in mind that server may not
490
+ # support it. at least ruby's CGI doesn't.
491
+ @body = body
492
+ remember_pos(@body)
493
+ @size = nil
494
+ elsif boundary and Message.multiparam_query?(body)
495
+ @body = build_query_multipart_str(body, boundary)
496
+ @size = @body.size
497
+ else
498
+ @body = Message.create_query_part_str(body)
499
+ @size = @body.size
500
+ end
501
+ end
502
+
503
+ def remember_pos(io)
504
+ # IO may not support it (ex. IO.pipe)
505
+ @positions[io] = io.pos rescue nil
506
+ end
507
+
508
+ def reset_pos(io)
509
+ io.pos = @positions[io] if @positions.key?(io)
510
+ end
511
+
512
+ def dump_chunks(io, dev)
513
+ buf = ''
514
+ while !io.read(@chunk_size, buf).nil?
515
+ dev << dump_chunk(buf)
516
+ end
517
+ end
518
+
519
+ def dump_chunk(str)
520
+ dump_chunk_size(str.size) + (str + CRLF)
521
+ end
522
+
523
+ def dump_last_chunk
524
+ dump_chunk_size(0)
525
+ end
526
+
527
+ def dump_chunk_size(size)
528
+ sprintf("%x", size) + CRLF
529
+ end
530
+
531
+ class Parts
532
+ attr_reader :size
533
+
534
+ def initialize
535
+ @body = []
536
+ @size = 0
537
+ @as_stream = false
538
+ end
539
+
540
+ def add(part)
541
+ if Message.file?(part)
542
+ @as_stream = true
543
+ @body << part
544
+ if part.respond_to?(:size)
545
+ if sz = part.size
546
+ @size += sz
547
+ else
548
+ @size = nil
549
+ end
550
+ elsif part.respond_to?(:lstat)
551
+ @size += part.lstat.size
552
+ else
553
+ # use chunked upload
554
+ @size = nil
555
+ end
556
+ elsif @body[-1].is_a?(String)
557
+ @body[-1] += part.to_s
558
+ @size += part.to_s.size if @size
559
+ else
560
+ @body << part.to_s
561
+ @size += part.to_s.size if @size
562
+ end
563
+ end
564
+
565
+ def parts
566
+ if @as_stream
567
+ @body
568
+ else
569
+ [@body.join]
570
+ end
571
+ end
572
+ end
573
+
574
+ def build_query_multipart_str(query, boundary)
575
+ parts = Parts.new
576
+ query.each do |attr, value|
577
+ value ||= ''
578
+ headers = ["--#{boundary}"]
579
+ if Message.file?(value)
580
+ remember_pos(value)
581
+ param_str = params_from_file(value).collect { |k, v|
582
+ "#{k}=\"#{v}\""
583
+ }.join("; ")
584
+ if value.respond_to?(:mime_type)
585
+ content_type = value.mime_type
586
+ else
587
+ content_type = Message.mime_type(value.path)
588
+ end
589
+ headers << %{Content-Disposition: form-data; name="#{attr}"; #{param_str}}
590
+ headers << %{Content-Type: #{content_type}}
591
+ else
592
+ headers << %{Content-Disposition: form-data; name="#{attr}"}
593
+ end
594
+ parts.add(headers.join(CRLF) + CRLF + CRLF)
595
+ parts.add(value)
596
+ parts.add(CRLF)
597
+ end
598
+ parts.add("--#{boundary}--" + CRLF + CRLF) # empty epilogue
599
+ parts
600
+ end
601
+
602
+ def params_from_file(value)
603
+ params = {}
604
+ params['filename'] = File.basename(value.path || '')
605
+ # Creation time is not available from File::Stat
606
+ if value.respond_to?(:mtime)
607
+ params['modification-date'] = value.mtime.rfc822
608
+ end
609
+ if value.respond_to?(:atime)
610
+ params['read-date'] = value.atime.rfc822
611
+ end
612
+ params
613
+ end
614
+ end
615
+
616
+
617
+ class << self
618
+ private :new
619
+
620
+ # Creates a Message instance of 'CONNECT' request.
621
+ # 'CONNECT' request does not have Body.
622
+ # uri:: an URI that need to connect. Only uri.host and uri.port are used.
623
+ def new_connect_request(uri)
624
+ m = new
625
+ m.header.init_connect_request(uri)
626
+ m.header.body_size = nil
627
+ m
628
+ end
629
+
630
+ # Creates a Message instance of general request.
631
+ # method:: HTTP method String.
632
+ # uri:: an URI object which represents an URL of web resource.
633
+ # query:: a Hash or an Array of query part of URL.
634
+ # e.g. { "a" => "b" } => 'http://host/part?a=b'
635
+ # Give an array to pass multiple value like
636
+ # [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'
637
+ # body:: a Hash or an Array of body part.
638
+ # e.g. { "a" => "b" } => 'a=b'.
639
+ # Give an array to pass multiple value like
640
+ # [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
641
+ # boundary:: When the boundary given, it is sent as
642
+ # a multipart/form-data using this boundary String.
643
+ def new_request(method, uri, query = nil, body = nil, boundary = nil)
644
+ m = new
645
+ m.header.init_request(method, uri, query)
646
+ m.body = Body.new
647
+ m.body.init_request(body || '', boundary)
648
+ if body
649
+ m.header.body_size = m.body.size
650
+ m.header.chunked = true if m.body.size.nil?
651
+ else
652
+ m.header.body_size = nil
653
+ end
654
+ m
655
+ end
656
+
657
+ # Creates a Message instance of response.
658
+ # body:: a String or an IO of response message body.
659
+ def new_response(body)
660
+ m = new
661
+ m.header.init_response(Status::OK)
662
+ m.body = Body.new
663
+ m.body.init_response(body)
664
+ m.header.body_size = m.body.size || 0
665
+ m
666
+ end
667
+
668
+ @@mime_type_handler = nil
669
+
670
+ # Sets MIME type handler.
671
+ #
672
+ # handler must respond to :call with a single argument :path and returns
673
+ # a MIME type String e.g. 'text/html'.
674
+ # When the handler returns nil or an empty String,
675
+ # 'application/octet-stream' is used.
676
+ #
677
+ # When you set nil to the handler, internal_mime_type is used instead.
678
+ # The handler is nil by default.
679
+ def mime_type_handler=(handler)
680
+ @@mime_type_handler = handler
681
+ end
682
+
683
+ # Returns MIME type handler.
684
+ def mime_type_handler
685
+ @@mime_type_handler
686
+ end
687
+
688
+ # For backward compatibility.
689
+ alias set_mime_type_func mime_type_handler=
690
+ alias get_mime_type_func mime_type_handler
691
+
692
+ def mime_type(path) # :nodoc:
693
+ if @@mime_type_handler
694
+ res = @@mime_type_handler.call(path)
695
+ if !res || res.to_s == ''
696
+ return 'application/octet-stream'
697
+ else
698
+ return res
699
+ end
700
+ else
701
+ internal_mime_type(path)
702
+ end
703
+ end
704
+
705
+ # Default MIME type handler.
706
+ # See mime_type_handler=.
707
+ def internal_mime_type(path)
708
+ case path
709
+ when /\.txt$/i
710
+ 'text/plain'
711
+ when /\.(htm|html)$/i
712
+ 'text/html'
713
+ when /\.doc$/i
714
+ 'application/msword'
715
+ when /\.png$/i
716
+ 'image/png'
717
+ when /\.gif$/i
718
+ 'image/gif'
719
+ when /\.(jpg|jpeg)$/i
720
+ 'image/jpeg'
721
+ else
722
+ 'application/octet-stream'
723
+ end
724
+ end
725
+
726
+ # Returns true if the given HTTP version allows keep alive connection.
727
+ # version:: Float
728
+ def keep_alive_enabled?(version)
729
+ version >= 1.1
730
+ end
731
+
732
+ # Returns true if the given query (or body) has a multiple parameter.
733
+ def multiparam_query?(query)
734
+ query.is_a?(Array) or query.is_a?(Hash)
735
+ end
736
+
737
+ # Returns true if the given object is a File. In HTTPClient, a file is;
738
+ # * must respond to :read for retrieving String chunks.
739
+ # * must respond to :path and returns a path for Content-Disposition.
740
+ # * must respond to :pos and :pos= to rewind for reading.
741
+ # Rewinding is only needed for following HTTP redirect. Some IO impl
742
+ # defines :pos= but raises an Exception for pos= such as StringIO
743
+ # but there's no problem as far as using it for non-following methods
744
+ # (get/post/etc.)
745
+ def file?(obj)
746
+ obj.respond_to?(:read) and obj.respond_to?(:path) and
747
+ obj.respond_to?(:pos) and obj.respond_to?(:pos=)
748
+ end
749
+
750
+ def create_query_part_str(query) # :nodoc:
751
+ if multiparam_query?(query)
752
+ escape_query(query)
753
+ elsif query.respond_to?(:read)
754
+ query = query.read
755
+ else
756
+ query.to_s
757
+ end
758
+ end
759
+
760
+ def escape_query(query) # :nodoc:
761
+ query.collect { |attr, value|
762
+ if value.respond_to?(:read)
763
+ value = value.read
764
+ end
765
+ escape(attr.to_s) << '=' << escape(value.to_s)
766
+ }.join('&')
767
+ end
768
+
769
+ # from CGI.escape
770
+ def escape(str) # :nodoc:
771
+ str.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
772
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
773
+ }.tr(' ', '+')
774
+ end
775
+ end
776
+
777
+
778
+ # HTTP::Message::Headers:: message header.
779
+ attr_accessor :header
780
+
781
+ # HTTP::Message::Body:: message body.
782
+ attr_reader :body
783
+
784
+ # OpenSSL::X509::Certificate:: response only. server certificate which is
785
+ # used for retrieving the response.
786
+ attr_accessor :peer_cert
787
+
788
+ # Creates a Message. This method should be used internally.
789
+ # Use Message.new_connect_request, Message.new_request or
790
+ # Message.new_response instead.
791
+ def initialize # :nodoc:
792
+ @header = Headers.new
793
+ @body = @peer_cert = nil
794
+ end
795
+
796
+ # Dumps message (header and body) to given dev.
797
+ # dev needs to respond to <<.
798
+ def dump(dev = '')
799
+ str = header.dump + CRLF
800
+ if header.chunked
801
+ dev = body.dump_chunked(str, dev)
802
+ elsif body
803
+ dev = body.dump(str, dev)
804
+ else
805
+ dev << str
806
+ end
807
+ dev
808
+ end
809
+
810
+ # Sets a new body. header.body_size is updated with new body.size.
811
+ def body=(body)
812
+ @body = body
813
+ @header.body_size = @body.size if @header
814
+ end
815
+
816
+ # Returns HTTP version in a HTTP header. Float.
817
+ def version
818
+ @header.http_version
819
+ end
820
+
821
+ # Sets HTTP version in a HTTP header. Float.
822
+ def version=(version)
823
+ @header.http_version = version
824
+ end
825
+
826
+ # Returns HTTP status code in response. Integer.
827
+ def status
828
+ @header.status_code
829
+ end
830
+
831
+ alias code status
832
+ alias status_code status
833
+
834
+ # Sets HTTP status code of response. Integer.
835
+ # Reason phrase is updated, too.
836
+ def status=(status)
837
+ @header.status_code = status
838
+ end
839
+
840
+ # Returns HTTP status reason phrase in response. String.
841
+ def reason
842
+ @header.reason_phrase
843
+ end
844
+
845
+ # Sets HTTP status reason phrase of response. String.
846
+ def reason=(reason)
847
+ @header.reason_phrase = reason
848
+ end
849
+
850
+ # Sets 'Content-Type' header value. Overrides if already exists.
851
+ def contenttype
852
+ @header.contenttype
853
+ end
854
+
855
+ # Returns 'Content-Type' header value.
856
+ def contenttype=(contenttype)
857
+ @header.contenttype = contenttype
858
+ end
859
+
860
+ # Returns a content of message body. A String or an IO.
861
+ def content
862
+ @body.content
863
+ end
864
+ end
865
+
866
+
867
+ end