Merge lp:~cmiller/desktopcouch/pair-with-oauth into lp:desktopcouch

Proposed by Chad Miller
Status: Merged
Approved by: Chad Miller
Approved revision: 63
Merged at revision: not available
Proposed branch: lp:~cmiller/desktopcouch/pair-with-oauth
Merge into: lp:desktopcouch
Diff against target: None lines
To merge this branch: bzr merge lp:~cmiller/desktopcouch/pair-with-oauth
Reviewer Review Type Date Requested Status
Stuart Langridge (community) Approve
Vincenzo Di Somma (community) Approve
Review via email: mp+11576@code.launchpad.net

Commit message

Assume the couchdb is secure. Big change, that.

Finally, remove pairings when asked to.

Transmit hostid and oauth information to paired devices.

To post a comment you must log in.
Revision history for this message
Stuart Langridge (sil) wrote :

Starting up the pairing tool and then clicking "Listen for invitations" throws the following traceback;

Traceback (most recent call last):
  File "bin/desktopcouch-pair", line 606, in listen
    listening = Listening(couchdb_instance)
  File "bin/desktopcouch-pair", line 398, in __init__
    hostid, userid, listen_port = self.make_listener(couchdb_instance)
  File "bin/desktopcouch-pair", line 335, in make_listener
    couchdb_io.get_my_host_unique_id(create=True),
TypeError: get_my_host_unique_id() takes no arguments (1 given)

review: Needs Fixing
Revision history for this message
Chad Miller (cmiller) wrote :

> Starting up the pairing tool and then clicking "Listen for invitations" throws
> the following traceback;
>
> Traceback (most recent call last):
> File "bin/desktopcouch-pair", line 606, in listen
> listening = Listening(couchdb_instance)
> File "bin/desktopcouch-pair", line 398, in __init__
> hostid, userid, listen_port = self.make_listener(couchdb_instance)
> File "bin/desktopcouch-pair", line 335, in make_listener
> couchdb_io.get_my_host_unique_id(create=True),
> TypeError: get_my_host_unique_id() takes no arguments (1 given)

yeah, this relies on other code ("replicate-to-u1") landing first.

Revision history for this message
Vincenzo Di Somma (vds) wrote :

What ln 228 of the diff means? In any case I'm not a gtk expert but the code looks good and all the tests pass.

review: Approve
58. By Chad Miller

Merge from trunk.

59. By Chad Miller

Do not use the host id as the record id for couchdb, as there may be "deleted"
records with this id.

Make URI parameter optional in get_my_host_unique_id() .

60. By Chad Miller

Finish work on making pairing us oauth.

Rearrange the main widgets so they make more sense.

Revision history for this message
Stuart Langridge (sil) wrote :

Pulled and re-tested. Looks OK. Doesn't show existing paired local servers (should it? or is that in another branch?)

Revision history for this message
Chad Miller (cmiller) wrote :

> What ln 228 of the diff means? In any case I'm not a gtk expert but the code
> looks good and all the tests pass.

If you mean 'Narriative 'Alice'", it refers to a large comment near the top that describes the two roles that the single program plays.

61. By Chad Miller

Use the records API to get the server but do it left-handed, by getting a
database and then accessising the db object's private _server member.

Fix failing tests: Import the file that mutates the environment so that a
separate couchdb server is used for testing. Update put_dynamic_paired_host()
test to take its new hostname parameter.

62. By Chad Miller

Add a column for hostname in listening section, so a populated already-
paired section doesn't look so weird.

Add tests for unpairing.

Combine views of pairings.

Revision history for this message
Stuart Langridge (sil) wrote :

> Pulled and re-tested. Looks OK. Doesn't show existing paired local servers
> (should it? or is that in another branch?)

I am now ecstatically happy, yes I am.

review: Approve
63. By Chad Miller

Save a create-time value in pairing records, so we have something
useful to give to the user. Put this time in the second column of
the already-paired list.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/desktopcouch-pair'
2--- bin/desktopcouch-pair 2009-09-04 14:44:20 +0000
3+++ bin/desktopcouch-pair 2009-09-11 03:23:30 +0000
4@@ -64,9 +64,11 @@
5 import gobject
6 import pango
7
8+from desktopcouch.pair.couchdb_pairing import couchdb_io
9 from desktopcouch.pair.couchdb_pairing import network_io
10 from desktopcouch.pair.couchdb_pairing import dbus_io
11 from desktopcouch.pair import pairing_record_type
12+from desktopcouch import local_files
13
14 discovery_tool_version = "1"
15
16@@ -133,9 +135,9 @@
17 """The window is destroyed."""
18 self.inviter.close()
19
20- def auth_completed(self, remote_host, remote_port, remote_info):
21+ def auth_completed(self, remote_host, remote_id, remote_oauth):
22 """The auth stage is finished. Now pair with the remote host."""
23- pair_with_host(remote_host, remote_port, remote_info)
24+ pair_with_host(remote_host, remote_id, remote_oauth)
25 self.window.destroy()
26
27 def on_close(self):
28@@ -162,7 +164,7 @@
29
30 self.inviter = network_io.start_send_invitation(hostname, port,
31 self.auth_completed, self.secret_message, self.public_seed,
32- self.on_close)
33+ self.on_close, local_hostid, local_oauth)
34
35 top_vbox = gtk.VBox()
36 self.window.add(top_vbox)
37@@ -210,7 +212,8 @@
38 # FIXME this is unimplemented and unused.
39 self.destroy()
40
41- def __init__(self, remote_host, is_secret_valid, send_valid_key):
42+ def __init__(self, remote_host, is_secret_valid, send_valid_key,
43+ remote_hostid, remote_oauth):
44 self.logging = logging.getLogger(self.__class__.__name__)
45
46 self.is_secret_valid = is_secret_valid
47@@ -231,6 +234,8 @@
48 self.window.add(top_vbox)
49
50 self.remote_hostname = dbus_io.get_remote_hostname(remote_host)
51+ self.remote_hostid = remote_hostid
52+ self.remote_oauth = remote_oauth
53
54 description = gtk.Label(
55 _("To verify your pairing with %s, enter its secret.") %
56@@ -279,7 +284,7 @@
57
58 proposed_secret = unmap_easily_mistaken(self.entry_box.get_text())
59 if self.is_secret_valid(proposed_secret):
60- pair_with_host(self.remote_hostname, "(port)", "(info)")
61+ pair_with_host(self.remote_hostname, remote_hostid, remote_oauth)
62 self.send_valid_key(proposed_secret)
63 self.window.destroy()
64 else:
65@@ -306,13 +311,13 @@
66 self.listener.close()
67
68 def receive_invitation_challenge(self, remote_address, is_secret_valid,
69- send_secret):
70+ send_secret, remote_hostid, remote_oauth):
71 """When we receive an invitation, check its validity and if
72 it's what we expected, then continue and accept it."""
73
74 self.logging.warn("received invitation from %s", remote_address)
75 self.acceptor = AcceptInvitation(remote_address, is_secret_valid,
76- send_secret)
77+ send_secret, remote_hostid, remote_oauth)
78 self.acceptor.window.connect("destroy",
79 lambda *args: setattr(self, "acceptor", None) and False)
80
81@@ -326,7 +331,9 @@
82
83 self.listener = network_io.ListenForInvitations(
84 self.receive_invitation_challenge,
85- lambda: self.window.destroy())
86+ lambda: self.window.destroy(),
87+ couchdb_io.get_my_host_unique_id(create=True),
88+ local_files.get_oauth_tokens())
89
90 listen_port = self.listener.get_local_port()
91
92@@ -522,19 +529,6 @@
93 pair_with_cloud_service(service, hostname, port)
94 return
95
96-
97- self.logging.error("Local pairing is not yet supported. Aborting.")
98- fail_note = gtk.MessageDialog(
99- parent=self.window,
100- flags=gtk.DIALOG_DESTROY_WITH_PARENT,
101- buttons=gtk.BUTTONS_OK,
102- type=gtk.MESSAGE_ERROR,
103- message_format =_("Sorry, couchdb authentication is not yet enabled, so local pairing is not supported"))
104- fail_note.run()
105- fail_note.destroy()
106- return False
107-
108-
109 self.logging.info("connecting to %s:%s tcp to invite",
110 hostname, port)
111 if self.inviting != None:
112@@ -608,19 +602,6 @@
113 # assume local for this user. FIXME
114 pass
115
116-
117- self.logging.error("Local pairing is not yet supported. Aborting.")
118- fail_note = gtk.MessageDialog(
119- parent=self.window,
120- flags=gtk.DIALOG_DESTROY_WITH_PARENT,
121- buttons=gtk.BUTTONS_OK,
122- type=gtk.MESSAGE_ERROR,
123- message_format =_("Sorry, couchdb authentication is not yet enabled, so local pairing is not supported"))
124- fail_note.run()
125- fail_note.destroy()
126- return False
127-
128-
129 btn.set_sensitive(False)
130 listening = Listening(couchdb_instance)
131 listening.window.connect("destroy",
132@@ -682,14 +663,12 @@
133 self.window.show()
134
135
136-def pair_with_host(host, port, info):
137+def pair_with_host(hostname, hostid, oauth_data):
138 """We've verified all is correct and authorized, so now we pair
139 the databases."""
140- logging.info("verified host %s/%s/%s. Done!", host, port, info)
141+ logging.info("verified host %s/%s/%s. Done!", host, hostid, oauth)
142
143- # Some kind of two-phase commit would be nice here, before we say
144- # successful.
145- #couchdb_io.replicate_to(...)
146+ couchdb.put_dynamic_paired_host(hostname, hostid, oauth_data)
147
148 success_note = gtk.Dialog(title=_("Paired with %(host)s") % locals(),
149 parent=pick_or_listen.window,
150
151=== modified file 'desktopcouch/pair/couchdb_pairing/dbus_io.py'
152--- desktopcouch/pair/couchdb_pairing/dbus_io.py 2009-08-26 19:04:39 +0000
153+++ desktopcouch/pair/couchdb_pairing/dbus_io.py 2009-09-11 03:23:30 +0000
154@@ -210,7 +210,7 @@
155
156
157 def discover_services(add_commport_name_cb, del_commport_name_cb,
158- show_local=False):
159+ show_local=True):
160 """Start looking for services. Use two callbacks to handle seeing
161 new services and seeing services disappear."""
162
163
164=== modified file 'desktopcouch/pair/couchdb_pairing/network_io.py'
165--- desktopcouch/pair/couchdb_pairing/network_io.py 2009-07-28 03:43:02 +0000
166+++ desktopcouch/pair/couchdb_pairing/network_io.py 2009-09-11 03:23:30 +0000
167@@ -35,18 +35,74 @@
168 hash = hashlib.sha512
169
170
171+def dict_to_bytes(d):
172+ """Convert a dictionary of string key/values into a string."""
173+ parts = list()
174+
175+ for k, v in d.iteritems():
176+ l = len(k)
177+ parts.append(chr(l>>8))
178+ parts.append(chr(l&255))
179+ parts.append(k)
180+
181+ l = len(v)
182+ parts.append(chr(l>>8))
183+ parts.append(chr(l&255))
184+ parts.append(v)
185+
186+
187+ blob = "".join(parts)
188+ l = len(blob)
189+ blob_size = list()
190+ blob_size.append(chr(l>>24))
191+ blob_size.append(chr(l>>16&255))
192+ blob_size.append(chr(l>>8&255))
193+ blob_size.append(chr(l&255))
194+
195+ return "CMbydi0" + "".join(blob_size) + blob
196+
197+
198+def bytes_to_dict(b):
199+ """Convert a string from C{dict_to_bytes} back into a dictionary."""
200+ if b[:7] != "CMbydi0":
201+ raise ValueError("magic bytes missing. Invalid string. %r", b[:10])
202+ b = b[7:]
203+
204+ blob_size = 0
205+ for c in b[:4]:
206+ blob_size = (blob_size << 8) + ord(c)
207+
208+ blob = b[4:]
209+ if blob_size != len(blob):
210+ raise ValueError("bytes are corrupt; expected %d, got %d" % (blob_size,
211+ len(blob)))
212+
213+ d = {}
214+ blob_cursor = 0
215+
216+ while blob_cursor < blob_size:
217+ k_len = (ord(blob[blob_cursor+0])<<8) + ord(blob[blob_cursor+1])
218+ k = blob[blob_cursor+2:blob_cursor+2+k_len]
219+ blob_cursor += k_len + 2
220+ v_len = (ord(blob[blob_cursor+0])<<8) + ord(blob[blob_cursor+1])
221+ v = blob[blob_cursor+2:blob_cursor+2+v_len]
222+ blob_cursor += v_len + 2
223+ d[k] = v
224+ return d
225+
226+
227 class ListenForInvitations():
228 """Narrative "Alice".
229
230 This is the first half of a TCP listening socket. We spawn off
231 processors when we accept invitation-connections."""
232
233- def __init__(self, get_secret_from_user, on_close):
234+ def __init__(self, get_secret_from_user, on_close, hostid, oauth_data):
235 """Initialize."""
236 self.logging = logging.getLogger(self.__class__.__name__)
237
238 self.factory = ProcessAnInvitationFactory(get_secret_from_user,
239- on_close)
240+ on_close, hostid, oauth_data)
241 self.listening_port = reactor.listenTCP(0, self.factory)
242
243 def get_local_port(self):
244@@ -84,15 +140,19 @@
245 self.logging.debug("connection lost")
246 basic.LineReceiver.connectionLost(self, reason)
247
248- def lineReceived(self, message):
249+ def lineReceived(self, rich_message):
250 """Handler for receipt of a message from the Bob end."""
251- h = hash()
252- digest_nybble_count = h.digest_size * 2
253- self.expected_hash = message[0:digest_nybble_count]
254- self.public_seed = message[digest_nybble_count:]
255+ d = bytes_to_dict(rich_message)
256+
257+ self.expected_hash = d.pop("secret_message")
258+ self.public_seed = d.pop("public_seed")
259+ remote_hostid = d.pop("hostid")
260+ remote_oauth = d
261+
262 self.factory.get_secret_from_user(self.transport.getPeer().host,
263 self.check_secret_from_user,
264- self.send_secret_to_remote)
265+ self.send_secret_to_remote,
266+ remote_hostid, remote_oauth)
267
268 def send_secret_to_remote(self, secret_message):
269 """A callback for the invitation protocol to start a new phase
270@@ -101,12 +161,17 @@
271 h = hash()
272 h.update(self.public_seed)
273 h.update(secret_message)
274- self.sendLine(h.hexdigest())
275+ all_dict = dict()
276+ all_dict.update(self.factory.oauth_info)
277+ all_dict["hostid"] = self.factory.hostid
278+ all_dict["secret_message"] = h.hexdigest()
279+ self.sendLine(dict_to_bytes(all_dict))
280
281 def check_secret_from_user(self, secret_message):
282 """A callback for the invitation protocol to verify the secret
283 that the user gives, against the hash we received over the
284 network."""
285+
286 h = hash()
287 h.update(secret_message)
288 digest = h.hexdigest()
289@@ -115,7 +180,11 @@
290 h = hash()
291 h.update(self.public_seed)
292 h.update(secret_message)
293- self.sendLine(h.hexdigest())
294+ all_dict = dict()
295+ all_dict.update(self.factory.oauth_info)
296+ all_dict["hostid"] = self.factory.hostid
297+ all_dict["secret_message"] = h.hexdigest()
298+ self.sendLine(dict_to_bytes(all_dict))
299
300 self.logging.debug("User knew secret!")
301
302@@ -132,10 +201,12 @@
303
304 protocol = ProcessAnInvitationProtocol
305
306- def __init__(self, get_secret_from_user, on_close):
307+ def __init__(self, get_secret_from_user, on_close, hostid, oauth_info):
308 self.logging = logging.getLogger(self.__class__.__name__)
309 self.get_secret_from_user = get_secret_from_user
310 self.on_close = on_close
311+ self.hostid = hostid
312+ self.oauth_info = oauth_info
313
314
315 class SendInvitationProtocol(basic.LineReceiver):
316@@ -153,7 +224,11 @@
317
318 h = hash()
319 h.update(self.factory.secret_message)
320- self.sendLine(h.hexdigest() + self.factory.public_seed)
321+ d = dict(secret_message=h.hexdigest(),
322+ public_seed=self.factory.public_seed,
323+ hostid=self.factory.local_hostid)
324+ d.update(self.factory.local_oauth_info)
325+ self.sendLine(dict_to_bytes(d))
326
327 h = hash()
328 h.update(self.factory.public_seed)
329@@ -161,15 +236,19 @@
330 self.expected_hash_of_secret = h.hexdigest()
331
332
333- def lineReceived(self, message):
334+ def lineReceived(self, rich_message):
335 """Handler for receipt of a message from the Alice end."""
336+ d = bytes_to_dict(rich_message)
337+ message = d.pop("secret_message")
338+
339 if message == self.expected_hash_of_secret:
340 remote_host = self.transport.getPeer().host
341 try:
342 remote_hostname = get_remote_hostname(remote_host)
343 except dbus.exceptions.DBusException:
344 remote_hostname = None
345- self.factory.auth_complete_cb(remote_hostname, "(port)", "(info)")
346+ remote_hostid = d.pop("hostid")
347+ self.factory.auth_complete_cb(remote_hostname, remote_hostid, d)
348 self.transport.loseConnection()
349 else:
350 self.logging.warn("Expected %r from invitation.",
351@@ -188,12 +267,14 @@
352 protocol = SendInvitationProtocol
353
354 def __init__(self, auth_complete_cb, secret_message, public_seed,
355- on_close):
356+ on_close, local_hostid, local_oauth_info):
357 self.logging = logging.getLogger(self.__class__.__name__)
358 self.auth_complete_cb = auth_complete_cb
359 self.secret_message = secret_message
360 self.public_seed = public_seed
361 self.on_close = on_close
362+ self.local_hostid = local_hostid
363+ self.local_oauth_info = local_oauth_info
364 self.logging.debug("initialized")
365
366 def close(self):
367@@ -214,12 +295,12 @@
368
369
370 def start_send_invitation(host, port, auth_complete_cb, secret_message,
371- public_seed, on_close):
372+ public_seed, on_close, local_hostid, local_oauth):
373 """Instantiate the factory to hold configuration data about sending an
374 invitation and let the reactor add it to its event-handling loop by way of
375 starting a TCP connection."""
376 factory = SendInvitationFactory(auth_complete_cb, secret_message,
377- public_seed, on_close)
378+ public_seed, on_close, local_hostid, local_oauth)
379 reactor.connectTCP(host, port, factory)
380
381 return factory
382
383=== modified file 'desktopcouch/pair/tests/test_network_io.py'
384--- desktopcouch/pair/tests/test_network_io.py 2009-08-22 13:18:00 +0000
385+++ desktopcouch/pair/tests/test_network_io.py 2009-09-11 03:23:30 +0000
386@@ -26,10 +26,19 @@
387 start_send_invitation, ListenForInvitations)
388 from desktopcouch.pair.couchdb_pairing.dbus_io import get_local_hostname
389
390+import os
391 import unittest
392
393 local_hostname = ".".join(get_local_hostname())
394
395+a_hostid = "hostid a"
396+a_oauth_data = dict(token="atoken", token_secret="atokensecret",
397+ consumer="aconsumer", consumer_secret="aconsumersecret")
398+
399+b_hostid = "hostid b"
400+b_oauth_data = dict(token="btoken", token_secret="btokensecret",
401+ consumer="bconsumer", consumer_secret="bconsumersecret")
402+
403
404 class TestNetworkIO(unittest.TestCase):
405
406@@ -43,8 +52,13 @@
407
408 secret = "sekrit"
409
410- def listener_get_secret_from_user(sender, f, send_secret):
411+ def listener_get_secret_from_user(sender, f, send_secret, hostid,
412+ oauth_info):
413 """Get secret from user. Try several first."""
414+
415+ self.assertEquals("hostid b", hostid)
416+ self.assertTrue("token" in oauth_info)
417+
418 self.assertFalse(f("sek"))
419 self.assertFalse(f(""))
420 self.assertFalse(f(" fs"))
421@@ -60,15 +74,16 @@
422 def inviter_close_socket():
423 self._inviter_socket_state = "closed"
424
425- def inviter_complete_auth(a, b, c):
426+ def inviter_complete_auth(hostname, hostid, oauth_info):
427+ self.assertEquals("hostid a", hostid)
428+ self.assertTrue("token" in oauth_info)
429 self._inviter_auth_completed = True
430
431 def listener_complete_auth():
432 self._listener_auth_completed = True
433
434-
435 self.listener = ListenForInvitations(listener_get_secret_from_user,
436- listener_close_socket)
437+ listener_close_socket, a_hostid, a_oauth_data)
438 listener_port = self.listener.get_local_port()
439
440 def inviter_display_message(*args):
441@@ -76,7 +91,8 @@
442 logging.info("display message from inviter: %s", args)
443
444 self.inviter = start_send_invitation(local_hostname, listener_port,
445- inviter_complete_auth, secret, "seed", inviter_close_socket)
446+ inviter_complete_auth, secret, "seed", inviter_close_socket,
447+ b_hostid, b_oauth_data)
448
449 def exit_on_success():
450 if self._listener_auth_completed and self._inviter_auth_completed:

Subscribers

People subscribed via source and target branches