## SouncCloudAPI implements a Python wrapper around the SoundCloud RESTful ## API ## ## Copyright (C) 2008 Diez B. Roggisch ## Contact mailto:deets@soundcloud.com ## ## This library is free software; you can redistribute it and/or ## modify it under the terms of the GNU Lesser General Public ## License as published by the Free Software Foundation; either ## version 2.1 of the License, or (at your option) any later version. ## ## This library is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## Lesser General Public License for more details. ## ## You should have received a copy of the GNU Lesser General Public ## License along with this library; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import base64 import time, random import urlparse import hmac import hashlib from scapi.util import escape import logging USE_DOUBLE_ESCAPE_HACK = True """ There seems to be an uncertainty on the way parameters are to be escaped. For now, this variable switches between two escaping mechanisms. If True, the passed parameters - GET or POST - are escaped *twice*. """ logger = logging.getLogger(__name__) class OAuthSignatureMethod_HMAC_SHA1(object): FORBIDDEN = ['realm', 'oauth_signature'] def get_name(self): return 'HMAC-SHA1' def build_signature(self, request, parameters, consumer_secret, token_secret, oauth_parameters): if logger.level == logging.DEBUG: logger.debug("request: %r", request) logger.debug("parameters: %r", parameters) logger.debug("consumer_secret: %r", consumer_secret) logger.debug("token_secret: %r", token_secret) logger.debug("oauth_parameters: %r", oauth_parameters) temp = {} temp.update(oauth_parameters) for p in self.FORBIDDEN: if p in temp: del temp[p] if parameters is not None: temp.update(parameters) sig = ( escape(self.get_normalized_http_method(request)), escape(self.get_normalized_http_url(request)), self.get_normalized_parameters(temp), # these are escaped in the method already ) key = '%s&' % consumer_secret if token_secret is not None: key += token_secret raw = '&'.join(sig) logger.debug("raw basestring: %s", raw) logger.debug("key: %s", key) # hmac object hashed = hmac.new(key, raw, hashlib.sha1) # calculate the digest base 64 signature = escape(base64.b64encode(hashed.digest())) return signature def get_normalized_http_method(self, request): return request.get_method().upper() # parses the url and rebuilds it to be scheme://host/path def get_normalized_http_url(self, request): url = request.get_full_url() parts = urlparse.urlparse(url) url_string = '%s://%s%s' % (parts.scheme, parts.netloc, parts.path) return url_string def get_normalized_parameters(self, params): if params is None: params = {} try: # exclude the signature if it exists del params['oauth_signature'] except: pass key_values = [] for key, values in params.iteritems(): if isinstance(values, file): continue if isinstance(values, (int, long, float)): values = str(values) if isinstance(values, (list, tuple)): values = [str(v) for v in values] if isinstance(values, basestring): values = [values] if USE_DOUBLE_ESCAPE_HACK and not key.startswith("ouath"): key = escape(key) for v in values: v = v.encode("utf-8") key = key.encode("utf-8") if USE_DOUBLE_ESCAPE_HACK and not key.startswith("oauth"): # this is a dirty hack to make the # thing work with the current server-side # implementation. Or is it by spec? v = escape(v) key_values.append(escape("%s=%s" % (key, v))) # sort lexicographically, first after key, then after value key_values.sort() # combine key value pairs in string return escape('&').join(key_values) class OAuthAuthenticator(object): OAUTH_API_VERSION = '1.0' AUTHORIZATION_HEADER = "Authorization" def __init__(self, consumer=None, consumer_secret=None, token=None, secret=None, signature_method=OAuthSignatureMethod_HMAC_SHA1()): if consumer == None: raise ValueError("The consumer key must be passed for all public requests; it may not be None") self._consumer, self._token, self._secret = consumer, token, secret self._consumer_secret = consumer_secret self._signature_method = signature_method random.seed() def augment_request(self, req, parameters, use_multipart=False, oauth_callback=None, oauth_verifier=None): oauth_parameters = { 'oauth_consumer_key': self._consumer, 'oauth_timestamp': self.generate_timestamp(), 'oauth_nonce': self.generate_nonce(), 'oauth_version': self.OAUTH_API_VERSION, 'oauth_signature_method': self._signature_method.get_name(), #'realm' : "http://soundcloud.com", } if self._token is not None: oauth_parameters['oauth_token'] = self._token if oauth_callback is not None: oauth_parameters['oauth_callback'] = oauth_callback if oauth_verifier is not None: oauth_parameters['oauth_verifier'] = oauth_verifier # in case we upload large files, we don't # sign the request over the parameters # There's a bug in the OAuth 1.0 (and a) specs that says that PUT request should omit parameters from the base string. # This is fixed in the IETF draft, don't know when this will be released though. - HT if use_multipart or req.get_method() == 'PUT': parameters = None oauth_parameters['oauth_signature'] = self._signature_method.build_signature(req, parameters, self._consumer_secret, self._secret, oauth_parameters) def to_header(d): return ",".join('%s="%s"' % (key, value) for key, value in sorted(oauth_parameters.items())) req.add_header(self.AUTHORIZATION_HEADER, "OAuth %s" % to_header(oauth_parameters)) def generate_timestamp(self): return int(time.time())# * 1000.0) def generate_nonce(self, length=8): return ''.join(str(random.randint(0, 9)) for i in range(length)) class BasicAuthenticator(object): def __init__(self, user, password, consumer, consumer_secret): self._base64string = base64.encodestring("%s:%s" % (user, password))[:-1] self._x_auth_header = 'OAuth oauth_consumer_key="%s" oauth_consumer_secret="%s"' % (consumer, consumer_secret) def augment_request(self, req, parameters): req.add_header("Authorization", "Basic %s" % self._base64string) req.add_header("X-Authorization", self._x_auth_header)