Merge lp:~alecu/ubuntuone-storage-protocol/timestamp-autofix-2-0 into lp:ubuntuone-storage-protocol/stable-2-0

Proposed by Alejandro J. Cura
Status: Merged
Approved by: Alejandro J. Cura
Approved revision: 142
Merged at revision: 141
Proposed branch: lp:~alecu/ubuntuone-storage-protocol/timestamp-autofix-2-0
Merge into: lp:ubuntuone-storage-protocol/stable-2-0
Diff against target: 335 lines (+243/-4)
3 files modified
tests/test_client.py (+143/-2)
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-2-0
Reviewer Review Type Date Requested Status
John O'Brien (community) Approve
Diego Sarmentero (community) Approve
Review via email: mp+81792@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.
141. By Alejandro J. Cura

Use the dedicated time url

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

Text conflict in tests/test_client.py
1 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
Alejandro J. Cura (alecu) wrote :

Merged with stable branch, so the conflict should be solved now.

142. By Alejandro J. Cura

merged with stable-2-0

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

Looks great, all the tests were successful.

review: Approve
Revision history for this message
John O'Brien (jdobrien) wrote :

This looks good. Bonus points for the formatting fixes.

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

Subscribers

People subscribed via source and target branches