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
=== modified file 'dmedia-service'
--- dmedia-service 2012-10-04 20:25:21 +0000
+++ dmedia-service 2012-10-09 20:57:30 +0000
@@ -90,7 +90,7 @@
90 self.core.init_default_store()90 self.core.init_default_store()
91 if self.core.local.get('default_store') is None:91 if self.core.local.get('default_store') is None:
92 self.core.set_default_store('shared')92 self.core.set_default_store('shared')
93 self.env_s = dumps(self.core.env)93 self.env_s = dumps(self.core.env, pretty=True)
94 self.ssl_config = self.couch.get_ssl_config()94 self.ssl_config = self.couch.get_ssl_config()
9595
96 def start_httpd(self):96 def start_httpd(self):
9797
=== added file 'dmedia/gtk/peering.py'
--- dmedia/gtk/peering.py 1970-01-01 00:00:00 +0000
+++ dmedia/gtk/peering.py 2012-10-09 20:57:30 +0000
@@ -0,0 +1,100 @@
1from os import path
2import json
3
4from gi.repository import GObject, Gtk, WebKit
5
6
7ui = path.join(path.dirname(path.abspath(__file__)), 'ui')
8assert path.isdir(ui)
9
10
11class Hub(GObject.GObject):
12 def __init__(self, view):
13 super().__init__()
14 self._view = view
15 view.connect('notify::title', self._on_notify_title)
16
17 def _on_notify_title(self, view, notify):
18 title = view.get_property('title')
19 if title is None:
20 return
21 obj = json.loads(title)
22 self.emit(obj['signal'], *obj['args'])
23
24 def send(self, signal, *args):
25 """
26 Emit a signal by calling the JavaScript Signal.recv() function.
27 """
28 script = 'Hub.recv({!r})'.format(
29 json.dumps({'signal': signal, 'args': args})
30 )
31 self._view.execute_script(script)
32 self.emit(signal, *args)
33
34
35def iter_gsignals(signals):
36 assert isinstance(signals, dict)
37 for (name, argnames) in signals.items():
38 assert isinstance(argnames, list)
39 args = [GObject.TYPE_PYOBJECT for argname in argnames]
40 yield (name, (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, args))
41
42
43def hub_factory(signals):
44 if signals:
45 class FactoryHub(Hub):
46 __gsignals__ = dict(iter_gsignals(signals))
47 return FactoryHub
48 return Hub
49
50
51class BaseUI:
52 inspector = None
53 signals = None
54 title = 'Novacut' # Default Gtk.Window title
55 page = 'peering.html' # Default page to load once CouchDB is available
56 width = 960 # Default Gtk.Window width
57 height = 540 # Default Gtk.Window height
58
59 def __init__(self):
60 self.build_window()
61 self.hub = hub_factory(self.signals)(self.view)
62 self.connect_hub_signals(self.hub)
63
64 def show(self):
65 self.window.show_all()
66
67 def run(self):
68 self.window.connect('destroy', self.quit)
69 self.window.show_all()
70 Gtk.main()
71
72 def quit(self, *args):
73 Gtk.main_quit()
74
75 def connect_hub_signals(self, hub):
76 pass
77
78 def build_window(self):
79 self.window = Gtk.Window()
80 self.window.set_position(Gtk.WindowPosition.CENTER)
81 self.window.set_default_size(self.width, self.height)
82 self.window.set_title(self.title)
83 self.vpaned = Gtk.VPaned()
84 self.window.add(self.vpaned)
85 self.view = WebKit.WebView()
86 self.view.get_settings().set_property('enable-developer-extras', True)
87 inspector = self.view.get_inspector()
88 inspector.connect('inspect-web-view', self.on_inspect)
89 self.view.load_uri('file://' + path.join(ui, self.page))
90 self.vpaned.pack1(self.view, True, True)
91
92 def on_inspect(self, *args):
93 assert self.inspector is None
94 self.inspector = WebKit.WebView()
95 pos = self.window.get_allocated_height() * 2 // 3
96 self.vpaned.set_position(pos)
97 self.vpaned.pack2(self.inspector, True, True)
98 self.inspector.show_all()
99 return self.inspector
100
0101
=== added directory 'dmedia/gtk/ui'
=== added file 'dmedia/gtk/ui/client.html'
--- dmedia/gtk/ui/client.html 1970-01-01 00:00:00 +0000
+++ dmedia/gtk/ui/client.html 2012-10-09 20:57:30 +0000
@@ -0,0 +1,166 @@
1<!DOCTYPE html>
2<html>
3<head>
4<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5<link rel="stylesheet" href="peering.css" />
6<script src="peering.js"></script>
7<script>
8
9"use strict";
10
11var B32ALPHABET = '234567ABCDEFGHIJKLMNOPQRSTUVWXYZ';
12
13var UI = {
14 on_load: function() {
15 UI.input = $('input');
16 UI.input.oninput = UI.on_input;
17 UI.form = $('form');
18 UI.form.onsubmit = UI.on_submit;
19 UI.show('screen1');
20 },
21
22 on_input: function(event) {
23 var orig = UI.input.value.toUpperCase();
24 var value = '';
25 var b32, i;
26 for (i=0; i<orig.length; i++) {
27 b32 = orig[i];
28 if (B32ALPHABET.indexOf(b32) >= 0 ) {
29 value += b32;
30 }
31 }
32 UI.input.value = value;
33 if (UI.input.value.length == 8) {
34 $('finish').classList.remove('hidden');
35 }
36 else {
37 $('finish').classList.add('hidden');
38 }
39 },
40
41 on_submit: function(event) {
42 event.preventDefault();
43 event.stopPropagation();
44 UI.have_secret();
45 },
46
47 have_secret: function() {
48 if (UI.input.value.length == 8 && !UI.input.disabled) {
49 UI.input.disabled = true;
50 Hub.send('have_secret', UI.input.value);
51 }
52 },
53
54 show: function(id) {
55 $hide(UI.current);
56 UI.current = $show(id);
57 },
58}
59
60
61window.onload = UI.on_load;
62
63
64Hub.connect('show_screen2a',
65 function() {
66 UI.show('screen2a');
67 $('logo').classList.add('spinleft');
68 }
69);
70
71Hub.connect('show_screen2b',
72 function() {
73 UI.show('screen2b');
74 $('logo').classList.add('spinright');
75 }
76);
77
78Hub.connect('show_screen3b',
79 function() {
80 $hide('logo');
81 UI.input.value = '';
82 UI.show('screen3b');
83 }
84);
85
86Hub.connect('spin_orb',
87 function() {
88 $('logo2').classList.add('spinright');
89 }
90);
91
92Hub.connect('set_message',
93 function(message) {
94 $('message').textContent = message;
95 }
96);
97
98Hub.connect('response',
99 function(success) {
100 if (!success) {
101 UI.input.value = '';
102 UI.input.disabled = false;
103 UI.input.focus();
104 $('finish').classList.add('hidden');
105 }
106 else {
107 $hide('finish');
108 $show('logo2');
109 }
110 }
111);
112
113</script>
114</head>
115<body>
116
117<img src="novacut.svg" id="logo">
118
119<div id="screen1" class="hide">
120 <div id="first" onclick="Hub.send('first_time')">
121 <p class="top">
122 This is my first time using Novacut
123 </p>
124 <p>
125 (You can add more devices later)
126 </p>
127 </div>
128
129 <div id="sync" onclick="Hub.send('already_using')">
130 <p class="top">
131 I'm already using Novacut
132 </p>
133 <p>
134 Sync with my other devices!
135 </p>
136 </div>
137</div>
138
139<div id="screen2a" class="hide">
140 <p class="gen">
141 Pretty words here
142 </p>
143</div>
144
145<div id="screen2b" class="hide">
146 <p class="gen">
147 Accept the peering offer on your other device
148 </p>
149</div>
150
151<div id="screen3b" class="hide">
152 <p class="gen">
153 Enter your secret code:
154 </p>
155 <div class="secret">
156 <form id="form">
157 <input id="input" type="text" maxlength="8" size="8" autofocus="1"></input>
158 </form>
159 </div>
160 <p id="message"></p>
161 <img src="sync.svg" id="finish" class="hidden" onclick="UI.have_secret()">
162 <img src="novacut.svg" id="logo2" class="hide">
163</div>
164
165</body>
166</html>
0167
=== added file 'dmedia/gtk/ui/novacut.svg'
--- dmedia/gtk/ui/novacut.svg 1970-01-01 00:00:00 +0000
+++ dmedia/gtk/ui/novacut.svg 2012-10-09 20:57:30 +0000
@@ -0,0 +1,133 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 xmlns:dc="http://purl.org/dc/elements/1.1/"
6 xmlns:cc="http://creativecommons.org/ns#"
7 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8 xmlns:svg="http://www.w3.org/2000/svg"
9 xmlns="http://www.w3.org/2000/svg"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12 width="512"
13 height="512"
14 id="svg4778"
15 version="1.1"
16 inkscape:version="0.48.1 r9760"
17 sodipodi:docname="novacut-solo-brandmark_PINK_FINAL-SVG.svg"
18 inkscape:export-filename="/home/izo/Pictures/Client_Work/Novacut/Final-Artwork/web/PNG/novacut-solo-brandmark_PINK_FINAL-PNG-300dpi.png"
19 inkscape:export-xdpi="300.05859"
20 inkscape:export-ydpi="300.05859">
21 <defs
22 id="defs4780">
23 <inkscape:path-effect
24 effect="spiro"
25 id="path-effect5868"
26 is_visible="true" />
27 </defs>
28 <sodipodi:namedview
29 id="base"
30 pagecolor="#ffffff"
31 bordercolor="#666666"
32 borderopacity="1.0"
33 inkscape:pageopacity="0.0"
34 inkscape:pageshadow="2"
35 inkscape:zoom="1"
36 inkscape:cx="233.49618"
37 inkscape:cy="280"
38 inkscape:current-layer="layer1"
39 inkscape:document-units="px"
40 showgrid="false"
41 inkscape:window-width="1614"
42 inkscape:window-height="1026"
43 inkscape:window-x="66"
44 inkscape:window-y="24"
45 inkscape:window-maximized="1"
46 showguides="false"
47 inkscape:guide-bbox="true">
48 <inkscape:grid
49 type="xygrid"
50 id="grid2994"
51 empspacing="4"
52 visible="true"
53 enabled="true"
54 snapvisiblegridlinesonly="true" />
55 <sodipodi:guide
56 orientation="1,0"
57 position="256,88"
58 id="guide3002" />
59 <sodipodi:guide
60 orientation="0,1"
61 position="592,256"
62 id="guide3004" />
63 <sodipodi:guide
64 position="0,0"
65 orientation="0,512"
66 id="guide3006" />
67 <sodipodi:guide
68 position="512,0"
69 orientation="-512,0"
70 id="guide3008" />
71 <sodipodi:guide
72 position="512,512"
73 orientation="0,-512"
74 id="guide3010" />
75 <sodipodi:guide
76 position="0,512"
77 orientation="512,0"
78 id="guide3012" />
79 </sodipodi:namedview>
80 <metadata
81 id="metadata4783">
82 <rdf:RDF>
83 <cc:Work
84 rdf:about="">
85 <dc:format>image/svg+xml</dc:format>
86 <dc:type
87 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
88 <dc:title></dc:title>
89 </cc:Work>
90 </rdf:RDF>
91 </metadata>
92 <g
93 id="layer1"
94 inkscape:label="Layer 1"
95 inkscape:groupmode="layer"
96 transform="translate(0,32)">
97 <path
98 transform="matrix(1.0666667,0,0,1.0666667,-85.333334,-32.000001)"
99 d="m 545,240 a 225,225 0 1 1 -450,0 225,225 0 1 1 450,0 z"
100 sodipodi:ry="225"
101 sodipodi:rx="225"
102 sodipodi:cy="240"
103 sodipodi:cx="320"
104 id="path5924"
105 style="fill:#e81f3b;fill-opacity:1;stroke:none"
106 sodipodi:type="arc" />
107 <path
108 id="path4023"
109 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"
110 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"
111 inkscape:connector-curvature="0" />
112 <path
113 sodipodi:type="arc"
114 style="fill:#ffffff;fill-opacity:1;stroke:none"
115 id="path4025"
116 sodipodi:cx="836.50732"
117 sodipodi:cy="230.95239"
118 sodipodi:rx="13.435029"
119 sodipodi:ry="13.435029"
120 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"
121 transform="matrix(2.193362,0,0,2.193362,-1651.9421,-440.84345)" />
122 <path
123 transform="matrix(2.193362,0,0,2.193362,-1505.7305,-124.45147)"
124 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"
125 sodipodi:ry="13.435029"
126 sodipodi:rx="13.435029"
127 sodipodi:cy="230.95239"
128 sodipodi:cx="836.50732"
129 id="path4027"
130 style="fill:#ffffff;fill-opacity:1;stroke:none"
131 sodipodi:type="arc" />
132 </g>
133</svg>
0134
=== added file 'dmedia/gtk/ui/peering.css'
--- dmedia/gtk/ui/peering.css 1970-01-01 00:00:00 +0000
+++ dmedia/gtk/ui/peering.css 2012-10-09 20:57:30 +0000
@@ -0,0 +1,124 @@
1.hide {
2 display: none !important;
3}
4
5body {
6 font-family: Lato;
7 font-size: 21px;
8 background-color: #301438;
9 color: #fff;
10 text-align: center;
11}
12
13code, input {
14 font-family: "Ubuntu Mono";
15 font-size: 60px;
16 font-weight: bold;
17 color: #333;
18 text-shadow: 0px 0px 3px #e81f3b;
19 letter-spacing: 0.15em;
20 line-height: 1.4em;
21 margin: 0.4em;
22}
23
24input {
25 width: 5.4em;
26}
27
28.secret {
29 text-align: center;
30 background-color: #fff;
31 text-align: center;
32 box-shadow: 2px 2px 6px #000;
33 border-radius: 10px;
34 cursor: pointer;
35 -webkit-user-select: none;
36 display: inline-block;
37}
38
39#first, #sync {
40 position: fixed;
41 width: 440px;
42 background-color: #fff;
43 text-align: center;
44 box-shadow: 2px 2px 5px #000;
45 border-radius: 8px;
46 cursor: pointer;
47 -webkit-user-select: none;
48 color: #000;
49}
50
51#first p, #sync p {
52 margin: 16px;
53}
54
55#first {
56 top: 30px;
57 left: 71px;
58}
59
60#sync {
61 bottom: 30px;
62 /*left: 449px;*/
63 right: 71px;
64}
65
66p.top {
67 font-weight: bold;
68}
69
70#logo {
71 position: fixed;
72 top: 190px;
73 left: 400px;
74 width: 160px;
75 height: 160px;
76 -webkit-transition: -webkit-transform 500ms linear;
77}
78
79#logo.spinleft {
80 -webkit-transform: rotate(-180deg);
81}
82
83#logo.spinright {
84 -webkit-transform: rotate(180deg);
85}
86
87
88p.gen {
89 margin: 2em;
90 font-size: 28px;
91}
92
93
94#finish {
95 position: fixed;
96 width: 160px;
97 height: 160px;
98 bottom: 60px;
99 right: 60px;
100 cursor: pointer;
101 -webkit-transition-timing-function: ease;
102 -webkit-transition-duration: 300ms;
103 -webkit-transition-property: right, bottom;
104}
105
106#finish.hidden {
107 bottom: -134px;
108 right: -134px;
109}
110
111#logo2 {
112 z-index: 10;
113 position: fixed;
114 width: 160px;
115 height: 160px;
116 bottom: 60px;
117 right: 60px;
118 -webkit-transition: -webkit-transform 2000ms linear;
119}
120
121#logo2.spinright {
122 -webkit-transform: rotate(360deg);
123}
124
0125
=== added file 'dmedia/gtk/ui/peering.js'
--- dmedia/gtk/ui/peering.js 1970-01-01 00:00:00 +0000
+++ dmedia/gtk/ui/peering.js 2012-10-09 20:57:30 +0000
@@ -0,0 +1,183 @@
1"use strict";
2
3var Hub = {
4 /*
5 Relay signals between JavaScript and Gtk.
6
7 For example, to send a signal to Gtk via document.title:
8
9 >>> Hub.send('click');
10 >>> Hub.send('changed', 'foo', 'bar');
11
12 Or from the Gtk side, send a signal to JavaScript by using
13 WebView.execute_script() to call Hub.recv() like this:
14
15 >>> Hub.recv('{"signal": "error", "args": ["oops!"]}');
16
17 Use userwebkit.BaseApp.send() as a shortcut to do the above.
18
19 Lastly, to emit a signal from JavaScript to JavaScript handlers, use
20 Hub.emit() like this:
21
22 >>> Hub.emit('changed', 'foo', 'bar');
23
24 */
25 i: 0,
26
27 names: {},
28
29 connect: function(signal, callback, self) {
30 /*
31 Connect a signal handler.
32
33 For example:
34
35 >>> Hub.connect('changed', this.on_changed, this);
36
37 */
38 if (! Hub.names[signal]) {
39 Hub.names[signal] = [];
40 }
41 Hub.names[signal].push({callback: callback, self: self});
42 },
43
44 send: function() {
45 /*
46 Send a signal to the Gtk side by changing document.title.
47
48 For example:
49
50 >>> Hub.send('changed', 'foo', 'bar');
51
52 */
53 var params = Array.prototype.slice.call(arguments);
54 var signal = params[0];
55 var args = params.slice(1);
56 Hub._emit(signal, args);
57 var obj = {
58 'i': Hub.i,
59 'signal': signal,
60 'args': args,
61 };
62 Hub.i += 1;
63 document.title = JSON.stringify(obj);
64 },
65
66 recv: function(data) {
67 /*
68 Gtk should call this function to emit a signal to JavaScript handlers.
69
70 For example:
71
72 >>> Hub.recv('{"signal": "changed", "args": ["foo", "bar"]}');
73
74 If you need to emit a signal from JavaScript to JavaScript handlers,
75 use Hub.emit() instead.
76 */
77 var obj = JSON.parse(data);
78 Hub._emit(obj.signal, obj.args);
79 },
80
81 emit: function() {
82 /*
83 Emit a signal from JavaScript to JavaScript handlers.
84
85 For example:
86
87 >>> Hub.emit('changed', 'foo', 'bar');
88
89 */
90 var params = Array.prototype.slice.call(arguments);
91 Hub._emit(params[0], params.slice(1));
92 },
93
94 _emit: function(signal, args) {
95 /*
96 Low-level private function to emit a signal to JavaScript handlers.
97 */
98 var handlers = Hub.names[signal];
99 if (handlers) {
100 handlers.forEach(function(h) {
101 h.callback.apply(h.self, args);
102 });
103 }
104 },
105}
106
107
108function $bind(func, self) {
109 return function() {
110 var args = Array.prototype.slice.call(arguments);
111 return func.apply(self, args);
112 }
113}
114
115
116function $(id) {
117 /*
118 Return the element with id="id".
119
120 If `id` is an Element, it is returned unchanged.
121
122 Examples:
123
124 >>> $('browser');
125 <div id="browser" class="box">
126 >>> var el = $('browser');
127 >>> $(el);
128 <div id="browser" class="box">
129
130 */
131 if (id instanceof Element) {
132 return id;
133 }
134 return document.getElementById(id);
135}
136
137
138function $el(tag, attributes) {
139 /*
140 Convenience function to create a new DOM element and set its attributes.
141
142 Examples:
143
144 >>> $el('img');
145 <img>
146 >>> $el('img', {'class': 'thumbnail', 'src': 'foo.png'});
147 <img class="thumbnail" src="foo.png">
148
149 */
150 var el = document.createElement(tag);
151 if (attributes) {
152 var key;
153 for (key in attributes) {
154 var value = attributes[key];
155 if (key == 'textContent') {
156 el.textContent = value;
157 }
158 else {
159 el.setAttribute(key, value);
160 }
161 }
162 }
163 return el;
164}
165
166
167function $hide(id) {
168 var element = $(id);
169 if (element) {
170 element.classList.add('hide');
171 return element;
172 }
173}
174
175
176function $show(id) {
177 var element = $(id);
178 if (element) {
179 element.classList.remove('hide');
180 return element;
181 }
182}
183
0184
=== added file 'dmedia/gtk/ui/server.html'
--- dmedia/gtk/ui/server.html 1970-01-01 00:00:00 +0000
+++ dmedia/gtk/ui/server.html 2012-10-09 20:57:30 +0000
@@ -0,0 +1,37 @@
1<!DOCTYPE html>
2<html>
3<head>
4<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5<link rel="stylesheet" href="peering.css" />
6<script src="peering.js"></script>
7<script>
8
9"use strict";
10
11Hub.connect('display_secret',
12 function(secret) {
13 $('secret').textContent = secret;
14 }
15);
16
17Hub.connect('set_message',
18 function(message) {
19 $('message').textContent = message;
20 }
21);
22
23window.onload = function() {
24 Hub.send('get_secret');
25}
26
27</script>
28</head>
29<p class="gen">
30This is your secret code:
31</p>
32<div class="secret">
33<code id="secret"></code>
34</div>
35<p id="message"></p>
36</body>
37</html>
038
=== added file 'dmedia/gtk/ui/sync.svg'
--- dmedia/gtk/ui/sync.svg 1970-01-01 00:00:00 +0000
+++ dmedia/gtk/ui/sync.svg 2012-10-09 20:57:30 +0000
@@ -0,0 +1,119 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 xmlns:dc="http://purl.org/dc/elements/1.1/"
6 xmlns:cc="http://creativecommons.org/ns#"
7 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8 xmlns:svg="http://www.w3.org/2000/svg"
9 xmlns="http://www.w3.org/2000/svg"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12 width="512"
13 height="512"
14 id="svg4778"
15 version="1.1"
16 inkscape:version="0.48.3.1 r9886"
17 sodipodi:docname="sync.svg"
18 inkscape:export-filename="/home/izo/Pictures/Client_Work/Novacut/Final-Artwork/web/PNG/novacut-solo-brandmark_PINK_FINAL-PNG-300dpi.png"
19 inkscape:export-xdpi="300.05859"
20 inkscape:export-ydpi="300.05859">
21 <defs
22 id="defs4780">
23 <inkscape:path-effect
24 effect="spiro"
25 id="path-effect5868"
26 is_visible="true" />
27 </defs>
28 <sodipodi:namedview
29 id="base"
30 pagecolor="#ffffff"
31 bordercolor="#666666"
32 borderopacity="1.0"
33 inkscape:pageopacity="0.0"
34 inkscape:pageshadow="2"
35 inkscape:zoom="1"
36 inkscape:cx="233.49618"
37 inkscape:cy="280"
38 inkscape:current-layer="layer1"
39 inkscape:document-units="px"
40 showgrid="false"
41 inkscape:window-width="2495"
42 inkscape:window-height="1576"
43 inkscape:window-x="65"
44 inkscape:window-y="24"
45 inkscape:window-maximized="1"
46 showguides="false"
47 inkscape:guide-bbox="true">
48 <inkscape:grid
49 type="xygrid"
50 id="grid2994"
51 empspacing="4"
52 visible="true"
53 enabled="true"
54 snapvisiblegridlinesonly="true" />
55 <sodipodi:guide
56 orientation="1,0"
57 position="256,88"
58 id="guide3002" />
59 <sodipodi:guide
60 orientation="0,1"
61 position="592,256"
62 id="guide3004" />
63 <sodipodi:guide
64 position="0,0"
65 orientation="0,512"
66 id="guide3006" />
67 <sodipodi:guide
68 position="512,0"
69 orientation="-512,0"
70 id="guide3008" />
71 <sodipodi:guide
72 position="512,512"
73 orientation="0,-512"
74 id="guide3010" />
75 <sodipodi:guide
76 position="0,512"
77 orientation="512,0"
78 id="guide3012" />
79 </sodipodi:namedview>
80 <metadata
81 id="metadata4783">
82 <rdf:RDF>
83 <cc:Work
84 rdf:about="">
85 <dc:format>image/svg+xml</dc:format>
86 <dc:type
87 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
88 <dc:title />
89 </cc:Work>
90 </rdf:RDF>
91 </metadata>
92 <g
93 id="layer1"
94 inkscape:label="Layer 1"
95 inkscape:groupmode="layer"
96 transform="translate(0,32)">
97 <path
98 transform="matrix(1.0666667,0,0,1.0666667,-85.333334,-32.000001)"
99 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"
100 sodipodi:ry="225"
101 sodipodi:rx="225"
102 sodipodi:cy="240"
103 sodipodi:cx="320"
104 id="path5924"
105 style="fill:#e81f3b;fill-opacity:1;stroke:none"
106 sodipodi:type="arc" />
107 <text
108 xml:space="preserve"
109 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"
110 x="71.52964"
111 y="277.69928"
112 id="text2994"
113 sodipodi:linespacing="125%"><tspan
114 sodipodi:role="line"
115 id="tspan2996"
116 x="71.52964"
117 y="277.69928">Sync!</tspan></text>
118 </g>
119</svg>
0120
=== modified file 'dmedia/httpd.py'
--- dmedia/httpd.py 2012-10-04 20:46:18 +0000
+++ dmedia/httpd.py 2012-10-09 20:57:30 +0000
@@ -272,6 +272,12 @@
272 self.remote = '{REMOTE_ADDR} {REMOTE_PORT}'.format(**environ)272 self.remote = '{REMOTE_ADDR} {REMOTE_PORT}'.format(**environ)
273 self.start = None273 self.start = None
274274
275 def handle(self):
276 if self.environ['wsgi.multithread']:
277 self.handle_many()
278 else:
279 self.handle_one()
280
275 def handle_many(self):281 def handle_many(self):
276 count = 0282 count = 0
277 try:283 try:
@@ -412,6 +418,12 @@
412 self.url = template.format(self.scheme, self.port)418 self.url = template.format(self.scheme, self.port)
413 self.environ = self.build_base_environ()419 self.environ = self.build_base_environ()
414 self.socket.listen(5)420 self.socket.listen(5)
421 self.thread = None
422 self.running = False
423
424 def __del__(self):
425 if self.running:
426 self.shutdown()
415427
416 def build_base_environ(self):428 def build_base_environ(self):
417 """429 """
@@ -469,6 +481,7 @@
469 def serve_forever(self):481 def serve_forever(self):
470 while True:482 while True:
471 (conn, address) = self.socket.accept()483 (conn, address) = self.socket.accept()
484 conn.settimeout(SOCKET_TIMEOUT)
472 thread = threading.Thread(485 thread = threading.Thread(
473 target=self.handle_connection,486 target=self.handle_connection,
474 args=(conn, address),487 args=(conn, address),
@@ -476,12 +489,45 @@
476 thread.daemon = True489 thread.daemon = True
477 thread.start()490 thread.start()
478491
492 def start(self):
493 assert self.thread is None
494 assert self.running is False
495 self.running = True
496 self.thread = threading.Thread(
497 target=self.serve_single_threaded,
498 )
499 self.thread.daemon = True
500 self.thread.start()
501
502 def shutdown(self):
503 assert self.running is True
504 self.running = False
505 self.thread.join()
506 self.thread = None
507
508 def reconfigure(self, app, ssl_config):
509 assert set(ssl_config) == set(['cert_file', 'key_file', 'ca_file'])
510 self.shutdown()
511 self.app = app
512 self.context = build_server_ssl_context(ssl_config)
513 self.start()
514
515 def serve_single_threaded(self):
516 self.environ['wsgi.multithread'] = False
517 self.socket.settimeout(0.25)
518 while self.running:
519 try:
520 (conn, address) = self.socket.accept()
521 conn.settimeout(0.50)
522 self.handle_connection(conn, address)
523 except socket.timeout:
524 pass
525
479 def handle_connection(self, conn, address):526 def handle_connection(self, conn, address):
480 #conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)527 #conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
481 remote = '{} {}'.format(address[0], address[1])528 remote = '{} {}'.format(address[0], address[1])
482 try:529 try:
483 log.info('%s\tNew Connection', remote)530 log.info('%s\tNew Connection', remote)
484 conn.settimeout(SOCKET_TIMEOUT)
485 if self.context is not None:531 if self.context is not None:
486 conn = self.context.wrap_socket(conn, server_side=True)532 conn = self.context.wrap_socket(conn, server_side=True)
487 self.handle_requests(conn, address)533 self.handle_requests(conn, address)
@@ -497,7 +543,7 @@
497 environ = self.environ.copy()543 environ = self.environ.copy()
498 environ.update(self.build_connection_environ(conn, address))544 environ.update(self.build_connection_environ(conn, address))
499 handler = Handler(self.app, environ, conn)545 handler = Handler(self.app, environ, conn)
500 handler.handle_many()546 handler.handle()
501547
502548
503############################549############################
@@ -537,8 +583,11 @@
537583
538584
539def run_server(queue, app, bind_address='::1', ssl_config=None):585def run_server(queue, app, bind_address='::1', ssl_config=None):
540 server = make_server(app, bind_address, ssl_config)586 try:
541 env = {'port': server.port, 'url': server.url}587 server = make_server(app, bind_address, ssl_config)
542 queue.put(env)588 env = {'port': server.port, 'url': server.url}
543 server.serve_forever()589 queue.put(env)
590 server.serve_forever()
591 except Exception as e:
592 queue.put(e)
544593
545594
=== modified file 'dmedia/peering.py'
--- dmedia/peering.py 2012-10-03 17:21:46 +0000
+++ dmedia/peering.py 2012-10-09 20:57:30 +0000
@@ -107,7 +107,7 @@
107107
108"""108"""
109109
110from base64 import b32encode, b32decode110import base64
111import os111import os
112from os import path112from os import path
113import stat113import stat
@@ -115,9 +115,14 @@
115import shutil115import shutil
116from collections import namedtuple116from collections import namedtuple
117from subprocess import check_call, check_output117from subprocess import check_call, check_output
118import json
119import socket
120import logging
118121
119from skein import skein512122from skein import skein512
120from microfiber import random_id123from microfiber import random_id, dumps
124
125from dmedia.httpd import WSGIError
121126
122127
123DAYS = 365 * 10128DAYS = 365 * 10
@@ -128,6 +133,11 @@
128PERS_PUBKEY = b'20120918 jderose@novacut.com dmedia/pubkey'133PERS_PUBKEY = b'20120918 jderose@novacut.com dmedia/pubkey'
129PERS_RESPONSE = b'20120918 jderose@novacut.com dmedia/response'134PERS_RESPONSE = b'20120918 jderose@novacut.com dmedia/response'
130135
136USER = os.environ.get('USER')
137HOST = socket.gethostname()
138
139log = logging.getLogger()
140
131141
132class IdentityError(Exception):142class IdentityError(Exception):
133 def __init__(self, filename, expected, got):143 def __init__(self, filename, expected, got):
@@ -147,6 +157,15 @@
147 pass157 pass
148158
149159
160class IssuerError(IdentityError):
161 pass
162
163
164class VerificationError(IdentityError):
165 pass
166
167
168
150def create_key(dst_file, bits=2048):169def create_key(dst_file, bits=2048):
151 """170 """
152 Create an RSA keypair and save it to *dst_file*.171 Create an RSA keypair and save it to *dst_file*.
@@ -262,6 +281,33 @@
262 return line[len(prefix):]281 return line[len(prefix):]
263282
264283
284def get_issuer(cert_file):
285 """
286 Get the issuer from an X509 certificate (CA or issued certificate).
287 """
288 line = check_output(['openssl', 'x509',
289 '-issuer',
290 '-noout',
291 '-in', cert_file,
292 ]).decode('utf-8').rstrip('\n')
293
294 prefix = 'issuer= ' # Different than get_csr_subject()
295 if not line.startswith(prefix):
296 raise Exception(line)
297 return line[len(prefix):]
298
299
300def ssl_verify(cert_file, ca_file):
301 line = check_output(['openssl', 'verify',
302 '-CAfile', ca_file,
303 cert_file
304 ]).decode('utf-8')
305 expected = '{}: OK\n'.format(cert_file)
306 if line != expected:
307 raise VerificationError(cert_file, expected, line)
308 return cert_file
309
310
265def verify_key(filename, _id):311def verify_key(filename, _id):
266 actual_id = hash_pubkey(get_rsa_pubkey(filename))312 actual_id = hash_pubkey(get_rsa_pubkey(filename))
267 if _id != actual_id:313 if _id != actual_id:
@@ -291,6 +337,38 @@
291 return filename337 return filename
292338
293339
340def verify_ca(filename, _id):
341 filename = verify(filename, _id)
342 issuer = make_subject(_id)
343 actual_issuer = get_issuer(filename)
344 if issuer != actual_issuer:
345 raise IssuerError(filename, issuer, actual_issuer)
346 return ssl_verify(filename, filename)
347 return filename
348
349
350def verify_cert(cert_file, cert_id, ca_file, ca_id):
351 filename = verify(cert_file, cert_id)
352 issuer = make_subject(ca_id)
353 actual_issuer = get_issuer(filename)
354 if issuer != actual_issuer:
355 raise IssuerError(filename, issuer, actual_issuer)
356 return ssl_verify(filename, ca_file)
357 return filename
358
359
360def encode(value):
361 assert isinstance(value, bytes)
362 assert len(value) > 0 and len(value) % 5 == 0
363 return base64.b32encode(value).decode('utf-8')
364
365
366def decode(value):
367 assert isinstance(value, str)
368 assert len(value) > 0 and len(value) % 8 == 0
369 return base64.b32decode(value.encode('utf-8'))
370
371
294def _hash_pubkey(data):372def _hash_pubkey(data):
295 return skein512(data,373 return skein512(data,
296 digest_bits=240,374 digest_bits=240,
@@ -299,7 +377,7 @@
299377
300378
301def hash_pubkey(data):379def hash_pubkey(data):
302 return b32encode(_hash_pubkey(data)).decode('utf-8')380 return encode(_hash_pubkey(data))
303381
304382
305def _hash_cert(cert_data):383def _hash_cert(cert_data):
@@ -310,7 +388,7 @@
310388
311389
312def hash_cert(cert_data):390def hash_cert(cert_data):
313 return b32encode(_hash_cert(cert_data)).decode('utf-8')391 return encode(_hash_cert(cert_data))
314392
315393
316def compute_response(secret, challenge, nonce, challenger_hash, responder_hash):394def compute_response(secret, challenge, nonce, challenger_hash, responder_hash):
@@ -326,15 +404,253 @@
326404
327 :param responder_hash: hash of the responders certificate405 :param responder_hash: hash of the responders certificate
328 """406 """
407 assert len(secret) == 5
408 assert len(challenge) == 20
409 assert len(nonce) == 20
410 assert len(challenger_hash) == 30
411 assert len(responder_hash) == 30
329 skein = skein512(412 skein = skein512(
330 digest_bits=280,413 digest_bits=280,
331 pers=PERS_RESPONSE,414 pers=PERS_RESPONSE,
332 key=secret,415 key=secret,
333 nonce=(challange + nonce),416 nonce=(challenge + nonce),
334 )417 )
335 skein.update(challenger_hash)418 skein.update(challenger_hash)
336 skein.update(responder_hash)419 skein.update(responder_hash)
337 return b32encode(skein.digest()).decode('utf-8')420 return encode(skein.digest())
421
422
423class WrongResponse(Exception):
424 def __init__(self, expected, got):
425 self.expected = expected
426 self.got = got
427 super().__init__('Incorrect response')
428
429
430class ChallengeResponse:
431 def __init__(self, _id, peer_id):
432 self.id = _id
433 self.peer_id = peer_id
434 self.local_hash = decode(_id)
435 self.remote_hash = decode(peer_id)
436 assert len(self.local_hash) == 30
437 assert len(self.remote_hash) == 30
438
439 def get_secret(self):
440 # 40-bit secret (8 characters when base32 encoded)
441 self.secret = os.urandom(5)
442 return encode(self.secret)
443
444 def set_secret(self, secret):
445 assert len(secret) == 8
446 self.secret = decode(secret)
447 assert len(self.secret) == 5
448
449 def get_challenge(self):
450 self.challenge = os.urandom(20)
451 return encode(self.challenge)
452
453 def create_response(self, challenge):
454 nonce = os.urandom(20)
455 response = compute_response(
456 self.secret,
457 decode(challenge),
458 nonce,
459 self.remote_hash,
460 self.local_hash
461 )
462 return (encode(nonce), response)
463
464 def check_response(self, nonce, response):
465 expected = compute_response(
466 self.secret,
467 self.challenge,
468 decode(nonce),
469 self.local_hash,
470 self.remote_hash
471 )
472 if response != expected:
473 del self.secret
474 del self.challenge
475 raise WrongResponse(expected, response)
476
477
478class InfoApp:
479 def __init__(self, _id):
480 self.id = _id
481 obj = {
482 'id': _id,
483 'user': USER,
484 'host': HOST,
485 }
486 self.info = dumps(obj).encode('utf-8')
487 self.info_length = str(len(self.info))
488
489 def __call__(self, environ, start_response):
490 if environ['wsgi.multithread'] is not False:
491 raise WSGIError('500 Internal Server Error')
492 if environ['PATH_INFO'] != '/':
493 raise WSGIError('410 Gone')
494 if environ['REQUEST_METHOD'] != 'GET':
495 raise WSGIError('405 Method Not Allowed')
496 start_response('200 OK',
497 [
498 ('Content-Length', self.info_length),
499 ('Content-Type', 'application/json'),
500 ]
501 )
502 return [self.info]
503
504
505class ClientApp:
506 allowed_states = (
507 'ready',
508 'gave_challenge',
509 'in_response',
510 'wrong_response',
511 'response_ok',
512 )
513
514 forwarded_states = (
515 'wrong_response',
516 'response_ok',
517 )
518
519 def __init__(self, cr, queue):
520 assert isinstance(cr, ChallengeResponse)
521 self.cr = cr
522 self.queue = queue
523 self.__state = None
524 self.map = {
525 '/challenge': self.get_challenge,
526 '/response': self.put_response,
527 }
528
529 def get_state(self):
530 return self.__state
531
532 def set_state(self, state):
533 if state not in self.__class__.allowed_states:
534 self.__state = None
535 log.error('invalid state: %r', state)
536 raise Exception('invalid state: {!r}'.format(state))
537 self.__state = state
538 if state in self.__class__.forwarded_states:
539 self.queue.put(state)
540
541 state = property(get_state, set_state)
542
543 def __call__(self, environ, start_response):
544 if environ['wsgi.multithread'] is not False:
545 raise WSGIError('500 Internal Server Error')
546 if environ.get('SSL_CLIENT_VERIFY') != 'SUCCESS':
547 raise WSGIError('403 Forbidden')
548 if environ.get('SSL_CLIENT_S_DN_CN') != self.cr.peer_id:
549 raise WSGIError('403 Forbidden')
550 if environ.get('SSL_CLIENT_I_DN_CN') != self.cr.peer_id:
551 raise WSGIError('403 Forbidden')
552
553 path_info = environ['PATH_INFO']
554 if path_info not in self.map:
555 raise WSGIError('410 Gone')
556 log.info('%s %s', environ['REQUEST_METHOD'], environ['PATH_INFO'])
557 try:
558 obj = self.map[path_info](environ)
559 data = json.dumps(obj).encode('utf-8')
560 start_response('200 OK',
561 [
562 ('Content-Length', str(len(data))),
563 ('Content-Type', 'application/json'),
564 ]
565 )
566 return [data]
567 except WSGIError as e:
568 raise e
569 except Exception:
570 log.exception('500 Internal Server Error')
571 raise WSGIError('500 Internal Server Error')
572
573 def get_challenge(self, environ):
574 if self.state != 'ready':
575 raise WSGIError('400 Bad Request Order')
576 self.state = 'gave_challenge'
577 if environ['REQUEST_METHOD'] != 'GET':
578 raise WSGIError('405 Method Not Allowed')
579 return {
580 'challenge': self.cr.get_challenge(),
581 }
582
583 def put_response(self, environ):
584 if self.state != 'gave_challenge':
585 raise WSGIError('400 Bad Request Order')
586 self.state = 'in_response'
587 if environ['REQUEST_METHOD'] != 'PUT':
588 raise WSGIError('405 Method Not Allowed')
589 data = environ['wsgi.input'].read()
590 obj = json.loads(data.decode('utf-8'))
591 nonce = obj['nonce']
592 response = obj['response']
593 try:
594 self.cr.check_response(nonce, response)
595 except WrongResponse:
596 self.state = 'wrong_response'
597 raise WSGIError('401 Unauthorized')
598 self.state = 'response_ok'
599 return {'ok': True}
600
601
602class ServerApp(ClientApp):
603
604 allowed_states = (
605 'info',
606 'counter_response_ok',
607 'in_csr',
608 'bad_csr',
609 'cert_issued',
610 ) + ClientApp.allowed_states
611
612 forwarded_states = (
613 'bad_csr',
614 'cert_issued',
615 ) + ClientApp.forwarded_states
616
617 def __init__(self, cr, queue, pki):
618 super().__init__(cr, queue)
619 self.pki = pki
620 self.map['/'] = self.get_info
621 self.map['/csr'] = self.post_csr
622
623 def get_info(self, environ):
624 if self.state != 'info':
625 raise WSGIError('400 Bad Request State')
626 self.state = 'ready'
627 if environ['REQUEST_METHOD'] != 'GET':
628 raise WSGIError('405 Method Not Allowed')
629 return {
630 'id': self.cr.id,
631 'user': USER,
632 'host': HOST,
633 }
634
635 def post_csr(self, environ):
636 if self.state != 'counter_response_ok':
637 raise WSGIError('400 Bad Request Order')
638 self.state = 'in_csr'
639 if environ['REQUEST_METHOD'] != 'POST':
640 raise WSGIError('405 Method Not Allowed')
641 data = environ['wsgi.input'].read()
642 obj = json.loads(data.decode('utf-8'))
643 csr_data = base64.b64decode(obj['csr'].encode('utf-8'))
644 try:
645 self.pki.write_csr(self.cr.peer_id, csr_data)
646 self.pki.issue_cert(self.cr.peer_id, self.cr.id)
647 cert_data = self.pki.read_cert2(self.cr.peer_id, self.cr.id)
648 except Exception as e:
649 log.exception('could not issue cert')
650 self.state = 'bad_csr'
651 raise WSGIError('401 Unauthorized')
652 self.state = 'cert_issued'
653 return {'cert': base64.b64encode(cert_data).decode('utf-8')}
338654
339655
340def ensuredir(d):656def ensuredir(d):
@@ -376,6 +692,18 @@
376 key_file = self.path(_id, 'key')692 key_file = self.path(_id, 'key')
377 return verify_key(key_file, _id)693 return verify_key(key_file, _id)
378694
695 def read_key(self, _id):
696 key_file = self.verify_key(_id)
697 return open(key_file, 'rb').read()
698
699 def write_key(self, _id, data):
700 tmp_file = self.random_tmp()
701 open(tmp_file, 'wb').write(data)
702 verify_key(tmp_file, _id)
703 key_file = self.path(_id, 'key')
704 os.rename(tmp_file, key_file)
705 return key_file
706
379 def create_ca(self, _id):707 def create_ca(self, _id):
380 key_file = self.verify_key(_id)708 key_file = self.verify_key(_id)
381 subject = make_subject(_id)709 subject = make_subject(_id)
@@ -387,7 +715,19 @@
387715
388 def verify_ca(self, _id):716 def verify_ca(self, _id):
389 ca_file = self.path(_id, 'ca')717 ca_file = self.path(_id, 'ca')
390 return verify(ca_file, _id)718 return verify_ca(ca_file, _id)
719
720 def read_ca(self, _id):
721 ca_file = self.verify_ca(_id)
722 return open(ca_file, 'rb').read()
723
724 def write_ca(self, _id, data):
725 tmp_file = self.random_tmp()
726 open(tmp_file, 'wb').write(data)
727 verify_ca(tmp_file, _id)
728 ca_file = self.path(_id, 'ca')
729 os.rename(tmp_file, ca_file)
730 return ca_file
391731
392 def create_csr(self, _id):732 def create_csr(self, _id):
393 key_file = self.verify_key(_id)733 key_file = self.verify_key(_id)
@@ -402,6 +742,18 @@
402 csr_file = self.path(_id, 'csr')742 csr_file = self.path(_id, 'csr')
403 return verify_csr(csr_file, _id)743 return verify_csr(csr_file, _id)
404744
745 def read_csr(self, _id):
746 csr_file = self.verify_csr(_id)
747 return open(csr_file, 'rb').read()
748
749 def write_csr(self, _id, data):
750 tmp_file = self.random_tmp()
751 open(tmp_file, 'wb').write(data)
752 verify_csr(tmp_file, _id)
753 csr_file = self.path(_id, 'csr')
754 os.rename(tmp_file, csr_file)
755 return csr_file
756
405 def issue_cert(self, _id, ca_id):757 def issue_cert(self, _id, ca_id):
406 csr_file = self.verify_csr(_id)758 csr_file = self.verify_csr(_id)
407 tmp_file = self.random_tmp()759 tmp_file = self.random_tmp()
@@ -432,6 +784,36 @@
432 cert_file = self.path(_id, 'cert')784 cert_file = self.path(_id, 'cert')
433 return verify(cert_file, _id)785 return verify(cert_file, _id)
434786
787 def verify_cert2(self, cert_id, ca_id):
788 cert_file = self.path(cert_id, 'cert')
789 ca_file = self.verify_ca(ca_id)
790 return verify_cert(cert_file, cert_id, ca_file, ca_id)
791
792 def read_cert(self, _id):
793 cert_file = self.verify_cert(_id)
794 return open(cert_file, 'rb').read()
795
796 def read_cert2(self, cert_id, ca_id):
797 cert_file = self.verify_cert2(cert_id, ca_id)
798 return open(cert_file, 'rb').read()
799
800 def write_cert(self, _id, data):
801 tmp_file = self.random_tmp()
802 open(tmp_file, 'wb').write(data)
803 verify(tmp_file, _id)
804 cert_file = self.path(_id, 'cert')
805 os.rename(tmp_file, cert_file)
806 return cert_file
807
808 def write_cert2(self, cert_id, ca_id, cert_data):
809 ca_file = self.verify_ca(ca_id)
810 tmp_file = self.random_tmp()
811 open(tmp_file, 'wb').write(cert_data)
812 verify_cert(tmp_file, cert_id, ca_file, ca_id)
813 cert_file = self.path(cert_id, 'cert')
814 os.rename(tmp_file, cert_file)
815 return cert_file
816
435 def get_ca(self, _id):817 def get_ca(self, _id):
436 return CA(_id, self.verify_ca(_id))818 return CA(_id, self.verify_ca(_id))
437819
@@ -499,4 +881,3 @@
499 'key_file': self.client.key_file, 881 'key_file': self.client.key_file,
500 })882 })
501 return config883 return config
502
503884
=== modified file 'dmedia/service/avahi.py'
--- dmedia/service/avahi.py 2012-10-04 20:23:22 +0000
+++ dmedia/service/avahi.py 2012-10-09 20:57:30 +0000
@@ -36,6 +36,7 @@
36from dmedia import util, views36from dmedia import util, views
3737
38log = logging.getLogger()38log = logging.getLogger()
39PROTO = 0 # Protocol -1 = both, 0 = IPv4, 1 = IPv6
39Peer = namedtuple('Peer', 'env names')40Peer = namedtuple('Peer', 'env names')
40PEERS = '_local/peers'41PEERS = '_local/peers'
4142
@@ -69,7 +70,7 @@
69 )70 )
70 self.group.AddService(71 self.group.AddService(
71 -1, # Interface72 -1, # Interface
72 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv673 PROTO, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
73 0, # Flags74 0, # Flags
74 self.id,75 self.id,
75 self.service,76 self.service,
@@ -82,9 +83,9 @@
82 self.group.Commit(dbus_interface='org.freedesktop.Avahi.EntryGroup')83 self.group.Commit(dbus_interface='org.freedesktop.Avahi.EntryGroup')
83 browser_path = self.avahi.ServiceBrowserNew(84 browser_path = self.avahi.ServiceBrowserNew(
84 -1, # Interface85 -1, # Interface
85 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv686 PROTO, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
86 self.service,87 self.service,
87 'local',88 '', # Domain, default to .local
88 0, # Flags89 0, # Flags
89 dbus_interface='org.freedesktop.Avahi.Server'90 dbus_interface='org.freedesktop.Avahi.Server'
90 )91 )
@@ -107,7 +108,8 @@
107 if key == self.id:108 if key == self.id:
108 return109 return
109 self.avahi.ResolveService(110 self.avahi.ResolveService(
110 interface, protocol, key, _type, domain, -1, 0,111 # 2nd to last arg is Protocol, again for some reason
112 interface, protocol, key, _type, domain, PROTO, 0,
111 dbus_interface='org.freedesktop.Avahi.Server',113 dbus_interface='org.freedesktop.Avahi.Server',
112 reply_handler=self.on_reply,114 reply_handler=self.on_reply,
113 error_handler=self.on_error,115 error_handler=self.on_error,
@@ -175,7 +177,7 @@
175 except KeyError:177 except KeyError:
176 pass178 pass
177 self.remove_replication_peer(key)179 self.remove_replication_peer(key)
178 180
179 def on_timeout(self):181 def on_timeout(self):
180 if not self.replications:182 if not self.replications:
181 return True # Repeat timeout call183 return True # Repeat timeout call
182184
=== modified file 'dmedia/service/peers.py'
--- dmedia/service/peers.py 2012-09-20 12:56:04 +0000
+++ dmedia/service/peers.py 2012-10-09 20:57:30 +0000
@@ -21,45 +21,255 @@
2121
22"""22"""
23Browse for Dmedia peer offerings, publish the same.23Browse for Dmedia peer offerings, publish the same.
24
25Existing machines constantly listen for _dmedia-offer._tcp.
26
27New machine publishes _dmedia-offer._tcp, and listens for _dmedia-accept._tcp.
28
29Existing machine prompts user, and if they accept, machine publishes
30_dmedia-accept._tcp, which initiates peering process.
24"""31"""
2532
26import logging33import logging
34from collections import namedtuple
35import ssl
36import socket
37import threading
2738
28import dbus39import dbus
40from dbus.mainloop.glib import DBusGMainLoop
29from gi.repository import GObject41from gi.repository import GObject
3042from microfiber import _start_thread, random_id, CouchBase, dumps, build_ssl_context
3143
44PROTO = 0 # Protocol -1 = both, 0 = IPv4, 1 = IPv6
45GObject.threads_init()
46DBusGMainLoop(set_as_default=True)
32log = logging.getLogger()47log = logging.getLogger()
3348
3449Peer = namedtuple('Peer', 'id ip port')
35class Peer:50Info = namedtuple('Info', 'name host url id')
36 def __init__(self, _id):51
52
53def get_service(verb):
54 """
55 Get Avahi service name for appropriate direction.
56
57 For example, for an offer:
58
59 >>> get_service('offer')
60 '_dmedia-offer._tcp'
61
62 And for an accept:
63
64 >>> get_service('accept')
65 '_dmedia-accept._tcp'
66
67 """
68 assert verb in ('offer', 'accept')
69 return '_dmedia-{}._tcp'.format(verb)
70
71
72class State:
73 """
74 A state machine to help prevent silly mistakes.
75
76 So that threading issues don't make the code difficult to reason about,
77 a thread-lock is acquired when making a state change. To be on the safe
78 side, you should only make state changes from the main thread. But the
79 thread-lock is there as a safety in case an attacker could change the
80 execution such that something isn't called from the main thread, or in case
81 an oversight is made by the programmer.
82 """
83 def __init__(self):
84 self.__state = 'free'
85 self.__peer_id = None
86 self.__lock = threading.Lock()
87
88 def __repr__(self):
89 return 'State(state={!r}, peer_id={!r})'.format(
90 self.__state, self.__peer_id
91 )
92
93 @property
94 def state(self):
95 return self.__state
96
97 @property
98 def peer_id(self):
99 return self.__peer_id
100
101 def bind(self, peer_id):
102 with self.__lock:
103 assert peer_id is not None
104 if self.__state != 'free':
105 return False
106 if self.__peer_id is not None:
107 return False
108 self.__state = 'bound'
109 self.__peer_id = peer_id
110 return True
111
112 def verify(self, peer_id):
113 with self.__lock:
114 if self.__state != 'bound':
115 return False
116 if peer_id is None or peer_id != self.__peer_id:
117 return False
118 self.__state = 'verified'
119 return True
120
121 def unbind(self, peer_id):
122 with self.__lock:
123 if self.__state not in ('bound', 'verified'):
124 return False
125 if peer_id is None or peer_id != self.__peer_id:
126 return False
127 self.__state = 'unbound'
128 return True
129
130 def activate(self, peer_id):
131 with self.__lock:
132 if self.__state != 'verified':
133 return False
134 if peer_id is None or peer_id != self.__peer_id:
135 return False
136 self.__state = 'activated'
137 self.__peer_id = peer_id
138 return True
139
140 def deactivate(self, peer_id):
141 with self.__lock:
142 if self.__state != 'activated':
143 return False
144 if peer_id is None or peer_id != self.__peer_id:
145 return False
146 self.__state = 'deactivated'
147 return True
148
149 def free(self, peer_id):
150 with self.__lock:
151 if self.__state not in ('unbound', 'deactivated'):
152 return False
153 if peer_id is None or peer_id != self.__peer_id:
154 return False
155 self.__state = 'free'
156 self.__peer_id = None
157 return True
158
159
160class AvahiPeer(GObject.GObject):
161 __gsignals__ = {
162 'offer': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
163 [GObject.TYPE_PYOBJECT]
164 ),
165 'accept': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
166 [GObject.TYPE_PYOBJECT]
167 ),
168 'retract': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
169 []
170 ),
171 }
172
173 def __init__(self, pki, client_mode=False):
174 super().__init__()
37 self.group = None175 self.group = None
38 self.id = _id176 self.pki = pki
39 log.info('This cert_id = %s', _id)177 self.client_mode = client_mode
178 self.id = (pki.machine.id if client_mode else pki.user.id)
179 self.cert_file = pki.verify_ca(self.id)
180 self.key_file = pki.verify_key(self.id)
181 self.state = State()
182 self.peer = None
183 self.info = None
40 self.bus = dbus.SystemBus()184 self.bus = dbus.SystemBus()
41 self.avahi = self.bus.get_object('org.freedesktop.Avahi', '/')185 self.avahi = self.bus.get_object('org.freedesktop.Avahi', '/')
42186
43 def __del__(self):187 def __del__(self):
44 self.unpublish()188 self.unpublish()
45189
46 def browse(self, service):190 def activate(self, peer_id):
47 self.bservice = service191 if not self.state.activate(peer_id):
48 log.info('Avahi(%s): browsing...', service)192 raise Exception(
49 browser_path = self.avahi.ServiceBrowserNew(193 'Cannot activate {!r} from {!r}'.format(peer_id, self.state)
50 -1, # Interface194 )
51 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6195 log.info('Activated session with %r', self.peer)
52 service,196 assert self.state.state == 'activated'
53 'local',197 assert self.state.peer_id == peer_id
54 0, # Flags198 assert self.peer.id == peer_id
55 dbus_interface='org.freedesktop.Avahi.Server'199 assert self.info.id == peer_id
56 )200 assert self.info.url == 'https://{}:{}/'.format(
57 self.browser = self.bus.get_object('org.freedesktop.Avahi', browser_path)201 self.peer.ip, self.peer.port
58 self.browser.connect_to_signal('ItemNew', self.on_ItemNew)202 )
59 self.browser.connect_to_signal('ItemRemove', self.on_ItemRemove)203
60204 def deactivate(self, peer_id):
61 def publish(self, service, port):205 if not self.state.deactivate(peer_id):
62 self.pservice = service206 raise Exception(
207 'Cannot deactivate {!r} from {!r}'.format(peer_id, self.state)
208 )
209 log.info('Deactivated session with %r', self.peer)
210 assert self.state.state == 'deactivated'
211 assert self.state.peer_id == peer_id
212 assert self.peer.id == peer_id
213 assert self.info.id == peer_id
214 assert self.info.url == 'https://{}:{}/'.format(
215 self.peer.ip, self.peer.port
216 )
217 GObject.timeout_add(15 * 1000, self.on_timeout, peer_id)
218
219 def abort(self, peer_id):
220 GObject.idle_add(self.unbind, peer_id)
221
222 def unbind(self, peer_id):
223 retract = (self.state.state == 'verified')
224 if not self.state.unbind(peer_id):
225 log.error('Cannot unbind %s from %r', peer_id, self.state)
226 return
227 log.info('Unbound from %s', peer_id)
228 assert self.state.peer_id == peer_id
229 assert self.state.state == 'unbound'
230 if retract:
231 log.info("Firing 'retract' signal")
232 self.emit('retract')
233 GObject.timeout_add(10 * 1000, self.on_timeout, peer_id)
234
235 def on_timeout(self, peer_id):
236 if not self.state.free(peer_id):
237 log.error('Cannot free %s from %r', peer_id, self.state)
238 return
239 log.info('Rate-limiting timeout reached, freeing from %s', peer_id)
240 assert self.state.state == 'free'
241 assert self.state.peer_id is None
242 self.info = None
243 self.peer = None
244
245 def get_server_config(self):
246 """
247 Get the initial server SSL config.
248 """
249 assert self.state.state in ('free', 'activated')
250 config = {
251 'key_file': self.key_file,
252 'cert_file': self.cert_file,
253 }
254 if self.client_mode is False or self.state.state == 'activated':
255 config['ca_file'] = self.pki.verify_ca(self.state.peer_id)
256 return config
257
258 def get_client_config(self):
259 """
260 Get the client SSL config.
261 """
262 assert self.state.state == 'activated'
263 return {
264 'ca_file': self.pki.verify_ca(self.state.peer_id),
265 'check_hostname': False,
266 'key_file': self.key_file,
267 'cert_file': self.cert_file,
268 }
269
270 def publish(self, port):
271 verb = ('offer' if self.client_mode else 'accept')
272 service = get_service(verb)
63 self.group = self.bus.get_object(273 self.group = self.bus.get_object(
64 'org.freedesktop.Avahi',274 'org.freedesktop.Avahi',
65 self.avahi.EntryGroupNew(275 self.avahi.EntryGroupNew(
@@ -67,11 +277,11 @@
67 )277 )
68 )278 )
69 log.info(279 log.info(
70 'Avahi(%s): publishing %s on port %s', service, self.id, port280 'Publishing %s for %r on port %s', self.id, service, port
71 )281 )
72 self.group.AddService(282 self.group.AddService(
73 -1, # Interface283 -1, # Interface
74 0, # Protocol -1 = both, 0 = ipv4, 1 = ipv6284 PROTO, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
75 0, # Flags285 0, # Flags
76 self.id,286 self.id,
77 service,287 service,
@@ -85,37 +295,125 @@
85295
86 def unpublish(self):296 def unpublish(self):
87 if self.group is not None:297 if self.group is not None:
88 log.info('Avahi(%s): unpublishing %s', self.pservice, self.id)298 log.info('Un-publishing %s', self.id)
89 self.group.Reset(dbus_interface='org.freedesktop.Avahi.EntryGroup')299 self.group.Reset(dbus_interface='org.freedesktop.Avahi.EntryGroup')
90 self.group = None300 self.group = None
91301
92 def on_ItemNew(self, interface, protocol, key, _type, domain, flags):302 def browse(self):
303 verb = ('accept' if self.client_mode else 'offer')
304 service = get_service(verb)
305 log.info('Browsing for %r', service)
306 path = self.avahi.ServiceBrowserNew(
307 -1, # Interface
308 PROTO, # Protocol -1 = both, 0 = ipv4, 1 = ipv6
309 service,
310 '', # Domain, default to .local
311 0, # Flags
312 dbus_interface='org.freedesktop.Avahi.Server'
313 )
314 self.browser = self.bus.get_object('org.freedesktop.Avahi', path)
315 self.browser.connect_to_signal('ItemNew', self.on_ItemNew)
316 self.browser.connect_to_signal('ItemRemove', self.on_ItemRemove)
317
318 def on_ItemNew(self, interface, protocol, peer_id, _type, domain, flags):
319 log.info('Peer added: %s', peer_id)
320 if not self.state.bind(str(peer_id)):
321 log.error('Cannot bind %s from %r', peer_id, self.state)
322 log.warning('Possible attack from %s', peer_id)
323 return
324 assert self.state.state == 'bound'
325 assert self.state.peer_id == peer_id
326 log.info('Bound to %s', peer_id)
93 self.avahi.ResolveService(327 self.avahi.ResolveService(
94 interface, protocol, key, _type, domain, -1, 0,328 # 2nd to last arg is Protocol, again for some reason
329 interface, protocol, peer_id, _type, domain, PROTO, 0,
95 dbus_interface='org.freedesktop.Avahi.Server',330 dbus_interface='org.freedesktop.Avahi.Server',
96 reply_handler=self.on_reply,331 reply_handler=self.on_reply,
97 error_handler=self.on_error,332 error_handler=self.on_error,
98 )333 )
99334
100 def on_reply(self, *args):335 def on_reply(self, *args):
101 key = args[2]336 peer_id = args[2]
337 if self.state.peer_id != peer_id or self.state.state != 'bound':
338 log.error(
339 '%s: state mismatch in on_reply(): %r', peer_id, self.state
340 )
341 return
102 (ip, port) = args[7:9]342 (ip, port) = args[7:9]
103 url = 'http://{}:{}/'.format(ip, port)343 log.info('%s is at %s, port %s', peer_id, ip, port)
104 log.info('Avahi(%s): new peer %s at %s', self.bservice, key, url)344 self.peer = Peer(str(peer_id), str(ip), int(port))
105345 _start_thread(self.cert_thread, self.peer)
106 def on_error(self, exception):346
107 log.error('%s: error calling ResolveService(): %r', self.bservice, exception)347 def on_error(self, error):
108348 log.error(
109 def on_ItemRemove(self, interface, protocol, key, _type, domain, flags):349 '%s: error calling ResolveService(): %r', self.state.peer_id, error
110 log.info('Avahi(%s): peer removed: %s', self.bservice, key)350 )
111351 self.abort(self.state.peer_id)
112352
113353 def on_ItemRemove(self, interface, protocol, peer_id, _type, domain, flags):
114class Browser:354 log.info('Peer removed: %s', peer_id)
115 def __init__(self, service, add_callback, remove_callback):355 self.abort(peer_id)
116 self.service = service356
117 self.add_callback = add_callback357 def cert_thread(self, peer):
118 self.remove_callback = remove_callback358 # 1 Retrieve the peer certificate:
119 359 try:
120 360 ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
121361 ctx.options |= ssl.OP_NO_COMPRESSION
362 if self.client_mode:
363 # The server will only let its cert be retrieved by the client
364 # bound to the peering session
365 ctx.load_cert_chain(self.cert_file, self.key_file)
366 sock = ctx.wrap_socket(
367 socket.socket(socket.AF_INET, socket.SOCK_STREAM)
368 )
369 sock.connect((peer.ip, peer.port))
370 pem = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True))
371 except Exception as e:
372 log.exception('Could not retrieve cert for %r', peer)
373 return self.abort(peer.id)
374 log.info('Retrieved cert for %r', peer)
375
376 # 2 Make sure peer cert has correct intrinsic CN, etc:
377 try:
378 ca_file = self.pki.write_ca(peer.id, pem.encode('ascii'))
379 except Exception as e:
380 log.exception('Could not verify cert for %r', peer)
381 return self.abort(peer.id)
382 log.info('Verified cert for %r', peer)
383
384 # 3 Make get request to verify peer has private key:
385 try:
386 url = 'https://{}:{}/'.format(peer.ip, peer.port)
387 ssl_config = {
388 'ca_file': ca_file,
389 'check_hostname': False,
390 }
391 if self.client_mode:
392 ssl_config.update({
393 'key_file': self.key_file,
394 'cert_file': self.cert_file,
395 })
396 client = CouchBase({'url': url, 'ssl': ssl_config})
397 d = client.get()
398 info = Info(d['user'], d['host'], url, peer.id)
399 except Exception as e:
400 log.exception('GET / failed for %r', peer)
401 return self.abort(peer.id)
402 log.info('GET / succeeded with %r', info)
403 GObject.idle_add(self.on_cert_complete, peer, info)
404
405 def on_cert_complete(self, peer, info):
406 if not self.state.verify(peer.id):
407 log.error(
408 '%s: mismatch in on_cert_complete(): %r', peer.id, self.state
409 )
410 return
411 assert self.state.state == 'verified'
412 assert self.state.peer_id == peer.id
413 assert peer is self.peer
414 assert self.info is None
415 self.info = info
416 log.info('Cert checked-out for %r', peer)
417 signal = ('accept' if self.client_mode else 'offer')
418 log.info('Firing %r signal for %r', signal, info)
419 self.emit(signal, info)
122420
=== modified file 'dmedia/tests/test_peering.py'
--- dmedia/tests/test_peering.py 2012-10-01 20:00:25 +0000
+++ dmedia/tests/test_peering.py 2012-10-09 20:57:30 +0000
@@ -27,10 +27,15 @@
27import os27import os
28from os import path28from os import path
29import subprocess29import subprocess
30import socket
31from queue import Queue
3032
31from microfiber import random_id33import microfiber
34from microfiber import random_id, CouchBase
3235
33from .base import TempDir36from .base import TempDir
37from dmedia.httpd import make_server
38from dmedia.peering import encode, decode
34from dmedia import peering39from dmedia import peering
3540
3641
@@ -162,6 +167,445 @@
162 os.remove(key_file)167 os.remove(key_file)
163 self.assertEqual(peering.get_csr_subject(csr_file), subject)168 self.assertEqual(peering.get_csr_subject(csr_file), subject)
164169
170 def test_get_issuer(self):
171 tmp = TempDir()
172
173 foo_subject = '/CN={}'.format(random_id(30))
174 foo_key = tmp.join('foo.key')
175 foo_ca = tmp.join('foo.ca')
176 foo_srl = tmp.join('foo.srl')
177 peering.create_key(foo_key)
178 peering.create_ca(foo_key, foo_subject, foo_ca)
179 self.assertEqual(peering.get_issuer(foo_ca), foo_subject)
180
181 bar_subject = '/CN={}'.format(random_id(30))
182 bar_key = tmp.join('bar.key')
183 bar_csr = tmp.join('bar.csr')
184 bar_cert = tmp.join('bar.cert')
185 peering.create_key(bar_key)
186 peering.create_csr(bar_key, bar_subject, bar_csr)
187 peering.issue_cert(bar_csr, foo_ca, foo_key, foo_srl, bar_cert)
188 self.assertEqual(peering.get_csr_subject(bar_csr), bar_subject)
189 self.assertEqual(peering.get_issuer(bar_cert), foo_subject)
190
191 def test_ssl_verify(self):
192 tmp = TempDir()
193 pki = peering.PKI(tmp.dir)
194
195 ca1 = pki.create_key()
196 pki.create_ca(ca1)
197 cert1 = pki.create_key()
198 pki.create_csr(cert1)
199 pki.issue_cert(cert1, ca1)
200 ca1_file = pki.path(ca1, 'ca')
201 cert1_file = pki.path(cert1, 'cert')
202 self.assertEqual(peering.ssl_verify(ca1_file, ca1_file), ca1_file)
203 self.assertEqual(peering.ssl_verify(cert1_file, ca1_file), cert1_file)
204 with self.assertRaises(peering.VerificationError) as cm:
205 peering.ssl_verify(ca1_file, cert1_file)
206 with self.assertRaises(peering.VerificationError) as cm:
207 peering.ssl_verify(cert1_file, cert1_file)
208
209 ca2 = pki.create_key()
210 pki.create_ca(ca2)
211 cert2 = pki.create_key()
212 pki.create_csr(cert2)
213 pki.issue_cert(cert2, ca2)
214 ca2_file = pki.path(ca2, 'ca')
215 cert2_file = pki.path(cert2, 'cert')
216 self.assertEqual(peering.ssl_verify(ca2_file, ca2_file), ca2_file)
217 self.assertEqual(peering.ssl_verify(cert2_file, ca2_file), cert2_file)
218 with self.assertRaises(peering.VerificationError) as cm:
219 peering.ssl_verify(ca2_file, cert2_file)
220 with self.assertRaises(peering.VerificationError) as cm:
221 peering.ssl_verify(cert2_file, cert2_file)
222
223 with self.assertRaises(peering.VerificationError) as cm:
224 peering.ssl_verify(ca2_file, ca1_file)
225 with self.assertRaises(peering.VerificationError) as cm:
226 peering.ssl_verify(cert2_file, ca1_file)
227 with self.assertRaises(peering.VerificationError) as cm:
228 peering.ssl_verify(cert2_file, cert1_file)
229
230
231class TestChallengeResponse(TestCase):
232 def test_init(self):
233 id1 = random_id(30)
234 id2 = random_id(30)
235 inst = peering.ChallengeResponse(id1, id2)
236 self.assertIs(inst.id, id1)
237 self.assertIs(inst.peer_id, id2)
238 self.assertEqual(inst.local_hash, peering.decode(id1))
239 self.assertEqual(inst.remote_hash, peering.decode(id2))
240
241 def test_get_secret(self):
242 id1 = random_id(30)
243 id2 = random_id(30)
244 inst = peering.ChallengeResponse(id1, id2)
245 s1 = inst.get_secret()
246 self.assertIsInstance(s1, str)
247 self.assertEqual(len(s1), 8)
248 self.assertEqual(peering.decode(s1), inst.secret)
249 s2 = inst.get_secret()
250 self.assertNotEqual(s1, s2)
251 self.assertIsInstance(s2, str)
252 self.assertEqual(len(s2), 8)
253 self.assertEqual(peering.decode(s2), inst.secret)
254
255 def test_set_secret(self):
256 id1 = random_id(30)
257 id2 = random_id(30)
258 inst = peering.ChallengeResponse(id1, id2)
259 s1 = random_id(5)
260 self.assertIsNone(inst.set_secret(s1))
261 self.assertEqual(peering.encode(inst.secret), s1)
262 s2 = random_id(5)
263 self.assertIsNone(inst.set_secret(s2))
264 self.assertEqual(peering.encode(inst.secret), s2)
265
266 def test_get_challenge(self):
267 id1 = random_id(30)
268 id2 = random_id(30)
269 inst = peering.ChallengeResponse(id1, id2)
270 c1 = inst.get_challenge()
271 self.assertIsInstance(c1, str)
272 self.assertEqual(len(c1), 32)
273 self.assertEqual(peering.decode(c1), inst.challenge)
274 c2 = inst.get_challenge()
275 self.assertNotEqual(c1, c2)
276 self.assertIsInstance(c2, str)
277 self.assertEqual(len(c2), 32)
278 self.assertEqual(peering.decode(c2), inst.challenge)
279
280 def test_create_response(self):
281 id1 = random_id(30)
282 id2 = random_id(30)
283 inst = peering.ChallengeResponse(id1, id2)
284 local_hash = decode(id1)
285 remote_hash = decode(id2)
286 secret1 = random_id(5)
287 challenge1 = random_id(20)
288 inst.set_secret(secret1)
289 (nonce1, response1) = inst.create_response(challenge1)
290 self.assertIsInstance(nonce1, str)
291 self.assertEqual(len(nonce1), 32)
292 self.assertIsInstance(response1, str)
293 self.assertEqual(len(response1), 56)
294 self.assertEqual(response1,
295 peering.compute_response(
296 decode(secret1), decode(challenge1), decode(nonce1),
297 remote_hash, local_hash
298 )
299 )
300
301 # Same secret and challenge, make sure a new nonce is used
302 (nonce2, response2) = inst.create_response(challenge1)
303 self.assertNotEqual(nonce2, nonce1)
304 self.assertNotEqual(response2, response1)
305 self.assertIsInstance(nonce2, str)
306 self.assertEqual(len(nonce2), 32)
307 self.assertIsInstance(response2, str)
308 self.assertEqual(len(response2), 56)
309 self.assertEqual(response2,
310 peering.compute_response(
311 decode(secret1), decode(challenge1), decode(nonce2),
312 remote_hash, local_hash
313 )
314 )
315
316 # Different secret
317 secret2 = random_id(5)
318 inst.set_secret(secret2)
319 (nonce3, response3) = inst.create_response(challenge1)
320 self.assertNotEqual(nonce3, nonce1)
321 self.assertNotEqual(response3, response1)
322 self.assertNotEqual(nonce3, nonce2)
323 self.assertNotEqual(response3, response2)
324 self.assertIsInstance(nonce3, str)
325 self.assertEqual(len(nonce3), 32)
326 self.assertIsInstance(response3, str)
327 self.assertEqual(len(response3), 56)
328 self.assertEqual(response3,
329 peering.compute_response(
330 decode(secret2), decode(challenge1), decode(nonce3),
331 remote_hash, local_hash
332 )
333 )
334
335 # Different challenge
336 challenge2 = random_id(20)
337 (nonce4, response4) = inst.create_response(challenge2)
338 self.assertNotEqual(nonce4, nonce1)
339 self.assertNotEqual(response4, response1)
340 self.assertNotEqual(nonce4, nonce2)
341 self.assertNotEqual(response4, response2)
342 self.assertNotEqual(nonce4, nonce3)
343 self.assertNotEqual(response4, response3)
344 self.assertIsInstance(nonce4, str)
345 self.assertEqual(len(nonce4), 32)
346 self.assertIsInstance(response4, str)
347 self.assertEqual(len(response4), 56)
348 self.assertEqual(response4,
349 peering.compute_response(
350 decode(secret2), decode(challenge2), decode(nonce4),
351 remote_hash, local_hash
352 )
353 )
354
355 def test_check_response(self):
356 id1 = random_id(30)
357 id2 = random_id(30)
358 inst = peering.ChallengeResponse(id1, id2)
359 local_hash = decode(id1)
360 remote_hash = decode(id2)
361 secret = inst.get_secret()
362 challenge = inst.get_challenge()
363 nonce = random_id(20)
364 response = peering.compute_response(
365 decode(secret), decode(challenge), decode(nonce),
366 local_hash, remote_hash
367 )
368 self.assertIsNone(inst.check_response(nonce, response))
369
370 # Test with (local, remote) order flipped
371 bad = peering.compute_response(
372 decode(secret), decode(challenge), decode(nonce),
373 remote_hash, local_hash
374 )
375 with self.assertRaises(peering.WrongResponse) as cm:
376 inst.check_response(nonce, bad)
377 self.assertEqual(cm.exception.expected, response)
378 self.assertEqual(cm.exception.got, bad)
379 self.assertFalse(hasattr(inst, 'secret'))
380 self.assertFalse(hasattr(inst, 'challenge'))
381 inst.secret = decode(secret)
382 inst.challenge = decode(challenge)
383
384 # Test with wrong secret
385 for i in range(100):
386 bad = peering.compute_response(
387 os.urandom(5), decode(challenge), decode(nonce),
388 local_hash, remote_hash
389 )
390 with self.assertRaises(peering.WrongResponse) as cm:
391 inst.check_response(nonce, bad)
392 self.assertEqual(cm.exception.expected, response)
393 self.assertEqual(cm.exception.got, bad)
394 self.assertFalse(hasattr(inst, 'secret'))
395 self.assertFalse(hasattr(inst, 'challenge'))
396 inst.secret = decode(secret)
397 inst.challenge = decode(challenge)
398
399 # Test with wrong challenge
400 for i in range(100):
401 bad = peering.compute_response(
402 decode(secret), os.urandom(20), decode(nonce),
403 local_hash, remote_hash
404 )
405 with self.assertRaises(peering.WrongResponse) as cm:
406 inst.check_response(nonce, bad)
407 self.assertEqual(cm.exception.expected, response)
408 self.assertEqual(cm.exception.got, bad)
409 self.assertFalse(hasattr(inst, 'secret'))
410 self.assertFalse(hasattr(inst, 'challenge'))
411 inst.secret = decode(secret)
412 inst.challenge = decode(challenge)
413
414 # Test with wrong nonce
415 for i in range(100):
416 bad = peering.compute_response(
417 decode(secret), decode(challenge), os.urandom(20),
418 local_hash, remote_hash
419 )
420 with self.assertRaises(peering.WrongResponse) as cm:
421 inst.check_response(nonce, bad)
422 self.assertEqual(cm.exception.expected, response)
423 self.assertEqual(cm.exception.got, bad)
424 self.assertFalse(hasattr(inst, 'secret'))
425 self.assertFalse(hasattr(inst, 'challenge'))
426 inst.secret = decode(secret)
427 inst.challenge = decode(challenge)
428
429 # Test with wrong local_hash
430 for i in range(100):
431 bad = peering.compute_response(
432 decode(secret), decode(challenge), decode(nonce),
433 os.urandom(30), remote_hash
434 )
435 with self.assertRaises(peering.WrongResponse) as cm:
436 inst.check_response(nonce, bad)
437 self.assertEqual(cm.exception.expected, response)
438 self.assertEqual(cm.exception.got, bad)
439 self.assertFalse(hasattr(inst, 'secret'))
440 self.assertFalse(hasattr(inst, 'challenge'))
441 inst.secret = decode(secret)
442 inst.challenge = decode(challenge)
443
444 # Test with wrong remote_hash
445 for i in range(100):
446 bad = peering.compute_response(
447 decode(secret), decode(challenge), decode(nonce),
448 local_hash, os.urandom(30)
449 )
450 with self.assertRaises(peering.WrongResponse) as cm:
451 inst.check_response(nonce, bad)
452 self.assertEqual(cm.exception.expected, response)
453 self.assertEqual(cm.exception.got, bad)
454 self.assertFalse(hasattr(inst, 'secret'))
455 self.assertFalse(hasattr(inst, 'challenge'))
456 inst.secret = decode(secret)
457 inst.challenge = decode(challenge)
458
459 # Test with more nonce, used as expected:
460 for i in range(100):
461 newnonce = random_id(20)
462 good = peering.compute_response(
463 decode(secret), decode(challenge), decode(newnonce),
464 local_hash, remote_hash
465 )
466 self.assertNotEqual(good, response)
467 self.assertIsNone(inst.check_response(newnonce, good))
468
469 # Sanity check on directionality, in other words, check that the
470 # response created locally can't accidentally be verified as the
471 # response from the other end
472 secret = random_id(5)
473 for i in range(1000):
474 inst.set_secret(secret)
475 challenge = inst.get_challenge()
476 (nonce, response) = inst.create_response(challenge)
477 with self.assertRaises(peering.WrongResponse) as cm:
478 inst.check_response(nonce, response)
479
480
481class TestServerApp(TestCase):
482 def test_live(self):
483 tmp = TempDir()
484 pki = peering.PKI(tmp.dir)
485 local_id = pki.create_key()
486 pki.create_ca(local_id)
487 remote_id = pki.create_key()
488 pki.create_ca(remote_id)
489 server_config = {
490 'cert_file': pki.path(local_id, 'ca'),
491 'key_file': pki.path(local_id, 'key'),
492 'ca_file': pki.path(remote_id, 'ca'),
493 }
494 client_config = {
495 'check_hostname': False,
496 'ca_file': pki.path(local_id, 'ca'),
497 'cert_file': pki.path(remote_id, 'ca'),
498 'key_file': pki.path(remote_id, 'key'),
499 }
500 local = peering.ChallengeResponse(local_id, remote_id)
501 remote = peering.ChallengeResponse(remote_id, local_id)
502 q = Queue()
503 app = peering.ServerApp(local, q, None)
504 server = make_server(app, '127.0.0.1', server_config)
505 client = CouchBase({'url': server.url, 'ssl': client_config})
506 server.start()
507 secret = local.get_secret()
508 remote.set_secret(secret)
509
510 self.assertIsNone(app.state)
511 with self.assertRaises(microfiber.BadRequest) as cm:
512 client.get('')
513 self.assertEqual(
514 str(cm.exception),
515 '400 Bad Request State: GET /'
516 )
517 app.state = 'info'
518 self.assertEqual(client.get(),
519 {
520 'id': local_id,
521 'user': os.environ.get('USER'),
522 'host': socket.gethostname(),
523 }
524 )
525 self.assertEqual(app.state, 'ready')
526 with self.assertRaises(microfiber.BadRequest) as cm:
527 client.get('')
528 self.assertEqual(
529 str(cm.exception),
530 '400 Bad Request State: GET /'
531 )
532 self.assertEqual(app.state, 'ready')
533
534 app.state = 'info'
535 with self.assertRaises(microfiber.BadRequest) as cm:
536 client.get('challenge')
537 self.assertEqual(
538 str(cm.exception),
539 '400 Bad Request Order: GET /challenge'
540 )
541 with self.assertRaises(microfiber.BadRequest) as cm:
542 client.put({'hello': 'world'}, 'response')
543 self.assertEqual(
544 str(cm.exception),
545 '400 Bad Request Order: PUT /response'
546 )
547
548 app.state = 'ready'
549 self.assertEqual(app.state, 'ready')
550 obj = client.get('challenge')
551 self.assertEqual(app.state, 'gave_challenge')
552 self.assertIsInstance(obj, dict)
553 self.assertEqual(set(obj), set(['challenge']))
554 self.assertEqual(local.challenge, decode(obj['challenge']))
555 with self.assertRaises(microfiber.BadRequest) as cm:
556 client.get('challenge')
557 self.assertEqual(
558 str(cm.exception),
559 '400 Bad Request Order: GET /challenge'
560 )
561 self.assertEqual(app.state, 'gave_challenge')
562
563 (nonce, response) = remote.create_response(obj['challenge'])
564 obj = {'nonce': nonce, 'response': response}
565 self.assertEqual(client.put(obj, 'response'), {'ok': True})
566 self.assertEqual(app.state, 'response_ok')
567 with self.assertRaises(microfiber.BadRequest) as cm:
568 client.put(obj, 'response')
569 self.assertEqual(
570 str(cm.exception),
571 '400 Bad Request Order: PUT /response'
572 )
573 self.assertEqual(app.state, 'response_ok')
574 self.assertEqual(q.get(), 'response_ok')
575
576 # Test when an error occurs in put_response()
577 app.state = 'gave_challenge'
578 with self.assertRaises(microfiber.ServerError) as cm:
579 client.put(b'bad json', 'response')
580 self.assertEqual(app.state, 'in_response')
581
582 # Test with wrong secret
583 app.state = 'ready'
584 secret = local.get_secret()
585 remote.get_secret()
586 challenge = client.get('challenge')['challenge']
587 self.assertEqual(app.state, 'gave_challenge')
588 (nonce, response) = remote.create_response(challenge)
589 with self.assertRaises(microfiber.Unauthorized) as cm:
590 client.put({'nonce': nonce, 'response': response}, 'response')
591 self.assertEqual(app.state, 'wrong_response')
592 self.assertFalse(hasattr(local, 'secret'))
593 self.assertFalse(hasattr(local, 'challenge'))
594
595 # Verify that you can't retry
596 remote.set_secret(secret)
597 (nonce, response) = remote.create_response(challenge)
598 with self.assertRaises(microfiber.BadRequest) as cm:
599 client.put({'nonce': nonce, 'response': response}, 'response')
600 self.assertEqual(
601 str(cm.exception),
602 '400 Bad Request Order: PUT /response'
603 )
604 self.assertEqual(app.state, 'wrong_response')
605 self.assertEqual(q.get(), 'wrong_response')
606
607 server.shutdown()
608
165609
166class TestPKI(TestCase):610class TestPKI(TestCase):
167 def test_init(self):611 def test_init(self):
@@ -229,6 +673,56 @@
229 with self.assertRaises(subprocess.CalledProcessError) as cm:673 with self.assertRaises(subprocess.CalledProcessError) as cm:
230 pki.verify_key(id2)674 pki.verify_key(id2)
231675
676 def test_read_key(self):
677 tmp = TempDir()
678 pki = peering.PKI(tmp.dir)
679 id1 = pki.create_key()
680 key1_file = tmp.join(id1 + '.key')
681 data1 = open(key1_file, 'rb').read()
682 id2 = pki.create_key()
683 key2_file = tmp.join(id2 + '.key')
684 data2 = open(key2_file, 'rb').read()
685 self.assertEqual(pki.read_key(id1), data1)
686 self.assertEqual(pki.read_key(id2), data2)
687 os.remove(key1_file)
688 os.rename(key2_file, key1_file)
689 with self.assertRaises(peering.PublicKeyError) as cm:
690 pki.read_key(id1)
691 self.assertEqual(cm.exception.filename, key1_file)
692 self.assertEqual(cm.exception.expected, id1)
693 self.assertEqual(cm.exception.got, id2)
694 with self.assertRaises(subprocess.CalledProcessError) as cm:
695 pki.read_key(id2)
696
697 def test_write_key(self):
698 tmp1 = TempDir()
699 src = peering.PKI(tmp1.dir)
700 tmp2 = TempDir()
701 dst = peering.PKI(tmp2.dir)
702
703 id1 = src.create_key()
704 data1 = open(src.verify_key(id1), 'rb').read()
705 id2 = src.create_key()
706 data2 = open(src.verify_key(id2), 'rb').read()
707
708 with self.assertRaises(peering.PublicKeyError) as cm:
709 dst.write_key(id1, data2)
710 self.assertEqual(path.dirname(cm.exception.filename), dst.tmpdir)
711 self.assertEqual(cm.exception.expected, id1)
712 self.assertEqual(cm.exception.got, id2)
713
714 with self.assertRaises(peering.PublicKeyError) as cm:
715 dst.write_key(id2, data1)
716 self.assertEqual(path.dirname(cm.exception.filename), dst.tmpdir)
717 self.assertEqual(cm.exception.expected, id2)
718 self.assertEqual(cm.exception.got, id1)
719
720 self.assertEqual(dst.write_key(id1, data1), dst.path(id1, 'key'))
721 self.assertEqual(open(dst.path(id1, 'key'), 'rb').read(), data1)
722
723 self.assertEqual(dst.write_key(id2, data2), dst.path(id2, 'key'))
724 self.assertEqual(open(dst.path(id2, 'key'), 'rb').read(), data2)
725
232 def test_create_ca(self):726 def test_create_ca(self):
233 tmp = TempDir()727 tmp = TempDir()
234 pki = peering.PKI(tmp.dir)728 pki = peering.PKI(tmp.dir)
@@ -275,6 +769,24 @@
275 self.assertEqual(cm.exception.expected, '/CN={}'.format(id3))769 self.assertEqual(cm.exception.expected, '/CN={}'.format(id3))
276 self.assertEqual(cm.exception.got, '/CN={}'.format(id1))770 self.assertEqual(cm.exception.got, '/CN={}'.format(id1))
277771
772 # Test with bad issuer
773 pki.create_ca(id3)
774 id4 = pki.create_key()
775 pki.create_csr(id4)
776 pki.issue_cert(id4, id3)
777 os.rename(pki.path(id4, 'cert'), pki.path(id4, 'ca'))
778 with self.assertRaises(peering.IssuerError) as cm:
779 pki.verify_ca(id4)
780 self.assertEqual(cm.exception.filename, pki.path(id4, 'ca'))
781 self.assertEqual(cm.exception.expected, '/CN={}'.format(id4))
782 self.assertEqual(cm.exception.got, '/CN={}'.format(id3))
783
784 def test_read_ca(self):
785 self.skipTest('FIXME')
786
787 def test_write_ca(self):
788 self.skipTest('FIXME')
789
278 def test_create_csr(self):790 def test_create_csr(self):
279 tmp = TempDir()791 tmp = TempDir()
280 pki = peering.PKI(tmp.dir)792 pki = peering.PKI(tmp.dir)
@@ -321,6 +833,43 @@
321 self.assertEqual(cm.exception.expected, '/CN={}'.format(id3))833 self.assertEqual(cm.exception.expected, '/CN={}'.format(id3))
322 self.assertEqual(cm.exception.got, '/CN={}'.format(id1))834 self.assertEqual(cm.exception.got, '/CN={}'.format(id1))
323835
836 def test_read_csr(self):
837 tmp = TempDir()
838 pki = peering.PKI(tmp.dir)
839 id1 = pki.create_key()
840 id2 = pki.create_key()
841 csr1_file = pki.create_csr(id1)
842 csr2_file = pki.create_csr(id2)
843 data1 = open(csr1_file, 'rb').read()
844 data2 = open(csr2_file, 'rb').read()
845 os.remove(tmp.join(id1 + '.key'))
846 os.remove(tmp.join(id2 + '.key'))
847 self.assertEqual(pki.read_csr(id1), data1)
848 self.assertEqual(pki.read_csr(id2), data2)
849 os.remove(csr1_file)
850 os.rename(csr2_file, csr1_file)
851 with self.assertRaises(peering.PublicKeyError) as cm:
852 pki.read_csr(id1)
853 self.assertEqual(cm.exception.filename, csr1_file)
854 self.assertEqual(cm.exception.expected, id1)
855 self.assertEqual(cm.exception.got, id2)
856 with self.assertRaises(subprocess.CalledProcessError) as cm:
857 pki.read_csr(id2)
858
859 # Test with bad subject
860 id3 = pki.create_key()
861 key_file = pki.path(id3, 'key')
862 csr_file = pki.path(id3, 'csr')
863 peering.create_csr(key_file, '/CN={}'.format(id1), csr_file)
864 with self.assertRaises(peering.SubjectError) as cm:
865 pki.read_csr(id3)
866 self.assertEqual(cm.exception.filename, csr_file)
867 self.assertEqual(cm.exception.expected, '/CN={}'.format(id3))
868 self.assertEqual(cm.exception.got, '/CN={}'.format(id1))
869
870 def test_write_csr(self):
871 self.skipTest('FIXME')
872
324 def test_issue_cert(self):873 def test_issue_cert(self):
325 tmp = TempDir()874 tmp = TempDir()
326 pki = peering.PKI(tmp.dir)875 pki = peering.PKI(tmp.dir)
327876
=== added file 'run-browse.py'
--- run-browse.py 1970-01-01 00:00:00 +0000
+++ run-browse.py 2012-10-09 20:57:30 +0000
@@ -0,0 +1,188 @@
1#!/usr/bin/python3
2
3import logging
4import tempfile
5from gettext import gettext as _
6
7from gi.repository import GObject, Gtk, AppIndicator3
8from microfiber import CouchBase, _start_thread
9from queue import Queue
10
11from dmedia.startup import DmediaCouch
12from dmedia.gtk.ubuntu import NotifyManager
13from dmedia.peering import ChallengeResponse, ServerApp
14from dmedia.service.peers import AvahiPeer
15from dmedia.gtk.peering import BaseUI
16from dmedia.httpd import WSGIError, make_server
17
18
19format = [
20 '%(levelname)s',
21 '%(processName)s',
22 '%(threadName)s',
23 '%(message)s',
24]
25logging.basicConfig(level=logging.DEBUG, format='\t'.join(format))
26log = logging.getLogger()
27
28
29mainloop = GObject.MainLoop()
30
31
32
33class UI(BaseUI):
34 page = 'server.html'
35
36 signals = {
37 'get_secret': [],
38 'display_secret': ['secret'],
39 'set_message': ['message'],
40 }
41
42 def __init__(self, cr):
43 super().__init__()
44 self.cr = cr
45
46 def connect_hub_signals(self, hub):
47 hub.connect('get_secret', self.on_get_secret)
48
49 def on_get_secret(self, hub):
50 secret = self.cr.get_secret()
51 hub.send('display_secret', secret)
52
53
54class Session:
55 def __init__(self, pki, _id, peer, server_config, client_config):
56 self.pki = pki
57 self.peer_id = peer.id
58 self.peer = peer
59 self.cr = ChallengeResponse(_id, peer.id)
60 self.q = Queue()
61 _start_thread(self.monitor_response)
62 self.app = ServerApp(self.cr, self.q, pki)
63 self.app.state = 'info'
64 self.httpd = make_server(self.app, '0.0.0.0', server_config)
65 env = {'url': peer.url, 'ssl': client_config}
66 self.client = CouchBase(env)
67 self.httpd.start()
68 self.ui = UI(self.cr)
69
70 def monitor_response(self):
71 while True:
72 signal = self.q.get()
73 if signal == 'wrong_response':
74 GObject.idle_add(self.retry)
75 elif signal == 'response_ok':
76 GObject.timeout_add(500, self.on_response_ok)
77 break
78
79 def monitor_cert_request(self):
80 status = self.q.get()
81 if status != 'cert_issued':
82 log.error('Bad cert request from %r', self.peer)
83 log.warning('Possible malicious peer: %r', self.peer)
84 GObject.idle_add(self.on_cert_request, status)
85
86 def retry(self):
87 self.httpd.shutdown()
88 secret = self.cr.get_secret()
89 self.ui.hub.send('display_secret', secret)
90 self.ui.hub.send('set_message',
91 _('Typo? Please try again with new secret.')
92 )
93 self.app.state = 'ready'
94 self.httpd.start()
95
96 def on_response_ok(self):
97 assert self.app.state == 'response_ok'
98 self.ui.hub.send('set_message', _('Counter-Challenge...'))
99 _start_thread(self.counter_challenge)
100
101 def counter_challenge(self):
102 log.info('Getting counter-challenge from %r', self.peer)
103 challenge = self.client.get('challenge')['challenge']
104 (nonce, response) = self.cr.create_response(challenge)
105 obj = {'nonce': nonce, 'response': response}
106 log.info('Posting counter-response to %r', self.peer)
107 try:
108 r = self.client.put(obj, 'response')
109 log.info('Counter-response accepted')
110 GObject.idle_add(self.on_counter_response_ok)
111 except Unauthorized:
112 log.error('Counter-response rejected!')
113 log.warning('Possible malicious peer: %r', self.peer)
114 GObject.idle_add(self.on_counter_response_fail)
115
116 def on_counter_response_ok(self):
117 assert self.app.state == 'response_ok'
118 self.app.state = 'counter_response_ok'
119 _start_thread(self.monitor_cert_request)
120 self.ui.hub.send('set_message', _('Issuing Certificate...'))
121
122 def on_counter_response_fail(self):
123 self.ui.hub.send('set_message', _('Very Bad Things!'))
124
125 def on_cert_request(self, status):
126 print('on_cert_request', status)
127 self.ui.hub.send('set_message', _('Done!'))
128
129
130class Browse:
131 def __init__(self):
132 self.couch = DmediaCouch(tempfile.mkdtemp())
133 self.couch.firstrun_init(create_user=True)
134 self.couch.load_pki()
135 self.avahi = AvahiPeer(self.couch.pki)
136 self.avahi.connect('offer', self.on_offer)
137 self.avahi.connect('retract', self.on_retract)
138 self.avahi.browse()
139 self.notifymanager = NotifyManager()
140 self.indicator = None
141 self.session = None
142
143 def on_offer(self, avahi, info):
144 assert self.indicator is None
145 self.indicator = AppIndicator3.Indicator.new(
146 'dmedia-peer',
147 'indicator-novacut',
148 AppIndicator3.IndicatorCategory.APPLICATION_STATUS
149 )
150 menu = Gtk.Menu()
151 accept = Gtk.MenuItem()
152 accept.set_label(_('Accept {}@{}').format(info.name, info.host))
153 accept.connect('activate', self.on_accept, info)
154 menu.append(accept)
155 menu.show_all()
156 self.indicator.set_menu(menu)
157 self.indicator.set_status(AppIndicator3.IndicatorStatus.ATTENTION)
158 self.notifymanager.replace(
159 _('Novacut Peering Offer'),
160 '{}@{}'.format(info.name, info.host),
161 )
162
163 def on_retract(self, avahi):
164 if hasattr(self, 'indicator'):
165 del self.indicator
166 self.notifymanager.replace(_('Peering Offer Removed'))
167
168 def on_accept(self, menuitem, info):
169 assert self.session is None
170 self.avahi.activate(info.id)
171 self.indicator = None
172 self.session = Session(self.couch.pki, self.avahi.id, info,
173 self.avahi.get_server_config(),
174 self.avahi.get_client_config()
175 )
176 self.session.ui.window.connect('destroy', self.on_destroy)
177 self.session.ui.show()
178 self.avahi.publish(self.session.httpd.port)
179
180 def on_destroy(self, *args):
181 self.session.httpd.shutdown()
182 self.session.ui.window.destroy()
183 self.avahi.deactivate(self.session.peer_id)
184 self.session = None
185
186browse = Browse()
187mainloop.run()
188
0189
=== removed file 'run-browse.py'
--- run-browse.py 2012-09-20 12:27:37 +0000
+++ run-browse.py 1970-01-01 00:00:00 +0000
@@ -1,24 +0,0 @@
1#!/usr/bin/python3
2
3import logging
4
5import dbus
6from dbus.mainloop.glib import DBusGMainLoop
7from gi.repository import GObject
8from microfiber import random_id
9
10from dmedia.service.peers import Peer
11from dmedia.peering import TempPKI
12
13log = logging.getLogger()
14GObject.threads_init()
15DBusGMainLoop(set_as_default=True)
16logging.basicConfig(level=logging.DEBUG)
17
18pki = TempPKI()
19cert_id = pki.create(random_id())
20
21peer = Peer(cert_id)
22peer.browse('_dmedia-offer._tcp')
23mainloop = GObject.MainLoop()
24mainloop.run()
250
=== added file 'run-publish.py'
--- run-publish.py 1970-01-01 00:00:00 +0000
+++ run-publish.py 2012-10-09 20:57:30 +0000
@@ -0,0 +1,196 @@
1#!/usr/bin/python3
2
3import logging
4import tempfile
5from queue import Queue
6from gettext import gettext as _
7from base64 import b64encode, b64decode
8
9from gi.repository import GObject, Gtk
10from microfiber import dumps, CouchBase, Unauthorized, _start_thread
11
12from dmedia.startup import DmediaCouch
13from dmedia import peering
14from dmedia.service.peers import AvahiPeer
15from dmedia.gtk.peering import BaseUI
16from dmedia.peering import ChallengeResponse, ClientApp, InfoApp, encode, decode
17from dmedia.httpd import WSGIError, make_server, build_server_ssl_context
18
19
20format = [
21 '%(levelname)s',
22 '%(processName)s',
23 '%(threadName)s',
24 '%(message)s',
25]
26logging.basicConfig(level=logging.DEBUG, format='\t'.join(format))
27log = logging.getLogger()
28
29
30class Session:
31 def __init__(self, hub, pki, _id, peer, client_config):
32 self.hub = hub
33 self.pki = pki
34 self.peer = peer
35 self.id = _id
36 self.peer_id = peer.id
37 self.cr = ChallengeResponse(_id, peer.id)
38 self.q = Queue()
39 self.app = ClientApp(self.cr, self.q)
40 env = {'url': peer.url, 'ssl': client_config}
41 self.client = CouchBase(env)
42
43 def challenge(self):
44 log.info('Getting challenge from %r', self.peer)
45 challenge = self.client.get('challenge')['challenge']
46 (nonce, response) = self.cr.create_response(challenge)
47 obj = {'nonce': nonce, 'response': response}
48 log.info('Putting response to %r', self.peer)
49 try:
50 r = self.client.put(obj, 'response')
51 log.info('Response accepted')
52 success = True
53 except Unauthorized:
54 log.info('Response rejected')
55 success = False
56 GObject.idle_add(self.on_response, success)
57
58 def on_response(self, success):
59 if success:
60 self.app.state = 'ready'
61 _start_thread(self.monitor_counter_response)
62 else:
63 del self.cr.secret
64 self.hub.send('response', success)
65
66 def monitor_counter_response(self):
67 # FIXME: Should use a timeout with queue.get()
68 status = self.q.get()
69 log.info('Counter-response gave %r', status)
70 if status != 'response_ok':
71 log.error('Wrong counter-response!')
72 log.warning('Possible malicious peer: %r', self.peer)
73 GObject.timeout_add(500, self.on_counter_response, status)
74
75 def on_counter_response(self, status):
76 assert self.app.state == status
77 if status == 'response_ok':
78 _start_thread(self.request_cert)
79 self.hub.send('counter_response', status)
80
81 def request_cert(self):
82 log.info('Creating CSR')
83 try:
84 self.pki.create_csr(self.id)
85 csr_data = self.pki.read_csr(self.id)
86 obj = {'csr': b64encode(csr_data).decode('utf-8')}
87 r = self.client.post(obj, 'csr')
88 cert_data = b64decode(r['cert'].encode('utf-8'))
89 self.pki.write_cert2(self.id, self.peer_id, cert_data)
90 self.pki.verify_cert2(self.id, self.peer_id)
91 status = 'cert_issued'
92 except Exception as e:
93 status = 'error'
94 log.exception('Could not request cert')
95 GObject.idle_add(self.on_csr_response, status)
96
97 def on_csr_response(self, status):
98 log.info('on_csr_response %r', status)
99 self.hub.send('csr_response', status)
100
101
102class UI(BaseUI):
103 page = 'client.html'
104
105 signals = {
106 'first_time': [],
107 'already_using': [],
108 'have_secret': ['secret'],
109 'response': ['success'],
110 'counter_response': ['status'],
111 'csr_response': ['status'],
112 'set_message': ['message'],
113
114 'show_screen2a': [],
115 'show_screen2b': [],
116 'show_screen3b': [],
117 }
118
119 def __init__(self):
120 super().__init__()
121 self.couch = DmediaCouch(tempfile.mkdtemp())
122 self.couch.firstrun_init(create_user=False)
123 self.couch.load_pki()
124 self.avahi = None
125
126 def quit(self, *args):
127 if self.avahi:
128 self.avahi.unpublish()
129 Gtk.main_quit()
130
131 def connect_hub_signals(self, hub):
132 hub.connect('first_time', self.on_first_time)
133 hub.connect('already_using', self.on_already_using)
134 hub.connect('have_secret', self.on_have_secret)
135 hub.connect('response', self.on_response)
136 hub.connect('counter_response', self.on_counter_response)
137 hub.connect('csr_response', self.on_csr_response)
138
139 def on_first_time(self, hub):
140 hub.send('show_screen2a')
141
142 def on_already_using(self, hub):
143 if self.avahi is not None:
144 print('oop, duplicate click')
145 return
146 self.avahi = AvahiPeer(self.couch.pki, client_mode=True)
147 self.avahi.connect('accept', self.on_accept)
148 app = InfoApp(self.avahi.id)
149 self.httpd = make_server(app, '0.0.0.0',
150 self.avahi.get_server_config()
151 )
152 self.httpd.start()
153 self.avahi.browse()
154 self.avahi.publish(self.httpd.port)
155 GObject.idle_add(hub.send, 'show_screen2b')
156
157 def on_accept(self, avahi, peer):
158 self.avahi.activate(peer.id)
159 self.session = Session(self.hub, self.couch.pki, avahi.id, peer,
160 avahi.get_client_config()
161 )
162 # Reconfigure HTTPD to only accept connections from bound peer
163 self.httpd.reconfigure(self.session.app, avahi.get_server_config())
164 avahi.unpublish()
165 GObject.idle_add(self.hub.send, 'show_screen3b')
166
167 def on_have_secret(self, hub, secret):
168 if hasattr(self.session.cr, 'secret'):
169 log.warning("duplicate 'have_secret' signal received")
170 return
171 self.session.cr.set_secret(secret)
172 hub.send('set_message', _('Challenge...'))
173 _start_thread(self.session.challenge)
174
175 def on_response(self, hub, success):
176 if success:
177 hub.send('set_message', _('Counter-Challenge...'))
178 GObject.timeout_add(250, hub.send, 'spin_orb')
179 else:
180 hub.send('set_message', _('Typo? Please try again with new secret.'))
181
182 def on_counter_response(self, hub, status):
183 if status == 'response_ok':
184 hub.send('set_message', _('Requesting Certificate...'))
185 else:
186 hub.send('set_message', _('Very Bad Things!'))
187
188 def on_csr_response(self, hub, status):
189 if status == 'cert_issued':
190 hub.send('set_message', _('Done!'))
191 else:
192 hub.send('set_message', _('Very Bad Things with Certificate!'))
193
194
195ui = UI()
196ui.run()
0197
=== removed file 'run-publish.py'
--- run-publish.py 2012-09-20 12:27:37 +0000
+++ run-publish.py 1970-01-01 00:00:00 +0000
@@ -1,26 +0,0 @@
1#!/usr/bin/python3
2
3import logging
4
5import dbus
6from dbus.mainloop.glib import DBusGMainLoop
7from gi.repository import GObject
8from microfiber import random_id
9
10from dmedia.service.peers import Peer
11from dmedia.peering import TempPKI
12
13
14log = logging.getLogger()
15GObject.threads_init()
16DBusGMainLoop(set_as_default=True)
17logging.basicConfig(level=logging.DEBUG)
18
19pki = TempPKI()
20cert_id = pki.create(random_id())
21
22peer = Peer(cert_id)
23peer.browse('_dmedia-accept._tcp')
24peer.publish('_dmedia-offer._tcp', 5000)
25mainloop = GObject.MainLoop()
26mainloop.run()
270
=== modified file 'setup.py'
--- setup.py 2012-09-05 08:04:08 +0000
+++ setup.py 2012-10-09 20:57:30 +0000
@@ -159,6 +159,7 @@
159 ),159 ),
160 ('share/icons/hicolor/scalable/status',160 ('share/icons/hicolor/scalable/status',
161 [161 [
162 'share/indicator-novacut.svg',
162 'share/indicator-dmedia.svg',163 'share/indicator-dmedia.svg',
163 'share/indicator-dmedia-att.svg',164 'share/indicator-dmedia-att.svg',
164 ]165 ]
165166
=== added file 'share/indicator-novacut.svg'
--- share/indicator-novacut.svg 1970-01-01 00:00:00 +0000
+++ share/indicator-novacut.svg 2012-10-09 20:57:30 +0000
@@ -0,0 +1,133 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 xmlns:dc="http://purl.org/dc/elements/1.1/"
6 xmlns:cc="http://creativecommons.org/ns#"
7 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8 xmlns:svg="http://www.w3.org/2000/svg"
9 xmlns="http://www.w3.org/2000/svg"
10 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12 width="512"
13 height="512"
14 id="svg4778"
15 version="1.1"
16 inkscape:version="0.48.1 r9760"
17 sodipodi:docname="novacut-solo-brandmark_PINK_FINAL-SVG.svg"
18 inkscape:export-filename="/home/izo/Pictures/Client_Work/Novacut/Final-Artwork/web/PNG/novacut-solo-brandmark_PINK_FINAL-PNG-300dpi.png"
19 inkscape:export-xdpi="300.05859"
20 inkscape:export-ydpi="300.05859">
21 <defs
22 id="defs4780">
23 <inkscape:path-effect
24 effect="spiro"
25 id="path-effect5868"
26 is_visible="true" />
27 </defs>
28 <sodipodi:namedview
29 id="base"
30 pagecolor="#ffffff"
31 bordercolor="#666666"
32 borderopacity="1.0"
33 inkscape:pageopacity="0.0"
34 inkscape:pageshadow="2"
35 inkscape:zoom="1"
36 inkscape:cx="233.49618"
37 inkscape:cy="280"
38 inkscape:current-layer="layer1"
39 inkscape:document-units="px"
40 showgrid="false"
41 inkscape:window-width="1614"
42 inkscape:window-height="1026"
43 inkscape:window-x="66"
44 inkscape:window-y="24"
45 inkscape:window-maximized="1"
46 showguides="false"
47 inkscape:guide-bbox="true">
48 <inkscape:grid
49 type="xygrid"
50 id="grid2994"
51 empspacing="4"
52 visible="true"
53 enabled="true"
54 snapvisiblegridlinesonly="true" />
55 <sodipodi:guide
56 orientation="1,0"
57 position="256,88"
58 id="guide3002" />
59 <sodipodi:guide
60 orientation="0,1"
61 position="592,256"
62 id="guide3004" />
63 <sodipodi:guide
64 position="0,0"
65 orientation="0,512"
66 id="guide3006" />
67 <sodipodi:guide
68 position="512,0"
69 orientation="-512,0"
70 id="guide3008" />
71 <sodipodi:guide
72 position="512,512"
73 orientation="0,-512"
74 id="guide3010" />
75 <sodipodi:guide
76 position="0,512"
77 orientation="512,0"
78 id="guide3012" />
79 </sodipodi:namedview>
80 <metadata
81 id="metadata4783">
82 <rdf:RDF>
83 <cc:Work
84 rdf:about="">
85 <dc:format>image/svg+xml</dc:format>
86 <dc:type
87 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
88 <dc:title></dc:title>
89 </cc:Work>
90 </rdf:RDF>
91 </metadata>
92 <g
93 id="layer1"
94 inkscape:label="Layer 1"
95 inkscape:groupmode="layer"
96 transform="translate(0,32)">
97 <path
98 transform="matrix(1.0666667,0,0,1.0666667,-85.333334,-32.000001)"
99 d="m 545,240 a 225,225 0 1 1 -450,0 225,225 0 1 1 450,0 z"
100 sodipodi:ry="225"
101 sodipodi:rx="225"
102 sodipodi:cy="240"
103 sodipodi:cx="320"
104 id="path5924"
105 style="fill:#e81f3b;fill-opacity:1;stroke:none"
106 sodipodi:type="arc" />
107 <path
108 id="path4023"
109 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"
110 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"
111 inkscape:connector-curvature="0" />
112 <path
113 sodipodi:type="arc"
114 style="fill:#ffffff;fill-opacity:1;stroke:none"
115 id="path4025"
116 sodipodi:cx="836.50732"
117 sodipodi:cy="230.95239"
118 sodipodi:rx="13.435029"
119 sodipodi:ry="13.435029"
120 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"
121 transform="matrix(2.193362,0,0,2.193362,-1651.9421,-440.84345)" />
122 <path
123 transform="matrix(2.193362,0,0,2.193362,-1505.7305,-124.45147)"
124 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"
125 sodipodi:ry="13.435029"
126 sodipodi:rx="13.435029"
127 sodipodi:cy="230.95239"
128 sodipodi:cx="836.50732"
129 id="path4027"
130 style="fill:#ffffff;fill-opacity:1;stroke:none"
131 sodipodi:type="arc" />
132 </g>
133</svg>

Subscribers

People subscribed via source and target branches