rufus-verbs 0.10 → 1.0.0

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