httpclient 2.1.2 → 2.1.3

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.
@@ -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