1 from oauth import oauth
6 import org.maemo.hermes.engine.provider
13 class OAuthProvider(org.maemo.hermes.engine.provider.Provider):
14 """Basis for OAuth services for Hermes. Sub-classes of this should
15 install keys to '<id>_...', and implement the following
19 * get_urls (tuple of request token URL, access token URL & authorize URL)
20 * verify_verifier (returns name of authenticated user)
21 * additional_prefs [optional]
22 * handle_prefs_button [optional]
24 Copyright (c) Andrew Flegg <andrew@bleb.org> 2010.
25 Released under the Artistic Licence."""
27 GCONF_API_KEY = '/apps/maemo/hermes/%s_key'
28 GCONF_API_SECRET = '/apps/maemo/hermes/%s_secret'
29 GCONF_ACCESS_TOKEN = '/apps/maemo/hermes/%s_access_token'
30 GCONF_USER = '/apps/maemo/hermes/%s_user'
32 # -----------------------------------------------------------------------
34 """Initialise the provider, and ensure the environment is going to work."""
36 self._gc = gnome.gconf.client_get_default()
38 api_key = self._gc.get_string(self.GCONF_API_KEY % (self.get_id()))
39 secret_key = self._gc.get_string(self.GCONF_API_SECRET % (self.get_id()))
40 if api_key is None or secret_key is None:
41 raise Exception('No application keys found for %s. Installation error.' % (self.get_id()))
43 self.access_token = self._get_access_token_from_gconf()
44 self.consumer = oauth.OAuthConsumer(api_key, secret_key)
45 self.sig_method = oauth.OAuthSignatureMethod_HMAC_SHA1()
48 # -----------------------------------------------------------------------
50 """Return a tuple containing request token, access token & authorize URLs."""
52 return (None, None, None)
55 # -----------------------------------------------------------------------
56 def get_account_detail(self):
57 """Return the name of the linked LinkedIn account."""
59 return self._gc.get_string(self.GCONF_USER % (self.get_id()))
62 # -----------------------------------------------------------------------
63 def has_preferences(self):
64 """Whether or not this provider has any preferences. If it does not,
65 open_preferences must NOT be called; as the behaviour is undetermined."""
70 # -----------------------------------------------------------------------
71 def open_preferences(self, parent):
72 """Open the preferences for this provider as a child of the 'parent' widget."""
74 self.main_window = parent
75 dialog = gtk.Dialog(self.get_name(), parent)
76 dialog.add_button(_('Disable'), gtk.RESPONSE_NO)
77 enable = dialog.add_button(_('Enable'), gtk.RESPONSE_YES)
79 button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
80 hildon.BUTTON_ARRANGEMENT_VERTICAL)
81 self._handle_button(None, button, enable)
82 button.connect('clicked', self._handle_button, button, enable)
84 dialog.vbox.add(button)
85 self.additional_prefs(dialog)
86 dialog.vbox.add(gtk.Label(""))
92 self.handle_prefs_response(result)
93 if result == gtk.RESPONSE_CANCEL or result == gtk.RESPONSE_DELETE_EVENT:
96 return result == gtk.RESPONSE_YES
99 # -----------------------------------------------------------------------
100 def additional_prefs(self, dialog):
101 """Override to add additional controls to the authentication dialogue."""
103 dialog.vbox.add(gtk.Label(""))
104 dialog.vbox.add(gtk.Label(""))
107 # -----------------------------------------------------------------------
108 def handle_prefs_response(self, result):
109 """Override to handle the response from a dialogue button."""
114 # -----------------------------------------------------------------------
115 def _handle_button(self, e, button, enable):
116 """Ensure the button state is correct."""
118 authenticated = self._get_access_token_from_gconf() is not None
121 self._remove_access_token_from_gconf()
123 self.authenticate(lambda: None, self.block_for_auth)
125 authenticated = self._get_access_token_from_gconf() is not None
127 button.set_title(authenticated and _("Clear authorisation") or _("Authorise"))
128 enable.set_sensitive(authenticated)
131 # -----------------------------------------------------------------------
132 def block_for_auth(self, url):
133 """Part of the GUI callback API."""
137 note = gtk.Dialog(_('Service authorisation'), self.main_window)
138 note.add_button(_("Validate"), gtk.RESPONSE_OK)
139 input = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
140 input.set_property('is-focus', False)
141 input.set_property('hildon-input-mode', gtk.HILDON_GTK_INPUT_MODE_NUMERIC)
142 note.set_title(_("Verification code from web browser"))
148 if result == gtk.RESPONSE_OK:
149 return input.get_text()
154 # -----------------------------------------------------------------------
155 def authenticate(self, need_auth, block_for_auth):
157 token = self._get_request_token()
158 url = self._get_authorize_url(token)
159 verifier = block_for_auth(url)
160 self.access_token = self._get_access_token(token, verifier)
161 name = self.verify_verifier(self.access_token)
162 self._gc.set_string(self.GCONF_USER % (self.get_id()), name)
165 # -----------------------------------------------------------------------
166 def _store_access_token_in_gconf(self, token_str):
167 if "oauth_problem" in token_str:
168 raise Exception("Authorization failure - access token reported OAuth problem")
170 self._gc.set_string(self.GCONF_ACCESS_TOKEN % (self.get_id()), token_str)
173 # -----------------------------------------------------------------------
174 def _get_access_token_from_gconf(self):
175 """Returns an oauth.OAuthToken, or None if the gconf value is empty"""
177 token_str = self._gc.get_string(self.GCONF_ACCESS_TOKEN % (self.get_id()))
179 if not token_str or len(token_str) < 8:
181 return oauth.OAuthToken.from_string(token_str)
184 # -----------------------------------------------------------------------
185 def _remove_access_token_from_gconf(self):
186 """Remove the oauth.OAuthToken, if any."""
188 self._gc.unset(self.GCONF_ACCESS_TOKEN % (self.get_id()))
189 self._gc.unset(self.GCONF_USER % (self.get_id()))
192 # -----------------------------------------------------------------------
193 def _get_request_token(self):
194 """Get a request token from OAuth provider."""
196 url = self.get_urls()[0]
197 hostname = self._get_hostname_from_url(url)
199 oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, callback="oob", http_url=url)
200 oauth_request.sign_request(self.sig_method, self.consumer, None)
202 connection = httplib.HTTPSConnection(hostname)
203 connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
204 response = connection.getresponse().read()
207 token = oauth.OAuthToken.from_string(response)
210 traceback.print_exc()
212 raise Exception("Authorization failure - failed to get request token")
216 # -----------------------------------------------------------------------
217 def _get_authorize_url(self, token):
218 """The URL that the user should browse to, in order to authorize the
219 application's request to access data"""
221 oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=self.get_urls()[2])
222 return oauth_request.to_url()
225 # -----------------------------------------------------------------------
226 def _get_access_token(self, token, verifier):
227 """If the verifier (which was displayed in the browser window) is
228 valid, then an access token is returned which should be used to
229 access data on the service."""
231 url = self.get_urls()[1]
232 hostname = self._get_hostname_from_url(url)
234 oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=token, verifier=verifier, http_url=url)
235 oauth_request.sign_request(self.sig_method, self.consumer, token)
237 connection = httplib.HTTPSConnection(hostname)
238 connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
239 response = connection.getresponse()
240 token_str = response.read()
241 if 'oauth_problem' in token_str:
242 raise Exception("Authorization failure - failed to get access token (" + token_str + ")")
243 self._store_access_token_in_gconf(token_str)
244 return oauth.OAuthToken.from_string(token_str)
247 # -----------------------------------------------------------------------
248 def make_api_request(self, url):
249 hostname = self._get_hostname_from_url(url)
251 oauth_request = oauth.OAuthRequest.from_consumer_and_token(self.consumer, token=self.access_token, http_url=url)
252 oauth_request.sign_request(self.sig_method, self.consumer, self.access_token)
253 connection = httplib.HTTPSConnection(hostname)
255 connection.request(oauth_request.http_method, url, headers=oauth_request.to_header())
256 xml = connection.getresponse().read()
259 raise Exception("Failed to contact LinkedIn at " + url)
262 # -----------------------------------------------------------------------
263 def _get_hostname_from_url(self, url):
264 """Extract the hostname from a URL."""
265 return re.sub(r'^\w+://(.+?)[/:].*$', r'\1', url)