-
-
Notifications
You must be signed in to change notification settings - Fork 267
/
consumer.rb
393 lines (330 loc) · 12.8 KB
/
consumer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
require 'net/http'
require 'net/https'
require 'oauth/oauth'
require 'oauth/client/net_http'
require 'oauth/errors'
require 'cgi'
module OAuth
class Consumer
# determine the certificate authority path to verify SSL certs
CA_FILES = %W(#{ENV['SSL_CERT_FILE']} /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt /usr/share/curl/curl-ca-bundle.crt)
CA_FILES.each do |ca_file|
if File.exist?(ca_file)
CA_FILE = ca_file
break
end
end
CA_FILE = nil unless defined?(CA_FILE)
@@default_options = {
# Signature method used by server. Defaults to HMAC-SHA1
:signature_method => 'HMAC-SHA1',
# default paths on site. These are the same as the defaults set up by the generators
:request_token_path => '/oauth/request_token',
:authorize_path => '/oauth/authorize',
:access_token_path => '/oauth/access_token',
:proxy => nil,
# How do we send the oauth values to the server see
# http://oauth.net/core/1.0/#consumer_req_param for more info
#
# Possible values:
#
# :header - via the Authorize header (Default) ( option 1. in spec)
# :body - url form encoded in body of POST request ( option 2. in spec)
# :query_string - via the query part of the url ( option 3. in spec)
:scheme => :header,
# Default http method used for OAuth Token Requests (defaults to :post)
:http_method => :post,
# Add a custom ca_file for consumer
# :ca_file => '/etc/certs.pem'
:oauth_version => "1.0"
}
attr_accessor :options, :key, :secret
attr_writer :site, :http
# Create a new consumer instance by passing it a configuration hash:
#
# @consumer = OAuth::Consumer.new(key, secret, {
# :site => "http://term.ie",
# :scheme => :header,
# :http_method => :post,
# :request_token_path => "/oauth/example/request_token.php",
# :access_token_path => "/oauth/example/access_token.php",
# :authorize_path => "/oauth/example/authorize.php"
# })
#
# Start the process by requesting a token
#
# @request_token = @consumer.get_request_token
# session[:request_token] = @request_token
# redirect_to @request_token.authorize_url
#
# When user returns create an access_token
#
# @access_token = @request_token.get_access_token
# @photos=@access_token.get('/photos.xml')
#
def initialize(consumer_key, consumer_secret, options = {})
@key = consumer_key
@secret = consumer_secret
# ensure that keys are symbols
@options = @@default_options.merge(options.inject({}) do |opts, (key, value)|
opts[key.to_sym] = value
opts
end)
end
# The default http method
def http_method
@http_method ||= @options[:http_method] || :post
end
# The HTTP object for the site. The HTTP Object is what you get when you do Net::HTTP.new
def http
@http ||= create_http
end
# Contains the root URI for this site
def uri(custom_uri = nil)
if custom_uri
@uri = custom_uri
@http = create_http # yike, oh well. less intrusive this way
else # if no custom passed, we use existing, which, if unset, is set to site uri
@uri ||= URI.parse(site)
end
end
def get_access_token(request_token, request_options = {}, *arguments, &block)
response = token_request(http_method, (access_token_url? ? access_token_url : access_token_path), request_token, request_options, *arguments, &block)
OAuth::AccessToken.from_hash(self, response)
end
# Makes a request to the service for a new OAuth::RequestToken
#
# @request_token = @consumer.get_request_token
#
# To include OAuth parameters:
#
# @request_token = @consumer.get_request_token \
# :oauth_callback => "http://example.com/cb"
#
# To include application-specific parameters:
#
# @request_token = @consumer.get_request_token({}, :foo => "bar")
#
# TODO oauth_callback should be a mandatory parameter
def get_request_token(request_options = {}, *arguments, &block)
# if oauth_callback wasn't provided, it is assumed that oauth_verifiers
# will be exchanged out of band
request_options[:oauth_callback] ||= OAuth::OUT_OF_BAND unless request_options[:exclude_callback]
if block_given?
response = token_request(http_method,
(request_token_url? ? request_token_url : request_token_path),
nil,
request_options,
*arguments, &block)
else
response = token_request(http_method, (request_token_url? ? request_token_url : request_token_path), nil, request_options, *arguments)
end
OAuth::RequestToken.from_hash(self, response)
end
# Creates, signs and performs an http request.
# It's recommended to use the OAuth::Token classes to set this up correctly.
# request_options take precedence over consumer-wide options when signing
# a request.
# arguments are POST and PUT bodies (a Hash, string-encoded parameters, or
# absent), followed by additional HTTP headers.
#
# @consumer.request(:get, '/people', @token, { :scheme => :query_string })
# @consumer.request(:post, '/people', @token, {}, @person.to_xml, { 'Content-Type' => 'application/xml' })
#
def request(http_method, path, token = nil, request_options = {}, *arguments)
if path !~ /^\//
@http = create_http(path)
_uri = URI.parse(path)
path = "#{_uri.path}#{_uri.query ? "?#{_uri.query}" : ""}"
end
# override the request with your own, this is useful for file uploads which Net::HTTP does not do
req = create_signed_request(http_method, path, token, request_options, *arguments)
return nil if block_given? and yield(req) == :done
rsp = http.request(req)
# check for an error reported by the Problem Reporting extension
# (http://wiki.oauth.net/ProblemReporting)
# note: a 200 may actually be an error; check for an oauth_problem key to be sure
if !(headers = rsp.to_hash["www-authenticate"]).nil? &&
(h = headers.select { |hdr| hdr =~ /^OAuth / }).any? &&
h.first =~ /oauth_problem/
# puts "Header: #{h.first}"
# TODO doesn't handle broken responses from api.login.yahoo.com
# remove debug code when done
params = OAuth::Helper.parse_header(h.first)
# puts "Params: #{params.inspect}"
# puts "Body: #{rsp.body}"
raise OAuth::Problem.new(params.delete("oauth_problem"), rsp, params)
end
rsp
end
# Creates and signs an http request.
# It's recommended to use the Token classes to set this up correctly
def create_signed_request(http_method, path, token = nil, request_options = {}, *arguments)
request = create_http_request(http_method, path, *arguments)
sign!(request, token, request_options)
request
end
# Creates a request and parses the result as url_encoded. This is used internally for the RequestToken and AccessToken requests.
def token_request(http_method, path, token = nil, request_options = {}, *arguments)
request_options[:token_request] ||= true
response = request(http_method, path, token, request_options, *arguments)
case response.code.to_i
when (200..299)
if block_given?
yield response.body
else
# symbolize keys
# TODO this could be considered unexpected behavior; symbols or not?
# TODO this also drops subsequent values from multi-valued keys
CGI.parse(response.body).inject({}) do |h,(k,v)|
h[k.strip.to_sym] = v.first
h[k.strip] = v.first
h
end
end
when (300..399)
# this is a redirect
uri = URI.parse(response.header['location'])
response.error! if uri.path == path # careful of those infinite redirects
self.token_request(http_method, uri.path, token, request_options, arguments)
when (400..499)
raise OAuth::Unauthorized, response
else
response.error!
end
end
# Sign the Request object. Use this if you have an externally generated http request object you want to sign.
def sign!(request, token = nil, request_options = {})
request.oauth!(http, self, token, options.merge(request_options))
end
# Return the signature_base_string
def signature_base_string(request, token = nil, request_options = {})
request.signature_base_string(http, self, token, options.merge(request_options))
end
def site
@options[:site].to_s
end
def request_endpoint
return nil if @options[:request_endpoint].nil?
@options[:request_endpoint].to_s
end
def scheme
@options[:scheme]
end
def request_token_path
@options[:request_token_path]
end
def authorize_path
@options[:authorize_path]
end
def access_token_path
@options[:access_token_path]
end
# TODO this is ugly, rewrite
def request_token_url
@options[:request_token_url] || site + request_token_path
end
def request_token_url?
@options.has_key?(:request_token_url)
end
def authorize_url
@options[:authorize_url] || site + authorize_path
end
def authorize_url?
@options.has_key?(:authorize_url)
end
def access_token_url
@options[:access_token_url] || site + access_token_path
end
def access_token_url?
@options.has_key?(:access_token_url)
end
def proxy
@options[:proxy]
end
protected
# Instantiates the http object
def create_http(_url = nil)
if !request_endpoint.nil?
_url = request_endpoint
end
if _url.nil? || _url[0] =~ /^\//
our_uri = URI.parse(site)
else
our_uri = URI.parse(_url)
end
if proxy.nil?
http_object = Net::HTTP.new(our_uri.host, our_uri.port)
else
proxy_uri = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
http_object = Net::HTTP.new(our_uri.host, our_uri.port, proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
end
http_object.use_ssl = (our_uri.scheme == 'https')
if @options[:ca_file] || CA_FILE
http_object.ca_file = @options[:ca_file] || CA_FILE
http_object.verify_mode = OpenSSL::SSL::VERIFY_PEER
http_object.verify_depth = 5
else
http_object.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
http_object.read_timeout = http_object.open_timeout = @options[:timeout] || 30
http_object.open_timeout = @options[:open_timeout] if @options[:open_timeout]
http_object
end
# create the http request object for a given http_method and path
def create_http_request(http_method, path, *arguments)
http_method = http_method.to_sym
if [:post, :put, :patch].include?(http_method)
data = arguments.shift
end
# if the base site contains a path, add it now
uri = URI.parse(site)
path = uri.path + path if uri.path && uri.path != '/'
headers = arguments.first.is_a?(Hash) ? arguments.shift : {}
case http_method
when :post
request = Net::HTTP::Post.new(path,headers)
request["Content-Length"] = '0' # Default to 0
when :put
request = Net::HTTP::Put.new(path,headers)
request["Content-Length"] = '0' # Default to 0
when :patch
request = Net::HTTP::Patch.new(path,headers)
request["Content-Length"] = '0' # Default to 0
when :get
request = Net::HTTP::Get.new(path,headers)
when :delete
request = Net::HTTP::Delete.new(path,headers)
when :head
request = Net::HTTP::Head.new(path,headers)
else
raise ArgumentError, "Don't know how to handle http_method: :#{http_method.to_s}"
end
if data.is_a?(Hash)
request.body = OAuth::Helper.normalize(data)
request.content_type = 'application/x-www-form-urlencoded'
elsif data
if data.respond_to?(:read)
request.body_stream = data
if data.respond_to?(:length)
request["Content-Length"] = data.length.to_s
elsif data.respond_to?(:stat) && data.stat.respond_to?(:size)
request["Content-Length"] = data.stat.size.to_s
else
raise ArgumentError, "Don't know how to send a body_stream that doesn't respond to .length or .stat.size"
end
else
request.body = data.to_s
request["Content-Length"] = request.body.length.to_s
end
end
request
end
def marshal_dump(*args)
{:key => @key, :secret => @secret, :options => @options}
end
def marshal_load(data)
initialize(data[:key], data[:secret], data[:options])
end
end
end