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

Proposed by Alejandro J. Cura
Status: Merged
Approved by: dobey
Approved revision: 123
Merged at revision: 121
Proposed branch: lp:~alecu/ubuntuone-storage-protocol/timestamp-autofix-1-4
Merge into: lp:ubuntuone-storage-protocol/stable-1-4
Diff against target: 345 lines (+245/-7)
3 files modified
tests/test_client.py (+145/-4)
ubuntuone/storageprotocol/client.py (+30/-3)
ubuntuone/storageprotocol/utils.py (+70/-0)
To merge this branch: bzr merge lp:~alecu/ubuntuone-storage-protocol/timestamp-autofix-1-4
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Diego Sarmentero (community) Approve
Review via email: mp+81758@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.
Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

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

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

+1

review: Approve
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Looks good, tested IRL.

review: Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :

The attempt to merge lp:~alecu/ubuntuone-storage-protocol/timestamp-autofix-1-4 into lp:ubuntuone-storage-protocol/stable-1-4 failed. Below is the output from the failed tests.

running build
running build_py
creating build
creating build/lib.linux-i686-2.6
creating build/lib.linux-i686-2.6/ubuntuone
copying ubuntuone/__init__.py -> build/lib.linux-i686-2.6/ubuntuone
creating build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/context.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/proxy_tunnel.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/content_hash.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/protocol_pb2.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/__init__.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/utils.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/volumes.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/delta.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/errors.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/client.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/dircontent.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/dircontent_pb2.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/validators.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/sharersp.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
copying ubuntuone/storageprotocol/request.py -> build/lib.linux-i686-2.6/ubuntuone/storageprotocol
running check

error: No such file or directory

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

Subscribers

People subscribed via source and target branches