Merge lp:~alecu/ubuntu-sso-client/timestamp-autofix-1-0 into lp:ubuntu-sso-client/stable-1-0

Proposed by Alejandro J. Cura on 2011-11-18
Status: Merged
Approved by: Natalia Bidart on 2011-12-02
Approved revision: 649
Merged at revision: 647
Proposed branch: lp:~alecu/ubuntu-sso-client/timestamp-autofix-1-0
Merge into: lp:ubuntu-sso-client/stable-1-0
Diff against target: 431 lines (+315/-6)
5 files modified
ubuntu_sso/main.py (+32/-4)
ubuntu_sso/tests/test_main.py (+38/-2)
ubuntu_sso/utils/__init__.py (+81/-0)
ubuntu_sso/utils/tests/__init__.py (+16/-0)
ubuntu_sso/utils/tests/test_oauth_headers.py (+148/-0)
To merge this branch: bzr merge lp:~alecu/ubuntu-sso-client/timestamp-autofix-1-0
Reviewer Review Type Date Requested Status
Natalia Bidart Approve on 2011-12-02
Diego Sarmentero (community) 2011-11-18 Approve on 2011-11-24
Review via email: mp+82737@code.launchpad.net

Commit message

Do a HEAD request on the server to get accurate timestamp (LP: #692597 & LP: #891644)

Description of the change

Do a HEAD request on the server to get accurate timestamp (LP: #692597 & LP: #891644)

To post a comment you must log in.
Diego Sarmentero (diegosarmentero) wrote :

Text conflict in bin/ubuntu-sso-login
Text conflict in data/gtk/ui.glade
Text conflict in run-tests
Text conflict in setup.py
Text conflict in ubuntu_sso/gtk/gui.py
Text conflict in ubuntu_sso/gtk/tests/test_gui.py
Text conflict in ubuntu_sso/keyring/linux.py
Text conflict in ubuntu_sso/keyring/tests/test_linux.py
Text conflict in ubuntu_sso/main/linux.py
Text conflict in ubuntu_sso/main/tests/test_linux.py
Conflict adding file ubuntu_sso/utils. Moved existing file to ubuntu_sso/utils.moved.
11 conflicts encountered.

review: Needs Fixing
review: Abstain
649. By Alejandro J. Cura on 2011-11-23

fix year in file headers

Diego Sarmentero (diegosarmentero) wrote :

+1 looks great!

review: Approve
Natalia Bidart (nataliabidart) wrote :

Tested locally and IRL. It works great, and all tests are green.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntu_sso/main.py'
2--- ubuntu_sso/main.py 2011-03-29 17:26:26 +0000
3+++ ubuntu_sso/main.py 2011-11-23 19:50:27 +0000
4@@ -3,7 +3,7 @@
5 # Author: Natalia Bidart <natalia.bidart@canonical.com>
6 # Author: Alejandro J. Cura <alecu@canonical.com>
7 #
8-# Copyright 2009 Canonical Ltd.
9+# Copyright 2009, 2011 Canonical Ltd.
10 #
11 # This program is free software: you can redistribute it and/or modify it
12 # under the terms of the GNU General Public License version 3, as published
13@@ -46,6 +46,7 @@
14 from ubuntu_sso import DBUS_IFACE_USER_NAME, DBUS_IFACE_CRED_NAME
15 from ubuntu_sso.keyring import Keyring, get_token_name, U1_APP_NAME
16 from ubuntu_sso.logger import setup_logging
17+from ubuntu_sso.utils import timestamp_checker
18
19
20 # Disable the invalid name warning, as we have a lot of DBus style names
21@@ -117,6 +118,27 @@
22 return creds
23
24
25+class TimestampedAuthorizer(OAuthAuthorizer):
26+ """Includes a custom timestamp on OAuth signatures."""
27+
28+ def __init__(self, get_timestamp, *args, **kwargs):
29+ """Store the get_timestamp method, and move on."""
30+ OAuthAuthorizer.__init__(self, *args, **kwargs)
31+ self.get_timestamp = get_timestamp
32+
33+ # pylint: disable=C0103,E1101
34+ def authorizeRequest(self, absolute_uri, method, body, headers):
35+ """Override authorizeRequest including the timestamp."""
36+ parameters = {"oauth_timestamp": self.get_timestamp()}
37+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(
38+ self.consumer, self.access_token, http_url=absolute_uri,
39+ parameters=parameters)
40+ oauth_request.sign_request(
41+ oauth.OAuthSignatureMethod_PLAINTEXT(),
42+ self.consumer, self.access_token)
43+ headers.update(oauth_request.to_header(self.oauth_realm))
44+
45+
46 class SSOLoginProcessor(object):
47 """Login and register users using the Ubuntu Single Sign On service."""
48
49@@ -236,7 +258,9 @@
50 if sso_service is None:
51 oauth_token = oauth.OAuthToken(token['token'],
52 token['token_secret'])
53- authorizer = OAuthAuthorizer(token['consumer_key'],
54+ authorizer = TimestampedAuthorizer(
55+ timestamp_checker.get_faithful_time,
56+ token['consumer_key'],
57 token['consumer_secret'],
58 oauth_token)
59 sso_service = self.sso_service_class(authorizer, self.service_url)
60@@ -258,7 +282,8 @@
61 token_name=token_name)
62
63 oauth_token = oauth.OAuthToken(token['token'], token['token_secret'])
64- authorizer = OAuthAuthorizer(token['consumer_key'],
65+ authorizer = TimestampedAuthorizer(
66+ timestamp_checker.get_faithful_time,
67 token['consumer_secret'],
68 oauth_token)
69 sso_service = self.sso_service_class(authorizer, self.service_url)
70@@ -607,9 +632,12 @@
71 credentials['consumer_secret'])
72 token = oauth.OAuthToken(credentials['token'],
73 credentials['token_secret'])
74+ timestamp = timestamp_checker.get_faithful_time()
75+ parameters = {"oauth_timestamp": timestamp}
76 get_request = oauth.OAuthRequest.from_consumer_and_token
77 oauth_req = get_request(oauth_consumer=consumer, token=token,
78- http_method="GET", http_url=url)
79+ http_method="GET", http_url=url,
80+ parameters=parameters)
81 oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
82 consumer, token)
83 request = urllib2.Request(url, headers=oauth_req.to_header())
84
85=== modified file 'ubuntu_sso/tests/test_main.py'
86--- ubuntu_sso/tests/test_main.py 2011-03-29 17:26:26 +0000
87+++ ubuntu_sso/tests/test_main.py 2011-11-23 19:50:27 +0000
88@@ -5,7 +5,7 @@
89 # Author: Natalia Bidart <natalia.bidart@canonical.com>
90 # Author: Alejandro J. Cura <alecu@canonical.com>
91 #
92-# Copyright 2009-2010 Canonical Ltd.
93+# Copyright 2009-2011 Canonical Ltd.
94 #
95 # This program is free software: you can redistribute it and/or modify it
96 # under the terms of the GNU General Public License version 3, as published
97@@ -22,6 +22,7 @@
98
99 import logging
100 import os
101+import time
102 import urllib2
103
104 import gobject
105@@ -31,6 +32,7 @@
106 from lazr.restfulclient.errors import HTTPError
107 # pylint: enable=F0401
108 from mocker import Mocker, MockerTestCase, ARGS, KWARGS, ANY
109+from oauth import oauth
110 from twisted.internet.defer import Deferred
111 from twisted.trial.unittest import TestCase
112
113@@ -46,7 +48,7 @@
114 keyring_get_credentials, keyring_store_credentials, logger,
115 NewPasswordError, PING_URL, SERVICE_URL,
116 RegistrationError, ResetPasswordTokenError,
117- SSOCredentials, SSOLogin, SSOLoginProcessor)
118+ SSOCredentials, SSOLogin, SSOLoginProcessor, TimestampedAuthorizer)
119
120
121 # Access to a protected member 'yyy' of a client class
122@@ -193,6 +195,38 @@
123 self.accounts = FakedAccounts()
124
125
126+class TimestampedAuthorizerTestCase(TestCase):
127+ """Test suite for the TimestampedAuthorizer."""
128+
129+ def test_authorize_request_includes_timestamp(self):
130+ """The authorizeRequest method includes the timestamp."""
131+ fromcandt_call = []
132+ fake_uri = "http://protocultura.net"
133+ fake_timestamp = 1234
134+ get_fake_timestamp = lambda: fake_timestamp
135+ original_oauthrequest = oauth.OAuthRequest
136+
137+ class FakeOAuthRequest(oauth.OAuthRequest):
138+ """A Fake OAuthRequest class."""
139+
140+ # pylint: disable=W0221
141+ @staticmethod
142+ def from_consumer_and_token(*args, **kwargs):
143+ """A fake from_consumer_and_token."""
144+ fromcandt_call.append((args, kwargs))
145+ builder = original_oauthrequest.from_consumer_and_token
146+ return builder(*args, **kwargs)
147+ # pylint: enable=W0221
148+
149+ self.patch(oauth, "OAuthRequest", FakeOAuthRequest)
150+
151+ authorizer = TimestampedAuthorizer(get_fake_timestamp, "ubuntuone")
152+ authorizer.authorizeRequest(fake_uri, "POST", None, {})
153+ call_kwargs = fromcandt_call[0][1]
154+ parameters = call_kwargs["parameters"]
155+ self.assertEqual(parameters["oauth_timestamp"], fake_timestamp)
156+
157+
158 class SSOLoginProcessorTestCase(TestCase, MockerTestCase):
159 """Test suite for the SSO login processor."""
160
161@@ -1426,6 +1460,8 @@
162 return FakedResponse(code=200)
163
164 self.patch(urllib2, 'urlopen', fake_it)
165+ self.patch(ubuntu_sso.main.timestamp_checker, "get_faithful_time",
166+ time.time)
167
168 self.client = SSOCredentials(None)
169
170
171=== added directory 'ubuntu_sso/utils'
172=== added file 'ubuntu_sso/utils/__init__.py'
173--- ubuntu_sso/utils/__init__.py 1970-01-01 00:00:00 +0000
174+++ ubuntu_sso/utils/__init__.py 2011-11-23 19:50:27 +0000
175@@ -0,0 +1,81 @@
176+# -*- coding: utf-8 -*-
177+
178+# Author: Alejandro J. Cura <alecu@canonical.com>
179+#
180+# Copyright 2010, 2011 Canonical Ltd.
181+#
182+# This program is free software: you can redistribute it and/or modify it
183+# under the terms of the GNU General Public License version 3, as published
184+# by the Free Software Foundation.
185+#
186+# This program is distributed in the hope that it will be useful, but
187+# WITHOUT ANY WARRANTY; without even the implied warranties of
188+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
189+# PURPOSE. See the GNU General Public License for more details.
190+#
191+# You should have received a copy of the GNU General Public License along
192+# with this program. If not, see <http://www.gnu.org/licenses/>.
193+
194+"""Utility modules that may find use outside ubuntu_sso."""
195+import time
196+import urllib2
197+from twisted.web import http
198+
199+from ubuntu_sso.logger import setup_logging
200+logger = setup_logging("ubuntu_sso.utils")
201+
202+
203+class RequestHead(urllib2.Request):
204+ """A request with the method set to HEAD."""
205+
206+ _request_method = "HEAD"
207+
208+ def get_method(self):
209+ """Return the desired method."""
210+ return self._request_method
211+
212+
213+class SyncTimestampChecker(object):
214+ """A timestamp that's regularly checked with a server."""
215+
216+ CHECKING_INTERVAL = 60 * 60 # in seconds
217+ ERROR_INTERVAL = 30 # in seconds
218+ SERVER_URL = "http://one.ubuntu.com/api/time"
219+
220+ def __init__(self):
221+ """Initialize this instance."""
222+ self.next_check = time.time()
223+ self.skew = 0
224+
225+ def get_server_time(self):
226+ """Get the time at the server."""
227+ headers = {"Cache-Control": "no-cache"}
228+ request = RequestHead(self.SERVER_URL, headers=headers)
229+ response = urllib2.urlopen(request)
230+ date_string = response.info()["Date"]
231+ timestamp = http.stringToDatetime(date_string)
232+ return timestamp
233+
234+ def get_faithful_time(self):
235+ """Get an accurate timestamp."""
236+ local_time = time.time()
237+ if local_time >= self.next_check:
238+ try:
239+ server_time = self.get_server_time()
240+ self.next_check = local_time + self.CHECKING_INTERVAL
241+ self.skew = server_time - local_time
242+ logger.debug("Calculated server-local time skew: %r",
243+ self.skew)
244+ #pylint: disable=W0703
245+ except Exception, server_error:
246+ logger.debug("Error while verifying the server time skew: %r",
247+ server_error)
248+ self.next_check = local_time + self.ERROR_INTERVAL
249+ logger.debug("Using corrected timestamp: %r",
250+ http.datetimeToString(local_time + self.skew))
251+ return int(local_time + self.skew)
252+
253+
254+# pylint: disable=C0103
255+timestamp_checker = SyncTimestampChecker()
256+# pylint: enable=C0103
257
258=== added directory 'ubuntu_sso/utils/tests'
259=== added file 'ubuntu_sso/utils/tests/__init__.py'
260--- ubuntu_sso/utils/tests/__init__.py 1970-01-01 00:00:00 +0000
261+++ ubuntu_sso/utils/tests/__init__.py 2011-11-23 19:50:27 +0000
262@@ -0,0 +1,16 @@
263+# ubuntu_sso - Ubuntu Single Sign On client support for desktop apps
264+#
265+# Copyright 2011 Canonical Ltd.
266+#
267+# This program is free software: you can redistribute it and/or modify it
268+# under the terms of the GNU General Public License version 3, as published
269+# by the Free Software Foundation.
270+#
271+# This program is distributed in the hope that it will be useful, but
272+# WITHOUT ANY WARRANTY; without even the implied warranties of
273+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
274+# PURPOSE. See the GNU General Public License for more details.
275+#
276+# You should have received a copy of the GNU General Public License along
277+# with this program. If not, see <http://www.gnu.org/licenses/>.
278+"""Ubuntu Single Sign On utils tests."""
279
280=== added file 'ubuntu_sso/utils/tests/test_oauth_headers.py'
281--- ubuntu_sso/utils/tests/test_oauth_headers.py 1970-01-01 00:00:00 +0000
282+++ ubuntu_sso/utils/tests/test_oauth_headers.py 2011-11-23 19:50:27 +0000
283@@ -0,0 +1,148 @@
284+# -*- coding: utf-8 -*-
285+
286+# Author: Alejandro J. Cura <alecu@canonical.com>
287+#
288+# Copyright 2011 Canonical Ltd.
289+#
290+# This program is free software: you can redistribute it and/or modify it
291+# under the terms of the GNU General Public License version 3, as published
292+# by the Free Software Foundation.
293+#
294+# This program is distributed in the hope that it will be useful, but
295+# WITHOUT ANY WARRANTY; without even the implied warranties of
296+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
297+# PURPOSE. See the GNU General Public License for more details.
298+#
299+# You should have received a copy of the GNU General Public License along
300+# with this program. If not, see <http://www.gnu.org/licenses/>.
301+
302+"""Tests for the oauth_headers helper function."""
303+
304+import time
305+
306+from twisted.application import internet, service
307+from twisted.internet import defer
308+from twisted.internet.threads import deferToThread
309+from twisted.trial.unittest import TestCase
310+from twisted.web import server, resource
311+
312+from ubuntu_sso.utils import SyncTimestampChecker
313+
314+
315+class RootResource(resource.Resource):
316+ """A root resource that logs the number of calls."""
317+
318+ isLeaf = True
319+
320+ def __init__(self, *args, **kwargs):
321+ """Initialize this fake instance."""
322+ resource.Resource.__init__(self, *args, **kwargs)
323+ self.count = 0
324+ self.request_headers = []
325+
326+ # pylint: disable=C0103
327+ def render_HEAD(self, request):
328+ """Increase the counter on each render."""
329+ self.count += 1
330+ self.request_headers.append(request.requestHeaders)
331+ return ""
332+
333+
334+class MockWebServer(object):
335+ """A mock webserver for testing."""
336+
337+ def __init__(self):
338+ """Start up this instance."""
339+ # pylint: disable=E1101
340+ self.root = RootResource()
341+ site = server.Site(self.root)
342+ application = service.Application('web')
343+ self.service_collection = service.IServiceCollection(application)
344+ self.tcpserver = internet.TCPServer(0, site)
345+ self.tcpserver.setServiceParent(self.service_collection)
346+ self.service_collection.startService()
347+
348+ def get_url(self):
349+ """Build the url for this mock server."""
350+ # pylint: disable=W0212
351+ port_num = self.tcpserver._port.getHost().port
352+ return "http://localhost:%d/" % port_num
353+
354+ def stop(self):
355+ """Shut it down."""
356+ # pylint: disable=E1101
357+ self.service_collection.stopService()
358+
359+
360+class FakedError(Exception):
361+ """A mock, test, sample, and fake exception."""
362+
363+
364+class TimestampCheckerTestCase(TestCase):
365+ """Tests for the timestamp checker."""
366+
367+ def setUp(self):
368+ """Initialize a fake webserver."""
369+ self.ws = MockWebServer()
370+ self.addCleanup(self.ws.stop)
371+ self.patch(SyncTimestampChecker, "SERVER_URL", self.ws.get_url())
372+
373+ @defer.inlineCallbacks
374+ def test_returned_value_is_int(self):
375+ """The returned value is an integer."""
376+ checker = SyncTimestampChecker()
377+ timestamp = yield deferToThread(checker.get_faithful_time)
378+ self.assertEqual(type(timestamp), int)
379+
380+ @defer.inlineCallbacks
381+ def test_first_call_does_head(self):
382+ """The first call gets the clock from our web."""
383+ checker = SyncTimestampChecker()
384+ yield deferToThread(checker.get_faithful_time)
385+ self.assertEqual(self.ws.root.count, 1)
386+
387+ @defer.inlineCallbacks
388+ def test_second_call_is_cached(self):
389+ """For the second call, the time is cached."""
390+ checker = SyncTimestampChecker()
391+ yield deferToThread(checker.get_faithful_time)
392+ yield deferToThread(checker.get_faithful_time)
393+ self.assertEqual(self.ws.root.count, 1)
394+
395+ @defer.inlineCallbacks
396+ def test_after_timeout_cache_expires(self):
397+ """After some time, the cache expires."""
398+ fake_timestamp = 1
399+ self.patch(time, "time", lambda: fake_timestamp)
400+ checker = SyncTimestampChecker()
401+ yield deferToThread(checker.get_faithful_time)
402+ fake_timestamp += SyncTimestampChecker.CHECKING_INTERVAL
403+ yield deferToThread(checker.get_faithful_time)
404+ self.assertEqual(self.ws.root.count, 2)
405+
406+ @defer.inlineCallbacks
407+ def test_server_date_sends_nocache_headers(self):
408+ """Getting the server date sends the no-cache headers."""
409+ checker = SyncTimestampChecker()
410+ yield deferToThread(checker.get_server_time)
411+ assert len(self.ws.root.request_headers) == 1
412+ headers = self.ws.root.request_headers[0]
413+ result = headers.getRawHeaders("Cache-Control")
414+ self.assertEqual(result, ["no-cache"])
415+
416+ @defer.inlineCallbacks
417+ def test_server_error_means_skew_not_updated(self):
418+ """When server can't be reached, the skew is not updated."""
419+ fake_timestamp = 1
420+ self.patch(time, "time", lambda: fake_timestamp)
421+ checker = SyncTimestampChecker()
422+
423+ def failing_get_server_time():
424+ """Let's fail while retrieving the server time."""
425+ raise FakedError()
426+
427+ self.patch(checker, "get_server_time", failing_get_server_time)
428+ yield deferToThread(checker.get_faithful_time)
429+ self.assertEqual(checker.skew, 0)
430+ self.assertEqual(checker.next_check,
431+ fake_timestamp + SyncTimestampChecker.ERROR_INTERVAL)

Subscribers

People subscribed via source and target branches