[snowy] Update django-piston to 247:72a636fb7f12 (OAuth 1.0a)
- From: Sanford Armstrong <sharm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [snowy] Update django-piston to 247:72a636fb7f12 (OAuth 1.0a)
- Date: Wed, 16 Jun 2010 14:28:38 +0000 (UTC)
commit d525901789cdf92cd6259c13fcccc2f82b8cfbf2
Author: Olivier Le Thanh Duong <olivier lethanh be>
Date: Tue Jun 1 03:12:59 2010 +0200
Update django-piston to 247:72a636fb7f12 (OAuth 1.0a)
This version of django-piston implement OAuth 1.0a
CAUTION : This update will break existing installation since
it add a new column in the token model
https://bugzilla.gnome.org/show_bug.cgi?id=620126
lib/piston/authentication.py | 36 +++-
lib/piston/doc.py | 2 +-
lib/piston/emitters.py | 45 +++-
lib/piston/fixtures/models.json | 46 +++++
lib/piston/fixtures/oauth.json | 27 +++
lib/piston/forms.py | 2 +-
lib/piston/handler.py | 49 +++--
lib/piston/models.py | 136 ++++++++-----
lib/piston/oauth.py | 426 +++++++++++++++++++++++++--------------
lib/piston/resource.py | 218 +++++++++++++-------
lib/piston/signals.py | 14 ++
lib/piston/store.py | 11 +-
lib/piston/test.py | 62 ++++++
lib/piston/tests.py | 175 ++++++++++++++++
lib/piston/utils.py | 145 +++++++++++---
lib/piston/validate_jsonp.py | 210 +++++++++++++++++++
16 files changed, 1252 insertions(+), 352 deletions(-)
---
diff --git a/lib/piston/authentication.py b/lib/piston/authentication.py
index c0bd1cc..7d09707 100644
--- a/lib/piston/authentication.py
+++ b/lib/piston/authentication.py
@@ -60,7 +60,7 @@ class HttpBasicAuthentication(object):
request.user = self.auth_func(username=username, password=password) \
or AnonymousUser()
-
+
return not request.user in (False, None, AnonymousUser())
def challenge(self):
@@ -69,6 +69,20 @@ class HttpBasicAuthentication(object):
resp.status_code = 401
return resp
+ def __repr__(self):
+ return u'<HTTPBasic: realm=%s>' % self.realm
+
+class HttpBasicSimple(HttpBasicAuthentication):
+ def __init__(self, realm, username, password):
+ self.user = User.objects.get(username=username)
+ self.password = password
+
+ super(HttpBasicSimple, self).__init__(auth_func=self.hash, realm=realm)
+
+ def hash(self, username, password):
+ if username == self.user.username and password == self.password:
+ return self.user
+
def load_data_store():
'''Load data store for OAuth Consumers, Tokens, Nonces and Resources
'''
@@ -97,9 +111,19 @@ def initialize_server_request(request):
"""
Shortcut for initialization.
"""
+ if request.method == "POST": #and \
+# request.META['CONTENT_TYPE'] == "application/x-www-form-urlencoded":
+ params = dict(request.REQUEST.items())
+ else:
+ params = { }
+
+ # Seems that we want to put HTTP_AUTHORIZATION into 'Authorization'
+ # for oauth.py to understand. Lovely.
+ request.META['Authorization'] = request.META.get('HTTP_AUTHORIZATION', '')
+
oauth_request = oauth.OAuthRequest.from_request(
request.method, request.build_absolute_uri(),
- headers=request.META, parameters=dict(request.REQUEST.items()),
+ headers=request.META, parameters=params,
query_string=request.environ.get('QUERY_STRING', ''))
if oauth_request:
@@ -143,8 +167,8 @@ def oauth_request_token(request):
def oauth_auth_view(request, token, callback, params):
form = forms.OAuthAuthenticationForm(initial={
'oauth_token': token.key,
- 'oauth_callback': callback,
- })
+ 'oauth_callback': token.get_callback_url() or callback,
+ })
return render_to_response('piston/authorize_token.html',
{ 'form': form }, RequestContext(request))
@@ -165,7 +189,7 @@ def oauth_user_auth(request):
callback = oauth_server.get_callback(oauth_request)
except:
callback = None
-
+
if request.method == "GET":
params = oauth_request.get_normalized_parameters()
@@ -182,6 +206,7 @@ def oauth_user_auth(request):
args = '?'+token.to_string(only_key=True)
else:
args = '?error=%s' % 'Access not granted by user.'
+ print "FORM ERROR", form.errors
if not callback:
callback = getattr(settings, 'OAUTH_CALLBACK_VIEW')
@@ -235,6 +260,7 @@ class OAuthAuthentication(object):
if consumer and token:
request.user = token.user
+ request.consumer = consumer
request.throttle_extra = token.consumer.id
return True
diff --git a/lib/piston/doc.py b/lib/piston/doc.py
index 63f89ec..08b3343 100644
--- a/lib/piston/doc.py
+++ b/lib/piston/doc.py
@@ -105,7 +105,7 @@ class HandlerDocumentation(object):
@property
def is_anonymous(self):
- return handler.is_anonymous
+ return self.handler.is_anonymous
def get_model(self):
return getattr(self, 'model', None)
diff --git a/lib/piston/emitters.py b/lib/piston/emitters.py
index 92db0a7..912228e 100644
--- a/lib/piston/emitters.py
+++ b/lib/piston/emitters.py
@@ -31,6 +31,7 @@ from django.http import HttpResponse
from django.core import serializers
from utils import HttpStatusCode, Mimer
+from validate_jsonp import is_valid_jsonp_callback_value
try:
import cStringIO as StringIO
@@ -52,8 +53,15 @@ class Emitter(object):
conveniently returns a serialized `dict`. This is
usually the only method you want to use in your
emitter. See below for examples.
+
+ `RESERVED_FIELDS` was introduced when better resource
+ method detection came, and we accidentially caught these
+ as the methods on the handler. Issue58 says that's no good.
"""
EMITTERS = { }
+ RESERVED_FIELDS = set([ 'read', 'update', 'create',
+ 'delete', 'model', 'anonymous',
+ 'allowed_methods', 'fields', 'exclude' ])
def __init__(self, payload, typemapper, handler, fields=(), anonymous=True):
self.typemapper = typemapper
@@ -65,17 +73,18 @@ class Emitter(object):
if isinstance(self.data, Exception):
raise
- def method_fields(self, data, fields):
- if not data:
+ def method_fields(self, handler, fields):
+ if not handler:
return { }
- has = dir(data)
ret = dict()
- for field in fields:
- if field in has and callable(field):
- ret[field] = getattr(data, field)
-
+ for field in fields - Emitter.RESERVED_FIELDS:
+ t = getattr(handler, str(field), None)
+
+ if t and callable(t):
+ ret[field] = t
+
return ret
def construct(self):
@@ -111,6 +120,8 @@ class Emitter(object):
f = thing.__emittable__
if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
ret = _any(f())
+ elif repr(thing).startswith("<django.db.models.fields.related.RelatedManager"):
+ ret = _any(thing.all())
else:
ret = smart_unicode(thing, strings_only=True)
@@ -176,7 +187,7 @@ class Emitter(object):
get_fields = set(fields)
met_fields = self.method_fields(handler, get_fields)
-
+
for f in data._meta.local_fields:
if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
if not f.rel:
@@ -370,11 +381,11 @@ class JSONEmitter(Emitter):
JSON emitter, understands timestamps.
"""
def render(self, request):
- cb = request.GET.get('callback')
+ cb = request.GET.get('callback', None)
seria = simplejson.dumps(self.construct(), cls=DateTimeAwareJSONEncoder, ensure_ascii=False, indent=4)
# Callback
- if cb:
+ if cb and is_valid_jsonp_callback_value(cb):
return '%s(%s)' % (cb, seria)
return seria
@@ -392,7 +403,7 @@ class YAMLEmitter(Emitter):
if yaml: # Only register yaml if it was import successfully.
Emitter.register('yaml', YAMLEmitter, 'application/x-yaml; charset=utf-8')
- Mimer.register(yaml.load, ('application/x-yaml',))
+ Mimer.register(lambda s: dict(yaml.load(s)), ('application/x-yaml',))
class PickleEmitter(Emitter):
"""
@@ -402,7 +413,17 @@ class PickleEmitter(Emitter):
return pickle.dumps(self.construct())
Emitter.register('pickle', PickleEmitter, 'application/python-pickle')
-Mimer.register(pickle.loads, ('application/python-pickle',))
+
+"""
+WARNING: Accepting arbitrary pickled data is a huge security concern.
+The unpickler has been disabled by default now, and if you want to use
+it, please be aware of what implications it will have.
+
+Read more: http://nadiana.com/python-pickle-insecure
+
+Uncomment the line below to enable it. You're doing so at your own risk.
+"""
+# Mimer.register(pickle.loads, ('application/python-pickle',))
class DjangoEmitter(Emitter):
"""
diff --git a/lib/piston/fixtures/models.json b/lib/piston/fixtures/models.json
new file mode 100644
index 0000000..9520005
--- /dev/null
+++ b/lib/piston/fixtures/models.json
@@ -0,0 +1,46 @@
+[
+ {
+ "pk": 2,
+ "model": "auth.user",
+ "fields": {
+ "username": "pistontestuser",
+ "first_name": "Piston",
+ "last_name": "User",
+ "is_active": true,
+ "is_superuser": false,
+ "is_staff": false,
+ "last_login": "2009-08-03 13:11:53",
+ "groups": [],
+ "user_permissions": [],
+ "password": "sha1$b6c1f$83d5879f3854f6e9d27f393e3bcb4b8db05cf671",
+ "email": "pistontestuser example com",
+ "date_joined": "2009-08-03 13:11:53"
+ }
+ },
+ {
+ "pk": 3,
+ "model": "auth.user",
+ "fields": {
+ "username": "pistontestconsumer",
+ "first_name": "Piston",
+ "last_name": "Consumer",
+ "is_active": true,
+ "is_superuser": false,
+ "is_staff": false,
+ "last_login": "2009-08-03 13:11:53",
+ "groups": [],
+ "user_permissions": [],
+ "password": "sha1$b6c1f$83d5879f3854f6e9d27f393e3bcb4b8db05cf671",
+ "email": "pistontestconsumer example com",
+ "date_joined": "2009-08-03 13:11:53"
+ }
+ },
+ {
+ "pk": 1,
+ "model": "sites.site",
+ "fields": {
+ "domain": "example.com",
+ "name": "example.com"
+ }
+ }
+]
diff --git a/lib/piston/fixtures/oauth.json b/lib/piston/fixtures/oauth.json
new file mode 100644
index 0000000..1c6b913
--- /dev/null
+++ b/lib/piston/fixtures/oauth.json
@@ -0,0 +1,27 @@
+[
+ {
+ "pk": 1,
+ "model": "piston.consumer",
+ "fields": {
+ "status": "accepted",
+ "name": "Piston Test Consumer",
+ "secret": "T5XkNMkcjffDpC9mNQJbyQnJXGsenYbz",
+ "user": 2,
+ "key": "8aZSFj3W54h8J8sCpx",
+ "description": "A test consumer record for Piston unit tests."
+ }
+ },
+ {
+ "pk": 1,
+ "model": "piston.token",
+ "fields": {
+ "is_approved": true,
+ "timestamp": 1249347414,
+ "token_type": 2,
+ "secret": "qSWZq36t7yvkBquetYBkd8JxnuCu9jKk",
+ "user": 2,
+ "key": "Y7358vL5hDBbeP3HHL",
+ "consumer": 1
+ }
+ }
+]
diff --git a/lib/piston/forms.py b/lib/piston/forms.py
index 0cf9d4b..351df7c 100644
--- a/lib/piston/forms.py
+++ b/lib/piston/forms.py
@@ -23,7 +23,7 @@ class ModelForm(forms.ModelForm):
class OAuthAuthenticationForm(forms.Form):
oauth_token = forms.CharField(widget=forms.HiddenInput)
- oauth_callback = forms.CharField(widget=forms.HiddenInput)
+ oauth_callback = forms.CharField(widget=forms.HiddenInput, required=False)
authorize_access = forms.BooleanField(required=True)
csrf_signature = forms.CharField(widget=forms.HiddenInput)
diff --git a/lib/piston/handler.py b/lib/piston/handler.py
index 07e1a65..11161fe 100644
--- a/lib/piston/handler.py
+++ b/lib/piston/handler.py
@@ -1,5 +1,8 @@
+import warnings
+
from utils import rc
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
+from django.conf import settings
typemapper = { }
handler_tracker = [ ]
@@ -11,12 +14,22 @@ class HandlerMetaClass(type):
"""
def __new__(cls, name, bases, attrs):
new_cls = type.__new__(cls, name, bases, attrs)
-
+
+ def already_registered(model, anon):
+ for k, (m, a) in typemapper.iteritems():
+ if model == m and anon == a:
+ return k
+
if hasattr(new_cls, 'model'):
+ if already_registered(new_cls.model, new_cls.is_anonymous):
+ if not getattr(settings, 'PISTON_IGNORE_DUPE_MODELS', False):
+ warnings.warn("Handler already registered for model %s, "
+ "you may experience inconsistent results." % new_cls.model.__name__)
+
typemapper[new_cls] = (new_cls.model, new_cls.is_anonymous)
else:
typemapper[new_cls] = (None, new_cls.is_anonymous)
-
+
if name not in ('BaseHandler', 'AnonymousBaseHandler'):
handler_tracker.append(new_cls)
@@ -27,43 +40,43 @@ class BaseHandler(object):
Basehandler that gives you CRUD for free.
You are supposed to subclass this for specific
functionality.
-
+
All CRUD methods (`read`/`update`/`create`/`delete`)
receive a request as the first argument from the
resource. Use this for checking `request.user`, etc.
"""
__metaclass__ = HandlerMetaClass
-
+
allowed_methods = ('GET', 'POST', 'PUT', 'DELETE')
anonymous = is_anonymous = False
exclude = ( 'id', )
fields = ( )
-
+
def flatten_dict(self, dct):
return dict([ (str(k), dct.get(k)) for k in dct.keys() ])
-
+
def has_model(self):
return hasattr(self, 'model') or hasattr(self, 'queryset')
def queryset(self, request):
return self.model.objects.all()
-
+
def value_from_tuple(tu, name):
for int_, n in tu:
if n == name:
return int_
return None
-
+
def exists(self, **kwargs):
if not self.has_model():
raise NotImplementedError
-
+
try:
self.model.objects.get(**kwargs)
return True
except self.model.DoesNotExist:
return False
-
+
def read(self, request, *args, **kwargs):
if not self.has_model():
return rc.NOT_IMPLEMENTED
@@ -79,13 +92,13 @@ class BaseHandler(object):
return rc.BAD_REQUEST
else:
return self.queryset(request).filter(*args, **kwargs)
-
+
def create(self, request, *args, **kwargs):
if not self.has_model():
return rc.NOT_IMPLEMENTED
-
- attrs = self.flatten_dict(request.POST)
-
+
+ attrs = self.flatten_dict(request.data)
+
try:
inst = self.queryset(request).get(**attrs)
return rc.DUPLICATE_ENTRY
@@ -95,7 +108,7 @@ class BaseHandler(object):
return inst
except self.model.MultipleObjectsReturned:
return rc.DUPLICATE_ENTRY
-
+
def update(self, request, *args, **kwargs):
if not self.has_model():
return rc.NOT_IMPLEMENTED
@@ -113,13 +126,13 @@ class BaseHandler(object):
except MultipleObjectsReturned: # should never happen, since we're using a PK
return rc.BAD_REQUEST
- attrs = self.flatten_dict(request.POST)
+ attrs = self.flatten_dict(request.data)
for k,v in attrs.iteritems():
setattr( inst, k, v )
inst.save()
return rc.ALL_OK
-
+
def delete(self, request, *args, **kwargs):
if not self.has_model():
raise NotImplementedError
@@ -134,7 +147,7 @@ class BaseHandler(object):
return rc.DUPLICATE_ENTRY
except self.model.DoesNotExist:
return rc.NOT_HERE
-
+
class AnonymousBaseHandler(BaseHandler):
"""
Anonymous handler.
diff --git a/lib/piston/models.py b/lib/piston/models.py
index b03eaba..9c93af8 100644
--- a/lib/piston/models.py
+++ b/lib/piston/models.py
@@ -1,19 +1,30 @@
-import urllib
+import urllib, time, urlparse
+
+# Django imports
+from django.db.models.signals import post_save, post_delete
from django.db import models
from django.contrib.auth.models import User
from django.contrib import admin
-from django.conf import settings
from django.core.mail import send_mail, mail_admins
-from django.template import loader
-from managers import TokenManager, ConsumerManager, ResourceManager, KEY_SIZE, SECRET_SIZE
+# Piston imports
+from managers import TokenManager, ConsumerManager, ResourceManager
+from signals import consumer_post_save, consumer_post_delete
+
+KEY_SIZE = 18
+SECRET_SIZE = 32
+VERIFIER_SIZE = 10
CONSUMER_STATES = (
- ('pending', 'Pending approval'),
+ ('pending', 'Pending'),
('accepted', 'Accepted'),
('canceled', 'Canceled'),
+ ('rejected', 'Rejected')
)
+def generate_random(length=SECRET_SIZE):
+ return User.objects.make_random_password(length=length)
+
class Nonce(models.Model):
token_key = models.CharField(max_length=KEY_SIZE)
consumer_key = models.CharField(max_length=KEY_SIZE)
@@ -24,18 +35,6 @@ class Nonce(models.Model):
admin.site.register(Nonce)
-class Resource(models.Model):
- name = models.CharField(max_length=255)
- url = models.TextField(max_length=2047)
- is_readonly = models.BooleanField(default=True)
-
- objects = ResourceManager()
-
- def __unicode__(self):
- return u"Resource %s with url %s" % (self.name, self.url)
-
-admin.site.register(Resource)
-
class Consumer(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
@@ -51,39 +50,26 @@ class Consumer(models.Model):
def __unicode__(self):
return u"Consumer %s with key %s" % (self.name, self.key)
- def save(self, **kwargs):
- super(Consumer, self).save(**kwargs)
-
- if self.id and self.user:
- subject = "API Consumer"
- rcpt = [ self.user.email, ]
-
- if self.status == "accepted":
- template = "api/mails/consumer_accepted.txt"
- subject += " was accepted!"
- elif self.status == "canceled":
- template = "api/mails/consumer_canceled.txt"
- subject += " has been canceled"
- else:
- template = "api/mails/consumer_pending.txt"
- subject += " application received"
-
- for admin in settings.ADMINS:
- bcc.append(admin[1])
-
- body = loader.render_to_string(template,
- { 'consumer': self, 'user': self.user })
-
- send_mail(subject, body, settings.DEFAULT_FROM_EMAIL,
- rcpt, fail_silently=True)
-
- if self.status == 'pending':
- mail_admins(subject, body, fail_silently=True)
-
- if settings.DEBUG:
- print "Mail being sent, to=%s" % rcpt
- print "Subject: %s" % subject
- print body
+ def generate_random_codes(self):
+ """
+ Used to generate random key/secret pairings. Use this after you've
+ added the other data in place of save().
+
+ c = Consumer()
+ c.name = "My consumer"
+ c.description = "An app that makes ponies from the API."
+ c.user = some_user_object
+ c.generate_random_codes()
+ """
+ key = User.objects.make_random_password(length=KEY_SIZE)
+ secret = generate_random(SECRET_SIZE)
+
+ while Consumer.objects.filter(key__exact=key, secret__exact=secret).count():
+ secret = generate_random(SECRET_SIZE)
+
+ self.key = key
+ self.secret = secret
+ self.save()
admin.site.register(Consumer)
@@ -94,13 +80,17 @@ class Token(models.Model):
key = models.CharField(max_length=KEY_SIZE)
secret = models.CharField(max_length=SECRET_SIZE)
+ verifier = models.CharField(max_length=VERIFIER_SIZE)
token_type = models.IntegerField(choices=TOKEN_TYPES)
- timestamp = models.IntegerField()
+ timestamp = models.IntegerField(default=long(time.time()))
is_approved = models.BooleanField(default=False)
user = models.ForeignKey(User, null=True, blank=True, related_name='tokens')
consumer = models.ForeignKey(Consumer)
+ callback = models.CharField(max_length=255, null=True, blank=True)
+ callback_confirmed = models.BooleanField(default=False)
+
objects = TokenManager()
def __unicode__(self):
@@ -109,10 +99,52 @@ class Token(models.Model):
def to_string(self, only_key=False):
token_dict = {
'oauth_token': self.key,
- 'oauth_token_secret': self.secret
+ 'oauth_token_secret': self.secret,
+ 'oauth_callback_confirmed': 'true',
}
+
+ if self.verifier:
+ token_dict.update({ 'oauth_verifier': self.verifier })
+
if only_key:
del token_dict['oauth_token_secret']
+
return urllib.urlencode(token_dict)
+ def generate_random_codes(self):
+ key = User.objects.make_random_password(length=KEY_SIZE)
+ secret = generate_random(SECRET_SIZE)
+
+ while Token.objects.filter(key__exact=key, secret__exact=secret).count():
+ secret = generate_random(SECRET_SIZE)
+
+ self.key = key
+ self.secret = secret
+ self.save()
+
+ # -- OAuth 1.0a stuff
+
+ def get_callback_url(self):
+ if self.callback and self.verifier:
+ # Append the oauth_verifier.
+ parts = urlparse.urlparse(self.callback)
+ scheme, netloc, path, params, query, fragment = parts[:6]
+ if query:
+ query = '%s&oauth_verifier=%s' % (query, self.verifier)
+ else:
+ query = 'oauth_verifier=%s' % self.verifier
+ return urlparse.urlunparse((scheme, netloc, path, params,
+ query, fragment))
+ return self.callback
+
+ def set_callback(self, callback):
+ if callback != "oob": # out of band, says "we can't do this!"
+ self.callback = callback
+ self.callback_confirmed = True
+ self.save()
+
admin.site.register(Token)
+
+# Attach our signals
+post_save.connect(consumer_post_save, sender=Consumer)
+post_delete.connect(consumer_post_delete, sender=Consumer)
diff --git a/lib/piston/oauth.py b/lib/piston/oauth.py
index 1e50f1c..3a42e20 100644
--- a/lib/piston/oauth.py
+++ b/lib/piston/oauth.py
@@ -1,50 +1,81 @@
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
import cgi
import urllib
import time
import random
import urlparse
import hmac
-import base64
+import binascii
+
VERSION = '1.0' # Hi Blaine!
HTTP_METHOD = 'GET'
SIGNATURE_METHOD = 'PLAINTEXT'
-# Generic exception class
-class OAuthError(RuntimeError):
- def get_message(self):
- return self._message
-
- def set_message(self, message):
- self._message = message
-
- message = property(get_message, set_message)
+class OAuthError(RuntimeError):
+ """Generic exception class."""
def __init__(self, message='OAuth error occured.'):
self.message = message
-# optional WWW-Authenticate header (401 error)
def build_authenticate_header(realm=''):
- return { 'WWW-Authenticate': 'OAuth realm="%s"' % realm }
+ """Optional WWW-Authenticate header (401 error)"""
+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
-# url escape
def escape(s):
- # escape '/' too
+ """Escape a URL including any /."""
return urllib.quote(s, safe='~')
-# util function: current timestamp
-# seconds since epoch (UTC)
+def _utf8_str(s):
+ """Convert unicode to utf-8."""
+ if isinstance(s, unicode):
+ return s.encode("utf-8")
+ else:
+ return str(s)
+
def generate_timestamp():
+ """Get seconds since epoch (UTC)."""
return int(time.time())
-# util function: nonce
-# pseudorandom number
def generate_nonce(length=8):
- return ''.join(str(random.randint(0, 9)) for i in range(length))
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+def generate_verifier(length=8):
+ """Generate pseudorandom number."""
+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
-# OAuthConsumer is a data type that represents the identity of the Consumer
-# via its shared secret with the Service Provider.
class OAuthConsumer(object):
+ """Consumer of OAuth authentication.
+
+ OAuthConsumer is a data type that represents the identity of the Consumer
+ via its shared secret with the Service Provider.
+
+ """
key = None
secret = None
@@ -52,39 +83,79 @@ class OAuthConsumer(object):
self.key = key
self.secret = secret
-# OAuthToken is a data type that represents an End User via either an access
-# or request token.
+
class OAuthToken(object):
- # access tokens and request tokens
+ """OAuthToken is a data type that represents an End User via either an access
+ or request token.
+
+ key -- the token
+ secret -- the token secret
+
+ """
key = None
secret = None
+ callback = None
+ callback_confirmed = None
+ verifier = None
- '''
- key = the token
- secret = the token secret
- '''
def __init__(self, key, secret):
self.key = key
self.secret = secret
- def to_string(self):
- return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
+ def set_callback(self, callback):
+ self.callback = callback
+ self.callback_confirmed = 'true'
+
+ def set_verifier(self, verifier=None):
+ if verifier is not None:
+ self.verifier = verifier
+ else:
+ self.verifier = generate_verifier()
+
+ def get_callback_url(self):
+ if self.callback and self.verifier:
+ # Append the oauth_verifier.
+ parts = urlparse.urlparse(self.callback)
+ scheme, netloc, path, params, query, fragment = parts[:6]
+ if query:
+ query = '%s&oauth_verifier=%s' % (query, self.verifier)
+ else:
+ query = 'oauth_verifier=%s' % self.verifier
+ return urlparse.urlunparse((scheme, netloc, path, params,
+ query, fragment))
+ return self.callback
- # return a token from something like:
- # oauth_token_secret=digg&oauth_token=digg
- @staticmethod
+ def to_string(self):
+ data = {
+ 'oauth_token': self.key,
+ 'oauth_token_secret': self.secret,
+ }
+ if self.callback_confirmed is not None:
+ data['oauth_callback_confirmed'] = self.callback_confirmed
+ return urllib.urlencode(data)
+
def from_string(s):
+ """ Returns a token from something like:
+ oauth_token_secret=xxx&oauth_token=xxx
+ """
params = cgi.parse_qs(s, keep_blank_values=False)
key = params['oauth_token'][0]
secret = params['oauth_token_secret'][0]
- return OAuthToken(key, secret)
+ token = OAuthToken(key, secret)
+ try:
+ token.callback_confirmed = params['oauth_callback_confirmed'][0]
+ except KeyError:
+ pass # 1.0, no callback confirmed.
+ return token
+ from_string = staticmethod(from_string)
def __str__(self):
return self.to_string()
-# OAuthRequest represents the request and can be serialized
+
class OAuthRequest(object):
- '''
+ """OAuthRequest represents the request and can be serialized.
+
OAuth parameters:
- oauth_consumer_key
- oauth_token
@@ -93,9 +164,10 @@ class OAuthRequest(object):
- oauth_timestamp
- oauth_nonce
- oauth_version
+ - oauth_verifier
... any additional parameters, as defined by the Service Provider.
- '''
- parameters = None # oauth parameters
+ """
+ parameters = None # OAuth parameters.
http_method = HTTP_METHOD
http_url = None
version = VERSION
@@ -115,93 +187,107 @@ class OAuthRequest(object):
raise OAuthError('Parameter not found: %s' % parameter)
def _get_timestamp_nonce(self):
- return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce')
+ return self.get_parameter('oauth_timestamp'), self.get_parameter(
+ 'oauth_nonce')
- # get any non-oauth parameters
def get_nonoauth_parameters(self):
+ """Get any non-OAuth parameters."""
parameters = {}
for k, v in self.parameters.iteritems():
- # ignore oauth parameters
+ # Ignore oauth parameters.
if k.find('oauth_') < 0:
parameters[k] = v
return parameters
- # serialize as a header for an HTTPAuth request
def to_header(self, realm=''):
+ """Serialize as a header for an HTTPAuth request."""
auth_header = 'OAuth realm="%s"' % realm
- # add the oauth parameters
+ # Add the oauth parameters.
if self.parameters:
for k, v in self.parameters.iteritems():
- auth_header += ', %s="%s"' % (k, escape(str(v)))
+ if k[:6] == 'oauth_':
+ auth_header += ', %s="%s"' % (k, escape(str(v)))
return {'Authorization': auth_header}
- # serialize as post data for a POST request
def to_postdata(self):
- return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems())
+ """Serialize as post data for a POST request."""
+ return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
+ for k, v in self.parameters.iteritems()])
- # serialize as a url for a GET request
def to_url(self):
+ """Serialize as a URL for a GET request."""
return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
- # return a string that consists of all the parameters that need to be signed
def get_normalized_parameters(self):
+ """Return a string that contains the parameters that must be signed."""
params = self.parameters
try:
- # exclude the signature if it exists
+ # Exclude the signature if it exists.
del params['oauth_signature']
except:
pass
- key_values = params.items()
- # sort lexicographically, first after key, then after value
+ # Escape key values before sorting.
+ key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
+ for k,v in params.items()]
+ # Sort lexicographically, first after key, then after value.
key_values.sort()
- # combine key value pairs in string and escape
- return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values)
+ # Combine key value pairs into a string.
+ return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
- # just uppercases the http method
def get_normalized_http_method(self):
+ """Uppercases the http method."""
return self.http_method.upper()
- # parses the url and rebuilds it to be scheme://host/path
def get_normalized_http_url(self):
+ """Parses the URL and rebuilds it to be scheme://host/path."""
parts = urlparse.urlparse(self.http_url)
- url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
- return url_string
-
- # set the signature parameter to the result of build_signature
+ scheme, netloc, path = parts[:3]
+ # Exclude default port numbers.
+ if scheme == 'http' and netloc[-3:] == ':80':
+ netloc = netloc[:-3]
+ elif scheme == 'https' and netloc[-4:] == ':443':
+ netloc = netloc[:-4]
+ return '%s://%s%s' % (scheme, netloc, path)
+
def sign_request(self, signature_method, consumer, token):
- # set the signature method
- self.set_parameter('oauth_signature_method', signature_method.get_name())
- # set the signature
- self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token))
+ """Set the signature parameter to the result of build_signature."""
+ # Set the signature method.
+ self.set_parameter('oauth_signature_method',
+ signature_method.get_name())
+ # Set the signature.
+ self.set_parameter('oauth_signature',
+ self.build_signature(signature_method, consumer, token))
def build_signature(self, signature_method, consumer, token):
- # call the build signature method within the signature method
+ """Calls the build signature method within the signature method."""
return signature_method.build_signature(self, consumer, token)
- @staticmethod
- def from_request(http_method, http_url, headers=None, parameters=None, query_string=None):
- # combine multiple parameter sources
+ def from_request(http_method, http_url, headers=None, parameters=None,
+ query_string=None):
+ """Combines multiple parameter sources."""
if parameters is None:
parameters = {}
- # headers
- if headers and 'HTTP_AUTHORIZATION' in headers:
- auth_header = headers['HTTP_AUTHORIZATION']
- # check that the authorization header is OAuth
- if auth_header.index('OAuth') > -1:
+ # Headers
+ if headers and 'Authorization' in headers:
+ auth_header = headers['Authorization']
+ # Check that the authorization header is OAuth.
+ if auth_header[:6] == 'OAuth ':
+ auth_header = auth_header[6:]
try:
- # get the parameters from the header
+ # Get the parameters from the header.
header_params = OAuthRequest._split_header(auth_header)
parameters.update(header_params)
except:
- raise OAuthError('Unable to parse OAuth parameters from Authorization header.')
+ raise OAuthError('Unable to parse OAuth parameters from '
+ 'Authorization header.')
- # GET or POST query string
+ # GET or POST query string.
if query_string:
query_params = OAuthRequest._split_url_string(query_string)
parameters.update(query_params)
- # URL parameters
+ # URL parameters.
param_str = urlparse.urlparse(http_url)[4] # query
url_params = OAuthRequest._split_url_string(param_str)
parameters.update(url_params)
@@ -210,9 +296,11 @@ class OAuthRequest(object):
return OAuthRequest(http_method, http_url, parameters)
return None
+ from_request = staticmethod(from_request)
- @staticmethod
- def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
+ def from_consumer_and_token(oauth_consumer, token=None,
+ callback=None, verifier=None, http_method=HTTP_METHOD,
+ http_url=None, parameters=None):
if not parameters:
parameters = {}
@@ -228,50 +316,57 @@ class OAuthRequest(object):
if token:
parameters['oauth_token'] = token.key
+ parameters['oauth_callback'] = token.callback
+ # 1.0a support for verifier.
+ parameters['oauth_verifier'] = verifier
+ elif callback:
+ # 1.0a support for callback in the request token request.
+ parameters['oauth_callback'] = callback
return OAuthRequest(http_method, http_url, parameters)
+ from_consumer_and_token = staticmethod(from_consumer_and_token)
- @staticmethod
- def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
+ def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
+ http_url=None, parameters=None):
if not parameters:
parameters = {}
parameters['oauth_token'] = token.key
if callback:
- parameters['oauth_callback'] = escape(callback)
+ parameters['oauth_callback'] = callback
return OAuthRequest(http_method, http_url, parameters)
+ from_token_and_callback = staticmethod(from_token_and_callback)
- # util function: turn Authorization: header into parameters, has to do some unescaping
- @staticmethod
def _split_header(header):
+ """Turn Authorization: header into parameters."""
params = {}
- header = header.replace('OAuth ', '', 1)
parts = header.split(',')
for param in parts:
- # ignore realm parameter
+ # Ignore realm parameter.
if param.find('realm') > -1:
continue
- # remove whitespace
+ # Remove whitespace.
param = param.strip()
- # split key-value
+ # Split key-value.
param_parts = param.split('=', 1)
- # remove quotes and unescape the value
+ # Remove quotes and unescape the value.
params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
return params
-
- # util function: turn url string into parameters, has to do some unescaping
- @staticmethod
+ _split_header = staticmethod(_split_header)
+
def _split_url_string(param_str):
+ """Turn URL string into parameters."""
parameters = cgi.parse_qs(param_str, keep_blank_values=False)
for k, v in parameters.iteritems():
parameters[k] = urllib.unquote(v[0])
return parameters
+ _split_url_string = staticmethod(_split_url_string)
-# OAuthServer is a worker to check a requests validity against a data store
class OAuthServer(object):
- timestamp_threshold = 300 # in seconds, five minutes
+ """A worker to check the validity of a request against a data store."""
+ timestamp_threshold = 300 # In seconds, five minutes.
version = VERSION
signature_methods = None
data_store = None
@@ -280,7 +375,7 @@ class OAuthServer(object):
self.data_store = data_store
self.signature_methods = signature_methods or {}
- def set_data_store(self, oauth_data_store):
+ def set_data_store(self, data_store):
self.data_store = data_store
def get_data_store(self):
@@ -290,57 +385,64 @@ class OAuthServer(object):
self.signature_methods[signature_method.get_name()] = signature_method
return self.signature_methods
- # process a request_token request
- # returns the request token on success
def fetch_request_token(self, oauth_request):
+ """Processes a request_token request and returns the
+ request token on success.
+ """
try:
- # get the request token for authorization
+ # Get the request token for authorization.
token = self._get_token(oauth_request, 'request')
except OAuthError:
- # no token required for the initial token request
+ # No token required for the initial token request.
version = self._get_version(oauth_request)
consumer = self._get_consumer(oauth_request)
+ try:
+ callback = self.get_callback(oauth_request)
+ except OAuthError:
+ callback = None # 1.0, no callback specified.
self._check_signature(oauth_request, consumer, None)
- # fetch a new token
- token = self.data_store.fetch_request_token(consumer)
+ # Fetch a new token.
+ token = self.data_store.fetch_request_token(consumer, callback)
return token
- # process an access_token request
- # returns the access token on success
def fetch_access_token(self, oauth_request):
+ """Processes an access_token request and returns the
+ access token on success.
+ """
version = self._get_version(oauth_request)
consumer = self._get_consumer(oauth_request)
- # get the request token
+ verifier = self._get_verifier(oauth_request)
+ # Get the request token.
token = self._get_token(oauth_request, 'request')
self._check_signature(oauth_request, consumer, token)
- new_token = self.data_store.fetch_access_token(consumer, token)
+ new_token = self.data_store.fetch_access_token(consumer, token, verifier)
return new_token
- # verify an api call, checks all the parameters
def verify_request(self, oauth_request):
+ """Verifies an api call and checks all the parameters."""
# -> consumer and token
version = self._get_version(oauth_request)
consumer = self._get_consumer(oauth_request)
- # get the access token
+ # Get the access token.
token = self._get_token(oauth_request, 'access')
self._check_signature(oauth_request, consumer, token)
parameters = oauth_request.get_nonoauth_parameters()
return consumer, token, parameters
- # authorize a request token
def authorize_token(self, token, user):
+ """Authorize a request token."""
return self.data_store.authorize_request_token(token, user)
-
- # get the callback url
+
def get_callback(self, oauth_request):
+ """Get the callback URL."""
return oauth_request.get_parameter('oauth_callback')
-
- # optional support for the authenticate header
+
def build_authenticate_header(self, realm=''):
+ """Optional support for the authenticate header."""
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
- # verify the correct version request for this server
def _get_version(self, oauth_request):
+ """Verify the correct version request for this server."""
try:
version = oauth_request.get_parameter('oauth_version')
except:
@@ -349,37 +451,40 @@ class OAuthServer(object):
raise OAuthError('OAuth version %s not supported.' % str(version))
return version
- # figure out the signature with some defaults
def _get_signature_method(self, oauth_request):
+ """Figure out the signature with some defaults."""
try:
- signature_method = oauth_request.get_parameter('oauth_signature_method')
+ signature_method = oauth_request.get_parameter(
+ 'oauth_signature_method')
except:
signature_method = SIGNATURE_METHOD
try:
- # get the signature method object
+ # Get the signature method object.
signature_method = self.signature_methods[signature_method]
except:
signature_method_names = ', '.join(self.signature_methods.keys())
- raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
+ raise OAuthError('Signature method %s not supported try one of the '
+ 'following: %s' % (signature_method, signature_method_names))
return signature_method
def _get_consumer(self, oauth_request):
consumer_key = oauth_request.get_parameter('oauth_consumer_key')
- if not consumer_key:
- raise OAuthError('Invalid consumer key.')
consumer = self.data_store.lookup_consumer(consumer_key)
if not consumer:
raise OAuthError('Invalid consumer.')
return consumer
- # try to find the token for the provided request token key
def _get_token(self, oauth_request, token_type='access'):
+ """Try to find the token for the provided request token key."""
token_field = oauth_request.get_parameter('oauth_token')
token = self.data_store.lookup_token(token_type, token_field)
if not token:
raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
return token
+
+ def _get_verifier(self, oauth_request):
+ return oauth_request.get_parameter('oauth_verifier')
def _check_signature(self, oauth_request, consumer, token):
timestamp, nonce = oauth_request._get_timestamp_nonce()
@@ -390,29 +495,35 @@ class OAuthServer(object):
signature = oauth_request.get_parameter('oauth_signature')
except:
raise OAuthError('Missing signature.')
- # validate the signature
- valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature)
+ # Validate the signature.
+ valid_sig = signature_method.check_signature(oauth_request, consumer,
+ token, signature)
if not valid_sig:
- key, base = signature_method.build_signature_base_string(oauth_request, consumer, token)
- raise OAuthError('Invalid signature. Expected signature base string: %s' % base)
+ key, base = signature_method.build_signature_base_string(
+ oauth_request, consumer, token)
+ raise OAuthError('Invalid signature. Expected signature base '
+ 'string: %s' % base)
built = signature_method.build_signature(oauth_request, consumer, token)
def _check_timestamp(self, timestamp):
- # verify that timestamp is recentish
+ """Verify that timestamp is recentish."""
timestamp = int(timestamp)
now = int(time.time())
lapsed = now - timestamp
if lapsed > self.timestamp_threshold:
- raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
+ raise OAuthError('Expired timestamp: given %d and now %s has a '
+ 'greater difference than threshold %d' %
+ (timestamp, now, self.timestamp_threshold))
def _check_nonce(self, consumer, token, nonce):
- # verify that the nonce is uniqueish
+ """Verify that the nonce is uniqueish."""
nonce = self.data_store.lookup_nonce(consumer, token, nonce)
if nonce:
raise OAuthError('Nonce already used: %s' % str(nonce))
-# OAuthClient is a worker to attempt to execute a request
+
class OAuthClient(object):
+ """OAuthClient is a worker to attempt to execute a request."""
consumer = None
token = None
@@ -427,62 +538,65 @@ class OAuthClient(object):
return self.token
def fetch_request_token(self, oauth_request):
- # -> OAuthToken
+ """-> OAuthToken."""
raise NotImplementedError
def fetch_access_token(self, oauth_request):
- # -> OAuthToken
+ """-> OAuthToken."""
raise NotImplementedError
def access_resource(self, oauth_request):
- # -> some protected resource
+ """-> Some protected resource."""
raise NotImplementedError
-# OAuthDataStore is a database abstraction used to lookup consumers and tokens
+
class OAuthDataStore(object):
+ """A database abstraction used to lookup consumers and tokens."""
def lookup_consumer(self, key):
- # -> OAuthConsumer
+ """-> OAuthConsumer."""
raise NotImplementedError
def lookup_token(self, oauth_consumer, token_type, token_token):
- # -> OAuthToken
+ """-> OAuthToken."""
raise NotImplementedError
- def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp):
- # -> OAuthToken
+ def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+ """-> OAuthToken."""
raise NotImplementedError
- def fetch_request_token(self, oauth_consumer):
- # -> OAuthToken
+ def fetch_request_token(self, oauth_consumer, oauth_callback):
+ """-> OAuthToken."""
raise NotImplementedError
- def fetch_access_token(self, oauth_consumer, oauth_token):
- # -> OAuthToken
+ def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
+ """-> OAuthToken."""
raise NotImplementedError
def authorize_request_token(self, oauth_token, user):
- # -> OAuthToken
+ """-> OAuthToken."""
raise NotImplementedError
-# OAuthSignatureMethod is a strategy class that implements a signature method
+
class OAuthSignatureMethod(object):
+ """A strategy class that implements a signature method."""
def get_name(self):
- # -> str
+ """-> str."""
raise NotImplementedError
def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
- # -> str key, str raw
+ """-> str key, str raw."""
raise NotImplementedError
def build_signature(self, oauth_request, oauth_consumer, oauth_token):
- # -> str
+ """-> str."""
raise NotImplementedError
def check_signature(self, oauth_request, consumer, token, signature):
built = self.build_signature(oauth_request, consumer, token)
return built == signature
+
class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
def get_name(self):
@@ -502,19 +616,21 @@ class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
return key, raw
def build_signature(self, oauth_request, consumer, token):
- # build the base signature string
- key, raw = self.build_signature_base_string(oauth_request, consumer, token)
+ """Builds the base signature string."""
+ key, raw = self.build_signature_base_string(oauth_request, consumer,
+ token)
- # hmac object
+ # HMAC object.
try:
import hashlib # 2.5
hashed = hmac.new(key, raw, hashlib.sha1)
except:
- import sha # deprecated
+ import sha # Deprecated
hashed = hmac.new(key, raw, sha)
- # calculate the digest base 64
- return base64.b64encode(hashed.digest())
+ # Calculate the digest base 64.
+ return binascii.b2a_base64(hashed.digest())[:-1]
+
class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
@@ -522,11 +638,13 @@ class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
return 'PLAINTEXT'
def build_signature_base_string(self, oauth_request, consumer, token):
- # concatenate the consumer key and secret
- sig = escape(consumer.secret) + '&'
+ """Concatenates the consumer key and secret."""
+ sig = '%s&' % escape(consumer.secret)
if token:
sig = sig + escape(token.secret)
- return sig
+ return sig, sig
def build_signature(self, oauth_request, consumer, token):
- return self.build_signature_base_string(oauth_request, consumer, token)
+ key, raw = self.build_signature_base_string(oauth_request, consumer,
+ token)
+ return key
\ No newline at end of file
diff --git a/lib/piston/resource.py b/lib/piston/resource.py
index 612ac9f..c63fd54 100644
--- a/lib/piston/resource.py
+++ b/lib/piston/resource.py
@@ -7,6 +7,7 @@ from django.views.decorators.vary import vary_on_headers
from django.conf import settings
from django.core.mail import send_mail, EmailMessage
from django.db.models.query import QuerySet
+from django.http import Http404
from emitters import Emitter
from handler import typemapper
@@ -15,6 +16,8 @@ from authentication import NoAuthentication
from utils import coerce_put_post, FormValidationError, HttpStatusCode
from utils import rc, format_error, translate_mime, MimerDataException
+CHALLENGE = object()
+
class Resource(object):
"""
Resource. Create one for your URL mappings, just
@@ -23,20 +26,22 @@ class Resource(object):
is an authentication handler. If not specified,
`NoAuthentication` will be used by default.
"""
- callmap = { 'GET': 'read', 'POST': 'create',
+ callmap = { 'GET': 'read', 'POST': 'create',
'PUT': 'update', 'DELETE': 'delete' }
-
+
def __init__(self, handler, authentication=None):
if not callable(handler):
raise AttributeError, "Handler not callable."
-
+
self.handler = handler()
-
+
if not authentication:
- self.authentication = NoAuthentication()
- else:
+ self.authentication = (NoAuthentication(),)
+ elif isinstance(authentication, (list, tuple)):
self.authentication = authentication
-
+ else:
+ self.authentication = (authentication,)
+
# Erroring
self.email_errors = getattr(settings, 'PISTON_EMAIL_ERRORS', True)
self.display_errors = getattr(settings, 'PISTON_DISPLAY_ERRORS', True)
@@ -53,12 +58,22 @@ class Resource(object):
that as well.
"""
em = kwargs.pop('emitter_format', None)
-
+
if not em:
em = request.GET.get('format', 'json')
return em
-
+
+ def form_validation_response(self, e):
+ """
+ Method to return form validation error information.
+ You will probably want to override this in your own
+ `Resource` subclass.
+ """
+ resp = rc.BAD_REQUEST
+ resp.write(' '+str(e.form.errors))
+ return resp
+
@property
def anonymous(self):
"""
@@ -69,16 +84,32 @@ class Resource(object):
"""
if hasattr(self.handler, 'anonymous'):
anon = self.handler.anonymous
-
+
if callable(anon):
return anon
for klass in typemapper.keys():
if anon == klass.__name__:
return klass
-
+
return None
-
+
+ def authenticate(self, request, rm):
+ actor, anonymous = False, True
+
+ for authenticator in self.authentication:
+ if not authenticator.is_authenticated(request):
+ if self.anonymous and \
+ rm in self.anonymous.allowed_methods:
+
+ actor, anonymous = self.anonymous(), True
+ else:
+ actor, anonymous = authenticator.challenge, CHALLENGE
+ else:
+ return self.handler, self.handler.is_anonymous
+
+ return actor, anonymous
+
@vary_on_headers('Authorization')
def __call__(self, request, *args, **kwargs):
"""
@@ -92,30 +123,30 @@ class Resource(object):
if rm == "PUT":
coerce_put_post(request)
- if not self.authentication.is_authenticated(request):
- if self.anonymous and \
- rm in self.anonymous.allowed_methods:
+ actor, anonymous = self.authenticate(request, rm)
- handler = self.anonymous()
- anonymous = True
- else:
- return self.authentication.challenge()
+ if anonymous is CHALLENGE:
+ return actor()
else:
- handler = self.handler
- anonymous = handler.is_anonymous
-
+ handler = actor
+
# Translate nested datastructs into `request.data` here.
if rm in ('POST', 'PUT'):
try:
translate_mime(request)
except MimerDataException:
return rc.BAD_REQUEST
-
+ if not hasattr(request, 'data'):
+ if rm == 'POST':
+ request.data = request.POST
+ else:
+ request.data = request.PUT
+
if not rm in handler.allowed_methods:
return HttpResponseNotAllowed(handler.allowed_methods)
-
+
meth = getattr(handler, self.callmap.get(rm), None)
-
+
if not meth:
raise Http404
@@ -123,60 +154,17 @@ class Resource(object):
em_format = self.determine_emitter(request, *args, **kwargs)
kwargs.pop('emitter_format', None)
-
+
# Clean up the request object a bit, since we might
# very well have `oauth_`-headers in there, and we
# don't want to pass these along to the handler.
request = self.cleanup_request(request)
-
+
try:
result = meth(request, *args, **kwargs)
- except FormValidationError, e:
- # TODO: Use rc.BAD_REQUEST here
- return HttpResponse("Bad Request: %s" % e.form.errors, status=400)
- except TypeError, e:
- result = rc.BAD_REQUEST
- hm = HandlerMethod(meth)
- sig = hm.get_signature()
-
- msg = 'Method signature does not match.\n\n'
-
- if sig:
- msg += 'Signature should be: %s' % sig
- else:
- msg += 'Resource does not expect any parameters.'
-
- if self.display_errors:
- msg += '\n\nException was: %s' % str(e)
-
- result.content = format_error(msg)
- except HttpStatusCode, e:
- #result = e ## why is this being passed on and not just dealt with now?
- return e.response
except Exception, e:
- """
- On errors (like code errors), we'd like to be able to
- give crash reports to both admins and also the calling
- user. There's two setting parameters for this:
-
- Parameters::
- - `PISTON_EMAIL_ERRORS`: Will send a Django formatted
- error email to people in `settings.ADMINS`.
- - `PISTON_DISPLAY_ERRORS`: Will return a simple traceback
- to the caller, so he can tell you what error they got.
-
- If `PISTON_DISPLAY_ERRORS` is not enabled, the caller will
- receive a basic "500 Internal Server Error" message.
- """
- exc_type, exc_value, tb = sys.exc_info()
- rep = ExceptionReporter(request, exc_type, exc_value, tb.tb_next)
- if self.email_errors:
- self.email_exception(rep)
- if self.display_errors:
- return HttpResponseServerError(
- format_error('\n'.join(rep.format_exception())))
- else:
- raise
+ result = self.error_handler(e, request, meth)
+
emitter, ct = Emitter.get(em_format)
fields = handler.fields
@@ -184,6 +172,18 @@ class Resource(object):
isinstance(result, list) or isinstance(result, QuerySet)):
fields = handler.list_fields
+ status_code = 200
+
+ # If we're looking at a response object which contains non-string
+ # content, then assume we should use the emitter to format that
+ # content
+ if isinstance(result, HttpResponse) and not result._is_string:
+ status_code = result.status_code
+ # Note: We can't use result.content here because that method attempts
+ # to convert the content into a string which we don't want.
+ # when _is_string is False _container is the raw data
+ result = result._container
+
srl = emitter(result, typemapper, handler, fields, anonymous)
try:
@@ -196,7 +196,10 @@ class Resource(object):
if self.stream: stream = srl.stream_render(request)
else: stream = srl.render(request)
- resp = HttpResponse(stream, mimetype=ct)
+ if not isinstance(stream, HttpResponse):
+ resp = HttpResponse(stream, mimetype=ct, status=status_code)
+ else:
+ resp = stream
resp.streaming = self.stream
@@ -215,17 +218,17 @@ class Resource(object):
if True in [ k.startswith("oauth_") for k in block.keys() ]:
sanitized = block.copy()
-
+
for k in sanitized.keys():
if k.startswith("oauth_"):
sanitized.pop(k)
-
+
setattr(request, method_type, sanitized)
return request
-
- # --
-
+
+ # --
+
def email_exception(self, reporter):
subject = "Piston crash report"
html = reporter.get_traceback_html()
@@ -233,6 +236,63 @@ class Resource(object):
message = EmailMessage(settings.EMAIL_SUBJECT_PREFIX+subject,
html, settings.SERVER_EMAIL,
[ admin[1] for admin in settings.ADMINS ])
-
+
message.content_subtype = 'html'
message.send(fail_silently=True)
+
+
+ def error_handler(self, e, request, meth):
+ """
+ Override this method to add handling of errors customized for your
+ needs
+ """
+ if isinstance(e, FormValidationError):
+ return self.form_validation_response(e)
+
+ elif isinstance(e, TypeError):
+ result = rc.BAD_REQUEST
+ hm = HandlerMethod(meth)
+ sig = hm.signature
+
+ msg = 'Method signature does not match.\n\n'
+
+ if sig:
+ msg += 'Signature should be: %s' % sig
+ else:
+ msg += 'Resource does not expect any parameters.'
+
+ if self.display_errors:
+ msg += '\n\nException was: %s' % str(e)
+
+ result.content = format_error(msg)
+ return result
+ elif isinstance(e, Http404):
+ return rc.NOT_FOUND
+
+ elif isinstance(e, HttpStatusCode):
+ return e.response
+
+ else:
+ """
+ On errors (like code errors), we'd like to be able to
+ give crash reports to both admins and also the calling
+ user. There's two setting parameters for this:
+
+ Parameters::
+ - `PISTON_EMAIL_ERRORS`: Will send a Django formatted
+ error email to people in `settings.ADMINS`.
+ - `PISTON_DISPLAY_ERRORS`: Will return a simple traceback
+ to the caller, so he can tell you what error they got.
+
+ If `PISTON_DISPLAY_ERRORS` is not enabled, the caller will
+ receive a basic "500 Internal Server Error" message.
+ """
+ exc_type, exc_value, tb = sys.exc_info()
+ rep = ExceptionReporter(request, exc_type, exc_value, tb.tb_next)
+ if self.email_errors:
+ self.email_exception(rep)
+ if self.display_errors:
+ return HttpResponseServerError(
+ format_error('\n'.join(rep.format_exception())))
+ else:
+ raise
diff --git a/lib/piston/signals.py b/lib/piston/signals.py
new file mode 100644
index 0000000..133be13
--- /dev/null
+++ b/lib/piston/signals.py
@@ -0,0 +1,14 @@
+# Django imports
+import django.dispatch
+
+# Piston imports
+from utils import send_consumer_mail
+
+def consumer_post_save(sender, instance, created, **kwargs):
+ send_consumer_mail(instance)
+
+def consumer_post_delete(sender, instance, **kwargs):
+ instance.status = 'canceled'
+ send_consumer_mail(instance)
+
+
diff --git a/lib/piston/store.py b/lib/piston/store.py
index 741a470..787791a 100644
--- a/lib/piston/store.py
+++ b/lib/piston/store.py
@@ -1,6 +1,7 @@
import oauth
from models import Nonce, Token, Consumer
+from models import generate_random, VERIFIER_SIZE
class DataStore(oauth.OAuthDataStore):
"""Layer between Python OAuth and Django database."""
@@ -39,17 +40,22 @@ class DataStore(oauth.OAuthDataStore):
else:
return nonce.key
- def fetch_request_token(self, oauth_consumer):
+ def fetch_request_token(self, oauth_consumer, oauth_callback):
if oauth_consumer.key == self.consumer.key:
self.request_token = Token.objects.create_token(consumer=self.consumer,
token_type=Token.REQUEST,
timestamp=self.timestamp)
+
+ if oauth_callback:
+ self.request_token.set_callback(oauth_callback)
+
return self.request_token
return None
- def fetch_access_token(self, oauth_consumer, oauth_token):
+ def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
if oauth_consumer.key == self.consumer.key \
and oauth_token.key == self.request_token.key \
+ and oauth_verifier == self.request_token.verifier \
and self.request_token.is_approved:
self.access_token = Token.objects.create_token(consumer=self.consumer,
token_type=Token.ACCESS,
@@ -63,6 +69,7 @@ class DataStore(oauth.OAuthDataStore):
# authorize the request token in the store
self.request_token.is_approved = True
self.request_token.user = user
+ self.request_token.verifier = generate_random(VERIFIER_SIZE)
self.request_token.save()
return self.request_token
return None
\ No newline at end of file
diff --git a/lib/piston/test.py b/lib/piston/test.py
new file mode 100644
index 0000000..57eda72
--- /dev/null
+++ b/lib/piston/test.py
@@ -0,0 +1,62 @@
+# Django imports
+import django.test.client as client
+import django.test as test
+from django.utils.http import urlencode
+
+# Piston imports
+from piston import oauth
+from piston.models import Consumer, Token
+
+# 3rd/Python party imports
+import httplib2, urllib, cgi
+
+URLENCODED_FORM_CONTENT = 'application/x-www-form-urlencoded'
+
+class OAuthClient(client.Client):
+ def __init__(self, consumer, token):
+ self.token = oauth.OAuthToken(token.key, token.secret)
+ self.consumer = oauth.OAuthConsumer(consumer.key, consumer.secret)
+ self.signature = oauth.OAuthSignatureMethod_HMAC_SHA1()
+
+ super(OAuthClient, self).__init__()
+
+ def request(self, **request):
+ # Figure out parameters from request['QUERY_STRING'] and FakePayload
+ params = {}
+ if request['REQUEST_METHOD'] in ('POST', 'PUT'):
+ if request['CONTENT_TYPE'] == URLENCODED_FORM_CONTENT:
+ payload = request['wsgi.input'].read()
+ request['wsgi.input'] = client.FakePayload(payload)
+ params = cgi.parse_qs(payload)
+
+ url = "http://testserver" + request['PATH_INFO']
+
+ req = oauth.OAuthRequest.from_consumer_and_token(
+ self.consumer, token=self.token,
+ http_method=request['REQUEST_METHOD'], http_url=url,
+ parameters=params
+ )
+
+ req.sign_request(self.signature, self.consumer, self.token)
+ headers = req.to_header()
+ request['HTTP_AUTHORIZATION'] = headers['Authorization']
+
+ return super(OAuthClient, self).request(**request)
+
+ def post(self, path, data={}, content_type=None, follow=False, **extra):
+ if content_type is None:
+ content_type = URLENCODED_FORM_CONTENT
+
+ if isinstance(data, dict):
+ data = urlencode(data)
+
+ return super(OAuthClient, self).post(path, data, content_type, follow, **extra)
+
+class TestCase(test.TestCase):
+ pass
+
+class OAuthTestCase(TestCase):
+ @property
+ def oauth(self):
+ return OAuthClient(self.consumer, self.token)
+
diff --git a/lib/piston/tests.py b/lib/piston/tests.py
new file mode 100644
index 0000000..ed097fe
--- /dev/null
+++ b/lib/piston/tests.py
@@ -0,0 +1,175 @@
+# Django imports
+from django.core import mail
+from django.contrib.auth.models import User
+from django.conf import settings
+from django.http import HttpRequest, HttpResponse
+from django.utils import simplejson
+
+# Piston imports
+from test import TestCase
+from models import Consumer
+from handler import BaseHandler
+from utils import rc
+from resource import Resource
+
+class ConsumerTest(TestCase):
+ fixtures = ['models.json']
+
+ def setUp(self):
+ self.consumer = Consumer()
+ self.consumer.name = "Piston Test Consumer"
+ self.consumer.description = "A test consumer for Piston."
+ self.consumer.user = User.objects.get(pk=3)
+ self.consumer.generate_random_codes()
+
+ def test_create_pending(self):
+ """ Ensure creating a pending Consumer sends proper emails """
+ # If it's pending we should have two messages in the outbox; one
+ # to the consumer and one to the site admins.
+ if len(settings.ADMINS):
+ self.assertEquals(len(mail.outbox), 2)
+ else:
+ self.assertEquals(len(mail.outbox), 1)
+
+ expected = "Your API Consumer for example.com is awaiting approval."
+ self.assertEquals(mail.outbox[0].subject, expected)
+
+ def test_delete_consumer(self):
+ """ Ensure deleting a Consumer sends a cancel email """
+
+ # Clear out the outbox before we test for the cancel email.
+ mail.outbox = []
+
+ # Delete the consumer, which should fire off the cancel email.
+ self.consumer.delete()
+
+ self.assertEquals(len(mail.outbox), 1)
+ expected = "Your API Consumer for example.com has been canceled."
+ self.assertEquals(mail.outbox[0].subject, expected)
+
+
+class CustomResponseWithStatusCodeTest(TestCase):
+ """
+ Test returning content to be formatted and a custom response code from a
+ handler method. In this case we're returning 201 (created) and a dictionary
+ of data. This data will be formatted as json.
+ """
+
+ def test_reponse_with_data_and_status_code(self):
+ response_data = dict(complex_response=dict(something='good',
+ something_else='great'))
+
+ class MyHandler(BaseHandler):
+ """
+ Handler which returns a response w/ both data and a status code (201)
+ """
+ allowed_methods = ('POST', )
+
+ def create(self, request):
+ resp = rc.CREATED
+ resp.content = response_data
+ return resp
+
+ resource = Resource(MyHandler)
+ request = HttpRequest()
+ request.method = 'POST'
+ response = resource(request, emitter_format='json')
+
+ self.assertEquals(201, response.status_code)
+ self.assertTrue(response._is_string, "Expected response content to be a string")
+
+ # compare the original data dict with the json response
+ # converted to a dict
+ self.assertEquals(response_data, simplejson.loads(response.content))
+
+
+class ErrorHandlerTest(TestCase):
+ def test_customized_error_handler(self):
+ """
+ Throw a custom error from a handler method and catch (and format) it
+ in an overridden error_handler method on the associated Resource object
+ """
+ class GoAwayError(Exception):
+ def __init__(self, name, reason):
+ self.name = name
+ self.reason = reason
+
+ class MyHandler(BaseHandler):
+ """
+ Handler which raises a custom exception
+ """
+ allowed_methods = ('GET',)
+
+ def read(self, request):
+ raise GoAwayError('Jerome', 'No one likes you')
+
+ class MyResource(Resource):
+ def error_handler(self, error, request, meth):
+ # if the exception is our exeption then generate a
+ # custom response with embedded content that will be
+ # formatted as json
+ if isinstance(error, GoAwayError):
+ response = rc.FORBIDDEN
+ response.content = dict(error=dict(
+ name=error.name,
+ message="Get out of here and dont come back",
+ reason=error.reason
+ ))
+
+ return response
+
+ return super(MyResource, self).error_handler(error, request, meth)
+
+ resource = MyResource(MyHandler)
+
+ request = HttpRequest()
+ request.method = 'GET'
+ response = resource(request, emitter_format='json')
+
+ self.assertEquals(401, response.status_code)
+
+ # verify the content we got back can be converted back to json
+ # and examine the dictionary keys all exist as expected
+ response_data = simplejson.loads(response.content)
+ self.assertTrue('error' in response_data)
+ self.assertTrue('name' in response_data['error'])
+ self.assertTrue('message' in response_data['error'])
+ self.assertTrue('reason' in response_data['error'])
+
+ def test_type_error(self):
+ """
+ Verify that type errors thrown from a handler method result in a valid
+ HttpResonse object being returned from the error_handler method
+ """
+ class MyHandler(BaseHandler):
+ def read(self, request):
+ raise TypeError()
+
+ request = HttpRequest()
+ request.method = 'GET'
+ response = Resource(MyHandler)(request)
+
+ self.assertTrue(isinstance(response, HttpResponse), "Expected a response, not: %s"
+ % response)
+
+
+ def test_other_error(self):
+ """
+ Verify that other exceptions thrown from a handler method result in a valid
+ HttpResponse object being returned from the error_handler method
+ """
+ class MyHandler(BaseHandler):
+ def read(self, request):
+ raise Exception()
+
+ resource = Resource(MyHandler)
+ resource.display_errors = True
+ resource.email_errors = False
+
+ request = HttpRequest()
+ request.method = 'GET'
+ response = resource(request)
+
+ self.assertTrue(isinstance(response, HttpResponse), "Expected a response, not: %s"
+ % response)
+
diff --git a/lib/piston/utils.py b/lib/piston/utils.py
index b197ded..d8461d8 100644
--- a/lib/piston/utils.py
+++ b/lib/piston/utils.py
@@ -1,12 +1,18 @@
+import time
from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest
from django.core.urlresolvers import reverse
from django.core.cache import cache
from django import get_version as django_version
+from django.core.mail import send_mail, mail_admins
+from django.conf import settings
+from django.utils.translation import ugettext as _
+from django.template import loader, TemplateDoesNotExist
+from django.contrib.sites.models import Site
from decorator import decorator
from datetime import datetime, timedelta
-__version__ = '0.2.2'
+__version__ = '0.2.3rc1'
def get_version():
return __version__
@@ -27,6 +33,7 @@ class rc_factory(object):
NOT_FOUND = ('Not Found', 404),
DUPLICATE_ENTRY = ('Conflict/Duplicate', 409),
NOT_HERE = ('Gone', 410),
+ INTERNAL_ERROR = ('Internal Error', 500),
NOT_IMPLEMENTED = ('Not Implemented', 501),
THROTTLED = ('Throttled', 503))
@@ -41,7 +48,30 @@ class rc_factory(object):
except TypeError:
raise AttributeError(attr)
- return HttpResponse(r, content_type='text/plain', status=c)
+ class HttpResponseWrapper(HttpResponse):
+ """
+ Wrap HttpResponse and make sure that the internal _is_string
+ flag is updated when the _set_content method (via the content
+ property) is called
+ """
+ def _set_content(self, content):
+ """
+ Set the _container and _is_string properties based on the
+ type of the value parameter. This logic is in the construtor
+ for HttpResponse, but doesn't get repeated when setting
+ HttpResponse.content although this bug report (feature request)
+ suggests that it should: http://code.djangoproject.com/ticket/9403
+ """
+ if not isinstance(content, basestring) and hasattr(content, '__iter__'):
+ self._container = content
+ self._is_string = False
+ else:
+ self._container = [content]
+ self._is_string = True
+
+ content = property(HttpResponse._get_content, _set_content)
+
+ return HttpResponseWrapper(r, content_type='text/plain', status=c)
rc = rc_factory()
@@ -59,6 +89,7 @@ def validate(v_form, operation='POST'):
form = v_form(getattr(request, operation))
if form.is_valid():
+ setattr(request, 'form', form)
return f(self, request, *a, **kwa)
else:
raise FormValidationError(form)
@@ -102,24 +133,20 @@ def throttle(max_requests, timeout=60*60, extra=''):
"""
ident += ':%s' % extra
- now = datetime.now()
- ts_key = 'throttle:ts:%s' % ident
- timestamp = cache.get(ts_key)
- offset = now + timedelta(seconds=timeout)
-
- if timestamp and timestamp < offset:
+ now = time.time()
+ count, expiration = cache.get(ident, (1, None))
+
+ if expiration is None:
+ expiration = now + timeout
+
+ if count >= max_requests and expiration > now:
t = rc.THROTTLED
- wait = timeout - (offset-timestamp).seconds
+ wait = int(expiration - now)
t.content = 'Throttled, wait %d seconds.' % wait
-
+ t['Retry-After'] = wait
return t
-
- count = cache.get(ident, 1)
- cache.set(ident, count+1)
-
- if count >= max_requests:
- cache.set(ts_key, offset, timeout)
- cache.set(ident, 1)
+
+ cache.set(ident, (count+1, expiration), (expiration - now))
return f(self, request, *args, **kwargs)
return wrap
@@ -135,6 +162,20 @@ def coerce_put_post(request):
in mod_python. This should fix it.
"""
if request.method == "PUT":
+ # Bug fix: if _load_post_and_files has already been called, for
+ # example by middleware accessing request.POST, the below code to
+ # pretend the request is a POST instead of a PUT will be too late
+ # to make a difference. Also calling _load_post_and_files will result
+ # in the following exception:
+ # AttributeError: You cannot set the upload handlers after the upload has been processed.
+ # The fix is to check for the presence of the _post field which is set
+ # the first time _load_post_and_files is called (both by wsgi.py and
+ # modpython.py). If it's set, the request has to be 'reset' to redo
+ # the query value parsing in POST mode.
+ if hasattr(request, '_post'):
+ del request._post
+ del request._files
+
try:
request.method = "POST"
request._load_post_and_files()
@@ -176,7 +217,7 @@ class Mimer(object):
for mime in mimes:
if ctype.startswith(mime):
return loadee
-
+
def content_type(self):
"""
Returns the content type of the request in all cases where it is
@@ -186,11 +227,10 @@ class Mimer(object):
ctype = self.request.META.get('CONTENT_TYPE', type_formencoded)
- if ctype.startswith(type_formencoded):
+ if type_formencoded in ctype:
return None
return ctype
-
def translate(self):
"""
@@ -211,14 +251,18 @@ class Mimer(object):
if not self.is_multipart() and ctype:
loadee = self.loader_for_type(ctype)
- try:
- self.request.data = loadee(self.request.raw_post_data)
-
- # Reset both POST and PUT from request, as its
- # misleading having their presence around.
- self.request.POST = self.request.PUT = dict()
- except (TypeError, ValueError):
- raise MimerDataException
+ if loadee:
+ try:
+ self.request.data = loadee(self.request.raw_post_data)
+
+ # Reset both POST and PUT from request, as its
+ # misleading having their presence around.
+ self.request.POST = self.request.PUT = dict()
+ except (TypeError, ValueError):
+ # This also catches if loadee is None.
+ raise MimerDataException
+ else:
+ self.request.data = None
return self.request
@@ -260,3 +304,48 @@ def require_mime(*mimes):
require_extended = require_mime('json', 'yaml', 'xml', 'pickle')
+def send_consumer_mail(consumer):
+ """
+ Send a consumer an email depending on what their status is.
+ """
+ try:
+ subject = settings.PISTON_OAUTH_EMAIL_SUBJECTS[consumer.status]
+ except AttributeError:
+ subject = "Your API Consumer for %s " % Site.objects.get_current().name
+ if consumer.status == "accepted":
+ subject += "was accepted!"
+ elif consumer.status == "canceled":
+ subject += "has been canceled."
+ elif consumer.status == "rejected":
+ subject += "has been rejected."
+ else:
+ subject += "is awaiting approval."
+
+ template = "piston/mails/consumer_%s.txt" % consumer.status
+
+ try:
+ body = loader.render_to_string(template,
+ { 'consumer' : consumer, 'user' : consumer.user })
+ except TemplateDoesNotExist:
+ """
+ They haven't set up the templates, which means they might not want
+ these emails sent.
+ """
+ return
+
+ try:
+ sender = settings.PISTON_FROM_EMAIL
+ except AttributeError:
+ sender = settings.DEFAULT_FROM_EMAIL
+
+ if consumer.user:
+ send_mail(_(subject), body, sender, [consumer.user.email], fail_silently=True)
+
+ if consumer.status == 'pending' and len(settings.ADMINS):
+ mail_admins(_(subject), body, fail_silently=True)
+
+ if settings.DEBUG and consumer.user:
+ print "Mail being sent, to=%s" % consumer.user.email
+ print "Subject: %s" % _(subject)
+ print body
+
diff --git a/lib/piston/validate_jsonp.py b/lib/piston/validate_jsonp.py
new file mode 100644
index 0000000..10fba21
--- /dev/null
+++ b/lib/piston/validate_jsonp.py
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+
+# Placed into the Public Domain by tav <tav espians com>
+
+"""Validate Javascript Identifiers for use as JSON-P callback parameters."""
+
+import re
+from unicodedata import category
+
+# ------------------------------------------------------------------------------
+# javascript identifier unicode categories and "exceptional" chars
+# ------------------------------------------------------------------------------
+
+valid_jsid_categories_start = frozenset([
+ 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl'
+ ])
+
+valid_jsid_categories = frozenset([
+ 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl', 'Mn', 'Mc', 'Nd', 'Pc'
+ ])
+
+valid_jsid_chars = ('$', '_')
+
+# ------------------------------------------------------------------------------
+# regex to find array[index] patterns
+# ------------------------------------------------------------------------------
+
+array_index_regex = re.compile(r'\[[0-9]+\]$')
+
+has_valid_array_index = array_index_regex.search
+replace_array_index = array_index_regex.sub
+
+# ------------------------------------------------------------------------------
+# javascript reserved words -- including keywords and null/boolean literals
+# ------------------------------------------------------------------------------
+
+is_reserved_js_word = frozenset([
+
+ 'abstract', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class',
+ 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double',
+ 'else', 'enum', 'export', 'extends', 'false', 'final', 'finally', 'float',
+ 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof',
+ 'int', 'interface', 'long', 'native', 'new', 'null', 'package', 'private',
+ 'protected', 'public', 'return', 'short', 'static', 'super', 'switch',
+ 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try',
+ 'typeof', 'var', 'void', 'volatile', 'while', 'with',
+
+ # potentially reserved in a future version of the ES5 standard
+ # 'let', 'yield'
+
+ ]).__contains__
+
+# ------------------------------------------------------------------------------
+# the core validation functions
+# ------------------------------------------------------------------------------
+
+def is_valid_javascript_identifier(identifier, escape=r'\u', ucd_cat=category):
+ """Return whether the given ``id`` is a valid Javascript identifier."""
+
+ if not identifier:
+ return False
+
+ if not isinstance(identifier, unicode):
+ try:
+ identifier = unicode(identifier, 'utf-8')
+ except UnicodeDecodeError:
+ return False
+
+ if escape in identifier:
+
+ new = []; add_char = new.append
+ split_id = identifier.split(escape)
+ add_char(split_id.pop(0))
+
+ for segment in split_id:
+ if len(segment) < 4:
+ return False
+ try:
+ add_char(unichr(int('0x' + segment[:4], 16)))
+ except Exception:
+ return False
+ add_char(segment[4:])
+
+ identifier = u''.join(new)
+
+ if is_reserved_js_word(identifier):
+ return False
+
+ first_char = identifier[0]
+
+ if not ((first_char in valid_jsid_chars) or
+ (ucd_cat(first_char) in valid_jsid_categories_start)):
+ return False
+
+ for char in identifier[1:]:
+ if not ((char in valid_jsid_chars) or
+ (ucd_cat(char) in valid_jsid_categories)):
+ return False
+
+ return True
+
+
+def is_valid_jsonp_callback_value(value):
+ """Return whether the given ``value`` can be used as a JSON-P callback."""
+
+ for identifier in value.split(u'.'):
+ while '[' in identifier:
+ if not has_valid_array_index(identifier):
+ return False
+ identifier = replace_array_index(u'', identifier)
+ if not is_valid_javascript_identifier(identifier):
+ return False
+
+ return True
+
+# ------------------------------------------------------------------------------
+# test
+# ------------------------------------------------------------------------------
+
+def test():
+ """
+ The function ``is_valid_javascript_identifier`` validates a given identifier
+ according to the latest draft of the ECMAScript 5 Specification:
+
+ >>> is_valid_javascript_identifier('hello')
+ True
+
+ >>> is_valid_javascript_identifier('alert()')
+ False
+
+ >>> is_valid_javascript_identifier('a-b')
+ False
+
+ >>> is_valid_javascript_identifier('23foo')
+ False
+
+ >>> is_valid_javascript_identifier('foo23')
+ True
+
+ >>> is_valid_javascript_identifier('$210')
+ True
+
+ >>> is_valid_javascript_identifier(u'Stra\u00dfe')
+ True
+
+ >>> is_valid_javascript_identifier(r'\u0062') # u'b'
+ True
+
+ >>> is_valid_javascript_identifier(r'\u62')
+ False
+
+ >>> is_valid_javascript_identifier(r'\u0020')
+ False
+
+ >>> is_valid_javascript_identifier('_bar')
+ True
+
+ >>> is_valid_javascript_identifier('some_var')
+ True
+
+ >>> is_valid_javascript_identifier('$')
+ True
+
+ But ``is_valid_jsonp_callback_value`` is the function you want to use for
+ validating JSON-P callback parameter values:
+
+ >>> is_valid_jsonp_callback_value('somevar')
+ True
+
+ >>> is_valid_jsonp_callback_value('function')
+ False
+
+ >>> is_valid_jsonp_callback_value(' somevar')
+ False
+
+ It supports the possibility of '.' being present in the callback name, e.g.
+
+ >>> is_valid_jsonp_callback_value('$.ajaxHandler')
+ True
+
+ >>> is_valid_jsonp_callback_value('$.23')
+ False
+
+ As well as the pattern of providing an array index lookup, e.g.
+
+ >>> is_valid_jsonp_callback_value('array_of_functions[42]')
+ True
+
+ >>> is_valid_jsonp_callback_value('array_of_functions[42][1]')
+ True
+
+ >>> is_valid_jsonp_callback_value('$.ajaxHandler[42][1].foo')
+ True
+
+ >>> is_valid_jsonp_callback_value('array_of_functions[42]foo[1]')
+ False
+
+ >>> is_valid_jsonp_callback_value('array_of_functions[]')
+ False
+
+ >>> is_valid_jsonp_callback_value('array_of_functions["key"]')
+ False
+
+ Enjoy!
+
+ """
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod()
\ No newline at end of file
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]