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