PageRenderTime 52ms CodeModel.GetById 20ms app.highlight 27ms RepoModel.GetById 1ms app.codeStats 0ms

/gdata/oauth/__init__.py

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