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.
- data/lib/http-access2.rb +1 -2
- data/lib/httpclient.rb +631 -1832
- data/lib/httpclient/auth.rb +510 -0
- data/lib/httpclient/connection.rb +84 -0
- data/lib/httpclient/cookie.rb +82 -71
- data/lib/httpclient/http.rb +726 -484
- data/lib/httpclient/session.rb +855 -0
- data/lib/httpclient/ssl_config.rb +379 -0
- data/lib/httpclient/timeout.rb +122 -0
- data/lib/httpclient/util.rb +86 -0
- metadata +50 -37
@@ -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
|
data/lib/httpclient/cookie.rb
CHANGED
@@ -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
|
-
|
34
|
-
|
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 (
|
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?(
|
44
|
+
return tail_match?(domainname, '.' + hostname)
|
43
45
|
else
|
44
|
-
return (
|
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.
|
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
|
-
|
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
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
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
|
372
|
+
path ||= url.path.sub(%r|/[^/]*|, '')
|
363
373
|
domain ||= domainname
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
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
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
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)
|
data/lib/httpclient/http.rb
CHANGED
@@ -1,604 +1,846 @@
|
|
1
|
-
#
|
2
|
-
# Copyright (C)
|
1
|
+
# HTTPClient - HTTP client library.
|
2
|
+
# Copyright (C) 2000-2008 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
|
3
3
|
#
|
4
|
-
# This
|
5
|
-
#
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
]
|
36
|
-
end
|
40
|
+
]
|
37
41
|
|
38
|
-
|
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
|
-
]
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
|
47
|
-
class Error < StandardError; end
|
48
|
-
class BadResponseError < Error; end
|
46
|
+
]
|
49
47
|
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
#
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
180
|
-
|
181
|
-
|
207
|
+
# Returns 'Content-Type' header value.
|
208
|
+
def contenttype
|
209
|
+
self['Content-Type'][0]
|
210
|
+
end
|
182
211
|
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
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
|
-
|
213
|
-
|
214
|
-
|
238
|
+
# Adds a header. Addition order is preserved.
|
239
|
+
def set(key, value)
|
240
|
+
@header_item.push([key, value])
|
241
|
+
end
|
215
242
|
|
216
|
-
|
217
|
-
if
|
218
|
-
|
219
|
-
|
220
|
-
|
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
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
255
|
+
# Returns an Array of all headers.
|
256
|
+
def all
|
257
|
+
@header_item
|
258
|
+
end
|
228
259
|
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
234
|
-
|
235
|
-
|
266
|
+
# Adds a header. See set.
|
267
|
+
def []=(key, value)
|
268
|
+
set(key, value)
|
269
|
+
end
|
236
270
|
|
237
|
-
|
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
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
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
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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
|
-
|
261
|
-
|
262
|
-
|
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
|
-
|
266
|
-
|
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
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
-
|
277
|
-
|
341
|
+
def charset_label(charset)
|
342
|
+
CHARSET_MAP[charset] || 'us-ascii'
|
278
343
|
end
|
279
344
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
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
|
-
|
359
|
+
query_str = Message.create_query_part_str(query)
|
286
360
|
end
|
287
|
-
|
288
|
-
|
289
|
-
|
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
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
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
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
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
|
-
|
312
|
-
|
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
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
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
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
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
|
-
|
341
|
-
|
342
|
-
|
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
|
-
|
358
|
-
|
359
|
-
|
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
|
-
|
362
|
-
|
363
|
-
|
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
|
-
|
491
|
+
def reset_pos(io)
|
492
|
+
io.pos = @positions[io] if @positions.key?(io)
|
493
|
+
end
|
372
494
|
|
373
|
-
|
374
|
-
|
375
|
-
|
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
|
-
|
378
|
-
|
379
|
-
|
502
|
+
def dump_chunk(str)
|
503
|
+
dump_chunk_size(str.size) + (str + CRLF)
|
504
|
+
end
|
380
505
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
end
|
506
|
+
def dump_last_chunk
|
507
|
+
dump_chunk_size(0)
|
508
|
+
end
|
385
509
|
|
386
|
-
|
387
|
-
|
388
|
-
|
510
|
+
def dump_chunk_size(size)
|
511
|
+
sprintf("%x", size) + CRLF
|
512
|
+
end
|
389
513
|
|
390
|
-
|
391
|
-
|
392
|
-
end
|
514
|
+
class Parts
|
515
|
+
attr_reader :size
|
393
516
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
517
|
+
def initialize
|
518
|
+
@body = []
|
519
|
+
@size = 0
|
520
|
+
@as_stream = false
|
521
|
+
end
|
398
522
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
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
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
548
|
+
def parts
|
549
|
+
if @as_stream
|
550
|
+
@body
|
551
|
+
else
|
552
|
+
[@body.join]
|
553
|
+
end
|
554
|
+
end
|
555
|
+
end
|
415
556
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
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
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
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
|
-
|
437
|
-
|
438
|
-
end
|
600
|
+
class << self
|
601
|
+
private :new
|
439
602
|
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
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
|
-
|
446
|
-
|
447
|
-
|
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
|
-
|
450
|
-
|
451
|
-
|
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
|
-
|
454
|
-
|
455
|
-
|
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
|
-
|
458
|
-
|
459
|
-
|
662
|
+
# Returns MIME type handler.
|
663
|
+
def mime_type_handler
|
664
|
+
@@mime_type_handler
|
665
|
+
end
|
460
666
|
|
461
|
-
|
462
|
-
|
463
|
-
|
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
|
-
|
466
|
-
|
467
|
-
|
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
|
-
|
470
|
-
|
471
|
-
|
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
|
-
|
474
|
-
|
475
|
-
|
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
|
-
|
478
|
-
|
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
|
-
|
481
|
-
|
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
|
-
|
485
|
-
|
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
|
-
|
489
|
-
|
490
|
-
|
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
|
-
|
784
|
+
dev << str
|
493
785
|
end
|
786
|
+
dev
|
494
787
|
end
|
495
788
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
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
|
-
|
533
|
-
|
795
|
+
# Returns HTTP version in a HTTP header. Float.
|
796
|
+
def version
|
797
|
+
@header.http_version
|
534
798
|
end
|
535
799
|
|
536
|
-
|
537
|
-
|
538
|
-
|
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
|
-
#
|
543
|
-
def
|
544
|
-
|
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
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
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
|
-
|
563
|
-
|
564
|
-
|
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
|
-
|
824
|
+
# Sets HTTP status reason phrase of response. String.
|
825
|
+
def reason=(reason)
|
826
|
+
@header.reason_phrase = reason
|
827
|
+
end
|
583
828
|
|
584
|
-
|
585
|
-
|
586
|
-
@header.
|
587
|
-
|
588
|
-
|
589
|
-
|
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
|
-
|
594
|
-
|
595
|
-
@body.
|
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
|