3
# Copyright 2011 Canonical Ltd.
5
# Ubuntu One Couch is free software: you can redistribute it and/or
6
# modify it under the terms of the GNU Lesser General Public License
7
# version 3 as published by the Free Software Foundation.
9
# Ubuntu One Couch is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
# Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public
15
# License along with desktopcouch. If not, see
16
# <http://www.gnu.org/licenses/>.
2
18
"""ubuntuone-couchdb-query: Command line query tool for Ubuntu One CouchDBs"""
4
# sil@kryogenix.org, 2010-02-13
6
21
from optparse import OptionParser
7
from oauth import oauth
8
import gnomekeyring, gobject, httplib2, simplejson, urlparse, cgi, urllib
11
from ubuntu_sso.main import SSOCredentials
24
from u1couch import query
16
26
socket.setdefaulttimeout(5)
18
def get_prod_oauth_token():
19
"""Get the token from the keyring"""
20
gobject.set_application_name("Ubuntu One Web API Tool")
22
consumer = oauth.OAuthConsumer("ubuntuone", "hammertime")
24
items = gnomekeyring.find_items_sync(
25
gnomekeyring.ITEM_GENERIC_SECRET,
26
{'ubuntuone-realm': "https://ubuntuone.com",
27
'oauth-consumer-key': consumer.key})
28
return oauth.OAuthToken.from_string(items[0].secret)
30
def get_prod_oauth_token(explicit_token_store=None):
31
"""Get the token from the keyring"""
32
gobject.set_application_name("Ubuntu One Web API Tool")
33
if (SSOCredentials is not None) and (explicit_token_store in ["sso", None]):
34
creds = SSOCredentials('Ubuntu One')
35
info = creds.find_credentials('Ubuntu One')
36
consumer = oauth.OAuthConsumer(info['consumer_key'],
37
info['consumer_secret'])
38
secret='oauth_token=%s&oauth_token_secret=%s' % (info['token'],
40
elif explicit_token_store in ["hammertime", None]:
41
consumer = oauth.OAuthConsumer("ubuntuone", "hammertime")
43
items = gnomekeyring.find_items_sync(
44
gnomekeyring.ITEM_GENERIC_SECRET,
45
{'ubuntuone-realm': "https://ubuntuone.com",
46
'oauth-consumer-key': consumer.key})
47
secret = items[0].secret
49
raise Exception("Wasn't able to get a token")
51
return (oauth.OAuthToken.from_string(secret), consumer)
53
def get_oauth_request_header(consumer, access_token, http_url, signature_method):
54
"""Get an oauth request header given the token and the url"""
55
assert http_url.startswith("https")
56
oauth_request = oauth.OAuthRequest.from_consumer_and_token(
59
oauth_consumer=consumer,
61
oauth_request.sign_request(signature_method, consumer, access_token)
62
return oauth_request.to_header()
64
def request(urlpath, sigmeth, http_method, request_body, show_tokens,
65
server_override=None, explicit_token_store=None):
66
"""Make a request to couchdb.one.ubuntu.com for the user's data.
68
The user supplies a urlpath (for example, dbname). We need to actually
69
request https://couchdb.one.ubuntu.com/PREFIX/dbname, and sign it with
70
the user's OAuth token, which must be in the keyring.
72
We find the prefix by querying https://one.ubuntu.com/api/account/
73
(see desktopcouch.replication_services.ubuntuone, which does this).
76
# First check that there's a token in the keyring.
78
(access_token, consumer) = get_prod_oauth_token(explicit_token_store)
79
except: # bare except, norty
80
raise Exception("Unable to retrieve your Ubuntu One access details "
81
"from the keyring. Try connecting your machine to Ubuntu One.")
83
# Set the signature method. This should be HMAC unless you have a jolly
84
# good reason for it to not be.
85
if sigmeth == "PLAINTEXT":
86
signature_method = oauth.OAuthSignatureMethod_PLAINTEXT()
87
elif sigmeth == "HMAC_SHA1":
88
signature_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
90
signature_method = oauth.OAuthSignatureMethod_PLAINTEXT()
93
print "Using OAuth data:"
94
print "consumer: %s : %s\ntoken: %s" % (
95
consumer.key, consumer.secret, access_token)
97
# Look up the user's prefix
98
infourl = "https://one.ubuntu.com/api/account/"
99
oauth_header = get_oauth_request_header(consumer, access_token, infourl, signature_method)
100
client = httplib2.Http()
101
resp, content = client.request(infourl, "GET", headers=oauth_header)
102
if resp['status'] == "200":
104
document = simplejson.loads(content)
106
raise Exception("Got unexpected content:\n%s" % content)
107
if "couchdb_root" not in document:
108
raise ValueError("couchdb_root not found in %s" % (document,))
109
if "id" not in document:
110
raise ValueError("id not found in %s" % (document,))
111
COUCH_ROOT = document["couchdb_root"]
112
USERID = document["id"]
114
raise ValueError("Error retrieving user data (%s)" % (resp['status'],
117
# COUCH_ROOT must have all internal slashes escaped
118
schema, netloc, path, params, query, fragment = urlparse.urlparse(COUCH_ROOT)
120
netloc = server_override
121
path = "/" + urllib.quote(path[1:], safe="") # don't escape the first /
122
COUCH_ROOT = urlparse.urlunparse((schema, netloc, path, params, query, fragment))
124
# Now use COUCH_ROOT and the specified user urlpath to get data
125
if urlpath == "_all_dbs":
126
couch_url = "%s%s" % (COUCH_ROOT, urlpath)
127
couch_url = urlparse.urlunparse((schema, netloc, "_all_dbs", None, "user_id=%s" % USERID, None))
129
couch_url = "%s%%2F%s" % (COUCH_ROOT, urlpath)
130
schema, netloc, path, params, query, fragment = urlparse.urlparse(couch_url)
131
querystr_as_dict = dict(cgi.parse_qsl(query))
132
oauth_request = oauth.OAuthRequest.from_consumer_and_token(
134
http_method=http_method,
135
oauth_consumer=consumer,
137
parameters=querystr_as_dict)
138
oauth_request.sign_request(signature_method, consumer, access_token)
140
#print "Connecting to effective Couch URL", oauth_request.to_url()
141
#print "Connecting to actual Couch URL (with OAuth header)", couch_url
142
#print oauth_request.to_header(), querystr_as_dict
145
resp, content = client.request(couch_url, http_method,
146
headers=oauth_request.to_header(), body=request_body)
155
print "(request failed %s time%s)" % (failed, s)
156
if resp['status'] == "200":
158
return simplejson.loads(content)
160
print "(data returned from CouchDB was invalid JSON)"
162
elif resp['status'] == "400":
163
print "The server could not parse the oauth token:\n%s" % content
164
elif resp['status'] == "401":
165
print "Access Denied"
170
"There was a problem processing the request:\nstatus:%s, response:"
171
" %r" % (resp['status'], content))
174
29
if __name__ == "__main__":
175
parser = OptionParser(usage="prog [options] urlpath")
176
parser.add_option("--oauth-signature-method", dest="sigmeth",
30
PARSER = OptionParser(usage="prog [options] urlpath")
31
PARSER.add_option("--oauth-signature-method", dest="sigmeth",
177
32
default="HMAC_SHA1",
178
33
help="OAuth signature method to use (PLAINTEXT or "
180
parser.add_option("--http-method", dest="http_method",
35
PARSER.add_option("--http-method", dest="http_method",
182
37
help="HTTP method to use")
183
parser.add_option("--body", dest="body",
38
PARSER.add_option("--body", dest="body",
185
40
help="HTTP request body")
186
parser.add_option("--show-tokens", dest="show_tokens",
41
PARSER.add_option("--show-tokens", dest="show_tokens",
188
43
help="Show the OAuth tokens we're using")
189
parser.add_option("--server-override", dest="server_override",
44
PARSER.add_option("--server-override", dest="server_override",
191
46
help="Use a different server")
192
parser.add_option("--explicit-token-store", dest="explicit_token_store",
194
help="Explicitly choose a token store (sso or hammertime)")
196
(options, args) = parser.parse_args()
198
parser.error("You must specify a urlpath (e.g., a dbname)")
200
urlpath=args[0], sigmeth=options.sigmeth,
201
http_method=options.http_method, request_body=options.body,
202
show_tokens=options.show_tokens,
203
server_override=options.server_override,
204
explicit_token_store=options.explicit_token_store)
48
(OPTIONS, ARGS) = PARSER.parse_args()
50
PARSER.error("You must specify a urlpath (e.g., a dbname)")
52
urlpath=ARGS[0], sig_meth=OPTIONS.sigmeth,
53
http_method=OPTIONS.http_method, request_body=OPTIONS.body,
54
show_tokens=OPTIONS.show_tokens,
55
server_override=OPTIONS.server_override)