Merge lp:~jderose/dmedia/secure-peering into lp:dmedia

Proposed by Jason Gerard DeRose
Status: Merged
Merged at revision: 467
Proposed branch: lp:~jderose/dmedia/secure-peering
Merge into: lp:dmedia
Diff against target: 3221 lines (+2345/-120)
17 files modified
dmedia-service (+1/-1)
dmedia/gtk/peering.py (+100/-0)
dmedia/gtk/ui/client.html (+166/-0)
dmedia/gtk/ui/novacut.svg (+133/-0)
dmedia/gtk/ui/peering.css (+124/-0)
dmedia/gtk/ui/peering.js (+183/-0)
dmedia/gtk/ui/server.html (+37/-0)
dmedia/gtk/ui/sync.svg (+119/-0)
dmedia/httpd.py (+55/-6)
dmedia/peering.py (+389/-8)
dmedia/service/avahi.py (+7/-5)
dmedia/service/peers.py (+347/-49)
dmedia/tests/test_peering.py (+550/-1)
run-browse.py (+0/-24)
run-publish.py (+0/-26)
setup.py (+1/-0)
share/indicator-novacut.svg (+133/-0)
To merge this branch: bzr merge lp:~jderose/dmedia/secure-peering
Reviewer Review Type Date Requested Status
James Raymond Approve
Review via email: mp+128820@code.launchpad.net

Description of the change

Please see the related bug for details:

https://bugs.launchpad.net/dmedia/+bug/1064674

One note on something that I plan to change soon in a later merge: I want to incorporate proof of having the secret in the POST /csr request, and in the response containing the cert. I feel this is important in case somehow this step was reachable without passing the challenge response. Similar to the existing challenge-response, it will be tied to the public key hashes in question, and also to the data payload in the CSR and cert.

To post a comment you must log in.
Revision history for this message
James Raymond (jamesmr) wrote :

Phew, that was a long read!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'dmedia-service'
2--- dmedia-service 2012-10-04 20:25:21 +0000
3+++ dmedia-service 2012-10-09 20:57:30 +0000
4@@ -90,7 +90,7 @@
5 self.core.init_default_store()
6 if self.core.local.get('default_store') is None:
7 self.core.set_default_store('shared')
8- self.env_s = dumps(self.core.env)
9+ self.env_s = dumps(self.core.env, pretty=True)
10 self.ssl_config = self.couch.get_ssl_config()
11
12 def start_httpd(self):
13
14=== added file 'dmedia/gtk/peering.py'
15--- dmedia/gtk/peering.py 1970-01-01 00:00:00 +0000
16+++ dmedia/gtk/peering.py 2012-10-09 20:57:30 +0000
17@@ -0,0 +1,100 @@
18+from os import path
19+import json
20+
21+from gi.repository import GObject, Gtk, WebKit
22+
23+
24+ui = path.join(path.dirname(path.abspath(__file__)), 'ui')
25+assert path.isdir(ui)
26+
27+
28+class Hub(GObject.GObject):
29+ def __init__(self, view):
30+ super().__init__()
31+ self._view = view
32+ view.connect('notify::title', self._on_notify_title)
33+
34+ def _on_notify_title(self, view, notify):
35+ title = view.get_property('title')
36+ if title is None:
37+ return
38+ obj = json.loads(title)
39+ self.emit(obj['signal'], *obj['args'])
40+
41+ def send(self, signal, *args):
42+ """
43+ Emit a signal by calling the JavaScript Signal.recv() function.
44+ """
45+ script = 'Hub.recv({!r})'.format(
46+ json.dumps({'signal': signal, 'args': args})
47+ )
48+ self._view.execute_script(script)
49+ self.emit(signal, *args)
50+
51+
52+def iter_gsignals(signals):
53+ assert isinstance(signals, dict)
54+ for (name, argnames) in signals.items():
55+ assert isinstance(argnames, list)
56+ args = [GObject.TYPE_PYOBJECT for argname in argnames]
57+ yield (name, (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, args))
58+
59+
60+def hub_factory(signals):
61+ if signals:
62+ class FactoryHub(Hub):
63+ __gsignals__ = dict(iter_gsignals(signals))
64+ return FactoryHub
65+ return Hub
66+
67+
68+class BaseUI:
69+ inspector = None
70+ signals = None
71+ title = 'Novacut' # Default Gtk.Window title
72+ page = 'peering.html' # Default page to load once CouchDB is available
73+ width = 960 # Default Gtk.Window width
74+ height = 540 # Default Gtk.Window height
75+
76+ def __init__(self):
77+ self.build_window()
78+ self.hub = hub_factory(self.signals)(self.view)
79+ self.connect_hub_signals(self.hub)
80+
81+ def show(self):
82+ self.window.show_all()
83+
84+ def run(self):
85+ self.window.connect('destroy', self.quit)
86+ self.window.show_all()
87+ Gtk.main()
88+
89+ def quit(self, *args):
90+ Gtk.main_quit()
91+
92+ def connect_hub_signals(self, hub):
93+ pass
94+
95+ def build_window(self):
96+ self.window = Gtk.Window()
97+ self.window.set_position(Gtk.WindowPosition.CENTER)
98+ self.window.set_default_size(self.width, self.height)
99+ self.window.set_title(self.title)
100+ self.vpaned = Gtk.VPaned()
101+ self.window.add(self.vpaned)
102+ self.view = WebKit.WebView()
103+ self.view.get_settings().set_property('enable-developer-extras', True)
104+ inspector = self.view.get_inspector()
105+ inspector.connect('inspect-web-view', self.on_inspect)
106+ self.view.load_uri('file://' + path.join(ui, self.page))
107+ self.vpaned.pack1(self.view, True, True)
108+
109+ def on_inspect(self, *args):
110+ assert self.inspector is None
111+ self.inspector = WebKit.WebView()
112+ pos = self.window.get_allocated_height() * 2 // 3
113+ self.vpaned.set_position(pos)
114+ self.vpaned.pack2(self.inspector, True, True)
115+ self.inspector.show_all()
116+ return self.inspector
117+
118
119=== added directory 'dmedia/gtk/ui'
120=== added file 'dmedia/gtk/ui/client.html'
121--- dmedia/gtk/ui/client.html 1970-01-01 00:00:00 +0000
122+++ dmedia/gtk/ui/client.html 2012-10-09 20:57:30 +0000
123@@ -0,0 +1,166 @@
124+<!DOCTYPE html>
125+<html>
126+<head>
127+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
128+<link rel="stylesheet" href="peering.css" />
129+<script src="peering.js"></script>
130+<script>
131+
132+"use strict";
133+
134+var B32ALPHABET = '234567ABCDEFGHIJKLMNOPQRSTUVWXYZ';
135+
136+var UI = {
137+ on_load: function() {
138+ UI.input = $('input');
139+ UI.input.oninput = UI.on_input;
140+ UI.form = $('form');
141+ UI.form.onsubmit = UI.on_submit;
142+ UI.show('screen1');
143+ },
144+
145+ on_input: function(event) {
146+ var orig = UI.input.value.toUpperCase();
147+ var value = '';
148+ var b32, i;
149+ for (i=0; i<orig.length; i++) {
150+ b32 = orig[i];
151+ if (B32ALPHABET.indexOf(b32) >= 0 ) {
152+ value += b32;
153+ }
154+ }
155+ UI.input.value = value;
156+ if (UI.input.value.length == 8) {
157+ $('finish').classList.remove('hidden');
158+ }
159+ else {
160+ $('finish').classList.add('hidden');
161+ }
162+ },
163+
164+ on_submit: function(event) {
165+ event.preventDefault();
166+ event.stopPropagation();
167+ UI.have_secret();
168+ },
169+
170+ have_secret: function() {
171+ if (UI.input.value.length == 8 && !UI.input.disabled) {
172+ UI.input.disabled = true;
173+ Hub.send('have_secret', UI.input.value);
174+ }
175+ },
176+
177+ show: function(id) {
178+ $hide(UI.current);
179+ UI.current = $show(id);
180+ },
181+}
182+
183+
184+window.onload = UI.on_load;
185+
186+
187+Hub.connect('show_screen2a',
188+ function() {
189+ UI.show('screen2a');
190+ $('logo').classList.add('spinleft');
191+ }
192+);
193+
194+Hub.connect('show_screen2b',
195+ function() {
196+ UI.show('screen2b');
197+ $('logo').classList.add('spinright');
198+ }
199+);
200+
201+Hub.connect('show_screen3b',
202+ function() {
203+ $hide('logo');
204+ UI.input.value = '';
205+ UI.show('screen3b');
206+ }
207+);
208+
209+Hub.connect('spin_orb',
210+ function() {
211+ $('logo2').classList.add('spinright');
212+ }
213+);
214+
215+Hub.connect('set_message',
216+ function(message) {
217+ $('message').textContent = message;
218+ }
219+);
220+
221+Hub.connect('response',
222+ function(success) {
223+ if (!success) {
224+ UI.input.value = '';
225+ UI.input.disabled = false;
226+ UI.input.focus();
227+ $('finish').classList.add('hidden');
228+ }
229+ else {
230+ $hide('finish');
231+ $show('logo2');
232+ }
233+ }
234+);
235+
236+</script>
237+</head>
238+<body>
239+
240+<img src="novacut.svg" id="logo">
241+
242+<div id="screen1" class="hide">
243+ <div id="first" onclick="Hub.send('first_time')">
244+ <p class="top">
245+ This is my first time using Novacut
246+ </p>
247+ <p>
248+ (You can add more devices later)
249+ </p>
250+ </div>
251+
252+ <div id="sync" onclick="Hub.send('already_using')">
253+ <p class="top">
254+ I'm already using Novacut
255+ </p>
256+ <p>
257+ Sync with my other devices!
258+ </p>
259+ </div>
260+</div>
261+
262+<div id="screen2a" class="hide">
263+ <p class="gen">
264+ Pretty words here
265+ </p>
266+</div>
267+
268+<div id="screen2b" class="hide">
269+ <p class="gen">
270+ Accept the peering offer on your other device
271+ </p>
272+</div>
273+
274+<div id="screen3b" class="hide">
275+ <p class="gen">
276+ Enter your secret code:
277+ </p>
278+ <div class="secret">
279+ <form id="form">
280+ <input id="input" type="text" maxlength="8" size="8" autofocus="1"></input>
281+ </form>
282+ </div>
283+ <p id="message"></p>
284+ <img src="sync.svg" id="finish" class="hidden" onclick="UI.have_secret()">
285+ <img src="novacut.svg" id="logo2" class="hide">
286+</div>
287+
288+</body>
289+</html>
290
291=== added file 'dmedia/gtk/ui/novacut.svg'
292--- dmedia/gtk/ui/novacut.svg 1970-01-01 00:00:00 +0000
293+++ dmedia/gtk/ui/novacut.svg 2012-10-09 20:57:30 +0000
294@@ -0,0 +1,133 @@
295+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
296+<!-- Created with Inkscape (http://www.inkscape.org/) -->
297+
298+<svg
299+ xmlns:dc="http://purl.org/dc/elements/1.1/"
300+ xmlns:cc="http://creativecommons.org/ns#"
301+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
302+ xmlns:svg="http://www.w3.org/2000/svg"
303+ xmlns="http://www.w3.org/2000/svg"
304+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
305+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
306+ width="512"
307+ height="512"
308+ id="svg4778"
309+ version="1.1"
310+ inkscape:version="0.48.1 r9760"
311+ sodipodi:docname="novacut-solo-brandmark_PINK_FINAL-SVG.svg"
312+ inkscape:export-filename="/home/izo/Pictures/Client_Work/Novacut/Final-Artwork/web/PNG/novacut-solo-brandmark_PINK_FINAL-PNG-300dpi.png"
313+ inkscape:export-xdpi="300.05859"
314+ inkscape:export-ydpi="300.05859">
315+ <defs
316+ id="defs4780">
317+ <inkscape:path-effect
318+ effect="spiro"
319+ id="path-effect5868"
320+ is_visible="true" />
321+ </defs>
322+ <sodipodi:namedview
323+ id="base"
324+ pagecolor="#ffffff"
325+ bordercolor="#666666"
326+ borderopacity="1.0"
327+ inkscape:pageopacity="0.0"
328+ inkscape:pageshadow="2"
329+ inkscape:zoom="1"
330+ inkscape:cx="233.49618"
331+ inkscape:cy="280"
332+ inkscape:current-layer="layer1"
333+ inkscape:document-units="px"
334+ showgrid="false"
335+ inkscape:window-width="1614"
336+ inkscape:window-height="1026"
337+ inkscape:window-x="66"
338+ inkscape:window-y="24"
339+ inkscape:window-maximized="1"
340+ showguides="false"
341+ inkscape:guide-bbox="true">
342+ <inkscape:grid
343+ type="xygrid"
344+ id="grid2994"
345+ empspacing="4"
346+ visible="true"
347+ enabled="true"
348+ snapvisiblegridlinesonly="true" />
349+ <sodipodi:guide
350+ orientation="1,0"
351+ position="256,88"
352+ id="guide3002" />
353+ <sodipodi:guide
354+ orientation="0,1"
355+ position="592,256"
356+ id="guide3004" />
357+ <sodipodi:guide
358+ position="0,0"
359+ orientation="0,512"
360+ id="guide3006" />
361+ <sodipodi:guide
362+ position="512,0"
363+ orientation="-512,0"
364+ id="guide3008" />
365+ <sodipodi:guide
366+ position="512,512"
367+ orientation="0,-512"
368+ id="guide3010" />
369+ <sodipodi:guide
370+ position="0,512"
371+ orientation="512,0"
372+ id="guide3012" />
373+ </sodipodi:namedview>
374+ <metadata
375+ id="metadata4783">
376+ <rdf:RDF>
377+ <cc:Work
378+ rdf:about="">
379+ <dc:format>image/svg+xml</dc:format>
380+ <dc:type
381+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
382+ <dc:title></dc:title>
383+ </cc:Work>
384+ </rdf:RDF>
385+ </metadata>
386+ <g
387+ id="layer1"
388+ inkscape:label="Layer 1"
389+ inkscape:groupmode="layer"
390+ transform="translate(0,32)">
391+ <path
392+ transform="matrix(1.0666667,0,0,1.0666667,-85.333334,-32.000001)"
393+ d="m 545,240 a 225,225 0 1 1 -450,0 225,225 0 1 1 450,0 z"
394+ sodipodi:ry="225"
395+ sodipodi:rx="225"
396+ sodipodi:cy="240"
397+ sodipodi:cx="320"
398+ id="path5924"
399+ style="fill:#e81f3b;fill-opacity:1;stroke:none"
400+ sodipodi:type="arc" />
401+ <path
402+ id="path4023"
403+ d="m 157.74011,106.26326 0,233.73017 48.11687,0 0,-156.48267 0.68543,0 37.83549,60.93432 0,-81.0173 -35.57359,-57.16452 -51.0642,0 z m 149.28569,0 0,156.82539 -0.68543,0 -37.8355,-60.86578 0,81.0173 35.23088,56.75326 51.40692,0 0,-233.73017 -48.11687,0 z"
404+ style="font-size:144px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Helvetica Neue LT Com;-inkscape-font-specification:Helvetica Neue LT Com Bold"
405+ inkscape:connector-curvature="0" />
406+ <path
407+ sodipodi:type="arc"
408+ style="fill:#ffffff;fill-opacity:1;stroke:none"
409+ id="path4025"
410+ sodipodi:cx="836.50732"
411+ sodipodi:cy="230.95239"
412+ sodipodi:rx="13.435029"
413+ sodipodi:ry="13.435029"
414+ d="m 849.94235,230.95239 a 13.435029,13.435029 0 1 1 -26.87005,0 13.435029,13.435029 0 1 1 26.87005,0 z"
415+ transform="matrix(2.193362,0,0,2.193362,-1651.9421,-440.84345)" />
416+ <path
417+ transform="matrix(2.193362,0,0,2.193362,-1505.7305,-124.45147)"
418+ d="m 849.94235,230.95239 a 13.435029,13.435029 0 1 1 -26.87005,0 13.435029,13.435029 0 1 1 26.87005,0 z"
419+ sodipodi:ry="13.435029"
420+ sodipodi:rx="13.435029"
421+ sodipodi:cy="230.95239"
422+ sodipodi:cx="836.50732"
423+ id="path4027"
424+ style="fill:#ffffff;fill-opacity:1;stroke:none"
425+ sodipodi:type="arc" />
426+ </g>
427+</svg>
428
429=== added file 'dmedia/gtk/ui/peering.css'
430--- dmedia/gtk/ui/peering.css 1970-01-01 00:00:00 +0000
431+++ dmedia/gtk/ui/peering.css 2012-10-09 20:57:30 +0000
432@@ -0,0 +1,124 @@
433+.hide {
434+ display: none !important;
435+}
436+
437+body {
438+ font-family: Lato;
439+ font-size: 21px;
440+ background-color: #301438;
441+ color: #fff;
442+ text-align: center;
443+}
444+
445+code, input {
446+ font-family: "Ubuntu Mono";
447+ font-size: 60px;
448+ font-weight: bold;
449+ color: #333;
450+ text-shadow: 0px 0px 3px #e81f3b;
451+ letter-spacing: 0.15em;
452+ line-height: 1.4em;
453+ margin: 0.4em;
454+}
455+
456+input {
457+ width: 5.4em;
458+}
459+
460+.secret {
461+ text-align: center;
462+ background-color: #fff;
463+ text-align: center;
464+ box-shadow: 2px 2px 6px #000;
465+ border-radius: 10px;
466+ cursor: pointer;
467+ -webkit-user-select: none;
468+ display: inline-block;
469+}
470+
471+#first, #sync {
472+ position: fixed;
473+ width: 440px;
474+ background-color: #fff;
475+ text-align: center;
476+ box-shadow: 2px 2px 5px #000;
477+ border-radius: 8px;
478+ cursor: pointer;
479+ -webkit-user-select: none;
480+ color: #000;
481+}
482+
483+#first p, #sync p {
484+ margin: 16px;
485+}
486+
487+#first {
488+ top: 30px;
489+ left: 71px;
490+}
491+
492+#sync {
493+ bottom: 30px;
494+ /*left: 449px;*/
495+ right: 71px;
496+}
497+
498+p.top {
499+ font-weight: bold;
500+}
501+
502+#logo {
503+ position: fixed;
504+ top: 190px;
505+ left: 400px;
506+ width: 160px;
507+ height: 160px;
508+ -webkit-transition: -webkit-transform 500ms linear;
509+}
510+
511+#logo.spinleft {
512+ -webkit-transform: rotate(-180deg);
513+}
514+
515+#logo.spinright {
516+ -webkit-transform: rotate(180deg);
517+}
518+
519+
520+p.gen {
521+ margin: 2em;
522+ font-size: 28px;
523+}
524+
525+
526+#finish {
527+ position: fixed;
528+ width: 160px;
529+ height: 160px;
530+ bottom: 60px;
531+ right: 60px;
532+ cursor: pointer;
533+ -webkit-transition-timing-function: ease;
534+ -webkit-transition-duration: 300ms;
535+ -webkit-transition-property: right, bottom;
536+}
537+
538+#finish.hidden {
539+ bottom: -134px;
540+ right: -134px;
541+}
542+
543+#logo2 {
544+ z-index: 10;
545+ position: fixed;
546+ width: 160px;
547+ height: 160px;
548+ bottom: 60px;
549+ right: 60px;
550+ -webkit-transition: -webkit-transform 2000ms linear;
551+}
552+
553+#logo2.spinright {
554+ -webkit-transform: rotate(360deg);
555+}
556+
557
558=== added file 'dmedia/gtk/ui/peering.js'
559--- dmedia/gtk/ui/peering.js 1970-01-01 00:00:00 +0000
560+++ dmedia/gtk/ui/peering.js 2012-10-09 20:57:30 +0000
561@@ -0,0 +1,183 @@
562+"use strict";
563+
564+var Hub = {
565+ /*
566+ Relay signals between JavaScript and Gtk.
567+
568+ For example, to send a signal to Gtk via document.title:
569+
570+ >>> Hub.send('click');
571+ >>> Hub.send('changed', 'foo', 'bar');
572+
573+ Or from the Gtk side, send a signal to JavaScript by using
574+ WebView.execute_script() to call Hub.recv() like this:
575+
576+ >>> Hub.recv('{"signal": "error", "args": ["oops!"]}');
577+
578+ Use userwebkit.BaseApp.send() as a shortcut to do the above.
579+
580+ Lastly, to emit a signal from JavaScript to JavaScript handlers, use
581+ Hub.emit() like this:
582+
583+ >>> Hub.emit('changed', 'foo', 'bar');
584+
585+ */
586+ i: 0,
587+
588+ names: {},
589+
590+ connect: function(signal, callback, self) {
591+ /*
592+ Connect a signal handler.
593+
594+ For example:
595+
596+ >>> Hub.connect('changed', this.on_changed, this);
597+
598+ */
599+ if (! Hub.names[signal]) {
600+ Hub.names[signal] = [];
601+ }
602+ Hub.names[signal].push({callback: callback, self: self});
603+ },
604+
605+ send: function() {
606+ /*
607+ Send a signal to the Gtk side by changing document.title.
608+
609+ For example:
610+
611+ >>> Hub.send('changed', 'foo', 'bar');
612+
613+ */
614+ var params = Array.prototype.slice.call(arguments);
615+ var signal = params[0];
616+ var args = params.slice(1);
617+ Hub._emit(signal, args);
618+ var obj = {
619+ 'i': Hub.i,
620+ 'signal': signal,
621+ 'args': args,
622+ };
623+ Hub.i += 1;
624+ document.title = JSON.stringify(obj);
625+ },
626+
627+ recv: function(data) {
628+ /*
629+ Gtk should call this function to emit a signal to JavaScript handlers.
630+
631+ For example:
632+
633+ >>> Hub.recv('{"signal": "changed", "args": ["foo", "bar"]}');
634+
635+ If you need to emit a signal from JavaScript to JavaScript handlers,
636+ use Hub.emit() instead.
637+ */
638+ var obj = JSON.parse(data);
639+ Hub._emit(obj.signal, obj.args);
640+ },
641+
642+ emit: function() {
643+ /*
644+ Emit a signal from JavaScript to JavaScript handlers.
645+
646+ For example:
647+
648+ >>> Hub.emit('changed', 'foo', 'bar');
649+
650+ */
651+ var params = Array.prototype.slice.call(arguments);
652+ Hub._emit(params[0], params.slice(1));
653+ },
654+
655+ _emit: function(signal, args) {
656+ /*
657+ Low-level private function to emit a signal to JavaScript handlers.
658+ */
659+ var handlers = Hub.names[signal];
660+ if (handlers) {
661+ handlers.forEach(function(h) {
662+ h.callback.apply(h.self, args);
663+ });
664+ }
665+ },
666+}
667+
668+
669+function $bind(func, self) {
670+ return function() {
671+ var args = Array.prototype.slice.call(arguments);
672+ return func.apply(self, args);
673+ }
674+}
675+
676+
677+function $(id) {
678+ /*
679+ Return the element with id="id".
680+
681+ If `id` is an Element, it is returned unchanged.
682+
683+ Examples:
684+
685+ >>> $('browser');
686+ <div id="browser" class="box">
687+ >>> var el = $('browser');
688+ >>> $(el);
689+ <div id="browser" class="box">
690+
691+ */
692+ if (id instanceof Element) {
693+ return id;
694+ }
695+ return document.getElementById(id);
696+}
697+
698+
699+function $el(tag, attributes) {
700+ /*
701+ Convenience function to create a new DOM element and set its attributes.
702+
703+ Examples:
704+
705+ >>> $el('img');
706+ <img>
707+ >>> $el('img', {'class': 'thumbnail', 'src': 'foo.png'});
708+ <img class="thumbnail" src="foo.png">
709+
710+ */
711+ var el = document.createElement(tag);
712+ if (attributes) {
713+ var key;
714+ for (key in attributes) {
715+ var value = attributes[key];
716+ if (key == 'textContent') {
717+ el.textContent = value;
718+ }
719+ else {
720+ el.setAttribute(key, value);
721+ }
722+ }
723+ }
724+ return el;
725+}
726+
727+
728+function $hide(id) {
729+ var element = $(id);
730+ if (element) {
731+ element.classList.add('hide');
732+ return element;
733+ }
734+}
735+
736+
737+function $show(id) {
738+ var element = $(id);
739+ if (element) {
740+ element.classList.remove('hide');
741+ return element;
742+ }
743+}
744+
745
746=== added file 'dmedia/gtk/ui/server.html'
747--- dmedia/gtk/ui/server.html 1970-01-01 00:00:00 +0000
748+++ dmedia/gtk/ui/server.html 2012-10-09 20:57:30 +0000
749@@ -0,0 +1,37 @@
750+<!DOCTYPE html>
751+<html>
752+<head>
753+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
754+<link rel="stylesheet" href="peering.css" />
755+<script src="peering.js"></script>
756+<script>
757+
758+"use strict";
759+
760+Hub.connect('display_secret',
761+ function(secret) {
762+ $('secret').textContent = secret;
763+ }
764+);
765+
766+Hub.connect('set_message',
767+ function(message) {
768+ $('message').textContent = message;
769+ }
770+);
771+
772+window.onload = function() {
773+ Hub.send('get_secret');
774+}
775+
776+</script>
777+</head>
778+<p class="gen">
779+This is your secret code:
780+</p>
781+<div class="secret">
782+<code id="secret"></code>
783+</div>
784+<p id="message"></p>
785+</body>
786+</html>
787
788=== added file 'dmedia/gtk/ui/sync.svg'
789--- dmedia/gtk/ui/sync.svg 1970-01-01 00:00:00 +0000
790+++ dmedia/gtk/ui/sync.svg 2012-10-09 20:57:30 +0000
791@@ -0,0 +1,119 @@
792+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
793+<!-- Created with Inkscape (http://www.inkscape.org/) -->
794+
795+<svg
796+ xmlns:dc="http://purl.org/dc/elements/1.1/"
797+ xmlns:cc="http://creativecommons.org/ns#"
798+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
799+ xmlns:svg="http://www.w3.org/2000/svg"
800+ xmlns="http://www.w3.org/2000/svg"
801+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
802+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
803+ width="512"
804+ height="512"
805+ id="svg4778"
806+ version="1.1"
807+ inkscape:version="0.48.3.1 r9886"
808+ sodipodi:docname="sync.svg"
809+ inkscape:export-filename="/home/izo/Pictures/Client_Work/Novacut/Final-Artwork/web/PNG/novacut-solo-brandmark_PINK_FINAL-PNG-300dpi.png"
810+ inkscape:export-xdpi="300.05859"
811+ inkscape:export-ydpi="300.05859">
812+ <defs
813+ id="defs4780">
814+ <inkscape:path-effect
815+ effect="spiro"
816+ id="path-effect5868"
817+ is_visible="true" />
818+ </defs>
819+ <sodipodi:namedview
820+ id="base"
821+ pagecolor="#ffffff"
822+ bordercolor="#666666"
823+ borderopacity="1.0"
824+ inkscape:pageopacity="0.0"
825+ inkscape:pageshadow="2"
826+ inkscape:zoom="1"
827+ inkscape:cx="233.49618"
828+ inkscape:cy="280"
829+ inkscape:current-layer="layer1"
830+ inkscape:document-units="px"
831+ showgrid="false"
832+ inkscape:window-width="2495"
833+ inkscape:window-height="1576"
834+ inkscape:window-x="65"
835+ inkscape:window-y="24"
836+ inkscape:window-maximized="1"
837+ showguides="false"
838+ inkscape:guide-bbox="true">
839+ <inkscape:grid
840+ type="xygrid"
841+ id="grid2994"
842+ empspacing="4"
843+ visible="true"
844+ enabled="true"
845+ snapvisiblegridlinesonly="true" />
846+ <sodipodi:guide
847+ orientation="1,0"
848+ position="256,88"
849+ id="guide3002" />
850+ <sodipodi:guide
851+ orientation="0,1"
852+ position="592,256"
853+ id="guide3004" />
854+ <sodipodi:guide
855+ position="0,0"
856+ orientation="0,512"
857+ id="guide3006" />
858+ <sodipodi:guide
859+ position="512,0"
860+ orientation="-512,0"
861+ id="guide3008" />
862+ <sodipodi:guide
863+ position="512,512"
864+ orientation="0,-512"
865+ id="guide3010" />
866+ <sodipodi:guide
867+ position="0,512"
868+ orientation="512,0"
869+ id="guide3012" />
870+ </sodipodi:namedview>
871+ <metadata
872+ id="metadata4783">
873+ <rdf:RDF>
874+ <cc:Work
875+ rdf:about="">
876+ <dc:format>image/svg+xml</dc:format>
877+ <dc:type
878+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
879+ <dc:title />
880+ </cc:Work>
881+ </rdf:RDF>
882+ </metadata>
883+ <g
884+ id="layer1"
885+ inkscape:label="Layer 1"
886+ inkscape:groupmode="layer"
887+ transform="translate(0,32)">
888+ <path
889+ transform="matrix(1.0666667,0,0,1.0666667,-85.333334,-32.000001)"
890+ d="M 545,240 C 545,364.26407 444.26407,465 320,465 195.73593,465 95,364.26407 95,240 95,115.73593 195.73593,15 320,15 444.26407,15 545,115.73593 545,240 z"
891+ sodipodi:ry="225"
892+ sodipodi:rx="225"
893+ sodipodi:cy="240"
894+ sodipodi:cx="320"
895+ id="path5924"
896+ style="fill:#e81f3b;fill-opacity:1;stroke:none"
897+ sodipodi:type="arc" />
898+ <text
899+ xml:space="preserve"
900+ style="font-size:159.66772461px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Lato;-inkscape-font-specification:Lato"
901+ x="71.52964"
902+ y="277.69928"
903+ id="text2994"
904+ sodipodi:linespacing="125%"><tspan
905+ sodipodi:role="line"
906+ id="tspan2996"
907+ x="71.52964"
908+ y="277.69928">Sync!</tspan></text>
909+ </g>
910+</svg>
911
912=== modified file 'dmedia/httpd.py'
913--- dmedia/httpd.py 2012-10-04 20:46:18 +0000
914+++ dmedia/httpd.py 2012-10-09 20:57:30 +0000
915@@ -272,6 +272,12 @@
916 self.remote = '{REMOTE_ADDR} {REMOTE_PORT}'.format(**environ)
917 self.start = None
918
919+ def handle(self):
920+ if self.environ['wsgi.multithread']:
921+ self.handle_many()
922+ else:
923+ self.handle_one()
924+
925 def handle_many(self):
926 count = 0
927 try:
928@@ -412,6 +418,12 @@
929 self.url = template.format(self.scheme, self.port)
930 self.environ = self.build_base_environ()
931 self.socket.listen(5)
932+ self.thread = None
933+ self.running = False
934+
935+ def __del__(self):
936+ if self.running:
937+ self.shutdown()
938
939 def build_base_environ(self):
940 """
941@@ -469,6 +481,7 @@
942 def serve_forever(self):
943 while True:
944 (conn, address) = self.socket.accept()
945+ conn.settimeout(SOCKET_TIMEOUT)
946 thread = threading.Thread(
947 target=self.handle_connection,
948 args=(conn, address),
949@@ -476,12 +489,45 @@
950 thread.daemon = True
951 thread.start()
952
953+ def start(self):
954+ assert self.thread is None
955+ assert self.running is False
956+ self.running = True
957+ self.thread = threading.Thread(
958+ target=self.serve_single_threaded,
959+ )
960+ self.thread.daemon = True
961+ self.thread.start()
962+
963+ def shutdown(self):
964+ assert self.running is True
965+ self.running = False
966+ self.thread.join()
967+ self.thread = None
968+
969+ def reconfigure(self, app, ssl_config):
970+ assert set(ssl_config) == set(['cert_file', 'key_file', 'ca_file'])
971+ self.shutdown()
972+ self.app = app
973+ self.context = build_server_ssl_context(ssl_config)
974+ self.start()
975+
976+ def serve_single_threaded(self):
977+ self.environ['wsgi.multithread'] = False
978+ self.socket.settimeout(0.25)
979+ while self.running:
980+ try:
981+ (conn, address) = self.socket.accept()
982+ conn.settimeout(0.50)
983+ self.handle_connection(conn, address)
984+ except socket.timeout:
985+ pass
986+
987 def handle_connection(self, conn, address):
988 #conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
989 remote = '{} {}'.format(address[0], address[1])
990 try:
991 log.info('%s\tNew Connection', remote)
992- conn.settimeout(SOCKET_TIMEOUT)
993 if self.context is not None:
994 conn = self.context.wrap_socket(conn, server_side=True)
995 self.handle_requests(conn, address)
996@@ -497,7 +543,7 @@
997 environ = self.environ.copy()
998 environ.update(self.build_connection_environ(conn, address))
999 handler = Handler(self.app, environ, conn)
1000- handler.handle_many()
1001+ handler.handle()
1002
1003
1004 ############################
1005@@ -537,8 +583,11 @@
1006
1007
1008 def run_server(queue, app, bind_address='::1', ssl_config=None):
1009- server = make_server(app, bind_address, ssl_config)
1010- env = {'port': server.port, 'url': server.url}
1011- queue.put(env)
1012- server.serve_forever()
1013+ try:
1014+ server = make_server(app, bind_address, ssl_config)
1015+ env = {'port': server.port, 'url': server.url}
1016+ queue.put(env)
1017+ server.serve_forever()
1018+ except Exception as e:
1019+ queue.put(e)
1020
1021
1022=== modified file 'dmedia/peering.py'
1023--- dmedia/peering.py 2012-10-03 17:21:46 +0000
1024+++ dmedia/peering.py 2012-10-09 20:57:30 +0000
1025@@ -107,7 +107,7 @@
1026
1027 """
1028
1029-from base64 import b32encode, b32decode
1030+import base64
1031 import os
1032 from os import path
1033 import stat
1034@@ -115,9 +115,14 @@
1035 import shutil
1036 from collections import namedtuple
1037 from subprocess import check_call, check_output
1038+import json
1039+import socket
1040+import logging
1041
1042 from skein import skein512
1043-from microfiber import random_id
1044+from microfiber import random_id, dumps
1045+
1046+from dmedia.httpd import WSGIError
1047
1048
1049 DAYS = 365 * 10
1050@@ -128,6 +133,11 @@
1051 PERS_PUBKEY = b'20120918 jderose@novacut.com dmedia/pubkey'
1052 PERS_RESPONSE = b'20120918 jderose@novacut.com dmedia/response'
1053
1054+USER = os.environ.get('USER')
1055+HOST = socket.gethostname()
1056+
1057+log = logging.getLogger()
1058+
1059
1060 class IdentityError(Exception):
1061 def __init__(self, filename, expected, got):
1062@@ -147,6 +157,15 @@
1063 pass
1064
1065
1066+class IssuerError(IdentityError):
1067+ pass
1068+
1069+
1070+class VerificationError(IdentityError):
1071+ pass
1072+
1073+
1074+
1075 def create_key(dst_file, bits=2048):
1076 """
1077 Create an RSA keypair and save it to *dst_file*.
1078@@ -262,6 +281,33 @@
1079 return line[len(prefix):]
1080
1081
1082+def get_issuer(cert_file):
1083+ """
1084+ Get the issuer from an X509 certificate (CA or issued certificate).
1085+ """
1086+ line = check_output(['openssl', 'x509',
1087+ '-issuer',
1088+ '-noout',
1089+ '-in', cert_file,
1090+ ]).decode('utf-8').rstrip('\n')
1091+
1092+ prefix = 'issuer= ' # Different than get_csr_subject()
1093+ if not line.startswith(prefix):
1094+ raise Exception(line)
1095+ return line[len(prefix):]
1096+
1097+
1098+def ssl_verify(cert_file, ca_file):
1099+ line = check_output(['openssl', 'verify',
1100+ '-CAfile', ca_file,
1101+ cert_file
1102+ ]).decode('utf-8')
1103+ expected = '{}: OK\n'.format(cert_file)
1104+ if line != expected:
1105+ raise VerificationError(cert_file, expected, line)
1106+ return cert_file
1107+
1108+
1109 def verify_key(filename, _id):
1110 actual_id = hash_pubkey(get_rsa_pubkey(filename))
1111 if _id != actual_id:
1112@@ -291,6 +337,38 @@
1113 return filename
1114
1115
1116+def verify_ca(filename, _id):
1117+ filename = verify(filename, _id)
1118+ issuer = make_subject(_id)
1119+ actual_issuer = get_issuer(filename)
1120+ if issuer != actual_issuer:
1121+ raise IssuerError(filename, issuer, actual_issuer)
1122+ return ssl_verify(filename, filename)
1123+ return filename
1124+
1125+
1126+def verify_cert(cert_file, cert_id, ca_file, ca_id):
1127+ filename = verify(cert_file, cert_id)
1128+ issuer = make_subject(ca_id)
1129+ actual_issuer = get_issuer(filename)
1130+ if issuer != actual_issuer:
1131+ raise IssuerError(filename, issuer, actual_issuer)
1132+ return ssl_verify(filename, ca_file)
1133+ return filename
1134+
1135+
1136+def encode(value):
1137+ assert isinstance(value, bytes)
1138+ assert len(value) > 0 and len(value) % 5 == 0
1139+ return base64.b32encode(value).decode('utf-8')
1140+
1141+
1142+def decode(value):
1143+ assert isinstance(value, str)
1144+ assert len(value) > 0 and len(value) % 8 == 0
1145+ return base64.b32decode(value.encode('utf-8'))
1146+
1147+
1148 def _hash_pubkey(data):
1149 return skein512(data,
1150 digest_bits=240,
1151@@ -299,7 +377,7 @@
1152
1153
1154 def hash_pubkey(data):
1155- return b32encode(_hash_pubkey(data)).decode('utf-8')
1156+ return encode(_hash_pubkey(data))
1157
1158
1159 def _hash_cert(cert_data):
1160@@ -310,7 +388,7 @@
1161
1162
1163 def hash_cert(cert_data):
1164- return b32encode(_hash_cert(cert_data)).decode('utf-8')
1165+ return encode(_hash_cert(cert_data))
1166
1167
1168 def compute_response(secret, challenge, nonce, challenger_hash, responder_hash):
1169@@ -326,15 +404,253 @@
1170
1171 :param responder_hash: hash of the responders certificate
1172 """
1173+ assert len(secret) == 5
1174+ assert len(challenge) == 20
1175+ assert len(nonce) == 20
1176+ assert len(challenger_hash) == 30
1177+ assert len(responder_hash) == 30
1178 skein = skein512(
1179 digest_bits=280,
1180 pers=PERS_RESPONSE,
1181 key=secret,
1182- nonce=(challange + nonce),
1183+ nonce=(challenge + nonce),
1184 )
1185 skein.update(challenger_hash)
1186 skein.update(responder_hash)
1187- return b32encode(skein.digest()).decode('utf-8')
1188+ return encode(skein.digest())
1189+
1190+
1191+class WrongResponse(Exception):
1192+ def __init__(self, expected, got):
1193+ self.expected = expected
1194+ self.got = got
1195+ super().__init__('Incorrect response')
1196+
1197+
1198+class ChallengeResponse:
1199+ def __init__(self, _id, peer_id):
1200+ self.id = _id
1201+ self.peer_id = peer_id
1202+ self.local_hash = decode(_id)
1203+ self.remote_hash = decode(peer_id)
1204+ assert len(self.local_hash) == 30
1205+ assert len(self.remote_hash) == 30
1206+
1207+ def get_secret(self):
1208+ # 40-bit secret (8 characters when base32 encoded)
1209+ self.secret = os.urandom(5)
1210+ return encode(self.secret)
1211+
1212+ def set_secret(self, secret):
1213+ assert len(secret) == 8
1214+ self.secret = decode(secret)
1215+ assert len(self.secret) == 5
1216+
1217+ def get_challenge(self):
1218+ self.challenge = os.urandom(20)
1219+ return encode(self.challenge)
1220+
1221+ def create_response(self, challenge):
1222+ nonce = os.urandom(20)
1223+ response = compute_response(
1224+ self.secret,
1225+ decode(challenge),
1226+ nonce,
1227+ self.remote_hash,
1228+ self.local_hash
1229+ )
1230+ return (encode(nonce), response)
1231+
1232+ def check_response(self, nonce, response):
1233+ expected = compute_response(
1234+ self.secret,
1235+ self.challenge,
1236+ decode(nonce),
1237+ self.local_hash,
1238+ self.remote_hash
1239+ )
1240+ if response != expected:
1241+ del self.secret
1242+ del self.challenge
1243+ raise WrongResponse(expected, response)
1244+
1245+
1246+class InfoApp:
1247+ def __init__(self, _id):
1248+ self.id = _id
1249+ obj = {
1250+ 'id': _id,
1251+ 'user': USER,
1252+ 'host': HOST,
1253+ }
1254+ self.info = dumps(obj).encode('utf-8')
1255+ self.info_length = str(len(self.info))
1256+
1257+ def __call__(self, environ, start_response):
1258+ if environ['wsgi.multithread'] is not False:
1259+ raise WSGIError('500 Internal Server Error')
1260+ if environ['PATH_INFO'] != '/':
1261+ raise WSGIError('410 Gone')
1262+ if environ['REQUEST_METHOD'] != 'GET':
1263+ raise WSGIError('405 Method Not Allowed')
1264+ start_response('200 OK',
1265+ [
1266+ ('Content-Length', self.info_length),
1267+ ('Content-Type', 'application/json'),
1268+ ]
1269+ )
1270+ return [self.info]
1271+
1272+
1273+class ClientApp:
1274+ allowed_states = (
1275+ 'ready',
1276+ 'gave_challenge',
1277+ 'in_response',
1278+ 'wrong_response',
1279+ 'response_ok',
1280+ )
1281+
1282+ forwarded_states = (
1283+ 'wrong_response',
1284+ 'response_ok',
1285+ )
1286+
1287+ def __init__(self, cr, queue):
1288+ assert isinstance(cr, ChallengeResponse)
1289+ self.cr = cr
1290+ self.queue = queue
1291+ self.__state = None
1292+ self.map = {
1293+ '/challenge': self.get_challenge,
1294+ '/response': self.put_response,
1295+ }
1296+
1297+ def get_state(self):
1298+ return self.__state
1299+
1300+ def set_state(self, state):
1301+ if state not in self.__class__.allowed_states:
1302+ self.__state = None
1303+ log.error('invalid state: %r', state)
1304+ raise Exception('invalid state: {!r}'.format(state))
1305+ self.__state = state
1306+ if state in self.__class__.forwarded_states:
1307+ self.queue.put(state)
1308+
1309+ state = property(get_state, set_state)
1310+
1311+ def __call__(self, environ, start_response):
1312+ if environ['wsgi.multithread'] is not False:
1313+ raise WSGIError('500 Internal Server Error')
1314+ if environ.get('SSL_CLIENT_VERIFY') != 'SUCCESS':
1315+ raise WSGIError('403 Forbidden')
1316+ if environ.get('SSL_CLIENT_S_DN_CN') != self.cr.peer_id:
1317+ raise WSGIError('403 Forbidden')
1318+ if environ.get('SSL_CLIENT_I_DN_CN') != self.cr.peer_id:
1319+ raise WSGIError('403 Forbidden')
1320+
1321+ path_info = environ['PATH_INFO']
1322+ if path_info not in self.map:
1323+ raise WSGIError('410 Gone')
1324+ log.info('%s %s', environ['REQUEST_METHOD'], environ['PATH_INFO'])
1325+ try:
1326+ obj = self.map[path_info](environ)
1327+ data = json.dumps(obj).encode('utf-8')
1328+ start_response('200 OK',
1329+ [
1330+ ('Content-Length', str(len(data))),
1331+ ('Content-Type', 'application/json'),
1332+ ]
1333+ )
1334+ return [data]
1335+ except WSGIError as e:
1336+ raise e
1337+ except Exception:
1338+ log.exception('500 Internal Server Error')
1339+ raise WSGIError('500 Internal Server Error')
1340+
1341+ def get_challenge(self, environ):
1342+ if self.state != 'ready':
1343+ raise WSGIError('400 Bad Request Order')
1344+ self.state = 'gave_challenge'
1345+ if environ['REQUEST_METHOD'] != 'GET':
1346+ raise WSGIError('405 Method Not Allowed')
1347+ return {
1348+ 'challenge': self.cr.get_challenge(),
1349+ }
1350+
1351+ def put_response(self, environ):
1352+ if self.state != 'gave_challenge':
1353+ raise WSGIError('400 Bad Request Order')
1354+ self.state = 'in_response'
1355+ if environ['REQUEST_METHOD'] != 'PUT':
1356+ raise WSGIError('405 Method Not Allowed')
1357+ data = environ['wsgi.input'].read()
1358+ obj = json.loads(data.decode('utf-8'))
1359+ nonce = obj['nonce']
1360+ response = obj['response']
1361+ try:
1362+ self.cr.check_response(nonce, response)
1363+ except WrongResponse:
1364+ self.state = 'wrong_response'
1365+ raise WSGIError('401 Unauthorized')
1366+ self.state = 'response_ok'
1367+ return {'ok': True}
1368+
1369+
1370+class ServerApp(ClientApp):
1371+
1372+ allowed_states = (
1373+ 'info',
1374+ 'counter_response_ok',
1375+ 'in_csr',
1376+ 'bad_csr',
1377+ 'cert_issued',
1378+ ) + ClientApp.allowed_states
1379+
1380+ forwarded_states = (
1381+ 'bad_csr',
1382+ 'cert_issued',
1383+ ) + ClientApp.forwarded_states
1384+
1385+ def __init__(self, cr, queue, pki):
1386+ super().__init__(cr, queue)
1387+ self.pki = pki
1388+ self.map['/'] = self.get_info
1389+ self.map['/csr'] = self.post_csr
1390+
1391+ def get_info(self, environ):
1392+ if self.state != 'info':
1393+ raise WSGIError('400 Bad Request State')
1394+ self.state = 'ready'
1395+ if environ['REQUEST_METHOD'] != 'GET':
1396+ raise WSGIError('405 Method Not Allowed')
1397+ return {
1398+ 'id': self.cr.id,
1399+ 'user': USER,
1400+ 'host': HOST,
1401+ }
1402+
1403+ def post_csr(self, environ):
1404+ if self.state != 'counter_response_ok':
1405+ raise WSGIError('400 Bad Request Order')
1406+ self.state = 'in_csr'
1407+ if environ['REQUEST_METHOD'] != 'POST':
1408+ raise WSGIError('405 Method Not Allowed')
1409+ data = environ['wsgi.input'].read()
1410+ obj = json.loads(data.decode('utf-8'))
1411+ csr_data = base64.b64decode(obj['csr'].encode('utf-8'))
1412+ try:
1413+ self.pki.write_csr(self.cr.peer_id, csr_data)
1414+ self.pki.issue_cert(self.cr.peer_id, self.cr.id)
1415+ cert_data = self.pki.read_cert2(self.cr.peer_id, self.cr.id)
1416+ except Exception as e:
1417+ log.exception('could not issue cert')
1418+ self.state = 'bad_csr'
1419+ raise WSGIError('401 Unauthorized')
1420+ self.state = 'cert_issued'
1421+ return {'cert': base64.b64encode(cert_data).decode('utf-8')}
1422
1423
1424 def ensuredir(d):
1425@@ -376,6 +692,18 @@
1426 key_file = self.path(_id, 'key')
1427 return verify_key(key_file, _id)
1428
1429+ def read_key(self, _id):
1430+ key_file = self.verify_key(_id)
1431+ return open(key_file, 'rb').read()
1432+
1433+ def write_key(self, _id, data):
1434+ tmp_file = self.random_tmp()
1435+ open(tmp_file, 'wb').write(data)
1436+ verify_key(tmp_file, _id)
1437+ key_file = self.path(_id, 'key')
1438+ os.rename(tmp_file, key_file)
1439+ return key_file
1440+
1441 def create_ca(self, _id):
1442 key_file = self.verify_key(_id)
1443 subject = make_subject(_id)
1444@@ -387,7 +715,19 @@
1445
1446 def verify_ca(self, _id):
1447 ca_file = self.path(_id, 'ca')
1448- return verify(ca_file, _id)
1449+ return verify_ca(ca_file, _id)
1450+
1451+ def read_ca(self, _id):
1452+ ca_file = self.verify_ca(_id)
1453+ return open(ca_file, 'rb').read()
1454+
1455+ def write_ca(self, _id, data):
1456+ tmp_file = self.random_tmp()
1457+ open(tmp_file, 'wb').write(data)
1458+ verify_ca(tmp_file, _id)
1459+ ca_file = self.path(_id, 'ca')
1460+ os.rename(tmp_file, ca_file)
1461+ return ca_file
1462
1463 def create_csr(self, _id):
1464 key_file = self.verify_key(_id)
1465@@ -402,6 +742,18 @@
1466 csr_file = self.path(_id, 'csr')
1467 return verify_csr(csr_file, _id)
1468
1469+ def read_csr(self, _id):
1470+ csr_file = self.verify_csr(_id)
1471+ return open(csr_file, 'rb').read()
1472+
1473+ def write_csr(self, _id, data):
1474+ tmp_file = self.random_tmp()
1475+ open(tmp_file, 'wb').write(data)
1476+ verify_csr(tmp_file, _id)
1477+ csr_file = self.path(_id, 'csr')
1478+ os.rename(tmp_file, csr_file)
1479+ return csr_file
1480+
1481 def issue_cert(self, _id, ca_id):
1482 csr_file = self.verify_csr(_id)
1483 tmp_file = self.random_tmp()
1484@@ -432,6 +784,36 @@
1485 cert_file = self.path(_id, 'cert')
1486 return verify(cert_file, _id)
1487
1488+ def verify_cert2(self, cert_id, ca_id):
1489+ cert_file = self.path(cert_id, 'cert')
1490+ ca_file = self.verify_ca(ca_id)
1491+ return verify_cert(cert_file, cert_id, ca_file, ca_id)
1492+
1493+ def read_cert(self, _id):
1494+ cert_file = self.verify_cert(_id)
1495+ return open(cert_file, 'rb').read()
1496+
1497+ def read_cert2(self, cert_id, ca_id):
1498+ cert_file = self.verify_cert2(cert_id, ca_id)
1499+ return open(cert_file, 'rb').read()
1500+
1501+ def write_cert(self, _id, data):
1502+ tmp_file = self.random_tmp()
1503+ open(tmp_file, 'wb').write(data)
1504+ verify(tmp_file, _id)
1505+ cert_file = self.path(_id, 'cert')
1506+ os.rename(tmp_file, cert_file)
1507+ return cert_file
1508+
1509+ def write_cert2(self, cert_id, ca_id, cert_data):
1510+ ca_file = self.verify_ca(ca_id)
1511+ tmp_file = self.random_tmp()
1512+ open(tmp_file, 'wb').write(cert_data)
1513+ verify_cert(tmp_file, cert_id, ca_file, ca_id)
1514+ cert_file = self.path(cert_id, 'cert')
1515+ os.rename(tmp_file, cert_file)
1516+ return cert_file
1517+
1518 def get_ca(self, _id):
1519 return CA(_id, self.verify_ca(_id))
1520
1521@@ -499,4 +881,3 @@
1522 'key_file': self.client.key_file,
1523 })
1524 return config
1525-
1526
1527=== modified file 'dmedia/service/avahi.py'
1528--- dmedia/service/avahi.py 2012-10-04 20:23:22 +0000
1529+++ dmedia/service/avahi.py 2012-10-09 20:57:30 +0000
1530@@ -36,6 +36,7 @@
1531 from dmedia import util, views
1532
1533 log = logging.getLogger()
1534+PROTO = 0 # Protocol -1 = both, 0 = IPv4, 1 = IPv6
1535 Peer = namedtuple('Peer', 'env names')
1536 PEERS = '_local/peers'
1537
1538@@ -69,7 +70,7 @@
1539 )
1540 self.group.AddService(
1541 -1, # Interface
1542- 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
1543+ PROTO, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
1544 0, # Flags
1545 self.id,
1546 self.service,
1547@@ -82,9 +83,9 @@
1548 self.group.Commit(dbus_interface='org.freedesktop.Avahi.EntryGroup')
1549 browser_path = self.avahi.ServiceBrowserNew(
1550 -1, # Interface
1551- 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
1552+ PROTO, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
1553 self.service,
1554- 'local',
1555+ '', # Domain, default to .local
1556 0, # Flags
1557 dbus_interface='org.freedesktop.Avahi.Server'
1558 )
1559@@ -107,7 +108,8 @@
1560 if key == self.id:
1561 return
1562 self.avahi.ResolveService(
1563- interface, protocol, key, _type, domain, -1, 0,
1564+ # 2nd to last arg is Protocol, again for some reason
1565+ interface, protocol, key, _type, domain, PROTO, 0,
1566 dbus_interface='org.freedesktop.Avahi.Server',
1567 reply_handler=self.on_reply,
1568 error_handler=self.on_error,
1569@@ -175,7 +177,7 @@
1570 except KeyError:
1571 pass
1572 self.remove_replication_peer(key)
1573-
1574+
1575 def on_timeout(self):
1576 if not self.replications:
1577 return True # Repeat timeout call
1578
1579=== modified file 'dmedia/service/peers.py'
1580--- dmedia/service/peers.py 2012-09-20 12:56:04 +0000
1581+++ dmedia/service/peers.py 2012-10-09 20:57:30 +0000
1582@@ -21,45 +21,255 @@
1583
1584 """
1585 Browse for Dmedia peer offerings, publish the same.
1586+
1587+Existing machines constantly listen for _dmedia-offer._tcp.
1588+
1589+New machine publishes _dmedia-offer._tcp, and listens for _dmedia-accept._tcp.
1590+
1591+Existing machine prompts user, and if they accept, machine publishes
1592+_dmedia-accept._tcp, which initiates peering process.
1593 """
1594
1595 import logging
1596+from collections import namedtuple
1597+import ssl
1598+import socket
1599+import threading
1600
1601 import dbus
1602+from dbus.mainloop.glib import DBusGMainLoop
1603 from gi.repository import GObject
1604-
1605-
1606+from microfiber import _start_thread, random_id, CouchBase, dumps, build_ssl_context
1607+
1608+PROTO = 0 # Protocol -1 = both, 0 = IPv4, 1 = IPv6
1609+GObject.threads_init()
1610+DBusGMainLoop(set_as_default=True)
1611 log = logging.getLogger()
1612
1613-
1614-class Peer:
1615- def __init__(self, _id):
1616+Peer = namedtuple('Peer', 'id ip port')
1617+Info = namedtuple('Info', 'name host url id')
1618+
1619+
1620+def get_service(verb):
1621+ """
1622+ Get Avahi service name for appropriate direction.
1623+
1624+ For example, for an offer:
1625+
1626+ >>> get_service('offer')
1627+ '_dmedia-offer._tcp'
1628+
1629+ And for an accept:
1630+
1631+ >>> get_service('accept')
1632+ '_dmedia-accept._tcp'
1633+
1634+ """
1635+ assert verb in ('offer', 'accept')
1636+ return '_dmedia-{}._tcp'.format(verb)
1637+
1638+
1639+class State:
1640+ """
1641+ A state machine to help prevent silly mistakes.
1642+
1643+ So that threading issues don't make the code difficult to reason about,
1644+ a thread-lock is acquired when making a state change. To be on the safe
1645+ side, you should only make state changes from the main thread. But the
1646+ thread-lock is there as a safety in case an attacker could change the
1647+ execution such that something isn't called from the main thread, or in case
1648+ an oversight is made by the programmer.
1649+ """
1650+ def __init__(self):
1651+ self.__state = 'free'
1652+ self.__peer_id = None
1653+ self.__lock = threading.Lock()
1654+
1655+ def __repr__(self):
1656+ return 'State(state={!r}, peer_id={!r})'.format(
1657+ self.__state, self.__peer_id
1658+ )
1659+
1660+ @property
1661+ def state(self):
1662+ return self.__state
1663+
1664+ @property
1665+ def peer_id(self):
1666+ return self.__peer_id
1667+
1668+ def bind(self, peer_id):
1669+ with self.__lock:
1670+ assert peer_id is not None
1671+ if self.__state != 'free':
1672+ return False
1673+ if self.__peer_id is not None:
1674+ return False
1675+ self.__state = 'bound'
1676+ self.__peer_id = peer_id
1677+ return True
1678+
1679+ def verify(self, peer_id):
1680+ with self.__lock:
1681+ if self.__state != 'bound':
1682+ return False
1683+ if peer_id is None or peer_id != self.__peer_id:
1684+ return False
1685+ self.__state = 'verified'
1686+ return True
1687+
1688+ def unbind(self, peer_id):
1689+ with self.__lock:
1690+ if self.__state not in ('bound', 'verified'):
1691+ return False
1692+ if peer_id is None or peer_id != self.__peer_id:
1693+ return False
1694+ self.__state = 'unbound'
1695+ return True
1696+
1697+ def activate(self, peer_id):
1698+ with self.__lock:
1699+ if self.__state != 'verified':
1700+ return False
1701+ if peer_id is None or peer_id != self.__peer_id:
1702+ return False
1703+ self.__state = 'activated'
1704+ self.__peer_id = peer_id
1705+ return True
1706+
1707+ def deactivate(self, peer_id):
1708+ with self.__lock:
1709+ if self.__state != 'activated':
1710+ return False
1711+ if peer_id is None or peer_id != self.__peer_id:
1712+ return False
1713+ self.__state = 'deactivated'
1714+ return True
1715+
1716+ def free(self, peer_id):
1717+ with self.__lock:
1718+ if self.__state not in ('unbound', 'deactivated'):
1719+ return False
1720+ if peer_id is None or peer_id != self.__peer_id:
1721+ return False
1722+ self.__state = 'free'
1723+ self.__peer_id = None
1724+ return True
1725+
1726+
1727+class AvahiPeer(GObject.GObject):
1728+ __gsignals__ = {
1729+ 'offer': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
1730+ [GObject.TYPE_PYOBJECT]
1731+ ),
1732+ 'accept': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
1733+ [GObject.TYPE_PYOBJECT]
1734+ ),
1735+ 'retract': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
1736+ []
1737+ ),
1738+ }
1739+
1740+ def __init__(self, pki, client_mode=False):
1741+ super().__init__()
1742 self.group = None
1743- self.id = _id
1744- log.info('This cert_id = %s', _id)
1745+ self.pki = pki
1746+ self.client_mode = client_mode
1747+ self.id = (pki.machine.id if client_mode else pki.user.id)
1748+ self.cert_file = pki.verify_ca(self.id)
1749+ self.key_file = pki.verify_key(self.id)
1750+ self.state = State()
1751+ self.peer = None
1752+ self.info = None
1753 self.bus = dbus.SystemBus()
1754 self.avahi = self.bus.get_object('org.freedesktop.Avahi', '/')
1755
1756 def __del__(self):
1757 self.unpublish()
1758
1759- def browse(self, service):
1760- self.bservice = service
1761- log.info('Avahi(%s): browsing...', service)
1762- browser_path = self.avahi.ServiceBrowserNew(
1763- -1, # Interface
1764- 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
1765- service,
1766- 'local',
1767- 0, # Flags
1768- dbus_interface='org.freedesktop.Avahi.Server'
1769- )
1770- self.browser = self.bus.get_object('org.freedesktop.Avahi', browser_path)
1771- self.browser.connect_to_signal('ItemNew', self.on_ItemNew)
1772- self.browser.connect_to_signal('ItemRemove', self.on_ItemRemove)
1773-
1774- def publish(self, service, port):
1775- self.pservice = service
1776+ def activate(self, peer_id):
1777+ if not self.state.activate(peer_id):
1778+ raise Exception(
1779+ 'Cannot activate {!r} from {!r}'.format(peer_id, self.state)
1780+ )
1781+ log.info('Activated session with %r', self.peer)
1782+ assert self.state.state == 'activated'
1783+ assert self.state.peer_id == peer_id
1784+ assert self.peer.id == peer_id
1785+ assert self.info.id == peer_id
1786+ assert self.info.url == 'https://{}:{}/'.format(
1787+ self.peer.ip, self.peer.port
1788+ )
1789+
1790+ def deactivate(self, peer_id):
1791+ if not self.state.deactivate(peer_id):
1792+ raise Exception(
1793+ 'Cannot deactivate {!r} from {!r}'.format(peer_id, self.state)
1794+ )
1795+ log.info('Deactivated session with %r', self.peer)
1796+ assert self.state.state == 'deactivated'
1797+ assert self.state.peer_id == peer_id
1798+ assert self.peer.id == peer_id
1799+ assert self.info.id == peer_id
1800+ assert self.info.url == 'https://{}:{}/'.format(
1801+ self.peer.ip, self.peer.port
1802+ )
1803+ GObject.timeout_add(15 * 1000, self.on_timeout, peer_id)
1804+
1805+ def abort(self, peer_id):
1806+ GObject.idle_add(self.unbind, peer_id)
1807+
1808+ def unbind(self, peer_id):
1809+ retract = (self.state.state == 'verified')
1810+ if not self.state.unbind(peer_id):
1811+ log.error('Cannot unbind %s from %r', peer_id, self.state)
1812+ return
1813+ log.info('Unbound from %s', peer_id)
1814+ assert self.state.peer_id == peer_id
1815+ assert self.state.state == 'unbound'
1816+ if retract:
1817+ log.info("Firing 'retract' signal")
1818+ self.emit('retract')
1819+ GObject.timeout_add(10 * 1000, self.on_timeout, peer_id)
1820+
1821+ def on_timeout(self, peer_id):
1822+ if not self.state.free(peer_id):
1823+ log.error('Cannot free %s from %r', peer_id, self.state)
1824+ return
1825+ log.info('Rate-limiting timeout reached, freeing from %s', peer_id)
1826+ assert self.state.state == 'free'
1827+ assert self.state.peer_id is None
1828+ self.info = None
1829+ self.peer = None
1830+
1831+ def get_server_config(self):
1832+ """
1833+ Get the initial server SSL config.
1834+ """
1835+ assert self.state.state in ('free', 'activated')
1836+ config = {
1837+ 'key_file': self.key_file,
1838+ 'cert_file': self.cert_file,
1839+ }
1840+ if self.client_mode is False or self.state.state == 'activated':
1841+ config['ca_file'] = self.pki.verify_ca(self.state.peer_id)
1842+ return config
1843+
1844+ def get_client_config(self):
1845+ """
1846+ Get the client SSL config.
1847+ """
1848+ assert self.state.state == 'activated'
1849+ return {
1850+ 'ca_file': self.pki.verify_ca(self.state.peer_id),
1851+ 'check_hostname': False,
1852+ 'key_file': self.key_file,
1853+ 'cert_file': self.cert_file,
1854+ }
1855+
1856+ def publish(self, port):
1857+ verb = ('offer' if self.client_mode else 'accept')
1858+ service = get_service(verb)
1859 self.group = self.bus.get_object(
1860 'org.freedesktop.Avahi',
1861 self.avahi.EntryGroupNew(
1862@@ -67,11 +277,11 @@
1863 )
1864 )
1865 log.info(
1866- 'Avahi(%s): publishing %s on port %s', service, self.id, port
1867+ 'Publishing %s for %r on port %s', self.id, service, port
1868 )
1869 self.group.AddService(
1870 -1, # Interface
1871- 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
1872+ PROTO, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
1873 0, # Flags
1874 self.id,
1875 service,
1876@@ -85,37 +295,125 @@
1877
1878 def unpublish(self):
1879 if self.group is not None:
1880- log.info('Avahi(%s): unpublishing %s', self.pservice, self.id)
1881+ log.info('Un-publishing %s', self.id)
1882 self.group.Reset(dbus_interface='org.freedesktop.Avahi.EntryGroup')
1883 self.group = None
1884
1885- def on_ItemNew(self, interface, protocol, key, _type, domain, flags):
1886+ def browse(self):
1887+ verb = ('accept' if self.client_mode else 'offer')
1888+ service = get_service(verb)
1889+ log.info('Browsing for %r', service)
1890+ path = self.avahi.ServiceBrowserNew(
1891+ -1, # Interface
1892+ PROTO, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
1893+ service,
1894+ '', # Domain, default to .local
1895+ 0, # Flags
1896+ dbus_interface='org.freedesktop.Avahi.Server'
1897+ )
1898+ self.browser = self.bus.get_object('org.freedesktop.Avahi', path)
1899+ self.browser.connect_to_signal('ItemNew', self.on_ItemNew)
1900+ self.browser.connect_to_signal('ItemRemove', self.on_ItemRemove)
1901+
1902+ def on_ItemNew(self, interface, protocol, peer_id, _type, domain, flags):
1903+ log.info('Peer added: %s', peer_id)
1904+ if not self.state.bind(str(peer_id)):
1905+ log.error('Cannot bind %s from %r', peer_id, self.state)
1906+ log.warning('Possible attack from %s', peer_id)
1907+ return
1908+ assert self.state.state == 'bound'
1909+ assert self.state.peer_id == peer_id
1910+ log.info('Bound to %s', peer_id)
1911 self.avahi.ResolveService(
1912- interface, protocol, key, _type, domain, -1, 0,
1913+ # 2nd to last arg is Protocol, again for some reason
1914+ interface, protocol, peer_id, _type, domain, PROTO, 0,
1915 dbus_interface='org.freedesktop.Avahi.Server',
1916 reply_handler=self.on_reply,
1917 error_handler=self.on_error,
1918 )
1919
1920 def on_reply(self, *args):
1921- key = args[2]
1922+ peer_id = args[2]
1923+ if self.state.peer_id != peer_id or self.state.state != 'bound':
1924+ log.error(
1925+ '%s: state mismatch in on_reply(): %r', peer_id, self.state
1926+ )
1927+ return
1928 (ip, port) = args[7:9]
1929- url = 'http://{}:{}/'.format(ip, port)
1930- log.info('Avahi(%s): new peer %s at %s', self.bservice, key, url)
1931-
1932- def on_error(self, exception):
1933- log.error('%s: error calling ResolveService(): %r', self.bservice, exception)
1934-
1935- def on_ItemRemove(self, interface, protocol, key, _type, domain, flags):
1936- log.info('Avahi(%s): peer removed: %s', self.bservice, key)
1937-
1938-
1939-
1940-class Browser:
1941- def __init__(self, service, add_callback, remove_callback):
1942- self.service = service
1943- self.add_callback = add_callback
1944- self.remove_callback = remove_callback
1945-
1946-
1947-
1948+ log.info('%s is at %s, port %s', peer_id, ip, port)
1949+ self.peer = Peer(str(peer_id), str(ip), int(port))
1950+ _start_thread(self.cert_thread, self.peer)
1951+
1952+ def on_error(self, error):
1953+ log.error(
1954+ '%s: error calling ResolveService(): %r', self.state.peer_id, error
1955+ )
1956+ self.abort(self.state.peer_id)
1957+
1958+ def on_ItemRemove(self, interface, protocol, peer_id, _type, domain, flags):
1959+ log.info('Peer removed: %s', peer_id)
1960+ self.abort(peer_id)
1961+
1962+ def cert_thread(self, peer):
1963+ # 1 Retrieve the peer certificate:
1964+ try:
1965+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
1966+ ctx.options |= ssl.OP_NO_COMPRESSION
1967+ if self.client_mode:
1968+ # The server will only let its cert be retrieved by the client
1969+ # bound to the peering session
1970+ ctx.load_cert_chain(self.cert_file, self.key_file)
1971+ sock = ctx.wrap_socket(
1972+ socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1973+ )
1974+ sock.connect((peer.ip, peer.port))
1975+ pem = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True))
1976+ except Exception as e:
1977+ log.exception('Could not retrieve cert for %r', peer)
1978+ return self.abort(peer.id)
1979+ log.info('Retrieved cert for %r', peer)
1980+
1981+ # 2 Make sure peer cert has correct intrinsic CN, etc:
1982+ try:
1983+ ca_file = self.pki.write_ca(peer.id, pem.encode('ascii'))
1984+ except Exception as e:
1985+ log.exception('Could not verify cert for %r', peer)
1986+ return self.abort(peer.id)
1987+ log.info('Verified cert for %r', peer)
1988+
1989+ # 3 Make get request to verify peer has private key:
1990+ try:
1991+ url = 'https://{}:{}/'.format(peer.ip, peer.port)
1992+ ssl_config = {
1993+ 'ca_file': ca_file,
1994+ 'check_hostname': False,
1995+ }
1996+ if self.client_mode:
1997+ ssl_config.update({
1998+ 'key_file': self.key_file,
1999+ 'cert_file': self.cert_file,
2000+ })
2001+ client = CouchBase({'url': url, 'ssl': ssl_config})
2002+ d = client.get()
2003+ info = Info(d['user'], d['host'], url, peer.id)
2004+ except Exception as e:
2005+ log.exception('GET / failed for %r', peer)
2006+ return self.abort(peer.id)
2007+ log.info('GET / succeeded with %r', info)
2008+ GObject.idle_add(self.on_cert_complete, peer, info)
2009+
2010+ def on_cert_complete(self, peer, info):
2011+ if not self.state.verify(peer.id):
2012+ log.error(
2013+ '%s: mismatch in on_cert_complete(): %r', peer.id, self.state
2014+ )
2015+ return
2016+ assert self.state.state == 'verified'
2017+ assert self.state.peer_id == peer.id
2018+ assert peer is self.peer
2019+ assert self.info is None
2020+ self.info = info
2021+ log.info('Cert checked-out for %r', peer)
2022+ signal = ('accept' if self.client_mode else 'offer')
2023+ log.info('Firing %r signal for %r', signal, info)
2024+ self.emit(signal, info)
2025
2026=== modified file 'dmedia/tests/test_peering.py'
2027--- dmedia/tests/test_peering.py 2012-10-01 20:00:25 +0000
2028+++ dmedia/tests/test_peering.py 2012-10-09 20:57:30 +0000
2029@@ -27,10 +27,15 @@
2030 import os
2031 from os import path
2032 import subprocess
2033+import socket
2034+from queue import Queue
2035
2036-from microfiber import random_id
2037+import microfiber
2038+from microfiber import random_id, CouchBase
2039
2040 from .base import TempDir
2041+from dmedia.httpd import make_server
2042+from dmedia.peering import encode, decode
2043 from dmedia import peering
2044
2045
2046@@ -162,6 +167,445 @@
2047 os.remove(key_file)
2048 self.assertEqual(peering.get_csr_subject(csr_file), subject)
2049
2050+ def test_get_issuer(self):
2051+ tmp = TempDir()
2052+
2053+ foo_subject = '/CN={}'.format(random_id(30))
2054+ foo_key = tmp.join('foo.key')
2055+ foo_ca = tmp.join('foo.ca')
2056+ foo_srl = tmp.join('foo.srl')
2057+ peering.create_key(foo_key)
2058+ peering.create_ca(foo_key, foo_subject, foo_ca)
2059+ self.assertEqual(peering.get_issuer(foo_ca), foo_subject)
2060+
2061+ bar_subject = '/CN={}'.format(random_id(30))
2062+ bar_key = tmp.join('bar.key')
2063+ bar_csr = tmp.join('bar.csr')
2064+ bar_cert = tmp.join('bar.cert')
2065+ peering.create_key(bar_key)
2066+ peering.create_csr(bar_key, bar_subject, bar_csr)
2067+ peering.issue_cert(bar_csr, foo_ca, foo_key, foo_srl, bar_cert)
2068+ self.assertEqual(peering.get_csr_subject(bar_csr), bar_subject)
2069+ self.assertEqual(peering.get_issuer(bar_cert), foo_subject)
2070+
2071+ def test_ssl_verify(self):
2072+ tmp = TempDir()
2073+ pki = peering.PKI(tmp.dir)
2074+
2075+ ca1 = pki.create_key()
2076+ pki.create_ca(ca1)
2077+ cert1 = pki.create_key()
2078+ pki.create_csr(cert1)
2079+ pki.issue_cert(cert1, ca1)
2080+ ca1_file = pki.path(ca1, 'ca')
2081+ cert1_file = pki.path(cert1, 'cert')
2082+ self.assertEqual(peering.ssl_verify(ca1_file, ca1_file), ca1_file)
2083+ self.assertEqual(peering.ssl_verify(cert1_file, ca1_file), cert1_file)
2084+ with self.assertRaises(peering.VerificationError) as cm:
2085+ peering.ssl_verify(ca1_file, cert1_file)
2086+ with self.assertRaises(peering.VerificationError) as cm:
2087+ peering.ssl_verify(cert1_file, cert1_file)
2088+
2089+ ca2 = pki.create_key()
2090+ pki.create_ca(ca2)
2091+ cert2 = pki.create_key()
2092+ pki.create_csr(cert2)
2093+ pki.issue_cert(cert2, ca2)
2094+ ca2_file = pki.path(ca2, 'ca')
2095+ cert2_file = pki.path(cert2, 'cert')
2096+ self.assertEqual(peering.ssl_verify(ca2_file, ca2_file), ca2_file)
2097+ self.assertEqual(peering.ssl_verify(cert2_file, ca2_file), cert2_file)
2098+ with self.assertRaises(peering.VerificationError) as cm:
2099+ peering.ssl_verify(ca2_file, cert2_file)
2100+ with self.assertRaises(peering.VerificationError) as cm:
2101+ peering.ssl_verify(cert2_file, cert2_file)
2102+
2103+ with self.assertRaises(peering.VerificationError) as cm:
2104+ peering.ssl_verify(ca2_file, ca1_file)
2105+ with self.assertRaises(peering.VerificationError) as cm:
2106+ peering.ssl_verify(cert2_file, ca1_file)
2107+ with self.assertRaises(peering.VerificationError) as cm:
2108+ peering.ssl_verify(cert2_file, cert1_file)
2109+
2110+
2111+class TestChallengeResponse(TestCase):
2112+ def test_init(self):
2113+ id1 = random_id(30)
2114+ id2 = random_id(30)
2115+ inst = peering.ChallengeResponse(id1, id2)
2116+ self.assertIs(inst.id, id1)
2117+ self.assertIs(inst.peer_id, id2)
2118+ self.assertEqual(inst.local_hash, peering.decode(id1))
2119+ self.assertEqual(inst.remote_hash, peering.decode(id2))
2120+
2121+ def test_get_secret(self):
2122+ id1 = random_id(30)
2123+ id2 = random_id(30)
2124+ inst = peering.ChallengeResponse(id1, id2)
2125+ s1 = inst.get_secret()
2126+ self.assertIsInstance(s1, str)
2127+ self.assertEqual(len(s1), 8)
2128+ self.assertEqual(peering.decode(s1), inst.secret)
2129+ s2 = inst.get_secret()
2130+ self.assertNotEqual(s1, s2)
2131+ self.assertIsInstance(s2, str)
2132+ self.assertEqual(len(s2), 8)
2133+ self.assertEqual(peering.decode(s2), inst.secret)
2134+
2135+ def test_set_secret(self):
2136+ id1 = random_id(30)
2137+ id2 = random_id(30)
2138+ inst = peering.ChallengeResponse(id1, id2)
2139+ s1 = random_id(5)
2140+ self.assertIsNone(inst.set_secret(s1))
2141+ self.assertEqual(peering.encode(inst.secret), s1)
2142+ s2 = random_id(5)
2143+ self.assertIsNone(inst.set_secret(s2))
2144+ self.assertEqual(peering.encode(inst.secret), s2)
2145+
2146+ def test_get_challenge(self):
2147+ id1 = random_id(30)
2148+ id2 = random_id(30)
2149+ inst = peering.ChallengeResponse(id1, id2)
2150+ c1 = inst.get_challenge()
2151+ self.assertIsInstance(c1, str)
2152+ self.assertEqual(len(c1), 32)
2153+ self.assertEqual(peering.decode(c1), inst.challenge)
2154+ c2 = inst.get_challenge()
2155+ self.assertNotEqual(c1, c2)
2156+ self.assertIsInstance(c2, str)
2157+ self.assertEqual(len(c2), 32)
2158+ self.assertEqual(peering.decode(c2), inst.challenge)
2159+
2160+ def test_create_response(self):
2161+ id1 = random_id(30)
2162+ id2 = random_id(30)
2163+ inst = peering.ChallengeResponse(id1, id2)
2164+ local_hash = decode(id1)
2165+ remote_hash = decode(id2)
2166+ secret1 = random_id(5)
2167+ challenge1 = random_id(20)
2168+ inst.set_secret(secret1)
2169+ (nonce1, response1) = inst.create_response(challenge1)
2170+ self.assertIsInstance(nonce1, str)
2171+ self.assertEqual(len(nonce1), 32)
2172+ self.assertIsInstance(response1, str)
2173+ self.assertEqual(len(response1), 56)
2174+ self.assertEqual(response1,
2175+ peering.compute_response(
2176+ decode(secret1), decode(challenge1), decode(nonce1),
2177+ remote_hash, local_hash
2178+ )
2179+ )
2180+
2181+ # Same secret and challenge, make sure a new nonce is used
2182+ (nonce2, response2) = inst.create_response(challenge1)
2183+ self.assertNotEqual(nonce2, nonce1)
2184+ self.assertNotEqual(response2, response1)
2185+ self.assertIsInstance(nonce2, str)
2186+ self.assertEqual(len(nonce2), 32)
2187+ self.assertIsInstance(response2, str)
2188+ self.assertEqual(len(response2), 56)
2189+ self.assertEqual(response2,
2190+ peering.compute_response(
2191+ decode(secret1), decode(challenge1), decode(nonce2),
2192+ remote_hash, local_hash
2193+ )
2194+ )
2195+
2196+ # Different secret
2197+ secret2 = random_id(5)
2198+ inst.set_secret(secret2)
2199+ (nonce3, response3) = inst.create_response(challenge1)
2200+ self.assertNotEqual(nonce3, nonce1)
2201+ self.assertNotEqual(response3, response1)
2202+ self.assertNotEqual(nonce3, nonce2)
2203+ self.assertNotEqual(response3, response2)
2204+ self.assertIsInstance(nonce3, str)
2205+ self.assertEqual(len(nonce3), 32)
2206+ self.assertIsInstance(response3, str)
2207+ self.assertEqual(len(response3), 56)
2208+ self.assertEqual(response3,
2209+ peering.compute_response(
2210+ decode(secret2), decode(challenge1), decode(nonce3),
2211+ remote_hash, local_hash
2212+ )
2213+ )
2214+
2215+ # Different challenge
2216+ challenge2 = random_id(20)
2217+ (nonce4, response4) = inst.create_response(challenge2)
2218+ self.assertNotEqual(nonce4, nonce1)
2219+ self.assertNotEqual(response4, response1)
2220+ self.assertNotEqual(nonce4, nonce2)
2221+ self.assertNotEqual(response4, response2)
2222+ self.assertNotEqual(nonce4, nonce3)
2223+ self.assertNotEqual(response4, response3)
2224+ self.assertIsInstance(nonce4, str)
2225+ self.assertEqual(len(nonce4), 32)
2226+ self.assertIsInstance(response4, str)
2227+ self.assertEqual(len(response4), 56)
2228+ self.assertEqual(response4,
2229+ peering.compute_response(
2230+ decode(secret2), decode(challenge2), decode(nonce4),
2231+ remote_hash, local_hash
2232+ )
2233+ )
2234+
2235+ def test_check_response(self):
2236+ id1 = random_id(30)
2237+ id2 = random_id(30)
2238+ inst = peering.ChallengeResponse(id1, id2)
2239+ local_hash = decode(id1)
2240+ remote_hash = decode(id2)
2241+ secret = inst.get_secret()
2242+ challenge = inst.get_challenge()
2243+ nonce = random_id(20)
2244+ response = peering.compute_response(
2245+ decode(secret), decode(challenge), decode(nonce),
2246+ local_hash, remote_hash
2247+ )
2248+ self.assertIsNone(inst.check_response(nonce, response))
2249+
2250+ # Test with (local, remote) order flipped
2251+ bad = peering.compute_response(
2252+ decode(secret), decode(challenge), decode(nonce),
2253+ remote_hash, local_hash
2254+ )
2255+ with self.assertRaises(peering.WrongResponse) as cm:
2256+ inst.check_response(nonce, bad)
2257+ self.assertEqual(cm.exception.expected, response)
2258+ self.assertEqual(cm.exception.got, bad)
2259+ self.assertFalse(hasattr(inst, 'secret'))
2260+ self.assertFalse(hasattr(inst, 'challenge'))
2261+ inst.secret = decode(secret)
2262+ inst.challenge = decode(challenge)
2263+
2264+ # Test with wrong secret
2265+ for i in range(100):
2266+ bad = peering.compute_response(
2267+ os.urandom(5), decode(challenge), decode(nonce),
2268+ local_hash, remote_hash
2269+ )
2270+ with self.assertRaises(peering.WrongResponse) as cm:
2271+ inst.check_response(nonce, bad)
2272+ self.assertEqual(cm.exception.expected, response)
2273+ self.assertEqual(cm.exception.got, bad)
2274+ self.assertFalse(hasattr(inst, 'secret'))
2275+ self.assertFalse(hasattr(inst, 'challenge'))
2276+ inst.secret = decode(secret)
2277+ inst.challenge = decode(challenge)
2278+
2279+ # Test with wrong challenge
2280+ for i in range(100):
2281+ bad = peering.compute_response(
2282+ decode(secret), os.urandom(20), decode(nonce),
2283+ local_hash, remote_hash
2284+ )
2285+ with self.assertRaises(peering.WrongResponse) as cm:
2286+ inst.check_response(nonce, bad)
2287+ self.assertEqual(cm.exception.expected, response)
2288+ self.assertEqual(cm.exception.got, bad)
2289+ self.assertFalse(hasattr(inst, 'secret'))
2290+ self.assertFalse(hasattr(inst, 'challenge'))
2291+ inst.secret = decode(secret)
2292+ inst.challenge = decode(challenge)
2293+
2294+ # Test with wrong nonce
2295+ for i in range(100):
2296+ bad = peering.compute_response(
2297+ decode(secret), decode(challenge), os.urandom(20),
2298+ local_hash, remote_hash
2299+ )
2300+ with self.assertRaises(peering.WrongResponse) as cm:
2301+ inst.check_response(nonce, bad)
2302+ self.assertEqual(cm.exception.expected, response)
2303+ self.assertEqual(cm.exception.got, bad)
2304+ self.assertFalse(hasattr(inst, 'secret'))
2305+ self.assertFalse(hasattr(inst, 'challenge'))
2306+ inst.secret = decode(secret)
2307+ inst.challenge = decode(challenge)
2308+
2309+ # Test with wrong local_hash
2310+ for i in range(100):
2311+ bad = peering.compute_response(
2312+ decode(secret), decode(challenge), decode(nonce),
2313+ os.urandom(30), remote_hash
2314+ )
2315+ with self.assertRaises(peering.WrongResponse) as cm:
2316+ inst.check_response(nonce, bad)
2317+ self.assertEqual(cm.exception.expected, response)
2318+ self.assertEqual(cm.exception.got, bad)
2319+ self.assertFalse(hasattr(inst, 'secret'))
2320+ self.assertFalse(hasattr(inst, 'challenge'))
2321+ inst.secret = decode(secret)
2322+ inst.challenge = decode(challenge)
2323+
2324+ # Test with wrong remote_hash
2325+ for i in range(100):
2326+ bad = peering.compute_response(
2327+ decode(secret), decode(challenge), decode(nonce),
2328+ local_hash, os.urandom(30)
2329+ )
2330+ with self.assertRaises(peering.WrongResponse) as cm:
2331+ inst.check_response(nonce, bad)
2332+ self.assertEqual(cm.exception.expected, response)
2333+ self.assertEqual(cm.exception.got, bad)
2334+ self.assertFalse(hasattr(inst, 'secret'))
2335+ self.assertFalse(hasattr(inst, 'challenge'))
2336+ inst.secret = decode(secret)
2337+ inst.challenge = decode(challenge)
2338+
2339+ # Test with more nonce, used as expected:
2340+ for i in range(100):
2341+ newnonce = random_id(20)
2342+ good = peering.compute_response(
2343+ decode(secret), decode(challenge), decode(newnonce),
2344+ local_hash, remote_hash
2345+ )
2346+ self.assertNotEqual(good, response)
2347+ self.assertIsNone(inst.check_response(newnonce, good))
2348+
2349+ # Sanity check on directionality, in other words, check that the
2350+ # response created locally can't accidentally be verified as the
2351+ # response from the other end
2352+ secret = random_id(5)
2353+ for i in range(1000):
2354+ inst.set_secret(secret)
2355+ challenge = inst.get_challenge()
2356+ (nonce, response) = inst.create_response(challenge)
2357+ with self.assertRaises(peering.WrongResponse) as cm:
2358+ inst.check_response(nonce, response)
2359+
2360+
2361+class TestServerApp(TestCase):
2362+ def test_live(self):
2363+ tmp = TempDir()
2364+ pki = peering.PKI(tmp.dir)
2365+ local_id = pki.create_key()
2366+ pki.create_ca(local_id)
2367+ remote_id = pki.create_key()
2368+ pki.create_ca(remote_id)
2369+ server_config = {
2370+ 'cert_file': pki.path(local_id, 'ca'),
2371+ 'key_file': pki.path(local_id, 'key'),
2372+ 'ca_file': pki.path(remote_id, 'ca'),
2373+ }
2374+ client_config = {
2375+ 'check_hostname': False,
2376+ 'ca_file': pki.path(local_id, 'ca'),
2377+ 'cert_file': pki.path(remote_id, 'ca'),
2378+ 'key_file': pki.path(remote_id, 'key'),
2379+ }
2380+ local = peering.ChallengeResponse(local_id, remote_id)
2381+ remote = peering.ChallengeResponse(remote_id, local_id)
2382+ q = Queue()
2383+ app = peering.ServerApp(local, q, None)
2384+ server = make_server(app, '127.0.0.1', server_config)
2385+ client = CouchBase({'url': server.url, 'ssl': client_config})
2386+ server.start()
2387+ secret = local.get_secret()
2388+ remote.set_secret(secret)
2389+
2390+ self.assertIsNone(app.state)
2391+ with self.assertRaises(microfiber.BadRequest) as cm:
2392+ client.get('')
2393+ self.assertEqual(
2394+ str(cm.exception),
2395+ '400 Bad Request State: GET /'
2396+ )
2397+ app.state = 'info'
2398+ self.assertEqual(client.get(),
2399+ {
2400+ 'id': local_id,
2401+ 'user': os.environ.get('USER'),
2402+ 'host': socket.gethostname(),
2403+ }
2404+ )
2405+ self.assertEqual(app.state, 'ready')
2406+ with self.assertRaises(microfiber.BadRequest) as cm:
2407+ client.get('')
2408+ self.assertEqual(
2409+ str(cm.exception),
2410+ '400 Bad Request State: GET /'
2411+ )
2412+ self.assertEqual(app.state, 'ready')
2413+
2414+ app.state = 'info'
2415+ with self.assertRaises(microfiber.BadRequest) as cm:
2416+ client.get('challenge')
2417+ self.assertEqual(
2418+ str(cm.exception),
2419+ '400 Bad Request Order: GET /challenge'
2420+ )
2421+ with self.assertRaises(microfiber.BadRequest) as cm:
2422+ client.put({'hello': 'world'}, 'response')
2423+ self.assertEqual(
2424+ str(cm.exception),
2425+ '400 Bad Request Order: PUT /response'
2426+ )
2427+
2428+ app.state = 'ready'
2429+ self.assertEqual(app.state, 'ready')
2430+ obj = client.get('challenge')
2431+ self.assertEqual(app.state, 'gave_challenge')
2432+ self.assertIsInstance(obj, dict)
2433+ self.assertEqual(set(obj), set(['challenge']))
2434+ self.assertEqual(local.challenge, decode(obj['challenge']))
2435+ with self.assertRaises(microfiber.BadRequest) as cm:
2436+ client.get('challenge')
2437+ self.assertEqual(
2438+ str(cm.exception),
2439+ '400 Bad Request Order: GET /challenge'
2440+ )
2441+ self.assertEqual(app.state, 'gave_challenge')
2442+
2443+ (nonce, response) = remote.create_response(obj['challenge'])
2444+ obj = {'nonce': nonce, 'response': response}
2445+ self.assertEqual(client.put(obj, 'response'), {'ok': True})
2446+ self.assertEqual(app.state, 'response_ok')
2447+ with self.assertRaises(microfiber.BadRequest) as cm:
2448+ client.put(obj, 'response')
2449+ self.assertEqual(
2450+ str(cm.exception),
2451+ '400 Bad Request Order: PUT /response'
2452+ )
2453+ self.assertEqual(app.state, 'response_ok')
2454+ self.assertEqual(q.get(), 'response_ok')
2455+
2456+ # Test when an error occurs in put_response()
2457+ app.state = 'gave_challenge'
2458+ with self.assertRaises(microfiber.ServerError) as cm:
2459+ client.put(b'bad json', 'response')
2460+ self.assertEqual(app.state, 'in_response')
2461+
2462+ # Test with wrong secret
2463+ app.state = 'ready'
2464+ secret = local.get_secret()
2465+ remote.get_secret()
2466+ challenge = client.get('challenge')['challenge']
2467+ self.assertEqual(app.state, 'gave_challenge')
2468+ (nonce, response) = remote.create_response(challenge)
2469+ with self.assertRaises(microfiber.Unauthorized) as cm:
2470+ client.put({'nonce': nonce, 'response': response}, 'response')
2471+ self.assertEqual(app.state, 'wrong_response')
2472+ self.assertFalse(hasattr(local, 'secret'))
2473+ self.assertFalse(hasattr(local, 'challenge'))
2474+
2475+ # Verify that you can't retry
2476+ remote.set_secret(secret)
2477+ (nonce, response) = remote.create_response(challenge)
2478+ with self.assertRaises(microfiber.BadRequest) as cm:
2479+ client.put({'nonce': nonce, 'response': response}, 'response')
2480+ self.assertEqual(
2481+ str(cm.exception),
2482+ '400 Bad Request Order: PUT /response'
2483+ )
2484+ self.assertEqual(app.state, 'wrong_response')
2485+ self.assertEqual(q.get(), 'wrong_response')
2486+
2487+ server.shutdown()
2488+
2489
2490 class TestPKI(TestCase):
2491 def test_init(self):
2492@@ -229,6 +673,56 @@
2493 with self.assertRaises(subprocess.CalledProcessError) as cm:
2494 pki.verify_key(id2)
2495
2496+ def test_read_key(self):
2497+ tmp = TempDir()
2498+ pki = peering.PKI(tmp.dir)
2499+ id1 = pki.create_key()
2500+ key1_file = tmp.join(id1 + '.key')
2501+ data1 = open(key1_file, 'rb').read()
2502+ id2 = pki.create_key()
2503+ key2_file = tmp.join(id2 + '.key')
2504+ data2 = open(key2_file, 'rb').read()
2505+ self.assertEqual(pki.read_key(id1), data1)
2506+ self.assertEqual(pki.read_key(id2), data2)
2507+ os.remove(key1_file)
2508+ os.rename(key2_file, key1_file)
2509+ with self.assertRaises(peering.PublicKeyError) as cm:
2510+ pki.read_key(id1)
2511+ self.assertEqual(cm.exception.filename, key1_file)
2512+ self.assertEqual(cm.exception.expected, id1)
2513+ self.assertEqual(cm.exception.got, id2)
2514+ with self.assertRaises(subprocess.CalledProcessError) as cm:
2515+ pki.read_key(id2)
2516+
2517+ def test_write_key(self):
2518+ tmp1 = TempDir()
2519+ src = peering.PKI(tmp1.dir)
2520+ tmp2 = TempDir()
2521+ dst = peering.PKI(tmp2.dir)
2522+
2523+ id1 = src.create_key()
2524+ data1 = open(src.verify_key(id1), 'rb').read()
2525+ id2 = src.create_key()
2526+ data2 = open(src.verify_key(id2), 'rb').read()
2527+
2528+ with self.assertRaises(peering.PublicKeyError) as cm:
2529+ dst.write_key(id1, data2)
2530+ self.assertEqual(path.dirname(cm.exception.filename), dst.tmpdir)
2531+ self.assertEqual(cm.exception.expected, id1)
2532+ self.assertEqual(cm.exception.got, id2)
2533+
2534+ with self.assertRaises(peering.PublicKeyError) as cm:
2535+ dst.write_key(id2, data1)
2536+ self.assertEqual(path.dirname(cm.exception.filename), dst.tmpdir)
2537+ self.assertEqual(cm.exception.expected, id2)
2538+ self.assertEqual(cm.exception.got, id1)
2539+
2540+ self.assertEqual(dst.write_key(id1, data1), dst.path(id1, 'key'))
2541+ self.assertEqual(open(dst.path(id1, 'key'), 'rb').read(), data1)
2542+
2543+ self.assertEqual(dst.write_key(id2, data2), dst.path(id2, 'key'))
2544+ self.assertEqual(open(dst.path(id2, 'key'), 'rb').read(), data2)
2545+
2546 def test_create_ca(self):
2547 tmp = TempDir()
2548 pki = peering.PKI(tmp.dir)
2549@@ -275,6 +769,24 @@
2550 self.assertEqual(cm.exception.expected, '/CN={}'.format(id3))
2551 self.assertEqual(cm.exception.got, '/CN={}'.format(id1))
2552
2553+ # Test with bad issuer
2554+ pki.create_ca(id3)
2555+ id4 = pki.create_key()
2556+ pki.create_csr(id4)
2557+ pki.issue_cert(id4, id3)
2558+ os.rename(pki.path(id4, 'cert'), pki.path(id4, 'ca'))
2559+ with self.assertRaises(peering.IssuerError) as cm:
2560+ pki.verify_ca(id4)
2561+ self.assertEqual(cm.exception.filename, pki.path(id4, 'ca'))
2562+ self.assertEqual(cm.exception.expected, '/CN={}'.format(id4))
2563+ self.assertEqual(cm.exception.got, '/CN={}'.format(id3))
2564+
2565+ def test_read_ca(self):
2566+ self.skipTest('FIXME')
2567+
2568+ def test_write_ca(self):
2569+ self.skipTest('FIXME')
2570+
2571 def test_create_csr(self):
2572 tmp = TempDir()
2573 pki = peering.PKI(tmp.dir)
2574@@ -321,6 +833,43 @@
2575 self.assertEqual(cm.exception.expected, '/CN={}'.format(id3))
2576 self.assertEqual(cm.exception.got, '/CN={}'.format(id1))
2577
2578+ def test_read_csr(self):
2579+ tmp = TempDir()
2580+ pki = peering.PKI(tmp.dir)
2581+ id1 = pki.create_key()
2582+ id2 = pki.create_key()
2583+ csr1_file = pki.create_csr(id1)
2584+ csr2_file = pki.create_csr(id2)
2585+ data1 = open(csr1_file, 'rb').read()
2586+ data2 = open(csr2_file, 'rb').read()
2587+ os.remove(tmp.join(id1 + '.key'))
2588+ os.remove(tmp.join(id2 + '.key'))
2589+ self.assertEqual(pki.read_csr(id1), data1)
2590+ self.assertEqual(pki.read_csr(id2), data2)
2591+ os.remove(csr1_file)
2592+ os.rename(csr2_file, csr1_file)
2593+ with self.assertRaises(peering.PublicKeyError) as cm:
2594+ pki.read_csr(id1)
2595+ self.assertEqual(cm.exception.filename, csr1_file)
2596+ self.assertEqual(cm.exception.expected, id1)
2597+ self.assertEqual(cm.exception.got, id2)
2598+ with self.assertRaises(subprocess.CalledProcessError) as cm:
2599+ pki.read_csr(id2)
2600+
2601+ # Test with bad subject
2602+ id3 = pki.create_key()
2603+ key_file = pki.path(id3, 'key')
2604+ csr_file = pki.path(id3, 'csr')
2605+ peering.create_csr(key_file, '/CN={}'.format(id1), csr_file)
2606+ with self.assertRaises(peering.SubjectError) as cm:
2607+ pki.read_csr(id3)
2608+ self.assertEqual(cm.exception.filename, csr_file)
2609+ self.assertEqual(cm.exception.expected, '/CN={}'.format(id3))
2610+ self.assertEqual(cm.exception.got, '/CN={}'.format(id1))
2611+
2612+ def test_write_csr(self):
2613+ self.skipTest('FIXME')
2614+
2615 def test_issue_cert(self):
2616 tmp = TempDir()
2617 pki = peering.PKI(tmp.dir)
2618
2619=== added file 'run-browse.py'
2620--- run-browse.py 1970-01-01 00:00:00 +0000
2621+++ run-browse.py 2012-10-09 20:57:30 +0000
2622@@ -0,0 +1,188 @@
2623+#!/usr/bin/python3
2624+
2625+import logging
2626+import tempfile
2627+from gettext import gettext as _
2628+
2629+from gi.repository import GObject, Gtk, AppIndicator3
2630+from microfiber import CouchBase, _start_thread
2631+from queue import Queue
2632+
2633+from dmedia.startup import DmediaCouch
2634+from dmedia.gtk.ubuntu import NotifyManager
2635+from dmedia.peering import ChallengeResponse, ServerApp
2636+from dmedia.service.peers import AvahiPeer
2637+from dmedia.gtk.peering import BaseUI
2638+from dmedia.httpd import WSGIError, make_server
2639+
2640+
2641+format = [
2642+ '%(levelname)s',
2643+ '%(processName)s',
2644+ '%(threadName)s',
2645+ '%(message)s',
2646+]
2647+logging.basicConfig(level=logging.DEBUG, format='\t'.join(format))
2648+log = logging.getLogger()
2649+
2650+
2651+mainloop = GObject.MainLoop()
2652+
2653+
2654+
2655+class UI(BaseUI):
2656+ page = 'server.html'
2657+
2658+ signals = {
2659+ 'get_secret': [],
2660+ 'display_secret': ['secret'],
2661+ 'set_message': ['message'],
2662+ }
2663+
2664+ def __init__(self, cr):
2665+ super().__init__()
2666+ self.cr = cr
2667+
2668+ def connect_hub_signals(self, hub):
2669+ hub.connect('get_secret', self.on_get_secret)
2670+
2671+ def on_get_secret(self, hub):
2672+ secret = self.cr.get_secret()
2673+ hub.send('display_secret', secret)
2674+
2675+
2676+class Session:
2677+ def __init__(self, pki, _id, peer, server_config, client_config):
2678+ self.pki = pki
2679+ self.peer_id = peer.id
2680+ self.peer = peer
2681+ self.cr = ChallengeResponse(_id, peer.id)
2682+ self.q = Queue()
2683+ _start_thread(self.monitor_response)
2684+ self.app = ServerApp(self.cr, self.q, pki)
2685+ self.app.state = 'info'
2686+ self.httpd = make_server(self.app, '0.0.0.0', server_config)
2687+ env = {'url': peer.url, 'ssl': client_config}
2688+ self.client = CouchBase(env)
2689+ self.httpd.start()
2690+ self.ui = UI(self.cr)
2691+
2692+ def monitor_response(self):
2693+ while True:
2694+ signal = self.q.get()
2695+ if signal == 'wrong_response':
2696+ GObject.idle_add(self.retry)
2697+ elif signal == 'response_ok':
2698+ GObject.timeout_add(500, self.on_response_ok)
2699+ break
2700+
2701+ def monitor_cert_request(self):
2702+ status = self.q.get()
2703+ if status != 'cert_issued':
2704+ log.error('Bad cert request from %r', self.peer)
2705+ log.warning('Possible malicious peer: %r', self.peer)
2706+ GObject.idle_add(self.on_cert_request, status)
2707+
2708+ def retry(self):
2709+ self.httpd.shutdown()
2710+ secret = self.cr.get_secret()
2711+ self.ui.hub.send('display_secret', secret)
2712+ self.ui.hub.send('set_message',
2713+ _('Typo? Please try again with new secret.')
2714+ )
2715+ self.app.state = 'ready'
2716+ self.httpd.start()
2717+
2718+ def on_response_ok(self):
2719+ assert self.app.state == 'response_ok'
2720+ self.ui.hub.send('set_message', _('Counter-Challenge...'))
2721+ _start_thread(self.counter_challenge)
2722+
2723+ def counter_challenge(self):
2724+ log.info('Getting counter-challenge from %r', self.peer)
2725+ challenge = self.client.get('challenge')['challenge']
2726+ (nonce, response) = self.cr.create_response(challenge)
2727+ obj = {'nonce': nonce, 'response': response}
2728+ log.info('Posting counter-response to %r', self.peer)
2729+ try:
2730+ r = self.client.put(obj, 'response')
2731+ log.info('Counter-response accepted')
2732+ GObject.idle_add(self.on_counter_response_ok)
2733+ except Unauthorized:
2734+ log.error('Counter-response rejected!')
2735+ log.warning('Possible malicious peer: %r', self.peer)
2736+ GObject.idle_add(self.on_counter_response_fail)
2737+
2738+ def on_counter_response_ok(self):
2739+ assert self.app.state == 'response_ok'
2740+ self.app.state = 'counter_response_ok'
2741+ _start_thread(self.monitor_cert_request)
2742+ self.ui.hub.send('set_message', _('Issuing Certificate...'))
2743+
2744+ def on_counter_response_fail(self):
2745+ self.ui.hub.send('set_message', _('Very Bad Things!'))
2746+
2747+ def on_cert_request(self, status):
2748+ print('on_cert_request', status)
2749+ self.ui.hub.send('set_message', _('Done!'))
2750+
2751+
2752+class Browse:
2753+ def __init__(self):
2754+ self.couch = DmediaCouch(tempfile.mkdtemp())
2755+ self.couch.firstrun_init(create_user=True)
2756+ self.couch.load_pki()
2757+ self.avahi = AvahiPeer(self.couch.pki)
2758+ self.avahi.connect('offer', self.on_offer)
2759+ self.avahi.connect('retract', self.on_retract)
2760+ self.avahi.browse()
2761+ self.notifymanager = NotifyManager()
2762+ self.indicator = None
2763+ self.session = None
2764+
2765+ def on_offer(self, avahi, info):
2766+ assert self.indicator is None
2767+ self.indicator = AppIndicator3.Indicator.new(
2768+ 'dmedia-peer',
2769+ 'indicator-novacut',
2770+ AppIndicator3.IndicatorCategory.APPLICATION_STATUS
2771+ )
2772+ menu = Gtk.Menu()
2773+ accept = Gtk.MenuItem()
2774+ accept.set_label(_('Accept {}@{}').format(info.name, info.host))
2775+ accept.connect('activate', self.on_accept, info)
2776+ menu.append(accept)
2777+ menu.show_all()
2778+ self.indicator.set_menu(menu)
2779+ self.indicator.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
2780+ self.notifymanager.replace(
2781+ _('Novacut Peering Offer'),
2782+ '{}@{}'.format(info.name, info.host),
2783+ )
2784+
2785+ def on_retract(self, avahi):
2786+ if hasattr(self, 'indicator'):
2787+ del self.indicator
2788+ self.notifymanager.replace(_('Peering Offer Removed'))
2789+
2790+ def on_accept(self, menuitem, info):
2791+ assert self.session is None
2792+ self.avahi.activate(info.id)
2793+ self.indicator = None
2794+ self.session = Session(self.couch.pki, self.avahi.id, info,
2795+ self.avahi.get_server_config(),
2796+ self.avahi.get_client_config()
2797+ )
2798+ self.session.ui.window.connect('destroy', self.on_destroy)
2799+ self.session.ui.show()
2800+ self.avahi.publish(self.session.httpd.port)
2801+
2802+ def on_destroy(self, *args):
2803+ self.session.httpd.shutdown()
2804+ self.session.ui.window.destroy()
2805+ self.avahi.deactivate(self.session.peer_id)
2806+ self.session = None
2807+
2808+browse = Browse()
2809+mainloop.run()
2810+
2811
2812=== removed file 'run-browse.py'
2813--- run-browse.py 2012-09-20 12:27:37 +0000
2814+++ run-browse.py 1970-01-01 00:00:00 +0000
2815@@ -1,24 +0,0 @@
2816-#!/usr/bin/python3
2817-
2818-import logging
2819-
2820-import dbus
2821-from dbus.mainloop.glib import DBusGMainLoop
2822-from gi.repository import GObject
2823-from microfiber import random_id
2824-
2825-from dmedia.service.peers import Peer
2826-from dmedia.peering import TempPKI
2827-
2828-log = logging.getLogger()
2829-GObject.threads_init()
2830-DBusGMainLoop(set_as_default=True)
2831-logging.basicConfig(level=logging.DEBUG)
2832-
2833-pki = TempPKI()
2834-cert_id = pki.create(random_id())
2835-
2836-peer = Peer(cert_id)
2837-peer.browse('_dmedia-offer._tcp')
2838-mainloop = GObject.MainLoop()
2839-mainloop.run()
2840
2841=== added file 'run-publish.py'
2842--- run-publish.py 1970-01-01 00:00:00 +0000
2843+++ run-publish.py 2012-10-09 20:57:30 +0000
2844@@ -0,0 +1,196 @@
2845+#!/usr/bin/python3
2846+
2847+import logging
2848+import tempfile
2849+from queue import Queue
2850+from gettext import gettext as _
2851+from base64 import b64encode, b64decode
2852+
2853+from gi.repository import GObject, Gtk
2854+from microfiber import dumps, CouchBase, Unauthorized, _start_thread
2855+
2856+from dmedia.startup import DmediaCouch
2857+from dmedia import peering
2858+from dmedia.service.peers import AvahiPeer
2859+from dmedia.gtk.peering import BaseUI
2860+from dmedia.peering import ChallengeResponse, ClientApp, InfoApp, encode, decode
2861+from dmedia.httpd import WSGIError, make_server, build_server_ssl_context
2862+
2863+
2864+format = [
2865+ '%(levelname)s',
2866+ '%(processName)s',
2867+ '%(threadName)s',
2868+ '%(message)s',
2869+]
2870+logging.basicConfig(level=logging.DEBUG, format='\t'.join(format))
2871+log = logging.getLogger()
2872+
2873+
2874+class Session:
2875+ def __init__(self, hub, pki, _id, peer, client_config):
2876+ self.hub = hub
2877+ self.pki = pki
2878+ self.peer = peer
2879+ self.id = _id
2880+ self.peer_id = peer.id
2881+ self.cr = ChallengeResponse(_id, peer.id)
2882+ self.q = Queue()
2883+ self.app = ClientApp(self.cr, self.q)
2884+ env = {'url': peer.url, 'ssl': client_config}
2885+ self.client = CouchBase(env)
2886+
2887+ def challenge(self):
2888+ log.info('Getting challenge from %r', self.peer)
2889+ challenge = self.client.get('challenge')['challenge']
2890+ (nonce, response) = self.cr.create_response(challenge)
2891+ obj = {'nonce': nonce, 'response': response}
2892+ log.info('Putting response to %r', self.peer)
2893+ try:
2894+ r = self.client.put(obj, 'response')
2895+ log.info('Response accepted')
2896+ success = True
2897+ except Unauthorized:
2898+ log.info('Response rejected')
2899+ success = False
2900+ GObject.idle_add(self.on_response, success)
2901+
2902+ def on_response(self, success):
2903+ if success:
2904+ self.app.state = 'ready'
2905+ _start_thread(self.monitor_counter_response)
2906+ else:
2907+ del self.cr.secret
2908+ self.hub.send('response', success)
2909+
2910+ def monitor_counter_response(self):
2911+ # FIXME: Should use a timeout with queue.get()
2912+ status = self.q.get()
2913+ log.info('Counter-response gave %r', status)
2914+ if status != 'response_ok':
2915+ log.error('Wrong counter-response!')
2916+ log.warning('Possible malicious peer: %r', self.peer)
2917+ GObject.timeout_add(500, self.on_counter_response, status)
2918+
2919+ def on_counter_response(self, status):
2920+ assert self.app.state == status
2921+ if status == 'response_ok':
2922+ _start_thread(self.request_cert)
2923+ self.hub.send('counter_response', status)
2924+
2925+ def request_cert(self):
2926+ log.info('Creating CSR')
2927+ try:
2928+ self.pki.create_csr(self.id)
2929+ csr_data = self.pki.read_csr(self.id)
2930+ obj = {'csr': b64encode(csr_data).decode('utf-8')}
2931+ r = self.client.post(obj, 'csr')
2932+ cert_data = b64decode(r['cert'].encode('utf-8'))
2933+ self.pki.write_cert2(self.id, self.peer_id, cert_data)
2934+ self.pki.verify_cert2(self.id, self.peer_id)
2935+ status = 'cert_issued'
2936+ except Exception as e:
2937+ status = 'error'
2938+ log.exception('Could not request cert')
2939+ GObject.idle_add(self.on_csr_response, status)
2940+
2941+ def on_csr_response(self, status):
2942+ log.info('on_csr_response %r', status)
2943+ self.hub.send('csr_response', status)
2944+
2945+
2946+class UI(BaseUI):
2947+ page = 'client.html'
2948+
2949+ signals = {
2950+ 'first_time': [],
2951+ 'already_using': [],
2952+ 'have_secret': ['secret'],
2953+ 'response': ['success'],
2954+ 'counter_response': ['status'],
2955+ 'csr_response': ['status'],
2956+ 'set_message': ['message'],
2957+
2958+ 'show_screen2a': [],
2959+ 'show_screen2b': [],
2960+ 'show_screen3b': [],
2961+ }
2962+
2963+ def __init__(self):
2964+ super().__init__()
2965+ self.couch = DmediaCouch(tempfile.mkdtemp())
2966+ self.couch.firstrun_init(create_user=False)
2967+ self.couch.load_pki()
2968+ self.avahi = None
2969+
2970+ def quit(self, *args):
2971+ if self.avahi:
2972+ self.avahi.unpublish()
2973+ Gtk.main_quit()
2974+
2975+ def connect_hub_signals(self, hub):
2976+ hub.connect('first_time', self.on_first_time)
2977+ hub.connect('already_using', self.on_already_using)
2978+ hub.connect('have_secret', self.on_have_secret)
2979+ hub.connect('response', self.on_response)
2980+ hub.connect('counter_response', self.on_counter_response)
2981+ hub.connect('csr_response', self.on_csr_response)
2982+
2983+ def on_first_time(self, hub):
2984+ hub.send('show_screen2a')
2985+
2986+ def on_already_using(self, hub):
2987+ if self.avahi is not None:
2988+ print('oop, duplicate click')
2989+ return
2990+ self.avahi = AvahiPeer(self.couch.pki, client_mode=True)
2991+ self.avahi.connect('accept', self.on_accept)
2992+ app = InfoApp(self.avahi.id)
2993+ self.httpd = make_server(app, '0.0.0.0',
2994+ self.avahi.get_server_config()
2995+ )
2996+ self.httpd.start()
2997+ self.avahi.browse()
2998+ self.avahi.publish(self.httpd.port)
2999+ GObject.idle_add(hub.send, 'show_screen2b')
3000+
3001+ def on_accept(self, avahi, peer):
3002+ self.avahi.activate(peer.id)
3003+ self.session = Session(self.hub, self.couch.pki, avahi.id, peer,
3004+ avahi.get_client_config()
3005+ )
3006+ # Reconfigure HTTPD to only accept connections from bound peer
3007+ self.httpd.reconfigure(self.session.app, avahi.get_server_config())
3008+ avahi.unpublish()
3009+ GObject.idle_add(self.hub.send, 'show_screen3b')
3010+
3011+ def on_have_secret(self, hub, secret):
3012+ if hasattr(self.session.cr, 'secret'):
3013+ log.warning("duplicate 'have_secret' signal received")
3014+ return
3015+ self.session.cr.set_secret(secret)
3016+ hub.send('set_message', _('Challenge...'))
3017+ _start_thread(self.session.challenge)
3018+
3019+ def on_response(self, hub, success):
3020+ if success:
3021+ hub.send('set_message', _('Counter-Challenge...'))
3022+ GObject.timeout_add(250, hub.send, 'spin_orb')
3023+ else:
3024+ hub.send('set_message', _('Typo? Please try again with new secret.'))
3025+
3026+ def on_counter_response(self, hub, status):
3027+ if status == 'response_ok':
3028+ hub.send('set_message', _('Requesting Certificate...'))
3029+ else:
3030+ hub.send('set_message', _('Very Bad Things!'))
3031+
3032+ def on_csr_response(self, hub, status):
3033+ if status == 'cert_issued':
3034+ hub.send('set_message', _('Done!'))
3035+ else:
3036+ hub.send('set_message', _('Very Bad Things with Certificate!'))
3037+
3038+
3039+ui = UI()
3040+ui.run()
3041
3042=== removed file 'run-publish.py'
3043--- run-publish.py 2012-09-20 12:27:37 +0000
3044+++ run-publish.py 1970-01-01 00:00:00 +0000
3045@@ -1,26 +0,0 @@
3046-#!/usr/bin/python3
3047-
3048-import logging
3049-
3050-import dbus
3051-from dbus.mainloop.glib import DBusGMainLoop
3052-from gi.repository import GObject
3053-from microfiber import random_id
3054-
3055-from dmedia.service.peers import Peer
3056-from dmedia.peering import TempPKI
3057-
3058-
3059-log = logging.getLogger()
3060-GObject.threads_init()
3061-DBusGMainLoop(set_as_default=True)
3062-logging.basicConfig(level=logging.DEBUG)
3063-
3064-pki = TempPKI()
3065-cert_id = pki.create(random_id())
3066-
3067-peer = Peer(cert_id)
3068-peer.browse('_dmedia-accept._tcp')
3069-peer.publish('_dmedia-offer._tcp', 5000)
3070-mainloop = GObject.MainLoop()
3071-mainloop.run()
3072
3073=== modified file 'setup.py'
3074--- setup.py 2012-09-05 08:04:08 +0000
3075+++ setup.py 2012-10-09 20:57:30 +0000
3076@@ -159,6 +159,7 @@
3077 ),
3078 ('share/icons/hicolor/scalable/status',
3079 [
3080+ 'share/indicator-novacut.svg',
3081 'share/indicator-dmedia.svg',
3082 'share/indicator-dmedia-att.svg',
3083 ]
3084
3085=== added file 'share/indicator-novacut.svg'
3086--- share/indicator-novacut.svg 1970-01-01 00:00:00 +0000
3087+++ share/indicator-novacut.svg 2012-10-09 20:57:30 +0000
3088@@ -0,0 +1,133 @@
3089+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
3090+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3091+
3092+<svg
3093+ xmlns:dc="http://purl.org/dc/elements/1.1/"
3094+ xmlns:cc="http://creativecommons.org/ns#"
3095+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
3096+ xmlns:svg="http://www.w3.org/2000/svg"
3097+ xmlns="http://www.w3.org/2000/svg"
3098+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
3099+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
3100+ width="512"
3101+ height="512"
3102+ id="svg4778"
3103+ version="1.1"
3104+ inkscape:version="0.48.1 r9760"
3105+ sodipodi:docname="novacut-solo-brandmark_PINK_FINAL-SVG.svg"
3106+ inkscape:export-filename="/home/izo/Pictures/Client_Work/Novacut/Final-Artwork/web/PNG/novacut-solo-brandmark_PINK_FINAL-PNG-300dpi.png"
3107+ inkscape:export-xdpi="300.05859"
3108+ inkscape:export-ydpi="300.05859">
3109+ <defs
3110+ id="defs4780">
3111+ <inkscape:path-effect
3112+ effect="spiro"
3113+ id="path-effect5868"
3114+ is_visible="true" />
3115+ </defs>
3116+ <sodipodi:namedview
3117+ id="base"
3118+ pagecolor="#ffffff"
3119+ bordercolor="#666666"
3120+ borderopacity="1.0"
3121+ inkscape:pageopacity="0.0"
3122+ inkscape:pageshadow="2"
3123+ inkscape:zoom="1"
3124+ inkscape:cx="233.49618"
3125+ inkscape:cy="280"
3126+ inkscape:current-layer="layer1"
3127+ inkscape:document-units="px"
3128+ showgrid="false"
3129+ inkscape:window-width="1614"
3130+ inkscape:window-height="1026"
3131+ inkscape:window-x="66"
3132+ inkscape:window-y="24"
3133+ inkscape:window-maximized="1"
3134+ showguides="false"
3135+ inkscape:guide-bbox="true">
3136+ <inkscape:grid
3137+ type="xygrid"
3138+ id="grid2994"
3139+ empspacing="4"
3140+ visible="true"
3141+ enabled="true"
3142+ snapvisiblegridlinesonly="true" />
3143+ <sodipodi:guide
3144+ orientation="1,0"
3145+ position="256,88"
3146+ id="guide3002" />
3147+ <sodipodi:guide
3148+ orientation="0,1"
3149+ position="592,256"
3150+ id="guide3004" />
3151+ <sodipodi:guide
3152+ position="0,0"
3153+ orientation="0,512"
3154+ id="guide3006" />
3155+ <sodipodi:guide
3156+ position="512,0"
3157+ orientation="-512,0"
3158+ id="guide3008" />
3159+ <sodipodi:guide
3160+ position="512,512"
3161+ orientation="0,-512"
3162+ id="guide3010" />
3163+ <sodipodi:guide
3164+ position="0,512"
3165+ orientation="512,0"
3166+ id="guide3012" />
3167+ </sodipodi:namedview>
3168+ <metadata
3169+ id="metadata4783">
3170+ <rdf:RDF>
3171+ <cc:Work
3172+ rdf:about="">
3173+ <dc:format>image/svg+xml</dc:format>
3174+ <dc:type
3175+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
3176+ <dc:title></dc:title>
3177+ </cc:Work>
3178+ </rdf:RDF>
3179+ </metadata>
3180+ <g
3181+ id="layer1"
3182+ inkscape:label="Layer 1"
3183+ inkscape:groupmode="layer"
3184+ transform="translate(0,32)">
3185+ <path
3186+ transform="matrix(1.0666667,0,0,1.0666667,-85.333334,-32.000001)"
3187+ d="m 545,240 a 225,225 0 1 1 -450,0 225,225 0 1 1 450,0 z"
3188+ sodipodi:ry="225"
3189+ sodipodi:rx="225"
3190+ sodipodi:cy="240"
3191+ sodipodi:cx="320"
3192+ id="path5924"
3193+ style="fill:#e81f3b;fill-opacity:1;stroke:none"
3194+ sodipodi:type="arc" />
3195+ <path
3196+ id="path4023"
3197+ d="m 157.74011,106.26326 0,233.73017 48.11687,0 0,-156.48267 0.68543,0 37.83549,60.93432 0,-81.0173 -35.57359,-57.16452 -51.0642,0 z m 149.28569,0 0,156.82539 -0.68543,0 -37.8355,-60.86578 0,81.0173 35.23088,56.75326 51.40692,0 0,-233.73017 -48.11687,0 z"
3198+ style="font-size:144px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Helvetica Neue LT Com;-inkscape-font-specification:Helvetica Neue LT Com Bold"
3199+ inkscape:connector-curvature="0" />
3200+ <path
3201+ sodipodi:type="arc"
3202+ style="fill:#ffffff;fill-opacity:1;stroke:none"
3203+ id="path4025"
3204+ sodipodi:cx="836.50732"
3205+ sodipodi:cy="230.95239"
3206+ sodipodi:rx="13.435029"
3207+ sodipodi:ry="13.435029"
3208+ d="m 849.94235,230.95239 a 13.435029,13.435029 0 1 1 -26.87005,0 13.435029,13.435029 0 1 1 26.87005,0 z"
3209+ transform="matrix(2.193362,0,0,2.193362,-1651.9421,-440.84345)" />
3210+ <path
3211+ transform="matrix(2.193362,0,0,2.193362,-1505.7305,-124.45147)"
3212+ d="m 849.94235,230.95239 a 13.435029,13.435029 0 1 1 -26.87005,0 13.435029,13.435029 0 1 1 26.87005,0 z"
3213+ sodipodi:ry="13.435029"
3214+ sodipodi:rx="13.435029"
3215+ sodipodi:cy="230.95239"
3216+ sodipodi:cx="836.50732"
3217+ id="path4027"
3218+ style="fill:#ffffff;fill-opacity:1;stroke:none"
3219+ sodipodi:type="arc" />
3220+ </g>
3221+</svg>

Subscribers

People subscribed via source and target branches