david / django-roa (http://welldev.org/)

Turn your models into remote resources that you can access through Django's ORM. ROA stands for Resource Oriented Architecture.

Clone this repository (size: 560.7 KB): HTTPS / SSH
$ hg clone http://code.welldev.org/django-roa

Changed (Δ9.5 KB):

raw changeset »

piston/authentication.py (16 lines added, 4 lines removed)

piston/doc.py (5 lines added, 1 lines removed)

piston/emitters.py (28 lines added, 11 lines removed)

piston/forms.py (1 lines added, 1 lines removed)

piston/handler.py (15 lines added, 0 lines removed)

piston/models.py (82 lines added, 50 lines removed)

piston/oauth.py (272 lines added, 154 lines removed)

piston/resource.py (35 lines added, 9 lines removed)

piston/store.py (9 lines added, 2 lines removed)

piston/utils.py (71 lines added, 24 lines removed)

Up to file-list piston/authentication.py:

@@ -97,9 +97,19 @@ def initialize_server_request(request):
97
97
    """
98
98
    Shortcut for initialization.
99
99
    """
100
    if request.method == "POST": #and \
101
#       request.META['CONTENT_TYPE'] == "application/x-www-form-urlencoded":
102
        params = dict(request.REQUEST.items())
103
    else:
104
        params = { }
105
106
    # Seems that we want to put HTTP_AUTHORIZATION into 'Authorization'
107
    # for oauth.py to understand. Lovely.
108
    request.META['Authorization'] = request.META.get('HTTP_AUTHORIZATION', '')
109
100
110
    oauth_request = oauth.OAuthRequest.from_request(
101
111
        request.method, request.build_absolute_uri(), 
102
        headers=request.META, parameters=dict(request.REQUEST.items()),
112
        headers=request.META, parameters=params,
103
113
        query_string=request.environ.get('QUERY_STRING', ''))
104
114
        
105
115
    if oauth_request:
@@ -143,8 +153,8 @@ def oauth_request_token(request):
143
153
def oauth_auth_view(request, token, callback, params):
144
154
    form = forms.OAuthAuthenticationForm(initial={
145
155
        'oauth_token': token.key,
146
        'oauth_callback': callback,
147
        })
156
        'oauth_callback': token.get_callback_url() or callback,
157
      })
148
158
149
159
    return render_to_response('piston/authorize_token.html',
150
160
            { 'form': form }, RequestContext(request))
@@ -165,7 +175,7 @@ def oauth_user_auth(request):
165
175
        callback = oauth_server.get_callback(oauth_request)
166
176
    except:
167
177
        callback = None
168
        
178
    
169
179
    if request.method == "GET":
170
180
        params = oauth_request.get_normalized_parameters()
171
181
@@ -182,6 +192,7 @@ def oauth_user_auth(request):
182
192
                args = '?'+token.to_string(only_key=True)
183
193
            else:
184
194
                args = '?error=%s' % 'Access not granted by user.'
195
                print "FORM ERROR", form.errors
185
196
            
186
197
            if not callback:
187
198
                callback = getattr(settings, 'OAUTH_CALLBACK_VIEW')
@@ -235,6 +246,7 @@ class OAuthAuthentication(object):
235
246
236
247
            if consumer and token:
237
248
                request.user = token.user
249
                request.consumer = consumer
238
250
                request.throttle_extra = token.consumer.id
239
251
                return True
240
252
            

Up to file-list piston/doc.py:

@@ -84,7 +84,11 @@ class HandlerDocumentation(object):
84
84
        
85
85
    def get_methods(self, include_default=False):
86
86
        for method in "read create update delete".split():
87
            met = getattr(self.handler, method)
87
            met = getattr(self.handler, method, None)
88
89
            if not met:
90
                continue
91
                
88
92
            stale = inspect.getmodule(met) is handler
89
93
90
94
            if not self.handler.is_anonymous:

Up to file-list piston/emitters.py:

@@ -25,6 +25,7 @@ from django.db.models import Model, perm
25
25
from django.utils import simplejson
26
26
from django.utils.xmlutils import SimplerXMLGenerator
27
27
from django.utils.encoding import smart_unicode
28
from django.core.urlresolvers import reverse, NoReverseMatch
28
29
from django.core.serializers.json import DateTimeAwareJSONEncoder
29
30
from django.http import HttpResponse
30
31
from django.core import serializers
@@ -41,6 +42,9 @@ try:
41
42
except ImportError:
42
43
    import pickle
43
44
45
# Allow people to change the reverser (default `permalink`).
46
reverser = permalink
47
44
48
class Emitter(object):
45
49
    """
46
50
    Super emitter. All other emitters should subclass
@@ -48,8 +52,15 @@ class Emitter(object):
48
52
    conveniently returns a serialized `dict`. This is
49
53
    usually the only method you want to use in your
50
54
    emitter. See below for examples.
55
56
    `RESERVED_FIELDS` was introduced when better resource
57
    method detection came, and we accidentially caught these
58
    as the methods on the handler. Issue58 says that's no good.
51
59
    """
52
60
    EMITTERS = { }
61
    RESERVED_FIELDS = set([ 'read', 'update', 'create', 
62
                            'delete', 'model', 'anonymous',
63
                            'allowed_methods', 'fields', 'exclude' ])
53
64
54
65
    def __init__(self, payload, typemapper, handler, fields=(), anonymous=True):
55
66
        self.typemapper = typemapper
@@ -61,17 +72,18 @@ class Emitter(object):
61
72
        if isinstance(self.data, Exception):
62
73
            raise
63
74
    
64
    def method_fields(self, data, fields):
65
        if not data:
75
    def method_fields(self, handler, fields):
76
        if not handler:
66
77
            return { }
67
78
68
        has = dir(data)
69
79
        ret = dict()
70
80
            
71
        for field in fields:
72
            if field in has and callable(field):
73
                ret[field] = getattr(data, field)
74
        
81
        for field in fields - Emitter.RESERVED_FIELDS:
82
            t = getattr(handler, str(field), None)
83
84
            if t and callable(t):
85
                ret[field] = t
86
75
87
        return ret
76
88
    
77
89
    def construct(self):
@@ -107,6 +119,8 @@ class Emitter(object):
107
119
                f = thing.__emittable__
108
120
                if inspect.ismethod(f) and len(inspect.getargspec(f)[0]) == 1:
109
121
                    ret = _any(f())
122
            elif repr(thing).startswith("<django.db.models.fields.related.RelatedManager"):
123
                ret = _any(thing.all())
110
124
            else:
111
125
                ret = smart_unicode(thing, strings_only=True)
112
126
@@ -172,7 +186,7 @@ class Emitter(object):
172
186
                    get_fields = set(fields)
173
187
174
188
                met_fields = self.method_fields(handler, get_fields)
175
                
189
                           
176
190
                for f in data._meta.local_fields:
177
191
                    if f.serialize and not any([ p in met_fields for p in [ f.attname, f.name ]]):
178
192
                        if not f.rel:
@@ -239,9 +253,12 @@ class Emitter(object):
239
253
            if self.in_typemapper(type(data), self.anonymous):
240
254
                handler = self.in_typemapper(type(data), self.anonymous)
241
255
                if hasattr(handler, 'resource_uri'):
242
                    url_id, fields = handler.resource_uri()
243
                    ret['resource_uri'] = permalink( lambda: (url_id, 
244
                        (getattr(data, f) for f in fields) ) )()
256
                    url_id, fields = handler.resource_uri(data)
257
258
                    try:
259
                        ret['resource_uri'] = reverser( lambda: (url_id, fields) )()
260
                    except NoReverseMatch, e:
261
                        pass
245
262
            
246
263
            if hasattr(data, 'get_api_url') and 'resource_uri' not in ret:
247
264
                try: ret['resource_uri'] = data.get_api_url()

Up to file-list piston/forms.py:

@@ -23,7 +23,7 @@ class ModelForm(forms.ModelForm):
23
23
24
24
class OAuthAuthenticationForm(forms.Form):
25
25
    oauth_token = forms.CharField(widget=forms.HiddenInput)
26
    oauth_callback = forms.CharField(widget=forms.HiddenInput)
26
    oauth_callback = forms.CharField(widget=forms.HiddenInput, required=False)
27
27
    authorize_access = forms.BooleanField(required=True)
28
28
    csrf_signature = forms.CharField(widget=forms.HiddenInput)
29
29

Up to file-list piston/handler.py:

1
import warnings
2
1
3
from utils import rc
2
4
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
5
from django.conf import settings
3
6
4
7
typemapper = { }
5
8
handler_tracker = [ ]
@@ -11,9 +14,21 @@ class HandlerMetaClass(type):
11
14
    """
12
15
    def __new__(cls, name, bases, attrs):
13
16
        new_cls = type.__new__(cls, name, bases, attrs)
17
18
        def already_registered(model, anon):
19
            for k, (m, a) in typemapper.iteritems():
20
                if model == m and anon == a:
21
                    return k
14
22
        
15
23
        if hasattr(new_cls, 'model'):
24
            if already_registered(new_cls.model, new_cls.is_anonymous):
25
                if not getattr(settings, 'PISTON_IGNORE_DUPE_MODELS', False):
26
                    warnings.warn("Handler already registered for model %s, "
27
                        "you may experience inconsistent results." % new_cls.model.__name__)
28
                
16
29
            typemapper[new_cls] = (new_cls.model, new_cls.is_anonymous)
30
        else:
31
            typemapper[new_cls] = (None, new_cls.is_anonymous)
17
32
        
18
33
        if name not in ('BaseHandler', 'AnonymousBaseHandler'):
19
34
            handler_tracker.append(new_cls)

Up to file-list piston/models.py:

1
import urllib
1
import urllib, time, urlparse
2
3
# Django imports
4
from django.db.models.signals import post_save, post_delete
2
5
from django.db import models
3
6
from django.contrib.auth.models import User
4
7
from django.contrib import admin
5
from django.conf import settings
6
8
from django.core.mail import send_mail, mail_admins
7
from django.template import loader
8
9
9
from managers import TokenManager, ConsumerManager, ResourceManager, KEY_SIZE, SECRET_SIZE
10
# Piston imports
11
from managers import TokenManager, ConsumerManager, ResourceManager
12
from signals import consumer_post_save, consumer_post_delete
13
14
KEY_SIZE = 18
15
SECRET_SIZE = 32
16
VERIFIER_SIZE = 10
10
17
11
18
CONSUMER_STATES = (
12
    ('pending', 'Pending approval'),
19
    ('pending', 'Pending'),
13
20
    ('accepted', 'Accepted'),
14
21
    ('canceled', 'Canceled'),
22
    ('rejected', 'Rejected')
15
23
)
16
24
25
def generate_random(length=SECRET_SIZE):
26
    return User.objects.make_random_password(length=length)
27
17
28
class Nonce(models.Model):
18
29
    token_key = models.CharField(max_length=KEY_SIZE)
19
30
    consumer_key = models.CharField(max_length=KEY_SIZE)
@@ -24,18 +35,6 @@ class Nonce(models.Model):
24
35
25
36
admin.site.register(Nonce)
26
37
27
class Resource(models.Model):
28
    name = models.CharField(max_length=255)
29
    url = models.TextField(max_length=2047)
30
    is_readonly = models.BooleanField(default=True)
31
    
32
    objects = ResourceManager()
33
34
    def __unicode__(self):
35
        return u"Resource %s with url %s" % (self.name, self.url)
36
37
admin.site.register(Resource)
38
39
38
class Consumer(models.Model):
40
39
    name = models.CharField(max_length=255)
41
40
    description = models.TextField()
@@ -51,39 +50,26 @@ class Consumer(models.Model):
51
50
    def __unicode__(self):
52
51
        return u"Consumer %s with key %s" % (self.name, self.key)
53
52
54
    def save(self, **kwargs):
55
        super(Consumer, self).save(**kwargs)
56
        
57
        if self.id and self.user:
58
            subject = "API Consumer"
59
            rcpt = [ self.user.email, ]
53
    def generate_random_codes(self):
54
        """
55
        Used to generate random key/secret pairings. Use this after you've
56
        added the other data in place of save(). 
60
57
61
            if self.status == "accepted":
62
                template = "api/mails/consumer_accepted.txt"
63
                subject += " was accepted!"
64
            elif self.status == "canceled":
65
                template = "api/mails/consumer_canceled.txt"
66
                subject += " has been canceled"
67
            else:
68
                template = "api/mails/consumer_pending.txt"
69
                subject += " application received"
70
                
71
                for admin in settings.ADMINS:
72
                    bcc.append(admin[1])
58
        c = Consumer()
59
        c.name = "My consumer" 
60
        c.description = "An app that makes ponies from the API."
61
        c.user = some_user_object
62
        c.generate_random_codes()
63
        """
64
        key = User.objects.make_random_password(length=KEY_SIZE)
65
        secret = generate_random(SECRET_SIZE)
73
66
74
            body = loader.render_to_string(template, 
75
                    { 'consumer': self, 'user': self.user })
76
                    
77
            send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, 
78
                        rcpt, fail_silently=True)
79
            
80
            if self.status == 'pending':
81
                mail_admins(subject, body, fail_silently=True)
82
                        
83
            if settings.DEBUG:
84
                print "Mail being sent, to=%s" % rcpt
85
                print "Subject: %s" % subject
86
                print body
67
        while Consumer.objects.filter(key__exact=key, secret__exact=secret).count():
68
            secret = generate_random(SECRET_SIZE)
69
70
        self.key = key
71
        self.secret = secret
72
        self.save()
87
73
88
74
admin.site.register(Consumer)
89
75
@@ -94,13 +80,17 @@ class Token(models.Model):
94
80
    
95
81
    key = models.CharField(max_length=KEY_SIZE)
96
82
    secret = models.CharField(max_length=SECRET_SIZE)
83
    verifier = models.CharField(max_length=VERIFIER_SIZE)
97
84
    token_type = models.IntegerField(choices=TOKEN_TYPES)
98
    timestamp = models.IntegerField()
85
    timestamp = models.IntegerField(default=long(time.time()))
99
86
    is_approved = models.BooleanField(default=False)
100
87
    
101
88
    user = models.ForeignKey(User, null=True, blank=True, related_name='tokens')
102
89
    consumer = models.ForeignKey(Consumer)
103
90
    
91
    callback = models.CharField(max_length=255, null=True, blank=True)
92
    callback_confirmed = models.BooleanField(default=False)
93
    
104
94
    objects = TokenManager()
105
95
    
106
96
    def __unicode__(self):
@@ -109,10 +99,52 @@ class Token(models.Model):
109
99
    def to_string(self, only_key=False):
110
100
        token_dict = {
111
101
            'oauth_token': self.key, 
112
            'oauth_token_secret': self.secret
102
            'oauth_token_secret': self.secret,
103
            'oauth_callback_confirmed': 'true',
113
104
        }
105
106
        if self.verifier:
107
            token_dict.update({ 'oauth_verifier': self.verifier })
108
114
109
        if only_key:
115
110
            del token_dict['oauth_token_secret']
111
116
112
        return urllib.urlencode(token_dict)
117
113
114
    def generate_random_codes(self):
115
        key = User.objects.make_random_password(length=KEY_SIZE)
116
        secret = generate_random(SECRET_SIZE)
117
118
        while Token.objects.filter(key__exact=key, secret__exact=secret).count():
119
            secret = generate_random(SECRET_SIZE)
120
121
        self.key = key
122
        self.secret = secret
123
        self.save()
124
        
125
    # -- OAuth 1.0a stuff
126
127
    def get_callback_url(self):
128
        if self.callback and self.verifier:
129
            # Append the oauth_verifier.
130
            parts = urlparse.urlparse(self.callback)
131
            scheme, netloc, path, params, query, fragment = parts[:6]
132
            if query:
133
                query = '%s&oauth_verifier=%s' % (query, self.verifier)
134
            else:
135
                query = 'oauth_verifier=%s' % self.verifier
136
            return urlparse.urlunparse((scheme, netloc, path, params,
137
                query, fragment))
138
        return self.callback
139
    
140
    def set_callback(self, callback):
141
        if callback != "oob": # out of band, says "we can't do this!"
142
            self.callback = callback
143
            self.callback_confirmed = True
144
            self.save()
145
        
118
146
admin.site.register(Token)
147
148
# Attach our signals
149
post_save.connect(consumer_post_save, sender=Consumer)
150
post_delete.connect(consumer_post_delete, sender=Consumer)

Up to file-list piston/oauth.py:

1
"""
2
The MIT License
3
4
Copyright (c) 2007 Leah Culver
5
6
Permission is hereby granted, free of charge, to any person obtaining a copy
7
of this software and associated documentation files (the "Software"), to deal
8
in the Software without restriction, including without limitation the rights
9
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
copies of the Software, and to permit persons to whom the Software is
11
furnished to do so, subject to the following conditions:
12
13
The above copyright notice and this permission notice shall be included in
14
all copies or substantial portions of the Software.
15
16
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
THE SOFTWARE.
23
"""
24
1
25
import cgi
2
26
import urllib
3
27
import time
4
28
import random
5
29
import urlparse
6
30
import hmac
7
import base64
31
import binascii
32
8
33
9
34
VERSION = '1.0' # Hi Blaine!
10
35
HTTP_METHOD = 'GET'
11
36
SIGNATURE_METHOD = 'PLAINTEXT'
12
37
13
# Generic exception class
38
14
39
class OAuthError(RuntimeError):
15
    def get_message(self): 
16
        return self._message
17
18
    def set_message(self, message): 
19
        self._message = message
20
21
    message = property(get_message, set_message)
22
40
    """Generic exception class."""
23
41
    def __init__(self, message='OAuth error occured.'):
24
42
        self.message = message
25
43
26
# optional WWW-Authenticate header (401 error)
27
44
def build_authenticate_header(realm=''):
28
    return { 'WWW-Authenticate': 'OAuth realm="%s"' % realm }
45
    """Optional WWW-Authenticate header (401 error)"""
46
    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
29
47
30
# url escape
31
48
def escape(s):
32
    # escape '/' too
49
    """Escape a URL including any /."""
33
50
    return urllib.quote(s, safe='~')
34
51
35
# util function: current timestamp
36
# seconds since epoch (UTC)
52
def _utf8_str(s):
53
    """Convert unicode to utf-8."""
54
    if isinstance(s, unicode):
55
        return s.encode("utf-8")
56
    else:
57
        return str(s)
58
37
59
def generate_timestamp():
60
    """Get seconds since epoch (UTC)."""
38
61
    return int(time.time())
39
62
40
# util function: nonce
41
# pseudorandom number
42
63
def generate_nonce(length=8):
43
    return ''.join(str(random.randint(0, 9)) for i in range(length))
64
    """Generate pseudorandom number."""
65
    return ''.join([str(random.randint(0, 9)) for i in range(length)])
44
66
45
# OAuthConsumer is a data type that represents the identity of the Consumer
46
# via its shared secret with the Service Provider.
67
def generate_verifier(length=8):
68
    """Generate pseudorandom number."""
69
    return ''.join([str(random.randint(0, 9)) for i in range(length)])
70
71
47
72
class OAuthConsumer(object):
73
    """Consumer of OAuth authentication.
74
75
    OAuthConsumer is a data type that represents the identity of the Consumer
76
    via its shared secret with the Service Provider.
77
78
    """
48
79
    key = None
49
80
    secret = None
50
81
@@ -52,39 +83,79 @@ class OAuthConsumer(object):
52
83
        self.key = key
53
84
        self.secret = secret
54
85
55
# OAuthToken is a data type that represents an End User via either an access
56
# or request token.     
86
57
87
class OAuthToken(object):
58
    # access tokens and request tokens
88
    """OAuthToken is a data type that represents an End User via either an access
89
    or request token.
90
    
91
    key -- the token
92
    secret -- the token secret
93
94
    """
59
95
    key = None
60
96
    secret = None
97
    callback = None
98
    callback_confirmed = None
99
    verifier = None
61
100
62
    '''
63
    key = the token
64
    secret = the token secret
65
    '''
66
101
    def __init__(self, key, secret):
67
102
        self.key = key
68
103
        self.secret = secret
69
104
105
    def set_callback(self, callback):
106
        self.callback = callback
107
        self.callback_confirmed = 'true'
108
109
    def set_verifier(self, verifier=None):
110
        if verifier is not None:
111
            self.verifier = verifier
112
        else:
113
            self.verifier = generate_verifier()
114
115
    def get_callback_url(self):
116
        if self.callback and self.verifier:
117
            # Append the oauth_verifier.
118
            parts = urlparse.urlparse(self.callback)
119
            scheme, netloc, path, params, query, fragment = parts[:6]
120
            if query:
121
                query = '%s&oauth_verifier=%s' % (query, self.verifier)
122
            else:
123
                query = 'oauth_verifier=%s' % self.verifier
124
            return urlparse.urlunparse((scheme, netloc, path, params,
125
                query, fragment))
126
        return self.callback
127
70
128
    def to_string(self):
71
        return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
72
73
    # return a token from something like:
74
    # oauth_token_secret=digg&oauth_token=digg
75
    @staticmethod   
129
        data = {
130
            'oauth_token': self.key,
131
            'oauth_token_secret': self.secret,
132
        }
133
        if self.callback_confirmed is not None:
134
            data['oauth_callback_confirmed'] = self.callback_confirmed
135
        return urllib.urlencode(data)
136
 
76
137
    def from_string(s):
138
        """ Returns a token from something like:
139
        oauth_token_secret=xxx&oauth_token=xxx
140
        """
77
141
        params = cgi.parse_qs(s, keep_blank_values=False)
78
142
        key = params['oauth_token'][0]
79
143
        secret = params['oauth_token_secret'][0]
80
        return OAuthToken(key, secret)
144
        token = OAuthToken(key, secret)
145
        try:
146
            token.callback_confirmed = params['oauth_callback_confirmed'][0]
147
        except KeyError:
148
            pass # 1.0, no callback confirmed.
149
        return token
150
    from_string = staticmethod(from_string)
81
151
82
152
    def __str__(self):
83
153
        return self.to_string()
84
154
85
# OAuthRequest represents the request and can be serialized
155
86
156
class OAuthRequest(object):
87
    '''
157
    """OAuthRequest represents the request and can be serialized.
158
88
159
    OAuth parameters:
89
160
        - oauth_consumer_key 
90
161
        - oauth_token
@@ -93,9 +164,10 @@ class OAuthRequest(object):
93
164
        - oauth_timestamp 
94
165
        - oauth_nonce
95
166
        - oauth_version
167
        - oauth_verifier
96
168
        ... any additional parameters, as defined by the Service Provider.
97
    '''
98
    parameters = None # oauth parameters
169
    """
170
    parameters = None # OAuth parameters.
99
171
    http_method = HTTP_METHOD
100
172
    http_url = None
101
173
    version = VERSION
@@ -115,93 +187,107 @@ class OAuthRequest(object):
115
187
            raise OAuthError('Parameter not found: %s' % parameter)
116
188
117
189
    def _get_timestamp_nonce(self):
118
        return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce')
190
        return self.get_parameter('oauth_timestamp'), self.get_parameter(
191
            'oauth_nonce')
119
192
120
    # get any non-oauth parameters
121
193
    def get_nonoauth_parameters(self):
194
        """Get any non-OAuth parameters."""
122
195
        parameters = {}
123
196
        for k, v in self.parameters.iteritems():
124
            # ignore oauth parameters
197
            # Ignore oauth parameters.
125
198
            if k.find('oauth_') < 0:
126
199
                parameters[k] = v
127
200
        return parameters
128
201
129
    # serialize as a header for an HTTPAuth request
130
202
    def to_header(self, realm=''):
203
        """Serialize as a header for an HTTPAuth request."""
131
204
        auth_header = 'OAuth realm="%s"' % realm
132
        # add the oauth parameters
205
        # Add the oauth parameters.
133
206
        if self.parameters:
134
207
            for k, v in self.parameters.iteritems():
135
                auth_header += ', %s="%s"' % (k, escape(str(v)))
208
                if k[:6] == 'oauth_':
209
                    auth_header += ', %s="%s"' % (k, escape(str(v)))
136
210
        return {'Authorization': auth_header}
137
211
138
    # serialize as post data for a POST request
139
212
    def to_postdata(self):
140
        return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems())
213
        """Serialize as post data for a POST request."""
214
        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
215
            for k, v in self.parameters.iteritems()])
141
216
142
    # serialize as a url for a GET request
143
217
    def to_url(self):
218
        """Serialize as a URL for a GET request."""
144
219
        return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
145
220
146
    # return a string that consists of all the parameters that need to be signed
147
221
    def get_normalized_parameters(self):
222
        """Return a string that contains the parameters that must be signed."""
148
223
        params = self.parameters
149
224
        try:
150
            # exclude the signature if it exists
225
            # Exclude the signature if it exists.
151
226
            del params['oauth_signature']
152
227
        except:
153
228
            pass
154
        key_values = params.items()
155
        # sort lexicographically, first after key, then after value
229
        # Escape key values before sorting.
230
        key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
231
            for k,v in params.items()]
232
        # Sort lexicographically, first after key, then after value.
156
233
        key_values.sort()
157
        # combine key value pairs in string and escape
158
        return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values)
234
        # Combine key value pairs into a string.
235
        return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
159
236
160
    # just uppercases the http method
161
237
    def get_normalized_http_method(self):
238
        """Uppercases the http method."""
162
239
        return self.http_method.upper()
163
240
164
    # parses the url and rebuilds it to be scheme://host/path
165
241
    def get_normalized_http_url(self):
242
        """Parses the URL and rebuilds it to be scheme://host/path."""
166
243
        parts = urlparse.urlparse(self.http_url)
167
        url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
168
        return url_string
169
        
170
    # set the signature parameter to the result of build_signature
244
        scheme, netloc, path = parts[:3]
245
        # Exclude default port numbers.
246
        if scheme == 'http' and netloc[-3:] == ':80':
247
            netloc = netloc[:-3]
248
        elif scheme == 'https' and netloc[-4:] == ':443':
249
            netloc = netloc[:-4]
250
        return '%s://%s%s' % (scheme, netloc, path)
251
171
252
    def sign_request(self, signature_method, consumer, token):
172
        # set the signature method
173
        self.set_parameter('oauth_signature_method', signature_method.get_name())
174
        # set the signature
175
        self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token))
253
        """Set the signature parameter to the result of build_signature."""
254
        # Set the signature method.
255
        self.set_parameter('oauth_signature_method',
256
            signature_method.get_name())
257
        # Set the signature.
258
        self.set_parameter('oauth_signature',
259
            self.build_signature(signature_method, consumer, token))
176
260
177
261
    def build_signature(self, signature_method, consumer, token):
178
        # call the build signature method within the signature method
262
        """Calls the build signature method within the signature method."""
179
263
        return signature_method.build_signature(self, consumer, token)
180
264
181
    @staticmethod
182
    def from_request(http_method, http_url, headers=None, parameters=None, query_string=None):
183
        # combine multiple parameter sources
265
    def from_request(http_method, http_url, headers=None, parameters=None,
266
            query_string=None):
267
        """Combines multiple parameter sources."""
184
268
        if parameters is None:
185
269
            parameters = {}
186
270
187
        # headers
188
        if headers and 'HTTP_AUTHORIZATION' in headers:
189
            auth_header = headers['HTTP_AUTHORIZATION']
190
            # check that the authorization header is OAuth
191
            if auth_header.index('OAuth') > -1:
271
        # Headers
272
        if headers and 'Authorization' in headers:
273
            auth_header = headers['Authorization']
274
            # Check that the authorization header is OAuth.
275
            if auth_header[:6] == 'OAuth ':
276
                auth_header = auth_header[6:]
192
277
                try:
193
                    # get the parameters from the header
278
                    # Get the parameters from the header.
194
279
                    header_params = OAuthRequest._split_header(auth_header)
195
280
                    parameters.update(header_params)
196
281
                except:
197
                    raise OAuthError('Unable to parse OAuth parameters from Authorization header.')
282
                    raise OAuthError('Unable to parse OAuth parameters from '
283
                        'Authorization header.')
198
284
199
        # GET or POST query string
285
        # GET or POST query string.
200
286
        if query_string:
201
287
            query_params = OAuthRequest._split_url_string(query_string)
202
288
            parameters.update(query_params)
203
289
204
        # URL parameters
290
        # URL parameters.
205
291
        param_str = urlparse.urlparse(http_url)[4] # query
206
292
        url_params = OAuthRequest._split_url_string(param_str)
207
293
        parameters.update(url_params)
@@ -210,9 +296,11 @@ class OAuthRequest(object):
210
296
            return OAuthRequest(http_method, http_url, parameters)
211
297
212
298
        return None
299
    from_request = staticmethod(from_request)
213
300
214
    @staticmethod
215
    def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
301
    def from_consumer_and_token(oauth_consumer, token=None,
302
            callback=None, verifier=None, http_method=HTTP_METHOD,
303
            http_url=None, parameters=None):
216
304
        if not parameters:
217
305
            parameters = {}
218
306
@@ -228,50 +316,57 @@ class OAuthRequest(object):
228
316
229
317
        if token:
230
318
            parameters['oauth_token'] = token.key
319
            parameters['oauth_callback'] = token.callback
320
            # 1.0a support for verifier.
321
            parameters['oauth_verifier'] = verifier
322
        elif callback:
323
            # 1.0a support for callback in the request token request.
324
            parameters['oauth_callback'] = callback
231
325
232
326
        return OAuthRequest(http_method, http_url, parameters)
327
    from_consumer_and_token = staticmethod(from_consumer_and_token)
233
328
234
    @staticmethod
235
    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
329
    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
330
            http_url=None, parameters=None):
236
331
        if not parameters:
237
332
            parameters = {}
238
333
239
334
        parameters['oauth_token'] = token.key
240
335
241
336
        if callback:
242
            parameters['oauth_callback'] = escape(callback)
337
            parameters['oauth_callback'] = callback
243
338
244
339
        return OAuthRequest(http_method, http_url, parameters)
340
    from_token_and_callback = staticmethod(from_token_and_callback)
245
341
246
    # util function: turn Authorization: header into parameters, has to do some unescaping
247
    @staticmethod
248
342
    def _split_header(header):
343
        """Turn Authorization: header into parameters."""
249
344
        params = {}
250
        header = header.replace('OAuth ', '', 1)
251
345
        parts = header.split(',')
252
346
        for param in parts:
253
            # ignore realm parameter
347
            # Ignore realm parameter.
254
348
            if param.find('realm') > -1:
255
349
                continue
256
            # remove whitespace
350
            # Remove whitespace.
257
351
            param = param.strip()
258
            # split key-value
352
            # Split key-value.
259
353
            param_parts = param.split('=', 1)
260
            # remove quotes and unescape the value
354
            # Remove quotes and unescape the value.
261
355
            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
262
356
        return params
263
    
264
    # util function: turn url string into parameters, has to do some unescaping
265
    @staticmethod
357
    _split_header = staticmethod(_split_header)
358
266
359
    def _split_url_string(param_str):
360
        """Turn URL string into parameters."""
267
361
        parameters = cgi.parse_qs(param_str, keep_blank_values=False)
268
362
        for k, v in parameters.iteritems():
269
363
            parameters[k] = urllib.unquote(v[0])
270
364
        return parameters
365
    _split_url_string = staticmethod(_split_url_string)
271
366
272
# OAuthServer is a worker to check a requests validity against a data store
273
367
class OAuthServer(object):
274
    timestamp_threshold = 300 # in seconds, five minutes
368
    """A worker to check the validity of a request against a data store."""
369
    timestamp_threshold = 300 # In seconds, five minutes.
275
370
    version = VERSION
276
371
    signature_methods = None
277
372
    data_store = None
@@ -280,7 +375,7 @@ class OAuthServer(object):
280
375
        self.data_store = data_store
281
376
        self.signature_methods = signature_methods or {}
282
377
283
    def set_data_store(self, oauth_data_store):
378
    def set_data_store(self, data_store):
284
379
        self.data_store = data_store
285
380
286
381
    def get_data_store(self):
@@ -290,57 +385,64 @@ class OAuthServer(object):
290
385
        self.signature_methods[signature_method.get_name()] = signature_method
291
386
        return self.signature_methods
292
387
293
    # process a request_token request
294
    # returns the request token on success
295
388
    def fetch_request_token(self, oauth_request):
389
        """Processes a request_token request and returns the
390
        request token on success.
391
        """
296
392
        try:
297
            # get the request token for authorization
393
            # Get the request token for authorization.
298
394
            token = self._get_token(oauth_request, 'request')
299
395
        except OAuthError:
300
            # no token required for the initial token request
396
            # No token required for the initial token request.
301
397
            version = self._get_version(oauth_request)
302
398
            consumer = self._get_consumer(oauth_request)
399
            try:
400
                callback = self.get_callback(oauth_request)
401
            except OAuthError:
402
                callback = None # 1.0, no callback specified.
303
403
            self._check_signature(oauth_request, consumer, None)
304
            # fetch a new token
305
            token = self.data_store.fetch_request_token(consumer)
404
            # Fetch a new token.
405
            token = self.data_store.fetch_request_token(consumer, callback)
306
406
        return token
307
407
308
    # process an access_token request
309
    # returns the access token on success
310
408
    def fetch_access_token(self, oauth_request):
409
        """Processes an access_token request and returns the
410
        access token on success.
411
        """
311
412
        version = self._get_version(oauth_request)
312
413
        consumer = self._get_consumer(oauth_request)
313
        # get the request token
414
        verifier = self._get_verifier(oauth_request)
415
        # Get the request token.
314
416
        token = self._get_token(oauth_request, 'request')
315
417
        self._check_signature(oauth_request, consumer, token)
316
        new_token = self.data_store.fetch_access_token(consumer, token)
418
        new_token = self.data_store.fetch_access_token(consumer, token, verifier)
317
419
        return new_token
318
420
319
    # verify an api call, checks all the parameters
320
421
    def verify_request(self, oauth_request):
422
        """Verifies an api call and checks all the parameters."""
321
423
        # -> consumer and token
322
424
        version = self._get_version(oauth_request)
323
425
        consumer = self._get_consumer(oauth_request)
324
        # get the access token
426
        # Get the access token.
325
427
        token = self._get_token(oauth_request, 'access')
326
428
        self._check_signature(oauth_request, consumer, token)
327
429
        parameters = oauth_request.get_nonoauth_parameters()
328
430
        return consumer, token, parameters
329
431
330
    # authorize a request token
331
432
    def authorize_token(self, token, user):
433
        """Authorize a request token."""
332
434
        return self.data_store.authorize_request_token(token, user)
333
    
334
    # get the callback url
435
335
436
    def get_callback(self, oauth_request):
437
        """Get the callback URL."""
336
438
        return oauth_request.get_parameter('oauth_callback')
337
338
    # optional support for the authenticate header   
439
 
339
440
    def build_authenticate_header(self, realm=''):
441
        """Optional support for the authenticate header."""
340
442
        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
341
443
342
    # verify the correct version request for this server
343
444
    def _get_version(self, oauth_request):
445
        """Verify the correct version request for this server."""
344
446
        try:
345
447
            version = oauth_request.get_parameter('oauth_version')
346
448
        except:
@@ -349,37 +451,40 @@ class OAuthServer(object):
349
451
            raise OAuthError('OAuth version %s not supported.' % str(version))
350
452
        return version
351
453
352
    # figure out the signature with some defaults
353
454
    def _get_signature_method(self, oauth_request):
455
        """Figure out the signature with some defaults."""
354
456
        try:
355
            signature_method = oauth_request.get_parameter('oauth_signature_method')
457
            signature_method = oauth_request.get_parameter(
458
                'oauth_signature_method')
356
459
        except:
357
460
            signature_method = SIGNATURE_METHOD
358
461
        try:
359
            # get the signature method object
462
            # Get the signature method object.
360
463
            signature_method = self.signature_methods[signature_method]
361
464
        except:
362
465
            signature_method_names = ', '.join(self.signature_methods.keys())
363
            raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
466
            raise OAuthError('Signature method %s not supported try one of the '
467
                'following: %s' % (signature_method, signature_method_names))
364
468
365
469
        return signature_method
366
470
367
471
    def _get_consumer(self, oauth_request):
368
472
        consumer_key = oauth_request.get_parameter('oauth_consumer_key')
369
        if not consumer_key:
370
            raise OAuthError('Invalid consumer key.')
371
473
        consumer = self.data_store.lookup_consumer(consumer_key)
372
474
        if not consumer:
373
475
            raise OAuthError('Invalid consumer.')
374
476
        return consumer
375
477
376
    # try to find the token for the provided request token key
377
478
    def _get_token(self, oauth_request, token_type='access'):
479
        """Try to find the token for the provided request token key."""
378
480
        token_field = oauth_request.get_parameter('oauth_token')
379
481
        token = self.data_store.lookup_token(token_type, token_field)
380
482
        if not token:
381
483
            raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
382
484
        return token
485
    
486
    def _get_verifier(self, oauth_request):
487
        return oauth_request.get_parameter('oauth_verifier')
383
488
384
489
    def _check_signature(self, oauth_request, consumer, token):
385
490
        timestamp, nonce = oauth_request._get_timestamp_nonce()
@@ -390,29 +495,35 @@ class OAuthServer(object):
390
495
            signature = oauth_request.get_parameter('oauth_signature')
391
496
        except:
392
497
            raise OAuthError('Missing signature.')
393
        # validate the signature
394
        valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature)
498
        # Validate the signature.
499
        valid_sig = signature_method.check_signature(oauth_request, consumer,
500
            token, signature)
395
501
        if not valid_sig:
396
            key, base = signature_method.build_signature_base_string(oauth_request, consumer, token)
397
            raise OAuthError('Invalid signature. Expected signature base string: %s' % base)
502
            key, base = signature_method.build_signature_base_string(
503
                oauth_request, consumer, token)
504
            raise OAuthError('Invalid signature. Expected signature base '
505
                'string: %s' % base)
398
506
        built = signature_method.build_signature(oauth_request, consumer, token)
399
507
400
508
    def _check_timestamp(self, timestamp):
401
        # verify that timestamp is recentish
509
        """Verify that timestamp is recentish."""
402
510
        timestamp = int(timestamp)
403
511
        now = int(time.time())
404
512
        lapsed = now - timestamp
405
513
        if lapsed > self.timestamp_threshold:
406
            raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
514
            raise OAuthError('Expired timestamp: given %d and now %s has a '
515
                'greater difference than threshold %d' %
516
                (timestamp, now, self.timestamp_threshold))
407
517
408
518
    def _check_nonce(self, consumer, token, nonce):
409
        # verify that the nonce is uniqueish
519
        """Verify that the nonce is uniqueish."""
410
520
        nonce = self.data_store.lookup_nonce(consumer, token, nonce)
411
521
        if nonce:
412
522
            raise OAuthError('Nonce already used: %s' % str(nonce))
413
523
414
# OAuthClient is a worker to attempt to execute a request
524
415
525
class OAuthClient(object):
526
    """OAuthClient is a worker to attempt to execute a request."""
416
527
    consumer = None
417
528
    token = None
418
529
@@ -427,62 +538,65 @@ class OAuthClient(object):
427
538
        return self.token
428
539
429
540
    def fetch_request_token(self, oauth_request):
430
        # -> OAuthToken
541
        """-> OAuthToken."""
431
542
        raise NotImplementedError
432
543
433
544
    def fetch_access_token(self, oauth_request):
434
        # -> OAuthToken
545
        """-> OAuthToken."""
435
546
        raise NotImplementedError
436
547
437
548
    def access_resource(self, oauth_request):
438
        # -> some protected resource
549
        """-> Some protected resource."""
439
550
        raise NotImplementedError
440
551
441
# OAuthDataStore is a database abstraction used to lookup consumers and tokens
552
442
553
class OAuthDataStore(object):
554
    """A database abstraction used to lookup consumers and tokens."""
443
555
444
556
    def lookup_consumer(self, key):
445
        # -> OAuthConsumer
557
        """-> OAuthConsumer."""
446
558
        raise NotImplementedError
447
559
448
560
    def lookup_token(self, oauth_consumer, token_type, token_token):
449
        # -> OAuthToken
561
        """-> OAuthToken."""
450
562
        raise NotImplementedError
451
563
452
    def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp):
453
        # -> OAuthToken
564
    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
565
        """-> OAuthToken."""
454
566
        raise NotImplementedError
455
567
456
    def fetch_request_token(self, oauth_consumer):
457
        # -> OAuthToken
568
    def fetch_request_token(self, oauth_consumer, oauth_callback):
569
        """-> OAuthToken."""
458
570
        raise NotImplementedError
459
571
460
    def fetch_access_token(self, oauth_consumer, oauth_token):
461
        # -> OAuthToken
572
    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
573
        """-> OAuthToken."""
462
574
        raise NotImplementedError
463
575
464
576
    def authorize_request_token(self, oauth_token, user):
465
        # -> OAuthToken
577
        """-> OAuthToken."""
466
578
        raise NotImplementedError
467
579
468
# OAuthSignatureMethod is a strategy class that implements a signature method
580
469
581
class OAuthSignatureMethod(object):
582
    """A strategy class that implements a signature method."""
470
583
    def get_name(self):
471
        # -> str
584
        """-> str."""
472
585
        raise NotImplementedError
473
586
474
587
    def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
475
        # -> str key, str raw
588
        """-> str key, str raw."""
476
589
        raise NotImplementedError
477
590
478
591
    def build_signature(self, oauth_request, oauth_consumer, oauth_token):
479
        # -> str
592
        """-> str."""
480
593
        raise NotImplementedError
481
594
482
595
    def check_signature(self, oauth_request, consumer, token, signature):
483
596
        built = self.build_signature(oauth_request, consumer, token)
484
597
        return built == signature
485
598
599
486
600
class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
487
601
488
602
    def get_name(self):
@@ -502,19 +616,21 @@ class OAuthSignatureMethod_HMAC_SHA1(OAu
502
616
        return key, raw
503
617
504
618
    def build_signature(self, oauth_request, consumer, token):
505
        # build the base signature string
506
        key, raw = self.build_signature_base_string(oauth_request, consumer, token)
619
        """Builds the base signature string."""
620
        key, raw = self.build_signature_base_string(oauth_request, consumer,
621
            token)
507
622
508
        # hmac object
623
        # HMAC object.
509
624
        try:
510
625
            import hashlib # 2.5
511
626
            hashed = hmac.new(key, raw, hashlib.sha1)
512
627
        except:
513
            import sha # deprecated
628
            import sha # Deprecated
514
629
            hashed = hmac.new(key, raw, sha)
515
630
516
        # calculate the digest base 64
517
        return base64.b64encode(hashed.digest())
631
        # Calculate the digest base 64.
632
        return binascii.b2a_base64(hashed.digest())[:-1]
633
518
634
519
635
class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
520
636
@@ -522,11 +638,13 @@ class OAuthSignatureMethod_PLAINTEXT(OAu
522
638
        return 'PLAINTEXT'
523
639
524
640
    def build_signature_base_string(self, oauth_request, consumer, token):
525
        # concatenate the consumer key and secret
526
        sig = escape(consumer.secret) + '&'
641
        """Concatenates the consumer key and secret."""
642
        sig = '%s&' % escape(consumer.secret)
527
643
        if token:
528
644
            sig = sig + escape(token.secret)
529
        return sig
645
        return sig, sig
530
646
531
647
    def build_signature(self, oauth_request, consumer, token):
532
        return self.build_signature_base_string(oauth_request, consumer, token)
648
        key, raw = self.build_signature_base_string(oauth_request, consumer,
649
            token)
650
        return key

Up to file-list piston/resource.py:

@@ -7,6 +7,7 @@ from django.views.decorators.vary import
7
7
from django.conf import settings
8
8
from django.core.mail import send_mail, EmailMessage
9
9
from django.db.models.query import QuerySet
10
from django.http import Http404
10
11
11
12
from emitters import Emitter
12
13
from handler import typemapper
@@ -59,6 +60,26 @@ class Resource(object):
59
60
60
61
        return em
61
62
    
63
    @property
64
    def anonymous(self):
65
        """
66
        Gets the anonymous handler. Also tries to grab a class
67
        if the `anonymous` value is a string, so that we can define
68
        anonymous handlers that aren't defined yet (like, when
69
        you're subclassing your basehandler into an anonymous one.)
70
        """
71
        if hasattr(self.handler, 'anonymous'):
72
            anon = self.handler.anonymous
73
            
74
            if callable(anon):
75
                return anon
76
77
            for klass in typemapper.keys():
78
                if anon == klass.__name__:
79
                    return klass
80
            
81
        return None
82
    
62
83
    @vary_on_headers('Authorization')
63
84
    def __call__(self, request, *args, **kwargs):
64
85
        """
@@ -73,11 +94,10 @@ class Resource(object):
73
94
            coerce_put_post(request)
74
95
75
96
        if not self.authentication.is_authenticated(request):
76
            if hasattr(self.handler, 'anonymous') and \
77
                callable(self.handler.anonymous) and \
78
                rm in self.handler.anonymous.allowed_methods:
97
            if self.anonymous and \
98
                rm in self.anonymous.allowed_methods:
79
99
80
                handler = self.handler.anonymous()
100
                handler = self.anonymous()
81
101
                anonymous = True
82
102
            else:
83
103
                return self.authentication.challenge()
@@ -113,12 +133,14 @@ class Resource(object):
113
133
        try:
114
134
            result = meth(request, *args, **kwargs)
115
135
        except FormValidationError, e:
116
            # TODO: Use rc.BAD_REQUEST here
117
            return HttpResponse("Bad Request: %s" % e.form.errors, status=400)
136
            resp = rc.BAD_REQUEST
137
            resp.write(' '+str(e.form.errors))
138
            
139
            return resp
118
140
        except TypeError, e:
119
141
            result = rc.BAD_REQUEST
120
142
            hm = HandlerMethod(meth)
121
            sig = hm.get_signature()
143
            sig = hm.signature
122
144
123
145
            msg = 'Method signature does not match.\n\n'
124
146
            
@@ -131,8 +153,9 @@ class Resource(object):
131
153
                msg += '\n\nException was: %s' % str(e)
132
154
                
133
155
            result.content = format_error(msg)
156
        except Http404:
157
            return rc.NOT_FOUND
134
158
        except HttpStatusCode, e:
135
            #result = e ## why is this being passed on and not just dealt with now?
136
159
            return e.response
137
160
        except Exception, e:
138
161
            """
@@ -177,7 +200,10 @@ class Resource(object):
177
200
            if self.stream: stream = srl.stream_render(request)
178
201
            else: stream = srl.render(request)
179
202
180
            resp = HttpResponse(stream, mimetype=ct)
203
            if not isinstance(stream, HttpResponse):
204
                resp = HttpResponse(stream, mimetype=ct)
205
            else:
206
                resp = stream
181
207
182
208
            resp.streaming = self.stream
183
209

Up to file-list piston/store.py:

1
1
import oauth
2
2
3
3
from models import Nonce, Token, Consumer
4
from models import generate_random, VERIFIER_SIZE
4
5
5
6
class DataStore(oauth.OAuthDataStore):
6
7
    """Layer between Python OAuth and Django database."""
@@ -39,17 +40,22 @@ class DataStore(oauth.OAuthDataStore):
39
40
        else:
40
41
            return nonce.key
41
42
42
    def fetch_request_token(self, oauth_consumer):
43
    def fetch_request_token(self, oauth_consumer, oauth_callback):
43
44
        if oauth_consumer.key == self.consumer.key:
44
45
            self.request_token = Token.objects.create_token(consumer=self.consumer,
45
46
                                                            token_type=Token.REQUEST,
46
47
                                                            timestamp=self.timestamp)
48
            
49
            if oauth_callback:
50
                self.request_token.set_callback(oauth_callback)
51
            
47
52
            return self.request_token
48
53
        return None
49
54
50
    def fetch_access_token(self, oauth_consumer, oauth_token):
55
    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
51
56
        if oauth_consumer.key == self.consumer.key \
52
57
        and oauth_token.key == self.request_token.key \
58
        and oauth_verifier == self.request_token.verifier \
53
59
        and self.request_token.is_approved:
54
60
            self.access_token = Token.objects.create_token(consumer=self.consumer,
55
61
                                                           token_type=Token.ACCESS,
@@ -63,6 +69,7 @@ class DataStore(oauth.OAuthDataStore):
63
69
            # authorize the request token in the store
64
70
            self.request_token.is_approved = True
65
71
            self.request_token.user = user
72
            self.request_token.verifier = generate_random(VERIFIER_SIZE)
66
73
            self.request_token.save()
67
74
            return self.request_token
68
75
        return None

Up to file-list piston/utils.py:

1
import time
1
2
from django.http import HttpResponseNotAllowed, HttpResponseForbidden, HttpResponse, HttpResponseBadRequest
2
3
from django.core.urlresolvers import reverse
3
4
from django.core.cache import cache
4
5
from django import get_version as django_version
6
from django.core.mail import send_mail, mail_admins
7
from django.conf import settings
8
from django.utils.translation import ugettext as _
9
from django.template import loader, TemplateDoesNotExist
10
from django.contrib.sites.models import Site
5
11
from decorator import decorator
6
12
7
13
from datetime import datetime, timedelta
8
14
9
__version__ = '0.2.2'
15
__version__ = '0.2.3rc1'
10
16
11
17
def get_version():
12
18
    return __version__
@@ -27,6 +33,7 @@ class rc_factory(object):
27
33
                 NOT_FOUND = ('Not Found', 404),
28
34
                 DUPLICATE_ENTRY = ('Conflict/Duplicate', 409),
29
35
                 NOT_HERE = ('Gone', 410),
36
                 INTERNAL_ERROR = ('Internal Error', 500),
30
37
                 NOT_IMPLEMENTED = ('Not Implemented', 501),
31
38
                 THROTTLED = ('Throttled', 503))
32
39
@@ -102,24 +109,20 @@ def throttle(max_requests, timeout=60*60
102
109
            """
103
110
            ident += ':%s' % extra
104
111
    
105
            now = datetime.now()
106
            ts_key = 'throttle:ts:%s' % ident
107
            timestamp = cache.get(ts_key)
108
            offset = now + timedelta(seconds=timeout)
109
    
110
            if timestamp and timestamp < offset:
112
            now = time.time()
113
            count, expiration = cache.get(ident, (1, None))
114
115
            if expiration is None:
116
                expiration = now + timeout
117
118
            if count >= max_requests and expiration > now:
111
119
                t = rc.THROTTLED
112
                wait = timeout - (offset-timestamp).seconds
120
                wait = int(expiration - now)
113
121
                t.content = 'Throttled, wait %d seconds.' % wait
114
                
122
                t['Retry-After'] = wait
115
123
                return t
116
                
117
            count = cache.get(ident, 1)
118
            cache.set(ident, count+1)
119
            
120
            if count >= max_requests:
121
                cache.set(ts_key, offset, timeout)
122
                cache.set(ident, 1)
124
125
            cache.set(ident, (count+1, expiration), (expiration - now))
123
126
    
124
127
        return f(self, request, *args, **kwargs)
125
128
    return wrap
@@ -211,15 +214,15 @@ class Mimer(object):
211
214
        if not self.is_multipart() and ctype:
212
215
            loadee = self.loader_for_type(ctype)
213
216
            
214
            if loadee:
215
                try:
216
                    self.request.data = loadee(self.request.raw_post_data)
217
            try:
218
                self.request.data = loadee(self.request.raw_post_data)
217
219
                    
218
                    # Reset both POST and PUT from request, as its
219
                    # misleading having their presence around.
220
                    self.request.POST = self.request.PUT = dict()
221
                except (TypeError, ValueError):
222
                    raise MimerDataException
220
                # Reset both POST and PUT from request, as its
221
                # misleading having their presence around.
222
                self.request.POST = self.request.PUT = dict()
223
            except (TypeError, ValueError):
224
                # This also catches if loadee is None.
225
                raise MimerDataException
223
226
224
227
        return self.request
225
228
                
@@ -261,3 +264,47 @@ def require_mime(*mimes):
261
264
262
265
require_extended = require_mime('json', 'yaml', 'xml', 'pickle')
263
266
    
267
def send_consumer_mail(consumer):
268
    """
269
    Send a consumer an email depending on what their status is.
270
    """
271
    try:
272
        subject = settings.PISTON_OAUTH_EMAIL_SUBJECTS[consumer.status]
273
    except AttributeError:
274
        subject = "Your API Consumer for %s " % Site.objects.get_current().name
275
        if consumer.status == "accepted":
276
            subject += "was accepted!"
277
        elif consumer.status == "canceled":
278
            subject += "has been canceled."
279
        elif consumer.status == "rejected":
280
            subject += "has been rejected."
281
        else: 
282
            subject += "is awaiting approval."
283
284
    template = "piston/mails/consumer_%s.txt" % consumer.status    
285
    
286
    try:
287
        body = loader.render_to_string(template, 
288
            { 'consumer' : consumer, 'user' : consumer.user })
289
    except TemplateDoesNotExist:
290
        """ 
291
        They haven't set up the templates, which means they might not want
292
        these emails sent.
293
        """
294
        return 
295
296
    try:
297
        sender = settings.PISTON_FROM_EMAIL
298
    except AttributeError:
299
        sender = settings.DEFAULT_FROM_EMAIL
300
301
    send_mail(_(subject), body, sender, [consumer.user.email], fail_silently=True)
302
303
    if consumer.status == 'pending' and len(settings.ADMINS):
304
        mail_admins(_(subject), body, fail_silently=True)
305
306
    if settings.DEBUG:
307
        print "Mail being sent, to=%s" % consumer.user.email
308
        print "Subject: %s" % _(subject)
309
        print body
310