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
=== modified file 'tests/test_client.py'
--- tests/test_client.py 2011-04-25 21:00:47 +0000
+++ tests/test_client.py 2011-11-17 18:43:24 +0000
@@ -1,8 +1,9 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2#2#
3# Author: Natalia B. Bidart <natalia.bidart@canonical.com>3# Author: Natalia B. Bidart <natalia.bidart@canonical.com>
4# Author: Alejandro J. Cura <alecu@canonical.com>
4#5#
5# Copyright (C) 2009 Canonical Ltd.6# Copyright (C) 2009, 2011 Canonical Ltd.
6#7#
7# This program is free software: you can redistribute it and/or modify it8# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU Affero General Public License version 3,9# under the terms of the GNU Affero General Public License version 3,
@@ -19,16 +20,21 @@
1920
20import StringIO21import StringIO
21import os22import os
23import time
22import unittest24import unittest
23import uuid25import uuid
2426
25from twisted.internet.defer import Deferred27from twisted.application import internet, service
28from twisted.internet import defer
29from twisted.internet.defer import Deferred, inlineCallbacks
26from twisted.trial.unittest import TestCase as TwistedTestCase30from twisted.trial.unittest import TestCase as TwistedTestCase
31from twisted.web import server, resource
2732
28from ubuntuone.storageprotocol import protocol_pb2, sharersp, delta, request33from ubuntuone.storageprotocol import protocol_pb2, sharersp, delta, request
29from ubuntuone.storageprotocol.client import (34from ubuntuone.storageprotocol.client import (
30 StorageClient, CreateUDF, ListVolumes, DeleteVolume, GetDelta, Unlink,35 StorageClient, CreateUDF, ListVolumes, DeleteVolume, GetDelta, Unlink,
31 Authenticate, MakeFile, MakeDir, PutContent, Move, BytesMessageProducer,36 Authenticate, MakeFile, MakeDir, PutContent, Move, BytesMessageProducer,
37 oauth, TwistedTimestampChecker, tx_timestamp_checker,
32)38)
33from ubuntuone.storageprotocol import volumes39from ubuntuone.storageprotocol import volumes
34from tests import test_delta_info40from tests import test_delta_info
@@ -725,7 +731,7 @@
725 actual_msg.get_delta.share)731 actual_msg.get_delta.share)
726732
727733
728class TestAuth(unittest.TestCase):734class TestAuth(TwistedTestCase):
729 """Tests the authentication request."""735 """Tests the authentication request."""
730736
731 def test_session_id(self):737 def test_session_id(self):
@@ -741,6 +747,142 @@
741 self.assert_(req.done.called)747 self.assert_(req.done.called)
742 self.assertEqual(req.session_id, SESSION_ID)748 self.assertEqual(req.session_id, SESSION_ID)
743749
750 def test_oauth_authenticate_uses_server_timestamp(self):
751 """The oauth authentication uses the server timestamp."""
752 fromcandt_call = []
753
754 fake_token = oauth.OAuthToken('token', 'token_secret')
755 fake_consumer = oauth.OAuthConsumer('consumer_key', 'consumer_secret')
756
757 fake_timestamp = object()
758 timestamp_d = Deferred()
759 self.patch(tx_timestamp_checker, "get_faithful_time",
760 lambda: timestamp_d)
761 original_fromcandt = oauth.OAuthRequest.from_consumer_and_token
762
763 @staticmethod
764 def fake_from_consumer_and_token(**kwargs):
765 """A fake from_consumer_and_token."""
766 fromcandt_call.append(kwargs)
767 return original_fromcandt(**kwargs)
768
769 self.patch(oauth.OAuthRequest, "from_consumer_and_token",
770 fake_from_consumer_and_token)
771 protocol = FakedProtocol()
772 protocol.oauth_authenticate(fake_consumer, fake_token)
773 self.assertEqual(len(fromcandt_call), 0)
774 timestamp_d.callback(fake_timestamp)
775 parameters = fromcandt_call[0]["parameters"]
776 self.assertEqual(parameters["oauth_timestamp"], fake_timestamp)
777
778
779class RootResource(resource.Resource):
780 """A root resource that logs the number of calls."""
781
782 isLeaf = True
783
784 def __init__(self, *args, **kwargs):
785 """Initialize this fake instance."""
786 self.count = 0
787 self.request_headers = []
788
789 def render_HEAD(self, request):
790 """Increase the counter on each render."""
791 self.request_headers.append(request.requestHeaders)
792 self.count += 1
793 return ""
794
795
796class MockWebServer(object):
797 """A mock webserver for testing."""
798
799 def __init__(self):
800 """Start up this instance."""
801 self.root = RootResource()
802 site = server.Site(self.root)
803 application = service.Application('web')
804 self.service_collection = service.IServiceCollection(application)
805 self.tcpserver = internet.TCPServer(0, site)
806 self.tcpserver.setServiceParent(self.service_collection)
807 self.service_collection.startService()
808
809 def get_url(self):
810 """Build the url for this mock server."""
811 port_num = self.tcpserver._port.getHost().port
812 return "http://localhost:%d/" % port_num
813
814 def stop(self):
815 """Shut it down."""
816 self.service_collection.stopService()
817
818
819class TimestampCheckerTestCase(TwistedTestCase):
820 """Tests for the timestamp checker."""
821
822 @inlineCallbacks
823 def setUp(self):
824 """Initialize a fake webserver."""
825 yield super(TwistedTestCase, self).setUp()
826 self.ws = MockWebServer()
827 self.addCleanup(self.ws.stop)
828 self.patch(TwistedTimestampChecker, "SERVER_URL", self.ws.get_url())
829
830 @inlineCallbacks
831 def test_returned_value_is_int(self):
832 """The returned value is an integer."""
833 checker = TwistedTimestampChecker()
834 t = yield checker.get_faithful_time()
835 self.assertEqual(type(t), int)
836
837 @inlineCallbacks
838 def test_first_call_does_head(self):
839 """The first call gets the clock from our web."""
840 checker = TwistedTimestampChecker()
841 yield checker.get_faithful_time()
842 self.assertEqual(self.ws.root.count, 1)
843
844 @inlineCallbacks
845 def test_second_call_is_cached(self):
846 """For the second call, the time is cached."""
847 checker = TwistedTimestampChecker()
848 yield checker.get_faithful_time()
849 yield checker.get_faithful_time()
850 self.assertEqual(self.ws.root.count, 1)
851
852 @inlineCallbacks
853 def test_after_timeout_cache_expires(self):
854 """After some time, the cache expires."""
855 fake_timestamp = 1
856 self.patch(time, "time", lambda: fake_timestamp)
857 checker = TwistedTimestampChecker()
858 yield checker.get_faithful_time()
859 fake_timestamp += TwistedTimestampChecker.CHECKING_INTERVAL
860 yield checker.get_faithful_time()
861 self.assertEqual(self.ws.root.count, 2)
862
863 @inlineCallbacks
864 def test_server_error_means_skew_not_updated(self):
865 """When server can't be reached, the skew is not updated."""
866 fake_timestamp = 1
867 self.patch(time, "time", lambda: fake_timestamp)
868 checker = TwistedTimestampChecker()
869 failing_get_server_time = lambda: defer.fail(FakedError())
870 self.patch(checker, "get_server_time", failing_get_server_time)
871 yield checker.get_faithful_time()
872 self.assertEqual(checker.skew, 0)
873 self.assertEqual(checker.next_check,
874 fake_timestamp + TwistedTimestampChecker.ERROR_INTERVAL)
875
876 @inlineCallbacks
877 def test_server_date_sends_nocache_headers(self):
878 """Getting the server date sends the no-cache headers."""
879 checker = TwistedTimestampChecker()
880 yield checker.get_server_date_header(self.ws.get_url())
881 assert len(self.ws.root.request_headers) == 1
882 headers = self.ws.root.request_headers[0]
883 result = headers.getRawHeaders("Cache-Control")
884 self.assertEqual(result, ["no-cache"])
885
744886
745class TestGenerationInRequests(unittest.TestCase):887class TestGenerationInRequests(unittest.TestCase):
746 """Base class for testing that actions that change the volume will888 """Base class for testing that actions that change the volume will
747889
=== modified file 'ubuntuone/storageprotocol/client.py'
--- ubuntuone/storageprotocol/client.py 2011-04-25 21:00:47 +0000
+++ ubuntuone/storageprotocol/client.py 2011-11-17 18:43:24 +0000
@@ -4,6 +4,7 @@
4# Author: Natalia B. Bidart <natalia.bidart@canonical.com>4# Author: Natalia B. Bidart <natalia.bidart@canonical.com>
5# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>5# Author: Guillermo Gonzalez <guillermo.gonzalez@canonical.com>
6# Author: Facundo Batista <facundo@canonical.com>6# Author: Facundo Batista <facundo@canonical.com>
7# Author: Alejandro J. Cura <alecu@canonical.com>
7#8#
8# Copyright 2009-2011 Canonical Ltd.9# Copyright 2009-2011 Canonical Ltd.
9#10#
@@ -30,13 +31,36 @@
30from twisted.internet.protocol import ClientFactory31from twisted.internet.protocol import ClientFactory
31from twisted.internet import reactor, defer32from twisted.internet import reactor, defer
32from twisted.python import log33from twisted.python import log
34from twisted.web import client
35from twisted.web.http_headers import Headers
3336
34from ubuntuone.storageprotocol import (37from ubuntuone.storageprotocol import (
35 protocol_pb2, request, sharersp, volumes, delta)38 delta,
39 protocol_pb2,
40 request,
41 sharersp,
42 utils,
43 volumes,
44)
3645
37log_debug = partial(log.msg, loglevel=logging.DEBUG)46log_debug = partial(log.msg, loglevel=logging.DEBUG)
3847
3948
49class TwistedTimestampChecker(utils.BaseTimestampChecker):
50 """A timestamp that's regularly checked with a server."""
51
52 @defer.inlineCallbacks
53 def get_server_date_header(self, server_url):
54 """Get the server date using twisted webclient."""
55 agent = client.Agent(reactor)
56 headers = Headers({'Cache-Control': ['no-cache']})
57 response = yield agent.request("HEAD", server_url, headers)
58 defer.returnValue(response.headers.getRawHeaders("Date")[0])
59
60
61tx_timestamp_checker = TwistedTimestampChecker()
62
63
40class StorageClient(request.RequestHandler):64class StorageClient(request.RequestHandler):
41 """A Basic Storage Protocol client."""65 """A Basic Storage Protocol client."""
4266
@@ -101,6 +125,7 @@
101 p.start()125 p.start()
102 return p.deferred126 return p.deferred
103127
128 @defer.inlineCallbacks
104 def oauth_authenticate(self, consumer, token):129 def oauth_authenticate(self, consumer, token):
105 """Authenticate to a server using the OAuth provider.130 """Authenticate to a server using the OAuth provider.
106131
@@ -113,9 +138,11 @@
113 object when completed.138 object when completed.
114139
115 """140 """
141 timestamp = yield tx_timestamp_checker.get_faithful_time()
116 req = oauth.OAuthRequest.from_consumer_and_token(142 req = oauth.OAuthRequest.from_consumer_and_token(
117 oauth_consumer=consumer,143 oauth_consumer=consumer,
118 token=token,144 token=token,
145 parameters={"oauth_timestamp": timestamp},
119 http_method="GET",146 http_method="GET",
120 http_url="storage://server")147 http_url="storage://server")
121 req.sign_request(148 req.sign_request(
@@ -126,7 +153,8 @@
126 (key, str(value)) for key, value in req.parameters.iteritems())153 (key, str(value)) for key, value in req.parameters.iteritems())
127 p = Authenticate(self, auth_parameters)154 p = Authenticate(self, auth_parameters)
128 p.start()155 p.start()
129 return p.deferred156 result = yield p.deferred
157 defer.returnValue(result)
130158
131 def handle_ROOT(self, message):159 def handle_ROOT(self, message):
132 """Handle incoming ROOT message.160 """Handle incoming ROOT message.
133161
=== added file 'ubuntuone/storageprotocol/utils.py'
--- ubuntuone/storageprotocol/utils.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/storageprotocol/utils.py 2011-11-17 18:43:24 +0000
@@ -0,0 +1,70 @@
1# ubuntuone.storageprotocol.utils - some storage protocol utils
2#
3# Author: Alejandro J. Cura <alecu@canonical.com>
4#
5# Copyright 2011 Canonical Ltd.
6#
7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU Affero General Public License version 3,
9# as published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU Affero General Public License for more details.
15#
16# You should have received a copy of the GNU Affero General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18"""Some storage protocol utils."""
19
20import logging
21import time
22
23from functools import partial
24
25from twisted.internet import defer
26from twisted.python import log
27from twisted.web import http
28
29log_debug = partial(log.msg, loglevel=logging.DEBUG)
30
31
32class BaseTimestampChecker(object):
33 """A timestamp that's regularly checked with a server."""
34
35 CHECKING_INTERVAL = 60 * 60 # in seconds
36 ERROR_INTERVAL = 30 # in seconds
37 SERVER_URL = "http://one.ubuntu.com/api/time"
38
39 def __init__(self):
40 """Initialize this instance."""
41 self.next_check = time.time()
42 self.skew = 0
43
44 def get_server_date_header(self, server_url):
45 """Return a deferred with the server time, using your web client."""
46 return defer.fail(NotImplementedError())
47
48 @defer.inlineCallbacks
49 def get_server_time(self):
50 """Get the time at the server."""
51 date_string = yield self.get_server_date_header(self.SERVER_URL)
52 timestamp = http.stringToDatetime(date_string)
53 defer.returnValue(timestamp)
54
55 @defer.inlineCallbacks
56 def get_faithful_time(self):
57 """Get an accurate timestamp."""
58 local_time = time.time()
59 if local_time >= self.next_check:
60 try:
61 server_time = yield self.get_server_time()
62 self.next_check = local_time + self.CHECKING_INTERVAL
63 self.skew = server_time - local_time
64 log_debug("Calculated server-local time skew:", self.skew)
65 except Exception, e:
66 log_debug("Error while verifying the server time skew:", e)
67 self.next_check = local_time + self.ERROR_INTERVAL
68 log_debug("Using corrected timestamp:",
69 http.datetimeToString(local_time + self.skew))
70 defer.returnValue(int(local_time + self.skew))

Subscribers

People subscribed via source and target branches