Merge lp:~thisfred/ubuntuone-couch/add-tests into lp:~thisfred/ubuntuone-couch/trunk
- add-tests
- Merge into trunk
Proposed by
Eric Casteleijn
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Eric Casteleijn | ||||||||
Approved revision: | 14 | ||||||||
Merged at revision: | 2 | ||||||||
Proposed branch: | lp:~thisfred/ubuntuone-couch/add-tests | ||||||||
Merge into: | lp:~thisfred/ubuntuone-couch/trunk | ||||||||
Diff against target: |
614 lines (+391/-181) 3 files modified
bin/u1couch-query (+32/-181) tests/test_u1couchquery.py (+164/-0) u1couch/query.py (+195/-0) |
||||||||
To merge this branch: | bzr merge lp:~thisfred/ubuntuone-couch/add-tests | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Natalia Bidart (community) | Approve | ||
Alejandro J. Cura (community) | Approve | ||
Eric Casteleijn | Pending | ||
Review via email: mp+50206@code.launchpad.net |
Commit message
Fixed and refactored u1couch-query and added tests
Description of the change
Fixed and refactored u1couch-query and added tests
To post a comment you must log in.
- 14. By Eric Casteleijn
-
unchanged: attach bug
Revision history for this message
Natalia Bidart (nataliabidart) wrote : | # |
Added bug #720928 to use u1 specific login service.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === renamed file 'bin/ubuntuone-couchdb-query' => 'bin/u1couch-query' (properties changed: -x to +x) |
2 | --- bin/ubuntuone-couchdb-query 2011-02-14 20:21:08 +0000 |
3 | +++ bin/u1couch-query 2011-02-17 19:10:44 +0000 |
4 | @@ -1,204 +1,55 @@ |
5 | #!/usr/bin/python |
6 | + |
7 | +# Copyright 2011 Canonical Ltd. |
8 | +# |
9 | +# Ubuntu One Couch is free software: you can redistribute it and/or |
10 | +# modify it under the terms of the GNU Lesser General Public License |
11 | +# version 3 as published by the Free Software Foundation. |
12 | +# |
13 | +# Ubuntu One Couch is distributed in the hope that it will be useful, |
14 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
16 | +# Lesser General Public License for more details. |
17 | +# |
18 | +# You should have received a copy of the GNU Lesser General Public |
19 | +# License along with desktopcouch. If not, see |
20 | +# <http://www.gnu.org/licenses/>. |
21 | + |
22 | """ubuntuone-couchdb-query: Command line query tool for Ubuntu One CouchDBs""" |
23 | |
24 | -# sil@kryogenix.org, 2010-02-13 |
25 | |
26 | from optparse import OptionParser |
27 | -from oauth import oauth |
28 | -import gnomekeyring, gobject, httplib2, simplejson, urlparse, cgi, urllib |
29 | - |
30 | -try: |
31 | - from ubuntu_sso.main import SSOCredentials |
32 | -except ImportError: |
33 | - SSOCredentials = None |
34 | |
35 | import socket |
36 | +from u1couch import query |
37 | + |
38 | socket.setdefaulttimeout(5) |
39 | |
40 | -def get_prod_oauth_token(): |
41 | - """Get the token from the keyring""" |
42 | - gobject.set_application_name("Ubuntu One Web API Tool") |
43 | - |
44 | - consumer = oauth.OAuthConsumer("ubuntuone", "hammertime") |
45 | - items = [] |
46 | - items = gnomekeyring.find_items_sync( |
47 | - gnomekeyring.ITEM_GENERIC_SECRET, |
48 | - {'ubuntuone-realm': "https://ubuntuone.com", |
49 | - 'oauth-consumer-key': consumer.key}) |
50 | - return oauth.OAuthToken.from_string(items[0].secret) |
51 | - |
52 | -def get_prod_oauth_token(explicit_token_store=None): |
53 | - """Get the token from the keyring""" |
54 | - gobject.set_application_name("Ubuntu One Web API Tool") |
55 | - if (SSOCredentials is not None) and (explicit_token_store in ["sso", None]): |
56 | - creds = SSOCredentials('Ubuntu One') |
57 | - info = creds.find_credentials('Ubuntu One') |
58 | - consumer = oauth.OAuthConsumer(info['consumer_key'], |
59 | - info['consumer_secret']) |
60 | - secret='oauth_token=%s&oauth_token_secret=%s' % (info['token'], |
61 | - info['token_secret']) |
62 | - elif explicit_token_store in ["hammertime", None]: |
63 | - consumer = oauth.OAuthConsumer("ubuntuone", "hammertime") |
64 | - items = [] |
65 | - items = gnomekeyring.find_items_sync( |
66 | - gnomekeyring.ITEM_GENERIC_SECRET, |
67 | - {'ubuntuone-realm': "https://ubuntuone.com", |
68 | - 'oauth-consumer-key': consumer.key}) |
69 | - secret = items[0].secret |
70 | - else: |
71 | - raise Exception("Wasn't able to get a token") |
72 | - |
73 | - return (oauth.OAuthToken.from_string(secret), consumer) |
74 | - |
75 | -def get_oauth_request_header(consumer, access_token, http_url, signature_method): |
76 | - """Get an oauth request header given the token and the url""" |
77 | - assert http_url.startswith("https") |
78 | - oauth_request = oauth.OAuthRequest.from_consumer_and_token( |
79 | - http_url=http_url, |
80 | - http_method="GET", |
81 | - oauth_consumer=consumer, |
82 | - token=access_token) |
83 | - oauth_request.sign_request(signature_method, consumer, access_token) |
84 | - return oauth_request.to_header() |
85 | - |
86 | -def request(urlpath, sigmeth, http_method, request_body, show_tokens, |
87 | - server_override=None, explicit_token_store=None): |
88 | - """Make a request to couchdb.one.ubuntu.com for the user's data. |
89 | - |
90 | - The user supplies a urlpath (for example, dbname). We need to actually |
91 | - request https://couchdb.one.ubuntu.com/PREFIX/dbname, and sign it with |
92 | - the user's OAuth token, which must be in the keyring. |
93 | - |
94 | - We find the prefix by querying https://one.ubuntu.com/api/account/ |
95 | - (see desktopcouch.replication_services.ubuntuone, which does this). |
96 | - """ |
97 | - |
98 | - # First check that there's a token in the keyring. |
99 | - try: |
100 | - (access_token, consumer) = get_prod_oauth_token(explicit_token_store) |
101 | - except: # bare except, norty |
102 | - raise Exception("Unable to retrieve your Ubuntu One access details " |
103 | - "from the keyring. Try connecting your machine to Ubuntu One.") |
104 | - |
105 | - # Set the signature method. This should be HMAC unless you have a jolly |
106 | - # good reason for it to not be. |
107 | - if sigmeth == "PLAINTEXT": |
108 | - signature_method = oauth.OAuthSignatureMethod_PLAINTEXT() |
109 | - elif sigmeth == "HMAC_SHA1": |
110 | - signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() |
111 | - else: |
112 | - signature_method = oauth.OAuthSignatureMethod_PLAINTEXT() |
113 | - |
114 | - if show_tokens: |
115 | - print "Using OAuth data:" |
116 | - print "consumer: %s : %s\ntoken: %s" % ( |
117 | - consumer.key, consumer.secret, access_token) |
118 | - |
119 | - # Look up the user's prefix |
120 | - infourl = "https://one.ubuntu.com/api/account/" |
121 | - oauth_header = get_oauth_request_header(consumer, access_token, infourl, signature_method) |
122 | - client = httplib2.Http() |
123 | - resp, content = client.request(infourl, "GET", headers=oauth_header) |
124 | - if resp['status'] == "200": |
125 | - try: |
126 | - document = simplejson.loads(content) |
127 | - except ValueError: |
128 | - raise Exception("Got unexpected content:\n%s" % content) |
129 | - if "couchdb_root" not in document: |
130 | - raise ValueError("couchdb_root not found in %s" % (document,)) |
131 | - if "id" not in document: |
132 | - raise ValueError("id not found in %s" % (document,)) |
133 | - COUCH_ROOT = document["couchdb_root"] |
134 | - USERID = document["id"] |
135 | - else: |
136 | - raise ValueError("Error retrieving user data (%s)" % (resp['status'], |
137 | - url)) |
138 | - |
139 | - # COUCH_ROOT must have all internal slashes escaped |
140 | - schema, netloc, path, params, query, fragment = urlparse.urlparse(COUCH_ROOT) |
141 | - if server_override: |
142 | - netloc = server_override |
143 | - path = "/" + urllib.quote(path[1:], safe="") # don't escape the first / |
144 | - COUCH_ROOT = urlparse.urlunparse((schema, netloc, path, params, query, fragment)) |
145 | - |
146 | - # Now use COUCH_ROOT and the specified user urlpath to get data |
147 | - if urlpath == "_all_dbs": |
148 | - couch_url = "%s%s" % (COUCH_ROOT, urlpath) |
149 | - couch_url = urlparse.urlunparse((schema, netloc, "_all_dbs", None, "user_id=%s" % USERID, None)) |
150 | - else: |
151 | - couch_url = "%s%%2F%s" % (COUCH_ROOT, urlpath) |
152 | - schema, netloc, path, params, query, fragment = urlparse.urlparse(couch_url) |
153 | - querystr_as_dict = dict(cgi.parse_qsl(query)) |
154 | - oauth_request = oauth.OAuthRequest.from_consumer_and_token( |
155 | - http_url=couch_url, |
156 | - http_method=http_method, |
157 | - oauth_consumer=consumer, |
158 | - token=access_token, |
159 | - parameters=querystr_as_dict) |
160 | - oauth_request.sign_request(signature_method, consumer, access_token) |
161 | - failed = 0 |
162 | - #print "Connecting to effective Couch URL", oauth_request.to_url() |
163 | - #print "Connecting to actual Couch URL (with OAuth header)", couch_url |
164 | - #print oauth_request.to_header(), querystr_as_dict |
165 | - while 1: |
166 | - try: |
167 | - resp, content = client.request(couch_url, http_method, |
168 | - headers=oauth_request.to_header(), body=request_body) |
169 | - break |
170 | - except IOError: |
171 | - failed += 1 |
172 | - if failed > 0: |
173 | - if failed == 1: |
174 | - s = "" |
175 | - else: |
176 | - s = "s" |
177 | - print "(request failed %s time%s)" % (failed, s) |
178 | - if resp['status'] == "200": |
179 | - try: |
180 | - return simplejson.loads(content) |
181 | - except: |
182 | - print "(data returned from CouchDB was invalid JSON)" |
183 | - return content |
184 | - elif resp['status'] == "400": |
185 | - print "The server could not parse the oauth token:\n%s" % content |
186 | - elif resp['status'] == "401": |
187 | - print "Access Denied" |
188 | - print "Content:" |
189 | - return content |
190 | - else: |
191 | - return ( |
192 | - "There was a problem processing the request:\nstatus:%s, response:" |
193 | - " %r" % (resp['status'], content)) |
194 | - |
195 | |
196 | if __name__ == "__main__": |
197 | - parser = OptionParser(usage="prog [options] urlpath") |
198 | - parser.add_option("--oauth-signature-method", dest="sigmeth", |
199 | + PARSER = OptionParser(usage="prog [options] urlpath") |
200 | + PARSER.add_option("--oauth-signature-method", dest="sigmeth", |
201 | default="HMAC_SHA1", |
202 | help="OAuth signature method to use (PLAINTEXT or " |
203 | "HMAC_SHA1)") |
204 | - parser.add_option("--http-method", dest="http_method", |
205 | + PARSER.add_option("--http-method", dest="http_method", |
206 | default="GET", |
207 | help="HTTP method to use") |
208 | - parser.add_option("--body", dest="body", |
209 | + PARSER.add_option("--body", dest="body", |
210 | default=None, |
211 | help="HTTP request body") |
212 | - parser.add_option("--show-tokens", dest="show_tokens", |
213 | + PARSER.add_option("--show-tokens", dest="show_tokens", |
214 | default=False, |
215 | help="Show the OAuth tokens we're using") |
216 | - parser.add_option("--server-override", dest="server_override", |
217 | + PARSER.add_option("--server-override", dest="server_override", |
218 | default=None, |
219 | help="Use a different server") |
220 | - parser.add_option("--explicit-token-store", dest="explicit_token_store", |
221 | - default=None, |
222 | - help="Explicitly choose a token store (sso or hammertime)") |
223 | |
224 | - (options, args) = parser.parse_args() |
225 | - if len(args) != 1: |
226 | - parser.error("You must specify a urlpath (e.g., a dbname)") |
227 | - print request( |
228 | - urlpath=args[0], sigmeth=options.sigmeth, |
229 | - http_method=options.http_method, request_body=options.body, |
230 | - show_tokens=options.show_tokens, |
231 | - server_override=options.server_override, |
232 | - explicit_token_store=options.explicit_token_store) |
233 | + (OPTIONS, ARGS) = PARSER.parse_args() |
234 | + if len(ARGS) != 1: |
235 | + PARSER.error("You must specify a urlpath (e.g., a dbname)") |
236 | + print query.request( |
237 | + urlpath=ARGS[0], sig_meth=OPTIONS.sigmeth, |
238 | + http_method=OPTIONS.http_method, request_body=OPTIONS.body, |
239 | + show_tokens=OPTIONS.show_tokens, |
240 | + server_override=OPTIONS.server_override) |
241 | |
242 | === renamed file 'bin/ubuntuone-sign-uri' => 'bin/ubuntuone-sign-uri.py' (properties changed: -x to +x) |
243 | === added directory 'tests' |
244 | === added file 'tests/__init__.py' |
245 | === added file 'tests/test_u1couchquery.py' |
246 | --- tests/test_u1couchquery.py 1970-01-01 00:00:00 +0000 |
247 | +++ tests/test_u1couchquery.py 2011-02-17 19:10:44 +0000 |
248 | @@ -0,0 +1,164 @@ |
249 | +# Copyright 2011 Canonical Ltd. |
250 | +# |
251 | +# Ubuntu One Couch is free software: you can redistribute it and/or |
252 | +# modify it under the terms of the GNU Lesser General Public License |
253 | +# version 3 as published by the Free Software Foundation. |
254 | +# |
255 | +# Ubuntu One Couch is distributed in the hope that it will be useful, |
256 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
257 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
258 | +# Lesser General Public License for more details. |
259 | +# |
260 | +# You should have received a copy of the GNU Lesser General Public |
261 | +# License along with desktopcouch. If not, see |
262 | +# <http://www.gnu.org/licenses/>. |
263 | + |
264 | +"""Tests for u1couch.query.""" |
265 | + |
266 | +from twisted.trial.unittest import TestCase |
267 | +from mocker import Mocker, ANY |
268 | + |
269 | +import ubuntu_sso |
270 | +import json |
271 | +from oauth import oauth |
272 | +from u1couch import query |
273 | + |
274 | +CONSUMER_KEY = u'this_consumer_key' |
275 | +CONSUMER_SECRET = u'sssssh!' |
276 | +TOKEN_KEY = u'tokentokentoken' |
277 | +TOKEN_SECRET = u'ssssssshhhhhh!' |
278 | + |
279 | +CONSUMER = oauth.OAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET) |
280 | +TOKEN = oauth.OAuthToken(TOKEN_KEY, TOKEN_SECRET) |
281 | + |
282 | +URL = 'https://example.com' |
283 | + |
284 | + |
285 | +class QueryTestCase(TestCase): |
286 | + """Test case for u1couch.query.""" |
287 | + |
288 | + def setUp(self): |
289 | + self.mocker = Mocker() |
290 | + |
291 | + def tearDown(self): |
292 | + self.mocker.restore() |
293 | + self.mocker.verify() |
294 | + |
295 | + def test_get_oauth_request_header(self): |
296 | + """Test get_oauth_request_header returns correct headers.""" |
297 | + fake_headers = {'Authorization': |
298 | + 'OAuth realm="", oauth_nonce="39941541", ' |
299 | + 'oauth_timestamp="1297958903", ' |
300 | + 'oauth_consumer_key="this_consumer_key", ' |
301 | + 'oauth_signature_method="HMAC-SHA1", oauth_version="1.0", ' |
302 | + 'oauth_token="tokentokentoken", ' |
303 | + 'oauth_signature="TNIfersCweWluuuJW%2FT%2FbW9IHD0%3D"'} |
304 | + mock_oauth = self.mocker.replace("oauth.oauth") |
305 | + mock_oauth.OAuthRequest # pylint: disable=W0104 |
306 | + MockOAuthRequest = self.mocker.mock() # pylint: disable=C0103 |
307 | + self.mocker.result(MockOAuthRequest) |
308 | + MockOAuthRequest.from_consumer_and_token( |
309 | + http_url=URL, http_method='GET', oauth_consumer=CONSUMER, |
310 | + token=TOKEN) |
311 | + oauth_request = self.mocker.mock() |
312 | + self.mocker.result(oauth_request) |
313 | + oauth_request.sign_request(query.HMAC_SHA1, CONSUMER, TOKEN) |
314 | + oauth_request.to_header() |
315 | + self.mocker.result(fake_headers) |
316 | + self.mocker.replay() |
317 | + headers = query.get_oauth_request_header(CONSUMER, TOKEN, URL) |
318 | + self.assertEquals(fake_headers, headers) |
319 | + |
320 | + def test_get_oauth_request_header_http(self): |
321 | + """Test get_oauth_request_header fails on http urls.""" |
322 | + self.assertRaises( |
323 | + AssertionError, query.get_oauth_request_header, CONSUMER, TOKEN, |
324 | + 'http://example.com') |
325 | + |
326 | + def test_get_oauth_data(self): |
327 | + """Test get_oauth_data returns proper oauth data.""" |
328 | + self.patch(query, "undbusify", lambda x: x) |
329 | + dbus = self.mocker.replace("dbus") |
330 | + self.mocker.replace("dbus.mainloop.glib.DBusGMainLoop") |
331 | + bus = dbus.SessionBus() |
332 | + bus.get_object( |
333 | + ubuntu_sso.DBUS_BUS_NAME, ubuntu_sso.DBUS_CRED_PATH, |
334 | + follow_name_owner_changes=True) |
335 | + mock_proxy = self.mocker.mock() |
336 | + self.mocker.result(mock_proxy) |
337 | + mock_proxy.find_credentials(query.APP_NAME) |
338 | + self.mocker.result({ |
339 | + u'token': TOKEN_KEY, |
340 | + u'token_secret': TOKEN_SECRET, |
341 | + u'consumer_secret': CONSUMER_SECRET, |
342 | + u'consumer_key': CONSUMER_KEY}) |
343 | + self.mocker.replay() |
344 | + oauth_data = query.get_oauth_data() |
345 | + self.assertEquals(CONSUMER_KEY, oauth_data['consumer_key']) |
346 | + self.assertEquals(CONSUMER_SECRET, oauth_data['consumer_secret']) |
347 | + self.assertEquals(TOKEN_KEY, oauth_data['token']) |
348 | + self.assertEquals(TOKEN_SECRET, oauth_data['token_secret']) |
349 | + |
350 | + def test_get_prod_oauth_token(self): |
351 | + """Test get_prod_oauth_token returns token and consumer.""" |
352 | + info = { |
353 | + u'token': TOKEN_KEY, |
354 | + u'token_secret': TOKEN_SECRET, |
355 | + u'consumer_secret': CONSUMER_SECRET, |
356 | + u'consumer_key': CONSUMER_KEY} |
357 | + token, consumer = query.get_prod_oauth_token(info) |
358 | + self.assertEquals(TOKEN_KEY, token.key) |
359 | + self.assertEquals(TOKEN_SECRET, token.secret) |
360 | + self.assertEquals(CONSUMER_KEY, consumer.key) |
361 | + self.assertEquals(CONSUMER_SECRET, consumer.secret) |
362 | + |
363 | + def test_get_user_info(self): |
364 | + """Test get_user_info parses json correctly.""" |
365 | + mock_header = self.mocker.mock() |
366 | + Http = self.mocker.replace("httplib2.Http") # pylint: disable=C0103 |
367 | + http = Http() |
368 | + http.request(URL, "GET", headers=mock_header) |
369 | + self.mocker.result(( |
370 | + {'status': '200'}, |
371 | + '{"couchdb_root": "https://couchdb.one.ubuntu.com/u/abc/def/1337",' |
372 | + ' "id": 1337}')) |
373 | + self.mocker.replay() |
374 | + user_id, root = query.get_user_info(URL, mock_header) |
375 | + self.assertEquals(1337, user_id) |
376 | + self.assertEquals( |
377 | + "https://couchdb.one.ubuntu.com/u/abc/def/1337", root) |
378 | + |
379 | + def test_request(self): |
380 | + """Test a full request.""" |
381 | + fake_result = { |
382 | + 'committed_update_seq': 654, |
383 | + 'compact_running': False, |
384 | + 'db_name': 'u/abc/def/1337/contacts', |
385 | + 'disk_format_version': 5, |
386 | + 'disk_size': 929892, |
387 | + 'doc_count': 626, |
388 | + 'doc_del_count': 1, |
389 | + 'instance_start_time': '1297965776474824', |
390 | + 'purge_seq': 0, |
391 | + 'update_seq': 654} |
392 | + fake_json = json.dumps(fake_result) |
393 | + get_oauth_data = self.mocker.replace('u1couch.query.get_oauth_data') |
394 | + get_oauth_data() |
395 | + self.mocker.result({ |
396 | + u'token': TOKEN_KEY, |
397 | + u'token_secret': TOKEN_SECRET, |
398 | + u'consumer_secret': CONSUMER_SECRET, |
399 | + u'consumer_key': CONSUMER_KEY}) |
400 | + get_user_info = self.mocker.replace('u1couch.query.get_user_info') |
401 | + get_user_info('https://one.ubuntu.com/api/account/', ANY) |
402 | + self.mocker.result(( |
403 | + 1337, "https://couchdb.one.ubuntu.com/u/abc/def/1337")) |
404 | + Http = self.mocker.replace("httplib2.Http") # pylint: disable=C0103 |
405 | + http = Http() |
406 | + http.request( |
407 | + 'https://couchdb.one.ubuntu.com/u%2Fabc%2Fdef%2F1337%2Fcontacts', |
408 | + 'GET', headers=ANY, body=None) |
409 | + self.mocker.result(({'status': '200'}, fake_json)) |
410 | + self.mocker.replay() |
411 | + result = query.request('contacts') |
412 | + self.assertEquals(fake_result, result) |
413 | |
414 | === added directory 'u1couch' |
415 | === added file 'u1couch/__init__.py' |
416 | === added file 'u1couch/query.py' |
417 | --- u1couch/query.py 1970-01-01 00:00:00 +0000 |
418 | +++ u1couch/query.py 2011-02-17 19:10:44 +0000 |
419 | @@ -0,0 +1,195 @@ |
420 | +# Copyright 2011 Canonical Ltd. |
421 | +# |
422 | +# Ubuntu One Couch is free software: you can redistribute it and/or |
423 | +# modify it under the terms of the GNU Lesser General Public License |
424 | +# version 3 as published by the Free Software Foundation. |
425 | +# |
426 | +# Ubuntu One Couch is distributed in the hope that it will be useful, |
427 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
428 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
429 | +# Lesser General Public License for more details. |
430 | +# |
431 | +# You should have received a copy of the GNU Lesser General Public |
432 | +# License along with desktopcouch. If not, see |
433 | +# <http://www.gnu.org/licenses/>. |
434 | + |
435 | +"""Small library for talking to Ubuntu One CouchDBs.""" |
436 | + |
437 | +import cgi |
438 | +import dbus |
439 | +from dbus.mainloop.glib import DBusGMainLoop |
440 | +import logging |
441 | +import httplib2 |
442 | +import json |
443 | +import urllib |
444 | +import urlparse |
445 | +from time import sleep |
446 | +from oauth import oauth |
447 | + |
448 | +import ubuntu_sso |
449 | + |
450 | +APP_NAME = "Ubuntu One" |
451 | +HMAC_SHA1 = oauth.OAuthSignatureMethod_HMAC_SHA1() |
452 | + |
453 | + |
454 | +def undbusify(value): |
455 | + """Convert dbus types back to native types.""" |
456 | + for singleton in (None, True, False): |
457 | + if value == singleton: |
458 | + return singleton |
459 | + for val_type in (long, int, float, complex, |
460 | + unicode, str, |
461 | + list, tuple, dict, set): |
462 | + if isinstance(value, val_type): |
463 | + return val_type(value) |
464 | + raise TypeError(value) |
465 | + |
466 | + |
467 | +def get_oauth_data(): |
468 | + """Information needed to replicate to a server.""" |
469 | + |
470 | + DBusGMainLoop(set_as_default=True) |
471 | + |
472 | + bus = dbus.SessionBus() |
473 | + proxy = bus.get_object( |
474 | + ubuntu_sso.DBUS_BUS_NAME, ubuntu_sso.DBUS_CRED_PATH, |
475 | + follow_name_owner_changes=True) |
476 | + logging.info( |
477 | + 'get_oauth_data: asking for credentials to Ubuntu SSO. App name: %s', |
478 | + APP_NAME) |
479 | + oauth_data = dict( |
480 | + (undbusify(k), undbusify(v)) for k, v in |
481 | + proxy.find_credentials(APP_NAME).iteritems()) |
482 | + logging.info( |
483 | + 'Got credentials from Ubuntu SSO. Non emtpy credentials? %s', |
484 | + len(oauth_data) > 0) |
485 | + return oauth_data |
486 | + |
487 | + |
488 | +def get_prod_oauth_token(info): |
489 | + """Get the token from the keyring.""" |
490 | + consumer = oauth.OAuthConsumer( |
491 | + info['consumer_key'], info['consumer_secret']) |
492 | + secret = 'oauth_token=%s&oauth_token_secret=%s' % ( |
493 | + info['token'], info['token_secret']) |
494 | + |
495 | + return (oauth.OAuthToken.from_string(secret), consumer) |
496 | + |
497 | + |
498 | +def get_oauth_request_header(consumer, access_token, http_url, |
499 | + signature_method=HMAC_SHA1): |
500 | + """Get an oauth request header given the token and the url.""" |
501 | + assert http_url.startswith("https") |
502 | + oauth_request = oauth.OAuthRequest.from_consumer_and_token( |
503 | + http_url=http_url, |
504 | + http_method="GET", |
505 | + oauth_consumer=consumer, |
506 | + token=access_token) |
507 | + oauth_request.sign_request(signature_method, consumer, access_token) |
508 | + return oauth_request.to_header() |
509 | + |
510 | + |
511 | +def get_user_info(info_url, oauth_header): |
512 | + """Look up the user's user id and prefix.""" |
513 | + http = httplib2.Http() |
514 | + resp, content = http.request(info_url, "GET", headers=oauth_header) |
515 | + if resp['status'] not in ("200", "201"): |
516 | + raise ValueError( |
517 | + "Error retrieving user data (%s, %s)" % resp['status']) |
518 | + try: |
519 | + document = json.loads(content) |
520 | + except ValueError: |
521 | + raise Exception("Got unexpected content:\n%s" % content) |
522 | + if "couchdb_root" not in document: |
523 | + raise ValueError("couchdb_root not found in %s" % (document,)) |
524 | + if "id" not in document: |
525 | + raise ValueError("id not found in %s" % (document,)) |
526 | + return document["id"], document["couchdb_root"] |
527 | + |
528 | + |
529 | +def request(urlpath, sig_meth='HMAC_SHA1', http_method='GET', |
530 | + request_body=None, show_tokens=False, server_override=None, |
531 | + access_token=None, consumer=None): |
532 | + """Make a request to couchdb.one.ubuntu.com for the user's data. |
533 | + |
534 | + The user supplies a urlpath (for example, dbname). We need to actually |
535 | + request https://couchdb.one.ubuntu.com/PREFIX/dbname, and sign it with |
536 | + the user's OAuth token. |
537 | + |
538 | + We find the prefix by querying https://one.ubuntu.com/api/account/ |
539 | + (see desktopcouch.replication_services.ubuntuone, which does this). |
540 | + |
541 | + """ |
542 | + if access_token is None: |
543 | + info = get_oauth_data() |
544 | + (access_token, consumer) = get_prod_oauth_token(info) |
545 | + # Set the signature method. This should be HMAC unless you have a jolly |
546 | + # good reason for it to not be. |
547 | + if sig_meth == "PLAINTEXT": |
548 | + signature_method = oauth.OAuthSignatureMethod_PLAINTEXT() |
549 | + else: |
550 | + signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1() |
551 | + if show_tokens: |
552 | + print "Using OAuth data:" |
553 | + print "consumer: %s : %s\ntoken: %s" % ( |
554 | + consumer.key, consumer.secret, access_token) |
555 | + |
556 | + info_url = "https://one.ubuntu.com/api/account/" |
557 | + oauth_header = get_oauth_request_header( |
558 | + consumer, access_token, info_url, signature_method) |
559 | + |
560 | + userid, couch_root = get_user_info(info_url, oauth_header) |
561 | + schema, netloc, path, params, query, fragment = urlparse.urlparse( |
562 | + couch_root) |
563 | + if server_override: |
564 | + netloc = server_override |
565 | + # Don't escape the first / |
566 | + path = "/" + urllib.quote(path[1:], safe="") |
567 | + couch_root = urlparse.urlunparse(( |
568 | + schema, netloc, path, params, query, fragment)) |
569 | + |
570 | + # Now use COUCH_ROOT and the specified user urlpath to get data |
571 | + if urlpath == "_all_dbs": |
572 | + couch_url = "%s%s" % (couch_root, urlpath) |
573 | + couch_url = urlparse.urlunparse(( |
574 | + schema, netloc, "_all_dbs", None, "user_id=%s" % userid, None)) |
575 | + else: |
576 | + couch_url = "%s%%2F%s" % (couch_root, urlpath) |
577 | + schema, netloc, path, params, query, fragment = urlparse.urlparse( |
578 | + couch_url) |
579 | + querystr_as_dict = dict(cgi.parse_qsl(query)) |
580 | + oauth_request = oauth.OAuthRequest.from_consumer_and_token( |
581 | + http_url=couch_url, |
582 | + http_method=http_method, |
583 | + oauth_consumer=consumer, |
584 | + token=access_token, |
585 | + parameters=querystr_as_dict) |
586 | + oauth_request.sign_request(signature_method, consumer, access_token) |
587 | + failed = 0 |
588 | + while True: |
589 | + try: |
590 | + http = httplib2.Http() |
591 | + print couch_url, http_method |
592 | + resp, content = http.request(couch_url, http_method, |
593 | + headers=oauth_request.to_header(), body=request_body) |
594 | + break |
595 | + except IOError: |
596 | + sleep(2 ** failed) # 1, 2, 4, 8, 16 |
597 | + failed += 1 |
598 | + if failed > 5: |
599 | + break |
600 | + if failed: |
601 | + print "(request failed %s time%s)" % ( |
602 | + failed, "s" if failed > 1 else "") |
603 | + if resp['status'] in ("200", "201"): |
604 | + return json.loads(content) |
605 | + elif resp['status'] == "400": |
606 | + print "The server could not parse the oauth token:\n%s" % content |
607 | + elif resp['status'] == "401": |
608 | + print "Access Denied" |
609 | + print "Content:" |
610 | + return content |
611 | + else: |
612 | + return ( |
613 | + "There was a problem processing the request:\nstatus:%s, response:" |
614 | + " %r" % (resp['status'], content)) |
Great to see this being tested and tidied up. Good work!
I've added bug #720917 so the request method returns only json or Exceptions, and thisfred agreed to work on that on a subsequent branch.