httpclient-jgraichen 2.3.4.2
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.
- checksums.yaml +7 -0
- data/README.txt +759 -0
- data/bin/httpclient +65 -0
- data/lib/hexdump.rb +50 -0
- data/lib/http-access2.rb +55 -0
- data/lib/http-access2/cookie.rb +1 -0
- data/lib/http-access2/http.rb +1 -0
- data/lib/httpclient.rb +1156 -0
- data/lib/httpclient/auth.rb +899 -0
- data/lib/httpclient/cacert.p7s +1912 -0
- data/lib/httpclient/connection.rb +88 -0
- data/lib/httpclient/cookie.rb +438 -0
- data/lib/httpclient/http.rb +1046 -0
- data/lib/httpclient/include_client.rb +83 -0
- data/lib/httpclient/session.rb +1028 -0
- data/lib/httpclient/ssl_config.rb +405 -0
- data/lib/httpclient/timeout.rb +140 -0
- data/lib/httpclient/util.rb +178 -0
- data/lib/httpclient/version.rb +3 -0
- data/lib/oauthclient.rb +110 -0
- data/sample/async.rb +8 -0
- data/sample/auth.rb +11 -0
- data/sample/cookie.rb +18 -0
- data/sample/dav.rb +103 -0
- data/sample/howto.rb +49 -0
- data/sample/oauth_buzz.rb +57 -0
- data/sample/oauth_friendfeed.rb +59 -0
- data/sample/oauth_twitter.rb +61 -0
- data/sample/ssl/0cert.pem +22 -0
- data/sample/ssl/0key.pem +30 -0
- data/sample/ssl/1000cert.pem +19 -0
- data/sample/ssl/1000key.pem +18 -0
- data/sample/ssl/htdocs/index.html +10 -0
- data/sample/ssl/ssl_client.rb +22 -0
- data/sample/ssl/webrick_httpsd.rb +29 -0
- data/sample/stream.rb +21 -0
- data/sample/thread.rb +27 -0
- data/sample/wcat.rb +21 -0
- data/test/ca-chain.cert +44 -0
- data/test/ca.cert +23 -0
- data/test/client.cert +19 -0
- data/test/client.key +15 -0
- data/test/helper.rb +129 -0
- data/test/htdigest +1 -0
- data/test/htpasswd +2 -0
- data/test/runner.rb +2 -0
- data/test/server.cert +19 -0
- data/test/server.key +15 -0
- data/test/sslsvr.rb +65 -0
- data/test/subca.cert +21 -0
- data/test/test_auth.rb +348 -0
- data/test/test_cookie.rb +412 -0
- data/test/test_hexdump.rb +14 -0
- data/test/test_http-access2.rb +507 -0
- data/test/test_httpclient.rb +1783 -0
- data/test/test_include_client.rb +52 -0
- data/test/test_ssl.rb +235 -0
- metadata +100 -0
@@ -0,0 +1,1046 @@
|
|
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
|
+
if defined?(Encoding::ASCII_8BIT)
|
11
|
+
require 'open-uri' # for encoding
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# A namespace module for HTTP Message definitions used by HTTPClient.
|
16
|
+
module HTTP
|
17
|
+
|
18
|
+
|
19
|
+
# Represents HTTP response status code. Defines constants for HTTP response
|
20
|
+
# and some conditional methods.
|
21
|
+
module Status
|
22
|
+
OK = 200
|
23
|
+
CREATED = 201
|
24
|
+
ACCEPTED = 202
|
25
|
+
NON_AUTHORITATIVE_INFORMATION = 203
|
26
|
+
NO_CONTENT = 204
|
27
|
+
RESET_CONTENT = 205
|
28
|
+
PARTIAL_CONTENT = 206
|
29
|
+
MOVED_PERMANENTLY = 301
|
30
|
+
FOUND = 302
|
31
|
+
SEE_OTHER = 303
|
32
|
+
TEMPORARY_REDIRECT = MOVED_TEMPORARILY = 307
|
33
|
+
BAD_REQUEST = 400
|
34
|
+
UNAUTHORIZED = 401
|
35
|
+
PROXY_AUTHENTICATE_REQUIRED = 407
|
36
|
+
INTERNAL = 500
|
37
|
+
|
38
|
+
# Status codes for successful HTTP response.
|
39
|
+
SUCCESSFUL_STATUS = [
|
40
|
+
OK, CREATED, ACCEPTED,
|
41
|
+
NON_AUTHORITATIVE_INFORMATION, NO_CONTENT,
|
42
|
+
RESET_CONTENT, PARTIAL_CONTENT
|
43
|
+
]
|
44
|
+
|
45
|
+
# Status codes which is a redirect.
|
46
|
+
REDIRECT_STATUS = [
|
47
|
+
MOVED_PERMANENTLY, FOUND, SEE_OTHER,
|
48
|
+
TEMPORARY_REDIRECT, MOVED_TEMPORARILY
|
49
|
+
]
|
50
|
+
|
51
|
+
# Returns true if the given status represents successful HTTP response.
|
52
|
+
# See also SUCCESSFUL_STATUS.
|
53
|
+
def self.successful?(status)
|
54
|
+
SUCCESSFUL_STATUS.include?(status)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns true if the given status is thought to be redirect.
|
58
|
+
# See also REDIRECT_STATUS.
|
59
|
+
def self.redirect?(status)
|
60
|
+
REDIRECT_STATUS.include?(status)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# Represents a HTTP message. A message is for a request or a response.
|
66
|
+
#
|
67
|
+
# Request message is generated from given parameters internally so users
|
68
|
+
# don't need to care about it. Response message is the instance that
|
69
|
+
# methods of HTTPClient returns so users need to know how to extract
|
70
|
+
# HTTP response data from Message.
|
71
|
+
#
|
72
|
+
# Some attributes are only for a request or a response, not both.
|
73
|
+
#
|
74
|
+
# == How to use HTTP response message
|
75
|
+
#
|
76
|
+
# 1. Gets response message body.
|
77
|
+
#
|
78
|
+
# res = clnt.get(url)
|
79
|
+
# p res.content #=> String
|
80
|
+
#
|
81
|
+
# 2. Gets response status code.
|
82
|
+
#
|
83
|
+
# res = clnt.get(url)
|
84
|
+
# p res.status #=> 200, 501, etc. (Integer)
|
85
|
+
#
|
86
|
+
# 3. Gets response header.
|
87
|
+
#
|
88
|
+
# res = clnt.get(url)
|
89
|
+
# res.header['set-cookie'].each do |value|
|
90
|
+
# p value
|
91
|
+
# end
|
92
|
+
# assert_equal(1, res.header['last-modified'].size)
|
93
|
+
# p res.header['last-modified'].first
|
94
|
+
#
|
95
|
+
class Message
|
96
|
+
|
97
|
+
CRLF = "\r\n"
|
98
|
+
|
99
|
+
# Represents HTTP message header.
|
100
|
+
class Headers
|
101
|
+
# HTTP version in a HTTP header. String.
|
102
|
+
attr_accessor :http_version
|
103
|
+
# Size of body. nil when size is unknown (e.g. chunked response).
|
104
|
+
attr_reader :body_size
|
105
|
+
# Request/Response is chunked or not.
|
106
|
+
attr_accessor :chunked
|
107
|
+
|
108
|
+
# Request only. Requested method.
|
109
|
+
attr_reader :request_method
|
110
|
+
# Request only. Requested URI.
|
111
|
+
attr_accessor :request_uri
|
112
|
+
# Request only. Requested query.
|
113
|
+
attr_accessor :request_query
|
114
|
+
# Request only. Requested via proxy or not.
|
115
|
+
attr_accessor :request_absolute_uri
|
116
|
+
|
117
|
+
# Response only. HTTP status
|
118
|
+
attr_reader :status_code
|
119
|
+
# Response only. HTTP status reason phrase.
|
120
|
+
attr_accessor :reason_phrase
|
121
|
+
|
122
|
+
# Used for dumping response.
|
123
|
+
attr_accessor :body_type # :nodoc:
|
124
|
+
# Used for dumping response.
|
125
|
+
attr_accessor :body_charset # :nodoc:
|
126
|
+
# Used for dumping response.
|
127
|
+
attr_accessor :body_date # :nodoc:
|
128
|
+
# Used for keeping content encoding.
|
129
|
+
attr_reader :body_encoding # :nodoc:
|
130
|
+
|
131
|
+
# HTTP response status code to reason phrase mapping definition.
|
132
|
+
STATUS_CODE_MAP = {
|
133
|
+
Status::OK => 'OK',
|
134
|
+
Status::CREATED => "Created",
|
135
|
+
Status::NON_AUTHORITATIVE_INFORMATION => "Non-Authoritative Information",
|
136
|
+
Status::NO_CONTENT => "No Content",
|
137
|
+
Status::RESET_CONTENT => "Reset Content",
|
138
|
+
Status::PARTIAL_CONTENT => "Partial Content",
|
139
|
+
Status::MOVED_PERMANENTLY => 'Moved Permanently',
|
140
|
+
Status::FOUND => 'Found',
|
141
|
+
Status::SEE_OTHER => 'See Other',
|
142
|
+
Status::TEMPORARY_REDIRECT => 'Temporary Redirect',
|
143
|
+
Status::MOVED_TEMPORARILY => 'Temporary Redirect',
|
144
|
+
Status::BAD_REQUEST => 'Bad Request',
|
145
|
+
Status::INTERNAL => 'Internal Server Error',
|
146
|
+
}
|
147
|
+
|
148
|
+
# $KCODE to charset mapping definition.
|
149
|
+
CHARSET_MAP = {
|
150
|
+
'NONE' => 'us-ascii',
|
151
|
+
'EUC' => 'euc-jp',
|
152
|
+
'SJIS' => 'shift_jis',
|
153
|
+
'UTF8' => 'utf-8',
|
154
|
+
}
|
155
|
+
|
156
|
+
# Creates a Message::Headers. Use init_request, init_response, or
|
157
|
+
# init_connect_request for acutual initialize.
|
158
|
+
def initialize
|
159
|
+
@http_version = '1.1'
|
160
|
+
@body_size = nil
|
161
|
+
@chunked = false
|
162
|
+
|
163
|
+
@request_method = nil
|
164
|
+
@request_uri = nil
|
165
|
+
@request_query = nil
|
166
|
+
@request_absolute_uri = nil
|
167
|
+
|
168
|
+
@status_code = nil
|
169
|
+
@reason_phrase = nil
|
170
|
+
|
171
|
+
@body_type = nil
|
172
|
+
@body_charset = nil
|
173
|
+
@body_date = nil
|
174
|
+
@body_encoding = nil
|
175
|
+
|
176
|
+
@is_request = nil
|
177
|
+
@header_item = []
|
178
|
+
@dumped = false
|
179
|
+
end
|
180
|
+
|
181
|
+
# Initialize this instance as a CONNECT request.
|
182
|
+
def init_connect_request(uri)
|
183
|
+
@is_request = true
|
184
|
+
@request_method = 'CONNECT'
|
185
|
+
@request_uri = uri
|
186
|
+
@request_query = nil
|
187
|
+
@http_version = '1.0'
|
188
|
+
end
|
189
|
+
|
190
|
+
# Placeholder URI object for nil uri.
|
191
|
+
NIL_URI = HTTPClient::Util.urify('http://nil-uri-given/')
|
192
|
+
# Initialize this instance as a general request.
|
193
|
+
def init_request(method, uri, query = nil)
|
194
|
+
@is_request = true
|
195
|
+
@request_method = method
|
196
|
+
@request_uri = uri || NIL_URI
|
197
|
+
@request_query = query
|
198
|
+
@request_absolute_uri = false
|
199
|
+
end
|
200
|
+
|
201
|
+
# Initialize this instance as a response.
|
202
|
+
def init_response(status_code, req = nil)
|
203
|
+
@is_request = false
|
204
|
+
self.status_code = status_code
|
205
|
+
if req
|
206
|
+
@request_method = req.request_method
|
207
|
+
@request_uri = req.request_uri
|
208
|
+
@request_query = req.request_query
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Sets status code and reason phrase.
|
213
|
+
def status_code=(status_code)
|
214
|
+
@status_code = status_code
|
215
|
+
@reason_phrase = STATUS_CODE_MAP[@status_code]
|
216
|
+
end
|
217
|
+
|
218
|
+
# Returns 'Content-Type' header value.
|
219
|
+
def content_type
|
220
|
+
self['Content-Type'][0]
|
221
|
+
end
|
222
|
+
|
223
|
+
# Sets 'Content-Type' header value. Overrides if already exists.
|
224
|
+
def content_type=(content_type)
|
225
|
+
delete('Content-Type')
|
226
|
+
self['Content-Type'] = content_type
|
227
|
+
end
|
228
|
+
|
229
|
+
alias contenttype content_type
|
230
|
+
alias contenttype= content_type=
|
231
|
+
|
232
|
+
if defined?(Encoding::ASCII_8BIT)
|
233
|
+
def set_body_encoding
|
234
|
+
if type = self.content_type
|
235
|
+
OpenURI::Meta.init(o = '')
|
236
|
+
o.meta_add_field('content-type', type)
|
237
|
+
@body_encoding = o.encoding
|
238
|
+
end
|
239
|
+
end
|
240
|
+
else
|
241
|
+
def set_body_encoding
|
242
|
+
@body_encoding = nil
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Sets byte size of message body.
|
247
|
+
# body_size == nil means that the body is_a? IO
|
248
|
+
def body_size=(body_size)
|
249
|
+
@body_size = body_size
|
250
|
+
end
|
251
|
+
|
252
|
+
# Dumps message header part and returns a dumped String.
|
253
|
+
def dump
|
254
|
+
set_header
|
255
|
+
str = nil
|
256
|
+
if @is_request
|
257
|
+
str = request_line
|
258
|
+
else
|
259
|
+
str = response_status_line
|
260
|
+
end
|
261
|
+
str + @header_item.collect { |key, value|
|
262
|
+
"#{ key }: #{ value }#{ CRLF }"
|
263
|
+
}.join
|
264
|
+
end
|
265
|
+
|
266
|
+
# Set Date header
|
267
|
+
def set_date_header
|
268
|
+
set('Date', Time.now.httpdate)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Adds a header. Addition order is preserved.
|
272
|
+
def add(key, value)
|
273
|
+
if value.is_a?(Array)
|
274
|
+
value.each do |v|
|
275
|
+
@header_item.push([key, v])
|
276
|
+
end
|
277
|
+
else
|
278
|
+
@header_item.push([key, value])
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Sets a header.
|
283
|
+
def set(key, value)
|
284
|
+
delete(key)
|
285
|
+
add(key, value)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Returns an Array of headers for the given key. Each element is a pair
|
289
|
+
# of key and value. It returns an single element Array even if the only
|
290
|
+
# one header exists. If nil key given, it returns all headers.
|
291
|
+
def get(key = nil)
|
292
|
+
if key.nil?
|
293
|
+
all
|
294
|
+
else
|
295
|
+
key = key.upcase
|
296
|
+
@header_item.find_all { |k, v| k.upcase == key }
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns an Array of all headers.
|
301
|
+
def all
|
302
|
+
@header_item
|
303
|
+
end
|
304
|
+
|
305
|
+
# Deletes headers of the given key.
|
306
|
+
def delete(key)
|
307
|
+
key = key.upcase
|
308
|
+
@header_item.delete_if { |k, v| k.upcase == key }
|
309
|
+
end
|
310
|
+
|
311
|
+
# Adds a header. See set.
|
312
|
+
def []=(key, value)
|
313
|
+
set(key, value)
|
314
|
+
end
|
315
|
+
|
316
|
+
# Returns an Array of header values for the given key.
|
317
|
+
def [](key)
|
318
|
+
get(key).collect { |item| item[1] }
|
319
|
+
end
|
320
|
+
|
321
|
+
def set_headers(headers)
|
322
|
+
headers.each do |key, value|
|
323
|
+
add(key, value)
|
324
|
+
end
|
325
|
+
set_body_encoding
|
326
|
+
end
|
327
|
+
|
328
|
+
def create_query_uri()
|
329
|
+
if @request_method == 'CONNECT'
|
330
|
+
return "#{@request_uri.host}:#{@request_uri.port}"
|
331
|
+
end
|
332
|
+
path = @request_uri.path
|
333
|
+
path = '/' if path.nil? or path.empty?
|
334
|
+
if query_str = create_query_part()
|
335
|
+
path += "?#{query_str}"
|
336
|
+
end
|
337
|
+
path
|
338
|
+
end
|
339
|
+
|
340
|
+
def create_query_part()
|
341
|
+
query_str = nil
|
342
|
+
if @request_uri.query
|
343
|
+
query_str = @request_uri.query
|
344
|
+
end
|
345
|
+
if @request_query
|
346
|
+
if query_str
|
347
|
+
query_str += "&#{Message.create_query_part_str(@request_query)}"
|
348
|
+
else
|
349
|
+
query_str = Message.create_query_part_str(@request_query)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
query_str
|
353
|
+
end
|
354
|
+
|
355
|
+
private
|
356
|
+
|
357
|
+
def request_line
|
358
|
+
path = create_query_uri()
|
359
|
+
if @request_absolute_uri
|
360
|
+
path = "#{ @request_uri.scheme }://#{ @request_uri.host }:#{ @request_uri.port }#{ path }"
|
361
|
+
end
|
362
|
+
"#{ @request_method } #{ path } HTTP/#{ @http_version }#{ CRLF }"
|
363
|
+
end
|
364
|
+
|
365
|
+
def response_status_line
|
366
|
+
if defined?(Apache)
|
367
|
+
"HTTP/#{ @http_version } #{ @status_code } #{ @reason_phrase }#{ CRLF }"
|
368
|
+
else
|
369
|
+
"Status: #{ @status_code } #{ @reason_phrase }#{ CRLF }"
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def set_header
|
374
|
+
if @is_request
|
375
|
+
set_request_header
|
376
|
+
else
|
377
|
+
set_response_header
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def set_request_header
|
382
|
+
return if @dumped
|
383
|
+
@dumped = true
|
384
|
+
keep_alive = Message.keep_alive_enabled?(@http_version)
|
385
|
+
if !keep_alive and @request_method != 'CONNECT'
|
386
|
+
set('Connection', 'close')
|
387
|
+
end
|
388
|
+
if @chunked
|
389
|
+
set('Transfer-Encoding', 'chunked')
|
390
|
+
elsif @body_size and (keep_alive or @body_size != 0)
|
391
|
+
set('Content-Length', @body_size.to_s)
|
392
|
+
end
|
393
|
+
if @http_version >= '1.1' and get('Host').empty?
|
394
|
+
if @request_uri.port == @request_uri.default_port
|
395
|
+
# GFE/1.3 dislikes default port number (returns 404)
|
396
|
+
set('Host', "#{@request_uri.hostname}")
|
397
|
+
else
|
398
|
+
set('Host', "#{@request_uri.hostname}:#{@request_uri.port}")
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
def set_response_header
|
404
|
+
return if @dumped
|
405
|
+
@dumped = true
|
406
|
+
if defined?(Apache) && self['Date'].empty?
|
407
|
+
set_date_header
|
408
|
+
end
|
409
|
+
keep_alive = Message.keep_alive_enabled?(@http_version)
|
410
|
+
if @chunked
|
411
|
+
set('Transfer-Encoding', 'chunked')
|
412
|
+
else
|
413
|
+
if keep_alive or @body_size != 0
|
414
|
+
set('Content-Length', @body_size.to_s)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
if @body_date
|
418
|
+
set('Last-Modified', @body_date.httpdate)
|
419
|
+
end
|
420
|
+
if self['Content-Type'].empty?
|
421
|
+
set('Content-Type', "#{ @body_type || 'text/html' }; charset=#{ charset_label }")
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def charset_label
|
426
|
+
# TODO: should handle response encoding for 1.9 correctly.
|
427
|
+
if RUBY_VERSION > "1.9"
|
428
|
+
CHARSET_MAP[@body_charset] || 'us-ascii'
|
429
|
+
else
|
430
|
+
CHARSET_MAP[@body_charset || $KCODE] || 'us-ascii'
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
|
436
|
+
# Represents HTTP message body.
|
437
|
+
class Body
|
438
|
+
# Size of body. nil when size is unknown (e.g. chunked response).
|
439
|
+
attr_reader :size
|
440
|
+
# maxbytes of IO#read for streaming request. See DEFAULT_CHUNK_SIZE.
|
441
|
+
attr_accessor :chunk_size
|
442
|
+
|
443
|
+
# Default value for chunk_size
|
444
|
+
DEFAULT_CHUNK_SIZE = 1024 * 16
|
445
|
+
|
446
|
+
# Creates a Message::Body. Use init_request or init_response
|
447
|
+
# for acutual initialize.
|
448
|
+
def initialize
|
449
|
+
@body = nil
|
450
|
+
@size = nil
|
451
|
+
@positions = nil
|
452
|
+
@chunk_size = nil
|
453
|
+
end
|
454
|
+
|
455
|
+
# Initialize this instance as a request.
|
456
|
+
def init_request(body = nil, boundary = nil)
|
457
|
+
@boundary = boundary
|
458
|
+
@positions = {}
|
459
|
+
set_content(body, boundary)
|
460
|
+
@chunk_size = DEFAULT_CHUNK_SIZE
|
461
|
+
end
|
462
|
+
|
463
|
+
# Initialize this instance as a response.
|
464
|
+
def init_response(body = nil)
|
465
|
+
@body = body
|
466
|
+
if @body.respond_to?(:bytesize)
|
467
|
+
@size = @body.bytesize
|
468
|
+
elsif @body.respond_to?(:size)
|
469
|
+
@size = @body.size
|
470
|
+
else
|
471
|
+
@size = nil
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
# Dumps message body to given dev.
|
476
|
+
# dev needs to respond to <<.
|
477
|
+
#
|
478
|
+
# Message header must be given as the first argument for performance
|
479
|
+
# reason. (header is dumped to dev, too)
|
480
|
+
# If no dev (the second argument) given, this method returns a dumped
|
481
|
+
# String.
|
482
|
+
def dump(header = '', dev = '')
|
483
|
+
if @body.is_a?(Parts)
|
484
|
+
dev << header
|
485
|
+
@body.parts.each do |part|
|
486
|
+
if Message.file?(part)
|
487
|
+
reset_pos(part)
|
488
|
+
dump_file(part, dev)
|
489
|
+
else
|
490
|
+
dev << part
|
491
|
+
end
|
492
|
+
end
|
493
|
+
elsif Message.file?(@body)
|
494
|
+
dev << header
|
495
|
+
reset_pos(@body)
|
496
|
+
dump_file(@body, dev)
|
497
|
+
elsif @body
|
498
|
+
dev << header + @body
|
499
|
+
else
|
500
|
+
dev << header
|
501
|
+
end
|
502
|
+
dev
|
503
|
+
end
|
504
|
+
|
505
|
+
# Dumps message body with chunked encoding to given dev.
|
506
|
+
# dev needs to respond to <<.
|
507
|
+
#
|
508
|
+
# Message header must be given as the first argument for performance
|
509
|
+
# reason. (header is dumped to dev, too)
|
510
|
+
# If no dev (the second argument) given, this method returns a dumped
|
511
|
+
# String.
|
512
|
+
def dump_chunked(header = '', dev = '')
|
513
|
+
dev << header
|
514
|
+
if @body.is_a?(Parts)
|
515
|
+
@body.parts.each do |part|
|
516
|
+
if Message.file?(part)
|
517
|
+
reset_pos(part)
|
518
|
+
dump_chunks(part, dev)
|
519
|
+
else
|
520
|
+
dev << dump_chunk(part)
|
521
|
+
end
|
522
|
+
end
|
523
|
+
dev << (dump_last_chunk + CRLF)
|
524
|
+
elsif @body
|
525
|
+
reset_pos(@body)
|
526
|
+
dump_chunks(@body, dev)
|
527
|
+
dev << (dump_last_chunk + CRLF)
|
528
|
+
end
|
529
|
+
dev
|
530
|
+
end
|
531
|
+
|
532
|
+
# Returns a message body itself.
|
533
|
+
def content
|
534
|
+
@body
|
535
|
+
end
|
536
|
+
|
537
|
+
private
|
538
|
+
|
539
|
+
def set_content(body, boundary = nil)
|
540
|
+
if Message.file?(body)
|
541
|
+
# uses Transfer-Encoding: chunked if body does not respond to :size.
|
542
|
+
# bear in mind that server may not support it. at least ruby's CGI doesn't.
|
543
|
+
@body = body
|
544
|
+
remember_pos(@body)
|
545
|
+
@size = body.respond_to?(:size) ? body.size - body.pos : nil
|
546
|
+
elsif boundary and Message.multiparam_query?(body)
|
547
|
+
@body = build_query_multipart_str(body, boundary)
|
548
|
+
@size = @body.size
|
549
|
+
else
|
550
|
+
@body = Message.create_query_part_str(body)
|
551
|
+
@size = @body.bytesize
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
def remember_pos(io)
|
556
|
+
# IO may not support it (ex. IO.pipe)
|
557
|
+
@positions[io] = io.pos if io.respond_to?(:pos)
|
558
|
+
end
|
559
|
+
|
560
|
+
def reset_pos(io)
|
561
|
+
io.pos = @positions[io] if @positions.key?(io)
|
562
|
+
end
|
563
|
+
|
564
|
+
def dump_file(io, dev)
|
565
|
+
buf = ''
|
566
|
+
while !io.read(@chunk_size, buf).nil?
|
567
|
+
dev << buf
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
def dump_chunks(io, dev)
|
572
|
+
buf = ''
|
573
|
+
while !io.read(@chunk_size, buf).nil?
|
574
|
+
dev << dump_chunk(buf)
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
def dump_chunk(str)
|
579
|
+
dump_chunk_size(str.bytesize) + (str + CRLF)
|
580
|
+
end
|
581
|
+
|
582
|
+
def dump_last_chunk
|
583
|
+
dump_chunk_size(0)
|
584
|
+
end
|
585
|
+
|
586
|
+
def dump_chunk_size(size)
|
587
|
+
sprintf("%x", size) + CRLF
|
588
|
+
end
|
589
|
+
|
590
|
+
class Parts
|
591
|
+
attr_reader :size
|
592
|
+
|
593
|
+
def initialize
|
594
|
+
@body = []
|
595
|
+
@size = 0
|
596
|
+
@as_stream = false
|
597
|
+
end
|
598
|
+
|
599
|
+
def add(part)
|
600
|
+
if Message.file?(part)
|
601
|
+
@as_stream = true
|
602
|
+
@body << part
|
603
|
+
if part.respond_to?(:lstat)
|
604
|
+
@size += part.lstat.size
|
605
|
+
elsif part.respond_to?(:size)
|
606
|
+
if sz = part.size
|
607
|
+
@size += sz
|
608
|
+
else
|
609
|
+
@size = nil
|
610
|
+
end
|
611
|
+
else
|
612
|
+
# use chunked upload
|
613
|
+
@size = nil
|
614
|
+
end
|
615
|
+
elsif @body[-1].is_a?(String)
|
616
|
+
@body[-1] += part.to_s
|
617
|
+
@size += part.to_s.bytesize if @size
|
618
|
+
else
|
619
|
+
@body << part.to_s
|
620
|
+
@size += part.to_s.bytesize if @size
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
def parts
|
625
|
+
if @as_stream
|
626
|
+
@body
|
627
|
+
else
|
628
|
+
[@body.join]
|
629
|
+
end
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
def build_query_multipart_str(query, boundary)
|
634
|
+
parts = Parts.new
|
635
|
+
query.each do |attr, value|
|
636
|
+
headers = ["--#{boundary}"]
|
637
|
+
if Message.file?(value)
|
638
|
+
remember_pos(value)
|
639
|
+
param_str = params_from_file(value).collect { |k, v|
|
640
|
+
"#{k}=\"#{v}\""
|
641
|
+
}.join("; ")
|
642
|
+
if value.respond_to?(:mime_type)
|
643
|
+
content_type = value.mime_type
|
644
|
+
elsif value.respond_to?(:content_type)
|
645
|
+
content_type = value.content_type
|
646
|
+
else
|
647
|
+
path = value.respond_to?(:path) ? value.path : nil
|
648
|
+
content_type = Message.mime_type(path)
|
649
|
+
end
|
650
|
+
headers << %{Content-Disposition: form-data; name="#{attr}"; #{param_str}}
|
651
|
+
headers << %{Content-Type: #{content_type}}
|
652
|
+
elsif attr.is_a?(Hash)
|
653
|
+
h = attr
|
654
|
+
value = h[:content]
|
655
|
+
h.each do |h_key, h_val|
|
656
|
+
headers << %{#{h_key}: #{h_val}} if h_key != :content
|
657
|
+
end
|
658
|
+
remember_pos(value) if Message.file?(value)
|
659
|
+
else
|
660
|
+
headers << %{Content-Disposition: form-data; name="#{attr}"}
|
661
|
+
value = value.to_s
|
662
|
+
end
|
663
|
+
parts.add(headers.join(CRLF) + CRLF + CRLF)
|
664
|
+
parts.add(value)
|
665
|
+
parts.add(CRLF)
|
666
|
+
end
|
667
|
+
parts.add("--#{boundary}--" + CRLF + CRLF) # empty epilogue
|
668
|
+
parts
|
669
|
+
end
|
670
|
+
|
671
|
+
def params_from_file(value)
|
672
|
+
params = {}
|
673
|
+
path = value.respond_to?(:path) ? value.path : nil
|
674
|
+
params['filename'] = File.basename(path || '')
|
675
|
+
# Creation time is not available from File::Stat
|
676
|
+
if value.respond_to?(:mtime)
|
677
|
+
params['modification-date'] = value.mtime.rfc822
|
678
|
+
end
|
679
|
+
if value.respond_to?(:atime)
|
680
|
+
params['read-date'] = value.atime.rfc822
|
681
|
+
end
|
682
|
+
params
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
|
687
|
+
class << self
|
688
|
+
private :new
|
689
|
+
|
690
|
+
# Creates a Message instance of 'CONNECT' request.
|
691
|
+
# 'CONNECT' request does not have Body.
|
692
|
+
# uri:: an URI that need to connect. Only uri.host and uri.port are used.
|
693
|
+
def new_connect_request(uri)
|
694
|
+
m = new
|
695
|
+
m.http_header.init_connect_request(uri)
|
696
|
+
m.http_header.body_size = nil
|
697
|
+
m
|
698
|
+
end
|
699
|
+
|
700
|
+
# Creates a Message instance of general request.
|
701
|
+
# method:: HTTP method String.
|
702
|
+
# uri:: an URI object which represents an URL of web resource.
|
703
|
+
# query:: a Hash or an Array of query part of URL.
|
704
|
+
# e.g. { "a" => "b" } => 'http://host/part?a=b'
|
705
|
+
# Give an array to pass multiple value like
|
706
|
+
# [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'
|
707
|
+
# body:: a Hash or an Array of body part.
|
708
|
+
# e.g. { "a" => "b" } => 'a=b'.
|
709
|
+
# Give an array to pass multiple value like
|
710
|
+
# [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
|
711
|
+
# boundary:: When the boundary given, it is sent as
|
712
|
+
# a multipart/form-data using this boundary String.
|
713
|
+
def new_request(method, uri, query = nil, body = nil, boundary = nil)
|
714
|
+
m = new
|
715
|
+
m.http_header.init_request(method, uri, query)
|
716
|
+
m.http_body = Body.new
|
717
|
+
m.http_body.init_request(body || '', boundary)
|
718
|
+
if body
|
719
|
+
m.http_header.body_size = m.http_body.size
|
720
|
+
m.http_header.chunked = true if m.http_body.size.nil?
|
721
|
+
else
|
722
|
+
m.http_header.body_size = nil
|
723
|
+
end
|
724
|
+
m
|
725
|
+
end
|
726
|
+
|
727
|
+
# Creates a Message instance of response.
|
728
|
+
# body:: a String or an IO of response message body.
|
729
|
+
def new_response(body, req = nil)
|
730
|
+
m = new
|
731
|
+
m.http_header.init_response(Status::OK, req)
|
732
|
+
m.http_body = Body.new
|
733
|
+
m.http_body.init_response(body)
|
734
|
+
m.http_header.body_size = m.http_body.size || 0
|
735
|
+
m
|
736
|
+
end
|
737
|
+
|
738
|
+
@@mime_type_handler = nil
|
739
|
+
|
740
|
+
# Sets MIME type handler.
|
741
|
+
#
|
742
|
+
# handler must respond to :call with a single argument :path and returns
|
743
|
+
# a MIME type String e.g. 'text/html'.
|
744
|
+
# When the handler returns nil or an empty String,
|
745
|
+
# 'application/octet-stream' is used.
|
746
|
+
#
|
747
|
+
# When you set nil to the handler, internal_mime_type is used instead.
|
748
|
+
# The handler is nil by default.
|
749
|
+
def mime_type_handler=(handler)
|
750
|
+
@@mime_type_handler = handler
|
751
|
+
end
|
752
|
+
|
753
|
+
# Returns MIME type handler.
|
754
|
+
def mime_type_handler
|
755
|
+
@@mime_type_handler
|
756
|
+
end
|
757
|
+
|
758
|
+
# For backward compatibility.
|
759
|
+
alias set_mime_type_func mime_type_handler=
|
760
|
+
alias get_mime_type_func mime_type_handler
|
761
|
+
|
762
|
+
def mime_type(path) # :nodoc:
|
763
|
+
if @@mime_type_handler
|
764
|
+
res = @@mime_type_handler.call(path)
|
765
|
+
if !res || res.to_s == ''
|
766
|
+
return 'application/octet-stream'
|
767
|
+
else
|
768
|
+
return res
|
769
|
+
end
|
770
|
+
else
|
771
|
+
internal_mime_type(path)
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
# Default MIME type handler.
|
776
|
+
# See mime_type_handler=.
|
777
|
+
def internal_mime_type(path)
|
778
|
+
case path
|
779
|
+
when /\.txt$/i
|
780
|
+
'text/plain'
|
781
|
+
when /\.(htm|html)$/i
|
782
|
+
'text/html'
|
783
|
+
when /\.doc$/i
|
784
|
+
'application/msword'
|
785
|
+
when /\.png$/i
|
786
|
+
'image/png'
|
787
|
+
when /\.gif$/i
|
788
|
+
'image/gif'
|
789
|
+
when /\.(jpg|jpeg)$/i
|
790
|
+
'image/jpeg'
|
791
|
+
else
|
792
|
+
'application/octet-stream'
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
# Returns true if the given HTTP version allows keep alive connection.
|
797
|
+
# version:: String
|
798
|
+
def keep_alive_enabled?(version)
|
799
|
+
version >= '1.1'
|
800
|
+
end
|
801
|
+
|
802
|
+
# Returns true if the given query (or body) has a multiple parameter.
|
803
|
+
def multiparam_query?(query)
|
804
|
+
query.is_a?(Array) or query.is_a?(Hash)
|
805
|
+
end
|
806
|
+
|
807
|
+
# Returns true if the given object is a File. In HTTPClient, a file is;
|
808
|
+
# * must respond to :read for retrieving String chunks.
|
809
|
+
# * must respond to :pos and :pos= to rewind for reading.
|
810
|
+
# Rewinding is only needed for following HTTP redirect. Some IO impl
|
811
|
+
# defines :pos= but raises an Exception for pos= such as StringIO
|
812
|
+
# but there's no problem as far as using it for non-following methods
|
813
|
+
# (get/post/etc.)
|
814
|
+
def file?(obj)
|
815
|
+
obj.respond_to?(:read) and obj.respond_to?(:pos) and
|
816
|
+
obj.respond_to?(:pos=)
|
817
|
+
end
|
818
|
+
|
819
|
+
def create_query_part_str(query) # :nodoc:
|
820
|
+
if multiparam_query?(query)
|
821
|
+
escape_query(query)
|
822
|
+
elsif query.respond_to?(:read)
|
823
|
+
query = query.read
|
824
|
+
else
|
825
|
+
query.to_s
|
826
|
+
end
|
827
|
+
end
|
828
|
+
|
829
|
+
def Array.try_convert(value)
|
830
|
+
return value if value.instance_of?(Array)
|
831
|
+
return nil if !value.respond_to?(:to_ary)
|
832
|
+
converted = value.to_ary
|
833
|
+
return converted if converted.instance_of?(Array)
|
834
|
+
|
835
|
+
cname = value.class.name
|
836
|
+
raise TypeError, "can't convert %s to %s (%s#%s gives %s)" %
|
837
|
+
[cname, Array.name, cname, :to_ary, converted.class.name]
|
838
|
+
end unless Array.respond_to?(:try_convert)
|
839
|
+
|
840
|
+
def escape_query(query) # :nodoc:
|
841
|
+
pairs = []
|
842
|
+
query.each { |attr, value|
|
843
|
+
left = escape(attr.to_s) << '='
|
844
|
+
if values = Array.try_convert(value)
|
845
|
+
values.each { |value|
|
846
|
+
if value.respond_to?(:read)
|
847
|
+
value = value.read
|
848
|
+
end
|
849
|
+
pairs.push(left + escape(value.to_s))
|
850
|
+
}
|
851
|
+
else
|
852
|
+
if value.respond_to?(:read)
|
853
|
+
value = value.read
|
854
|
+
end
|
855
|
+
pairs.push(left << escape(value.to_s))
|
856
|
+
end
|
857
|
+
}
|
858
|
+
pairs.join('&')
|
859
|
+
end
|
860
|
+
|
861
|
+
# from CGI.escape
|
862
|
+
if defined?(Encoding::ASCII_8BIT)
|
863
|
+
def escape(str) # :nodoc:
|
864
|
+
str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(/([^ a-zA-Z0-9_.-]+)/) {
|
865
|
+
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
866
|
+
}.tr(' ', '+')
|
867
|
+
end
|
868
|
+
else
|
869
|
+
def escape(str) # :nodoc:
|
870
|
+
str.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
871
|
+
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
872
|
+
}.tr(' ', '+')
|
873
|
+
end
|
874
|
+
end
|
875
|
+
|
876
|
+
# from CGI.parse
|
877
|
+
def parse(query)
|
878
|
+
params = Hash.new([].freeze)
|
879
|
+
query.split(/[&;]/n).each do |pairs|
|
880
|
+
key, value = pairs.split('=',2).collect{|v| unescape(v) }
|
881
|
+
if params.has_key?(key)
|
882
|
+
params[key].push(value)
|
883
|
+
else
|
884
|
+
params[key] = [value]
|
885
|
+
end
|
886
|
+
end
|
887
|
+
params
|
888
|
+
end
|
889
|
+
|
890
|
+
# from CGI.unescape
|
891
|
+
def unescape(string)
|
892
|
+
string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
|
893
|
+
[$1.delete('%')].pack('H*')
|
894
|
+
end
|
895
|
+
end
|
896
|
+
end
|
897
|
+
|
898
|
+
|
899
|
+
# HTTP::Message::Headers:: message header.
|
900
|
+
attr_accessor :http_header
|
901
|
+
|
902
|
+
# HTTP::Message::Body:: message body.
|
903
|
+
attr_reader :http_body
|
904
|
+
|
905
|
+
# OpenSSL::X509::Certificate:: response only. server certificate which is
|
906
|
+
# used for retrieving the response.
|
907
|
+
attr_accessor :peer_cert
|
908
|
+
|
909
|
+
# Creates a Message. This method should be used internally.
|
910
|
+
# Use Message.new_connect_request, Message.new_request or
|
911
|
+
# Message.new_response instead.
|
912
|
+
def initialize # :nodoc:
|
913
|
+
@http_header = Headers.new
|
914
|
+
@http_body = @peer_cert = nil
|
915
|
+
end
|
916
|
+
|
917
|
+
# Dumps message (header and body) to given dev.
|
918
|
+
# dev needs to respond to <<.
|
919
|
+
def dump(dev = '')
|
920
|
+
str = @http_header.dump + CRLF
|
921
|
+
if @http_header.chunked
|
922
|
+
dev = @http_body.dump_chunked(str, dev)
|
923
|
+
elsif @http_body
|
924
|
+
dev = @http_body.dump(str, dev)
|
925
|
+
else
|
926
|
+
dev << str
|
927
|
+
end
|
928
|
+
dev
|
929
|
+
end
|
930
|
+
|
931
|
+
# Sets a new body. header.body_size is updated with new body.size.
|
932
|
+
def http_body=(body)
|
933
|
+
@http_body = body
|
934
|
+
@http_header.body_size = @http_body.size if @http_header
|
935
|
+
end
|
936
|
+
alias body= http_body=
|
937
|
+
|
938
|
+
# Returns HTTP version in a HTTP header. String.
|
939
|
+
def http_version
|
940
|
+
@http_header.http_version
|
941
|
+
end
|
942
|
+
|
943
|
+
# Sets HTTP version in a HTTP header. String.
|
944
|
+
def http_version=(http_version)
|
945
|
+
@http_header.http_version = http_version
|
946
|
+
end
|
947
|
+
|
948
|
+
VERSION_WARNING = 'Message#version (Float) is deprecated. Use Message#http_version (String) instead.'
|
949
|
+
def version
|
950
|
+
warn(VERSION_WARNING)
|
951
|
+
@http_header.http_version.to_f
|
952
|
+
end
|
953
|
+
|
954
|
+
def version=(version)
|
955
|
+
warn(VERSION_WARNING)
|
956
|
+
@http_header.http_version = version
|
957
|
+
end
|
958
|
+
|
959
|
+
# Returns HTTP status code in response. Integer.
|
960
|
+
def status
|
961
|
+
@http_header.status_code
|
962
|
+
end
|
963
|
+
|
964
|
+
alias code status
|
965
|
+
alias status_code status
|
966
|
+
|
967
|
+
# Sets HTTP status code of response. Integer.
|
968
|
+
# Reason phrase is updated, too.
|
969
|
+
def status=(status)
|
970
|
+
@http_header.status_code = status
|
971
|
+
end
|
972
|
+
|
973
|
+
# Returns HTTP status reason phrase in response. String.
|
974
|
+
def reason
|
975
|
+
@http_header.reason_phrase
|
976
|
+
end
|
977
|
+
|
978
|
+
# Sets HTTP status reason phrase of response. String.
|
979
|
+
def reason=(reason)
|
980
|
+
@http_header.reason_phrase = reason
|
981
|
+
end
|
982
|
+
|
983
|
+
# Returns 'Content-Type' header value.
|
984
|
+
def content_type
|
985
|
+
@http_header.content_type
|
986
|
+
end
|
987
|
+
|
988
|
+
# Sets 'Content-Type' header value. Overrides if already exists.
|
989
|
+
def content_type=(content_type)
|
990
|
+
@http_header.content_type = content_type
|
991
|
+
end
|
992
|
+
alias contenttype content_type
|
993
|
+
alias contenttype= content_type=
|
994
|
+
|
995
|
+
# Returns content encoding
|
996
|
+
def body_encoding
|
997
|
+
@http_header.body_encoding
|
998
|
+
end
|
999
|
+
|
1000
|
+
# Returns a content of message body. A String or an IO.
|
1001
|
+
def content
|
1002
|
+
@http_body.content
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
alias header http_header
|
1006
|
+
alias body content
|
1007
|
+
|
1008
|
+
# Returns Hash of header. key and value are both String. Each key has a
|
1009
|
+
# single value so you can't extract exact value when a message has multiple
|
1010
|
+
# headers like 'Set-Cookie'. Use header['Set-Cookie'] for that purpose.
|
1011
|
+
# (It returns an Array always)
|
1012
|
+
def headers
|
1013
|
+
Hash[*http_header.all.flatten]
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
# Extracts cookies from 'Set-Cookie' header.
|
1017
|
+
# Supports 'Set-Cookie' in response header only.
|
1018
|
+
# Do we need 'Cookie' support in request header?
|
1019
|
+
def cookies
|
1020
|
+
set_cookies = http_header['set-cookie']
|
1021
|
+
unless set_cookies.empty?
|
1022
|
+
uri = http_header.request_uri
|
1023
|
+
set_cookies.map { |str|
|
1024
|
+
cookie = WebAgent::Cookie.new
|
1025
|
+
cookie.parse(str, uri)
|
1026
|
+
cookie
|
1027
|
+
}
|
1028
|
+
end
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
# Convenience method to return boolean of whether we had a successful request
|
1032
|
+
def ok?
|
1033
|
+
HTTP::Status.successful?(status)
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
def redirect?
|
1037
|
+
HTTP::Status.redirect?(status)
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
# SEE_OTHER is a redirect, but it should sent as GET
|
1041
|
+
def see_other?
|
1042
|
+
status == HTTP::Status::SEE_OTHER
|
1043
|
+
end
|
1044
|
+
end
|
1045
|
+
|
1046
|
+
end
|