12328.1.1
by Julian Edwards
Revert r12325 as the new oauth module is buggy and causes a test to fail |
1 |
# pylint: disable-msg=C0301,E0602,E0211,E0213,W0105,W0231,W0702
|
2 |
||
3 |
import cgi |
|
4 |
import urllib |
|
5 |
import time |
|
6 |
import random |
|
7 |
import urlparse |
|
8 |
import hmac |
|
9 |
import base64 |
|
10 |
||
11 |
VERSION = '1.0' # Hi Blaine! |
|
12 |
HTTP_METHOD = 'GET' |
|
13 |
SIGNATURE_METHOD = 'PLAINTEXT' |
|
14 |
||
15 |
# Generic exception class
|
|
16 |
class OAuthError(RuntimeError): |
|
17 |
def __init__(self, message='OAuth error occured'): |
|
18 |
self.message = message |
|
19 |
||
20 |
# optional WWW-Authenticate header (401 error)
|
|
21 |
def build_authenticate_header(realm=''): |
|
22 |
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
|
23 |
||
24 |
# url escape
|
|
25 |
def escape(s): |
|
26 |
# escape '/' too
|
|
27 |
return urllib.quote(s, safe='~') |
|
28 |
||
29 |
# util function: current timestamp
|
|
30 |
# seconds since epoch (UTC)
|
|
31 |
def generate_timestamp(): |
|
32 |
return int(time.time()) |
|
33 |
||
34 |
# util function: nonce
|
|
35 |
# pseudorandom number
|
|
36 |
def generate_nonce(length=8): |
|
37 |
return ''.join(str(random.randint(0, 9)) for i in range(length)) |
|
38 |
||
39 |
# OAuthConsumer is a data type that represents the identity of the Consumer
|
|
40 |
# via its shared secret with the Service Provider.
|
|
41 |
class OAuthConsumer(object): |
|
42 |
key = None |
|
43 |
secret = None |
|
44 |
||
45 |
def __init__(self, key, secret): |
|
46 |
self.key = key |
|
47 |
self.secret = secret |
|
48 |
||
49 |
# OAuthToken is a data type that represents an End User via either an access
|
|
50 |
# or request token.
|
|
51 |
class OAuthToken(object): |
|
52 |
# access tokens and request tokens
|
|
53 |
key = None |
|
54 |
secret = None |
|
55 |
||
56 |
'''
|
|
57 |
key = the token
|
|
58 |
secret = the token secret
|
|
59 |
'''
|
|
60 |
def __init__(self, key, secret): |
|
61 |
self.key = key |
|
62 |
self.secret = secret |
|
63 |
||
64 |
def to_string(self): |
|
65 |
return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) |
|
66 |
||
67 |
# return a token from something like:
|
|
68 |
# oauth_token_secret=digg&oauth_token=digg
|
|
69 |
@staticmethod
|
|
70 |
def from_string(s): |
|
71 |
params = cgi.parse_qs(s, keep_blank_values=False) |
|
72 |
key = params['oauth_token'][0] |
|
73 |
secret = params['oauth_token_secret'][0] |
|
74 |
return OAuthToken(key, secret) |
|
75 |
||
76 |
def __str__(self): |
|
77 |
return self.to_string() |
|
78 |
||
79 |
# OAuthRequest represents the request and can be serialized
|
|
80 |
class OAuthRequest(object): |
|
81 |
'''
|
|
82 |
OAuth parameters:
|
|
83 |
- oauth_consumer_key
|
|
84 |
- oauth_token
|
|
85 |
- oauth_signature_method
|
|
86 |
- oauth_signature
|
|
87 |
- oauth_timestamp
|
|
88 |
- oauth_nonce
|
|
89 |
- oauth_version
|
|
90 |
... any additional parameters, as defined by the Service Provider.
|
|
91 |
'''
|
|
92 |
parameters = None # oauth parameters |
|
93 |
http_method = HTTP_METHOD |
|
94 |
http_url = None |
|
95 |
version = VERSION |
|
96 |
||
97 |
def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): |
|
98 |
self.http_method = http_method |
|
99 |
self.http_url = http_url |
|
100 |
self.parameters = parameters or {} |
|
101 |
||
102 |
def set_parameter(self, parameter, value): |
|
103 |
self.parameters[parameter] = value |
|
104 |
||
105 |
def get_parameter(self, parameter): |
|
106 |
try: |
|
107 |
return self.parameters[parameter] |
|
108 |
except: |
|
109 |
raise OAuthError('Parameter not found: %s' % parameter) |
|
110 |
||
111 |
def _get_timestamp_nonce(self): |
|
112 |
return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') |
|
113 |
||
114 |
# get any non-oauth parameters
|
|
115 |
def get_nonoauth_parameters(self): |
|
116 |
parameters = {} |
|
117 |
for k, v in self.parameters.iteritems(): |
|
118 |
# ignore oauth parameters
|
|
119 |
if k.find('oauth_') < 0: |
|
120 |
parameters[k] = v |
|
121 |
return parameters |
|
122 |
||
123 |
# serialize as a header for an HTTPAuth request
|
|
124 |
def to_header(self, realm=''): |
|
125 |
auth_header = 'OAuth realm="%s"' % realm |
|
126 |
# add the oauth parameters
|
|
127 |
if self.parameters: |
|
128 |
for k, v in self.parameters.iteritems(): |
|
129 |
auth_header += ', %s="%s"' % (k, v) |
|
130 |
return {'Authorization': auth_header} |
|
131 |
||
132 |
# serialize as post data for a POST request
|
|
133 |
def to_postdata(self): |
|
134 |
return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()) |
|
135 |
||
136 |
# serialize as a url for a GET request
|
|
137 |
def to_url(self): |
|
138 |
return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) |
|
139 |
||
140 |
# return a string that consists of all the parameters that need to be signed
|
|
141 |
def get_normalized_parameters(self): |
|
142 |
params = self.parameters |
|
143 |
try: |
|
144 |
# exclude the signature if it exists
|
|
145 |
del params['oauth_signature'] |
|
146 |
except: |
|
147 |
pass
|
|
148 |
key_values = params.items() |
|
149 |
# sort lexicographically, first after key, then after value
|
|
150 |
key_values.sort() |
|
151 |
# combine key value pairs in string and escape
|
|
152 |
return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values) |
|
153 |
||
154 |
# just uppercases the http method
|
|
155 |
def get_normalized_http_method(self): |
|
156 |
return self.http_method.upper() |
|
157 |
||
158 |
# parses the url and rebuilds it to be scheme://host/path
|
|
159 |
def get_normalized_http_url(self): |
|
160 |
parts = urlparse.urlparse(self.http_url) |
|
161 |
url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path |
|
162 |
return url_string |
|
163 |
||
164 |
# set the signature parameter to the result of build_signature
|
|
165 |
def sign_request(self, signature_method, consumer, token): |
|
166 |
# set the signature method
|
|
167 |
self.set_parameter('oauth_signature_method', signature_method.get_name()) |
|
168 |
# set the signature
|
|
169 |
self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) |
|
170 |
||
171 |
def build_signature(self, signature_method, consumer, token): |
|
172 |
# call the build signature method within the signature method
|
|
173 |
return signature_method.build_signature(self, consumer, token) |
|
174 |
||
175 |
@staticmethod
|
|
176 |
def from_request(http_method, http_url, headers=None, postdata=None, parameters=None): |
|
177 |
||
178 |
# let the library user override things however they'd like, if they know
|
|
179 |
# which parameters to use then go for it, for example XMLRPC might want to
|
|
180 |
# do this
|
|
181 |
if parameters is not None: |
|
182 |
return OAuthRequest(http_method, http_url, parameters) |
|
183 |
||
184 |
# from the headers
|
|
185 |
if headers is not None: |
|
186 |
try: |
|
187 |
auth_header = headers['Authorization'] |
|
188 |
# check that the authorization header is OAuth
|
|
189 |
auth_header.index('OAuth') |
|
190 |
# get the parameters from the header
|
|
191 |
parameters = OAuthRequest._split_header(auth_header) |
|
192 |
return OAuthRequest(http_method, http_url, parameters) |
|
193 |
except: |
|
194 |
raise OAuthError('Unable to parse OAuth parameters from Authorization header.') |
|
195 |
||
196 |
# from the parameter string (post body)
|
|
197 |
if http_method == 'POST' and postdata is not None: |
|
198 |
parameters = OAuthRequest._split_url_string(postdata) |
|
199 |
||
200 |
# from the url string
|
|
201 |
elif http_method == 'GET': |
|
202 |
param_str = urlparse.urlparse(http_url).query |
|
203 |
parameters = OAuthRequest._split_url_string(param_str) |
|
204 |
||
205 |
if parameters: |
|
206 |
return OAuthRequest(http_method, http_url, parameters) |
|
207 |
||
208 |
raise OAuthError('Missing all OAuth parameters. OAuth parameters must be in the headers, post body, or url.') |
|
209 |
||
210 |
@staticmethod
|
|
211 |
def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): |
|
212 |
if not parameters: |
|
213 |
parameters = {} |
|
214 |
||
215 |
defaults = { |
|
216 |
'oauth_consumer_key': oauth_consumer.key, |
|
217 |
'oauth_timestamp': generate_timestamp(), |
|
218 |
'oauth_nonce': generate_nonce(), |
|
219 |
'oauth_version': OAuthRequest.version, |
|
220 |
}
|
|
221 |
||
222 |
defaults.update(parameters) |
|
223 |
parameters = defaults |
|
224 |
||
225 |
if token: |
|
226 |
parameters['oauth_token'] = token.key |
|
227 |
||
228 |
return OAuthRequest(http_method, http_url, parameters) |
|
229 |
||
230 |
@staticmethod
|
|
231 |
def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): |
|
232 |
if not parameters: |
|
233 |
parameters = {} |
|
234 |
||
235 |
parameters['oauth_token'] = token.key |
|
236 |
||
237 |
if callback: |
|
238 |
parameters['oauth_callback'] = escape(callback) |
|
239 |
||
240 |
return OAuthRequest(http_method, http_url, parameters) |
|
241 |
||
242 |
# util function: turn Authorization: header into parameters, has to do some unescaping
|
|
243 |
@staticmethod
|
|
244 |
def _split_header(header): |
|
245 |
params = {} |
|
246 |
header = header.lstrip() |
|
247 |
if not header.startswith('OAuth '): |
|
248 |
raise ValueError("not an OAuth header: %r" % header) |
|
249 |
header = header[6:] |
|
250 |
parts = header.split(',') |
|
251 |
for param in parts: |
|
252 |
# remove whitespace
|
|
253 |
param = param.strip() |
|
254 |
# split key-value
|
|
255 |
param_parts = param.split('=', 1) |
|
256 |
if param_parts[0] == 'realm': |
|
257 |
# Realm header is not an OAuth parameter according to rfc5849
|
|
258 |
# section 3.4.1.3.1.
|
|
259 |
continue
|
|
260 |
# remove quotes and unescape the value
|
|
261 |
params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) |
|
262 |
return params |
|
263 |
||
264 |
# util function: turn url string into parameters, has to do some unescaping
|
|
265 |
@staticmethod
|
|
266 |
def _split_url_string(param_str): |
|
267 |
parameters = cgi.parse_qs(param_str, keep_blank_values=False) |
|
268 |
for k, v in parameters.iteritems(): |
|
269 |
parameters[k] = urllib.unquote(v[0]) |
|
270 |
return parameters |
|
271 |
||
272 |
# OAuthServer is a worker to check a requests validity against a data store
|
|
273 |
class OAuthServer(object): |
|
274 |
timestamp_threshold = 300 # in seconds, five minutes |
|
275 |
version = VERSION |
|
276 |
signature_methods = None |
|
277 |
data_store = None |
|
278 |
||
279 |
def __init__(self, data_store=None, signature_methods=None): |
|
280 |
self.data_store = data_store |
|
281 |
self.signature_methods = signature_methods or {} |
|
282 |
||
283 |
def set_data_store(self, oauth_data_store): |
|
284 |
self.data_store = oauth_data_store |
|
285 |
||
286 |
def get_data_store(self): |
|
287 |
return self.data_store |
|
288 |
||
289 |
def add_signature_method(self, signature_method): |
|
290 |
self.signature_methods[signature_method.get_name()] = signature_method |
|
291 |
return self.signature_methods |
|
292 |
||
293 |
# process a request_token request
|
|
294 |
# returns the request token on success
|
|
295 |
def fetch_request_token(self, oauth_request): |
|
296 |
try: |
|
297 |
# get the request token for authorization
|
|
298 |
token = self._get_token(oauth_request, 'request') |
|
299 |
except: |
|
300 |
# no token required for the initial token request
|
|
301 |
version = self._get_version(oauth_request) |
|
302 |
consumer = self._get_consumer(oauth_request) |
|
303 |
self._check_signature(oauth_request, consumer, None) |
|
304 |
# fetch a new token
|
|
305 |
token = self.data_store.fetch_request_token(consumer) |
|
306 |
return token |
|
307 |
||
308 |
# process an access_token request
|
|
309 |
# returns the access token on success
|
|
310 |
def fetch_access_token(self, oauth_request): |
|
311 |
version = self._get_version(oauth_request) |
|
312 |
consumer = self._get_consumer(oauth_request) |
|
313 |
# get the request token
|
|
314 |
token = self._get_token(oauth_request, 'request') |
|
315 |
self._check_signature(oauth_request, consumer, token) |
|
316 |
new_token = self.data_store.fetch_access_token(consumer, token) |
|
317 |
return new_token |
|
318 |
||
319 |
# verify an api call, checks all the parameters
|
|
320 |
def verify_request(self, oauth_request): |
|
321 |
# -> consumer and token
|
|
322 |
version = self._get_version(oauth_request) |
|
323 |
consumer = self._get_consumer(oauth_request) |
|
324 |
# get the access token
|
|
325 |
token = self._get_token(oauth_request, 'access') |
|
326 |
self._check_signature(oauth_request, consumer, token) |
|
327 |
parameters = oauth_request.get_nonoauth_parameters() |
|
328 |
return consumer, token, parameters |
|
329 |
||
330 |
# authorize a request token
|
|
331 |
def authorize_token(self, token, user): |
|
332 |
return self.data_store.authorize_request_token(token, user) |
|
333 |
||
334 |
# get the callback url
|
|
335 |
def get_callback(self, oauth_request): |
|
336 |
return oauth_request.get_parameter('oauth_callback') |
|
337 |
||
338 |
# optional support for the authenticate header
|
|
339 |
def build_authenticate_header(self, realm=''): |
|
340 |
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} |
|
341 |
||
342 |
# verify the correct version request for this server
|
|
343 |
def _get_version(self, oauth_request): |
|
344 |
try: |
|
345 |
version = oauth_request.get_parameter('oauth_version') |
|
346 |
except: |
|
347 |
version = VERSION |
|
348 |
if version and version != self.version: |
|
349 |
raise OAuthError('OAuth version %s not supported' % str(version)) |
|
350 |
return version |
|
351 |
||
352 |
# figure out the signature with some defaults
|
|
353 |
def _get_signature_method(self, oauth_request): |
|
354 |
try: |
|
355 |
signature_method = oauth_request.get_parameter('oauth_signature_method') |
|
356 |
except: |
|
357 |
signature_method = SIGNATURE_METHOD |
|
358 |
try: |
|
359 |
# get the signature method object
|
|
360 |
signature_method = self.signature_methods[signature_method] |
|
361 |
except: |
|
362 |
signature_method_names = ', '.join(self.signature_methods.keys()) |
|
363 |
raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) |
|
364 |
||
365 |
return signature_method |
|
366 |
||
367 |
def _get_consumer(self, oauth_request): |
|
368 |
consumer_key = oauth_request.get_parameter('oauth_consumer_key') |
|
369 |
if not consumer_key: |
|
370 |
raise OAuthError('Invalid consumer key') |
|
371 |
consumer = self.data_store.lookup_consumer(consumer_key) |
|
372 |
if not consumer: |
|
373 |
raise OAuthError('Invalid consumer') |
|
374 |
return consumer |
|
375 |
||
376 |
# try to find the token for the provided request token key
|
|
377 |
def _get_token(self, oauth_request, token_type='access'): |
|
378 |
token_field = oauth_request.get_parameter('oauth_token') |
|
379 |
token = self.data_store.lookup_token(token_type, token_field) |
|
380 |
if not token: |
|
381 |
raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) |
|
382 |
return token |
|
383 |
||
384 |
def _check_signature(self, oauth_request, consumer, token): |
|
385 |
timestamp, nonce = oauth_request._get_timestamp_nonce() |
|
386 |
self._check_timestamp(timestamp) |
|
387 |
self._check_nonce(consumer, token, nonce) |
|
388 |
signature_method = self._get_signature_method(oauth_request) |
|
389 |
try: |
|
390 |
signature = oauth_request.get_parameter('oauth_signature') |
|
391 |
except: |
|
392 |
raise OAuthError('Missing signature') |
|
393 |
# attempt to construct the same signature
|
|
394 |
built = signature_method.build_signature(oauth_request, consumer, token) |
|
395 |
if signature != built: |
|
396 |
key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) |
|
397 |
raise OAuthError('Signature does not match. Expected: %s Got: %s Expected signature base string: %s' % (built, signature, base)) |
|
398 |
||
399 |
def _check_timestamp(self, timestamp): |
|
400 |
# verify that timestamp is recentish
|
|
401 |
timestamp = int(timestamp) |
|
402 |
now = int(time.time()) |
|
403 |
lapsed = now - timestamp |
|
404 |
if lapsed > self.timestamp_threshold: |
|
405 |
raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) |
|
406 |
||
407 |
def _check_nonce(self, consumer, token, nonce): |
|
408 |
# verify that the nonce is uniqueish
|
|
409 |
try: |
|
410 |
self.data_store.lookup_nonce(consumer, token, nonce) |
|
411 |
raise OAuthError('Nonce already used: %s' % str(nonce)) |
|
412 |
except: |
|
413 |
pass
|
|
414 |
||
415 |
# OAuthClient is a worker to attempt to execute a request
|
|
416 |
class OAuthClient(object): |
|
417 |
consumer = None |
|
418 |
token = None |
|
419 |
||
420 |
def __init__(self, oauth_consumer, oauth_token): |
|
421 |
self.consumer = oauth_consumer |
|
422 |
self.token = oauth_token |
|
423 |
||
424 |
def get_consumer(self): |
|
425 |
return self.consumer |
|
426 |
||
427 |
def get_token(self): |
|
428 |
return self.token |
|
429 |
||
430 |
def fetch_request_token(self, oauth_request): |
|
431 |
# -> OAuthToken
|
|
432 |
raise NotImplementedError |
|
433 |
||
434 |
def fetch_access_token(self, oauth_request): |
|
435 |
# -> OAuthToken
|
|
436 |
raise NotImplementedError |
|
437 |
||
438 |
def access_resource(self, oauth_request): |
|
439 |
# -> some protected resource
|
|
440 |
raise NotImplementedError |
|
441 |
||
442 |
# OAuthDataStore is a database abstraction used to lookup consumers and tokens
|
|
443 |
class OAuthDataStore(object): |
|
444 |
||
445 |
def lookup_consumer(self, key): |
|
446 |
# -> OAuthConsumer
|
|
447 |
raise NotImplementedError |
|
448 |
||
449 |
def lookup_token(self, oauth_consumer, token_type, token_token): |
|
450 |
# -> OAuthToken
|
|
451 |
raise NotImplementedError |
|
452 |
||
453 |
def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): |
|
454 |
# -> OAuthToken
|
|
455 |
raise NotImplementedError |
|
456 |
||
457 |
def fetch_request_token(self, oauth_consumer): |
|
458 |
# -> OAuthToken
|
|
459 |
raise NotImplementedError |
|
460 |
||
461 |
def fetch_access_token(self, oauth_consumer, oauth_token): |
|
462 |
# -> OAuthToken
|
|
463 |
raise NotImplementedError |
|
464 |
||
465 |
def authorize_request_token(self, oauth_token, user): |
|
466 |
# -> OAuthToken
|
|
467 |
raise NotImplementedError |
|
468 |
||
469 |
# OAuthSignatureMethod is a strategy class that implements a signature method
|
|
470 |
class OAuthSignatureMethod(object): |
|
471 |
def get_name(): |
|
472 |
# -> str
|
|
473 |
raise NotImplementedError |
|
474 |
||
475 |
def build_signature_base_string(oauth_request, oauth_consumer, oauth_token): |
|
476 |
# -> str key, str raw
|
|
477 |
raise NotImplementedError |
|
478 |
||
479 |
def build_signature(oauth_request, oauth_consumer, oauth_token): |
|
480 |
# -> str
|
|
481 |
raise NotImplementedError |
|
482 |
||
483 |
class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): |
|
484 |
||
485 |
def get_name(self): |
|
486 |
return 'HMAC-SHA1' |
|
487 |
||
488 |
def build_signature_base_string(self, oauth_request, consumer, token): |
|
489 |
sig = ( |
|
490 |
escape(oauth_request.get_normalized_http_method()), |
|
491 |
escape(oauth_request.get_normalized_http_url()), |
|
492 |
escape(oauth_request.get_normalized_parameters()), |
|
493 |
)
|
|
494 |
||
495 |
key = '%s&' % escape(consumer.secret) |
|
496 |
if token: |
|
497 |
key += escape(token.secret) |
|
498 |
raw = '&'.join(sig) |
|
499 |
return key, raw |
|
500 |
||
501 |
def build_signature(self, oauth_request, consumer, token): |
|
502 |
# build the base signature string
|
|
503 |
key, raw = self.build_signature_base_string(oauth_request, consumer, token) |
|
504 |
||
505 |
# hmac object
|
|
506 |
try: |
|
507 |
import hashlib # 2.5 |
|
508 |
hashed = hmac.new(key, raw, hashlib.sha1) |
|
509 |
except: |
|
510 |
import sha # deprecated |
|
511 |
hashed = hmac.new(key, raw, sha) |
|
512 |
||
513 |
# calculate the digest base 64
|
|
514 |
return base64.b64encode(hashed.digest()) |
|
515 |
||
516 |
class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): |
|
517 |
||
518 |
def get_name(self): |
|
519 |
return 'PLAINTEXT' |
|
520 |
||
521 |
def build_signature_base_string(self, oauth_request, consumer, token): |
|
522 |
# concatenate the consumer key and secret
|
|
523 |
sig = escape(consumer.secret) |
|
524 |
if token: |
|
525 |
sig = '&'.join((sig, escape(token.secret))) |
|
526 |
return sig |
|
527 |
||
528 |
def build_signature(self, oauth_request, consumer, token): |
|
529 |
return self.build_signature_base_string(oauth_request, consumer, token) |