rufus-verbs 0.10 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,5 @@
1
- #
2
1
  #--
3
- # Copyright (c) 2008, John Mettraux, jmettraux@gmail.com
2
+ # Copyright (c) 2008-2010, John Mettraux, jmettraux@gmail.com
4
3
  #
5
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -8,10 +7,10 @@
8
7
  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
8
  # copies of the Software, and to permit persons to whom the Software is
10
9
  # furnished to do so, subject to the following conditions:
11
- #
10
+ #
12
11
  # The above copyright notice and this permission notice shall be included in
13
12
  # all copies or substantial portions of the Software.
14
- #
13
+ #
15
14
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
15
  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
16
  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -20,17 +19,9 @@
20
19
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
20
  # THE SOFTWARE.
22
21
  #
23
- # (MIT license)
22
+ # Made in Japan.
24
23
  #++
25
- #
26
24
 
27
- #
28
- # John Mettraux
29
- #
30
- # Made in Japan
31
- #
32
- # 2008/01/21
33
- #
34
25
 
35
26
  require 'digest/md5'
36
27
 
@@ -38,223 +29,223 @@ require 'digest/md5'
38
29
  module Rufus
39
30
  module Verbs
40
31
 
32
+ #
33
+ # Specified by http://www.ietf.org/rfc/rfc2617.txt
34
+ # Inspired by http://segment7.net/projects/ruby/snippets/digest_auth.rb
35
+ #
36
+ # The EndPoint classes mixes this in to support digest authentication.
37
+ #
38
+ module DigestAuthMixin
39
+
41
40
  #
42
- # Specified by http://www.ietf.org/rfc/rfc2617.txt
43
- # Inspired by http://segment7.net/projects/ruby/snippets/digest_auth.rb
44
- #
45
- # The EndPoint classes mixes this in to support digest authentication.
41
+ # Makes sure digest_auth is on
46
42
  #
47
- module DigestAuthMixin
43
+ def digest_auth (req, opts)
48
44
 
49
- #
50
- # Makes sure digest_auth is on
51
- #
52
- def digest_auth (req, opts)
45
+ #return if no_digest_auth
46
+ # already done in add_authentication()
53
47
 
54
- #return if no_digest_auth
55
- # already done in add_authentication()
48
+ @cnonce ||= generate_cnonce
49
+ @nonce_count ||= 0
56
50
 
57
- @cnonce ||= generate_cnonce
58
- @nonce_count ||= 0
51
+ mention_digest_auth(req, opts) \
52
+ and return
59
53
 
60
- mention_digest_auth(req, opts) \
61
- and return
54
+ mention_digest_auth(req, opts) \
55
+ if request_challenge(req, opts)
56
+ end
62
57
 
63
- mention_digest_auth(req, opts) \
64
- if request_challenge(req, opts)
65
- end
58
+ #
59
+ # Sets the 'Authorization' header with the appropriate info.
60
+ #
61
+ def mention_digest_auth (req, opts)
66
62
 
67
- #
68
- # Sets the 'Authorization' header with the appropriate info.
69
- #
70
- def mention_digest_auth (req, opts)
63
+ return false unless @challenge
71
64
 
72
- return false unless @challenge
65
+ req['Authorization'] = generate_header req, opts
73
66
 
74
- req['Authorization'] = generate_header req, opts
67
+ true
68
+ end
75
69
 
76
- true
77
- end
70
+ #
71
+ # Interprets the information in the response's 'Authorization-Info'
72
+ # header.
73
+ #
74
+ def check_authentication_info (res, opts)
78
75
 
79
- #
80
- # Interprets the information in the response's 'Authorization-Info'
81
- # header.
82
- #
83
- def check_authentication_info (res, opts)
76
+ return if no_digest_auth
77
+ # not using digest authentication
84
78
 
85
- return if no_digest_auth
86
- # not using digest authentication
79
+ return unless @challenge
80
+ # not yet authenticated
87
81
 
88
- return unless @challenge
89
- # not yet authenticated
82
+ authinfo = AuthInfo.new res
83
+ @challenge.nonce = authinfo.nextnonce
84
+ end
90
85
 
91
- authinfo = AuthInfo.new res
92
- @challenge.nonce = authinfo.nextnonce
93
- end
86
+ protected
94
87
 
95
- protected
88
+ #
89
+ # Returns true if :digest_authentication is set at endpoint
90
+ # or request level.
91
+ #
92
+ def no_digest_auth
96
93
 
97
- #
98
- # Returns true if :digest_authentication is set at endpoint
99
- # or request level.
100
- #
101
- def no_digest_auth
94
+ (not o(opts, :digest_authentication))
95
+ end
102
96
 
103
- (not o(opts, :digest_authentication))
104
- end
97
+ #
98
+ # To be enhanced.
99
+ #
100
+ # (For example http://www.intertwingly.net/blog/1585.html)
101
+ #
102
+ def generate_cnonce
105
103
 
106
- #
107
- # To be enhanced.
108
- #
109
- # (For example http://www.intertwingly.net/blog/1585.html)
110
- #
111
- def generate_cnonce
104
+ Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
105
+ end
112
106
 
113
- Digest::MD5.hexdigest("%x" % (Time.now.to_i + rand(65535)))
114
- end
107
+ def request_challenge (req, opts)
115
108
 
116
- def request_challenge (req, opts)
109
+ op = opts.dup
117
110
 
118
- op = opts.dup
111
+ op[:digest_authentication] = false
112
+ # preventing an infinite loop
119
113
 
120
- op[:digest_authentication] = false
121
- # preventing an infinite loop
114
+ method = req.class.const_get(:METHOD).downcase.to_sym
115
+ #method = :get
122
116
 
123
- method = req.class.const_get(:METHOD).downcase.to_sym
124
- #method = :get
117
+ res = request(method, op)
125
118
 
126
- res = request(method, op)
119
+ return false if res.code.to_i != 401
127
120
 
128
- return false if res.code.to_i != 401
121
+ @challenge = Challenge.new res
129
122
 
130
- @challenge = Challenge.new res
123
+ true
124
+ end
131
125
 
132
- true
133
- end
126
+ #
127
+ # Generates an MD5 digest of the arguments (joined by ":").
128
+ #
129
+ def h (*args)
134
130
 
135
- #
136
- # Generates an MD5 digest of the arguments (joined by ":").
137
- #
138
- def h (*args)
131
+ Digest::MD5.hexdigest(args.join(":"))
132
+ end
139
133
 
140
- Digest::MD5.hexdigest(args.join(":"))
141
- end
134
+ #
135
+ # Generates the Authentication header that will be returned
136
+ # to the server.
137
+ #
138
+ def generate_header (req, opts)
139
+
140
+ @nonce_count += 1
141
+
142
+ user, pass = o(opts, :digest_authentication)
143
+ realm = @challenge.realm || ""
144
+ method = req.class.const_get(:METHOD)
145
+ path = opts[:path]
146
+
147
+ a1 = if @challenge.algorithm == 'MD5-sess'
148
+ h(h(user, realm, pass), @challenge.nonce, @cnonce)
149
+ else
150
+ h(user, realm, pass)
151
+ end
152
+
153
+ a2, qop = if @challenge.qop.include?("auth-int")
154
+ [ h(method, path, h(req.body)), "auth-int" ]
155
+ else
156
+ [ h(method, path), "auth" ]
157
+ end
158
+
159
+ nc = ('%08x' % @nonce_count)
160
+
161
+ digest = h(
162
+ #a1, @challenge.nonce, nc, @cnonce, @challenge.qop, a2)
163
+ a1, @challenge.nonce, nc, @cnonce, "auth", a2)
164
+
165
+ header = ""
166
+ header << "Digest username=\"#{user}\", "
167
+ header << "realm=\"#{realm}\", "
168
+ header << "qop=\"#{qop}\", "
169
+ header << "uri=\"#{path}\", "
170
+ header << "nonce=\"#{@challenge.nonce}\", "
171
+ #header << "nc=##{nc}, "
172
+ header << "nc=#{nc}, "
173
+ header << "cnonce=\"#{@cnonce}\", "
174
+ header << "algorithm=\"#{@challenge.algorithm}\", "
175
+ #header << "algorithm=\"MD5-sess\", "
176
+ header << "response=\"#{digest}\", "
177
+ header << "opaque=\"#{@challenge.opaque}\""
178
+
179
+ header
180
+ end
142
181
 
143
- #
144
- # Generates the Authentication header that will be returned
145
- # to the server.
146
- #
147
- def generate_header (req, opts)
182
+ #
183
+ # A common parent class for Challenge and AuthInfo.
184
+ # Their header parsing code is here.
185
+ #
186
+ class ServerReply
148
187
 
149
- @nonce_count += 1
188
+ def initialize (res)
150
189
 
151
- user, pass = o(opts, :digest_authentication)
152
- realm = @challenge.realm || ""
153
- method = req.class.const_get(:METHOD)
154
- path = opts[:path]
190
+ s = res[header_name]
191
+ return nil unless s
155
192
 
156
- a1 = if @challenge.algorithm == 'MD5-sess'
157
- h(h(user, realm, pass), @challenge.nonce, @cnonce)
158
- else
159
- h(user, realm, pass)
160
- end
193
+ s = s[7..-1] if s[0, 6] == "Digest"
161
194
 
162
- a2, qop = if @challenge.qop.include?("auth-int")
163
- [ h(method, path, h(req.body)), "auth-int" ]
164
- else
165
- [ h(method, path), "auth" ]
166
- end
195
+ s = s.split ","
167
196
 
168
- nc = ('%08x' % @nonce_count)
197
+ s.each do |e|
169
198
 
170
- digest = h(
171
- #a1, @challenge.nonce, nc, @cnonce, @challenge.qop, a2)
172
- a1, @challenge.nonce, nc, @cnonce, "auth", a2)
199
+ k, v = parse_entry e
173
200
 
174
- header = ""
175
- header << "Digest username=\"#{user}\", "
176
- header << "realm=\"#{realm}\", "
177
- header << "qop=\"#{qop}\", "
178
- header << "uri=\"#{path}\", "
179
- header << "nonce=\"#{@challenge.nonce}\", "
180
- #header << "nc=##{nc}, "
181
- header << "nc=#{nc}, "
182
- header << "cnonce=\"#{@cnonce}\", "
183
- header << "algorithm=\"#{@challenge.algorithm}\", "
184
- #header << "algorithm=\"MD5-sess\", "
185
- header << "response=\"#{digest}\", "
186
- header << "opaque=\"#{@challenge.opaque}\""
201
+ if k == 'stale'
202
+ @stale = (v.downcase == 'true')
203
+ elsif k == 'nc'
204
+ @nc = v.to_i
205
+ elsif k == 'qop'
206
+ @qop = v.split ","
207
+ else
208
+ instance_variable_set "@#{k}".to_sym, v
209
+ end
210
+ end
211
+ end
187
212
 
188
- header
189
- end
213
+ protected
190
214
 
191
- #
192
- # A common parent class for Challenge and AuthInfo.
193
- # Their header parsing code is here.
194
- #
195
- class ServerReply
215
+ def parse_entry (e)
196
216
 
197
- def initialize (res)
217
+ k, v = e.split '=', 2
218
+ v = v[1..-2] if v[0, 1] == '"'
219
+ [ k.strip, v.strip ]
220
+ end
221
+ end
198
222
 
199
- s = res[header_name]
200
- return nil unless s
223
+ #
224
+ # Used when parsing a 'www-authenticate' header challenge.
225
+ #
226
+ class Challenge < ServerReply
201
227
 
202
- s = s[7..-1] if s[0, 6] == "Digest"
228
+ attr_accessor \
229
+ :opaque, :algorithm, :qop, :stale, :nonce, :realm, :charset
203
230
 
204
- s = s.split ","
231
+ def header_name
232
+ 'www-authenticate'
233
+ end
234
+ end
205
235
 
206
- s.each do |e|
236
+ #
237
+ # Used when parsing a 'authentication-info' header info.
238
+ #
239
+ class AuthInfo < ServerReply
207
240
 
208
- k, v = parse_entry e
241
+ attr_accessor \
242
+ :cnonce, :rspauth, :nextnonce, :qop, :nc
209
243
 
210
- if k == 'stale'
211
- @stale = (v.downcase == 'true')
212
- elsif k == 'nc'
213
- @nc = v.to_i
214
- elsif k == 'qop'
215
- @qop = v.split ","
216
- else
217
- instance_variable_set "@#{k}".to_sym, v
218
- end
219
- end
220
- end
221
-
222
- protected
223
-
224
- def parse_entry (e)
225
-
226
- k, v = e.split "=", 2
227
- v = v[1..-2] if v[0, 1] == '"'
228
- [ k.strip, v.strip ]
229
- end
230
- end
231
-
232
- #
233
- # Used when parsing a 'www-authenticate' header challenge.
234
- #
235
- class Challenge < ServerReply
236
-
237
- attr_accessor \
238
- :opaque, :algorithm, :qop, :stale, :nonce, :realm, :charset
239
-
240
- def header_name
241
- 'www-authenticate'
242
- end
243
- end
244
-
245
- #
246
- # Used when parsing a 'authentication-info' header info.
247
- #
248
- class AuthInfo < ServerReply
249
-
250
- attr_accessor \
251
- :cnonce, :rspauth, :nextnonce, :qop, :nc
252
-
253
- def header_name
254
- 'authentication-info'
255
- end
256
- end
244
+ def header_name
245
+ 'authentication-info'
246
+ end
257
247
  end
248
+ end
258
249
 
259
250
  end
260
251
  end
@@ -1,6 +1,5 @@
1
- #
2
1
  #--
3
- # Copyright (c) 2008, John Mettraux, jmettraux@gmail.com
2
+ # Copyright (c) 2008-2010, John Mettraux, jmettraux@gmail.com
4
3
  #
5
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -20,18 +19,11 @@
20
19
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
20
  # THE SOFTWARE.
22
21
  #
23
- # (MIT license)
22
+ # Made in Japan.
24
23
  #++
25
- #
26
24
 
27
- #
28
- # John Mettraux
29
- #
30
- # Made in Japan
31
- #
32
- # 2008/01/11
33
- #
34
25
 
26
+ require 'cgi'
35
27
  require 'uri'
36
28
  require 'yaml' # for StringIO (at least for now)
37
29
  require 'net/http'
@@ -46,602 +38,604 @@ require 'rufus/verbs/verbose'
46
38
  module Rufus
47
39
  module Verbs
48
40
 
49
- USER_AGENT = "Ruby rufus-verbs #{VERSION}"
41
+ USER_AGENT = "Ruby rufus-verbs #{VERSION}"
42
+
43
+ #
44
+ # An EndPoint can be used to share common options among a set of
45
+ # requests.
46
+ #
47
+ # ep = EndPoint.new(
48
+ # :host => "restful.server",
49
+ # :port => 7080,
50
+ # :resource => "inventory/tools")
51
+ #
52
+ # res = ep.get :id => 1
53
+ # # still a silver bullet ?
54
+ #
55
+ # res = ep.get :id => 0
56
+ # # where did the hammer go ?
57
+ #
58
+ # When a request gets prepared, the option values will be looked up
59
+ # in (1) its local (request) options, then (2) in the EndPoint options.
60
+ #
61
+ class EndPoint
62
+
63
+ include CookieMixin
64
+ include DigestAuthMixin
65
+ include VerboseMixin
50
66
 
51
67
  #
52
- # An EndPoint can be used to share common options among a set of
53
- # requests.
68
+ # The endpoint initialization opts (Hash instance)
69
+ #
70
+ attr_reader :opts
71
+
72
+ def initialize (opts)
73
+
74
+ @opts = opts
75
+
76
+ compute_target @opts
77
+
78
+ @opts[:http_basic_authentication] =
79
+ opts[:http_basic_authentication] || opts[:hba]
80
+
81
+ @opts[:user_agent] ||= USER_AGENT
82
+
83
+ @opts[:proxy] ||= ENV['HTTP_PROXY']
84
+
85
+ prepare_cookie_jar
86
+ end
87
+
88
+ def get (*args)
89
+
90
+ request :get, args
91
+ end
92
+
93
+ def post (*args, &block)
94
+
95
+ request :post, args, &block
96
+ end
97
+
98
+ def put (*args, &block)
99
+
100
+ request :put, args, &block
101
+ end
102
+
103
+ def delete (*args)
104
+
105
+ request :delete, args
106
+ end
107
+
108
+ def head (*args)
109
+
110
+ request :head, args
111
+ end
112
+
113
+ def options (*args)
114
+
115
+ request :options, args
116
+ end
117
+
54
118
  #
55
- # ep = EndPoint.new(
56
- # :host => "restful.server",
57
- # :port => 7080,
58
- # :resource => "inventory/tools")
119
+ # This is the method called by the module methods verbs.
59
120
  #
60
- # res = ep.get :id => 1
61
- # # still a silver bullet ?
121
+ # For example,
62
122
  #
63
- # res = ep.get :id => 0
64
- # # where did the hammer go ?
123
+ # RufusVerbs.get(args)
65
124
  #
66
- # When a request gets prepared, the option values will be looked up
67
- # in (1) its local (request) options, then (2) in the EndPoint options.
125
+ # calls
68
126
  #
69
- class EndPoint
127
+ # RufusVerbs::EndPoint.request(:get, args)
128
+ #
129
+ def self.request (method, args, &block)
70
130
 
71
- include CookieMixin
72
- include DigestAuthMixin
73
- include VerboseMixin
131
+ opts = extract_opts args
74
132
 
75
- #
76
- # The endpoint initialization opts (Hash instance)
77
- #
78
- attr_reader :opts
133
+ EndPoint.new(opts).request(method, opts, &block)
134
+ end
79
135
 
80
- def initialize (opts)
136
+ #
137
+ # The instance methods get, post, put and delete ultimately calls
138
+ # this request() method. All the work is done here.
139
+ #
140
+ def request (method, args, &block)
81
141
 
82
- @opts = opts
142
+ # prepare request
83
143
 
84
- compute_target @opts
144
+ opts = EndPoint.extract_opts args
85
145
 
86
- @opts[:http_basic_authentication] =
87
- opts[:http_basic_authentication] || opts[:hba]
146
+ compute_target opts
88
147
 
89
- @opts[:user_agent] ||= USER_AGENT
148
+ req = create_request method, opts
90
149
 
91
- @opts[:proxy] ||= ENV['HTTP_PROXY']
150
+ add_payload(req, opts, &block) if method == :post or method == :put
92
151
 
93
- prepare_cookie_jar
94
- end
152
+ add_authentication(req, opts)
95
153
 
96
- def get (*args)
154
+ add_conditional_headers(req, opts) if method == :get
97
155
 
98
- request :get, args
99
- end
156
+ mention_cookies(req, opts)
157
+ # if the :cookies option is disabled (the default)
158
+ # will have no effect
100
159
 
101
- def post (*args, &block)
160
+ vlog_request opts, req
102
161
 
103
- request :post, args, &block
104
- end
162
+ return req if o(opts, :dry_run) == true
105
163
 
106
- def put (*args, &block)
164
+ # trigger request
107
165
 
108
- request :put, args, &block
109
- end
166
+ http = prepare_http opts
110
167
 
111
- def delete (*args)
168
+ vlog_http opts, http
112
169
 
113
- request :delete, args
114
- end
170
+ res = nil
115
171
 
116
- def head (*args)
172
+ http.start do
173
+ res = http.request req
174
+ end
117
175
 
118
- request :head, args
119
- end
176
+ # handle response
120
177
 
121
- def options (*args)
178
+ class << res
179
+ attr_accessor :request
180
+ end
181
+ res.request = req
122
182
 
123
- request :options, args
124
- end
183
+ vlog_response opts, res
125
184
 
126
- #
127
- # This is the method called by the module methods verbs.
128
- #
129
- # For example,
130
- #
131
- # RufusVerbs.get(args)
132
- #
133
- # calls
134
- #
135
- # RufusVerbs::EndPoint.request(:get, args)
136
- #
137
- def self.request (method, args, &block)
138
-
139
- opts = extract_opts args
140
-
141
- EndPoint.new(opts).request(method, opts, &block)
142
- end
185
+ register_cookies res, opts
186
+ # if the :cookies option is disabled (the default)
187
+ # will have no effect
143
188
 
144
- #
145
- # The instance methods get, post, put and delete ultimately calls
146
- # this request() method. All the work is done here.
147
- #
148
- def request (method, args, &block)
189
+ return res if o(opts, :raw_response)
149
190
 
150
- # prepare request
191
+ check_authentication_info res, opts
192
+ # used in case of :digest_authentication
193
+ # will have no effect else
151
194
 
152
- opts = EndPoint.extract_opts args
195
+ res = handle_response method, res, opts
153
196
 
154
- compute_target opts
197
+ return parse_options(res) if method == :options
155
198
 
156
- req = create_request method, opts
199
+ return res.body if o(opts, :body)
157
200
 
158
- add_payload(req, opts, &block) if method == :post or method == :put
201
+ res
202
+ end
159
203
 
160
- add_authentication(req, opts)
204
+ private
161
205
 
162
- add_conditional_headers(req, opts) if method == :get
206
+ #
207
+ # Manages various args formats :
208
+ #
209
+ # uri
210
+ # [ uri ]
211
+ # [ uri, opts ]
212
+ # opts
213
+ #
214
+ def self.extract_opts (args)
163
215
 
164
- mention_cookies(req, opts)
165
- # if the :cookies option is disabled (the default)
166
- # will have no effect
216
+ opts = {}
167
217
 
168
- vlog_request opts, req
218
+ args = [ args ] unless args.is_a?(Array)
169
219
 
170
- return req if o(opts, :dry_run) == true
220
+ opts = args.last \
221
+ if args.last.is_a?(Hash)
171
222
 
172
- # trigger request
223
+ opts[:uri] = args.first \
224
+ if args.first.is_a?(String) or args.first.is_a?(URI)
173
225
 
174
- http = prepare_http opts
226
+ opts
227
+ end
175
228
 
176
- vlog_http opts, http
229
+ #
230
+ # Returns the value from the [request] opts or from the
231
+ # [endpoint] @opts.
232
+ #
233
+ def o (opts, key)
234
+
235
+ keys = Array key
236
+ keys.each { |k| (v = opts[k]; return v if v != nil) }
237
+ keys.each { |k| (v = @opts[k]; return v if v != nil) }
238
+ nil
239
+ end
177
240
 
178
- res = nil
241
+ #
242
+ # Returns scheme, host, port, path, query
243
+ #
244
+ def compute_target (opts)
245
+
246
+ u = opts[:uri] || opts[:u]
247
+
248
+ r = if opts[:host]
249
+
250
+ [ opts[:scheme] || 'http',
251
+ opts[:host],
252
+ opts[:port] || 80,
253
+ opts[:path] || '/',
254
+ opts[:query] || opts[:params] ]
255
+
256
+ elsif u
257
+
258
+ u = URI.parse u.to_s unless u.is_a?(URI)
259
+ [ u.scheme,
260
+ u.host,
261
+ u.port,
262
+ u.path,
263
+ query_to_h(u.query) ]
264
+ else
265
+
266
+ []
267
+ end
268
+
269
+ opts[:scheme] = r[0] || @opts[:scheme]
270
+ opts[:host] = r[1] || @opts[:host]
271
+ opts[:port] = r[2] || @opts[:port]
272
+ opts[:path] = r[3] || @opts[:path]
273
+
274
+ opts[:query] =
275
+ r[4] ||
276
+ opts[:params] || opts[:query] ||
277
+ @opts[:query] || @opts[:params] ||
278
+ {}
279
+
280
+ opts.delete :path if opts[:path] == ''
281
+
282
+ opts[:c_uri] = [
283
+ opts[:scheme],
284
+ opts[:host],
285
+ opts[:port],
286
+ opts[:path],
287
+ opts[:query] ].inspect
288
+ #
289
+ # can be used for conditional gets
290
+
291
+ r
292
+ end
179
293
 
180
- http.start do
181
- res = http.request req
182
- end
294
+ #
295
+ # Creates the Net::HTTP request instance.
296
+ #
297
+ # If :fake_put is set, will use Net::HTTP::Post
298
+ # and make sure the query string contains '_method=put' (or
299
+ # '_method=delete').
300
+ #
301
+ # This call will also advertise this rufus-verbs as
302
+ # 'accepting the gzip encoding' (in case of GET).
303
+ #
304
+ def create_request (method, opts)
183
305
 
184
- # handle response
306
+ if (o(opts, :fake_put) and
307
+ (method == :put or method == :delete))
185
308
 
186
- class << res
187
- attr_accessor :request
188
- end
189
- res.request = req
309
+ opts[:query][:_method] = method.to_s
310
+ method = :post
311
+ end
190
312
 
191
- vlog_response opts, res
313
+ path = compute_path opts
192
314
 
193
- register_cookies res, opts
194
- # if the :cookies option is disabled (the default)
195
- # will have no effect
315
+ r = eval("Net::HTTP::#{method.to_s.capitalize}").new path
196
316
 
197
- return res if o(opts, :raw_response)
317
+ r['User-Agent'] = o(opts, :user_agent)
318
+ # potentially overriden by opts[:headers]
198
319
 
199
- check_authentication_info res, opts
200
- # used in case of :digest_authentication
201
- # will have no effect else
320
+ h = opts[:headers] || opts[:h]
321
+ h.each { |k, v| r[k] = v } if h
202
322
 
203
- res = handle_response method, res, opts
323
+ r['Accept-Encoding'] = 'gzip' \
324
+ if method == :get and not o(opts, :nozip)
204
325
 
205
- return parse_options(res) if method == :options
326
+ r
327
+ end
206
328
 
207
- return res.body if o(opts, :body)
329
+ #
330
+ # If @user and @pass are set, will activate basic authentication.
331
+ # Else if the @auth option is set, will assume it contains a Proc
332
+ # and will call it (with the request as a parameter).
333
+ #
334
+ # This comment is too much... Just read the code...
335
+ #
336
+ def add_authentication (req, opts)
208
337
 
209
- res
210
- end
338
+ b = o(opts, :http_basic_authentication)
339
+ d = o(opts, :digest_authentication)
340
+ o = o(opts, :auth)
211
341
 
212
- private
213
-
214
- #
215
- # Manages various args formats :
216
- #
217
- # uri
218
- # [ uri ]
219
- # [ uri, opts ]
220
- # opts
221
- #
222
- def self.extract_opts (args)
223
-
224
- opts = {}
225
-
226
- args = [ args ] unless args.is_a?(Array)
227
-
228
- opts = args.last \
229
- if args.last.is_a?(Hash)
230
-
231
- opts[:uri] = args.first \
232
- if args.first.is_a?(String) or args.first.is_a?(URI)
342
+ if b and b != false
233
343
 
234
- opts
235
- end
344
+ req.basic_auth b[0], b[1]
236
345
 
237
- #
238
- # Returns the value from the [request] opts or from the
239
- # [endpoint] @opts.
240
- #
241
- def o (opts, key)
242
-
243
- keys = Array key
244
- keys.each { |k| (v = opts[k]; return v if v != nil) }
245
- keys.each { |k| (v = @opts[k]; return v if v != nil) }
246
- nil
247
- end
248
-
249
- #
250
- # Returns scheme, host, port, path, query
251
- #
252
- def compute_target (opts)
346
+ elsif d and d != false
253
347
 
254
- u = opts[:uri] || opts[:u]
348
+ digest_auth req, opts
255
349
 
256
- r = if opts[:host]
350
+ elsif o and o != false
257
351
 
258
- [ opts[:scheme] || 'http',
259
- opts[:host],
260
- opts[:port] || 80,
261
- opts[:path] || '/',
262
- opts[:query] || opts[:params] ]
352
+ o.call req
353
+ end
354
+ end
263
355
 
264
- elsif u
356
+ #
357
+ # In that base class, it's empty.
358
+ # It's implemented in ConditionalEndPoint.
359
+ #
360
+ # Only called for a GET.
361
+ #
362
+ def add_conditional_headers (req, opts)
265
363
 
266
- u = URI.parse u.to_s unless u.is_a?(URI)
267
- [ u.scheme,
268
- u.host,
269
- u.port,
270
- u.path,
271
- query_to_h(u.query) ]
272
- else
364
+ # nada
365
+ end
273
366
 
274
- []
275
- end
367
+ #
368
+ # Prepares a Net::HTTP instance, with potentially some
369
+ # https settings.
370
+ #
371
+ def prepare_http (opts)
276
372
 
277
- opts[:scheme] = r[0] || @opts[:scheme]
278
- opts[:host] = r[1] || @opts[:host]
279
- opts[:port] = r[2] || @opts[:port]
280
- opts[:path] = r[3] || @opts[:path]
373
+ compute_proxy opts
281
374
 
282
- opts[:query] =
283
- r[4] ||
284
- opts[:params] || opts[:query] ||
285
- @opts[:query] || @opts[:params] ||
286
- {}
375
+ http = Net::HTTP.new(
376
+ opts[:host], opts[:port],
377
+ opts[:proxy_host], opts[:proxy_port],
378
+ opts[:proxy_user], opts[:proxy_pass])
287
379
 
288
- opts.delete :path if opts[:path] == ""
380
+ set_timeout http, opts
289
381
 
290
- opts[:c_uri] = [
291
- opts[:scheme],
292
- opts[:host],
293
- opts[:port],
294
- opts[:path],
295
- opts[:query] ].inspect
296
- #
297
- # can be used for conditional gets
382
+ return http unless opts[:scheme].to_s == 'https'
298
383
 
299
- r
300
- end
384
+ require 'net/https'
301
385
 
302
- #
303
- # Creates the Net::HTTP request instance.
304
- #
305
- # If :fake_put is set, will use Net::HTTP::Post
306
- # and make sure the query string contains '_method=put' (or
307
- # '_method=delete').
308
- #
309
- # This call will also advertise this rufus-verbs as
310
- # 'accepting the gzip encoding' (in case of GET).
311
- #
312
- def create_request (method, opts)
386
+ http.use_ssl = true
387
+ http.enable_post_connection_check = true \
388
+ if http.respond_to?(:enable_post_connection_check=)
313
389
 
314
- if (o(opts, :fake_put) and
315
- (method == :put or method == :delete))
390
+ http.verify_mode = if o(opts, :ssl_verify_peer)
391
+ OpenSSL::SSL::VERIFY_PEER
392
+ else
393
+ OpenSSL::SSL::VERIFY_NONE
394
+ end
316
395
 
317
- opts[:query][:_method] = method.to_s
318
- method = :post
319
- end
396
+ store = OpenSSL::X509::Store.new
397
+ store.set_default_paths
398
+ http.cert_store = store
320
399
 
321
- path = compute_path opts
400
+ http
401
+ end
322
402
 
323
- r = eval("Net::HTTP::#{method.to_s.capitalize}").new path
403
+ #
404
+ # Sets both the open_timeout and the read_timeout for the http
405
+ # instance
406
+ #
407
+ def set_timeout (http, opts)
324
408
 
325
- r['User-Agent'] = o(opts, :user_agent)
326
- # potentially overriden by opts[:headers]
409
+ to = o(opts, :timeout) || o(opts, :to)
410
+ to = to.to_i
327
411
 
328
- h = opts[:headers] || opts[:h]
329
- h.each { |k, v| r[k] = v } if h
412
+ return if to == 0
330
413
 
331
- r['Accept-Encoding'] = 'gzip' \
332
- if method == :get and not o(opts, :nozip)
414
+ http.open_timeout = to
415
+ http.read_timeout = to
416
+ end
333
417
 
334
- r
335
- end
418
+ #
419
+ # Makes sure the request opts hold the proxy information.
420
+ #
421
+ # If the option :proxy is set to false, no proxy will be used.
422
+ #
423
+ def compute_proxy (opts)
336
424
 
337
- #
338
- # If @user and @pass are set, will activate basic authentication.
339
- # Else if the @auth option is set, will assume it contains a Proc
340
- # and will call it (with the request as a parameter).
341
- #
342
- # This comment is too much... Just read the code...
343
- #
344
- def add_authentication (req, opts)
425
+ p = o(opts, :proxy)
345
426
 
346
- b = o(opts, :http_basic_authentication)
347
- d = o(opts, :digest_authentication)
348
- o = o(opts, :auth)
427
+ return unless p
349
428
 
350
- if b and b != false
429
+ u = URI.parse p.to_s
351
430
 
352
- req.basic_auth b[0], b[1]
431
+ raise "not an HTTP[S] proxy '#{u.host}'" \
432
+ unless u.scheme.match(/^http/)
353
433
 
354
- elsif d and d != false
434
+ opts[:proxy_host] = u.host
435
+ opts[:proxy_port] = u.port
436
+ opts[:proxy_user] = u.user
437
+ opts[:proxy_pass] = u.password
438
+ end
355
439
 
356
- digest_auth req, opts
440
+ #
441
+ # Determines the full path of the request (path_info and
442
+ # query_string).
443
+ #
444
+ # For example :
445
+ #
446
+ # /items/4?style=whatever&maxcount=12
447
+ #
448
+ def compute_path (opts)
357
449
 
358
- elsif o and o != false
359
-
360
- o.call req
361
- end
362
- end
450
+ b = o(opts, :base)
451
+ r = o(opts, [ :res, :resource ])
452
+ i = o(opts, :id)
363
453
 
364
- #
365
- # In that base class, it's empty.
366
- # It's implemented in ConditionalEndPoint.
367
- #
368
- # Only called for a GET.
369
- #
370
- def add_conditional_headers (req, opts)
454
+ path = o(opts, :path)
371
455
 
372
- # nada
373
- end
456
+ if b or r or i
457
+ path = b ? "/#{b}" : ''
458
+ path += "/#{r}" if r
459
+ path += "/#{i}" if i
460
+ end
374
461
 
375
- #
376
- # Prepares a Net::HTTP instance, with potentially some
377
- # https settings.
378
- #
379
- def prepare_http (opts)
462
+ path = path[1..-1] if path[0..1] == '//'
380
463
 
381
- compute_proxy opts
464
+ query = opts[:query] || opts[:params]
382
465
 
383
- http = Net::HTTP.new(
384
- opts[:host], opts[:port],
385
- opts[:proxy_host], opts[:proxy_port],
386
- opts[:proxy_user], opts[:proxy_pass])
466
+ return path if not query or query.size < 1
387
467
 
388
- set_timeout http, opts
468
+ path + '?' + h_to_query(query, opts)
469
+ end
389
470
 
390
- return http unless opts[:scheme] == 'https'
471
+ #
472
+ # "a=A&b=B" -> { "a" => "A", "b" => "B" }
473
+ #
474
+ def query_to_h (q)
391
475
 
392
- require 'net/https'
476
+ return nil unless q
393
477
 
394
- http.use_ssl = true
395
- http.enable_post_connection_check = true
478
+ q.split('&').inject({}) do |r, e|
479
+ s = e.split('=')
480
+ r[s[0]] = s[1]
481
+ r
482
+ end
483
+ end
396
484
 
397
- http.verify_mode = if o(opts, :ssl_verify_peer)
398
- OpenSSL::SSL::VERIFY_PEER
399
- else
400
- OpenSSL::SSL::VERIFY_NONE
401
- end
485
+ #
486
+ # { "a" => "A", "b" => "B" } -> "a=A&b=B"
487
+ #
488
+ def h_to_query (h, opts)
489
+
490
+ h.entries.collect { |k, v|
491
+ unless o(opts, :no_escape)
492
+ #k = URI.escape k.to_s
493
+ #v = URI.escape v.to_s
494
+ k = CGI.escape(k.to_s)
495
+ v = CGI.escape(v.to_s)
496
+ end
497
+ "#{k}=#{v}"
498
+ }.join('&')
499
+ end
402
500
 
403
- store = OpenSSL::X509::Store.new
404
- store.set_default_paths
405
- http.cert_store = store
501
+ #
502
+ # Fills the request body (with the content of :d or :fd).
503
+ #
504
+ def add_payload (req, opts, &block)
505
+
506
+ d = opts[:d] || opts[:data]
507
+ fd = opts[:fd] || opts[:form_data]
508
+
509
+ if d
510
+ req.body = d
511
+ elsif fd
512
+ sep = opts[:fd_sep] #|| nil
513
+ req.set_form_data fd, sep
514
+ elsif block
515
+ req.body = block.call req
516
+ else
517
+ req.body = ''
518
+ end
519
+ end
406
520
 
407
- http
408
- end
521
+ #
522
+ # Handles the server response.
523
+ # Eventually follows redirections.
524
+ #
525
+ # Once the final response has been hit, will make sure
526
+ # it's decompressed.
527
+ #
528
+ def handle_response (method, res, opts)
409
529
 
410
- #
411
- # Sets both the open_timeout and the read_timeout for the http
412
- # instance
413
- #
414
- def set_timeout (http, opts)
530
+ nored = o(opts, [ :no_redirections, :noredir ])
415
531
 
416
- to = o(opts, :timeout) || o(opts, :to)
417
- to = to.to_i
532
+ #if res.is_a?(Net::HTTPRedirection)
533
+ if [ 301, 302, 303, 307 ].include?(res.code.to_i) and (nored != true)
418
534
 
419
- return if to == 0
535
+ maxr = o(opts, :max_redirections)
420
536
 
421
- http.open_timeout = to
422
- http.read_timeout = to
423
- end
537
+ if maxr
538
+ maxr = maxr - 1
539
+ raise 'too many redirections' if maxr == -1
540
+ opts[:max_redirections] = maxr
541
+ end
424
542
 
425
- #
426
- # Makes sure the request opts hold the proxy information.
427
- #
428
- # If the option :proxy is set to false, no proxy will be used.
429
- #
430
- def compute_proxy (opts)
431
-
432
- p = o(opts, :proxy)
433
-
434
- return unless p
435
-
436
- u = URI.parse p.to_s
437
-
438
- raise "not an HTTP[S] proxy '#{u.host}'" \
439
- unless u.scheme.match(/^http/)
440
-
441
- opts[:proxy_host] = u.host
442
- opts[:proxy_port] = u.port
443
- opts[:proxy_user] = u.user
444
- opts[:proxy_pass] = u.password
445
- end
446
-
447
- #
448
- # Determines the full path of the request (path_info and
449
- # query_string).
450
- #
451
- # For example :
452
- #
453
- # /items/4?style=whatever&maxcount=12
454
- #
455
- def compute_path (opts)
456
-
457
- b = o(opts, :base)
458
- r = o(opts, [ :res, :resource ])
459
- i = o(opts, :id)
460
-
461
- path = o(opts, :path)
462
-
463
- if b or r or i
464
- path = ""
465
- path = "/#{b}" if b
466
- path += "/#{r}" if r
467
- path += "/#{i}" if i
468
- end
469
-
470
- path = path[1..-1] if path[0..1] == '//'
471
-
472
- query = opts[:query] || opts[:params]
473
-
474
- return path if not query or query.size < 1
475
-
476
- path + '?' + h_to_query(query, opts)
477
- end
478
-
479
- #
480
- # "a=A&b=B" -> { "a" => "A", "b" => "B" }
481
- #
482
- def query_to_h (q)
483
-
484
- return nil unless q
485
-
486
- q.split("&").inject({}) do |r, e|
487
- s = e.split("=")
488
- r[s[0]] = s[1]
489
- r
490
- end
491
- end
492
-
493
- #
494
- # { "a" => "A", "b" => "B" } -> "a=A&b=B"
495
- #
496
- def h_to_query (h, opts)
497
-
498
- h.entries.collect { |k, v|
499
- unless o(opts, :no_escape)
500
- k = URI.escape k.to_s
501
- v = URI.escape v.to_s
502
- end
503
- "#{k}=#{v}"
504
- }.join("&")
505
- end
506
-
507
- #
508
- # Fills the request body (with the content of :d or :fd).
509
- #
510
- def add_payload (req, opts, &block)
511
-
512
- d = opts[:d] || opts[:data]
513
- fd = opts[:fd] || opts[:form_data]
514
-
515
- if d
516
- req.body = d
517
- elsif fd
518
- sep = opts[:fd_sep] #|| nil
519
- req.set_form_data fd, sep
520
- elsif block
521
- req.body = block.call req
522
- else
523
- req.body = ""
524
- end
525
- end
526
-
527
- #
528
- # Handles the server response.
529
- # Eventually follows redirections.
530
- #
531
- # Once the final response has been hit, will make sure
532
- # it's decompressed.
533
- #
534
- def handle_response (method, res, opts)
535
-
536
- nored = o(opts, [ :no_redirections, :noredir ])
537
-
538
- #if res.is_a?(Net::HTTPRedirection)
539
- if [ 301, 303, 307 ].include?(res.code.to_i) and (nored != true)
540
-
541
- maxr = o(opts, :max_redirections)
542
-
543
- if maxr
544
- maxr = maxr - 1
545
- raise "too many redirections" if maxr == -1
546
- opts[:max_redirections] = maxr
547
- end
548
-
549
- location = res['Location']
550
-
551
- prev_host = [ opts[:scheme], opts[:host] ]
552
-
553
- if location.match /^http/
554
- u = URI::parse location
555
- opts[:scheme] = u.scheme
556
- opts[:host] = u.host
557
- opts[:port] = u.port
558
- opts[:path] = u.path
559
- opts[:query] = u.query
560
- else
561
- opts[:path], opts[:query] = location.split "?"
562
- end
563
-
564
- if (authentication_is_on?(opts) and
565
- [ opts[:scheme], opts[:host] ] != prev_host)
566
-
567
- raise(
568
- "getting redirected to #{location} while " +
569
- "authentication is on. Stopping.")
570
- end
571
-
572
- opts[:query] = query_to_h opts[:query]
573
-
574
- return request(method, opts)
575
- #
576
- # following the redirection
577
- end
578
-
579
- decompress res
580
-
581
- res
582
- end
583
-
584
- #
585
- # Returns an array of symbols, like for example
586
- #
587
- # [ :get, :post ]
588
- #
589
- # obtained by parsing the 'Allow' response header.
590
- #
591
- # This method is used to provide the result of an OPTIONS
592
- # HTTP method.
593
- #
594
- def parse_options (res)
595
-
596
- s = res['Allow']
597
-
598
- return [] unless s
599
-
600
- s.split(",").collect do |m|
601
- m.strip.downcase.to_sym
602
- end
603
- end
604
-
605
- #
606
- # Returns true if the current request has authentication
607
- # going on.
608
- #
609
- def authentication_is_on? (opts)
610
-
611
- (o(opts, [ :http_basic_authentication, :hba, :auth ]) != nil)
612
- end
613
-
614
- #
615
- # Inflates the response body if necessary.
616
- #
617
- def decompress (res)
618
-
619
- if res['content-encoding'] == 'gzip'
620
-
621
- class << res
622
-
623
- attr_accessor :deflated_body
624
-
625
- alias :old_body :body
626
-
627
- def body
628
- @deflated_body || old_body
629
- end
630
- end
631
- #
632
- # reopened the response to add
633
- # a 'deflated_body' attr and let the the body
634
- # method point to it
543
+ location = res['Location']
635
544
 
636
- # now deflate...
545
+ prev_host = [ opts[:scheme], opts[:host] ]
637
546
 
638
- io = StringIO.new res.body
639
- gz = Zlib::GzipReader.new io
640
- res.deflated_body = gz.read
641
- gz.close
642
- end
643
- end
547
+ if location.match /^http/
548
+ u = URI::parse location
549
+ opts[:scheme] = u.scheme
550
+ opts[:host] = u.host
551
+ opts[:port] = u.port
552
+ opts[:path] = u.path
553
+ opts[:query] = u.query
554
+ else
555
+ opts[:path], opts[:query] = location.split "?"
556
+ end
557
+
558
+ if (authentication_is_on?(opts) and
559
+ [ opts[:scheme], opts[:host] ] != prev_host)
560
+
561
+ raise(
562
+ "getting redirected to #{location} while " +
563
+ "authentication is on. Stopping.")
564
+ end
565
+
566
+ opts[:query] = query_to_h opts[:query]
567
+
568
+ return request(method, opts)
569
+ #
570
+ # following the redirection
571
+ end
572
+
573
+ decompress res
574
+
575
+ res
576
+ end
577
+
578
+ #
579
+ # Returns an array of symbols, like for example
580
+ #
581
+ # [ :get, :post ]
582
+ #
583
+ # obtained by parsing the 'Allow' response header.
584
+ #
585
+ # This method is used to provide the result of an OPTIONS
586
+ # HTTP method.
587
+ #
588
+ def parse_options (res)
589
+
590
+ s = res['Allow']
591
+
592
+ return [] unless s
593
+
594
+ s.split(',').collect do |m|
595
+ m.strip.downcase.to_sym
596
+ end
597
+ end
598
+
599
+ #
600
+ # Returns true if the current request has authentication
601
+ # going on.
602
+ #
603
+ def authentication_is_on? (opts)
604
+
605
+ (o(opts, [ :http_basic_authentication, :hba, :auth ]) != nil)
606
+ end
607
+
608
+ #
609
+ # Inflates the response body if necessary.
610
+ #
611
+ def decompress (res)
612
+
613
+ if res['content-encoding'] == 'gzip'
614
+
615
+ class << res
616
+
617
+ attr_accessor :deflated_body
618
+
619
+ alias :old_body :body
620
+
621
+ def body
622
+ @deflated_body || old_body
623
+ end
624
+ end
625
+ #
626
+ # reopened the response to add
627
+ # a 'deflated_body' attr and let the the body
628
+ # method point to it
629
+
630
+ # now deflate...
631
+
632
+ io = StringIO.new res.body
633
+ gz = Zlib::GzipReader.new io
634
+ res.deflated_body = gz.read
635
+ gz.close
636
+ end
644
637
  end
638
+ end
645
639
  end
646
640
  end
647
641