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

Proposed by Alejandro J. Cura
Status: Merged
Approved by: Alejandro J. Cura
Approved revision: 148
Merged at revision: 141
Proposed branch: lp:~alecu/ubuntuone-storage-protocol/timestamp-autofix
Merge into: lp:ubuntuone-storage-protocol
Diff against target: 333 lines (+242/-4)
3 files modified
tests/test_client.py (+142/-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
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Diego Sarmentero (community) Approve
Review via email: mp+78505@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.
148. By Alejandro J. Cura

fix punctuation

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

+1

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

Looks good, though I wonder if using time.time() in the algorithm for get_faithful_time will work as expected in Windows.

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-07-29 21:52:42 +0000
+++ tests/test_client.py 2011-10-06 21:31:26 +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,
@@ -20,16 +21,21 @@
20import StringIO21import StringIO
21import os22import os
22import sys23import sys
24import time
23import unittest25import unittest
24import uuid26import uuid
2527
26from twisted.internet.defer import Deferred28from twisted.application import internet, service
29from twisted.internet import defer
30from twisted.internet.defer import Deferred, inlineCallbacks
27from twisted.trial.unittest import TestCase as TwistedTestCase31from twisted.trial.unittest import TestCase as TwistedTestCase
32from twisted.web import server, resource
2833
29from ubuntuone.storageprotocol import protocol_pb2, sharersp, delta, request34from ubuntuone.storageprotocol import protocol_pb2, sharersp, delta, request
30from ubuntuone.storageprotocol.client import (35from ubuntuone.storageprotocol.client import (
31 StorageClient, CreateUDF, ListVolumes, DeleteVolume, GetDelta, Unlink,36 StorageClient, CreateUDF, ListVolumes, DeleteVolume, GetDelta, Unlink,
32 Authenticate, MakeFile, MakeDir, PutContent, Move, BytesMessageProducer,37 Authenticate, MakeFile, MakeDir, PutContent, Move, BytesMessageProducer,
38 oauth, TwistedTimestampChecker, tx_timestamp_checker,
33)39)
34from ubuntuone.storageprotocol import volumes40from ubuntuone.storageprotocol import volumes
35from tests import test_delta_info41from tests import test_delta_info
@@ -794,6 +800,140 @@
794 self.assertEqual(len(msg.auth_parameters), 1)800 self.assertEqual(len(msg.auth_parameters), 1)
795 self.assertEqual(len(msg.metadata), 0)801 self.assertEqual(len(msg.metadata), 0)
796802
803 def test_oauth_authenticate_uses_server_timestamp(self):
804 """The oauth authentication uses the server timestamp."""
805 fromcandt_call = []
806
807 fake_token = oauth.OAuthToken('token', 'token_secret')
808 fake_consumer = oauth.OAuthConsumer('consumer_key', 'consumer_secret')
809
810 fake_timestamp = object()
811 timestamp_d = Deferred()
812 self.patch(tx_timestamp_checker, "get_faithful_time",
813 lambda: timestamp_d)
814 original_fromcandt = oauth.OAuthRequest.from_consumer_and_token
815
816 @staticmethod
817 def fake_from_consumer_and_token(**kwargs):
818 """A fake from_consumer_and_token."""
819 fromcandt_call.append(kwargs)
820 return original_fromcandt(**kwargs)
821
822 self.patch(oauth.OAuthRequest, "from_consumer_and_token",
823 fake_from_consumer_and_token)
824 protocol = FakedProtocol()
825 protocol.oauth_authenticate(fake_consumer, fake_token)
826 self.assertEqual(len(fromcandt_call), 0)
827 timestamp_d.callback(fake_timestamp)
828 parameters = fromcandt_call[0]["parameters"]
829 self.assertEqual(parameters["oauth_timestamp"], fake_timestamp)
830
831
832class RootResource(resource.Resource):
833 """A root resource that logs the number of calls."""
834
835 isLeaf = True
836
837 def __init__(self, *args, **kwargs):
838 """Initialize this fake instance."""
839 self.count = 0
840 self.request_headers = []
841
842 def render_HEAD(self, request):
843 """Increase the counter on each render."""
844 self.request_headers.append(request.requestHeaders)
845 self.count += 1
846 return ""
847
848
849class MockWebServer(object):
850 """A mock webserver for testing."""
851
852 def __init__(self):
853 """Start up this instance."""
854 self.root = RootResource()
855 site = server.Site(self.root)
856 application = service.Application('web')
857 self.service_collection = service.IServiceCollection(application)
858 self.tcpserver = internet.TCPServer(0, site)
859 self.tcpserver.setServiceParent(self.service_collection)
860 self.service_collection.startService()
861
862 def get_url(self):
863 """Build the url for this mock server."""
864 port_num = self.tcpserver._port.getHost().port
865 return "http://localhost:%d/" % port_num
866
867 def stop(self):
868 """Shut it down."""
869 self.service_collection.stopService()
870
871
872class TimestampCheckerTestCase(TwistedTestCase):
873 """Tests for the timestamp checker."""
874
875 def setUp(self):
876 """Initialize a fake webserver."""
877 self.ws = MockWebServer()
878 self.addCleanup(self.ws.stop)
879 self.patch(TwistedTimestampChecker, "SERVER_URL", self.ws.get_url())
880
881 @inlineCallbacks
882 def test_returned_value_is_int(self):
883 """The returned value is an integer."""
884 checker = TwistedTimestampChecker()
885 t = yield checker.get_faithful_time()
886 self.assertEqual(type(t), int)
887
888 @inlineCallbacks
889 def test_first_call_does_head(self):
890 """The first call gets the clock from our web."""
891 checker = TwistedTimestampChecker()
892 yield checker.get_faithful_time()
893 self.assertEqual(self.ws.root.count, 1)
894
895 @inlineCallbacks
896 def test_second_call_is_cached(self):
897 """For the second call, the time is cached."""
898 checker = TwistedTimestampChecker()
899 yield checker.get_faithful_time()
900 yield checker.get_faithful_time()
901 self.assertEqual(self.ws.root.count, 1)
902
903 @inlineCallbacks
904 def test_after_timeout_cache_expires(self):
905 """After some time, the cache expires."""
906 fake_timestamp = 1
907 self.patch(time, "time", lambda: fake_timestamp)
908 checker = TwistedTimestampChecker()
909 yield checker.get_faithful_time()
910 fake_timestamp += TwistedTimestampChecker.CHECKING_INTERVAL
911 yield checker.get_faithful_time()
912 self.assertEqual(self.ws.root.count, 2)
913
914 @inlineCallbacks
915 def test_server_error_means_skew_not_updated(self):
916 """When server can't be reached, the skew is not updated."""
917 fake_timestamp = 1
918 self.patch(time, "time", lambda: fake_timestamp)
919 checker = TwistedTimestampChecker()
920 failing_get_server_time = lambda: defer.fail(FakedError())
921 self.patch(checker, "get_server_time", failing_get_server_time)
922 yield checker.get_faithful_time()
923 self.assertEqual(checker.skew, 0)
924 self.assertEqual(checker.next_check,
925 fake_timestamp + TwistedTimestampChecker.ERROR_INTERVAL)
926
927 @inlineCallbacks
928 def test_server_date_sends_nocache_headers(self):
929 """Getting the server date sends the no-cache headers."""
930 checker = TwistedTimestampChecker()
931 yield checker.get_server_date_header(self.ws.get_url())
932 assert len(self.ws.root.request_headers) == 1
933 headers = self.ws.root.request_headers[0]
934 result = headers.getRawHeaders("Cache-Control")
935 self.assertEqual(result, ["no-cache"])
936
797937
798class TestGenerationInRequests(RequestTestCase):938class TestGenerationInRequests(RequestTestCase):
799 """Base class for testing that actions that change the volume will939 """Base class for testing that actions that change the volume will
800940
=== modified file 'ubuntuone/storageprotocol/client.py'
--- ubuntuone/storageprotocol/client.py 2011-07-29 21:52:42 +0000
+++ ubuntuone/storageprotocol/client.py 2011-10-06 21:31:26 +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
@@ -102,6 +126,7 @@
102 p.start()126 p.start()
103 return p.deferred127 return p.deferred
104128
129 @defer.inlineCallbacks
105 def oauth_authenticate(self, consumer, token, metadata=None):130 def oauth_authenticate(self, consumer, token, metadata=None):
106 """Authenticate to a server using the OAuth provider.131 """Authenticate to a server using the OAuth provider.
107132
@@ -115,9 +140,11 @@
115 object when completed.140 object when completed.
116141
117 """142 """
143 timestamp = yield tx_timestamp_checker.get_faithful_time()
118 req = oauth.OAuthRequest.from_consumer_and_token(144 req = oauth.OAuthRequest.from_consumer_and_token(
119 oauth_consumer=consumer,145 oauth_consumer=consumer,
120 token=token,146 token=token,
147 parameters={"oauth_timestamp": timestamp},
121 http_method="GET",148 http_method="GET",
122 http_url="storage://server")149 http_url="storage://server")
123 req.sign_request(150 req.sign_request(
@@ -128,7 +155,8 @@
128 (key, str(value)) for key, value in req.parameters.iteritems())155 (key, str(value)) for key, value in req.parameters.iteritems())
129 p = Authenticate(self, auth_parameters, metadata=metadata)156 p = Authenticate(self, auth_parameters, metadata=metadata)
130 p.start()157 p.start()
131 return p.deferred158 result = yield p.deferred
159 defer.returnValue(result)
132160
133 def handle_ROOT(self, message):161 def handle_ROOT(self, message):
134 """Handle incoming ROOT message.162 """Handle incoming ROOT message.
135163
=== added file 'ubuntuone/storageprotocol/utils.py'
--- ubuntuone/storageprotocol/utils.py 1970-01-01 00:00:00 +0000
+++ ubuntuone/storageprotocol/utils.py 2011-10-06 21:31:26 +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/"
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