Merge lp:~cmiller/desktopcouch/pair-with-oauth into lp:desktopcouch
- pair-with-oauth
- Merge into trunk
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 |
Related bugs: |
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.
Description of the change
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/desktopcou
> listening = Listening(
> File "bin/desktopcou
> hostid, userid, listen_port = self.make_
> File "bin/desktopcou
> couchdb_
> TypeError: get_my_
yeah, this relies on other code ("replicate-to-u1") landing first.
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.
- 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.
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?)
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.
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.
- 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
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: |
Starting up the pairing tool and then clicking "Listen for invitations" throws the following traceback;
Traceback (most recent call last): ch-pair" , line 606, in listen couchdb_ instance) ch-pair" , line 398, in __init__ listener( couchdb_ instance) ch-pair" , line 335, in make_listener io.get_ my_host_ unique_ id(create= True), host_unique_ id() takes no arguments (1 given)
File "bin/desktopcou
listening = Listening(
File "bin/desktopcou
hostid, userid, listen_port = self.make_
File "bin/desktopcou
couchdb_
TypeError: get_my_