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.
| commit 120: | 668d9e16c459 |
| parent 119: | 7f0bf6ff404a |
| branch: | default |
12 months ago
Changed (Δ9.5 KB):
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= |
|
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 |
|
|
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 |
|
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 |
|
|
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 b |
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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( |
|
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 |
# |
|
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 |
# |
|
205 |
# Add the oauth parameters. |
|
133 |
206 |
if self.parameters: |
134 |
207 |
for k, v in self.parameters.iteritems(): |
135 |
|
|
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 |
|
|
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 |
# |
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
# |
|
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 |
|
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'] = |
|
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 |
# |
|
347 |
# Ignore realm parameter. |
|
254 |
348 |
if param.find('realm') > -1: |
255 |
349 |
continue |
256 |
# |
|
350 |
# Remove whitespace. |
|
257 |
351 |
param = param.strip() |
258 |
# |
|
352 |
# Split key-value. |
|
259 |
353 |
param_parts = param.split('=', 1) |
260 |
# |
|
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 |
|
|
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 |
|
|
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, |
|
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 |
# |
|
393 |
# Get the request token for authorization. |
|
298 |
394 |
token = self._get_token(oauth_request, 'request') |
299 |
395 |
except OAuthError: |
300 |
# |
|
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 |
|
|
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 |
# |
|
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( |
|
457 |
signature_method = oauth_request.get_parameter( |
|
458 |
'oauth_signature_method') |
|
356 |
459 |
except: |
357 |
460 |
signature_method = SIGNATURE_METHOD |
358 |
461 |
try: |
359 |
# |
|
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 |
|
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 |
|
|
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 |
|
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 |
|
|
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 |
|
|
541 |
"""-> OAuthToken.""" |
|
431 |
542 |
raise NotImplementedError |
432 |
543 |
|
433 |
544 |
def fetch_access_token(self, oauth_request): |
434 |
|
|
545 |
"""-> OAuthToken.""" |
|
435 |
546 |
raise NotImplementedError |
436 |
547 |
|
437 |
548 |
def access_resource(self, oauth_request): |
438 |
|
|
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 |
|
|
557 |
"""-> OAuthConsumer.""" |
|
446 |
558 |
raise NotImplementedError |
447 |
559 |
|
448 |
560 |
def lookup_token(self, oauth_consumer, token_type, token_token): |
449 |
|
|
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 |
|
|
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 |
|
|
584 |
"""-> str.""" |
|
472 |
585 |
raise NotImplementedError |
473 |
586 |
|
474 |
587 |
def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): |
475 |
|
|
588 |
"""-> str key, str raw.""" |
|
476 |
589 |
raise NotImplementedError |
477 |
590 |
|
478 |
591 |
def build_signature(self, oauth_request, oauth_consumer, oauth_token): |
479 |
|
|
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 |
# |
|
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 # |
|
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 |
|
|
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 |
|
|
97 |
if self.anonymous and \ |
|
98 |
rm in self.anonymous.allowed_methods: |
|
79 |
99 |
|
80 |
handler = self. |
|
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. |
|
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 |
|
|
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. |
|
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 = |
|
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 |
|
|
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 |
|
|
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 |
