~ubuntu-branches/ubuntu/precise/desktopcouch/precise

« back to all changes in this revision

Viewing changes to desktopcouch/application/pair/couchdb_pairing/network_io.py

  • Committer: Bazaar Package Importer
  • Author(s): Chad MILLER
  • Date: 2011-01-12 15:08:25 UTC
  • mfrom: (1.5.10 upstream)
  • Revision ID: james.westby@ubuntu.com-20110112150825-bzvn23kzufr0qdyb
Tags: 1.0.5-0ubuntu1
* New upstream release, skipping a few buggy releases.
* Split code into binary packages:
  - desktopcouch, configuration files and dependencies, but no code.
  - python-desktopcouch: transitional package
  - python-desktopcouch-application: local DB startup and discovery
  - python-desktopcouch-records: library for DB access anywhere
  - python-desktopcouch-recordtypes: support specific data structures
  - desktopcouch-ubuntuone, replication and pairing with cloud service
* Drop patch that some maverick apps incorrectly needed.
  patches/0-items-should-expose-private-data-for-now.patch
* Update package compatibility-version, 6 -> 7.
* Use newer debhelper and use python-support instead of python-central.
* Depend on contemporary python-couchdb, instead of ancient version.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2009 Canonical Ltd.
 
2
#
 
3
# This file is part of desktopcouch.
 
4
#
 
5
#  desktopcouch is free software: you can redistribute it and/or modify
 
6
# it under the terms of the GNU Lesser General Public License version 3
 
7
# as published by the Free Software Foundation.
 
8
#
 
9
# desktopcouch is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU Lesser General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU Lesser General Public License
 
15
# along with desktopcouch.  If not, see <http://www.gnu.org/licenses/>.
 
16
 
 
17
"""All inter-tool communication."""
 
18
 
 
19
import logging
 
20
import hashlib
 
21
 
 
22
from twisted.internet import reactor
 
23
from twisted.internet.protocol import ServerFactory, ReconnectingClientFactory
 
24
from twisted.protocols import basic
 
25
 
 
26
import dbus
 
27
 
 
28
try:
 
29
    from desktopcouch.application.pair.couchdb_pairing.dbus_io import (
 
30
        get_remote_hostname)
 
31
except ImportError:
 
32
    logging.exception("Can't import dbus_io, because avahi not installed?")
 
33
    get_remote_hostname = lambda addr: None  # pylint: disable=C0103
 
34
 
 
35
 
 
36
hash_fn = hashlib.sha512  # pylint: disable=C0103,E1101
 
37
 
 
38
 
 
39
def dict_to_bytes(dictionary):
 
40
    """Convert a dictionary of string key/values into a string."""
 
41
    parts = list()
 
42
 
 
43
    for key, value in dictionary.iteritems():
 
44
        assert isinstance(key, str), key
 
45
        length = len(key)
 
46
        parts.append(chr(length >> 8))
 
47
        parts.append(chr(length & 255))
 
48
        parts.append(key)
 
49
 
 
50
        assert isinstance(value, str), value
 
51
        length = len(value)
 
52
        parts.append(chr(length >> 8))
 
53
        parts.append(chr(length & 255))
 
54
        parts.append(value)
 
55
 
 
56
    blob = "".join(parts)
 
57
    length = len(blob)
 
58
    blob_size = list()
 
59
    blob_size.append(chr(length >> 24))
 
60
    blob_size.append(chr(length >> 16 & 255))
 
61
    blob_size.append(chr(length >> 8 & 255))
 
62
    blob_size.append(chr(length & 255))
 
63
 
 
64
    return "CMbydi0" + "".join(blob_size) + blob
 
65
 
 
66
 
 
67
def bytes_to_dict(bytestring):
 
68
    """Convert a string from C{dict_to_bytes} back into a dictionary."""
 
69
    if bytestring[:7] != "CMbydi0":
 
70
        raise ValueError(
 
71
            "magic bytes missing.  Invalid string.  %r", bytestring[:10])
 
72
    bytestring = bytestring[7:]
 
73
 
 
74
    blob_size = 0
 
75
    for char in bytestring[:4]:
 
76
        blob_size = (blob_size << 8) + ord(char)
 
77
 
 
78
    blob = bytestring[4:]
 
79
    if blob_size != len(blob):
 
80
        raise ValueError("bytes are corrupt; expected %d, got %d" % (blob_size,
 
81
            len(blob)))
 
82
 
 
83
    dictionary = {}
 
84
    blob_cursor = 0
 
85
 
 
86
    while blob_cursor < blob_size:
 
87
        k_len = (ord(blob[blob_cursor + 0]) << 8) + ord(blob[blob_cursor + 1])
 
88
        key = blob[blob_cursor + 2:blob_cursor + 2 + k_len]
 
89
        blob_cursor += k_len + 2
 
90
        v_len = (ord(blob[blob_cursor + 0]) << 8) + ord(blob[blob_cursor + 1])
 
91
        value = blob[blob_cursor + 2:blob_cursor + 2 + v_len]
 
92
        blob_cursor += v_len + 2
 
93
        dictionary[key] = value
 
94
    return dictionary
 
95
 
 
96
 
 
97
class ListenForInvitations():
 
98
    """Narrative "Alice".
 
99
 
 
100
    This is the first half of a TCP listening socket.  We spawn off
 
101
    processors when we accept invitation-connections."""
 
102
 
 
103
    def __init__(self, get_secret_from_user, on_close, hostid, oauth_data):
 
104
        """Initialize."""
 
105
        self.logging = logging.getLogger(self.__class__.__name__)
 
106
 
 
107
        self.factory = ProcessAnInvitationFactory(get_secret_from_user,
 
108
                on_close, hostid, oauth_data)
 
109
        # pylint: disable=E1101
 
110
        self.listening_port = reactor.listenTCP(0, self.factory)
 
111
        # pylint: enable=E1101
 
112
 
 
113
    def get_local_port(self):
 
114
        """We created a socket, and the caller needs to know what our port
 
115
        number is, so it can advertise it."""
 
116
 
 
117
        port = self.listening_port.getHost().port
 
118
        self.logging.info("local port to receive invitations is %s", port)
 
119
        return port
 
120
 
 
121
    def close(self):
 
122
        """Called from the UI when a window is destroyed and we do not need
 
123
        this connection any more."""
 
124
        self.listening_port.stopListening()
 
125
 
 
126
 
 
127
# FIXME: it looks like this class does not implement the interface
 
128
# correctly, or pylint is completely stupid.
 
129
# pylint: disable=W0223
 
130
class ProcessAnInvitationProtocol(basic.LineReceiver):
 
131
    """Narrative "Alice".
 
132
 
 
133
    Listen for messages, and when we receive one, call the display callback
 
134
    function with the inviter details plus a key."""
 
135
 
 
136
    def __init__(self):
 
137
        """Initialize."""
 
138
        self.logging = logging.getLogger(self.__class__.__name__)
 
139
        self.expected_hash = None
 
140
        self.public_seed = None
 
141
 
 
142
    # FIXME: remove camel case
 
143
    def connectionMade(self):           # pylint: disable=C0103
 
144
        """Called when a connection is made.  No obligation here."""
 
145
        basic.LineReceiver.connectionMade(self)
 
146
 
 
147
    # FIXME: remove camel case
 
148
    def connectionLost(self, reason):   # pylint: disable=W0222,C0103
 
149
        """Called when a connection is lost."""
 
150
        self.logging.debug("connection lost")
 
151
        basic.LineReceiver.connectionLost(self, reason)
 
152
 
 
153
    # FIXME: remove camel case
 
154
    def lineReceived(self, rich_message):  # pylint: disable=C0103
 
155
        """Handler for receipt of a message from the Bob end."""
 
156
        d = bytes_to_dict(rich_message)
 
157
 
 
158
        self.expected_hash = d.pop("secret_message")
 
159
        self.public_seed = d.pop("public_seed")
 
160
        remote_hostid = d.pop("hostid")
 
161
        remote_oauth = d
 
162
        # pylint: disable=E1101
 
163
        self.factory.get_secret_from_user(self.transport.getPeer().host,
 
164
                self.check_secret_from_user,
 
165
                self.send_secret_to_remote,
 
166
                remote_hostid, remote_oauth)
 
167
        # pylint: enable=E1101
 
168
 
 
169
    def send_secret_to_remote(self, secret_message):
 
170
        """A callback for the invitation protocol to start a new phase
 
171
        involving the other end getting the hash-digest of the public
 
172
        seed and a secret we receive as a parameter."""
 
173
        hashed = hash_fn()
 
174
        hashed.update(self.public_seed)
 
175
        hashed.update(secret_message)
 
176
        all_dict = dict()
 
177
        all_dict.update(self.factory.oauth_info)  # pylint: disable=E1101
 
178
        all_dict["hostid"] = self.factory.hostid  # pylint: disable=E1101
 
179
        all_dict["secret_message"] = hashed.hexdigest()
 
180
        self.sendLine(dict_to_bytes(all_dict))
 
181
 
 
182
    def check_secret_from_user(self, secret_message):
 
183
        """A callback for the invitation protocol to verify the secret
 
184
        that the user gives, against the hash we received over the
 
185
        network."""
 
186
 
 
187
        hashed = hash_fn()
 
188
        hashed.update(secret_message)
 
189
        digest = hashed.hexdigest()
 
190
 
 
191
        if digest == self.expected_hash:
 
192
            hashed = hash_fn()
 
193
            hashed.update(self.public_seed)
 
194
            hashed.update(secret_message)
 
195
            all_dict = dict()
 
196
            all_dict.update(self.factory.oauth_info)  # pylint: disable=E1101
 
197
            all_dict["hostid"] = self.factory.hostid  # pylint: disable=E1101
 
198
            all_dict["secret_message"] = hashed.hexdigest()
 
199
            self.sendLine(dict_to_bytes(all_dict))
 
200
 
 
201
            self.logging.debug("User knew secret!")
 
202
 
 
203
            self.transport.loseConnection()
 
204
            return True
 
205
 
 
206
        self.logging.info("User secret %r is wrong.", secret_message)
 
207
        return False
 
208
# pylint: enable=W0223
 
209
 
 
210
 
 
211
class ProcessAnInvitationFactory(ServerFactory):
 
212
    """Hold configuration values for all the connections, and fire off a
 
213
    protocol to handle the data sent and received."""
 
214
 
 
215
    protocol = ProcessAnInvitationProtocol
 
216
 
 
217
    def __init__(self, get_secret_from_user, on_close, hostid, oauth_info):
 
218
        self.logging = logging.getLogger(self.__class__.__name__)
 
219
        self.get_secret_from_user = get_secret_from_user
 
220
        self.on_close = on_close
 
221
        self.hostid = hostid
 
222
        self.oauth_info = oauth_info
 
223
 
 
224
 
 
225
class SendInvitationProtocol(basic.LineReceiver):  # pylint: disable=W0223
 
226
    """Narrative "Bob"."""
 
227
 
 
228
    def __init__(self):
 
229
        """Initialize."""
 
230
        self.logging = logging.getLogger(self.__class__.__name__)
 
231
        self.logging.debug("initialized")
 
232
        self.expected_hash_of_secret = None
 
233
 
 
234
    # FIXME: remove camel case
 
235
    def connectionMade(self):           # pylint: disable=C0103
 
236
        """Fire when a connection is made to the listener.  No obligation
 
237
        here."""
 
238
        self.logging.debug("connection made")
 
239
 
 
240
        hashed = hash_fn()
 
241
        hashed.update(self.factory.secret_message)  # pylint: disable=E1101
 
242
        d = dict(secret_message=hashed.hexdigest(),
 
243
                public_seed=self.factory.public_seed,  # pylint: disable=E1101
 
244
                hostid=self.factory.local_hostid)      # pylint: disable=E1101
 
245
        d.update(self.factory.local_oauth_info)        # pylint: disable=E1101
 
246
        self.sendLine(dict_to_bytes(d))
 
247
 
 
248
        hashed = hash_fn()
 
249
        hashed.update(self.factory.public_seed)  # pylint: disable=E1101
 
250
        hashed.update(self.factory.secret_message)  # pylint: disable=E1101
 
251
        self.expected_hash_of_secret = hashed.hexdigest()
 
252
 
 
253
    # FIXME: remove camel case
 
254
    def lineReceived(self, rich_message):  # pylint: disable=C0103
 
255
        """Handler for receipt of a message from the Alice end."""
 
256
        d = bytes_to_dict(rich_message)
 
257
        message = d.pop("secret_message")
 
258
 
 
259
        if message == self.expected_hash_of_secret:
 
260
            remote_host = self.transport.getPeer().host
 
261
            try:
 
262
                remote_hostname = get_remote_hostname(remote_host)
 
263
            except dbus.exceptions.DBusException:
 
264
                remote_hostname = None
 
265
            remote_hostid = d.pop("hostid")
 
266
            self.factory.auth_complete_cb(  # pylint: disable=E1101
 
267
                remote_hostname, remote_hostid, d)
 
268
            self.transport.loseConnection()
 
269
        else:
 
270
            self.logging.warn("Expected %r from invitation.",
 
271
                    self.expected_hash_of_secret)
 
272
 
 
273
    # FIXME: remove camel case
 
274
    def connectionLost(self, reason):   # pylint: disable=W0222,C0103
 
275
        """When a connected socked is broken, this is fired."""
 
276
        self.logging.info("connection lost.")
 
277
        basic.LineReceiver.connectionLost(self, reason)
 
278
 
 
279
 
 
280
class SendInvitationFactory(ReconnectingClientFactory):
 
281
    """Hold configuration values for all the connections, and fire off a
 
282
    protocol to handle the data sent and received."""
 
283
 
 
284
    protocol = SendInvitationProtocol
 
285
 
 
286
    def __init__(self, auth_complete_cb, secret_message, public_seed,
 
287
            on_close, local_hostid, local_oauth_info):
 
288
        self.logging = logging.getLogger(self.__class__.__name__)
 
289
        self.auth_complete_cb = auth_complete_cb
 
290
        self.secret_message = secret_message
 
291
        self.public_seed = public_seed
 
292
        self.on_close = on_close
 
293
        self.local_hostid = local_hostid
 
294
        self.local_oauth_info = local_oauth_info
 
295
        self.logging.debug("initialized")
 
296
 
 
297
    def close(self):
 
298
        """Called from the UI when a window is destroyed and we do not need
 
299
        this connection any more."""
 
300
        self.logging.warn("close not handled properly")  # FIXME
 
301
 
 
302
    # FIXME: remove camel case
 
303
    # pylint: disable=C0103
 
304
    def clientConnectionFailed(self, connector, reason):
 
305
        """When we fail to connect to the listener, this is fired."""
 
306
        self.logging.warn("connect failed. %s", reason)
 
307
        ReconnectingClientFactory.clientConnectionFailed(self, connector,
 
308
                reason)
 
309
    # pylint: enable=C0103
 
310
 
 
311
    # FIXME: remove camel case
 
312
    def clientConnectionLost(self, connector, reason):  # pylint: disable=C0103
 
313
        """When a connected socked is broken, this is fired."""
 
314
        self.logging.info("connection lost. %s", reason)
 
315
        ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
 
316
 
 
317
 
 
318
def start_send_invitation(host, port, auth_complete_cb, secret_message,
 
319
        public_seed, on_close, local_hostid, local_oauth):
 
320
    """Instantiate the factory to hold configuration data about sending an
 
321
    invitation and let the reactor add it to its event-handling loop by way of
 
322
    starting a TCP connection."""
 
323
    factory = SendInvitationFactory(auth_complete_cb, secret_message,
 
324
            public_seed, on_close, local_hostid, local_oauth)
 
325
    reactor.connectTCP(host, port, factory)  # pylint: disable=E1101
 
326
 
 
327
    return factory