httpclient 2.1.2 → 2.1.3

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