1
# Copyright 2009 Canonical Ltd.
3
# This file is part of desktopcouch.
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.
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.
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/>.
17
"""All inter-tool communication."""
22
from twisted.internet import reactor
23
from twisted.internet.protocol import ServerFactory, ReconnectingClientFactory
24
from twisted.protocols import basic
29
from desktopcouch.application.pair.couchdb_pairing.dbus_io import (
32
logging.exception("Can't import dbus_io, because avahi not installed?")
33
get_remote_hostname = lambda addr: None # pylint: disable=C0103
36
hash_fn = hashlib.sha512 # pylint: disable=C0103,E1101
39
def dict_to_bytes(dictionary):
40
"""Convert a dictionary of string key/values into a string."""
43
for key, value in dictionary.iteritems():
44
assert isinstance(key, str), key
46
parts.append(chr(length >> 8))
47
parts.append(chr(length & 255))
50
assert isinstance(value, str), value
52
parts.append(chr(length >> 8))
53
parts.append(chr(length & 255))
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))
64
return "CMbydi0" + "".join(blob_size) + blob
67
def bytes_to_dict(bytestring):
68
"""Convert a string from C{dict_to_bytes} back into a dictionary."""
69
if bytestring[:7] != "CMbydi0":
71
"magic bytes missing. Invalid string. %r", bytestring[:10])
72
bytestring = bytestring[7:]
75
for char in bytestring[:4]:
76
blob_size = (blob_size << 8) + ord(char)
79
if blob_size != len(blob):
80
raise ValueError("bytes are corrupt; expected %d, got %d" % (blob_size,
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
97
class ListenForInvitations():
100
This is the first half of a TCP listening socket. We spawn off
101
processors when we accept invitation-connections."""
103
def __init__(self, get_secret_from_user, on_close, hostid, oauth_data):
105
self.logging = logging.getLogger(self.__class__.__name__)
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
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."""
117
port = self.listening_port.getHost().port
118
self.logging.info("local port to receive invitations is %s", port)
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()
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".
133
Listen for messages, and when we receive one, call the display callback
134
function with the inviter details plus a key."""
138
self.logging = logging.getLogger(self.__class__.__name__)
139
self.expected_hash = None
140
self.public_seed = None
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)
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)
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)
158
self.expected_hash = d.pop("secret_message")
159
self.public_seed = d.pop("public_seed")
160
remote_hostid = d.pop("hostid")
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
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."""
174
hashed.update(self.public_seed)
175
hashed.update(secret_message)
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))
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
188
hashed.update(secret_message)
189
digest = hashed.hexdigest()
191
if digest == self.expected_hash:
193
hashed.update(self.public_seed)
194
hashed.update(secret_message)
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))
201
self.logging.debug("User knew secret!")
203
self.transport.loseConnection()
206
self.logging.info("User secret %r is wrong.", secret_message)
208
# pylint: enable=W0223
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."""
215
protocol = ProcessAnInvitationProtocol
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
222
self.oauth_info = oauth_info
225
class SendInvitationProtocol(basic.LineReceiver): # pylint: disable=W0223
226
"""Narrative "Bob"."""
230
self.logging = logging.getLogger(self.__class__.__name__)
231
self.logging.debug("initialized")
232
self.expected_hash_of_secret = None
234
# FIXME: remove camel case
235
def connectionMade(self): # pylint: disable=C0103
236
"""Fire when a connection is made to the listener. No obligation
238
self.logging.debug("connection made")
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))
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()
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")
259
if message == self.expected_hash_of_secret:
260
remote_host = self.transport.getPeer().host
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()
270
self.logging.warn("Expected %r from invitation.",
271
self.expected_hash_of_secret)
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)
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."""
284
protocol = SendInvitationProtocol
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")
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
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,
309
# pylint: enable=C0103
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)
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