Merge lp:~mbp/launchpad/701545-oauth into lp:launchpad

Proposed by Martin Pool
Status: Superseded
Proposed branch: lp:~mbp/launchpad/701545-oauth
Merge into: lp:launchpad
Diff against target: 620 lines (+12/-537)
5 files modified
lib/canonical/launchpad/testing/pages.py (+2/-2)
lib/canonical/launchpad/webapp/authentication.py (+3/-2)
lib/canonical/launchpad/webapp/tests/test_authentication.py (+5/-2)
lib/canonical/launchpad/webapp/tests/test_publication.py (+2/-2)
lib/contrib/oauth.py (+0/-529)
To merge this branch: bzr merge lp:~mbp/launchpad/701545-oauth
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
j.c.sackett (community) code* Approve
Review via email: mp+48431@code.launchpad.net

This proposal has been superseded by a proposal from 2011-02-13.

Commit message

[r=jcsackett,sinzui][ui=none][bug=701545][no-qa] remove extra copy of oauth.py

Description of the change

Remove redundant copy of oauth.py.

Launchpad contains a copy of oauth.py, which is a bit out of date. It also has a proper copy of the python-oauth module in lp-sourcedeps.

This doesn't specifically fix any user-visible bugs that I know of, but it removes some cruft, and if the duplication hadn't been there in the first place we would have avoided at least one oauth bug that existed for years.

This branch just deletes the shipped copy, and updates import statements to fetch from the real copy. It passes a smoketest of './bin/test -m oauth' and I'm running a full test.

If this is acceptable would someone please sponsor landing of it?

To post a comment you must log in.
lp:~mbp/launchpad/701545-oauth updated
12314. By Launchpad PQM Bot

[r=sinzui][ui=sinzui][bug=708436] Fixed the width of the tag list so
 it doesn't wrap (bug #708436).

12315. By Launchpad PQM Bot

[r=bac,
 benji][ui=none][bug=699719] A (currently disabled) JS overlay has
 been added for advanced direct bug subscriptions.

12316. By Launchpad PQM Bot

[r=gmb][bug=711901] Implement LIFECYCLE notification level for direct
 subscriptions.

12317. By Launchpad PQM Bot

[r=mthaddon, stub][bug=712282,
 712285][no-qa] Improve nightly.sh transparency,
 and remove update-pkgcache.py from it.

12318. By Launchpad PQM Bot

[bug=75976] [r=abentley][ui=none] When someone asks to merge two
 accounts, Launchpad sends an email to the primary account asking for
 confirmation. That email was hard to understand and badly
 written. This branch rewrites that email's text and introduces
 a test that the email is sent with the correct text.

Revision history for this message
j.c.sackett (jcsackett) wrote :

Martin--

This looks good. Thanks!

review: Approve (code*)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Martin.

Thank you for cleaning up the code. We normally write copyrights as a range. Are you aware of utilities/update-copyright? It will update the copyright in all the modified files in your tree. I think this serial date style breaks the update-copyright script, subsequent updates will require the engineer to edit the copyright afterwards. You can either change the dates to ranges or verify that the script will work on these files in 2012.

review: Approve (code)
lp:~mbp/launchpad/701545-oauth updated
12319. By Launchpad PQM Bot

[no-qa] [r=allenap][ui=none][bug=707103] Clean-up of lint after
 bugs-with-blueprints-bug-707103.

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

On February 3, 2011, Martin Pool wrote:
> If this is acceptable would someone please sponsor landing of it?

All Canonical Bazaar Developers can land branches directly through PQM
nowawayds :-)

lp:~mbp/launchpad/701545-oauth updated
12320. By Launchpad PQM Bot

[r=deryck][ui=none][no-qa] Provide translation sharing information.

12321. By Launchpad PQM Bot

[r=jtv][bug=705652] Log warnings in the remove_translations script if
 current translations are removed. Removed the theoretically
 impossible case of finding a diverged translation being current
 in upstream which is linked to a POTemplate having a diverged
 translation being current in Ubuntu.

12322. By Launchpad PQM Bot

[r=leonardr][bug=705342] Fail nonvirtual builders in the ABORTED
 state rather than cleaning them,
 since we can't guarantee that there's no active build.

12323. By Launchpad PQM Bot

[r=lifeless][bug=709717] Remove an unnecessary join to
 sourcepackagerelease when counting packages that are being
 built/waiting to be built in ArchiveView.num_pkgs_building

Revision history for this message
Martin Pool (mbp) wrote :

Thanks, Curtis. I was not aware of that. I will send this to pqm.

Revision history for this message
Martin Pool (mbp) wrote :

On 4 February 2011 03:52, Francis J. Lacoste <email address hidden> wrote:
> On February 3, 2011, Martin Pool wrote:
>> If this is acceptable would someone please sponsor landing of it?
>
> All Canonical Bazaar Developers can land branches directly through PQM
> nowawayds :-)

No, I still get a gpgv error from pqm.

lp:~mbp/launchpad/701545-oauth updated
12324. By Launchpad PQM Bot

[r=thumper][bug=316694] Add a web_link property to the JSON
 representation of all web service entries that correspond to
 some page on the Launchpad website.

Revision history for this message
Martin Pool (mbp) wrote :

spm helped me sort this out and it's in the queue now.

- Martin
On 04/02/2011 11:35 AM, "Martin Pool" <email address hidden> wrote:
> On 4 February 2011 03:52, Francis J. Lacoste <email address hidden>
wrote:
>> On February 3, 2011, Martin Pool wrote:
>>> If this is acceptable would someone please sponsor landing of it?
>>
>> All Canonical Bazaar Developers can land branches directly through PQM
>> nowawayds :-)
>
> No, I still get a gpgv error from pqm.

lp:~mbp/launchpad/701545-oauth updated
12325. By Launchpad PQM Bot

[r=jcsackett,
 sinzui][ui=none][bug=701545][no-qa] remove extra copy of oauth.py

12326. By Launchpad PQM Bot

[r=lifeless][ui=none][no-qa] Remove duplicated urgencymap in gina.

12327. By Launchpad PQM Bot

[r=lifeless][bug=670452] Fix layout of related branches collapsible
 fieldset

12328. By Launchpad PQM Bot

[r=lifeless,
 wallyworld][bug=712882][no-qa] Silence non-error output from
 mirror-prober.sh.

12329. By Launchpad PQM Bot

[testfix][r=me][no-qa] Revert r12325 as the new oauth module is buggy
 and causes a test to fail

12330. By Launchpad PQM Bot

[r=henninge][ui=none][no-qa] Add direct unit tests for
 TM.shareIfPossible

12331. By Launchpad PQM Bot

[r=allenap][ui=none][bug=693689] Escape the token and title in
 lp.app.widgets.itemswidgets renderer.

12332. By Launchpad PQM Bot

[r=bac][bug=710054][bug=530476][bug=530477] Fix the protocol in the
 gpg documentation and branch and MP api docs.

12333. By Launchpad PQM Bot

[r=allenap][no-qa] Machinery for building documentation with sphinx

12334. By Launchpad PQM Bot

[r=henninge][ui=none][bug=697294] Refactors create_questionreopening
 to be called as a helper, rather than a listener to notifications;
 this avoids the need for a slew of guards on the method, and keeps it
 from doing work when we don't want it to.

12335. By Launchpad PQM Bot

[testfix][rs=sinzui] Print the escaped text of the structured string.

12336. By Launchpad PQM Bot

Merging db-stable at revno 12335

12337. By Launchpad PQM Bot

[release-critical=wallyworld][r=jtv][bug=710591][incr] Provide
 TranslationMessage.acceptFromImport and
 .acceptFromUpstreamImportOnPackage.

12338. By Launchpad PQM Bot

[wallyworld,spm] merge db-devel into devel; needed for an RC DB patch

12339. By Launchpad PQM Bot

[r=gmb][bug=548] adds a simple way to control whether you want to
 receive bug notifications for changes you generated.

12340. By Launchpad PQM Bot

[r=gmb][no-qa] test, tweak, and exercise the dbuser test helper

12341. By Launchpad PQM Bot

[r=jcsackett, sinzui][ui=none][bug=344125,
 712012] Remove obsolete method Bug.addChangeNotification().

12342. By Launchpad PQM Bot

[r=leonardr][no-qa] Use itertools.tee() to simplify and speed up
 CachingIterator.

12343. By Launchpad PQM Bot

[r=gmb][bug=713392] make the +structural-subscriptions UI correctly
 show the effect of an event filter setting.

12344. By Launchpad PQM Bot

[r=thumper, wallyworld][ui=none] [r=thumper, wallyworld][bug=661988,
 714312] Reduce the time taken for distro scope bug searches by 50% by
 using sequence-of-sets based eager loading rather than
 wide-query eager loading. As part of making this fit cleanly
 stop loading assignees during bug task search (they are not
 rendered so not needed).

12345. By Launchpad PQM Bot

[r=gmb][ui=none][bug=50616] Catch the ValueError while validating
 image file.

12346. By Launchpad PQM Bot

[r=leonardr][bug=714527] Properly escape labels in suggestion widgets.

12347. By Launchpad PQM Bot

[r=sinzui][bug=715474] Permit showing server side render times in the
 visible page (controlled by feature flag visible_render_time).

12348. By Launchpad PQM Bot

[r=lifeless][bug=715000] Upgrade to a bzr with a fix for the graph
 ancestry chained stacking issue.

12349. By Launchpad PQM Bot

[r=lifeless][bug=713234] Remove duplicated queries issued when
 calling ArchiveView.latest_updates

12350. By Launchpad PQM Bot

[r=leonardr][bug=715116] Add a big informational notice on the PPA
 pages if publishing is disabled.

12351. By Launchpad PQM Bot

[r=jtv][bug=715659] Use zope.exceptions to format logged exceptions
 so that __traceback_info__ is reported.

12352. By Launchpad PQM Bot

[r=adeuring][bug=181368] Gut BinaryPackageFilePublishing view.

12353. By Launchpad PQM Bot

[r=jtv][bug=714820] Ignore malformed responses when probing remote
 Bugzilla instances for XML-RPC capabilities.

12354. By Launchpad PQM Bot

[r=allenap][bug=710603] The new direct subscription overlay allows
 subscribing,
 unsubscribing and editing subscriptions and is available to the
 ~malone-alpha team.

12355. By Launchpad PQM Bot

[r=jtv][bug=705657] Fix mirror-prober.sh wrapper script to export the
 correct config. (Fix bug 705657)

12356. By Launchpad PQM Bot

[r=jtv] [ui=none][bug=316694][incr] Take advantage of web_link when
 changing the URL in the browser bar,
 rather than hacking self_link into web_link.

12357. By Launchpad PQM Bot

[r=flacoste][bug=716109] Update Launchpad to r202 of lazr-js,
 which includes an update to YUI 3.3.

12358. By Launchpad PQM Bot

[r=lifeless][bug=457856] Added manual close to tag dialogue if it is
 still open when tags are saved.

12359. By Launchpad PQM Bot

[r=allenap][bug=528367] Added alt tag to remove icons.

12360. By Launchpad PQM Bot

[r=thumper][no-qa] Mechanical move of
 canonical.launchpad.windmill.testing to lp.testing.windmill.

12361. By Launchpad PQM Bot

[r=jcsackett,sinzui][bug=713382] test and fix for bug 713382

12362. By Launchpad PQM Bot

[r=jcsackett,sinzui][no-qa] Create a job for merging translations.

12363. By Launchpad PQM Bot

[r=gmb][bug=632847] Adds check to prevent bugtasks for deactivated
 products from showing up for people who can't see the product.

12364. By Launchpad PQM Bot

[r=lifeless][bug=715992] Corrects use of canonical_url so that
 requests for pillars via alias on the API do not redirect
 outside of the API layer.

12365. By Launchpad PQM Bot

[r=stub,
 wallyworld][ui=none][no-qa] Enforce BugMessage.index population on
 all new BugMessage creation.

12366. By Launchpad PQM Bot

[r=sinzui][ui=sinzui][bug=710446] Unified the colours of the facets
 (bugs, code etc.) so that they are all the same.

12367. By Launchpad PQM Bot

[r=bac, benji][no-qa] move some code,
 which lets you look at bug activity log entires more precisely,
 from browser code to model code. This is a step along the path to
 addressing bug 164196.

12368. By Martin Pool

Remove dependency on oauth egg

12369. By Martin Pool

Use external python-oauth and remove contrib/oauth.py

12370. By Martin Pool

Also remove oauth from versions.cfg

Unmerged revisions

12370. By Martin Pool

Also remove oauth from versions.cfg

12369. By Martin Pool

Use external python-oauth and remove contrib/oauth.py

12368. By Martin Pool

Remove dependency on oauth egg

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/testing/pages.py'
2--- lib/canonical/launchpad/testing/pages.py 2010-11-30 08:11:30 +0000
3+++ lib/canonical/launchpad/testing/pages.py 2011-02-04 00:32:51 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Testing infrastructure for page tests."""
10@@ -28,7 +28,7 @@
11 SoupStrainer,
12 Tag,
13 )
14-from contrib.oauth import (
15+from oauth.oauth import (
16 OAuthConsumer,
17 OAuthRequest,
18 OAuthSignatureMethod_PLAINTEXT,
19
20=== modified file 'lib/canonical/launchpad/webapp/authentication.py'
21--- lib/canonical/launchpad/webapp/authentication.py 2010-09-20 16:45:03 +0000
22+++ lib/canonical/launchpad/webapp/authentication.py 2011-02-04 00:32:51 +0000
23@@ -1,4 +1,4 @@
24-# Copyright 2009 Canonical Ltd. This software is licensed under the
25+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
26 # GNU Affero General Public License version 3 (see the file LICENSE).
27
28 __metaclass__ = type
29@@ -18,7 +18,8 @@
30 import random
31 from UserDict import UserDict
32
33-from contrib.oauth import OAuthRequest
34+from oauth.oauth import OAuthRequest
35+
36 from zope.annotation.interfaces import IAnnotations
37 from zope.app.security.interfaces import ILoginPassword
38 from zope.app.security.principalregistry import UnauthenticatedPrincipal
39
40=== modified file 'lib/canonical/launchpad/webapp/tests/test_authentication.py'
41--- lib/canonical/launchpad/webapp/tests/test_authentication.py 2010-12-20 03:28:21 +0000
42+++ lib/canonical/launchpad/webapp/tests/test_authentication.py 2011-02-04 00:32:51 +0000
43@@ -1,4 +1,4 @@
44-# Copyright 2009 Canonical Ltd. This software is licensed under the
45+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
46 # GNU Affero General Public License version 3 (see the file LICENSE).
47
48 """Tests authentication.py"""
49@@ -10,7 +10,7 @@
50
51 from zope.app.security.principalregistry import UnauthenticatedPrincipal
52
53-from contrib.oauth import OAuthRequest
54+from oauth.oauth import OAuthRequest
55
56 from canonical.config import config
57 from canonical.launchpad.ftests import login
58@@ -65,6 +65,9 @@
59 # This was really a bug in the underlying contrib/oauth.py module, but
60 # it has no standalone test case.
61 #
62+ # Now we use the separate oauth module (bug 701545), and this has been
63+ # fixed upstream, but we might as well keep the test.
64+ #
65 # Note that the 'realm' parameter is not returned, because it's not
66 # included in the OAuth calculations.
67 headers = OAuthRequest._split_header(
68
69=== modified file 'lib/canonical/launchpad/webapp/tests/test_publication.py'
70--- lib/canonical/launchpad/webapp/tests/test_publication.py 2010-10-04 19:50:45 +0000
71+++ lib/canonical/launchpad/webapp/tests/test_publication.py 2011-02-04 00:32:51 +0000
72@@ -1,4 +1,4 @@
73-# Copyright 2009 Canonical Ltd. This software is licensed under the
74+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
75 # GNU Affero General Public License version 3 (see the file LICENSE).
76
77 """Tests publication.py"""
78@@ -9,7 +9,7 @@
79 import sys
80 import unittest
81
82-from contrib.oauth import (
83+from oauth.oauth import (
84 OAuthRequest,
85 OAuthSignatureMethod_PLAINTEXT,
86 )
87
88=== removed file 'lib/contrib/oauth.py'
89--- lib/contrib/oauth.py 2010-12-20 03:53:24 +0000
90+++ lib/contrib/oauth.py 1970-01-01 00:00:00 +0000
91@@ -1,529 +0,0 @@
92-# pylint: disable-msg=C0301,E0602,E0211,E0213,W0105,W0231,W0702
93-
94-import cgi
95-import urllib
96-import time
97-import random
98-import urlparse
99-import hmac
100-import base64
101-
102-VERSION = '1.0' # Hi Blaine!
103-HTTP_METHOD = 'GET'
104-SIGNATURE_METHOD = 'PLAINTEXT'
105-
106-# Generic exception class
107-class OAuthError(RuntimeError):
108- def __init__(self, message='OAuth error occured'):
109- self.message = message
110-
111-# optional WWW-Authenticate header (401 error)
112-def build_authenticate_header(realm=''):
113- return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
114-
115-# url escape
116-def escape(s):
117- # escape '/' too
118- return urllib.quote(s, safe='~')
119-
120-# util function: current timestamp
121-# seconds since epoch (UTC)
122-def generate_timestamp():
123- return int(time.time())
124-
125-# util function: nonce
126-# pseudorandom number
127-def generate_nonce(length=8):
128- return ''.join(str(random.randint(0, 9)) for i in range(length))
129-
130-# OAuthConsumer is a data type that represents the identity of the Consumer
131-# via its shared secret with the Service Provider.
132-class OAuthConsumer(object):
133- key = None
134- secret = None
135-
136- def __init__(self, key, secret):
137- self.key = key
138- self.secret = secret
139-
140-# OAuthToken is a data type that represents an End User via either an access
141-# or request token.
142-class OAuthToken(object):
143- # access tokens and request tokens
144- key = None
145- secret = None
146-
147- '''
148- key = the token
149- secret = the token secret
150- '''
151- def __init__(self, key, secret):
152- self.key = key
153- self.secret = secret
154-
155- def to_string(self):
156- return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret})
157-
158- # return a token from something like:
159- # oauth_token_secret=digg&oauth_token=digg
160- @staticmethod
161- def from_string(s):
162- params = cgi.parse_qs(s, keep_blank_values=False)
163- key = params['oauth_token'][0]
164- secret = params['oauth_token_secret'][0]
165- return OAuthToken(key, secret)
166-
167- def __str__(self):
168- return self.to_string()
169-
170-# OAuthRequest represents the request and can be serialized
171-class OAuthRequest(object):
172- '''
173- OAuth parameters:
174- - oauth_consumer_key
175- - oauth_token
176- - oauth_signature_method
177- - oauth_signature
178- - oauth_timestamp
179- - oauth_nonce
180- - oauth_version
181- ... any additional parameters, as defined by the Service Provider.
182- '''
183- parameters = None # oauth parameters
184- http_method = HTTP_METHOD
185- http_url = None
186- version = VERSION
187-
188- def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
189- self.http_method = http_method
190- self.http_url = http_url
191- self.parameters = parameters or {}
192-
193- def set_parameter(self, parameter, value):
194- self.parameters[parameter] = value
195-
196- def get_parameter(self, parameter):
197- try:
198- return self.parameters[parameter]
199- except:
200- raise OAuthError('Parameter not found: %s' % parameter)
201-
202- def _get_timestamp_nonce(self):
203- return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce')
204-
205- # get any non-oauth parameters
206- def get_nonoauth_parameters(self):
207- parameters = {}
208- for k, v in self.parameters.iteritems():
209- # ignore oauth parameters
210- if k.find('oauth_') < 0:
211- parameters[k] = v
212- return parameters
213-
214- # serialize as a header for an HTTPAuth request
215- def to_header(self, realm=''):
216- auth_header = 'OAuth realm="%s"' % realm
217- # add the oauth parameters
218- if self.parameters:
219- for k, v in self.parameters.iteritems():
220- auth_header += ', %s="%s"' % (k, v)
221- return {'Authorization': auth_header}
222-
223- # serialize as post data for a POST request
224- def to_postdata(self):
225- return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems())
226-
227- # serialize as a url for a GET request
228- def to_url(self):
229- return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
230-
231- # return a string that consists of all the parameters that need to be signed
232- def get_normalized_parameters(self):
233- params = self.parameters
234- try:
235- # exclude the signature if it exists
236- del params['oauth_signature']
237- except:
238- pass
239- key_values = params.items()
240- # sort lexicographically, first after key, then after value
241- key_values.sort()
242- # combine key value pairs in string and escape
243- return '&'.join('%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values)
244-
245- # just uppercases the http method
246- def get_normalized_http_method(self):
247- return self.http_method.upper()
248-
249- # parses the url and rebuilds it to be scheme://host/path
250- def get_normalized_http_url(self):
251- parts = urlparse.urlparse(self.http_url)
252- url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path
253- return url_string
254-
255- # set the signature parameter to the result of build_signature
256- def sign_request(self, signature_method, consumer, token):
257- # set the signature method
258- self.set_parameter('oauth_signature_method', signature_method.get_name())
259- # set the signature
260- self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token))
261-
262- def build_signature(self, signature_method, consumer, token):
263- # call the build signature method within the signature method
264- return signature_method.build_signature(self, consumer, token)
265-
266- @staticmethod
267- def from_request(http_method, http_url, headers=None, postdata=None, parameters=None):
268-
269- # let the library user override things however they'd like, if they know
270- # which parameters to use then go for it, for example XMLRPC might want to
271- # do this
272- if parameters is not None:
273- return OAuthRequest(http_method, http_url, parameters)
274-
275- # from the headers
276- if headers is not None:
277- try:
278- auth_header = headers['Authorization']
279- # check that the authorization header is OAuth
280- auth_header.index('OAuth')
281- # get the parameters from the header
282- parameters = OAuthRequest._split_header(auth_header)
283- return OAuthRequest(http_method, http_url, parameters)
284- except:
285- raise OAuthError('Unable to parse OAuth parameters from Authorization header.')
286-
287- # from the parameter string (post body)
288- if http_method == 'POST' and postdata is not None:
289- parameters = OAuthRequest._split_url_string(postdata)
290-
291- # from the url string
292- elif http_method == 'GET':
293- param_str = urlparse.urlparse(http_url).query
294- parameters = OAuthRequest._split_url_string(param_str)
295-
296- if parameters:
297- return OAuthRequest(http_method, http_url, parameters)
298-
299- raise OAuthError('Missing all OAuth parameters. OAuth parameters must be in the headers, post body, or url.')
300-
301- @staticmethod
302- def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
303- if not parameters:
304- parameters = {}
305-
306- defaults = {
307- 'oauth_consumer_key': oauth_consumer.key,
308- 'oauth_timestamp': generate_timestamp(),
309- 'oauth_nonce': generate_nonce(),
310- 'oauth_version': OAuthRequest.version,
311- }
312-
313- defaults.update(parameters)
314- parameters = defaults
315-
316- if token:
317- parameters['oauth_token'] = token.key
318-
319- return OAuthRequest(http_method, http_url, parameters)
320-
321- @staticmethod
322- def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None):
323- if not parameters:
324- parameters = {}
325-
326- parameters['oauth_token'] = token.key
327-
328- if callback:
329- parameters['oauth_callback'] = escape(callback)
330-
331- return OAuthRequest(http_method, http_url, parameters)
332-
333- # util function: turn Authorization: header into parameters, has to do some unescaping
334- @staticmethod
335- def _split_header(header):
336- params = {}
337- header = header.lstrip()
338- if not header.startswith('OAuth '):
339- raise ValueError("not an OAuth header: %r" % header)
340- header = header[6:]
341- parts = header.split(',')
342- for param in parts:
343- # remove whitespace
344- param = param.strip()
345- # split key-value
346- param_parts = param.split('=', 1)
347- if param_parts[0] == 'realm':
348- # Realm header is not an OAuth parameter according to rfc5849
349- # section 3.4.1.3.1.
350- continue
351- # remove quotes and unescape the value
352- params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
353- return params
354-
355- # util function: turn url string into parameters, has to do some unescaping
356- @staticmethod
357- def _split_url_string(param_str):
358- parameters = cgi.parse_qs(param_str, keep_blank_values=False)
359- for k, v in parameters.iteritems():
360- parameters[k] = urllib.unquote(v[0])
361- return parameters
362-
363-# OAuthServer is a worker to check a requests validity against a data store
364-class OAuthServer(object):
365- timestamp_threshold = 300 # in seconds, five minutes
366- version = VERSION
367- signature_methods = None
368- data_store = None
369-
370- def __init__(self, data_store=None, signature_methods=None):
371- self.data_store = data_store
372- self.signature_methods = signature_methods or {}
373-
374- def set_data_store(self, oauth_data_store):
375- self.data_store = oauth_data_store
376-
377- def get_data_store(self):
378- return self.data_store
379-
380- def add_signature_method(self, signature_method):
381- self.signature_methods[signature_method.get_name()] = signature_method
382- return self.signature_methods
383-
384- # process a request_token request
385- # returns the request token on success
386- def fetch_request_token(self, oauth_request):
387- try:
388- # get the request token for authorization
389- token = self._get_token(oauth_request, 'request')
390- except:
391- # no token required for the initial token request
392- version = self._get_version(oauth_request)
393- consumer = self._get_consumer(oauth_request)
394- self._check_signature(oauth_request, consumer, None)
395- # fetch a new token
396- token = self.data_store.fetch_request_token(consumer)
397- return token
398-
399- # process an access_token request
400- # returns the access token on success
401- def fetch_access_token(self, oauth_request):
402- version = self._get_version(oauth_request)
403- consumer = self._get_consumer(oauth_request)
404- # get the request token
405- token = self._get_token(oauth_request, 'request')
406- self._check_signature(oauth_request, consumer, token)
407- new_token = self.data_store.fetch_access_token(consumer, token)
408- return new_token
409-
410- # verify an api call, checks all the parameters
411- def verify_request(self, oauth_request):
412- # -> consumer and token
413- version = self._get_version(oauth_request)
414- consumer = self._get_consumer(oauth_request)
415- # get the access token
416- token = self._get_token(oauth_request, 'access')
417- self._check_signature(oauth_request, consumer, token)
418- parameters = oauth_request.get_nonoauth_parameters()
419- return consumer, token, parameters
420-
421- # authorize a request token
422- def authorize_token(self, token, user):
423- return self.data_store.authorize_request_token(token, user)
424-
425- # get the callback url
426- def get_callback(self, oauth_request):
427- return oauth_request.get_parameter('oauth_callback')
428-
429- # optional support for the authenticate header
430- def build_authenticate_header(self, realm=''):
431- return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
432-
433- # verify the correct version request for this server
434- def _get_version(self, oauth_request):
435- try:
436- version = oauth_request.get_parameter('oauth_version')
437- except:
438- version = VERSION
439- if version and version != self.version:
440- raise OAuthError('OAuth version %s not supported' % str(version))
441- return version
442-
443- # figure out the signature with some defaults
444- def _get_signature_method(self, oauth_request):
445- try:
446- signature_method = oauth_request.get_parameter('oauth_signature_method')
447- except:
448- signature_method = SIGNATURE_METHOD
449- try:
450- # get the signature method object
451- signature_method = self.signature_methods[signature_method]
452- except:
453- signature_method_names = ', '.join(self.signature_methods.keys())
454- raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
455-
456- return signature_method
457-
458- def _get_consumer(self, oauth_request):
459- consumer_key = oauth_request.get_parameter('oauth_consumer_key')
460- if not consumer_key:
461- raise OAuthError('Invalid consumer key')
462- consumer = self.data_store.lookup_consumer(consumer_key)
463- if not consumer:
464- raise OAuthError('Invalid consumer')
465- return consumer
466-
467- # try to find the token for the provided request token key
468- def _get_token(self, oauth_request, token_type='access'):
469- token_field = oauth_request.get_parameter('oauth_token')
470- token = self.data_store.lookup_token(token_type, token_field)
471- if not token:
472- raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
473- return token
474-
475- def _check_signature(self, oauth_request, consumer, token):
476- timestamp, nonce = oauth_request._get_timestamp_nonce()
477- self._check_timestamp(timestamp)
478- self._check_nonce(consumer, token, nonce)
479- signature_method = self._get_signature_method(oauth_request)
480- try:
481- signature = oauth_request.get_parameter('oauth_signature')
482- except:
483- raise OAuthError('Missing signature')
484- # attempt to construct the same signature
485- built = signature_method.build_signature(oauth_request, consumer, token)
486- if signature != built:
487- key, base = signature_method.build_signature_base_string(oauth_request, consumer, token)
488- raise OAuthError('Signature does not match. Expected: %s Got: %s Expected signature base string: %s' % (built, signature, base))
489-
490- def _check_timestamp(self, timestamp):
491- # verify that timestamp is recentish
492- timestamp = int(timestamp)
493- now = int(time.time())
494- lapsed = now - timestamp
495- if lapsed > self.timestamp_threshold:
496- raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
497-
498- def _check_nonce(self, consumer, token, nonce):
499- # verify that the nonce is uniqueish
500- try:
501- self.data_store.lookup_nonce(consumer, token, nonce)
502- raise OAuthError('Nonce already used: %s' % str(nonce))
503- except:
504- pass
505-
506-# OAuthClient is a worker to attempt to execute a request
507-class OAuthClient(object):
508- consumer = None
509- token = None
510-
511- def __init__(self, oauth_consumer, oauth_token):
512- self.consumer = oauth_consumer
513- self.token = oauth_token
514-
515- def get_consumer(self):
516- return self.consumer
517-
518- def get_token(self):
519- return self.token
520-
521- def fetch_request_token(self, oauth_request):
522- # -> OAuthToken
523- raise NotImplementedError
524-
525- def fetch_access_token(self, oauth_request):
526- # -> OAuthToken
527- raise NotImplementedError
528-
529- def access_resource(self, oauth_request):
530- # -> some protected resource
531- raise NotImplementedError
532-
533-# OAuthDataStore is a database abstraction used to lookup consumers and tokens
534-class OAuthDataStore(object):
535-
536- def lookup_consumer(self, key):
537- # -> OAuthConsumer
538- raise NotImplementedError
539-
540- def lookup_token(self, oauth_consumer, token_type, token_token):
541- # -> OAuthToken
542- raise NotImplementedError
543-
544- def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp):
545- # -> OAuthToken
546- raise NotImplementedError
547-
548- def fetch_request_token(self, oauth_consumer):
549- # -> OAuthToken
550- raise NotImplementedError
551-
552- def fetch_access_token(self, oauth_consumer, oauth_token):
553- # -> OAuthToken
554- raise NotImplementedError
555-
556- def authorize_request_token(self, oauth_token, user):
557- # -> OAuthToken
558- raise NotImplementedError
559-
560-# OAuthSignatureMethod is a strategy class that implements a signature method
561-class OAuthSignatureMethod(object):
562- def get_name():
563- # -> str
564- raise NotImplementedError
565-
566- def build_signature_base_string(oauth_request, oauth_consumer, oauth_token):
567- # -> str key, str raw
568- raise NotImplementedError
569-
570- def build_signature(oauth_request, oauth_consumer, oauth_token):
571- # -> str
572- raise NotImplementedError
573-
574-class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
575-
576- def get_name(self):
577- return 'HMAC-SHA1'
578-
579- def build_signature_base_string(self, oauth_request, consumer, token):
580- sig = (
581- escape(oauth_request.get_normalized_http_method()),
582- escape(oauth_request.get_normalized_http_url()),
583- escape(oauth_request.get_normalized_parameters()),
584- )
585-
586- key = '%s&' % escape(consumer.secret)
587- if token:
588- key += escape(token.secret)
589- raw = '&'.join(sig)
590- return key, raw
591-
592- def build_signature(self, oauth_request, consumer, token):
593- # build the base signature string
594- key, raw = self.build_signature_base_string(oauth_request, consumer, token)
595-
596- # hmac object
597- try:
598- import hashlib # 2.5
599- hashed = hmac.new(key, raw, hashlib.sha1)
600- except:
601- import sha # deprecated
602- hashed = hmac.new(key, raw, sha)
603-
604- # calculate the digest base 64
605- return base64.b64encode(hashed.digest())
606-
607-class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
608-
609- def get_name(self):
610- return 'PLAINTEXT'
611-
612- def build_signature_base_string(self, oauth_request, consumer, token):
613- # concatenate the consumer key and secret
614- sig = escape(consumer.secret)
615- if token:
616- sig = '&'.join((sig, escape(token.secret)))
617- return sig
618-
619- def build_signature(self, oauth_request, consumer, token):
620- return self.build_signature_base_string(oauth_request, consumer, token)