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