~launchpad-pqm/launchpad/devel

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)