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