Merge lp:~alecu/ubuntuone-storage-protocol/timestamp-autofix-1-6 into lp:ubuntuone-storage-protocol/stable-1-6

Proposed by Alejandro J. Cura
Status: Merged
Approved by: Natalia Bidart
Approved revision: 136
Merged at revision: 136
Proposed branch: lp:~alecu/ubuntuone-storage-protocol/timestamp-autofix-1-6
Merge into: lp:ubuntuone-storage-protocol/stable-1-6
Diff against target: 344 lines (+245/-5)
3 files modified
tests/test_client.py (+145/-3)
ubuntuone/storageprotocol/client.py (+30/-2)
ubuntuone/storageprotocol/utils.py (+70/-0)
To merge this branch: bzr merge lp:~alecu/ubuntuone-storage-protocol/timestamp-autofix-1-6
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Diego Sarmentero (community) Abstain
Review via email: mp+81791@code.launchpad.net

Commit message

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

Description of the change

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

To post a comment you must log in.
136. By Alejandro J. Cura

Use the dedicated time url

Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

Text conflict in setup.py
Text conflict in tests/test_client.py
Text conflict in ubuntuone/storageprotocol/client.py
3 conflicts encountered.

Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

Forgot the need fixing in the comment above

review: Needs Fixing
Revision history for this message
Diego Sarmentero (diegosarmentero) :
review: Abstain
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Works great! IRL tested.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'tests/test_client.py'
2--- tests/test_client.py 2011-04-25 21:00:47 +0000
3+++ tests/test_client.py 2011-11-17 18:43:24 +0000
4@@ -1,8 +1,9 @@
5 # -*- coding: utf-8 -*-
6 #
7 # Author: Natalia B. Bidart <natalia.bidart@canonical.com>
8+# Author: Alejandro J. Cura <alecu@canonical.com>
9 #
10-# Copyright (C) 2009 Canonical Ltd.
11+# Copyright (C) 2009, 2011 Canonical Ltd.
12 #
13 # This program is free software: you can redistribute it and/or modify it
14 # under the terms of the GNU Affero General Public License version 3,
15@@ -19,16 +20,21 @@
16
17 import StringIO
18 import os
19+import time
20 import unittest
21 import uuid
22
23-from twisted.internet.defer import Deferred
24+from twisted.application import internet, service
25+from twisted.internet import defer
26+from twisted.internet.defer import Deferred, inlineCallbacks
27 from twisted.trial.unittest import TestCase as TwistedTestCase
28+from twisted.web import server, resource
29
30 from ubuntuone.storageprotocol import protocol_pb2, sharersp, delta, request
31 from ubuntuone.storageprotocol.client import (
32 StorageClient, CreateUDF, ListVolumes, DeleteVolume, GetDelta, Unlink,
33 Authenticate, MakeFile, MakeDir, PutContent, Move, BytesMessageProducer,
34+ oauth, TwistedTimestampChecker, tx_timestamp_checker,
35 )
36 from ubuntuone.storageprotocol import volumes
37 from tests import test_delta_info
38@@ -725,7 +731,7 @@
39 actual_msg.get_delta.share)
40
41
42-class TestAuth(unittest.TestCase):
43+class TestAuth(TwistedTestCase):
44 """Tests the authentication request."""
45
46 def test_session_id(self):
47@@ -741,6 +747,142 @@
48 self.assert_(req.done.called)
49 self.assertEqual(req.session_id, SESSION_ID)
50
51+ def test_oauth_authenticate_uses_server_timestamp(self):
52+ """The oauth authentication uses the server timestamp."""
53+ fromcandt_call = []
54+
55+ fake_token = oauth.OAuthToken('token', 'token_secret')
56+ fake_consumer = oauth.OAuthConsumer('consumer_key', 'consumer_secret')
57+
58+ fake_timestamp = object()
59+ timestamp_d = Deferred()
60+ self.patch(tx_timestamp_checker, "get_faithful_time",
61+ lambda: timestamp_d)
62+ original_fromcandt = oauth.OAuthRequest.from_consumer_and_token
63+
64+ @staticmethod
65+ def fake_from_consumer_and_token(**kwargs):
66+ """A fake from_consumer_and_token."""
67+ fromcandt_call.append(kwargs)
68+ return original_fromcandt(**kwargs)
69+
70+ self.patch(oauth.OAuthRequest, "from_consumer_and_token",
71+ fake_from_consumer_and_token)
72+ protocol = FakedProtocol()
73+ protocol.oauth_authenticate(fake_consumer, fake_token)
74+ self.assertEqual(len(fromcandt_call), 0)
75+ timestamp_d.callback(fake_timestamp)
76+ parameters = fromcandt_call[0]["parameters"]
77+ self.assertEqual(parameters["oauth_timestamp"], fake_timestamp)
78+
79+
80+class RootResource(resource.Resource):
81+ """A root resource that logs the number of calls."""
82+
83+ isLeaf = True
84+
85+ def __init__(self, *args, **kwargs):
86+ """Initialize this fake instance."""
87+ self.count = 0
88+ self.request_headers = []
89+
90+ def render_HEAD(self, request):
91+ """Increase the counter on each render."""
92+ self.request_headers.append(request.requestHeaders)
93+ self.count += 1
94+ return ""
95+
96+
97+class MockWebServer(object):
98+ """A mock webserver for testing."""
99+
100+ def __init__(self):
101+ """Start up this instance."""
102+ self.root = RootResource()
103+ site = server.Site(self.root)
104+ application = service.Application('web')
105+ self.service_collection = service.IServiceCollection(application)
106+ self.tcpserver = internet.TCPServer(0, site)
107+ self.tcpserver.setServiceParent(self.service_collection)
108+ self.service_collection.startService()
109+
110+ def get_url(self):
111+ """Build the url for this mock server."""
112+ port_num = self.tcpserver._port.getHost().port
113+ return "http://localhost:%d/" % port_num
114+
115+ def stop(self):
116+ """Shut it down."""
117+ self.service_collection.stopService()
118+
119+
120+class TimestampCheckerTestCase(TwistedTestCase):
121+ """Tests for the timestamp checker."""
122+
123+ @inlineCallbacks
124+ def setUp(self):
125+ """Initialize a fake webserver."""
126+ yield super(TwistedTestCase, self).setUp()
127+ self.ws = MockWebServer()
128+ self.addCleanup(self.ws.stop)
129+ self.patch(TwistedTimestampChecker, "SERVER_URL", self.ws.get_url())
130+
131+ @inlineCallbacks
132+ def test_returned_value_is_int(self):
133+ """The returned value is an integer."""
134+ checker = TwistedTimestampChecker()
135+ t = yield checker.get_faithful_time()
136+ self.assertEqual(type(t), int)
137+
138+ @inlineCallbacks
139+ def test_first_call_does_head(self):
140+ """The first call gets the clock from our web."""
141+ checker = TwistedTimestampChecker()
142+ yield checker.get_faithful_time()
143+ self.assertEqual(self.ws.root.count, 1)
144+
145+ @inlineCallbacks
146+ def test_second_call_is_cached(self):
147+ """For the second call, the time is cached."""
148+ checker = TwistedTimestampChecker()
149+ yield checker.get_faithful_time()
150+ yield checker.get_faithful_time()
151+ self.assertEqual(self.ws.root.count, 1)
152+
153+ @inlineCallbacks
154+ def test_after_timeout_cache_expires(self):
155+ """After some time, the cache expires."""
156+ fake_timestamp = 1
157+ self.patch(time, "time", lambda: fake_timestamp)
158+ checker = TwistedTimestampChecker()
159+ yield checker.get_faithful_time()
160+ fake_timestamp += TwistedTimestampChecker.CHECKING_INTERVAL
161+ yield checker.get_faithful_time()
162+ self.assertEqual(self.ws.root.count, 2)
163+
164+ @inlineCallbacks
165+ def test_server_error_means_skew_not_updated(self):
166+ """When server can't be reached, the skew is not updated."""
167+ fake_timestamp = 1
168+ self.patch(time, "time", lambda: fake_timestamp)
169+ checker = TwistedTimestampChecker()
170+ failing_get_server_time = lambda: defer.fail(FakedError())
171+ self.patch(checker, "get_server_time", failing_get_server_time)
172+ yield checker.get_faithful_time()
173+ self.assertEqual(checker.skew, 0)
174+ self.assertEqual(checker.next_check,
175+ fake_timestamp + TwistedTimestampChecker.ERROR_INTERVAL)
176+
177+ @inlineCallbacks
178+ def test_server_date_sends_nocache_headers(self):
179+ """Getting the server date sends the no-cache headers."""
180+ checker = TwistedTimestampChecker()
181+ yield checker.get_server_date_header(self.ws.get_url())
182+ assert len(self.ws.root.request_headers) == 1
183+ headers = self.ws.root.request_headers[0]
184+ result = headers.getRawHeaders("Cache-Control")
185+ self.assertEqual(result, ["no-cache"])
186+
187
188 class TestGenerationInRequests(unittest.TestCase):
189 """Base class for testing that actions that change the volume will
190
191=== modified file 'ubuntuone/storageprotocol/client.py'
192--- ubuntuone/storageprotocol/client.py 2011-04-25 21:00:47 +0000
193+++ ubuntuone/storageprotocol/client.py 2011-11-17 18:43:24 +0000
194@@ -4,6 +4,7 @@
195 # Author: Natalia B. Bidart <natalia.bidart@canonical.com>
196 # Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
197 # Author: Facundo Batista <facundo@canonical.com>
198+# Author: Alejandro J. Cura <alecu@canonical.com>
199 #
200 # Copyright 2009-2011 Canonical Ltd.
201 #
202@@ -30,13 +31,36 @@
203 from twisted.internet.protocol import ClientFactory
204 from twisted.internet import reactor, defer
205 from twisted.python import log
206+from twisted.web import client
207+from twisted.web.http_headers import Headers
208
209 from ubuntuone.storageprotocol import (
210- protocol_pb2, request, sharersp, volumes, delta)
211+ delta,
212+ protocol_pb2,
213+ request,
214+ sharersp,
215+ utils,
216+ volumes,
217+)
218
219 log_debug = partial(log.msg, loglevel=logging.DEBUG)
220
221
222+class TwistedTimestampChecker(utils.BaseTimestampChecker):
223+ """A timestamp that's regularly checked with a server."""
224+
225+ @defer.inlineCallbacks
226+ def get_server_date_header(self, server_url):
227+ """Get the server date using twisted webclient."""
228+ agent = client.Agent(reactor)
229+ headers = Headers({'Cache-Control': ['no-cache']})
230+ response = yield agent.request("HEAD", server_url, headers)
231+ defer.returnValue(response.headers.getRawHeaders("Date")[0])
232+
233+
234+tx_timestamp_checker = TwistedTimestampChecker()
235+
236+
237 class StorageClient(request.RequestHandler):
238 """A Basic Storage Protocol client."""
239
240@@ -101,6 +125,7 @@
241 p.start()
242 return p.deferred
243
244+ @defer.inlineCallbacks
245 def oauth_authenticate(self, consumer, token):
246 """Authenticate to a server using the OAuth provider.
247
248@@ -113,9 +138,11 @@
249 object when completed.
250
251 """
252+ timestamp = yield tx_timestamp_checker.get_faithful_time()
253 req = oauth.OAuthRequest.from_consumer_and_token(
254 oauth_consumer=consumer,
255 token=token,
256+ parameters={"oauth_timestamp": timestamp},
257 http_method="GET",
258 http_url="storage://server")
259 req.sign_request(
260@@ -126,7 +153,8 @@
261 (key, str(value)) for key, value in req.parameters.iteritems())
262 p = Authenticate(self, auth_parameters)
263 p.start()
264- return p.deferred
265+ result = yield p.deferred
266+ defer.returnValue(result)
267
268 def handle_ROOT(self, message):
269 """Handle incoming ROOT message.
270
271=== added file 'ubuntuone/storageprotocol/utils.py'
272--- ubuntuone/storageprotocol/utils.py 1970-01-01 00:00:00 +0000
273+++ ubuntuone/storageprotocol/utils.py 2011-11-17 18:43:24 +0000
274@@ -0,0 +1,70 @@
275+# ubuntuone.storageprotocol.utils - some storage protocol utils
276+#
277+# Author: Alejandro J. Cura <alecu@canonical.com>
278+#
279+# Copyright 2011 Canonical Ltd.
280+#
281+# This program is free software: you can redistribute it and/or modify it
282+# under the terms of the GNU Affero General Public License version 3,
283+# as published by the Free Software Foundation.
284+#
285+# This program is distributed in the hope that it will be useful, but
286+# WITHOUT ANY WARRANTY; without even the implied warranties of
287+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
288+# PURPOSE. See the GNU Affero General Public License for more details.
289+#
290+# You should have received a copy of the GNU Affero General Public License
291+# along with this program. If not, see <http://www.gnu.org/licenses/>.
292+"""Some storage protocol utils."""
293+
294+import logging
295+import time
296+
297+from functools import partial
298+
299+from twisted.internet import defer
300+from twisted.python import log
301+from twisted.web import http
302+
303+log_debug = partial(log.msg, loglevel=logging.DEBUG)
304+
305+
306+class BaseTimestampChecker(object):
307+ """A timestamp that's regularly checked with a server."""
308+
309+ CHECKING_INTERVAL = 60 * 60 # in seconds
310+ ERROR_INTERVAL = 30 # in seconds
311+ SERVER_URL = "http://one.ubuntu.com/api/time"
312+
313+ def __init__(self):
314+ """Initialize this instance."""
315+ self.next_check = time.time()
316+ self.skew = 0
317+
318+ def get_server_date_header(self, server_url):
319+ """Return a deferred with the server time, using your web client."""
320+ return defer.fail(NotImplementedError())
321+
322+ @defer.inlineCallbacks
323+ def get_server_time(self):
324+ """Get the time at the server."""
325+ date_string = yield self.get_server_date_header(self.SERVER_URL)
326+ timestamp = http.stringToDatetime(date_string)
327+ defer.returnValue(timestamp)
328+
329+ @defer.inlineCallbacks
330+ def get_faithful_time(self):
331+ """Get an accurate timestamp."""
332+ local_time = time.time()
333+ if local_time >= self.next_check:
334+ try:
335+ server_time = yield self.get_server_time()
336+ self.next_check = local_time + self.CHECKING_INTERVAL
337+ self.skew = server_time - local_time
338+ log_debug("Calculated server-local time skew:", self.skew)
339+ except Exception, e:
340+ log_debug("Error while verifying the server time skew:", e)
341+ self.next_check = local_time + self.ERROR_INTERVAL
342+ log_debug("Using corrected timestamp:",
343+ http.datetimeToString(local_time + self.skew))
344+ defer.returnValue(int(local_time + self.skew))

Subscribers

People subscribed via source and target branches