Merge lp:~jderose/dmedia/all-in into lp:dmedia
- all-in
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 469 |
Proposed branch: | lp:~jderose/dmedia/all-in |
Merge into: | lp:dmedia |
Diff against target: |
5039 lines (+2468/-1581) 23 files modified
dmedia-cli (+2/-2) dmedia-gtk (+4/-3) dmedia-service (+111/-20) dmedia/core.py (+13/-26) dmedia/gtk/peering.py (+66/-9) dmedia/gtk/ui/client.html (+10/-12) dmedia/httpd.py (+1/-1) dmedia/parallel.py (+42/-0) dmedia/peering.py (+298/-439) dmedia/server.py (+301/-205) dmedia/service/__init__.py (+24/-5) dmedia/service/avahi.py (+74/-81) dmedia/service/peers.py (+324/-24) dmedia/service/tests/test_avahi.py (+54/-20) dmedia/startup.py (+45/-45) dmedia/tests/couch.py (+3/-1) dmedia/tests/test_core.py (+20/-59) dmedia/tests/test_peering.py (+419/-324) dmedia/tests/test_server.py (+559/-190) dmedia/tests/test_startup.py (+92/-111) run-browse.py (+2/-1) run-publish.py (+3/-3) setup.py (+1/-0) |
To merge this branch: | bzr merge lp:~jderose/dmedia/all-in |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jason Gerard DeRose | Approve | ||
Review via email: mp+129853@code.launchpad.net |
Commit message
Description of the change
This diff isn't as big as it looks because there was a lot of cosmetic changes in dmedia/peering.py, reordering things and so on so that it's easier to review this security sensitive code. So messy diff, but much nicer code.
Major changes include:
* dmedia-service wont start CouchDB when there isn't a user SSL identity
* dmedia-gtk uses the new init_if_needed() function that will walk the user through the first-run dialog if needed. The same will be added to novacut-gtk after this lands.
* After the first run init, dmedia-service will listen for _dmedia-offer._tcp as long as it has the private user key (without the private key, it can't issue new machine certificates anyway, so it would be pointless)
* Moved the peering related WSGI apps from dmedia/peering.py to dmedia/server.py to consolidate all the WSGI apps in one module
* Ported the file serving app to the new Dmedia HTTPD centric design, cleaned things up, added a lot of missing tests
* Ported the Avahi class to the brave new SSL world... it now does a test GET on the peer so it doesn't needlessly send the CouchDB replicator out to try and replicate to peers it can't authenticate to anyway
* POST /csr now includes a MAC proving the creator of the CSR knows the secret, and the response includes a similar MAC to prove the cert issuer knows the secret
* The response to POST /csr now includes the user private key. It's protected by the SSL channel, but I'd like to do something better here in case somehow this was reachable without going through SSL. I'd like to encrypt it to the machine CA, but I haven't figured out how to do that with openssl yet
* The PKI.verify_foo() methods are now, by my account, exhaustive in their checks. They check the subject and issuer for the correct intrinsic common names, and they do an SSL verification to check that CAs are correctly self-signed, and that certs are signed by the correct CA.
- 526. By Jason Gerard DeRose
-
Fixed sending Hub 'spin_orb' signal on ClientUI.
on_Response( )
Preview Diff
1 | === modified file 'dmedia-cli' |
2 | --- dmedia-cli 2012-08-16 10:38:13 +0000 |
3 | +++ dmedia-cli 2012-10-16 12:35:25 +0000 |
4 | @@ -121,8 +121,8 @@ |
5 | 'Get the _local/dmedia document (shows file-stores)' |
6 | |
7 | |
8 | -class LocalPeers(_Method): |
9 | - 'Get the _local/peers document' |
10 | +class Peers(_Method): |
11 | + 'Show local peers' |
12 | |
13 | |
14 | class CreateFileStore(_Method): |
15 | |
16 | === modified file 'dmedia-gtk' |
17 | --- dmedia-gtk 2012-08-16 12:48:30 +0000 |
18 | +++ dmedia-gtk 2012-10-16 12:35:25 +0000 |
19 | @@ -35,6 +35,7 @@ |
20 | from dmedia import views |
21 | from dmedia import schema |
22 | from dmedia.gtk.util import Timer |
23 | +from dmedia.service import init_if_needed |
24 | from dmedia.service.udisks import Ejector, Formatter |
25 | from dmedia.importer import ImportManager, has_magic_lantern |
26 | from gi.repository import GObject, Gtk, Gio |
27 | @@ -45,7 +46,8 @@ |
28 | UnityImportUX = None |
29 | |
30 | |
31 | -log = logging.getLogger() |
32 | +log = dmedia.configure_logging() |
33 | +init_if_needed() |
34 | |
35 | |
36 | class Importer: |
37 | @@ -261,8 +263,7 @@ |
38 | project.save(doc) |
39 | self.hub.send('project_created', doc['_id'], doc['title']) |
40 | |
41 | - |
42 | -dmedia.configure_logging() |
43 | + |
44 | app = App() |
45 | try: |
46 | app.run() |
47 | |
48 | === modified file 'dmedia-service' |
49 | --- dmedia-service 2012-10-06 07:56:57 +0000 |
50 | +++ dmedia-service 2012-10-16 12:35:25 +0000 |
51 | @@ -37,13 +37,14 @@ |
52 | from dbus.mainloop.glib import DBusGMainLoop |
53 | from gi.repository import GObject |
54 | from microfiber import NotFound, dumps |
55 | -from filestore import _start_thread |
56 | |
57 | import dmedia |
58 | +from dmedia.parallel import start_thread |
59 | from dmedia.startup import DmediaCouch |
60 | from dmedia.core import Core, start_httpd |
61 | from dmedia.service.udisks import UDisks |
62 | -from dmedia.service.avahi import FileServer |
63 | +from dmedia.service.avahi import Avahi |
64 | +from dmedia.service.peers import Browser, Publisher |
65 | |
66 | |
67 | BUS = dmedia.BUS |
68 | @@ -66,6 +67,11 @@ |
69 | couch = None |
70 | httpd = None |
71 | avahi = None |
72 | + peer = None |
73 | + ui = None |
74 | + thread = None |
75 | + publisher = None |
76 | + env_s = '{}' |
77 | |
78 | def __init__(self, bus, couch): |
79 | self.bus = bus |
80 | @@ -80,6 +86,13 @@ |
81 | self.udisks.connect('card_added', self.on_card_added) |
82 | self.udisks.connect('card_removed', self.on_card_removed) |
83 | |
84 | + def run(self): |
85 | + if self.couch.isfirstrun(): |
86 | + log.info('First run, not starting CouchDB.') |
87 | + else: |
88 | + self.start_core() |
89 | + mainloop.run() |
90 | + |
91 | def start_core(self): |
92 | start = time.time() |
93 | env = self.couch.auto_bootstrap() |
94 | @@ -91,36 +104,33 @@ |
95 | if self.core.local.get('default_store') is None: |
96 | self.core.set_default_store('shared') |
97 | self.env_s = dumps(self.core.env, pretty=True) |
98 | - self.ssl_config = self.couch.get_ssl_config() |
99 | + log.info('Finished core startup in %.3f', time.time() - start) |
100 | + GObject.timeout_add(1000, self.on_idle1) |
101 | |
102 | def start_httpd(self): |
103 | - if self.ssl_config is None: |
104 | - return |
105 | - (self.httpd, env) = start_httpd(self.core.env, self.ssl_config) |
106 | - self.avahi = FileServer(self.core.env, env['port']) |
107 | + ssl_config = self.couch.get_ssl_config() |
108 | + (self.httpd, env) = start_httpd(self.core.env, ssl_config) |
109 | + port = env['port'] |
110 | + self.avahi = Avahi(self.core.env, port, ssl_config) |
111 | self.avahi.run() |
112 | - |
113 | - def run(self): |
114 | - self.start_core() |
115 | - log.info('Finished core startup in %.3f', time.time() - start_time) |
116 | - GObject.timeout_add(2000, self.on_idle1) |
117 | - mainloop.run() |
118 | + if self.couch.pki.user.key_file is not None: |
119 | + self.peer = Browser(self.couch) |
120 | |
121 | def on_idle1(self): |
122 | log.info('[idle1]') |
123 | self.udisks.monitor() |
124 | - GObject.timeout_add(4000, self.on_idle2) |
125 | + GObject.timeout_add(1000, self.on_idle2) |
126 | return False # Don't repeat idle call |
127 | |
128 | def on_idle2(self): |
129 | log.info('[idle2]') |
130 | self.start_httpd() |
131 | - GObject.timeout_add(6000, self.on_idle3) |
132 | + GObject.timeout_add(1000, self.on_idle3) |
133 | return False # Don't repeat idle call |
134 | |
135 | def on_idle3(self): |
136 | log.info('[idle3]') |
137 | - _start_thread(self.core.init_project_views) |
138 | + start_thread(self.core.init_project_views) |
139 | self.core.start_background_tasks() |
140 | return False # Don't repeat idle call |
141 | |
142 | @@ -130,6 +140,10 @@ |
143 | |
144 | def shutdown(self): |
145 | log.info('Service.shutdown()') |
146 | + if self.peer is not None: |
147 | + log.info('* freeing peer') |
148 | + self.peer.free() |
149 | + self.peer = None |
150 | if self.avahi is not None: |
151 | log.info('* freeing avahi') |
152 | self.avahi.free() |
153 | @@ -170,6 +184,22 @@ |
154 | def CardRemoved(self, obj, mount): |
155 | pass |
156 | |
157 | + @dbus.service.signal(IFACE, signature='s') |
158 | + def Message(self, message): |
159 | + log.info('@Dmedia.Message(%r)', message) |
160 | + |
161 | + @dbus.service.signal(IFACE, signature='') |
162 | + def Accept(self): |
163 | + log.info('@Dmedia.Accept()') |
164 | + |
165 | + @dbus.service.signal(IFACE, signature='b') |
166 | + def Response(self, success): |
167 | + log.info('@Dmedia.Response(%r)', success) |
168 | + |
169 | + @dbus.service.signal(IFACE, signature='') |
170 | + def InitDone(self): |
171 | + log.info('@Dmedia.InitDone()') |
172 | + |
173 | @dbus.service.method(IFACE, in_signature='', out_signature='s') |
174 | def Version(self): |
175 | """ |
176 | @@ -177,6 +207,66 @@ |
177 | """ |
178 | return dmedia.__version__ |
179 | |
180 | + @dbus.service.method(IFACE, in_signature='', out_signature='b') |
181 | + def NeedsInit(self): |
182 | + """ |
183 | + Return True if we need to do the firstrun init. |
184 | + """ |
185 | + return self.couch.user is None |
186 | + |
187 | + @dbus.service.method(IFACE, in_signature='', out_signature='b') |
188 | + def CreateUser(self): |
189 | + log.info('Dmedia.CreateUser()') |
190 | + if self.couch.user is not None: |
191 | + return False |
192 | + if self.thread is not None: |
193 | + return False |
194 | + self.Message('Creating user certificate...') |
195 | + self.thread = start_thread(self.create_user) |
196 | + return True |
197 | + |
198 | + def create_user(self): |
199 | + log.info('create_user()') |
200 | + self.couch.create_user() |
201 | + GObject.idle_add(self.Message, 'Starting CouchDB...') |
202 | + self.start_core() |
203 | + GObject.idle_add(self.on_init_done) |
204 | + |
205 | + def on_init_done(self): |
206 | + log.info('on_init_done()') |
207 | + self.thread.join() |
208 | + self.thread = None |
209 | + self.Message('Done!') |
210 | + GObject.timeout_add(500, self.InitDone) |
211 | + |
212 | + @dbus.service.method(IFACE, in_signature='', out_signature='b') |
213 | + def PeerWithExisting(self): |
214 | + log.info('Dmedia.PeerWithExisting()') |
215 | + if self.couch.user is not None: |
216 | + return False |
217 | + if self.publisher is not None: |
218 | + return False |
219 | + self.publisher = Publisher(self, self.couch) |
220 | + GObject.idle_add(self.publisher.run) |
221 | + return True |
222 | + |
223 | + @dbus.service.method(IFACE, in_signature='s', out_signature='b') |
224 | + def SetSecret(self, secret): |
225 | + log.info('Dmedia.SetSecret()') |
226 | + return self.publisher.set_secret(secret) |
227 | + |
228 | + def set_user(self, user_id): |
229 | + log.info('set_user(%r)', user_id) |
230 | + assert self.couch.user is None |
231 | + assert self.thread is None |
232 | + self.Message('Starting CouchDB...') |
233 | + self.thread = start_thread(self._set_user, user_id) |
234 | + |
235 | + def _set_user(self, user_id): |
236 | + self.couch.set_user(user_id) |
237 | + self.start_core() |
238 | + GObject.idle_add(self.on_init_done) |
239 | + |
240 | @dbus.service.method(IFACE, in_signature='', out_signature='i') |
241 | def Kill(self): |
242 | """ |
243 | @@ -209,12 +299,13 @@ |
244 | return dumps(self.core.db.get('_local/dmedia')) |
245 | |
246 | @dbus.service.method(IFACE, in_signature='', out_signature='s') |
247 | - def LocalPeers(self): |
248 | + def Peers(self): |
249 | """ |
250 | Return the _local/peers doc. |
251 | """ |
252 | try: |
253 | - return dumps(self.core.db.get('_local/peers')) |
254 | + doc = self.core.db.get('_local/peers') |
255 | + return dumps(doc['peers'], pretty=True) |
256 | except NotFound: |
257 | return '{}' |
258 | |
259 | @@ -273,8 +364,8 @@ |
260 | # Now go for it: |
261 | dmedia.configure_logging() |
262 | try: |
263 | - if couch.isfirstrun(): |
264 | - couch.firstrun_init(create_user=True) |
265 | + if couch.machine is None: |
266 | + couch.create_machine() |
267 | service = Service(options.bus, couch) |
268 | try: |
269 | service.run() |
270 | |
271 | === modified file 'dmedia/core.py' |
272 | --- dmedia/core.py 2012-10-13 12:48:38 +0000 |
273 | +++ dmedia/core.py 2012-10-16 12:35:25 +0000 |
274 | @@ -41,9 +41,10 @@ |
275 | from copy import deepcopy |
276 | |
277 | from microfiber import Server, Database, NotFound, Conflict, BulkConflict |
278 | -from filestore import FileStore, check_root_hash, check_id, _start_thread |
279 | +from filestore import FileStore, check_root_hash, check_id |
280 | |
281 | import dmedia |
282 | +from dmedia.parallel import start_thread, start_process |
283 | from dmedia.server import run_server |
284 | from dmedia import util, schema |
285 | from dmedia.metastore import MetaStore |
286 | @@ -56,13 +57,6 @@ |
287 | PRIVATE = path.abspath(os.environ['HOME']) |
288 | |
289 | |
290 | -def start_process(target, *args): |
291 | - process = multiprocessing.Process(target=target, args=args) |
292 | - process.daemon = True |
293 | - process.start() |
294 | - return process |
295 | - |
296 | - |
297 | def start_httpd(couch_env, ssl_config): |
298 | queue = multiprocessing.Queue() |
299 | process = start_process(run_server, queue, couch_env, '0.0.0.0', ssl_config) |
300 | @@ -138,22 +132,17 @@ |
301 | self.db.save(self.local) |
302 | self.__local = deepcopy(self.local) |
303 | |
304 | - def load_identity(self, machine, user=None): |
305 | + def load_identity(self, machine, user): |
306 | try: |
307 | - self.db.save(machine) |
308 | - except Conflict: |
309 | + self.db.save_many([machine, user]) |
310 | + except BulkConflict: |
311 | pass |
312 | + log.info('machine_id = %s', machine['_id']) |
313 | + log.info('user_id = %s', user['_id']) |
314 | + self.env['machine_id'] = machine['_id'] |
315 | + self.env['user_id'] = user['_id'] |
316 | self.local['machine_id'] = machine['_id'] |
317 | - self.env['machine_id'] = machine['_id'] |
318 | - log.info('machine_id = %s', machine['_id']) |
319 | - if user is not None: |
320 | - try: |
321 | - self.db.save(user) |
322 | - except Conflict: |
323 | - pass |
324 | - self.local['user_id'] = user['_id'] |
325 | - self.env['user_id'] = user['_id'] |
326 | - log.info('user_id = %s', user['_id']) |
327 | + self.local['user_id'] = user['_id'] |
328 | self.save_local() |
329 | |
330 | def init_default_store(self): |
331 | @@ -175,10 +164,8 @@ |
332 | self._add_filestore(fs, doc) |
333 | |
334 | def _sync_stores(self): |
335 | - stores = self.stores.local_stores() |
336 | - if self.local.get('stores') != stores: |
337 | - self.local['stores'] = stores |
338 | - self.db.save(self.local) |
339 | + self.local['stores'] = self.stores.local_stores() |
340 | + self.save_local() |
341 | |
342 | def _add_filestore(self, fs, doc): |
343 | self.stores.add(fs) |
344 | @@ -211,7 +198,7 @@ |
345 | |
346 | def start_background_tasks(self): |
347 | assert self.thread is None |
348 | - self.thread = _start_thread(self._background_worker) |
349 | + self.thread = start_thread(self._background_worker) |
350 | |
351 | def init_project_views(self): |
352 | try: |
353 | |
354 | === modified file 'dmedia/gtk/peering.py' |
355 | --- dmedia/gtk/peering.py 2012-10-09 01:48:26 +0000 |
356 | +++ dmedia/gtk/peering.py 2012-10-16 12:35:25 +0000 |
357 | @@ -1,9 +1,11 @@ |
358 | from os import path |
359 | import json |
360 | +import logging |
361 | |
362 | from gi.repository import GObject, Gtk, WebKit |
363 | |
364 | |
365 | +log = logging.getLogger() |
366 | ui = path.join(path.dirname(path.abspath(__file__)), 'ui') |
367 | assert path.isdir(ui) |
368 | |
369 | @@ -61,20 +63,13 @@ |
370 | self.hub = hub_factory(self.signals)(self.view) |
371 | self.connect_hub_signals(self.hub) |
372 | |
373 | - def show(self): |
374 | - self.window.show_all() |
375 | + def connect_hub_signals(self, hub): |
376 | + pass |
377 | |
378 | def run(self): |
379 | - self.window.connect('destroy', self.quit) |
380 | self.window.show_all() |
381 | Gtk.main() |
382 | |
383 | - def quit(self, *args): |
384 | - Gtk.main_quit() |
385 | - |
386 | - def connect_hub_signals(self, hub): |
387 | - pass |
388 | - |
389 | def build_window(self): |
390 | self.window = Gtk.Window() |
391 | self.window.set_position(Gtk.WindowPosition.CENTER) |
392 | @@ -98,3 +93,65 @@ |
393 | self.inspector.show_all() |
394 | return self.inspector |
395 | |
396 | + |
397 | +class ClientUI(BaseUI): |
398 | + page = 'client.html' |
399 | + title = 'Welcome to Novacut!' |
400 | + |
401 | + signals = { |
402 | + 'create_user': [], |
403 | + 'peer_with_existing': [], |
404 | + 'have_secret': ['secret'], |
405 | + |
406 | + 'response': ['success'], |
407 | + 'message': ['message'], |
408 | + |
409 | + 'show_screen2a': [], |
410 | + 'show_screen2b': [], |
411 | + 'show_screen3b': [], |
412 | + 'spin_orb': [], |
413 | + } |
414 | + |
415 | + def __init__(self, Dmedia): |
416 | + super().__init__() |
417 | + self.Dmedia = Dmedia |
418 | + self.quit = False |
419 | + Dmedia.connect_to_signal('Message', self.on_Message) |
420 | + Dmedia.connect_to_signal('Accept', self.on_Accept) |
421 | + Dmedia.connect_to_signal('Response', self.on_Response) |
422 | + Dmedia.connect_to_signal('InitDone', self.on_InitDone) |
423 | + self.window.connect('destroy', Gtk.main_quit) |
424 | + self.window.connect('delete-event', self.on_delete_event) |
425 | + |
426 | + def on_delete_event(self, *args): |
427 | + self.quit = True |
428 | + |
429 | + def connect_hub_signals(self, hub): |
430 | + hub.connect('create_user', self.on_create_user) |
431 | + hub.connect('peer_with_existing', self.on_peer_with_existing) |
432 | + hub.connect('have_secret', self.on_have_secret) |
433 | + |
434 | + def on_Message(self, message): |
435 | + self.hub.send('message', message) |
436 | + |
437 | + def on_Accept(self): |
438 | + self.hub.send('show_screen3b') |
439 | + |
440 | + def on_Response(self, success): |
441 | + self.hub.send('response', success) |
442 | + if success: |
443 | + GObject.timeout_add(200, self.hub.send, 'spin_orb') |
444 | + |
445 | + def on_InitDone(self): |
446 | + self.window.destroy() |
447 | + |
448 | + def on_create_user(self, hub): |
449 | + self.Dmedia.CreateUser() |
450 | + hub.send('show_screen2a') |
451 | + |
452 | + def on_peer_with_existing(self, hub): |
453 | + self.Dmedia.PeerWithExisting() |
454 | + hub.send('show_screen2b') |
455 | + |
456 | + def on_have_secret(self, hub, secret): |
457 | + self.Dmedia.SetSecret(secret) |
458 | |
459 | === modified file 'dmedia/gtk/ui/client.html' |
460 | --- dmedia/gtk/ui/client.html 2012-10-09 05:37:35 +0000 |
461 | +++ dmedia/gtk/ui/client.html 2012-10-16 12:35:25 +0000 |
462 | @@ -89,7 +89,7 @@ |
463 | } |
464 | ); |
465 | |
466 | -Hub.connect('set_message', |
467 | +Hub.connect('message', |
468 | function(message) { |
469 | $('message').textContent = message; |
470 | } |
471 | @@ -97,16 +97,16 @@ |
472 | |
473 | Hub.connect('response', |
474 | function(success) { |
475 | - if (!success) { |
476 | + if (success) { |
477 | + $hide('finish'); |
478 | + $show('logo2'); |
479 | + } |
480 | + else { |
481 | UI.input.value = ''; |
482 | UI.input.disabled = false; |
483 | UI.input.focus(); |
484 | $('finish').classList.add('hidden'); |
485 | } |
486 | - else { |
487 | - $hide('finish'); |
488 | - $show('logo2'); |
489 | - } |
490 | } |
491 | ); |
492 | |
493 | @@ -117,7 +117,7 @@ |
494 | <img src="novacut.svg" id="logo"> |
495 | |
496 | <div id="screen1" class="hide"> |
497 | - <div id="first" onclick="Hub.send('first_time')"> |
498 | + <div id="first" onclick="Hub.send('create_user')"> |
499 | <p class="top"> |
500 | This is my first time using Novacut |
501 | </p> |
502 | @@ -126,7 +126,7 @@ |
503 | </p> |
504 | </div> |
505 | |
506 | - <div id="sync" onclick="Hub.send('already_using')"> |
507 | + <div id="sync" onclick="Hub.send('peer_with_existing')"> |
508 | <p class="top"> |
509 | I'm already using Novacut |
510 | </p> |
511 | @@ -137,9 +137,6 @@ |
512 | </div> |
513 | |
514 | <div id="screen2a" class="hide"> |
515 | - <p class="gen"> |
516 | - Pretty words here |
517 | - </p> |
518 | </div> |
519 | |
520 | <div id="screen2b" class="hide"> |
521 | @@ -157,10 +154,11 @@ |
522 | <input id="input" type="text" maxlength="8" size="8" autofocus="1"></input> |
523 | </form> |
524 | </div> |
525 | - <p id="message"></p> |
526 | <img src="sync.svg" id="finish" class="hidden" onclick="UI.have_secret()"> |
527 | <img src="novacut.svg" id="logo2" class="hide"> |
528 | </div> |
529 | |
530 | +<p id="message"></p> |
531 | + |
532 | </body> |
533 | </html> |
534 | |
535 | === modified file 'dmedia/httpd.py' |
536 | --- dmedia/httpd.py 2012-10-08 18:41:48 +0000 |
537 | +++ dmedia/httpd.py 2012-10-16 12:35:25 +0000 |
538 | @@ -125,7 +125,7 @@ |
539 | def build_server_ssl_context(config): |
540 | ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) |
541 | ctx.options |= ssl.OP_NO_COMPRESSION # Protect against CRIME-like attacks |
542 | - ctx.set_ciphers('RC4') |
543 | + # ctx.set_ciphers('RC4') # Example of how to change ciphers |
544 | ctx.load_cert_chain(config['cert_file'], config['key_file']) |
545 | if 'ca_file' in config or 'ca_path' in config: |
546 | ctx.verify_mode = ssl.CERT_REQUIRED |
547 | |
548 | === added file 'dmedia/parallel.py' |
549 | --- dmedia/parallel.py 1970-01-01 00:00:00 +0000 |
550 | +++ dmedia/parallel.py 2012-10-16 12:35:25 +0000 |
551 | @@ -0,0 +1,42 @@ |
552 | +# dmedia: distributed media library |
553 | +# Copyright (C) 2012 Novacut Inc |
554 | +# |
555 | +# This file is part of `dmedia`. |
556 | +# |
557 | +# `dmedia` is free software: you can redistribute it and/or modify it under |
558 | +# the terms of the GNU Affero General Public License as published by the Free |
559 | +# Software Foundation, either version 3 of the License, or (at your option) any |
560 | +# later version. |
561 | +# |
562 | +# `dmedia` is distributed in the hope that it will be useful, but WITHOUT ANY |
563 | +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR |
564 | +# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
565 | +# details. |
566 | +# |
567 | +# You should have received a copy of the GNU Affero General Public License along |
568 | +# with `dmedia`. If not, see <http://www.gnu.org/licenses/>. |
569 | +# |
570 | +# Authors: |
571 | +# Jason Gerard DeRose <jderose@novacut.com> |
572 | + |
573 | +""" |
574 | +Small helpers for starting threads and processes. |
575 | +""" |
576 | + |
577 | +import threading |
578 | +import multiprocessing |
579 | + |
580 | + |
581 | +def start_thread(target, *args, **kw): |
582 | + thread = threading.Thread(target=target, args=args, kwargs=kw) |
583 | + thread.daemon = True |
584 | + thread.start() |
585 | + return thread |
586 | + |
587 | + |
588 | +def start_process(target, *args, **kw): |
589 | + process = multiprocessing.Process(target=target, args=args, kwargs=kw) |
590 | + process.daemon = True |
591 | + process.start() |
592 | + return process |
593 | + |
594 | |
595 | === modified file 'dmedia/peering.py' |
596 | --- dmedia/peering.py 2012-10-09 03:49:01 +0000 |
597 | +++ dmedia/peering.py 2012-10-16 12:35:25 +0000 |
598 | @@ -20,94 +20,10 @@ |
599 | # Jason Gerard DeRose <jderose@novacut.com> |
600 | |
601 | """ |
602 | -Securely peer one device with another device on the same local network. |
603 | - |
604 | -We want it to be easy for a user to associate multiple devices with the same |
605 | -user CA. This is the local-network "user account" that works without any cloud |
606 | -infrastructure, without a Novacut account or any other 3rd-party account. |
607 | - |
608 | -To do the peering, the user needs both devices side-by-side. One device is |
609 | -already associated with the user, and the other is the one to be associated. |
610 | - |
611 | -The existing device generates a random secret and displays it on the screen. |
612 | -The user then enter the secret on the second device, and each device does a |
613 | -challenge-response to make the other prove it has the same secret. |
614 | - |
615 | -In a nutshell: |
616 | - |
617 | - 1. A generates random ``nonce1`` to challenge B |
618 | - 2. B must respond with ``hash(secret + nonce1)`` |
619 | - 3. B generates random ``nonce2`` to challenge A |
620 | - 4. A must respond with ``hash(secret + nonce2)`` |
621 | - |
622 | -We only give the responder one try, and if it's wrong, the process starts over |
623 | -with a new secret. If the user makes a typo, we don't let them try again to |
624 | -correctly type the initial secret... they must try and correctly type a new |
625 | -secret. |
626 | - |
627 | -Limiting the responder to only one attempt lets us to use a fairly low-entropy |
628 | -secret, something easy for the user to type. And importantly, the secret |
629 | -is not susceptible to any "offline" attack, because there isn't an intrinsicly |
630 | -correct answer. |
631 | - |
632 | -For example, this would not be the case if we used the secret to encrypt a |
633 | -message and send it from one to another. An attacker could run through the |
634 | -keyspace till (say) gpg stopped telling them the passphrase is wrong. |
635 | - |
636 | - |
637 | - |
638 | -Request 1: |
639 | - |
640 | - GET /challenge |
641 | - |
642 | -Response 1: |
643 | - |
644 | - {"challenge": ""} |
645 | - |
646 | - |
647 | -Request 2: |
648 | - |
649 | - POST /response |
650 | - |
651 | - {"nonce": "", "response": "", "counter_challenge": ""} |
652 | - |
653 | -Response 2: |
654 | - |
655 | - {"nonce": "", "counter_response": ""} |
656 | - |
657 | - |
658 | - |
659 | -1. A starts server, publishes _dmedia-offer._tcp under cert_a_id |
660 | - |
661 | -2. B downloads cert_a from A, if hash doesn't match cert_a_id, ABORT |
662 | - |
663 | -3. B prompts user about offer, if user declines, ABORT |
664 | - |
665 | -4. B starts server, publishes _dmedia-accept._tcp under cert_b_id |
666 | - |
667 | -5. A downloads cert_b from B, if hash doesn't match cert_b_id, ABORT |
668 | - |
669 | -6. A generates secret, displays to user, waits for request from B |
670 | - |
671 | -7. User enters secret on B |
672 | - |
673 | -8. B does GET /challenge to get challenge from A |
674 | - |
675 | -9. B does POST /response to post response and counter-challenge to A |
676 | - |
677 | -10. If response is wrong, A assumes user typo, RESTART at (6) with new secret |
678 | - |
679 | -11. A returns counter-response to B |
680 | - |
681 | -12. If counter-response is wrong, B ABORTS with scary warning |
682 | - |
683 | -13. DONE! |
684 | - |
685 | - |
686 | - |
687 | +Secure peering protocol, SSL-based machine and user identity. |
688 | """ |
689 | |
690 | -import base64 |
691 | +from base64 import b32encode, b32decode |
692 | import os |
693 | from os import path |
694 | import stat |
695 | @@ -115,30 +31,31 @@ |
696 | import shutil |
697 | from collections import namedtuple |
698 | from subprocess import check_call, check_output |
699 | -import json |
700 | -import socket |
701 | import logging |
702 | |
703 | from skein import skein512 |
704 | -from microfiber import random_id, dumps |
705 | - |
706 | -from dmedia.httpd import WSGIError |
707 | - |
708 | - |
709 | +from microfiber import random_id |
710 | + |
711 | + |
712 | +# Skein personalization strings |
713 | +PERS_PUBKEY = b'20120918 jderose@novacut.com dmedia/pubkey' |
714 | +PERS_RESPONSE = b'20120918 jderose@novacut.com dmedia/response' |
715 | +PERS_CSR = b'20120918 jderose@novacut.com dmedia/csr' |
716 | +PERS_CERT = b'20120918 jderose@novacut.com dmedia/cert' |
717 | + |
718 | +MAC_BITS = 280 |
719 | DAYS = 365 * 10 |
720 | CA = namedtuple('CA', 'id ca_file') |
721 | Cert = namedtuple('Cert', 'id cert_file key_file') |
722 | - |
723 | -# Skein personalization strings |
724 | -PERS_PUBKEY = b'20120918 jderose@novacut.com dmedia/pubkey' |
725 | -PERS_RESPONSE = b'20120918 jderose@novacut.com dmedia/response' |
726 | - |
727 | -USER = os.environ.get('USER') |
728 | -HOST = socket.gethostname() |
729 | +Machine = namedtuple('Machine', 'id ca_file key_file cert_file') |
730 | +User = namedtuple('User', 'id ca_file key_file') |
731 | |
732 | log = logging.getLogger() |
733 | |
734 | |
735 | +################### |
736 | +# Custom exceptions |
737 | + |
738 | class IdentityError(Exception): |
739 | def __init__(self, filename, expected, got): |
740 | self.filename = filename |
741 | @@ -152,18 +69,258 @@ |
742 | class PublicKeyError(IdentityError): |
743 | pass |
744 | |
745 | - |
746 | class SubjectError(IdentityError): |
747 | pass |
748 | |
749 | - |
750 | class IssuerError(IdentityError): |
751 | pass |
752 | |
753 | - |
754 | class VerificationError(IdentityError): |
755 | pass |
756 | - |
757 | + |
758 | + |
759 | +class WrongResponse(Exception): |
760 | + def __init__(self, expected, got): |
761 | + self.expected = expected |
762 | + self.got = got |
763 | + super().__init__('Incorrect response') |
764 | + |
765 | + |
766 | +class WrongMAC(Exception): |
767 | + def __init__(self, expected, got): |
768 | + self.expected = expected |
769 | + self.got = got |
770 | + super().__init__('Incorrect MAC') |
771 | + |
772 | + |
773 | + |
774 | +########################################### |
775 | +# Helper functions for base32 encode/decode |
776 | + |
777 | +def encode(value): |
778 | + """ |
779 | + Base32-encode the bytes *value*. |
780 | + |
781 | + For example: |
782 | + |
783 | + >>> encode(b'skein') |
784 | + 'ONVWK2LO' |
785 | + |
786 | + """ |
787 | + assert isinstance(value, bytes) |
788 | + assert len(value) > 0 and len(value) % 5 == 0 |
789 | + return b32encode(value).decode('utf-8') |
790 | + |
791 | + |
792 | +def decode(value): |
793 | + """ |
794 | + Base32-decode the str *value*. |
795 | + |
796 | + For example: |
797 | + |
798 | + >>> decode('ONVWK2LO') |
799 | + b'skein' |
800 | + |
801 | + """ |
802 | + assert isinstance(value, str) |
803 | + assert len(value) > 0 and len(value) % 8 == 0 |
804 | + return b32decode(value.encode('utf-8')) |
805 | + |
806 | + |
807 | + |
808 | +########################################################### |
809 | +# Skein-based hashing functions and ChallengeResponse class |
810 | + |
811 | +def hash_pubkey(pubkey_data): |
812 | + """ |
813 | + Hash an RSA public key to compute the Dmedia identity ID. |
814 | + |
815 | + For example: |
816 | + |
817 | + >>> hash_pubkey(b'The PEM encoded public key') |
818 | + 'JTHPOXBZKRMAUGDKXDR74ZRVVSKYUMTHZNQUOSUNTQ2IO4UD' |
819 | + |
820 | + """ |
821 | + skein = skein512(pubkey_data, |
822 | + digest_bits=240, |
823 | + pers=PERS_PUBKEY, |
824 | + ) |
825 | + return encode(skein.digest()) |
826 | + |
827 | + |
828 | +def compute_response(secret, challenge, nonce, challenger_hash, responder_hash): |
829 | + """ |
830 | + Compute response hash used in challenge-response protocol. |
831 | + |
832 | + :param secret: the shared secret |
833 | + :param challenge: a nonce generated by the challenger |
834 | + :param none: a nonce generated by the responder |
835 | + :param challenger_hash: hash of the challenger's public key |
836 | + :param responder_hash: hash of the responder's public key |
837 | + """ |
838 | + assert len(secret) == 5 |
839 | + assert len(challenge) == 20 |
840 | + assert len(nonce) == 20 |
841 | + assert len(challenger_hash) == 30 |
842 | + assert len(responder_hash) == 30 |
843 | + skein = skein512( |
844 | + digest_bits=MAC_BITS, |
845 | + pers=PERS_RESPONSE, |
846 | + key=secret, |
847 | + nonce=(challenge + nonce), |
848 | + ) |
849 | + skein.update(challenger_hash) |
850 | + skein.update(responder_hash) |
851 | + return encode(skein.digest()) |
852 | + |
853 | + |
854 | +def compute_csr_mac(secret, csr_data, remote_hash, local_hash): |
855 | + """ |
856 | + Compute MAC to prove machine that created CSR has the secret. |
857 | + |
858 | + :param secret: the shared secret |
859 | + :param csr_data: PEM encoded certificate signing request |
860 | + :param remote_hash: hash of remote peer's public key |
861 | + :param local_hash: hash of local peer's public key |
862 | + """ |
863 | + assert len(secret) == 5 |
864 | + assert len(remote_hash) == 30 |
865 | + assert len(local_hash) == 30 |
866 | + skein = skein512(csr_data, |
867 | + digest_bits=MAC_BITS, |
868 | + pers=PERS_CSR, |
869 | + key=secret, |
870 | + key_id=(remote_hash + local_hash), |
871 | + ) |
872 | + return encode(skein.digest()) |
873 | + |
874 | + |
875 | +def compute_cert_mac(secret, cert_data, remote_hash, local_hash): |
876 | + """ |
877 | + Compute MAC to prove machine that issued certificate has the secret. |
878 | + |
879 | + :param secret: the shared secret |
880 | + :param cert_data: PEM encoded certificate |
881 | + :param remote_hash: hash of remote peer's public key |
882 | + :param local_hash: hash of local peer's public key |
883 | + """ |
884 | + assert len(secret) == 5 |
885 | + assert len(remote_hash) == 30 |
886 | + assert len(local_hash) == 30 |
887 | + skein = skein512(cert_data, |
888 | + digest_bits=MAC_BITS, |
889 | + pers=PERS_CERT, |
890 | + key=secret, |
891 | + key_id=(remote_hash + local_hash), |
892 | + ) |
893 | + return encode(skein.digest()) |
894 | + |
895 | + |
896 | +class ChallengeResponse: |
897 | + """ |
898 | + Helper class to hold state for challenge-response protocol. |
899 | + """ |
900 | + |
901 | + def __init__(self, _id, peer_id): |
902 | + self.id = _id |
903 | + self.peer_id = peer_id |
904 | + self.local_hash = decode(_id) |
905 | + self.remote_hash = decode(peer_id) |
906 | + assert len(self.local_hash) == 30 |
907 | + assert len(self.remote_hash) == 30 |
908 | + |
909 | + def get_secret(self): |
910 | + # 40-bit secret (8 characters when base32 encoded) |
911 | + self.secret = os.urandom(5) |
912 | + return encode(self.secret) |
913 | + |
914 | + def set_secret(self, secret): |
915 | + assert len(secret) == 8 |
916 | + self.secret = decode(secret) |
917 | + assert len(self.secret) == 5 |
918 | + |
919 | + def get_challenge(self): |
920 | + self.challenge = os.urandom(20) |
921 | + return encode(self.challenge) |
922 | + |
923 | + def create_response(self, challenge): |
924 | + nonce = os.urandom(20) |
925 | + response = compute_response( |
926 | + self.secret, |
927 | + decode(challenge), |
928 | + nonce, |
929 | + self.remote_hash, |
930 | + self.local_hash |
931 | + ) |
932 | + return (encode(nonce), response) |
933 | + |
934 | + def check_response(self, nonce, response): |
935 | + expected = compute_response( |
936 | + self.secret, |
937 | + self.challenge, |
938 | + decode(nonce), |
939 | + self.local_hash, |
940 | + self.remote_hash |
941 | + ) |
942 | + if response != expected: |
943 | + del self.secret |
944 | + del self.challenge |
945 | + raise WrongResponse(expected, response) |
946 | + |
947 | + def csr_mac(self, csr_data): |
948 | + return compute_csr_mac( |
949 | + self.secret, |
950 | + csr_data, |
951 | + self.remote_hash, |
952 | + self.local_hash, |
953 | + ) |
954 | + |
955 | + def check_csr_mac(self, csr_data, mac): |
956 | + expected = compute_csr_mac( |
957 | + self.secret, |
958 | + csr_data, |
959 | + self.local_hash, |
960 | + self.remote_hash, |
961 | + ) |
962 | + if mac != expected: |
963 | + del self.secret |
964 | + raise WrongMAC(expected, mac) |
965 | + |
966 | + def cert_mac(self, cert_data): |
967 | + return compute_cert_mac( |
968 | + self.secret, |
969 | + cert_data, |
970 | + self.remote_hash, |
971 | + self.local_hash, |
972 | + ) |
973 | + |
974 | + def check_cert_mac(self, cert_data, mac): |
975 | + expected = compute_cert_mac( |
976 | + self.secret, |
977 | + cert_data, |
978 | + self.local_hash, |
979 | + self.remote_hash, |
980 | + ) |
981 | + if mac != expected: |
982 | + del self.secret |
983 | + raise WrongMAC(expected, mac) |
984 | + |
985 | + |
986 | + |
987 | +########################## |
988 | +# openssl helper functions |
989 | + |
990 | +def make_subject(cn): |
991 | + """ |
992 | + Make an openssl certificate subject from the common name *cn*. |
993 | + |
994 | + For example: |
995 | + |
996 | + >>> make_subject('foo') |
997 | + '/CN=foo' |
998 | + |
999 | + """ |
1000 | + return '/CN={}'.format(cn) |
1001 | |
1002 | |
1003 | def create_key(dst_file, bits=2048): |
1004 | @@ -171,6 +328,7 @@ |
1005 | Create an RSA keypair and save it to *dst_file*. |
1006 | """ |
1007 | assert bits % 1024 == 0 |
1008 | + assert bits >= 1024 |
1009 | check_call(['openssl', 'genrsa', |
1010 | '-out', dst_file, |
1011 | str(bits) |
1012 | @@ -357,301 +515,9 @@ |
1013 | return filename |
1014 | |
1015 | |
1016 | -def encode(value): |
1017 | - assert isinstance(value, bytes) |
1018 | - assert len(value) > 0 and len(value) % 5 == 0 |
1019 | - return base64.b32encode(value).decode('utf-8') |
1020 | - |
1021 | - |
1022 | -def decode(value): |
1023 | - assert isinstance(value, str) |
1024 | - assert len(value) > 0 and len(value) % 8 == 0 |
1025 | - return base64.b32decode(value.encode('utf-8')) |
1026 | - |
1027 | - |
1028 | -def _hash_pubkey(data): |
1029 | - return skein512(data, |
1030 | - digest_bits=240, |
1031 | - pers=PERS_PUBKEY, |
1032 | - ).digest() |
1033 | - |
1034 | - |
1035 | -def hash_pubkey(data): |
1036 | - return encode(_hash_pubkey(data)) |
1037 | - |
1038 | - |
1039 | -def _hash_cert(cert_data): |
1040 | - return skein512(cert_data, |
1041 | - digest_bits=200, |
1042 | - pers=PERS_CERT, |
1043 | - ).digest() |
1044 | - |
1045 | - |
1046 | -def hash_cert(cert_data): |
1047 | - return encode(_hash_cert(cert_data)) |
1048 | - |
1049 | - |
1050 | -def compute_response(secret, challenge, nonce, challenger_hash, responder_hash): |
1051 | - """ |
1052 | - |
1053 | - :param secret: the shared secret |
1054 | - |
1055 | - :param challenge: a nonce generated by the challenger |
1056 | - |
1057 | - :param none: a nonce generated by the responder |
1058 | - |
1059 | - :param challenger_hash: hash of the challengers certificate |
1060 | - |
1061 | - :param responder_hash: hash of the responders certificate |
1062 | - """ |
1063 | - assert len(secret) == 5 |
1064 | - assert len(challenge) == 20 |
1065 | - assert len(nonce) == 20 |
1066 | - assert len(challenger_hash) == 30 |
1067 | - assert len(responder_hash) == 30 |
1068 | - skein = skein512( |
1069 | - digest_bits=280, |
1070 | - pers=PERS_RESPONSE, |
1071 | - key=secret, |
1072 | - nonce=(challenge + nonce), |
1073 | - ) |
1074 | - skein.update(challenger_hash) |
1075 | - skein.update(responder_hash) |
1076 | - return encode(skein.digest()) |
1077 | - |
1078 | - |
1079 | -class WrongResponse(Exception): |
1080 | - def __init__(self, expected, got): |
1081 | - self.expected = expected |
1082 | - self.got = got |
1083 | - super().__init__('Incorrect response') |
1084 | - |
1085 | - |
1086 | -class ChallengeResponse: |
1087 | - def __init__(self, _id, peer_id): |
1088 | - self.id = _id |
1089 | - self.peer_id = peer_id |
1090 | - self.local_hash = decode(_id) |
1091 | - self.remote_hash = decode(peer_id) |
1092 | - assert len(self.local_hash) == 30 |
1093 | - assert len(self.remote_hash) == 30 |
1094 | - |
1095 | - def get_secret(self): |
1096 | - # 40-bit secret (8 characters when base32 encoded) |
1097 | - self.secret = os.urandom(5) |
1098 | - return encode(self.secret) |
1099 | - |
1100 | - def set_secret(self, secret): |
1101 | - assert len(secret) == 8 |
1102 | - self.secret = decode(secret) |
1103 | - assert len(self.secret) == 5 |
1104 | - |
1105 | - def get_challenge(self): |
1106 | - self.challenge = os.urandom(20) |
1107 | - return encode(self.challenge) |
1108 | - |
1109 | - def create_response(self, challenge): |
1110 | - nonce = os.urandom(20) |
1111 | - response = compute_response( |
1112 | - self.secret, |
1113 | - decode(challenge), |
1114 | - nonce, |
1115 | - self.remote_hash, |
1116 | - self.local_hash |
1117 | - ) |
1118 | - return (encode(nonce), response) |
1119 | - |
1120 | - def check_response(self, nonce, response): |
1121 | - expected = compute_response( |
1122 | - self.secret, |
1123 | - self.challenge, |
1124 | - decode(nonce), |
1125 | - self.local_hash, |
1126 | - self.remote_hash |
1127 | - ) |
1128 | - if response != expected: |
1129 | - del self.secret |
1130 | - del self.challenge |
1131 | - raise WrongResponse(expected, response) |
1132 | - |
1133 | - |
1134 | -class InfoApp: |
1135 | - def __init__(self, _id): |
1136 | - self.id = _id |
1137 | - obj = { |
1138 | - 'id': _id, |
1139 | - 'user': USER, |
1140 | - 'host': HOST, |
1141 | - } |
1142 | - self.info = dumps(obj).encode('utf-8') |
1143 | - self.info_length = str(len(self.info)) |
1144 | - |
1145 | - def __call__(self, environ, start_response): |
1146 | - if environ['wsgi.multithread'] is not False: |
1147 | - raise WSGIError('500 Internal Server Error') |
1148 | - if environ['PATH_INFO'] != '/': |
1149 | - raise WSGIError('410 Gone') |
1150 | - if environ['REQUEST_METHOD'] != 'GET': |
1151 | - raise WSGIError('405 Method Not Allowed') |
1152 | - start_response('200 OK', |
1153 | - [ |
1154 | - ('Content-Length', self.info_length), |
1155 | - ('Content-Type', 'application/json'), |
1156 | - ] |
1157 | - ) |
1158 | - return [self.info] |
1159 | - |
1160 | - |
1161 | -class ClientApp: |
1162 | - allowed_states = ( |
1163 | - 'ready', |
1164 | - 'gave_challenge', |
1165 | - 'in_response', |
1166 | - 'wrong_response', |
1167 | - 'response_ok', |
1168 | - ) |
1169 | - |
1170 | - forwarded_states = ( |
1171 | - 'wrong_response', |
1172 | - 'response_ok', |
1173 | - ) |
1174 | - |
1175 | - def __init__(self, cr, queue): |
1176 | - assert isinstance(cr, ChallengeResponse) |
1177 | - self.cr = cr |
1178 | - self.queue = queue |
1179 | - self.__state = None |
1180 | - self.map = { |
1181 | - '/challenge': self.get_challenge, |
1182 | - '/response': self.put_response, |
1183 | - } |
1184 | - |
1185 | - def get_state(self): |
1186 | - return self.__state |
1187 | - |
1188 | - def set_state(self, state): |
1189 | - if state not in self.__class__.allowed_states: |
1190 | - self.__state = None |
1191 | - log.error('invalid state: %r', state) |
1192 | - raise Exception('invalid state: {!r}'.format(state)) |
1193 | - self.__state = state |
1194 | - if state in self.__class__.forwarded_states: |
1195 | - self.queue.put(state) |
1196 | - |
1197 | - state = property(get_state, set_state) |
1198 | - |
1199 | - def __call__(self, environ, start_response): |
1200 | - if environ['wsgi.multithread'] is not False: |
1201 | - raise WSGIError('500 Internal Server Error') |
1202 | - if environ.get('SSL_CLIENT_VERIFY') != 'SUCCESS': |
1203 | - raise WSGIError('403 Forbidden') |
1204 | - if environ.get('SSL_CLIENT_S_DN_CN') != self.cr.peer_id: |
1205 | - raise WSGIError('403 Forbidden') |
1206 | - if environ.get('SSL_CLIENT_I_DN_CN') != self.cr.peer_id: |
1207 | - raise WSGIError('403 Forbidden') |
1208 | - |
1209 | - path_info = environ['PATH_INFO'] |
1210 | - if path_info not in self.map: |
1211 | - raise WSGIError('410 Gone') |
1212 | - log.info('%s %s', environ['REQUEST_METHOD'], environ['PATH_INFO']) |
1213 | - try: |
1214 | - obj = self.map[path_info](environ) |
1215 | - data = json.dumps(obj).encode('utf-8') |
1216 | - start_response('200 OK', |
1217 | - [ |
1218 | - ('Content-Length', str(len(data))), |
1219 | - ('Content-Type', 'application/json'), |
1220 | - ] |
1221 | - ) |
1222 | - return [data] |
1223 | - except WSGIError as e: |
1224 | - raise e |
1225 | - except Exception: |
1226 | - log.exception('500 Internal Server Error') |
1227 | - raise WSGIError('500 Internal Server Error') |
1228 | - |
1229 | - def get_challenge(self, environ): |
1230 | - if self.state != 'ready': |
1231 | - raise WSGIError('400 Bad Request Order') |
1232 | - self.state = 'gave_challenge' |
1233 | - if environ['REQUEST_METHOD'] != 'GET': |
1234 | - raise WSGIError('405 Method Not Allowed') |
1235 | - return { |
1236 | - 'challenge': self.cr.get_challenge(), |
1237 | - } |
1238 | - |
1239 | - def put_response(self, environ): |
1240 | - if self.state != 'gave_challenge': |
1241 | - raise WSGIError('400 Bad Request Order') |
1242 | - self.state = 'in_response' |
1243 | - if environ['REQUEST_METHOD'] != 'PUT': |
1244 | - raise WSGIError('405 Method Not Allowed') |
1245 | - data = environ['wsgi.input'].read() |
1246 | - obj = json.loads(data.decode('utf-8')) |
1247 | - nonce = obj['nonce'] |
1248 | - response = obj['response'] |
1249 | - try: |
1250 | - self.cr.check_response(nonce, response) |
1251 | - except WrongResponse: |
1252 | - self.state = 'wrong_response' |
1253 | - raise WSGIError('401 Unauthorized') |
1254 | - self.state = 'response_ok' |
1255 | - return {'ok': True} |
1256 | - |
1257 | - |
1258 | -class ServerApp(ClientApp): |
1259 | - |
1260 | - allowed_states = ( |
1261 | - 'info', |
1262 | - 'counter_response_ok', |
1263 | - 'in_csr', |
1264 | - 'bad_csr', |
1265 | - 'cert_issued', |
1266 | - ) + ClientApp.allowed_states |
1267 | - |
1268 | - forwarded_states = ( |
1269 | - 'bad_csr', |
1270 | - 'cert_issued', |
1271 | - ) + ClientApp.forwarded_states |
1272 | - |
1273 | - def __init__(self, cr, queue, pki): |
1274 | - super().__init__(cr, queue) |
1275 | - self.pki = pki |
1276 | - self.map['/'] = self.get_info |
1277 | - self.map['/csr'] = self.post_csr |
1278 | - |
1279 | - def get_info(self, environ): |
1280 | - if self.state != 'info': |
1281 | - raise WSGIError('400 Bad Request State') |
1282 | - self.state = 'ready' |
1283 | - if environ['REQUEST_METHOD'] != 'GET': |
1284 | - raise WSGIError('405 Method Not Allowed') |
1285 | - return { |
1286 | - 'id': self.cr.id, |
1287 | - 'user': USER, |
1288 | - 'host': HOST, |
1289 | - } |
1290 | - |
1291 | - def post_csr(self, environ): |
1292 | - if self.state != 'counter_response_ok': |
1293 | - raise WSGIError('400 Bad Request Order') |
1294 | - self.state = 'in_csr' |
1295 | - if environ['REQUEST_METHOD'] != 'POST': |
1296 | - raise WSGIError('405 Method Not Allowed') |
1297 | - data = environ['wsgi.input'].read() |
1298 | - obj = json.loads(data.decode('utf-8')) |
1299 | - csr_data = base64.b64decode(obj['csr'].encode('utf-8')) |
1300 | - try: |
1301 | - self.pki.write_csr(self.cr.peer_id, csr_data) |
1302 | - self.pki.issue_cert(self.cr.peer_id, self.cr.id) |
1303 | - cert_data = self.pki.read_cert2(self.cr.peer_id, self.cr.id) |
1304 | - except Exception as e: |
1305 | - log.exception('could not issue cert') |
1306 | - self.state = 'bad_csr' |
1307 | - raise WSGIError('401 Unauthorized') |
1308 | - self.state = 'cert_issued' |
1309 | - return {'cert': base64.b64encode(cert_data).decode('utf-8')} |
1310 | - |
1311 | + |
1312 | +########################### |
1313 | +# The PKI class and related |
1314 | |
1315 | def ensuredir(d): |
1316 | try: |
1317 | @@ -662,10 +528,6 @@ |
1318 | raise ValueError('not a directory: {!r}'.format(d)) |
1319 | |
1320 | |
1321 | -def make_subject(cn): |
1322 | - return '/CN={}'.format(cn) |
1323 | - |
1324 | - |
1325 | class PKI: |
1326 | def __init__(self, ssldir): |
1327 | self.ssldir = ssldir |
1328 | @@ -772,7 +634,7 @@ |
1329 | tmp_file = self.random_tmp() |
1330 | cert_file = self.path(_id, 'cert') |
1331 | |
1332 | - ca_file = self.verify_cert(subca_id) |
1333 | + ca_file = self.path(subca_id, 'cert') |
1334 | key_file = self.verify_key(subca_id) |
1335 | srl_file = self.path(subca_id, 'srl') |
1336 | |
1337 | @@ -780,32 +642,16 @@ |
1338 | os.rename(tmp_file, cert_file) |
1339 | return cert_file |
1340 | |
1341 | - def verify_cert(self, _id): |
1342 | - cert_file = self.path(_id, 'cert') |
1343 | - return verify(cert_file, _id) |
1344 | - |
1345 | - def verify_cert2(self, cert_id, ca_id): |
1346 | + def verify_cert(self, cert_id, ca_id): |
1347 | cert_file = self.path(cert_id, 'cert') |
1348 | ca_file = self.verify_ca(ca_id) |
1349 | return verify_cert(cert_file, cert_id, ca_file, ca_id) |
1350 | |
1351 | - def read_cert(self, _id): |
1352 | - cert_file = self.verify_cert(_id) |
1353 | - return open(cert_file, 'rb').read() |
1354 | - |
1355 | - def read_cert2(self, cert_id, ca_id): |
1356 | - cert_file = self.verify_cert2(cert_id, ca_id) |
1357 | - return open(cert_file, 'rb').read() |
1358 | - |
1359 | - def write_cert(self, _id, data): |
1360 | - tmp_file = self.random_tmp() |
1361 | - open(tmp_file, 'wb').write(data) |
1362 | - verify(tmp_file, _id) |
1363 | - cert_file = self.path(_id, 'cert') |
1364 | - os.rename(tmp_file, cert_file) |
1365 | - return cert_file |
1366 | - |
1367 | - def write_cert2(self, cert_id, ca_id, cert_data): |
1368 | + def read_cert(self, cert_id, ca_id): |
1369 | + cert_file = self.verify_cert(cert_id, ca_id) |
1370 | + return open(cert_file, 'rb').read() |
1371 | + |
1372 | + def write_cert(self, cert_id, ca_id, cert_data): |
1373 | ca_file = self.verify_ca(ca_id) |
1374 | tmp_file = self.random_tmp() |
1375 | open(tmp_file, 'wb').write(cert_data) |
1376 | @@ -817,20 +663,33 @@ |
1377 | def get_ca(self, _id): |
1378 | return CA(_id, self.verify_ca(_id)) |
1379 | |
1380 | - def get_cert(self, _id): |
1381 | - return Cert(_id, self.verify_cert(_id), self.verify_key(_id)) |
1382 | + def get_cert(self, cert_id, ca_id): |
1383 | + return Cert( |
1384 | + cert_id, |
1385 | + self.verify_cert(cert_id, ca_id), |
1386 | + self.verify_key(cert_id) |
1387 | + ) |
1388 | + |
1389 | + def load_machine(self, machine_id, user_id=None): |
1390 | + ca_file = self.verify_ca(machine_id) |
1391 | + key_file = self.verify_key(machine_id) |
1392 | + if user_id is None: |
1393 | + cert_file = None |
1394 | + else: |
1395 | + cert_file = self.verify_cert(machine_id, user_id) |
1396 | + return Machine(machine_id, ca_file, key_file, cert_file) |
1397 | + |
1398 | + def load_user(self, user_id): |
1399 | + ca_file = self.verify_ca(user_id) |
1400 | + if path.exists(self.path(user_id, 'key')): |
1401 | + key_file = self.verify_key(user_id) |
1402 | + else: |
1403 | + key_file = None |
1404 | + return User(user_id, ca_file, key_file) |
1405 | |
1406 | def load_pki(self, machine_id, user_id=None): |
1407 | - if user_id is None: |
1408 | - self.machine = Cert( |
1409 | - machine_id, |
1410 | - self.verify_ca(machine_id), |
1411 | - self.verify_key(machine_id) |
1412 | - ) |
1413 | - self.user = None |
1414 | - else: |
1415 | - self.machine = self.get_cert(machine_id) |
1416 | - self.user = self.get_ca(user_id) |
1417 | + self.machine = self.load_machine(machine_id, user_id) |
1418 | + self.user = (None if user_id is None else self.load_user(user_id)) |
1419 | |
1420 | |
1421 | class TempPKI(PKI): |
1422 | @@ -857,7 +716,7 @@ |
1423 | self.create_csr(cert_id) |
1424 | self.issue_cert(cert_id, ca_id) |
1425 | |
1426 | - return (self.get_ca(ca_id), self.get_cert(cert_id)) |
1427 | + return (self.get_ca(ca_id), self.get_cert(cert_id, ca_id)) |
1428 | |
1429 | def get_server_config(self): |
1430 | config = { |
1431 | |
1432 | === modified file 'dmedia/server.py' |
1433 | --- dmedia/server.py 2012-10-04 21:58:54 +0000 |
1434 | +++ dmedia/server.py 2012-10-16 12:35:25 +0000 |
1435 | @@ -20,96 +20,45 @@ |
1436 | # Jason Gerard DeRose <jderose@novacut.com> |
1437 | |
1438 | """ |
1439 | -dmedia HTTP server. |
1440 | - |
1441 | -== Security Note == |
1442 | - |
1443 | -To help prevent cross-site scripting attacks, the `HTTPError` raised for any |
1444 | -invalid request should not include any data supplied in the request. |
1445 | - |
1446 | -It is helpful to include a meaningful bit a text in the response body, plus it |
1447 | -allows us to test that an `HTTPError` is being raised because of the condition |
1448 | -we expected it to be raised for. But include only static messages. |
1449 | - |
1450 | -For example, this is okay: |
1451 | - |
1452 | ->>> raise BadRequest('too many slashes in request path') #doctest: +SKIP |
1453 | - |
1454 | -But this is not okay: |
1455 | - |
1456 | ->>> raise BadRequest('bad path: {}'.format(environ['PATH_INFO'])) #doctest: +SKIP |
1457 | - |
1458 | +Dmedia WSGI applications. |
1459 | """ |
1460 | |
1461 | import json |
1462 | +import os |
1463 | import socket |
1464 | +from base64 import b64encode, b64decode |
1465 | from wsgiref.util import shift_path_info |
1466 | import logging |
1467 | |
1468 | from filestore import DIGEST_B32LEN, B32ALPHABET, LEAF_SIZE |
1469 | -import microfiber |
1470 | +from microfiber import dumps, basic_auth_header, CouchBase |
1471 | |
1472 | +import dmedia |
1473 | from dmedia import __version__ |
1474 | -from dmedia.httpd import WSGIError, make_server |
1475 | -from dmedia import local |
1476 | - |
1477 | - |
1478 | -HTTP_METHODS = ('PUT', 'POST', 'GET', 'DELETE', 'HEAD') |
1479 | -WELCOME = json.dumps( |
1480 | - {'Dmedia': 'welcome', 'version': __version__}, |
1481 | - sort_keys=True, |
1482 | -).encode('utf-8') |
1483 | +from dmedia.httpd import WSGIError, make_server |
1484 | +from dmedia import local, peering |
1485 | + |
1486 | + |
1487 | +USER = os.environ.get('USER') |
1488 | +HOST = socket.gethostname() |
1489 | log = logging.getLogger() |
1490 | |
1491 | |
1492 | -def server_welcome(environ, start_response): |
1493 | - if environ['REQUEST_METHOD'] != 'GET': |
1494 | - raise WSGIError('405 Method Not Allowed') |
1495 | - start_response('200 OK', |
1496 | - [ |
1497 | - ('Content-Length', str(len(WELCOME))), |
1498 | - ('Content-Type', 'application/json'), |
1499 | - ] |
1500 | - ) |
1501 | - return [WELCOME] |
1502 | - |
1503 | - |
1504 | -class HTTPError(Exception): |
1505 | - def __init__(self, body=b'', headers=None): |
1506 | - if isinstance(body, str): |
1507 | - body = body.encode('utf-8') |
1508 | - headers = [('Content-Type', 'text/plain; charset=utf-8')] |
1509 | - self.body = body |
1510 | - self.headers = ([] if headers is None else headers) |
1511 | - super().__init__(self.status) |
1512 | - |
1513 | - |
1514 | -class BadRequest(HTTPError): |
1515 | - status = '400 Bad Request' |
1516 | - |
1517 | - |
1518 | -class NotFound(HTTPError): |
1519 | - status = '404 Not Found' |
1520 | - |
1521 | - |
1522 | -class MethodNotAllowed(HTTPError): |
1523 | - status = '405 Method Not Allowed' |
1524 | - |
1525 | - |
1526 | -class Conflict(HTTPError): |
1527 | - status = '409 Conflict' |
1528 | - |
1529 | - |
1530 | -class LengthRequired(HTTPError): |
1531 | - status = '411 Length Required' |
1532 | - |
1533 | - |
1534 | -class PreconditionFailed(HTTPError): |
1535 | - status = '412 Precondition Failed' |
1536 | - |
1537 | - |
1538 | -class BadRangeRequest(HTTPError): |
1539 | - status = '416 Requested Range Not Satisfiable' |
1540 | +def iter_headers(environ): |
1541 | + for (key, value) in environ.items(): |
1542 | + if key in ('CONTENT_LENGHT', 'CONTENT_TYPE'): |
1543 | + yield (key.replace('_', '-').lower(), value) |
1544 | + elif key.startswith('HTTP_'): |
1545 | + yield (key[5:].replace('_', '-').lower(), value) |
1546 | + |
1547 | + |
1548 | +def request_args(environ): |
1549 | + headers = dict(iter_headers(environ)) |
1550 | + if environ['wsgi.input']._avail: |
1551 | + body = environ['wsgi.input'].read() |
1552 | + else: |
1553 | + body = None |
1554 | + return (environ['REQUEST_METHOD'], environ['PATH_INFO'], body, headers) |
1555 | |
1556 | |
1557 | def get_slice(environ): |
1558 | @@ -168,27 +117,27 @@ |
1559 | """ |
1560 | unit = 'bytes=' |
1561 | if not value.startswith(unit): |
1562 | - raise BadRangeRequest('bad range units') |
1563 | + raise WSGIError('400 Bad Range Units') |
1564 | value = value[len(unit):] |
1565 | if value.startswith('-'): |
1566 | try: |
1567 | return (int(value), None) |
1568 | except ValueError: |
1569 | - raise BadRangeRequest('range -start is not an integer') |
1570 | + raise WSGIError('400 Bad Range Negative Start') |
1571 | parts = value.split('-') |
1572 | if not len(parts) == 2: |
1573 | - raise BadRangeRequest('not formatted as bytes=start-end') |
1574 | + raise WSGIError('400 Bad Range Format') |
1575 | try: |
1576 | start = int(parts[0]) |
1577 | except ValueError: |
1578 | - raise BadRangeRequest('range start is not an integer') |
1579 | + raise WSGIError('400 Bad Range Start') |
1580 | try: |
1581 | end = parts[1] |
1582 | stop = (int(end) + 1 if end else None) |
1583 | except ValueError: |
1584 | - raise BadRangeRequest('range end is not an integer') |
1585 | + raise WSGIError('400 Bad Range End') |
1586 | if not (stop is None or start < stop): |
1587 | - raise BadRangeRequest('range end must be less than or equal to start') |
1588 | + raise WSGIError('400 Bad Range') |
1589 | return (start, stop) |
1590 | |
1591 | |
1592 | @@ -207,35 +156,10 @@ |
1593 | 'bytes 500-999/1234' |
1594 | |
1595 | """ |
1596 | - assert 0 <= start < length |
1597 | - assert start < stop <= length |
1598 | + assert 0 <= start < stop <= length |
1599 | return 'bytes {}-{}/{}'.format(start, stop - 1, length) |
1600 | |
1601 | |
1602 | -class BaseWSGIMeta(type): |
1603 | - def __new__(meta, name, bases, dict): |
1604 | - http_methods = [] |
1605 | - cls = type.__new__(meta, name, bases, dict) |
1606 | - for name in filter(lambda n: n in HTTP_METHODS, dir(cls)): |
1607 | - method = getattr(cls, name) |
1608 | - if callable(method): |
1609 | - http_methods.append(name) |
1610 | - cls.http_methods = frozenset(http_methods) |
1611 | - return cls |
1612 | - |
1613 | - |
1614 | -class BaseWSGI(metaclass=BaseWSGIMeta): |
1615 | - def __call__(self, environ, start_response): |
1616 | - try: |
1617 | - name = environ['REQUEST_METHOD'] |
1618 | - if name not in self.__class__.http_methods: |
1619 | - raise MethodNotAllowed() |
1620 | - return getattr(self, name)(environ, start_response) |
1621 | - except HTTPError as e: |
1622 | - start_response(e.status, e.headers) |
1623 | - return [e.body] |
1624 | - |
1625 | - |
1626 | MiB = 1024 * 1024 |
1627 | |
1628 | |
1629 | @@ -259,92 +183,28 @@ |
1630 | assert remaining == 0 |
1631 | |
1632 | |
1633 | -class ReadOnlyApp(BaseWSGI): |
1634 | - def __init__(self, env): |
1635 | - self.local = local.LocalSlave(env) |
1636 | - info = { |
1637 | - 'Dmedia': 'Welcome', |
1638 | - 'version': __version__, |
1639 | - 'machine_id': env.get('machine_id'), |
1640 | - 'hostname': socket.gethostname(), |
1641 | - } |
1642 | - self._info = json.dumps(info, sort_keys=True).encode('utf-8') |
1643 | - |
1644 | - def server_info(self, environ, start_response): |
1645 | - start_response('200 OK', [('Content-Type', 'application/json')]) |
1646 | - return [self._info] |
1647 | - |
1648 | - def GET(self, environ, start_response): |
1649 | - path_info = environ['PATH_INFO'] |
1650 | - if path_info == '/': |
1651 | - return self.server_info(environ, start_response) |
1652 | - |
1653 | - _id = path_info.lstrip('/') |
1654 | - if not (len(_id) == DIGEST_B32LEN and set(_id).issubset(B32ALPHABET)): |
1655 | - raise NotFound() |
1656 | - try: |
1657 | - doc = self.local.get_doc(_id) |
1658 | - st = self.local.stat2(doc) |
1659 | - fp = open(st.name, 'rb') |
1660 | - except Exception: |
1661 | - raise NotFound() |
1662 | - |
1663 | - if doc.get('content_type'): |
1664 | - headers = [('Content-Type', doc['content_type'])] |
1665 | - else: |
1666 | - headers = [] |
1667 | - |
1668 | - if 'HTTP_RANGE' in environ: |
1669 | - (start, stop) = range_to_slice(environ['HTTP_RANGE']) |
1670 | - status = '206 Partial Content' |
1671 | - else: |
1672 | - start = 0 |
1673 | - stop = None |
1674 | - status = '200 OK' |
1675 | - |
1676 | - stop = (st.size if stop is None else min(st.size, stop)) |
1677 | - length = str(stop - start) |
1678 | - headers.append(('Content-Length', length)) |
1679 | - if 'HTTP_RANGE' in environ: |
1680 | - headers.append( |
1681 | - ('Content-Range', slice_to_content_range(start, stop, st.size)) |
1682 | - ) |
1683 | - |
1684 | - start_response(status, headers) |
1685 | - return FileSlice(fp, start, stop) |
1686 | - |
1687 | - |
1688 | -class ReadWriteApp(ReadOnlyApp): |
1689 | - def PUT(self, environ, start_response): |
1690 | - pass |
1691 | - |
1692 | - def POST(self, environ, start_response): |
1693 | - pass |
1694 | - |
1695 | - |
1696 | -def iter_headers(environ): |
1697 | - for (key, value) in environ.items(): |
1698 | - if key in ('CONTENT_LENGHT', 'CONTENT_TYPE'): |
1699 | - yield (key.replace('_', '-').lower(), value) |
1700 | - elif key.startswith('HTTP_'): |
1701 | - yield (key[5:].replace('_', '-').lower(), value) |
1702 | - |
1703 | - |
1704 | -def request_args(environ): |
1705 | - headers = dict(iter_headers(environ)) |
1706 | - if environ['wsgi.input']._avail: |
1707 | - body = environ['wsgi.input'].read() |
1708 | - else: |
1709 | - body = None |
1710 | - return (environ['REQUEST_METHOD'], environ['PATH_INFO'], body, headers) |
1711 | - |
1712 | - |
1713 | class RootApp: |
1714 | + """ |
1715 | + Main Dmedia WSGI app. |
1716 | + """ |
1717 | + |
1718 | def __init__(self, env): |
1719 | self.user_id = env['user_id'] |
1720 | + obj = { |
1721 | + 'user_id': env['user_id'], |
1722 | + 'machine_id': env['machine_id'], |
1723 | + 'version': dmedia.__version__, |
1724 | + 'user': USER, |
1725 | + 'host': HOST, |
1726 | + } |
1727 | + self.info = dumps(obj).encode('utf-8') |
1728 | + self.info_length = str(len(self.info)) |
1729 | + self.proxy = ProxyApp(env) |
1730 | + self.files = FilesApp(env) |
1731 | self.map = { |
1732 | - '': server_welcome, |
1733 | - 'couch': ProxyApp(env), |
1734 | + '': self.get_info, |
1735 | + 'couch': self.proxy, |
1736 | + 'files': self.files, |
1737 | } |
1738 | |
1739 | def __call__(self, environ, start_response): |
1740 | @@ -357,35 +217,35 @@ |
1741 | return self.map[key](environ, start_response) |
1742 | raise WSGIError('410 Gone') |
1743 | |
1744 | + def get_info(self, environ, start_response): |
1745 | + if environ['REQUEST_METHOD'] != 'GET': |
1746 | + raise WSGIError('405 Method Not Allowed') |
1747 | + start_response('200 OK', |
1748 | + [ |
1749 | + ('Content-Length', self.info_length), |
1750 | + ('Content-Type', 'application/json'), |
1751 | + ] |
1752 | + ) |
1753 | + return [self.info] |
1754 | + |
1755 | |
1756 | class ProxyApp: |
1757 | - def __init__(self, env, debug=False): |
1758 | - self.debug = debug |
1759 | - self.client = microfiber.CouchBase(env) |
1760 | + def __init__(self, env): |
1761 | + self.client = CouchBase(env) |
1762 | self.target_host = self.client.ctx.t.netloc |
1763 | - self.basic_auth = microfiber.basic_auth_header(env['basic']) |
1764 | + self.basic_auth = basic_auth_header(env['basic']) |
1765 | |
1766 | def __call__(self, environ, start_response): |
1767 | (method, path, body, headers) = request_args(environ) |
1768 | db = shift_path_info(environ) |
1769 | if db and db.startswith('_'): |
1770 | raise WSGIError('403 Forbidden') |
1771 | - if self.debug: |
1772 | - print('') |
1773 | - print('{REQUEST_METHOD} {PATH_INFO}'.format(**environ)) |
1774 | - for key in sorted(headers): |
1775 | - print('{}: {}'.format(key, headers[key])) |
1776 | headers['host'] = self.target_host |
1777 | headers['authorization'] = self.basic_auth |
1778 | |
1779 | response = self.client.raw_request(method, path, body, headers) |
1780 | status = '{} {}'.format(response.status, response.reason) |
1781 | headers = response.getheaders() |
1782 | - if self.debug: |
1783 | - print('-' * 80) |
1784 | - print(status) |
1785 | - for (key, value) in headers: |
1786 | - print('{}: {}'.format(key, value)) |
1787 | start_response(status, headers) |
1788 | body = response.read() |
1789 | if body: |
1790 | @@ -393,6 +253,242 @@ |
1791 | return [] |
1792 | |
1793 | |
1794 | +class FilesApp: |
1795 | + def __init__(self, env): |
1796 | + self.local = local.LocalSlave(env) |
1797 | + |
1798 | + def __call__(self, environ, start_response): |
1799 | + if environ['REQUEST_METHOD'] != 'GET': |
1800 | + raise WSGIError('405 Method Not Allowed') |
1801 | + _id = shift_path_info(environ) |
1802 | + if not (len(_id) == DIGEST_B32LEN and set(_id).issubset(B32ALPHABET)): |
1803 | + raise WSGIError('400 Bad Request ID') |
1804 | + try: |
1805 | + doc = self.local.get_doc(_id) |
1806 | + st = self.local.stat2(doc) |
1807 | + fp = open(st.name, 'rb') |
1808 | + except Exception: |
1809 | + raise WSGIError('404 Not Found') |
1810 | + |
1811 | + if 'HTTP_RANGE' in environ: |
1812 | + (start, stop) = range_to_slice(environ['HTTP_RANGE']) |
1813 | + status = '206 Partial Content' |
1814 | + else: |
1815 | + start = 0 |
1816 | + stop = None |
1817 | + status = '200 OK' |
1818 | + |
1819 | + # '416 Requested Range Not Satisfiable' |
1820 | + |
1821 | + stop = (st.size if stop is None else min(st.size, stop)) |
1822 | + length = str(stop - start) |
1823 | + headers = [('Content-Length', length)] |
1824 | + if 'HTTP_RANGE' in environ: |
1825 | + headers.append( |
1826 | + ('Content-Range', slice_to_content_range(start, stop, st.size)) |
1827 | + ) |
1828 | + start_response(status, headers) |
1829 | + return FileSlice(fp, start, stop) |
1830 | + |
1831 | + |
1832 | +class InfoApp: |
1833 | + """ |
1834 | + WSGI app initially used by the client-end of the peering process. |
1835 | + """ |
1836 | + |
1837 | + def __init__(self, _id): |
1838 | + self.id = _id |
1839 | + obj = { |
1840 | + 'id': _id, |
1841 | + 'version': dmedia.__version__, |
1842 | + 'user': USER, |
1843 | + 'host': HOST, |
1844 | + } |
1845 | + self.info = dumps(obj).encode('utf-8') |
1846 | + self.info_length = str(len(self.info)) |
1847 | + |
1848 | + def __call__(self, environ, start_response): |
1849 | + if environ['wsgi.multithread'] is not False: |
1850 | + raise WSGIError('500 Internal Server Error') |
1851 | + if environ['PATH_INFO'] != '/': |
1852 | + raise WSGIError('410 Gone') |
1853 | + if environ['REQUEST_METHOD'] != 'GET': |
1854 | + raise WSGIError('405 Method Not Allowed') |
1855 | + start_response('200 OK', |
1856 | + [ |
1857 | + ('Content-Length', self.info_length), |
1858 | + ('Content-Type', 'application/json'), |
1859 | + ] |
1860 | + ) |
1861 | + return [self.info] |
1862 | + |
1863 | + |
1864 | +class ClientApp: |
1865 | + """ |
1866 | + WSGI app used by the client-end of the peering process. |
1867 | + """ |
1868 | + |
1869 | + allowed_states = ( |
1870 | + 'ready', |
1871 | + 'gave_challenge', |
1872 | + 'in_response', |
1873 | + 'wrong_response', |
1874 | + 'response_ok', |
1875 | + ) |
1876 | + |
1877 | + forwarded_states = ( |
1878 | + 'wrong_response', |
1879 | + 'response_ok', |
1880 | + ) |
1881 | + |
1882 | + def __init__(self, cr, queue): |
1883 | + self.cr = cr |
1884 | + self.queue = queue |
1885 | + self.__state = None |
1886 | + self.map = { |
1887 | + '/challenge': self.get_challenge, |
1888 | + '/response': self.post_response, |
1889 | + } |
1890 | + |
1891 | + def get_state(self): |
1892 | + return self.__state |
1893 | + |
1894 | + def set_state(self, state): |
1895 | + if state not in self.__class__.allowed_states: |
1896 | + self.__state = None |
1897 | + log.error('invalid state: %r', state) |
1898 | + raise Exception('invalid state: {!r}'.format(state)) |
1899 | + self.__state = state |
1900 | + if state in self.__class__.forwarded_states: |
1901 | + self.queue.put(state) |
1902 | + |
1903 | + state = property(get_state, set_state) |
1904 | + |
1905 | + def __call__(self, environ, start_response): |
1906 | + if environ['wsgi.multithread'] is not False: |
1907 | + raise WSGIError('500 Internal Server Error') |
1908 | + if environ.get('SSL_CLIENT_VERIFY') != 'SUCCESS': |
1909 | + raise WSGIError('403 Forbidden SSL') |
1910 | + if environ.get('SSL_CLIENT_S_DN_CN') != self.cr.peer_id: |
1911 | + raise WSGIError('403 Forbidden Subject') |
1912 | + if environ.get('SSL_CLIENT_I_DN_CN') != self.cr.peer_id: |
1913 | + raise WSGIError('403 Forbidden Issuer') |
1914 | + |
1915 | + path_info = environ['PATH_INFO'] |
1916 | + if path_info not in self.map: |
1917 | + raise WSGIError('410 Gone') |
1918 | + log.info('%s %s', |
1919 | + environ.get('REQUEST_METHOD'), |
1920 | + environ.get('PATH_INFO') |
1921 | + ) |
1922 | + try: |
1923 | + obj = self.map[path_info](environ) |
1924 | + data = dumps(obj).encode('utf-8') |
1925 | + start_response('200 OK', |
1926 | + [ |
1927 | + ('Content-Length', str(len(data))), |
1928 | + ('Content-Type', 'application/json'), |
1929 | + ] |
1930 | + ) |
1931 | + return [data] |
1932 | + except WSGIError as e: |
1933 | + raise e |
1934 | + except Exception: |
1935 | + log.exception('500 Internal Server Error') |
1936 | + raise WSGIError('500 Internal Server Error') |
1937 | + |
1938 | + def get_challenge(self, environ): |
1939 | + if self.state != 'ready': |
1940 | + raise WSGIError('400 Bad Request Order') |
1941 | + self.state = 'gave_challenge' |
1942 | + if environ['REQUEST_METHOD'] != 'GET': |
1943 | + raise WSGIError('405 Method Not Allowed') |
1944 | + return { |
1945 | + 'challenge': self.cr.get_challenge(), |
1946 | + } |
1947 | + |
1948 | + def post_response(self, environ): |
1949 | + if self.state != 'gave_challenge': |
1950 | + raise WSGIError('400 Bad Request Order') |
1951 | + self.state = 'in_response' |
1952 | + if environ['REQUEST_METHOD'] != 'POST': |
1953 | + raise WSGIError('405 Method Not Allowed') |
1954 | + data = environ['wsgi.input'].read() |
1955 | + obj = json.loads(data.decode('utf-8')) |
1956 | + nonce = obj['nonce'] |
1957 | + response = obj['response'] |
1958 | + try: |
1959 | + self.cr.check_response(nonce, response) |
1960 | + except peering.WrongResponse: |
1961 | + self.state = 'wrong_response' |
1962 | + raise WSGIError('401 Unauthorized') |
1963 | + self.state = 'response_ok' |
1964 | + return {'ok': True} |
1965 | + |
1966 | + |
1967 | +class ServerApp(ClientApp): |
1968 | + """ |
1969 | + WSGI app used by the server-end of the peering process. |
1970 | + """ |
1971 | + |
1972 | + allowed_states = ( |
1973 | + 'info', |
1974 | + 'counter_response_ok', |
1975 | + 'in_csr', |
1976 | + 'bad_csr', |
1977 | + 'cert_issued', |
1978 | + ) + ClientApp.allowed_states |
1979 | + |
1980 | + forwarded_states = ( |
1981 | + 'bad_csr', |
1982 | + 'cert_issued', |
1983 | + ) + ClientApp.forwarded_states |
1984 | + |
1985 | + def __init__(self, cr, queue, pki): |
1986 | + super().__init__(cr, queue) |
1987 | + self.pki = pki |
1988 | + self.map['/'] = self.get_info |
1989 | + self.map['/csr'] = self.post_csr |
1990 | + |
1991 | + def get_info(self, environ): |
1992 | + if self.state != 'info': |
1993 | + raise WSGIError('400 Bad Request State') |
1994 | + self.state = 'ready' |
1995 | + if environ['REQUEST_METHOD'] != 'GET': |
1996 | + raise WSGIError('405 Method Not Allowed') |
1997 | + return { |
1998 | + 'id': self.cr.id, |
1999 | + 'user': USER, |
2000 | + 'host': HOST, |
2001 | + } |
2002 | + |
2003 | + def post_csr(self, environ): |
2004 | + if self.state != 'counter_response_ok': |
2005 | + raise WSGIError('400 Bad Request Order') |
2006 | + self.state = 'in_csr' |
2007 | + if environ['REQUEST_METHOD'] != 'POST': |
2008 | + raise WSGIError('405 Method Not Allowed') |
2009 | + data = environ['wsgi.input'].read() |
2010 | + d = json.loads(data.decode('utf-8')) |
2011 | + csr_data = b64decode(d['csr'].encode('utf-8')) |
2012 | + try: |
2013 | + self.cr.check_csr_mac(csr_data, d['mac']) |
2014 | + self.pki.write_csr(self.cr.peer_id, csr_data) |
2015 | + self.pki.issue_cert(self.cr.peer_id, self.cr.id) |
2016 | + cert_data = self.pki.read_cert(self.cr.peer_id, self.cr.id) |
2017 | + key_data = self.pki.read_key(self.cr.id) |
2018 | + except Exception as e: |
2019 | + log.exception('could not issue cert') |
2020 | + self.state = 'bad_csr' |
2021 | + raise WSGIError('401 Unauthorized') |
2022 | + self.state = 'cert_issued' |
2023 | + return { |
2024 | + 'cert': b64encode(cert_data).decode('utf-8'), |
2025 | + 'mac': self.cr.cert_mac(cert_data), |
2026 | + 'key': b64encode(key_data).decode('utf-8'), |
2027 | + } |
2028 | + |
2029 | + |
2030 | def run_server(queue, couch_env, bind_address, ssl_config): |
2031 | try: |
2032 | app = RootApp(couch_env) |
2033 | |
2034 | === modified file 'dmedia/service/__init__.py' |
2035 | --- dmedia/service/__init__.py 2012-04-15 05:29:57 +0000 |
2036 | +++ dmedia/service/__init__.py 2012-10-16 12:35:25 +0000 |
2037 | @@ -25,15 +25,34 @@ |
2038 | Code that is portable should go in dmedia/*.py (the dmedia core). |
2039 | """ |
2040 | |
2041 | +import logging |
2042 | + |
2043 | import dbus |
2044 | from dbus.mainloop.glib import DBusGMainLoop |
2045 | from gi.repository import GObject |
2046 | |
2047 | + |
2048 | +DBusGMainLoop(set_as_default=True) |
2049 | GObject.threads_init() |
2050 | -DBusGMainLoop(set_as_default=True) |
2051 | - |
2052 | - |
2053 | -def get_proxy(): |
2054 | + |
2055 | +BUS = 'org.freedesktop.Dmedia' |
2056 | + |
2057 | + |
2058 | +def get_proxy(bus=BUS): |
2059 | session = dbus.SessionBus() |
2060 | - return session.get_object('org.freedesktop.Dmedia', '/') |
2061 | + return session.get_object(bus, '/') |
2062 | + |
2063 | + |
2064 | +def init_if_needed(): |
2065 | + logging.info('Getting Dmedia proxy object...') |
2066 | + Dmedia = get_proxy() |
2067 | + logging.info('Checking Dmedia.NeedsInit()...') |
2068 | + logging.info(Dmedia.NeedsInit()) |
2069 | + if Dmedia.NeedsInit(): |
2070 | + logging.info('Needs firstrun init.') |
2071 | + from dmedia.gtk.peering import ClientUI |
2072 | + ui = ClientUI(Dmedia) |
2073 | + ui.run() |
2074 | + if ui.quit: |
2075 | + raise SystemExit(0) |
2076 | |
2077 | |
2078 | === modified file 'dmedia/service/avahi.py' |
2079 | --- dmedia/service/avahi.py 2012-10-06 23:43:40 +0000 |
2080 | +++ dmedia/service/avahi.py 2012-10-16 12:35:25 +0000 |
2081 | @@ -28,30 +28,49 @@ |
2082 | import time |
2083 | from collections import namedtuple |
2084 | |
2085 | -from filestore import _start_thread |
2086 | -from microfiber import Context, Server, Database, NotFound |
2087 | +from microfiber import dumps, NotFound, Context, Server, Database |
2088 | import dbus |
2089 | from gi.repository import GObject |
2090 | |
2091 | +from dmedia.parallel import start_thread |
2092 | from dmedia import util, views |
2093 | |
2094 | log = logging.getLogger() |
2095 | +Peer = namedtuple('Peer', 'env names') |
2096 | PROTO = 0 # Protocol -1 = both, 0 = IPv4, 1 = IPv6 |
2097 | -Peer = namedtuple('Peer', 'env names') |
2098 | -PEERS = '_local/peers' |
2099 | +PEERS_ID = '_local/peers' |
2100 | + |
2101 | + |
2102 | +def make_url(ip, port): |
2103 | + if PROTO == 0: |
2104 | + return 'https://{}:{}/'.format(ip, port) |
2105 | + elif PROTO == 1: |
2106 | + return 'https://[{}]:{}/'.format(ip, port) |
2107 | + raise Exception('bad PROTO') |
2108 | |
2109 | |
2110 | class Avahi: |
2111 | - """ |
2112 | - Base class to capture the messy Avahi DBus details. |
2113 | - """ |
2114 | - |
2115 | - service = '_example._tcp' |
2116 | - |
2117 | - def __init__(self, _id, port): |
2118 | + |
2119 | + service = '_dmedia._tcp' |
2120 | + |
2121 | + def __init__(self, env, port, ssl_config): |
2122 | self.group = None |
2123 | - self.id = _id |
2124 | + self.machine_id = env['machine_id'] |
2125 | + self.user_id = env['user_id'] |
2126 | self.port = port |
2127 | + self.ssl_config = ssl_config |
2128 | + ctx = Context(env) |
2129 | + self.db = Database('dmedia-0', ctx=ctx) |
2130 | + self.server = Server(ctx=ctx) |
2131 | + self.replications = {} |
2132 | + try: |
2133 | + self.peers = self.db.get(PEERS_ID) |
2134 | + if self.peers.get('peers') != {}: |
2135 | + self.peers['peers'] = {} |
2136 | + self.db.save(self.peers) |
2137 | + except NotFound: |
2138 | + self.peers = {'_id': PEERS_ID, 'peers': {}} |
2139 | + self.db.save(self.peers) |
2140 | |
2141 | def __del__(self): |
2142 | self.free() |
2143 | @@ -65,14 +84,12 @@ |
2144 | dbus_interface='org.freedesktop.Avahi.Server' |
2145 | ) |
2146 | ) |
2147 | - log.info( |
2148 | - 'Avahi(%s): advertising %s on port %s', self.service, self.id, self.port |
2149 | - ) |
2150 | + log.info('Avahi: advertising %s on port %s', self.machine_id, self.port) |
2151 | self.group.AddService( |
2152 | -1, # Interface |
2153 | PROTO, # Protocol -1 = both, 0 = ipv4, 1 = ipv6 |
2154 | 0, # Flags |
2155 | - self.id, |
2156 | + self.machine_id, |
2157 | self.service, |
2158 | '', # Domain, default to .local |
2159 | '', # Host, default to localhost |
2160 | @@ -90,22 +107,25 @@ |
2161 | dbus_interface='org.freedesktop.Avahi.Server' |
2162 | ) |
2163 | self.browser = system.get_object('org.freedesktop.Avahi', browser_path) |
2164 | + self.browser.connect_to_signal('ItemRemove', self.on_ItemRemove) |
2165 | self.browser.connect_to_signal('ItemNew', self.on_ItemNew) |
2166 | - self.browser.connect_to_signal('ItemRemove', self.on_ItemRemove) |
2167 | + self.timeout_id = GObject.timeout_add(15000, self.on_timeout) |
2168 | |
2169 | def free(self): |
2170 | if self.group is not None: |
2171 | - log.info( |
2172 | - 'Avahi(%s): freeing %s on port %s', self.service, self.id, self.port |
2173 | - ) |
2174 | + log.info('Avahi: freeing %s on port %s', self.machine_id, self.port) |
2175 | self.group.Reset(dbus_interface='org.freedesktop.Avahi.EntryGroup') |
2176 | self.group = None |
2177 | del self.browser |
2178 | del self.avahi |
2179 | |
2180 | + def on_ItemRemove(self, interface, protocol, key, _type, domain, flags): |
2181 | + log.info('Avahi: peer removed: %s', key) |
2182 | + GObject.idle_add(self.remove_peer, key) |
2183 | + |
2184 | def on_ItemNew(self, interface, protocol, key, _type, domain, flags): |
2185 | # Ignore what we publish ourselves: |
2186 | - if key == self.id: |
2187 | + if key == self.machine_id: |
2188 | return |
2189 | self.avahi.ResolveService( |
2190 | # 2nd to last arg is Protocol, again for some reason |
2191 | @@ -118,57 +138,30 @@ |
2192 | def on_reply(self, *args): |
2193 | key = args[2] |
2194 | (ip, port) = args[7:9] |
2195 | - url = 'https://{}:{}/'.format(ip, port) |
2196 | - log.info('Avahi(%s): new peer %s at %s', self.service, key, url) |
2197 | - self.add_peer(key, url) |
2198 | + url = make_url(ip, port) |
2199 | + log.info('Avahi: new peer %s at %s', key, url) |
2200 | + start_thread(self.info_thread, key, url) |
2201 | |
2202 | def on_error(self, exception): |
2203 | - log.error('%s: error calling ResolveService(): %r', self.service, exception) |
2204 | - |
2205 | - def on_ItemRemove(self, interface, protocol, key, _type, domain, flags): |
2206 | - log.info('Avahi(%s): peer removed: %s', self.service, key) |
2207 | - self.remove_peer(key) |
2208 | - |
2209 | - def add_peer(self, key, url): |
2210 | - raise NotImplementedError( |
2211 | - '{}.add_peer()'.format(self.__class__.__name__) |
2212 | - ) |
2213 | - |
2214 | - def remove_peer(self, key): |
2215 | - raise NotImplementedError( |
2216 | - '{}.remove_peer()'.format(self.__class__.__name__) |
2217 | - ) |
2218 | - |
2219 | - |
2220 | -class FileServer(Avahi): |
2221 | - """ |
2222 | - Advertise HTTP file server server over Avahi, discover other peers. |
2223 | - """ |
2224 | - service = '_dmedia._tcp' |
2225 | - |
2226 | - def __init__(self, env, port): |
2227 | - super().__init__(env['machine_id'], port) |
2228 | - ctx = Context(env) |
2229 | - self.db = Database('dmedia-0', ctx=ctx) |
2230 | - self.server = Server(ctx=ctx) |
2231 | - self.replications = {} |
2232 | - |
2233 | - def run(self): |
2234 | + log.error('Avahi: error calling ResolveService(): %r', exception) |
2235 | + |
2236 | + def info_thread(self, key, url): |
2237 | try: |
2238 | - self.peers = self.db.get(PEERS) |
2239 | - if self.peers.get('peers') != {}: |
2240 | - self.peers['peers'] = {} |
2241 | - self.db.save(self.peers) |
2242 | - except NotFound: |
2243 | - self.peers = {'_id': PEERS, 'peers': {}} |
2244 | - self.db.save(self.peers) |
2245 | - super().run() |
2246 | - self.timeout_id = GObject.timeout_add(15000, self.on_timeout) |
2247 | + env = {'url': url, 'ssl': self.ssl_config} |
2248 | + client = Server(env) |
2249 | + info = client.get() |
2250 | + assert info.pop('user_id') == self.user_id |
2251 | + assert info.pop('machine_id') == key |
2252 | + info['url'] = url |
2253 | + log.info('Avahi: got peer info: %s', dumps(info, pretty=True)) |
2254 | + GObject.idle_add(self.add_peer, key, info) |
2255 | + except Exception: |
2256 | + log.exception('Avahi: could not get info for %s', url) |
2257 | |
2258 | - def add_peer(self, key, url): |
2259 | - self.peers['peers'][key] = url |
2260 | + def add_peer(self, key, info): |
2261 | + self.peers['peers'][key] = info |
2262 | self.db.save(self.peers) |
2263 | - self.add_replication_peer(key, url) |
2264 | + self.add_replication_peer(key, info['url']) |
2265 | |
2266 | def remove_peer(self, key): |
2267 | try: |
2268 | @@ -178,6 +171,18 @@ |
2269 | pass |
2270 | self.remove_replication_peer(key) |
2271 | |
2272 | + def add_replication_peer(self, key, url): |
2273 | + env = {'url': url + 'couch/'} |
2274 | + cancel = self.replications.pop(key, None) |
2275 | + start = Peer(env, list(self.get_names())) |
2276 | + self.replications[key] = start |
2277 | + start_thread(self.replication_worker, cancel, start) |
2278 | + |
2279 | + def remove_replication_peer(self, key): |
2280 | + cancel = self.replications.pop(key, None) |
2281 | + if cancel: |
2282 | + start_thread(self.replication_worker, cancel, None) |
2283 | + |
2284 | def on_timeout(self): |
2285 | if not self.replications: |
2286 | return True # Repeat timeout call |
2287 | @@ -188,21 +193,9 @@ |
2288 | log.info('New databases: %r', sorted(new)) |
2289 | tmp = Peer(peer.env, tuple(new)) |
2290 | peer.names.extend(new) |
2291 | - _start_thread(self.replication_worker, None, tmp) |
2292 | + start_thread(self.replication_worker, None, tmp) |
2293 | return True # Repeat timeout call |
2294 | |
2295 | - def add_replication_peer(self, key, url): |
2296 | - env = {'url': url + 'couch/'} |
2297 | - cancel = self.replications.pop(key, None) |
2298 | - start = Peer(env, list(self.get_names())) |
2299 | - self.replications[key] = start |
2300 | - _start_thread(self.replication_worker, cancel, start) |
2301 | - |
2302 | - def remove_replication_peer(self, key): |
2303 | - cancel = self.replications.pop(key, None) |
2304 | - if cancel: |
2305 | - _start_thread(self.replication_worker, cancel, None) |
2306 | - |
2307 | def get_names(self): |
2308 | for name in self.server.get('_all_dbs'): |
2309 | if name.startswith('dmedia-0') or name.startswith('novacut-0'): |
2310 | @@ -248,4 +241,4 @@ |
2311 | log.exception('Error canceling push of %s to %s', name, env['url']) |
2312 | else: |
2313 | log.exception('Error starting push of %s to %s', name, env['url']) |
2314 | - |
2315 | + |
2316 | |
2317 | === modified file 'dmedia/service/peers.py' |
2318 | --- dmedia/service/peers.py 2012-10-08 19:01:37 +0000 |
2319 | +++ dmedia/service/peers.py 2012-10-16 12:35:25 +0000 |
2320 | @@ -35,19 +35,28 @@ |
2321 | import ssl |
2322 | import socket |
2323 | import threading |
2324 | +from queue import Queue |
2325 | +from base64 import b64encode, b64decode |
2326 | +from gettext import gettext as _ |
2327 | |
2328 | import dbus |
2329 | -from dbus.mainloop.glib import DBusGMainLoop |
2330 | -from gi.repository import GObject |
2331 | -from microfiber import _start_thread, random_id, CouchBase, dumps, build_ssl_context |
2332 | +from gi.repository import GObject, Gtk, AppIndicator3 |
2333 | +from microfiber import Unauthorized, CouchBase |
2334 | +from microfiber import random_id, dumps, build_ssl_context |
2335 | + |
2336 | + |
2337 | +from dmedia.parallel import start_thread |
2338 | +from dmedia.gtk.peering import BaseUI |
2339 | +from dmedia.gtk.ubuntu import NotifyManager |
2340 | +from dmedia.peering import ChallengeResponse |
2341 | +from dmedia.server import ServerApp, InfoApp, ClientApp |
2342 | +from dmedia.httpd import WSGIError, make_server |
2343 | + |
2344 | |
2345 | PROTO = 0 # Protocol -1 = both, 0 = IPv4, 1 = IPv6 |
2346 | -GObject.threads_init() |
2347 | -DBusGMainLoop(set_as_default=True) |
2348 | -log = logging.getLogger() |
2349 | - |
2350 | Peer = namedtuple('Peer', 'id ip port') |
2351 | Info = namedtuple('Info', 'name host url id') |
2352 | +log = logging.getLogger() |
2353 | |
2354 | |
2355 | def get_service(verb): |
2356 | @@ -175,9 +184,10 @@ |
2357 | self.group = None |
2358 | self.pki = pki |
2359 | self.client_mode = client_mode |
2360 | - self.id = (pki.machine.id if client_mode else pki.user.id) |
2361 | - self.cert_file = pki.verify_ca(self.id) |
2362 | - self.key_file = pki.verify_key(self.id) |
2363 | + ca = (pki.machine if client_mode else pki.user) |
2364 | + self.id = ca.id |
2365 | + self.cert_file = ca.ca_file |
2366 | + self.key_file = ca.key_file |
2367 | self.state = State() |
2368 | self.peer = None |
2369 | self.info = None |
2370 | @@ -192,7 +202,7 @@ |
2371 | raise Exception( |
2372 | 'Cannot activate {!r} from {!r}'.format(peer_id, self.state) |
2373 | ) |
2374 | - log.info('Activated session with %r', self.peer) |
2375 | + log.info('Peering: activated session with %r', self.peer) |
2376 | assert self.state.state == 'activated' |
2377 | assert self.state.peer_id == peer_id |
2378 | assert self.peer.id == peer_id |
2379 | @@ -206,7 +216,7 @@ |
2380 | raise Exception( |
2381 | 'Cannot deactivate {!r} from {!r}'.format(peer_id, self.state) |
2382 | ) |
2383 | - log.info('Deactivated session with %r', self.peer) |
2384 | + log.info('Peering: deactivated session with %r', self.peer) |
2385 | assert self.state.state == 'deactivated' |
2386 | assert self.state.peer_id == peer_id |
2387 | assert self.peer.id == peer_id |
2388 | @@ -222,21 +232,21 @@ |
2389 | def unbind(self, peer_id): |
2390 | retract = (self.state.state == 'verified') |
2391 | if not self.state.unbind(peer_id): |
2392 | - log.error('Cannot unbind %s from %r', peer_id, self.state) |
2393 | + log.error('Peering: cannot unbind %s from %r', peer_id, self.state) |
2394 | return |
2395 | - log.info('Unbound from %s', peer_id) |
2396 | + log.info('Peering: unbound from %s', peer_id) |
2397 | assert self.state.peer_id == peer_id |
2398 | assert self.state.state == 'unbound' |
2399 | if retract: |
2400 | - log.info("Firing 'retract' signal") |
2401 | + log.info("Peering: firing 'retract' signal") |
2402 | self.emit('retract') |
2403 | GObject.timeout_add(10 * 1000, self.on_timeout, peer_id) |
2404 | |
2405 | def on_timeout(self, peer_id): |
2406 | if not self.state.free(peer_id): |
2407 | - log.error('Cannot free %s from %r', peer_id, self.state) |
2408 | + log.error('Peering: cannot free %s from %r', peer_id, self.state) |
2409 | return |
2410 | - log.info('Rate-limiting timeout reached, freeing from %s', peer_id) |
2411 | + log.info('Peering: rate-limiting timeout reached, freeing from %s', peer_id) |
2412 | assert self.state.state == 'free' |
2413 | assert self.state.peer_id is None |
2414 | self.info = None |
2415 | @@ -277,7 +287,7 @@ |
2416 | ) |
2417 | ) |
2418 | log.info( |
2419 | - 'Publishing %s for %r on port %s', self.id, service, port |
2420 | + 'Peering: publishing %s for %r on port %s', self.id, service, port |
2421 | ) |
2422 | self.group.AddService( |
2423 | -1, # Interface |
2424 | @@ -295,14 +305,14 @@ |
2425 | |
2426 | def unpublish(self): |
2427 | if self.group is not None: |
2428 | - log.info('Un-publishing %s', self.id) |
2429 | + log.info('Peering: unpublishing %s', self.id) |
2430 | self.group.Reset(dbus_interface='org.freedesktop.Avahi.EntryGroup') |
2431 | self.group = None |
2432 | |
2433 | def browse(self): |
2434 | verb = ('accept' if self.client_mode else 'offer') |
2435 | service = get_service(verb) |
2436 | - log.info('Browsing for %r', service) |
2437 | + log.info('Peering: browsing for %r', service) |
2438 | path = self.avahi.ServiceBrowserNew( |
2439 | -1, # Interface |
2440 | PROTO, # Protocol -1 = both, 0 = ipv4, 1 = ipv6 |
2441 | @@ -316,10 +326,10 @@ |
2442 | self.browser.connect_to_signal('ItemRemove', self.on_ItemRemove) |
2443 | |
2444 | def on_ItemNew(self, interface, protocol, peer_id, _type, domain, flags): |
2445 | - log.info('Peer added: %s', peer_id) |
2446 | + log.info('Peering: peer added: %s', peer_id) |
2447 | if not self.state.bind(str(peer_id)): |
2448 | - log.error('Cannot bind %s from %r', peer_id, self.state) |
2449 | - log.warning('Possible attack from %s', peer_id) |
2450 | + log.error('Peering: cannot bind %s from %r', peer_id, self.state) |
2451 | + log.warning('Peering: possible attack from %s', peer_id) |
2452 | return |
2453 | assert self.state.state == 'bound' |
2454 | assert self.state.peer_id == peer_id |
2455 | @@ -342,7 +352,7 @@ |
2456 | (ip, port) = args[7:9] |
2457 | log.info('%s is at %s, port %s', peer_id, ip, port) |
2458 | self.peer = Peer(str(peer_id), str(ip), int(port)) |
2459 | - _start_thread(self.cert_thread, self.peer) |
2460 | + start_thread(self.cert_thread, self.peer) |
2461 | |
2462 | def on_error(self, error): |
2463 | log.error( |
2464 | @@ -417,3 +427,293 @@ |
2465 | signal = ('accept' if self.client_mode else 'offer') |
2466 | log.info('Firing %r signal for %r', signal, info) |
2467 | self.emit(signal, info) |
2468 | + |
2469 | + |
2470 | +class ServerUI(BaseUI): |
2471 | + page = 'server.html' |
2472 | + |
2473 | + signals = { |
2474 | + 'get_secret': [], |
2475 | + 'display_secret': ['secret'], |
2476 | + 'set_message': ['message'], |
2477 | + 'done': [], |
2478 | + } |
2479 | + |
2480 | + def __init__(self, cr): |
2481 | + super().__init__() |
2482 | + self.cr = cr |
2483 | + |
2484 | + def connect_hub_signals(self, hub): |
2485 | + hub.connect('get_secret', self.on_get_secret) |
2486 | + |
2487 | + def on_get_secret(self, hub): |
2488 | + secret = self.cr.get_secret() |
2489 | + hub.send('display_secret', secret) |
2490 | + |
2491 | + |
2492 | +class ServerSession: |
2493 | + def __init__(self, pki, _id, peer, server_config, client_config): |
2494 | + self.pki = pki |
2495 | + self.peer_id = peer.id |
2496 | + self.peer = peer |
2497 | + self.cr = ChallengeResponse(_id, peer.id) |
2498 | + self.q = Queue() |
2499 | + start_thread(self.monitor_response) |
2500 | + self.app = ServerApp(self.cr, self.q, pki) |
2501 | + self.app.state = 'info' |
2502 | + self.httpd = make_server(self.app, '0.0.0.0', server_config) |
2503 | + env = {'url': peer.url, 'ssl': client_config} |
2504 | + self.client = CouchBase(env) |
2505 | + self.httpd.start() |
2506 | + self.ui = ServerUI(self.cr) |
2507 | + |
2508 | + def monitor_response(self): |
2509 | + while True: |
2510 | + signal = self.q.get() |
2511 | + if signal == 'wrong_response': |
2512 | + GObject.idle_add(self.retry) |
2513 | + elif signal == 'response_ok': |
2514 | + GObject.timeout_add(500, self.on_response_ok) |
2515 | + break |
2516 | + |
2517 | + def monitor_cert_request(self): |
2518 | + status = self.q.get() |
2519 | + if status != 'cert_issued': |
2520 | + log.error('Bad cert request from %r', self.peer) |
2521 | + log.warning('Possible malicious peer: %r', self.peer) |
2522 | + GObject.idle_add(self.on_cert_request, status) |
2523 | + |
2524 | + def retry(self): |
2525 | + self.httpd.shutdown() |
2526 | + secret = self.cr.get_secret() |
2527 | + self.ui.hub.send('display_secret', secret) |
2528 | + self.ui.hub.send('set_message', |
2529 | + _('Typo? Please try again with new secret.') |
2530 | + ) |
2531 | + self.app.state = 'ready' |
2532 | + self.httpd.start() |
2533 | + |
2534 | + def on_response_ok(self): |
2535 | + assert self.app.state == 'response_ok' |
2536 | + self.ui.hub.send('set_message', _('Counter-Challenge...')) |
2537 | + start_thread(self.counter_challenge) |
2538 | + |
2539 | + def counter_challenge(self): |
2540 | + log.info('Getting counter-challenge from %r', self.peer) |
2541 | + challenge = self.client.get('challenge')['challenge'] |
2542 | + (nonce, response) = self.cr.create_response(challenge) |
2543 | + obj = {'nonce': nonce, 'response': response} |
2544 | + log.info('Posting counter-response to %r', self.peer) |
2545 | + try: |
2546 | + r = self.client.post(obj, 'response') |
2547 | + log.info('Counter-response accepted') |
2548 | + GObject.idle_add(self.on_counter_response_ok) |
2549 | + except Unauthorized: |
2550 | + log.error('Counter-response rejected!') |
2551 | + log.warning('Possible malicious peer: %r', self.peer) |
2552 | + GObject.idle_add(self.on_counter_response_fail) |
2553 | + |
2554 | + def on_counter_response_ok(self): |
2555 | + assert self.app.state == 'response_ok' |
2556 | + self.app.state = 'counter_response_ok' |
2557 | + start_thread(self.monitor_cert_request) |
2558 | + self.ui.hub.send('set_message', _('Issuing Certificate...')) |
2559 | + |
2560 | + def on_counter_response_fail(self): |
2561 | + self.ui.hub.send('set_message', _('Very Bad Things!')) |
2562 | + |
2563 | + def on_cert_request(self, status): |
2564 | + if status == 'cert_issued': |
2565 | + self.ui.hub.send('set_message', _('Done!')) |
2566 | + GObject.timeout_add(250, self.ui.hub.send, 'done') |
2567 | + else: |
2568 | + self.ui.hub.send('set_message', _('Security Problems in CSR!')) |
2569 | + |
2570 | + |
2571 | + |
2572 | +class Browser: |
2573 | + def __init__(self, couch): |
2574 | + self.couch = couch |
2575 | + self.avahi = AvahiPeer(couch.pki) |
2576 | + self.avahi.connect('offer', self.on_offer) |
2577 | + self.avahi.connect('retract', self.on_retract) |
2578 | + self.avahi.browse() |
2579 | + self.notifymanager = NotifyManager() |
2580 | + self.indicator = None |
2581 | + self.session = None |
2582 | + |
2583 | + def free(self): |
2584 | + self.avahi.unpublish() |
2585 | + |
2586 | + def on_offer(self, avahi, info): |
2587 | + assert self.indicator is None |
2588 | + self.indicator = AppIndicator3.Indicator.new( |
2589 | + 'dmedia-peer', |
2590 | + 'indicator-novacut', |
2591 | + AppIndicator3.IndicatorCategory.APPLICATION_STATUS |
2592 | + ) |
2593 | + menu = Gtk.Menu() |
2594 | + accept = Gtk.MenuItem() |
2595 | + accept.set_label(_('Accept {}@{}').format(info.name, info.host)) |
2596 | + accept.connect('activate', self.on_accept, info) |
2597 | + menu.append(accept) |
2598 | + menu.show_all() |
2599 | + self.indicator.set_menu(menu) |
2600 | + self.indicator.set_status(AppIndicator3.IndicatorStatus.ATTENTION) |
2601 | + self.notifymanager.replace( |
2602 | + _('Novacut Peering Offer'), |
2603 | + '{}@{}'.format(info.name, info.host), |
2604 | + ) |
2605 | + |
2606 | + def on_retract(self, avahi): |
2607 | + if self.indicator is not None: |
2608 | + self.indicator = None |
2609 | + self.notifymanager.replace(_('Peering Offer Removed')) |
2610 | + |
2611 | + def on_accept(self, menuitem, info): |
2612 | + assert self.session is None |
2613 | + self.avahi.activate(info.id) |
2614 | + self.indicator = None |
2615 | + self.session = ServerSession(self.couch.pki, self.avahi.id, info, |
2616 | + self.avahi.get_server_config(), |
2617 | + self.avahi.get_client_config() |
2618 | + ) |
2619 | + self.session.ui.window.connect('delete-event', self.on_delete_event) |
2620 | + self.session.ui.hub.connect('done', self.on_delete_event) |
2621 | + self.session.ui.window.show_all() |
2622 | + self.avahi.publish(self.session.httpd.port) |
2623 | + |
2624 | + def on_delete_event(self, *args): |
2625 | + self.session.httpd.shutdown() |
2626 | + self.session.ui.window.destroy() |
2627 | + self.avahi.unpublish() |
2628 | + self.avahi.deactivate(self.session.peer_id) |
2629 | + self.session = None |
2630 | + |
2631 | + |
2632 | +class Publisher: |
2633 | + def __init__(self, service, couch): |
2634 | + self.service = service |
2635 | + self.couch = couch |
2636 | + self.thread = None |
2637 | + self.avahi = None |
2638 | + |
2639 | + def __del__(self): |
2640 | + self.free() |
2641 | + |
2642 | + def free(self): |
2643 | + if self.avahi is not None: |
2644 | + self.avahi.unpublish() |
2645 | + self.avahi = None |
2646 | + del self.service |
2647 | + del self.couch |
2648 | + |
2649 | + def run(self): |
2650 | + self.couch.load_pki() |
2651 | + self.avahi = AvahiPeer(self.couch.pki, client_mode=True) |
2652 | + self.avahi.connect('accept', self.on_accept) |
2653 | + app = InfoApp(self.avahi.id) |
2654 | + self.httpd = make_server(app, '0.0.0.0', |
2655 | + self.avahi.get_server_config() |
2656 | + ) |
2657 | + self.httpd.start() |
2658 | + self.avahi.browse() |
2659 | + self.avahi.publish(self.httpd.port) |
2660 | + |
2661 | + def on_accept(self, avahi, peer): |
2662 | + log.info('Publisher.on_accept()') |
2663 | + self.avahi.activate(peer.id) |
2664 | + self.peer = peer |
2665 | + self.cr = ChallengeResponse(avahi.id, peer.id) |
2666 | + self.q = Queue() |
2667 | + self.app = ClientApp(self.cr, self.q) |
2668 | + # Reconfigure HTTPD to only accept connections from bound peer |
2669 | + self.httpd.reconfigure(self.app, avahi.get_server_config()) |
2670 | + env = {'url': peer.url, 'ssl': avahi.get_client_config()} |
2671 | + self.client = CouchBase(env) |
2672 | + avahi.unpublish() |
2673 | + self.service.Accept() |
2674 | + |
2675 | + def set_secret(self, secret): |
2676 | + if self.thread is not None: |
2677 | + return False |
2678 | + self.cr.set_secret(secret) |
2679 | + self.thread = start_thread(self.challenge) |
2680 | + self.service.Message(_('Challenge...')) |
2681 | + return True |
2682 | + |
2683 | + def challenge(self): |
2684 | + log.info('Getting challenge from %r', self.peer) |
2685 | + challenge = self.client.get('challenge')['challenge'] |
2686 | + (nonce, response) = self.cr.create_response(challenge) |
2687 | + obj = {'nonce': nonce, 'response': response} |
2688 | + log.info('Posting response to %r', self.peer) |
2689 | + try: |
2690 | + r = self.client.post(obj, 'response') |
2691 | + log.info('Response accepted') |
2692 | + success = True |
2693 | + except Unauthorized: |
2694 | + log.info('Response rejected') |
2695 | + success = False |
2696 | + GObject.idle_add(self.on_response, success) |
2697 | + |
2698 | + def on_response(self, success): |
2699 | + self.thread.join() |
2700 | + self.thread = None |
2701 | + if success: |
2702 | + self.app.state = 'ready' |
2703 | + self.thread = start_thread(self.monitor_counter_response) |
2704 | + self.service.Message(_('Counter-Challenge...')) |
2705 | + else: |
2706 | + self.service.Message(_('Typo? Please try again with new secret.')) |
2707 | + self.service.Response(success) |
2708 | + |
2709 | + def monitor_counter_response(self): |
2710 | + # FIXME: Should use a timeout with queue.get() |
2711 | + status = self.q.get() |
2712 | + log.info('Counter-response gave %r', status) |
2713 | + if status != 'response_ok': |
2714 | + log.error('Wrong counter-response!') |
2715 | + log.warning('Possible malicious peer: %r', self.peer) |
2716 | + GObject.timeout_add(500, self.on_counter_response, status) |
2717 | + |
2718 | + def on_counter_response(self, status): |
2719 | + self.thread.join() |
2720 | + self.thread = None |
2721 | + assert self.app.state == status |
2722 | + if status == 'response_ok': |
2723 | + self.thread = start_thread(self.request_cert) |
2724 | + self.service.Message(_('Requesting Certificate...')) |
2725 | + else: |
2726 | + self.service.Message(_('Scary! Counter-Challenge Failed!')) |
2727 | + |
2728 | + def request_cert(self): |
2729 | + log.info('Creating CSR') |
2730 | + success = False |
2731 | + try: |
2732 | + self.couch.pki.create_csr(self.cr.id) |
2733 | + csr_data = self.couch.pki.read_csr(self.cr.id) |
2734 | + obj = { |
2735 | + 'csr': b64encode(csr_data).decode('utf-8'), |
2736 | + 'mac': self.cr.csr_mac(csr_data), |
2737 | + } |
2738 | + d = self.client.post(obj, 'csr') |
2739 | + cert_data = b64decode(d['cert'].encode('utf-8')) |
2740 | + self.cr.check_cert_mac(cert_data, d['mac']) |
2741 | + self.couch.pki.write_cert(self.cr.id, self.cr.peer_id, cert_data) |
2742 | + self.couch.pki.verify_cert(self.cr.id, self.cr.peer_id) |
2743 | + key_data = b64decode(d['key'].encode('utf-8')) |
2744 | + self.couch.pki.write_key(self.cr.peer_id, key_data) |
2745 | + self.couch.pki.verify_key(self.cr.peer_id) |
2746 | + success = True |
2747 | + except Exception as e: |
2748 | + log.exception('Could not request cert') |
2749 | + GObject.idle_add(self.on_csr_response, success) |
2750 | + |
2751 | + def on_csr_response(self, success): |
2752 | + self.thread.join() |
2753 | + self.thread = None |
2754 | + if success: |
2755 | + self.service.set_user(self.cr.peer_id) |
2756 | + else: |
2757 | + self.service.Message(_('Scary! Certificate Request Failed!')) |
2758 | |
2759 | === modified file 'dmedia/service/tests/test_avahi.py' |
2760 | --- dmedia/service/tests/test_avahi.py 2012-10-04 20:23:22 +0000 |
2761 | +++ dmedia/service/tests/test_avahi.py 2012-10-16 12:35:25 +0000 |
2762 | @@ -23,32 +23,66 @@ |
2763 | Unit tests for `dmedia.service.avahi`. |
2764 | """ |
2765 | |
2766 | -from unittest import TestCase |
2767 | +from random import SystemRandom |
2768 | from copy import deepcopy |
2769 | |
2770 | -from microfiber import random_id |
2771 | -from usercouch import random_oauth |
2772 | +import microfiber |
2773 | |
2774 | +from dmedia.tests.couch import CouchCase |
2775 | from dmedia.service import avahi |
2776 | |
2777 | |
2778 | -class TestAvahi(TestCase): |
2779 | +random = SystemRandom() |
2780 | + |
2781 | + |
2782 | +def random_port(): |
2783 | + return random.randint(1001, 50000) |
2784 | + |
2785 | + |
2786 | +class TestAvahi(CouchCase): |
2787 | def test_init(self): |
2788 | - _id = random_id() |
2789 | - inst = avahi.Avahi(_id, 42) |
2790 | - self.assertEqual(inst.id, _id) |
2791 | - self.assertEqual(inst.port, 42) |
2792 | + db = microfiber.Database('dmedia-0', self.env) |
2793 | + self.assertTrue(db.ensure()) |
2794 | + port = random_port() |
2795 | + ssl_config = 'the SSL config' |
2796 | + inst = avahi.Avahi(self.env, port, ssl_config) |
2797 | self.assertIsNone(inst.group) |
2798 | - |
2799 | - def test_add_peer(self): |
2800 | - inst = avahi.Avahi('the id', 42) |
2801 | - with self.assertRaises(NotImplementedError) as cm: |
2802 | - inst.add_peer('key', 'url') |
2803 | - self.assertEqual(str(cm.exception), 'Avahi.add_peer()') |
2804 | - |
2805 | - def test_remove_peer(self): |
2806 | - inst = avahi.Avahi('the id', 42) |
2807 | - with self.assertRaises(NotImplementedError) as cm: |
2808 | - inst.remove_peer('key') |
2809 | - self.assertEqual(str(cm.exception), 'Avahi.remove_peer()') |
2810 | + self.assertIs(inst.machine_id, self.machine_id) |
2811 | + self.assertIs(inst.user_id, self.user_id) |
2812 | + self.assertIs(inst.port, port) |
2813 | + self.assertIs(inst.ssl_config, ssl_config) |
2814 | + self.assertIsInstance(inst.db, microfiber.Database) |
2815 | + self.assertIsInstance(inst.server, microfiber.Server) |
2816 | + self.assertIs(inst.db.ctx, inst.server.ctx) |
2817 | + self.assertEqual(inst.replications, {}) |
2818 | + self.assertEqual(inst.peers, |
2819 | + { |
2820 | + '_id': '_local/peers', |
2821 | + '_rev': '0-1', |
2822 | + 'peers': {}, |
2823 | + } |
2824 | + ) |
2825 | + self.assertEqual(db.get('_local/peers'), inst.peers) |
2826 | + |
2827 | + peers = deepcopy(inst.peers) |
2828 | + peers['peers'] = {'foo': 'bar'} |
2829 | + db.save(peers) |
2830 | + inst = avahi.Avahi(self.env, port, ssl_config) |
2831 | + self.assertEqual(inst.peers, |
2832 | + { |
2833 | + '_id': '_local/peers', |
2834 | + '_rev': '0-3', |
2835 | + 'peers': {}, |
2836 | + } |
2837 | + ) |
2838 | + inst = avahi.Avahi(self.env, port, ssl_config) |
2839 | + self.assertEqual(inst.peers, |
2840 | + { |
2841 | + '_id': '_local/peers', |
2842 | + '_rev': '0-3', |
2843 | + 'peers': {}, |
2844 | + } |
2845 | + ) |
2846 | + |
2847 | + inst.__del__() |
2848 | |
2849 | |
2850 | === modified file 'dmedia/startup.py' |
2851 | --- dmedia/startup.py 2012-10-03 19:45:56 +0000 |
2852 | +++ dmedia/startup.py 2012-10-16 12:35:25 +0000 |
2853 | @@ -79,29 +79,6 @@ |
2854 | } |
2855 | |
2856 | |
2857 | -def get_ssl_config(pki): |
2858 | - assert isinstance(pki, PKI) |
2859 | - if pki.user is None: |
2860 | - return None |
2861 | - return { |
2862 | - 'check_hostname': False, |
2863 | - 'max_depth': 1, |
2864 | - 'ca_file': pki.user.ca_file, |
2865 | - 'cert_file': pki.machine.cert_file, |
2866 | - 'key_file': pki.machine.key_file, |
2867 | - } |
2868 | - |
2869 | - |
2870 | -def get_bootstrap_config(pki): |
2871 | - assert isinstance(pki, PKI) |
2872 | - if pki.user is None: |
2873 | - return {'username': 'admin'} |
2874 | - return { |
2875 | - 'username': 'admin', |
2876 | - 'replicator': get_ssl_config(pki), |
2877 | - } |
2878 | - |
2879 | - |
2880 | class DmediaCouch(UserCouch): |
2881 | def __init__(self, basedir): |
2882 | super().__init__(basedir) |
2883 | @@ -116,44 +93,67 @@ |
2884 | save_config(path.join(self.basedir, name + '.json'), config) |
2885 | |
2886 | def isfirstrun(self): |
2887 | - return self.machine is None |
2888 | + return self.user is None |
2889 | |
2890 | - def firstrun_init(self, create_user=False): |
2891 | - if not self.isfirstrun(): |
2892 | - raise Exception('not first run, cannot call firstrun_init()') |
2893 | + def create_machine(self): |
2894 | + if self.machine is not None: |
2895 | + raise Exception('machine already exists') |
2896 | log.info('Creating RSA machine identity...') |
2897 | machine_id = self.pki.create_key() |
2898 | log.info('... machine_id: %s', machine_id) |
2899 | self.pki.create_ca(machine_id) |
2900 | - if create_user: |
2901 | - if self.user is None: |
2902 | - log.info('Creating RSA user identity...') |
2903 | - user_id = self.pki.create_key() |
2904 | - log.info('... user_id: %s', user_id) |
2905 | - self.pki.create_ca(user_id) |
2906 | - doc = create_doc(user_id, 'dmedia/user') |
2907 | - self.save_config('user', doc) |
2908 | - self.user = self.load_config('user') |
2909 | - else: |
2910 | - user_id = self.user['_id'] |
2911 | - self.pki.create_csr(machine_id) |
2912 | - self.pki.issue_cert(machine_id, user_id) |
2913 | doc = create_doc(machine_id, 'dmedia/machine') |
2914 | self.save_config('machine', doc) |
2915 | self.machine = self.load_config('machine') |
2916 | + return machine_id |
2917 | + |
2918 | + def create_user(self): |
2919 | + if self.machine is None: |
2920 | + raise Exception('must create machine first') |
2921 | + if self.user is not None: |
2922 | + raise Exception('user already exists') |
2923 | + log.info('Creating RSA user identity...') |
2924 | + user_id = self.pki.create_key() |
2925 | + log.info('... user_id: %s', user_id) |
2926 | + self.pki.create_ca(user_id) |
2927 | + self.pki.create_csr(self.machine['_id']) |
2928 | + self.pki.issue_cert(self.machine['_id'], user_id) |
2929 | + doc = create_doc(user_id, 'dmedia/user') |
2930 | + self.save_config('user', doc) |
2931 | + self.user = self.load_config('user') |
2932 | + return user_id |
2933 | + |
2934 | + def set_user(self, user_id): |
2935 | + log.info('... user_id: %s', user_id) |
2936 | + doc = create_doc(user_id, 'dmedia/user') |
2937 | + self.save_config('user', doc) |
2938 | + self.user = self.load_config('user') |
2939 | |
2940 | def load_pki(self): |
2941 | - if self.machine is None: |
2942 | - return |
2943 | if self.user is None: |
2944 | self.pki.load_pki(self.machine['_id']) |
2945 | else: |
2946 | self.pki.load_pki(self.machine['_id'], self.user['_id']) |
2947 | |
2948 | + def get_ssl_config(self): |
2949 | + return { |
2950 | + 'check_hostname': False, |
2951 | + 'max_depth': 1, |
2952 | + 'ca_file': self.pki.user.ca_file, |
2953 | + 'cert_file': self.pki.machine.cert_file, |
2954 | + 'key_file': self.pki.machine.key_file, |
2955 | + } |
2956 | + |
2957 | + def get_bootstrap_config(self): |
2958 | + return { |
2959 | + 'username': 'admin', |
2960 | + 'replicator': self.get_ssl_config(), |
2961 | + } |
2962 | + |
2963 | def auto_bootstrap(self): |
2964 | + assert self.user is not None |
2965 | + assert self.machine is not None |
2966 | self.load_pki() |
2967 | - config = get_bootstrap_config(self.pki) |
2968 | + config = self.get_bootstrap_config() |
2969 | return self.bootstrap('basic', config) |
2970 | |
2971 | - def get_ssl_config(self): |
2972 | - return get_ssl_config(self.pki) |
2973 | |
2974 | === modified file 'dmedia/tests/couch.py' |
2975 | --- dmedia/tests/couch.py 2011-10-04 14:00:22 +0000 |
2976 | +++ dmedia/tests/couch.py 2012-10-16 12:35:25 +0000 |
2977 | @@ -38,6 +38,8 @@ |
2978 | |
2979 | def setUp(self): |
2980 | super().setUp() |
2981 | - self.machine_id = random_id() |
2982 | + self.machine_id = random_id(30) |
2983 | + self.user_id = random_id(30) |
2984 | self.env['machine_id'] = self.machine_id |
2985 | + self.env['user_id'] = self.user_id |
2986 | |
2987 | |
2988 | === modified file 'dmedia/tests/test_core.py' |
2989 | --- dmedia/tests/test_core.py 2012-10-04 18:04:14 +0000 |
2990 | +++ dmedia/tests/test_core.py 2012-10-16 12:35:25 +0000 |
2991 | @@ -63,71 +63,14 @@ |
2992 | self.assertEqual(inst.local, {'_id': '_local/dmedia', 'stores': {}}) |
2993 | |
2994 | def test_load_identity(self): |
2995 | - id1 = random_id() |
2996 | - inst = core.Core(self.env) |
2997 | - inst.load_identity({'_id': id1}) |
2998 | - doc = inst.db.get(id1) |
2999 | - self.assertEqual(set(doc), set(['_id', '_rev'])) |
3000 | - self.assertTrue(doc['_rev'].startswith('1-')) |
3001 | - self.assertEqual( |
3002 | - inst.db.get('_local/dmedia'), |
3003 | - { |
3004 | - '_id': '_local/dmedia', |
3005 | - '_rev': '0-1', |
3006 | - 'stores': {}, |
3007 | - 'machine_id': id1, |
3008 | - } |
3009 | - ) |
3010 | - self.assertEqual(inst.local, inst.db.get('_local/dmedia')) |
3011 | - self.assertEqual(self.env['machine_id'], id1) |
3012 | - |
3013 | - inst.load_identity({'_id': id1}) |
3014 | - doc = inst.db.get(id1) |
3015 | - self.assertEqual(set(doc), set(['_id', '_rev'])) |
3016 | - self.assertTrue(doc['_rev'].startswith('1-')) |
3017 | - self.assertEqual( |
3018 | - inst.db.get('_local/dmedia'), |
3019 | - { |
3020 | - '_id': '_local/dmedia', |
3021 | - '_rev': '0-1', |
3022 | - 'stores': {}, |
3023 | - 'machine_id': id1, |
3024 | - } |
3025 | - ) |
3026 | - self.assertEqual(inst.local, inst.db.get('_local/dmedia')) |
3027 | - self.assertEqual(self.env['machine_id'], id1) |
3028 | - |
3029 | - id2 = random_id() |
3030 | - inst = core.Core(self.env) |
3031 | - inst.load_identity({'_id': id2}) |
3032 | - doc = inst.db.get(id2) |
3033 | - self.assertEqual(set(doc), set(['_id', '_rev'])) |
3034 | - self.assertTrue(doc['_rev'].startswith('1-')) |
3035 | - self.assertEqual( |
3036 | - inst.db.get('_local/dmedia'), |
3037 | - { |
3038 | - '_id': '_local/dmedia', |
3039 | - '_rev': '0-2', |
3040 | - 'stores': {}, |
3041 | - 'machine_id': id2, |
3042 | - } |
3043 | - ) |
3044 | - self.assertEqual(inst.local, inst.db.get('_local/dmedia')) |
3045 | - self.assertEqual(self.env['machine_id'], id2) |
3046 | - |
3047 | - def test_load_identity2(self): |
3048 | - """ |
3049 | - Test load_identity() with a user. |
3050 | - """ |
3051 | - machine_id = random_id() |
3052 | - user_id = random_id() |
3053 | + machine_id = random_id(30) |
3054 | + user_id = random_id(30) |
3055 | inst = core.Core(self.env) |
3056 | inst.load_identity({'_id': machine_id}, {'_id': user_id}) |
3057 | |
3058 | machine = inst.db.get(machine_id) |
3059 | self.assertEqual(set(machine), set(['_id', '_rev'])) |
3060 | self.assertTrue(machine['_rev'].startswith('1-')) |
3061 | - |
3062 | user = inst.db.get(user_id) |
3063 | self.assertEqual(set(user), set(['_id', '_rev'])) |
3064 | self.assertTrue(user['_rev'].startswith('1-')) |
3065 | @@ -147,6 +90,24 @@ |
3066 | self.assertEqual(self.env['machine_id'], machine_id) |
3067 | self.assertEqual(self.env['user_id'], user_id) |
3068 | |
3069 | + inst = core.Core(self.env) |
3070 | + inst.load_identity({'_id': machine_id}, {'_id': user_id}) |
3071 | + self.assertTrue(inst.db.get(machine_id)['_rev'].startswith('1-')) |
3072 | + self.assertTrue(inst.db.get(user_id)['_rev'].startswith('1-')) |
3073 | + |
3074 | + self.assertEqual(set(machine), set(['_id', '_rev'])) |
3075 | + self.assertTrue(machine['_rev'].startswith('1-')) |
3076 | + self.assertEqual( |
3077 | + inst.db.get('_local/dmedia'), |
3078 | + { |
3079 | + '_id': '_local/dmedia', |
3080 | + '_rev': '0-1', |
3081 | + 'stores': {}, |
3082 | + 'machine_id': machine_id, |
3083 | + 'user_id': user_id, |
3084 | + } |
3085 | + ) |
3086 | + |
3087 | def test_init_default_store(self): |
3088 | private = TempDir() |
3089 | shared = TempDir() |
3090 | |
3091 | === modified file 'dmedia/tests/test_peering.py' |
3092 | --- dmedia/tests/test_peering.py 2012-10-09 03:07:15 +0000 |
3093 | +++ dmedia/tests/test_peering.py 2012-10-16 12:35:25 +0000 |
3094 | @@ -25,207 +25,191 @@ |
3095 | |
3096 | from unittest import TestCase |
3097 | import os |
3098 | -from os import path |
3099 | +from os import path, urandom |
3100 | import subprocess |
3101 | import socket |
3102 | -from queue import Queue |
3103 | |
3104 | -import microfiber |
3105 | -from microfiber import random_id, CouchBase |
3106 | +from microfiber import random_id |
3107 | +from skein import skein512 |
3108 | |
3109 | from .base import TempDir |
3110 | -from dmedia.httpd import make_server |
3111 | from dmedia.peering import encode, decode |
3112 | from dmedia import peering |
3113 | |
3114 | |
3115 | -class TestSSLFunctions(TestCase): |
3116 | - def test_create_key(self): |
3117 | - tmp = TempDir() |
3118 | - key = tmp.join('key.pem') |
3119 | - |
3120 | - # bits=1024 |
3121 | - sizes = [883, 887, 891] |
3122 | - peering.create_key(key, bits=1024) |
3123 | - self.assertLess(min(sizes) - 25, path.getsize(key)) |
3124 | - self.assertLess(path.getsize(key), max(sizes) + 25) |
3125 | - os.remove(key) |
3126 | - |
3127 | - # bits=2048 (default) |
3128 | - sizes = [1671, 1675, 1679] |
3129 | - peering.create_key(key) |
3130 | - self.assertLess(min(sizes) - 25, path.getsize(key)) |
3131 | - self.assertLess(path.getsize(key), max(sizes) + 25) |
3132 | - os.remove(key) |
3133 | - |
3134 | - peering.create_key(key, bits=2048) |
3135 | - self.assertLess(min(sizes) - 25, path.getsize(key)) |
3136 | - self.assertLess(path.getsize(key), max(sizes) + 25) |
3137 | - os.remove(key) |
3138 | - |
3139 | - # bits=3072 |
3140 | - sizes = [2455, 2459] |
3141 | - peering.create_key(key, bits=3072) |
3142 | - self.assertLess(min(sizes) - 25, path.getsize(key)) |
3143 | - self.assertLess(path.getsize(key), max(sizes) + 25) |
3144 | - |
3145 | - def test_create_ca(self): |
3146 | - tmp = TempDir() |
3147 | - foo_key = tmp.join('foo.key') |
3148 | - foo_ca = tmp.join('foo.ca') |
3149 | - peering.create_key(foo_key) |
3150 | - self.assertFalse(path.exists(foo_ca)) |
3151 | - peering.create_ca(foo_key, '/CN=foo', foo_ca) |
3152 | - self.assertGreater(path.getsize(foo_ca), 0) |
3153 | - |
3154 | - def test_create_csr(self): |
3155 | - tmp = TempDir() |
3156 | - bar_key = tmp.join('bar.key') |
3157 | - bar_csr = tmp.join('bar.csr') |
3158 | - peering.create_key(bar_key) |
3159 | - self.assertFalse(path.exists(bar_csr)) |
3160 | - peering.create_csr(bar_key, '/CN=bar', bar_csr) |
3161 | - self.assertGreater(path.getsize(bar_csr), 0) |
3162 | - |
3163 | - def test_issue_cert(self): |
3164 | - tmp = TempDir() |
3165 | - |
3166 | - foo_key = tmp.join('foo.key') |
3167 | - foo_ca = tmp.join('foo.ca') |
3168 | - foo_srl = tmp.join('foo.srl') |
3169 | - peering.create_key(foo_key) |
3170 | - peering.create_ca(foo_key, '/CN=foo', foo_ca) |
3171 | - |
3172 | - bar_key = tmp.join('bar.key') |
3173 | - bar_csr = tmp.join('bar.csr') |
3174 | - bar_cert = tmp.join('bar.cert') |
3175 | - peering.create_key(bar_key) |
3176 | - peering.create_csr(bar_key, '/CN=bar', bar_csr) |
3177 | - |
3178 | - files = (foo_srl, bar_cert) |
3179 | - for f in files: |
3180 | - self.assertFalse(path.exists(f)) |
3181 | - peering.issue_cert(bar_csr, foo_ca, foo_key, foo_srl, bar_cert) |
3182 | - for f in files: |
3183 | - self.assertGreater(path.getsize(f), 0) |
3184 | - |
3185 | - def test_get_pubkey(self): |
3186 | - tmp = TempDir() |
3187 | - |
3188 | - # Create CA |
3189 | - foo_key = tmp.join('foo.key') |
3190 | - foo_ca = tmp.join('foo.ca') |
3191 | - foo_srl = tmp.join('foo.srl') |
3192 | - peering.create_key(foo_key) |
3193 | - foo_pubkey = peering.get_rsa_pubkey(foo_key) |
3194 | - peering.create_ca(foo_key, '/CN=foo', foo_ca) |
3195 | - |
3196 | - # Create CSR and issue cert |
3197 | - bar_key = tmp.join('bar.key') |
3198 | - bar_csr = tmp.join('bar.csr') |
3199 | - bar_cert = tmp.join('bar.cert') |
3200 | - peering.create_key(bar_key) |
3201 | - bar_pubkey = peering.get_rsa_pubkey(bar_key) |
3202 | - peering.create_csr(bar_key, '/CN=bar', bar_csr) |
3203 | - peering.issue_cert(bar_csr, foo_ca, foo_key, foo_srl, bar_cert) |
3204 | - |
3205 | - # Now compare |
3206 | - os.remove(foo_key) |
3207 | - os.remove(bar_key) |
3208 | - self.assertEqual(peering.get_pubkey(foo_ca), foo_pubkey) |
3209 | - self.assertEqual(peering.get_csr_pubkey(bar_csr), bar_pubkey) |
3210 | - self.assertEqual(peering.get_pubkey(bar_cert), bar_pubkey) |
3211 | - |
3212 | - def test_get_subject(self): |
3213 | - tmp = TempDir() |
3214 | - |
3215 | - foo_subject = '/CN={}'.format(random_id(30)) |
3216 | - foo_key = tmp.join('foo.key') |
3217 | - foo_ca = tmp.join('foo.ca') |
3218 | - foo_srl = tmp.join('foo.srl') |
3219 | - peering.create_key(foo_key) |
3220 | - peering.create_ca(foo_key, foo_subject, foo_ca) |
3221 | - self.assertEqual(peering.get_subject(foo_ca), foo_subject) |
3222 | - |
3223 | - bar_subject = '/CN={}'.format(random_id(30)) |
3224 | - bar_key = tmp.join('bar.key') |
3225 | - bar_csr = tmp.join('bar.csr') |
3226 | - bar_cert = tmp.join('bar.cert') |
3227 | - peering.create_key(bar_key) |
3228 | - peering.create_csr(bar_key, bar_subject, bar_csr) |
3229 | - peering.issue_cert(bar_csr, foo_ca, foo_key, foo_srl, bar_cert) |
3230 | - self.assertEqual(peering.get_csr_subject(bar_csr), bar_subject) |
3231 | - self.assertEqual(peering.get_subject(bar_cert), bar_subject) |
3232 | - |
3233 | - def test_get_csr_subject(self): |
3234 | - tmp = TempDir() |
3235 | - subject = '/CN={}'.format(random_id(30)) |
3236 | - key_file = tmp.join('foo.key') |
3237 | - csr_file = tmp.join('foo.csr') |
3238 | - peering.create_key(key_file) |
3239 | - peering.create_csr(key_file, subject, csr_file) |
3240 | - os.remove(key_file) |
3241 | - self.assertEqual(peering.get_csr_subject(csr_file), subject) |
3242 | - |
3243 | - def test_get_issuer(self): |
3244 | - tmp = TempDir() |
3245 | - |
3246 | - foo_subject = '/CN={}'.format(random_id(30)) |
3247 | - foo_key = tmp.join('foo.key') |
3248 | - foo_ca = tmp.join('foo.ca') |
3249 | - foo_srl = tmp.join('foo.srl') |
3250 | - peering.create_key(foo_key) |
3251 | - peering.create_ca(foo_key, foo_subject, foo_ca) |
3252 | - self.assertEqual(peering.get_issuer(foo_ca), foo_subject) |
3253 | - |
3254 | - bar_subject = '/CN={}'.format(random_id(30)) |
3255 | - bar_key = tmp.join('bar.key') |
3256 | - bar_csr = tmp.join('bar.csr') |
3257 | - bar_cert = tmp.join('bar.cert') |
3258 | - peering.create_key(bar_key) |
3259 | - peering.create_csr(bar_key, bar_subject, bar_csr) |
3260 | - peering.issue_cert(bar_csr, foo_ca, foo_key, foo_srl, bar_cert) |
3261 | - self.assertEqual(peering.get_csr_subject(bar_csr), bar_subject) |
3262 | - self.assertEqual(peering.get_issuer(bar_cert), foo_subject) |
3263 | - |
3264 | - def test_ssl_verify(self): |
3265 | - tmp = TempDir() |
3266 | - pki = peering.PKI(tmp.dir) |
3267 | - |
3268 | - ca1 = pki.create_key() |
3269 | - pki.create_ca(ca1) |
3270 | - cert1 = pki.create_key() |
3271 | - pki.create_csr(cert1) |
3272 | - pki.issue_cert(cert1, ca1) |
3273 | - ca1_file = pki.path(ca1, 'ca') |
3274 | - cert1_file = pki.path(cert1, 'cert') |
3275 | - self.assertEqual(peering.ssl_verify(ca1_file, ca1_file), ca1_file) |
3276 | - self.assertEqual(peering.ssl_verify(cert1_file, ca1_file), cert1_file) |
3277 | - with self.assertRaises(peering.VerificationError) as cm: |
3278 | - peering.ssl_verify(ca1_file, cert1_file) |
3279 | - with self.assertRaises(peering.VerificationError) as cm: |
3280 | - peering.ssl_verify(cert1_file, cert1_file) |
3281 | - |
3282 | - ca2 = pki.create_key() |
3283 | - pki.create_ca(ca2) |
3284 | - cert2 = pki.create_key() |
3285 | - pki.create_csr(cert2) |
3286 | - pki.issue_cert(cert2, ca2) |
3287 | - ca2_file = pki.path(ca2, 'ca') |
3288 | - cert2_file = pki.path(cert2, 'cert') |
3289 | - self.assertEqual(peering.ssl_verify(ca2_file, ca2_file), ca2_file) |
3290 | - self.assertEqual(peering.ssl_verify(cert2_file, ca2_file), cert2_file) |
3291 | - with self.assertRaises(peering.VerificationError) as cm: |
3292 | - peering.ssl_verify(ca2_file, cert2_file) |
3293 | - with self.assertRaises(peering.VerificationError) as cm: |
3294 | - peering.ssl_verify(cert2_file, cert2_file) |
3295 | - |
3296 | - with self.assertRaises(peering.VerificationError) as cm: |
3297 | - peering.ssl_verify(ca2_file, ca1_file) |
3298 | - with self.assertRaises(peering.VerificationError) as cm: |
3299 | - peering.ssl_verify(cert2_file, ca1_file) |
3300 | - with self.assertRaises(peering.VerificationError) as cm: |
3301 | - peering.ssl_verify(cert2_file, cert1_file) |
3302 | +class TestSkeinFunctions(TestCase): |
3303 | + def test_hash_pubkey(self): |
3304 | + data = urandom(500) |
3305 | + _id = peering.hash_pubkey(data) |
3306 | + self.assertIsInstance(_id, str) |
3307 | + self.assertEqual(len(_id), 48) |
3308 | + skein = skein512(data, |
3309 | + digest_bits=240, |
3310 | + pers=b'20120918 jderose@novacut.com dmedia/pubkey', |
3311 | + ) |
3312 | + self.assertEqual( |
3313 | + decode(_id), |
3314 | + skein.digest() |
3315 | + ) |
3316 | + |
3317 | + # Sanity check |
3318 | + for i in range(1000): |
3319 | + self.assertNotEqual(_id, |
3320 | + peering.hash_pubkey(urandom(500)), |
3321 | + ) |
3322 | + |
3323 | + def test_compute_response(self): |
3324 | + secret = urandom(5) |
3325 | + challenge = urandom(20) |
3326 | + nonce = urandom(20) |
3327 | + hash1 = urandom(30) |
3328 | + hash2 = urandom(30) |
3329 | + response = peering.compute_response( |
3330 | + secret, challenge, nonce, hash1, hash2 |
3331 | + ) |
3332 | + self.assertIsInstance(response, str) |
3333 | + self.assertEqual(len(response), 56) |
3334 | + skein = skein512(hash1 + hash2, |
3335 | + digest_bits=280, |
3336 | + pers=b'20120918 jderose@novacut.com dmedia/response', |
3337 | + key=secret, |
3338 | + nonce=(challenge + nonce), |
3339 | + ) |
3340 | + self.assertEqual( |
3341 | + decode(response), |
3342 | + skein.digest() |
3343 | + ) |
3344 | + |
3345 | + # Test with direction reversed |
3346 | + self.assertNotEqual(response, |
3347 | + peering.compute_response(secret, challenge, nonce, hash2, hash1) |
3348 | + ) |
3349 | + |
3350 | + # Test with wrong secret |
3351 | + for i in range(100): |
3352 | + self.assertNotEqual(response, |
3353 | + peering.compute_response(urandom(5), challenge, nonce, hash1, hash2) |
3354 | + ) |
3355 | + |
3356 | + # Test with wrong challange |
3357 | + for i in range(100): |
3358 | + self.assertNotEqual(response, |
3359 | + peering.compute_response(secret, urandom(20), nonce, hash1, hash2) |
3360 | + ) |
3361 | + |
3362 | + # Test with wrong nonce |
3363 | + for i in range(100): |
3364 | + self.assertNotEqual(response, |
3365 | + peering.compute_response(secret, challenge, urandom(20), hash1, hash2) |
3366 | + ) |
3367 | + |
3368 | + # Test with wrong challenger_hash |
3369 | + for i in range(100): |
3370 | + self.assertNotEqual(response, |
3371 | + peering.compute_response(secret, challenge, nonce, urandom(30), hash2) |
3372 | + ) |
3373 | + |
3374 | + # Test with wrong responder_hash |
3375 | + for i in range(100): |
3376 | + self.assertNotEqual(response, |
3377 | + peering.compute_response(secret, challenge, nonce, hash1, urandom(30)) |
3378 | + ) |
3379 | + |
3380 | + def test_compute_csr_mac(self): |
3381 | + secret = os.urandom(5) |
3382 | + remote_hash = os.urandom(30) |
3383 | + local_hash = os.urandom(30) |
3384 | + csr_data = os.urandom(500) |
3385 | + mac = peering.compute_csr_mac(secret, csr_data, remote_hash, local_hash) |
3386 | + self.assertIsInstance(mac, str) |
3387 | + self.assertEqual(len(mac), 56) |
3388 | + skein = skein512(csr_data, |
3389 | + digest_bits=280, |
3390 | + pers=b'20120918 jderose@novacut.com dmedia/csr', |
3391 | + key=secret, |
3392 | + key_id=(remote_hash + local_hash), |
3393 | + ) |
3394 | + self.assertEqual( |
3395 | + decode(mac), |
3396 | + skein.digest() |
3397 | + ) |
3398 | + |
3399 | + # Test with direction reversed |
3400 | + self.assertNotEqual(mac, |
3401 | + peering.compute_csr_mac(secret, csr_data, local_hash, remote_hash) |
3402 | + ) |
3403 | + |
3404 | + # Test with wrong secret |
3405 | + for i in range(100): |
3406 | + self.assertNotEqual(mac, |
3407 | + peering.compute_csr_mac(os.urandom(5), csr_data, remote_hash, local_hash) |
3408 | + ) |
3409 | + |
3410 | + # Test with wrong cert_data |
3411 | + for i in range(100): |
3412 | + self.assertNotEqual(mac, |
3413 | + peering.compute_csr_mac(secret, os.urandom(500), remote_hash, local_hash) |
3414 | + ) |
3415 | + |
3416 | + # Test with wrong remote_hash |
3417 | + for i in range(100): |
3418 | + self.assertNotEqual(mac, |
3419 | + peering.compute_csr_mac(secret, csr_data, os.urandom(30), local_hash) |
3420 | + ) |
3421 | + |
3422 | + # Test with wrong local_hash |
3423 | + for i in range(100): |
3424 | + self.assertNotEqual(mac, |
3425 | + peering.compute_csr_mac(secret, csr_data, remote_hash, os.urandom(30)) |
3426 | + ) |
3427 | + |
3428 | + def test_compute_cert_mac(self): |
3429 | + secret = os.urandom(5) |
3430 | + remote_hash = os.urandom(30) |
3431 | + local_hash = os.urandom(30) |
3432 | + cert_data = os.urandom(500) |
3433 | + mac = peering.compute_cert_mac(secret, cert_data, remote_hash, local_hash) |
3434 | + self.assertIsInstance(mac, str) |
3435 | + self.assertEqual(len(mac), 56) |
3436 | + skein = skein512(cert_data, |
3437 | + digest_bits=280, |
3438 | + pers=b'20120918 jderose@novacut.com dmedia/cert', |
3439 | + key=secret, |
3440 | + key_id=(remote_hash + local_hash), |
3441 | + ) |
3442 | + self.assertEqual( |
3443 | + decode(mac), |
3444 | + skein.digest() |
3445 | + ) |
3446 | + |
3447 | + # Test with direction reversed |
3448 | + self.assertNotEqual(mac, |
3449 | + peering.compute_cert_mac(secret, cert_data, local_hash, remote_hash) |
3450 | + ) |
3451 | + |
3452 | + # Test with wrong secret |
3453 | + for i in range(100): |
3454 | + self.assertNotEqual(mac, |
3455 | + peering.compute_cert_mac(os.urandom(5), cert_data, remote_hash, local_hash) |
3456 | + ) |
3457 | + |
3458 | + # Test with wrong cert_data |
3459 | + for i in range(100): |
3460 | + self.assertNotEqual(mac, |
3461 | + peering.compute_cert_mac(secret, os.urandom(500), remote_hash, local_hash) |
3462 | + ) |
3463 | + |
3464 | + # Test with wrong remote_hash |
3465 | + for i in range(100): |
3466 | + self.assertNotEqual(mac, |
3467 | + peering.compute_cert_mac(secret, cert_data, os.urandom(30), local_hash) |
3468 | + ) |
3469 | + |
3470 | + # Test with wrong local_hash |
3471 | + for i in range(100): |
3472 | + self.assertNotEqual(mac, |
3473 | + peering.compute_cert_mac(secret, cert_data, remote_hash, os.urandom(30)) |
3474 | + ) |
3475 | |
3476 | |
3477 | class TestChallengeResponse(TestCase): |
3478 | @@ -478,133 +462,193 @@ |
3479 | inst.check_response(nonce, response) |
3480 | |
3481 | |
3482 | -class TestServerApp(TestCase): |
3483 | - def test_live(self): |
3484 | +class TestSSLFunctions(TestCase): |
3485 | + def test_create_key(self): |
3486 | + tmp = TempDir() |
3487 | + key = tmp.join('key.pem') |
3488 | + |
3489 | + # bits=1024 |
3490 | + sizes = [883, 887, 891] |
3491 | + peering.create_key(key, bits=1024) |
3492 | + self.assertLess(min(sizes) - 25, path.getsize(key)) |
3493 | + self.assertLess(path.getsize(key), max(sizes) + 25) |
3494 | + os.remove(key) |
3495 | + |
3496 | + # bits=2048 (default) |
3497 | + sizes = [1671, 1675, 1679] |
3498 | + peering.create_key(key) |
3499 | + self.assertLess(min(sizes) - 25, path.getsize(key)) |
3500 | + self.assertLess(path.getsize(key), max(sizes) + 25) |
3501 | + os.remove(key) |
3502 | + |
3503 | + peering.create_key(key, bits=2048) |
3504 | + self.assertLess(min(sizes) - 25, path.getsize(key)) |
3505 | + self.assertLess(path.getsize(key), max(sizes) + 25) |
3506 | + os.remove(key) |
3507 | + |
3508 | + # bits=3072 |
3509 | + sizes = [2455, 2459] |
3510 | + peering.create_key(key, bits=3072) |
3511 | + self.assertLess(min(sizes) - 25, path.getsize(key)) |
3512 | + self.assertLess(path.getsize(key), max(sizes) + 25) |
3513 | + |
3514 | + def test_create_ca(self): |
3515 | + tmp = TempDir() |
3516 | + foo_key = tmp.join('foo.key') |
3517 | + foo_ca = tmp.join('foo.ca') |
3518 | + peering.create_key(foo_key) |
3519 | + self.assertFalse(path.exists(foo_ca)) |
3520 | + peering.create_ca(foo_key, '/CN=foo', foo_ca) |
3521 | + self.assertGreater(path.getsize(foo_ca), 0) |
3522 | + |
3523 | + def test_create_csr(self): |
3524 | + tmp = TempDir() |
3525 | + bar_key = tmp.join('bar.key') |
3526 | + bar_csr = tmp.join('bar.csr') |
3527 | + peering.create_key(bar_key) |
3528 | + self.assertFalse(path.exists(bar_csr)) |
3529 | + peering.create_csr(bar_key, '/CN=bar', bar_csr) |
3530 | + self.assertGreater(path.getsize(bar_csr), 0) |
3531 | + |
3532 | + def test_issue_cert(self): |
3533 | + tmp = TempDir() |
3534 | + |
3535 | + foo_key = tmp.join('foo.key') |
3536 | + foo_ca = tmp.join('foo.ca') |
3537 | + foo_srl = tmp.join('foo.srl') |
3538 | + peering.create_key(foo_key) |
3539 | + peering.create_ca(foo_key, '/CN=foo', foo_ca) |
3540 | + |
3541 | + bar_key = tmp.join('bar.key') |
3542 | + bar_csr = tmp.join('bar.csr') |
3543 | + bar_cert = tmp.join('bar.cert') |
3544 | + peering.create_key(bar_key) |
3545 | + peering.create_csr(bar_key, '/CN=bar', bar_csr) |
3546 | + |
3547 | + files = (foo_srl, bar_cert) |
3548 | + for f in files: |
3549 | + self.assertFalse(path.exists(f)) |
3550 | + peering.issue_cert(bar_csr, foo_ca, foo_key, foo_srl, bar_cert) |
3551 | + for f in files: |
3552 | + self.assertGreater(path.getsize(f), 0) |
3553 | + |
3554 | + def test_get_pubkey(self): |
3555 | + tmp = TempDir() |
3556 | + |
3557 | + # Create CA |
3558 | + foo_key = tmp.join('foo.key') |
3559 | + foo_ca = tmp.join('foo.ca') |
3560 | + foo_srl = tmp.join('foo.srl') |
3561 | + peering.create_key(foo_key) |
3562 | + foo_pubkey = peering.get_rsa_pubkey(foo_key) |
3563 | + peering.create_ca(foo_key, '/CN=foo', foo_ca) |
3564 | + |
3565 | + # Create CSR and issue cert |
3566 | + bar_key = tmp.join('bar.key') |
3567 | + bar_csr = tmp.join('bar.csr') |
3568 | + bar_cert = tmp.join('bar.cert') |
3569 | + peering.create_key(bar_key) |
3570 | + bar_pubkey = peering.get_rsa_pubkey(bar_key) |
3571 | + peering.create_csr(bar_key, '/CN=bar', bar_csr) |
3572 | + peering.issue_cert(bar_csr, foo_ca, foo_key, foo_srl, bar_cert) |
3573 | + |
3574 | + # Now compare |
3575 | + os.remove(foo_key) |
3576 | + os.remove(bar_key) |
3577 | + self.assertEqual(peering.get_pubkey(foo_ca), foo_pubkey) |
3578 | + self.assertEqual(peering.get_csr_pubkey(bar_csr), bar_pubkey) |
3579 | + self.assertEqual(peering.get_pubkey(bar_cert), bar_pubkey) |
3580 | + |
3581 | + def test_get_subject(self): |
3582 | + tmp = TempDir() |
3583 | + |
3584 | + foo_subject = '/CN={}'.format(random_id(30)) |
3585 | + foo_key = tmp.join('foo.key') |
3586 | + foo_ca = tmp.join('foo.ca') |
3587 | + foo_srl = tmp.join('foo.srl') |
3588 | + peering.create_key(foo_key) |
3589 | + peering.create_ca(foo_key, foo_subject, foo_ca) |
3590 | + self.assertEqual(peering.get_subject(foo_ca), foo_subject) |
3591 | + |
3592 | + bar_subject = '/CN={}'.format(random_id(30)) |
3593 | + bar_key = tmp.join('bar.key') |
3594 | + bar_csr = tmp.join('bar.csr') |
3595 | + bar_cert = tmp.join('bar.cert') |
3596 | + peering.create_key(bar_key) |
3597 | + peering.create_csr(bar_key, bar_subject, bar_csr) |
3598 | + peering.issue_cert(bar_csr, foo_ca, foo_key, foo_srl, bar_cert) |
3599 | + self.assertEqual(peering.get_csr_subject(bar_csr), bar_subject) |
3600 | + self.assertEqual(peering.get_subject(bar_cert), bar_subject) |
3601 | + |
3602 | + def test_get_csr_subject(self): |
3603 | + tmp = TempDir() |
3604 | + subject = '/CN={}'.format(random_id(30)) |
3605 | + key_file = tmp.join('foo.key') |
3606 | + csr_file = tmp.join('foo.csr') |
3607 | + peering.create_key(key_file) |
3608 | + peering.create_csr(key_file, subject, csr_file) |
3609 | + os.remove(key_file) |
3610 | + self.assertEqual(peering.get_csr_subject(csr_file), subject) |
3611 | + |
3612 | + def test_get_issuer(self): |
3613 | + tmp = TempDir() |
3614 | + |
3615 | + foo_subject = '/CN={}'.format(random_id(30)) |
3616 | + foo_key = tmp.join('foo.key') |
3617 | + foo_ca = tmp.join('foo.ca') |
3618 | + foo_srl = tmp.join('foo.srl') |
3619 | + peering.create_key(foo_key) |
3620 | + peering.create_ca(foo_key, foo_subject, foo_ca) |
3621 | + self.assertEqual(peering.get_issuer(foo_ca), foo_subject) |
3622 | + |
3623 | + bar_subject = '/CN={}'.format(random_id(30)) |
3624 | + bar_key = tmp.join('bar.key') |
3625 | + bar_csr = tmp.join('bar.csr') |
3626 | + bar_cert = tmp.join('bar.cert') |
3627 | + peering.create_key(bar_key) |
3628 | + peering.create_csr(bar_key, bar_subject, bar_csr) |
3629 | + peering.issue_cert(bar_csr, foo_ca, foo_key, foo_srl, bar_cert) |
3630 | + self.assertEqual(peering.get_csr_subject(bar_csr), bar_subject) |
3631 | + self.assertEqual(peering.get_issuer(bar_cert), foo_subject) |
3632 | + |
3633 | + def test_ssl_verify(self): |
3634 | tmp = TempDir() |
3635 | pki = peering.PKI(tmp.dir) |
3636 | - local_id = pki.create_key() |
3637 | - pki.create_ca(local_id) |
3638 | - remote_id = pki.create_key() |
3639 | - pki.create_ca(remote_id) |
3640 | - server_config = { |
3641 | - 'cert_file': pki.path(local_id, 'ca'), |
3642 | - 'key_file': pki.path(local_id, 'key'), |
3643 | - 'ca_file': pki.path(remote_id, 'ca'), |
3644 | - } |
3645 | - client_config = { |
3646 | - 'check_hostname': False, |
3647 | - 'ca_file': pki.path(local_id, 'ca'), |
3648 | - 'cert_file': pki.path(remote_id, 'ca'), |
3649 | - 'key_file': pki.path(remote_id, 'key'), |
3650 | - } |
3651 | - local = peering.ChallengeResponse(local_id, remote_id) |
3652 | - remote = peering.ChallengeResponse(remote_id, local_id) |
3653 | - q = Queue() |
3654 | - app = peering.ServerApp(local, q, None) |
3655 | - server = make_server(app, '127.0.0.1', server_config) |
3656 | - client = CouchBase({'url': server.url, 'ssl': client_config}) |
3657 | - server.start() |
3658 | - secret = local.get_secret() |
3659 | - remote.set_secret(secret) |
3660 | - |
3661 | - self.assertIsNone(app.state) |
3662 | - with self.assertRaises(microfiber.BadRequest) as cm: |
3663 | - client.get('') |
3664 | - self.assertEqual( |
3665 | - str(cm.exception), |
3666 | - '400 Bad Request State: GET /' |
3667 | - ) |
3668 | - app.state = 'info' |
3669 | - self.assertEqual(client.get(), |
3670 | - { |
3671 | - 'id': local_id, |
3672 | - 'user': os.environ.get('USER'), |
3673 | - 'host': socket.gethostname(), |
3674 | - } |
3675 | - ) |
3676 | - self.assertEqual(app.state, 'ready') |
3677 | - with self.assertRaises(microfiber.BadRequest) as cm: |
3678 | - client.get('') |
3679 | - self.assertEqual( |
3680 | - str(cm.exception), |
3681 | - '400 Bad Request State: GET /' |
3682 | - ) |
3683 | - self.assertEqual(app.state, 'ready') |
3684 | - |
3685 | - app.state = 'info' |
3686 | - with self.assertRaises(microfiber.BadRequest) as cm: |
3687 | - client.get('challenge') |
3688 | - self.assertEqual( |
3689 | - str(cm.exception), |
3690 | - '400 Bad Request Order: GET /challenge' |
3691 | - ) |
3692 | - with self.assertRaises(microfiber.BadRequest) as cm: |
3693 | - client.put({'hello': 'world'}, 'response') |
3694 | - self.assertEqual( |
3695 | - str(cm.exception), |
3696 | - '400 Bad Request Order: PUT /response' |
3697 | - ) |
3698 | - |
3699 | - app.state = 'ready' |
3700 | - self.assertEqual(app.state, 'ready') |
3701 | - obj = client.get('challenge') |
3702 | - self.assertEqual(app.state, 'gave_challenge') |
3703 | - self.assertIsInstance(obj, dict) |
3704 | - self.assertEqual(set(obj), set(['challenge'])) |
3705 | - self.assertEqual(local.challenge, decode(obj['challenge'])) |
3706 | - with self.assertRaises(microfiber.BadRequest) as cm: |
3707 | - client.get('challenge') |
3708 | - self.assertEqual( |
3709 | - str(cm.exception), |
3710 | - '400 Bad Request Order: GET /challenge' |
3711 | - ) |
3712 | - self.assertEqual(app.state, 'gave_challenge') |
3713 | - |
3714 | - (nonce, response) = remote.create_response(obj['challenge']) |
3715 | - obj = {'nonce': nonce, 'response': response} |
3716 | - self.assertEqual(client.put(obj, 'response'), {'ok': True}) |
3717 | - self.assertEqual(app.state, 'response_ok') |
3718 | - with self.assertRaises(microfiber.BadRequest) as cm: |
3719 | - client.put(obj, 'response') |
3720 | - self.assertEqual( |
3721 | - str(cm.exception), |
3722 | - '400 Bad Request Order: PUT /response' |
3723 | - ) |
3724 | - self.assertEqual(app.state, 'response_ok') |
3725 | - self.assertEqual(q.get(), 'response_ok') |
3726 | - |
3727 | - # Test when an error occurs in put_response() |
3728 | - app.state = 'gave_challenge' |
3729 | - with self.assertRaises(microfiber.ServerError) as cm: |
3730 | - client.put(b'bad json', 'response') |
3731 | - self.assertEqual(app.state, 'in_response') |
3732 | - |
3733 | - # Test with wrong secret |
3734 | - app.state = 'ready' |
3735 | - secret = local.get_secret() |
3736 | - remote.get_secret() |
3737 | - challenge = client.get('challenge')['challenge'] |
3738 | - self.assertEqual(app.state, 'gave_challenge') |
3739 | - (nonce, response) = remote.create_response(challenge) |
3740 | - with self.assertRaises(microfiber.Unauthorized) as cm: |
3741 | - client.put({'nonce': nonce, 'response': response}, 'response') |
3742 | - self.assertEqual(app.state, 'wrong_response') |
3743 | - self.assertFalse(hasattr(local, 'secret')) |
3744 | - self.assertFalse(hasattr(local, 'challenge')) |
3745 | - |
3746 | - # Verify that you can't retry |
3747 | - remote.set_secret(secret) |
3748 | - (nonce, response) = remote.create_response(challenge) |
3749 | - with self.assertRaises(microfiber.BadRequest) as cm: |
3750 | - client.put({'nonce': nonce, 'response': response}, 'response') |
3751 | - self.assertEqual( |
3752 | - str(cm.exception), |
3753 | - '400 Bad Request Order: PUT /response' |
3754 | - ) |
3755 | - self.assertEqual(app.state, 'wrong_response') |
3756 | - self.assertEqual(q.get(), 'wrong_response') |
3757 | - |
3758 | - server.shutdown() |
3759 | + |
3760 | + ca1 = pki.create_key() |
3761 | + pki.create_ca(ca1) |
3762 | + cert1 = pki.create_key() |
3763 | + pki.create_csr(cert1) |
3764 | + pki.issue_cert(cert1, ca1) |
3765 | + ca1_file = pki.path(ca1, 'ca') |
3766 | + cert1_file = pki.path(cert1, 'cert') |
3767 | + self.assertEqual(peering.ssl_verify(ca1_file, ca1_file), ca1_file) |
3768 | + self.assertEqual(peering.ssl_verify(cert1_file, ca1_file), cert1_file) |
3769 | + with self.assertRaises(peering.VerificationError) as cm: |
3770 | + peering.ssl_verify(ca1_file, cert1_file) |
3771 | + with self.assertRaises(peering.VerificationError) as cm: |
3772 | + peering.ssl_verify(cert1_file, cert1_file) |
3773 | + |
3774 | + ca2 = pki.create_key() |
3775 | + pki.create_ca(ca2) |
3776 | + cert2 = pki.create_key() |
3777 | + pki.create_csr(cert2) |
3778 | + pki.issue_cert(cert2, ca2) |
3779 | + ca2_file = pki.path(ca2, 'ca') |
3780 | + cert2_file = pki.path(cert2, 'cert') |
3781 | + self.assertEqual(peering.ssl_verify(ca2_file, ca2_file), ca2_file) |
3782 | + self.assertEqual(peering.ssl_verify(cert2_file, ca2_file), cert2_file) |
3783 | + with self.assertRaises(peering.VerificationError) as cm: |
3784 | + peering.ssl_verify(ca2_file, cert2_file) |
3785 | + with self.assertRaises(peering.VerificationError) as cm: |
3786 | + peering.ssl_verify(cert2_file, cert2_file) |
3787 | + |
3788 | + with self.assertRaises(peering.VerificationError) as cm: |
3789 | + peering.ssl_verify(ca2_file, ca1_file) |
3790 | + with self.assertRaises(peering.VerificationError) as cm: |
3791 | + peering.ssl_verify(cert2_file, ca1_file) |
3792 | + with self.assertRaises(peering.VerificationError) as cm: |
3793 | + peering.ssl_verify(cert2_file, cert1_file) |
3794 | |
3795 | |
3796 | class TestPKI(TestCase): |
3797 | @@ -935,17 +979,17 @@ |
3798 | |
3799 | cert1_file = pki.path(id1, 'cert') |
3800 | cert2_file = pki.path(id2, 'cert') |
3801 | - self.assertEqual(pki.verify_cert(id1), cert1_file) |
3802 | - self.assertEqual(pki.verify_cert(id2), cert2_file) |
3803 | + self.assertEqual(pki.verify_cert(id1, ca_id), cert1_file) |
3804 | + self.assertEqual(pki.verify_cert(id2, ca_id), cert2_file) |
3805 | os.remove(cert1_file) |
3806 | os.rename(cert2_file, cert1_file) |
3807 | with self.assertRaises(peering.PublicKeyError) as cm: |
3808 | - pki.verify_cert(id1) |
3809 | + pki.verify_cert(id1, ca_id) |
3810 | self.assertEqual(cm.exception.filename, cert1_file) |
3811 | self.assertEqual(cm.exception.expected, id1) |
3812 | self.assertEqual(cm.exception.got, id2) |
3813 | with self.assertRaises(subprocess.CalledProcessError) as cm: |
3814 | - pki.verify_cert(id2) |
3815 | + pki.verify_cert(id2, ca_id) |
3816 | |
3817 | # Test with bad subject |
3818 | id3 = pki.create_key() |
3819 | @@ -960,7 +1004,7 @@ |
3820 | cert_file |
3821 | ) |
3822 | with self.assertRaises(peering.SubjectError) as cm: |
3823 | - pki.verify_cert(id3) |
3824 | + pki.verify_cert(id3, ca_id) |
3825 | self.assertEqual(cm.exception.filename, cert_file) |
3826 | self.assertEqual(cm.exception.expected, '/CN={}'.format(id3)) |
3827 | self.assertEqual(cm.exception.got, '/CN={}'.format(id1)) |
3828 | @@ -968,7 +1012,7 @@ |
3829 | def test_get_ca(self): |
3830 | tmp = TempDir() |
3831 | pki = peering.PKI(tmp.dir) |
3832 | - ca_id = pki.create_key() |
3833 | + ca_id = pki.create_key(1024) |
3834 | pki.create_ca(ca_id) |
3835 | |
3836 | ca = pki.get_ca(ca_id) |
3837 | @@ -986,13 +1030,64 @@ |
3838 | pki.create_csr(cert_id) |
3839 | pki.issue_cert(cert_id, ca_id) |
3840 | |
3841 | - cert = pki.get_cert(cert_id) |
3842 | + cert = pki.get_cert(cert_id, ca_id) |
3843 | self.assertIsInstance(cert, peering.Cert) |
3844 | self.assertEqual(cert.id, cert_id) |
3845 | self.assertEqual(cert.cert_file, pki.path(cert_id, 'cert')) |
3846 | self.assertEqual(cert.key_file, pki.path(cert_id, 'key')) |
3847 | self.assertEqual(cert, (cert.id, cert.cert_file, cert.key_file)) |
3848 | |
3849 | + def test_load_machine(self): |
3850 | + tmp = TempDir() |
3851 | + pki = peering.PKI(tmp.dir) |
3852 | + machine_id = pki.create_key() |
3853 | + pki.create_ca(machine_id) |
3854 | + |
3855 | + machine = pki.load_machine(machine_id) |
3856 | + self.assertIsInstance(machine, peering.Machine) |
3857 | + self.assertEqual(machine.id, machine_id) |
3858 | + self.assertEqual(machine.ca_file, pki.path(machine_id, 'ca')) |
3859 | + self.assertEqual(machine.key_file, pki.path(machine_id, 'key')) |
3860 | + self.assertIsNone(machine.cert_file) |
3861 | + self.assertEqual(machine, |
3862 | + (machine.id, machine.ca_file, machine.key_file, None) |
3863 | + ) |
3864 | + |
3865 | + user_id = pki.create_key() |
3866 | + pki.create_ca(user_id) |
3867 | + pki.create_csr(machine_id) |
3868 | + pki.issue_cert(machine_id, user_id) |
3869 | + machine = pki.load_machine(machine_id, user_id) |
3870 | + self.assertIsInstance(machine, peering.Machine) |
3871 | + self.assertEqual(machine.id, machine_id) |
3872 | + self.assertEqual(machine.ca_file, pki.path(machine_id, 'ca')) |
3873 | + self.assertEqual(machine.key_file, pki.path(machine_id, 'key')) |
3874 | + self.assertEqual(machine.cert_file, pki.path(machine_id, 'cert')) |
3875 | + self.assertEqual(machine, |
3876 | + (machine.id, machine.ca_file, machine.key_file, machine.cert_file) |
3877 | + ) |
3878 | + |
3879 | + def test_load_user(self): |
3880 | + tmp = TempDir() |
3881 | + pki = peering.PKI(tmp.dir) |
3882 | + user_id = pki.create_key() |
3883 | + pki.create_ca(user_id) |
3884 | + |
3885 | + user = pki.load_user(user_id) |
3886 | + self.assertIsInstance(user, peering.User) |
3887 | + self.assertEqual(user.id, user_id) |
3888 | + self.assertEqual(user.ca_file, pki.path(user_id, 'ca')) |
3889 | + self.assertEqual(user.key_file, pki.path(user_id, 'key')) |
3890 | + self.assertEqual(user, (user.id, user.ca_file, user.key_file)) |
3891 | + |
3892 | + os.remove(pki.path(user_id, 'key')) |
3893 | + user = pki.load_user(user_id) |
3894 | + self.assertIsInstance(user, peering.User) |
3895 | + self.assertEqual(user.id, user_id) |
3896 | + self.assertEqual(user.ca_file, pki.path(user_id, 'ca')) |
3897 | + self.assertIsNone(user.key_file) |
3898 | + self.assertEqual(user, (user.id, user.ca_file, None)) |
3899 | + |
3900 | |
3901 | class TestTempPKI(TestCase): |
3902 | def test_init(self): |
3903 | |
3904 | === modified file 'dmedia/tests/test_server.py' |
3905 | --- dmedia/tests/test_server.py 2012-10-04 21:58:54 +0000 |
3906 | +++ dmedia/tests/test_server.py 2012-10-16 12:35:25 +0000 |
3907 | @@ -1,4 +1,4 @@ |
3908 | -# dmedia: dmedia hashing protocol and file layout |
3909 | +# dmedia: distributed media library |
3910 | # Copyright (C) 2011 Novacut Inc |
3911 | # |
3912 | # This file is part of `dmedia`. |
3913 | @@ -27,15 +27,21 @@ |
3914 | import multiprocessing |
3915 | import time |
3916 | from copy import deepcopy |
3917 | +import os |
3918 | +import socket |
3919 | +from base64 import b64encode, b64decode |
3920 | +from queue import Queue |
3921 | |
3922 | from usercouch.misc import TempCouch |
3923 | from filestore import DIGEST_B32LEN, DIGEST_BYTES |
3924 | import microfiber |
3925 | -from microfiber import random_id |
3926 | +from microfiber import random_id, dumps |
3927 | |
3928 | +from .base import TempDir |
3929 | import dmedia |
3930 | -from dmedia.peering import TempPKI |
3931 | -from dmedia import server, client |
3932 | +from dmedia.httpd import make_server, WSGIError, Input |
3933 | +from dmedia.peering import encode, decode |
3934 | +from dmedia import server, client, peering |
3935 | |
3936 | |
3937 | def random_dbname(): |
3938 | @@ -47,180 +53,41 @@ |
3939 | self.__called = False |
3940 | |
3941 | def __call__(self, status, headers): |
3942 | - assert not self.__callled |
3943 | + assert self.__called is False |
3944 | self.__called = True |
3945 | self.status = status |
3946 | self.headers = headers |
3947 | |
3948 | |
3949 | class TestFunctions(TestCase): |
3950 | - def test_get_slice(self): |
3951 | - # Test all the valid types of requests: |
3952 | - _id = random_id(DIGEST_BYTES) |
3953 | - self.assertEqual( |
3954 | - server.get_slice({'PATH_INFO': '/{}'.format(_id)}), |
3955 | - (_id, 0, None) |
3956 | - ) |
3957 | - |
3958 | - _id = random_id(DIGEST_BYTES) |
3959 | - self.assertEqual( |
3960 | - server.get_slice({'PATH_INFO': '/{}/0'.format(_id)}), |
3961 | - (_id, 0, None) |
3962 | - ) |
3963 | - |
3964 | - _id = random_id(DIGEST_BYTES) |
3965 | - self.assertEqual( |
3966 | - server.get_slice({'PATH_INFO': '/{}/17'.format(_id)}), |
3967 | - (_id, 17, None) |
3968 | - ) |
3969 | - |
3970 | - _id = random_id(DIGEST_BYTES) |
3971 | - self.assertEqual( |
3972 | - server.get_slice({'PATH_INFO': '/{}/17/21'.format(_id)}), |
3973 | - (_id, 17, 21) |
3974 | - ) |
3975 | - |
3976 | - _id = random_id(DIGEST_BYTES) |
3977 | - self.assertEqual( |
3978 | - server.get_slice({'PATH_INFO': '/{}/0/1'.format(_id)}), |
3979 | - (_id, 0, 1) |
3980 | - ) |
3981 | - |
3982 | - # Too many slashes |
3983 | - with self.assertRaises(server.BadRequest) as cm: |
3984 | - server.get_slice({'PATH_INFO': '/file-id/start/stop/other'}) |
3985 | - self.assertEqual(cm.exception.body, b'too many slashes in request path') |
3986 | - |
3987 | - with self.assertRaises(server.BadRequest) as cm: |
3988 | - server.get_slice({'PATH_INFO': 'file-id/start/stop/'}) |
3989 | - self.assertEqual(cm.exception.body, b'too many slashes in request path') |
3990 | - |
3991 | - with self.assertRaises(server.BadRequest) as cm: |
3992 | - server.get_slice({'PATH_INFO': '/file-id///'}) |
3993 | - self.assertEqual(cm.exception.body, b'too many slashes in request path') |
3994 | - |
3995 | - # Bad ID |
3996 | - attack = 'CCCCCCCCCCCCCCCCCCCCCCCCCCC\..\..\..\.ssh\id_rsa' |
3997 | - self.assertEqual(len(attack), DIGEST_B32LEN) |
3998 | - with self.assertRaises(server.BadRequest) as cm: |
3999 | - server.get_slice({'PATH_INFO': attack}) |
4000 | - self.assertEqual(cm.exception.body, b'badly formed dmedia ID') |
4001 | - |
4002 | - short = random_id(DIGEST_BYTES - 5) |
4003 | - with self.assertRaises(server.BadRequest) as cm: |
4004 | - server.get_slice({'PATH_INFO': short}) |
4005 | - self.assertEqual(cm.exception.body, b'badly formed dmedia ID') |
4006 | - |
4007 | - long = random_id(DIGEST_BYTES + 5) |
4008 | - with self.assertRaises(server.BadRequest) as cm: |
4009 | - server.get_slice({'PATH_INFO': long}) |
4010 | - self.assertEqual(cm.exception.body, b'badly formed dmedia ID') |
4011 | - |
4012 | - lower = random_id(DIGEST_BYTES).lower() |
4013 | - with self.assertRaises(server.BadRequest) as cm: |
4014 | - server.get_slice({'PATH_INFO': lower}) |
4015 | - self.assertEqual(cm.exception.body, b'badly formed dmedia ID') |
4016 | - |
4017 | - # start not integer |
4018 | - bad = '/{}/17.9'.format(random_id(DIGEST_BYTES)) |
4019 | - with self.assertRaises(server.BadRequest) as cm: |
4020 | - server.get_slice({'PATH_INFO': bad}) |
4021 | - self.assertEqual(cm.exception.body, b'start is not a valid integer') |
4022 | - |
4023 | - bad = '/{}/00ff'.format(random_id(DIGEST_BYTES)) |
4024 | - with self.assertRaises(server.BadRequest) as cm: |
4025 | - server.get_slice({'PATH_INFO': bad}) |
4026 | - self.assertEqual(cm.exception.body, b'start is not a valid integer') |
4027 | - |
4028 | - bad = '/{}/foo'.format(random_id(DIGEST_BYTES)) |
4029 | - with self.assertRaises(server.BadRequest) as cm: |
4030 | - server.get_slice({'PATH_INFO': bad}) |
4031 | - self.assertEqual(cm.exception.body, b'start is not a valid integer') |
4032 | - |
4033 | - bad = '/{}/17.9/333'.format(random_id(DIGEST_BYTES)) |
4034 | - with self.assertRaises(server.BadRequest) as cm: |
4035 | - server.get_slice({'PATH_INFO': bad}) |
4036 | - self.assertEqual(cm.exception.body, b'start is not a valid integer') |
4037 | - |
4038 | - bad = '/{}/00ff/333'.format(random_id(DIGEST_BYTES)) |
4039 | - with self.assertRaises(server.BadRequest) as cm: |
4040 | - server.get_slice({'PATH_INFO': bad}) |
4041 | - self.assertEqual(cm.exception.body, b'start is not a valid integer') |
4042 | - |
4043 | - bad = '/{}/foo/333'.format(random_id(DIGEST_BYTES)) |
4044 | - with self.assertRaises(server.BadRequest) as cm: |
4045 | - server.get_slice({'PATH_INFO': bad}) |
4046 | - self.assertEqual(cm.exception.body, b'start is not a valid integer') |
4047 | - |
4048 | - # stop not integer |
4049 | - bad = '/{}/18/21.2'.format(random_id(DIGEST_BYTES)) |
4050 | - with self.assertRaises(server.BadRequest) as cm: |
4051 | - server.get_slice({'PATH_INFO': bad}) |
4052 | - self.assertEqual(cm.exception.body, b'stop is not a valid integer') |
4053 | - |
4054 | - bad = '/{}/18/00ff'.format(random_id(DIGEST_BYTES)) |
4055 | - with self.assertRaises(server.BadRequest) as cm: |
4056 | - server.get_slice({'PATH_INFO': bad}) |
4057 | - self.assertEqual(cm.exception.body, b'stop is not a valid integer') |
4058 | - |
4059 | - bad = '/{}/18/foo'.format(random_id(DIGEST_BYTES)) |
4060 | - with self.assertRaises(server.BadRequest) as cm: |
4061 | - server.get_slice({'PATH_INFO': bad}) |
4062 | - self.assertEqual(cm.exception.body, b'stop is not a valid integer') |
4063 | - |
4064 | - # start < 0 |
4065 | - bad = '/{}/-1'.format(random_id(DIGEST_BYTES)) |
4066 | - with self.assertRaises(server.BadRequest) as cm: |
4067 | - server.get_slice({'PATH_INFO': bad}) |
4068 | - self.assertEqual(cm.exception.body, b'start cannot be less than zero') |
4069 | - |
4070 | - bad = '/{}/-1/18'.format(random_id(DIGEST_BYTES)) |
4071 | - with self.assertRaises(server.BadRequest) as cm: |
4072 | - server.get_slice({'PATH_INFO': bad}) |
4073 | - self.assertEqual(cm.exception.body, b'start cannot be less than zero') |
4074 | - |
4075 | - # start >= stop |
4076 | - bad = '/{}/18/17'.format(random_id(DIGEST_BYTES)) |
4077 | - with self.assertRaises(server.BadRequest) as cm: |
4078 | - server.get_slice({'PATH_INFO': bad}) |
4079 | - self.assertEqual(cm.exception.body, b'start must be less than stop') |
4080 | - |
4081 | - bad = '/{}/17/17'.format(random_id(DIGEST_BYTES)) |
4082 | - with self.assertRaises(server.BadRequest) as cm: |
4083 | - server.get_slice({'PATH_INFO': bad}) |
4084 | - self.assertEqual(cm.exception.body, b'start must be less than stop') |
4085 | - |
4086 | def test_range_to_slice(self): |
4087 | - with self.assertRaises(server.BadRangeRequest) as cm: |
4088 | + with self.assertRaises(WSGIError) as cm: |
4089 | (start, stop) = server.range_to_slice('goats=0-500') |
4090 | - self.assertEqual(cm.exception.body, b'bad range units') |
4091 | + self.assertEqual(cm.exception.status, '400 Bad Range Units') |
4092 | |
4093 | - with self.assertRaises(server.BadRangeRequest) as cm: |
4094 | + with self.assertRaises(WSGIError) as cm: |
4095 | (start, stop) = server.range_to_slice('bytes=-500-999') |
4096 | - self.assertEqual(cm.exception.body, b'range -start is not an integer') |
4097 | + self.assertEqual(cm.exception.status, '400 Bad Range Negative Start') |
4098 | |
4099 | - with self.assertRaises(server.BadRangeRequest) as cm: |
4100 | + with self.assertRaises(WSGIError) as cm: |
4101 | (start, stop) = server.range_to_slice('bytes=-foo') |
4102 | - self.assertEqual(cm.exception.body, b'range -start is not an integer') |
4103 | + self.assertEqual(cm.exception.status, '400 Bad Range Negative Start') |
4104 | |
4105 | - with self.assertRaises(server.BadRangeRequest) as cm: |
4106 | + with self.assertRaises(WSGIError) as cm: |
4107 | (start, stop) = server.range_to_slice('bytes=500') |
4108 | - self.assertEqual(cm.exception.body, b'not formatted as bytes=start-end') |
4109 | + self.assertEqual(cm.exception.status, '400 Bad Range Format') |
4110 | |
4111 | - with self.assertRaises(server.BadRangeRequest) as cm: |
4112 | + with self.assertRaises(WSGIError) as cm: |
4113 | (start, stop) = server.range_to_slice('bytes=foo-999') |
4114 | - self.assertEqual(cm.exception.body, b'range start is not an integer') |
4115 | + self.assertEqual(cm.exception.status, '400 Bad Range Start') |
4116 | |
4117 | - with self.assertRaises(server.BadRangeRequest) as cm: |
4118 | + with self.assertRaises(WSGIError) as cm: |
4119 | (start, stop) = server.range_to_slice('bytes=500-bar') |
4120 | - self.assertEqual(cm.exception.body, b'range end is not an integer') |
4121 | + self.assertEqual(cm.exception.status, '400 Bad Range End') |
4122 | |
4123 | - with self.assertRaises(server.BadRangeRequest) as cm: |
4124 | + with self.assertRaises(WSGIError) as cm: |
4125 | (start, stop) = server.range_to_slice('bytes=500-499') |
4126 | - self.assertEqual( |
4127 | - cm.exception.body, |
4128 | - b'range end must be less than or equal to start' |
4129 | - ) |
4130 | + self.assertEqual(cm.exception.status, '400 Bad Range') |
4131 | |
4132 | self.assertEqual( |
4133 | server.range_to_slice('bytes=0-0'), (0, 1) |
4134 | @@ -257,31 +124,370 @@ |
4135 | ) |
4136 | |
4137 | |
4138 | -class TestBaseWSGI(TestCase): |
4139 | - def test_metaclass(self): |
4140 | - self.assertEqual(server.BaseWSGI.http_methods, frozenset()) |
4141 | - |
4142 | - class Example(server.BaseWSGI): |
4143 | - def PUT(self, environ, start_response): |
4144 | - pass |
4145 | - |
4146 | - def POST(self, environ, start_response): |
4147 | - pass |
4148 | - |
4149 | - def GET(self, environ, start_response): |
4150 | - pass |
4151 | - |
4152 | - def DELETE(self, environ, start_response): |
4153 | - pass |
4154 | - |
4155 | - def HEAD(self, environ, start_response): |
4156 | - pass |
4157 | - |
4158 | - self.assertEqual( |
4159 | - Example.http_methods, |
4160 | - frozenset(['PUT', 'POST', 'GET', 'DELETE', 'HEAD']) |
4161 | - ) |
4162 | - |
4163 | +class TestRootApp(TestCase): |
4164 | + def test_init(self): |
4165 | + user_id = random_id(30) |
4166 | + machine_id = random_id(30) |
4167 | + password = random_id() |
4168 | + env = { |
4169 | + 'user_id': user_id, |
4170 | + 'machine_id': machine_id, |
4171 | + 'basic': {'username': 'admin', 'password': password}, |
4172 | + 'url': microfiber.HTTP_IPv4_URL, |
4173 | + } |
4174 | + app = server.RootApp(env) |
4175 | + self.assertIs(app.user_id, user_id) |
4176 | + self.assertEqual( |
4177 | + app.info, |
4178 | + microfiber.dumps( |
4179 | + { |
4180 | + 'user_id': user_id, |
4181 | + 'machine_id': machine_id, |
4182 | + 'version': dmedia.__version__, |
4183 | + 'user': os.environ.get('USER'), |
4184 | + 'host': socket.gethostname(), |
4185 | + } |
4186 | + ).encode('utf-8') |
4187 | + ) |
4188 | + self.assertEqual(app.info_length, str(int(len(app.info)))) |
4189 | + self.assertIsInstance(app.proxy, server.ProxyApp) |
4190 | + self.assertIsInstance(app.files, server.FilesApp) |
4191 | + self.assertEqual(app.map, |
4192 | + { |
4193 | + '': app.get_info, |
4194 | + 'couch': app.proxy, |
4195 | + 'files': app.files, |
4196 | + } |
4197 | + ) |
4198 | + |
4199 | + def test_call(self): |
4200 | + user_id = random_id(30) |
4201 | + machine_id = random_id(30) |
4202 | + password = random_id() |
4203 | + env = { |
4204 | + 'user_id': user_id, |
4205 | + 'machine_id': machine_id, |
4206 | + 'basic': {'username': 'admin', 'password': password}, |
4207 | + 'url': microfiber.HTTP_IPv4_URL, |
4208 | + } |
4209 | + app = server.RootApp(env) |
4210 | + |
4211 | + # SSL_CLIENT_VERIFY |
4212 | + with self.assertRaises(WSGIError) as cm: |
4213 | + app({}, None) |
4214 | + self.assertEqual(cm.exception.status, '403 Forbidden SSL') |
4215 | + with self.assertRaises(WSGIError) as cm: |
4216 | + app({'SSL_CLIENT_VERIFY': 'NOPE'}, None) |
4217 | + self.assertEqual(cm.exception.status, '403 Forbidden SSL') |
4218 | + |
4219 | + # SSL_CLIENT_I_DN_CN |
4220 | + environ = { |
4221 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4222 | + } |
4223 | + with self.assertRaises(WSGIError) as cm: |
4224 | + app(environ, None) |
4225 | + self.assertEqual(cm.exception.status, '403 Forbidden Issuer') |
4226 | + environ = { |
4227 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4228 | + 'SSL_CLIENT_I_DN_CN': random_id(30), |
4229 | + } |
4230 | + with self.assertRaises(WSGIError) as cm: |
4231 | + app(environ, None) |
4232 | + self.assertEqual(cm.exception.status, '403 Forbidden Issuer') |
4233 | + |
4234 | + # PATH_INFO |
4235 | + environ = { |
4236 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4237 | + 'SSL_CLIENT_I_DN_CN': user_id, |
4238 | + 'PATH_INFO': '/foo', |
4239 | + } |
4240 | + with self.assertRaises(WSGIError) as cm: |
4241 | + app(environ, None) |
4242 | + self.assertEqual(cm.exception.status, '410 Gone') |
4243 | + |
4244 | + # REQUEST_METHOD |
4245 | + environ = { |
4246 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4247 | + 'SSL_CLIENT_I_DN_CN': user_id, |
4248 | + 'PATH_INFO': '/', |
4249 | + 'REQUEST_METHOD': 'HEAD', |
4250 | + } |
4251 | + with self.assertRaises(WSGIError) as cm: |
4252 | + app(environ, None) |
4253 | + self.assertEqual(cm.exception.status, '405 Method Not Allowed') |
4254 | + environ = { |
4255 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4256 | + 'SSL_CLIENT_I_DN_CN': user_id, |
4257 | + 'PATH_INFO': '/files/' + random_id(30), |
4258 | + 'REQUEST_METHOD': 'PUT', |
4259 | + } |
4260 | + with self.assertRaises(WSGIError) as cm: |
4261 | + app(environ, None) |
4262 | + self.assertEqual(cm.exception.status, '405 Method Not Allowed') |
4263 | + |
4264 | + # Test when it's all good |
4265 | + environ = { |
4266 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4267 | + 'SSL_CLIENT_I_DN_CN': user_id, |
4268 | + 'PATH_INFO': '/', |
4269 | + 'REQUEST_METHOD': 'GET', |
4270 | + } |
4271 | + sr = StartResponse() |
4272 | + ret = app(environ, sr) |
4273 | + self.assertEqual(ret, [app.info]) |
4274 | + self.assertEqual(sr.status, '200 OK') |
4275 | + self.assertEqual(sr.headers, |
4276 | + [ |
4277 | + ('Content-Length', app.info_length), |
4278 | + ('Content-Type', 'application/json'), |
4279 | + ] |
4280 | + ) |
4281 | + |
4282 | + |
4283 | +class TestProxyApp(TestCase): |
4284 | + def test_init(self): |
4285 | + password = random_id() |
4286 | + env = { |
4287 | + 'basic': {'username': 'admin', 'password': password}, |
4288 | + 'url': microfiber.HTTP_IPv4_URL, |
4289 | + } |
4290 | + app = server.ProxyApp(deepcopy(env)) |
4291 | + self.assertIsInstance(app.client, microfiber.CouchBase) |
4292 | + self.assertEqual(app.client.env, env) |
4293 | + self.assertEqual(app.target_host, '127.0.0.1:5984') |
4294 | + self.assertEqual(app.basic_auth, |
4295 | + microfiber.basic_auth_header(env['basic']) |
4296 | + ) |
4297 | + |
4298 | + def test_call(self): |
4299 | + password = random_id() |
4300 | + env = { |
4301 | + 'basic': {'username': 'admin', 'password': password}, |
4302 | + 'url': microfiber.HTTP_IPv4_URL, |
4303 | + } |
4304 | + app = server.ProxyApp(env) |
4305 | + |
4306 | + # PATH_INFO |
4307 | + environ = { |
4308 | + 'REQUEST_METHOD': 'GET', |
4309 | + 'PATH_INFO': '/_config/foo', |
4310 | + 'wsgi.input': Input(None, {'REQUEST_METHOD': 'GET'}), |
4311 | + } |
4312 | + with self.assertRaises(WSGIError) as cm: |
4313 | + app(environ, None) |
4314 | + self.assertEqual(cm.exception.status, '403 Forbidden') |
4315 | + |
4316 | + |
4317 | +class TestInfoApp(TestCase): |
4318 | + def test_init(self): |
4319 | + _id = random_id(30) |
4320 | + app = server.InfoApp(_id) |
4321 | + self.assertIs(app.id, _id) |
4322 | + self.assertEqual( |
4323 | + app.info, |
4324 | + microfiber.dumps( |
4325 | + { |
4326 | + 'id': _id, |
4327 | + 'version': dmedia.__version__, |
4328 | + 'user': os.environ.get('USER'), |
4329 | + 'host': socket.gethostname(), |
4330 | + } |
4331 | + ).encode('utf-8') |
4332 | + ) |
4333 | + self.assertEqual(app.info_length, str(int(len(app.info)))) |
4334 | + |
4335 | + def test_call(self): |
4336 | + app = server.InfoApp(random_id(30)) |
4337 | + |
4338 | + # wsgi.multithread |
4339 | + with self.assertRaises(WSGIError) as cm: |
4340 | + app({'wsgi.multithread': True}, None) |
4341 | + self.assertEqual(cm.exception.status, '500 Internal Server Error') |
4342 | + with self.assertRaises(WSGIError) as cm: |
4343 | + app({'wsgi.multithread': 0}, None) |
4344 | + self.assertEqual(cm.exception.status, '500 Internal Server Error') |
4345 | + |
4346 | + # PATH_INFO |
4347 | + environ = { |
4348 | + 'wsgi.multithread': False, |
4349 | + 'PATH_INFO': '/foo', |
4350 | + } |
4351 | + with self.assertRaises(WSGIError) as cm: |
4352 | + app(environ, None) |
4353 | + self.assertEqual(cm.exception.status, '410 Gone') |
4354 | + |
4355 | + # REQUEST_METHOD |
4356 | + environ = { |
4357 | + 'wsgi.multithread': False, |
4358 | + 'PATH_INFO': '/', |
4359 | + 'REQUEST_METHOD': 'HEAD', |
4360 | + } |
4361 | + with self.assertRaises(WSGIError) as cm: |
4362 | + app(environ, None) |
4363 | + self.assertEqual(cm.exception.status, '405 Method Not Allowed') |
4364 | + |
4365 | + # Test when it's all good |
4366 | + environ = { |
4367 | + 'wsgi.multithread': False, |
4368 | + 'PATH_INFO': '/', |
4369 | + 'REQUEST_METHOD': 'GET', |
4370 | + } |
4371 | + sr = StartResponse() |
4372 | + ret = app(environ, sr) |
4373 | + self.assertEqual(ret, [app.info]) |
4374 | + self.assertEqual(sr.status, '200 OK') |
4375 | + self.assertEqual(sr.headers, |
4376 | + [ |
4377 | + ('Content-Length', app.info_length), |
4378 | + ('Content-Type', 'application/json'), |
4379 | + ] |
4380 | + ) |
4381 | + |
4382 | + |
4383 | +class TestClientApp(TestCase): |
4384 | + def test_init(self): |
4385 | + id1 = random_id(30) |
4386 | + id2 = random_id(30) |
4387 | + cr = peering.ChallengeResponse(id1, id2) |
4388 | + q = Queue() |
4389 | + app = server.ClientApp(cr, q) |
4390 | + self.assertIs(app.cr, cr) |
4391 | + self.assertIs(app.queue, q) |
4392 | + self.assertEqual(app.map, |
4393 | + { |
4394 | + '/challenge': app.get_challenge, |
4395 | + '/response': app.post_response, |
4396 | + } |
4397 | + ) |
4398 | + |
4399 | + def test_call(self): |
4400 | + _id = random_id(30) |
4401 | + peer_id = random_id(30) |
4402 | + cr = peering.ChallengeResponse(_id, peer_id) |
4403 | + app = server.ClientApp(cr, Queue()) |
4404 | + |
4405 | + # wsgi.multithread |
4406 | + with self.assertRaises(WSGIError) as cm: |
4407 | + app({'wsgi.multithread': True}, None) |
4408 | + self.assertEqual(cm.exception.status, '500 Internal Server Error') |
4409 | + with self.assertRaises(WSGIError) as cm: |
4410 | + app({'wsgi.multithread': 0}, None) |
4411 | + self.assertEqual(cm.exception.status, '500 Internal Server Error') |
4412 | + |
4413 | + # SSL_CLIENT_VERIFY |
4414 | + with self.assertRaises(WSGIError) as cm: |
4415 | + app({'wsgi.multithread': False}, None) |
4416 | + self.assertEqual(cm.exception.status, '403 Forbidden SSL') |
4417 | + environ = { |
4418 | + 'wsgi.multithread': False, |
4419 | + 'SSL_CLIENT_VERIFY': 'NOPE', |
4420 | + } |
4421 | + with self.assertRaises(WSGIError) as cm: |
4422 | + app(environ, None) |
4423 | + self.assertEqual(cm.exception.status, '403 Forbidden SSL') |
4424 | + |
4425 | + # SSL_CLIENT_S_DN_CN |
4426 | + environ = { |
4427 | + 'wsgi.multithread': False, |
4428 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4429 | + } |
4430 | + with self.assertRaises(WSGIError) as cm: |
4431 | + app(environ, None) |
4432 | + self.assertEqual(cm.exception.status, '403 Forbidden Subject') |
4433 | + environ = { |
4434 | + 'wsgi.multithread': False, |
4435 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4436 | + 'SSL_CLIENT_S_DN_CN': random_id(30), |
4437 | + } |
4438 | + with self.assertRaises(WSGIError) as cm: |
4439 | + app(environ, None) |
4440 | + self.assertEqual(cm.exception.status, '403 Forbidden Subject') |
4441 | + |
4442 | + # SSL_CLIENT_I_DN_CN |
4443 | + environ = { |
4444 | + 'wsgi.multithread': False, |
4445 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4446 | + 'SSL_CLIENT_S_DN_CN': peer_id, |
4447 | + } |
4448 | + with self.assertRaises(WSGIError) as cm: |
4449 | + app(environ, None) |
4450 | + self.assertEqual(cm.exception.status, '403 Forbidden Issuer') |
4451 | + environ = { |
4452 | + 'wsgi.multithread': False, |
4453 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4454 | + 'SSL_CLIENT_S_DN_CN': peer_id, |
4455 | + 'SSL_CLIENT_I_DN_CN': random_id(30), |
4456 | + } |
4457 | + with self.assertRaises(WSGIError) as cm: |
4458 | + app(environ, None) |
4459 | + self.assertEqual(cm.exception.status, '403 Forbidden Issuer') |
4460 | + |
4461 | + # PATH_INFO |
4462 | + environ = { |
4463 | + 'wsgi.multithread': False, |
4464 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4465 | + 'SSL_CLIENT_S_DN_CN': peer_id, |
4466 | + 'SSL_CLIENT_I_DN_CN': peer_id, |
4467 | + 'PATH_INFO': '/', |
4468 | + } |
4469 | + with self.assertRaises(WSGIError) as cm: |
4470 | + app(environ, None) |
4471 | + self.assertEqual(cm.exception.status, '410 Gone') |
4472 | + |
4473 | + # state |
4474 | + environ = { |
4475 | + 'wsgi.multithread': False, |
4476 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4477 | + 'SSL_CLIENT_S_DN_CN': peer_id, |
4478 | + 'SSL_CLIENT_I_DN_CN': peer_id, |
4479 | + 'PATH_INFO': '/challenge', |
4480 | + } |
4481 | + with self.assertRaises(WSGIError) as cm: |
4482 | + app(environ, None) |
4483 | + self.assertEqual(cm.exception.status, '400 Bad Request Order') |
4484 | + |
4485 | + # REQUEST_METHOD |
4486 | + app.state = 'ready' |
4487 | + environ = { |
4488 | + 'wsgi.multithread': False, |
4489 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4490 | + 'SSL_CLIENT_S_DN_CN': peer_id, |
4491 | + 'SSL_CLIENT_I_DN_CN': peer_id, |
4492 | + 'PATH_INFO': '/challenge', |
4493 | + 'REQUEST_METHOD': 'POST', |
4494 | + } |
4495 | + with self.assertRaises(WSGIError) as cm: |
4496 | + app(environ, None) |
4497 | + self.assertEqual(cm.exception.status, '405 Method Not Allowed') |
4498 | + self.assertEqual(app.state, 'gave_challenge') |
4499 | + |
4500 | + # Test when it's all good |
4501 | + app.state = 'ready' |
4502 | + environ = { |
4503 | + 'wsgi.multithread': False, |
4504 | + 'SSL_CLIENT_VERIFY': 'SUCCESS', |
4505 | + 'SSL_CLIENT_S_DN_CN': peer_id, |
4506 | + 'SSL_CLIENT_I_DN_CN': peer_id, |
4507 | + 'PATH_INFO': '/challenge', |
4508 | + 'REQUEST_METHOD': 'GET', |
4509 | + } |
4510 | + sr = StartResponse() |
4511 | + ret = app(environ, sr) |
4512 | + data = dumps({'challenge': encode(cr.challenge)}).encode('utf-8') |
4513 | + self.assertEqual(ret, [data]) |
4514 | + self.assertEqual(sr.status, '200 OK') |
4515 | + self.assertEqual(sr.headers, |
4516 | + [ |
4517 | + ('Content-Length', str(len(data))), |
4518 | + ('Content-Type', 'application/json'), |
4519 | + ] |
4520 | + ) |
4521 | + self.assertEqual(app.state, 'gave_challenge') |
4522 | + |
4523 | + |
4524 | + |
4525 | +#################################### |
4526 | +# Live test cases using Dmedia HTTPD |
4527 | |
4528 | class TempHTTPD: |
4529 | def __init__(self, couch_env, ssl_config): |
4530 | @@ -306,14 +512,15 @@ |
4531 | self.process.join() |
4532 | |
4533 | |
4534 | -class TestRootApp(TestCase): |
4535 | +class TestRootAppLive(TestCase): |
4536 | def test_call(self): |
4537 | couch = TempCouch() |
4538 | couch_env = couch.bootstrap() |
4539 | - couch_env['user_id'] = random_id() |
4540 | + couch_env['user_id'] = random_id(30) |
4541 | + couch_env['machine_id'] = random_id(30) |
4542 | |
4543 | # Test when client PKI isn't configured |
4544 | - pki = TempPKI() |
4545 | + pki = peering.TempPKI() |
4546 | httpd = TempHTTPD(couch_env, pki.get_server_config()) |
4547 | env = deepcopy(httpd.env) |
4548 | env['ssl'] = pki.get_client_config() |
4549 | @@ -323,7 +530,7 @@ |
4550 | self.assertEqual(cm.exception.response.reason, 'Forbidden SSL') |
4551 | |
4552 | # Test when cert issuer is wrong |
4553 | - pki = TempPKI(True) |
4554 | + pki = peering.TempPKI(True) |
4555 | httpd = TempHTTPD(couch_env, pki.get_server_config()) |
4556 | env = deepcopy(httpd.env) |
4557 | env['ssl'] = pki.get_client_config() |
4558 | @@ -333,14 +540,21 @@ |
4559 | self.assertEqual(cm.exception.response.reason, 'Forbidden Issuer') |
4560 | |
4561 | # Test when SSL config is correct, then test other aspects |
4562 | - pki = TempPKI(True) |
4563 | + pki = peering.TempPKI(True) |
4564 | couch_env['user_id'] = pki.client_ca.id |
4565 | + couch_env['machine_id'] = pki.server.id |
4566 | httpd = TempHTTPD(couch_env, pki.get_server_config()) |
4567 | env = deepcopy(httpd.env) |
4568 | env['ssl'] = pki.get_client_config() |
4569 | client = microfiber.CouchBase(env) |
4570 | self.assertEqual(client.get(), |
4571 | - {'Dmedia': 'welcome', 'version': dmedia.__version__} |
4572 | + { |
4573 | + 'user_id': pki.client_ca.id, |
4574 | + 'machine_id': pki.server.id, |
4575 | + 'version': dmedia.__version__, |
4576 | + 'user': os.environ.get('USER'), |
4577 | + 'host': socket.gethostname(), |
4578 | + } |
4579 | ) |
4580 | self.assertEqual(client.get('couch')['couchdb'], 'Welcome') |
4581 | |
4582 | @@ -357,7 +571,7 @@ |
4583 | """ |
4584 | Test push replication Couch1 => HTTPD => Couch2. |
4585 | """ |
4586 | - pki = TempPKI(True) |
4587 | + pki = peering.TempPKI(True) |
4588 | config = {'replicator': pki.get_client_config()} |
4589 | couch1 = TempCouch() |
4590 | couch2 = TempCouch() |
4591 | @@ -369,6 +583,7 @@ |
4592 | # couch2 needs env['user_id'] |
4593 | env2 = couch2.bootstrap('basic', None) |
4594 | env2['user_id'] = pki.client_ca.id |
4595 | + env2['machine_id'] = pki.server.id |
4596 | s2 = microfiber.Server(env2) |
4597 | |
4598 | # httpd is the SSL frontend for couch2 |
4599 | @@ -392,3 +607,157 @@ |
4600 | for doc in docs: |
4601 | self.assertEqual(s2.get(name2, doc['_id']), doc) |
4602 | |
4603 | + |
4604 | +class TestServerAppLive(TestCase): |
4605 | + def test_live(self): |
4606 | + tmp = TempDir() |
4607 | + pki = peering.PKI(tmp.dir) |
4608 | + local_id = pki.create_key() |
4609 | + pki.create_ca(local_id) |
4610 | + remote_id = pki.create_key() |
4611 | + pki.create_ca(remote_id) |
4612 | + server_config = { |
4613 | + 'cert_file': pki.path(local_id, 'ca'), |
4614 | + 'key_file': pki.path(local_id, 'key'), |
4615 | + 'ca_file': pki.path(remote_id, 'ca'), |
4616 | + } |
4617 | + client_config = { |
4618 | + 'check_hostname': False, |
4619 | + 'ca_file': pki.path(local_id, 'ca'), |
4620 | + 'cert_file': pki.path(remote_id, 'ca'), |
4621 | + 'key_file': pki.path(remote_id, 'key'), |
4622 | + } |
4623 | + local = peering.ChallengeResponse(local_id, remote_id) |
4624 | + remote = peering.ChallengeResponse(remote_id, local_id) |
4625 | + q = Queue() |
4626 | + app = server.ServerApp(local, q, None) |
4627 | + httpd = make_server(app, '127.0.0.1', server_config) |
4628 | + client = microfiber.CouchBase( |
4629 | + {'url': httpd.url, 'ssl': client_config} |
4630 | + ) |
4631 | + httpd.start() |
4632 | + secret = local.get_secret() |
4633 | + remote.set_secret(secret) |
4634 | + |
4635 | + self.assertIsNone(app.state) |
4636 | + with self.assertRaises(microfiber.BadRequest) as cm: |
4637 | + client.get('') |
4638 | + self.assertEqual( |
4639 | + str(cm.exception), |
4640 | + '400 Bad Request State: GET /' |
4641 | + ) |
4642 | + app.state = 'info' |
4643 | + self.assertEqual(client.get(), |
4644 | + { |
4645 | + 'id': local_id, |
4646 | + 'user': os.environ.get('USER'), |
4647 | + 'host': socket.gethostname(), |
4648 | + } |
4649 | + ) |
4650 | + self.assertEqual(app.state, 'ready') |
4651 | + with self.assertRaises(microfiber.BadRequest) as cm: |
4652 | + client.get('') |
4653 | + self.assertEqual( |
4654 | + str(cm.exception), |
4655 | + '400 Bad Request State: GET /' |
4656 | + ) |
4657 | + self.assertEqual(app.state, 'ready') |
4658 | + |
4659 | + app.state = 'info' |
4660 | + with self.assertRaises(microfiber.BadRequest) as cm: |
4661 | + client.get('challenge') |
4662 | + self.assertEqual( |
4663 | + str(cm.exception), |
4664 | + '400 Bad Request Order: GET /challenge' |
4665 | + ) |
4666 | + with self.assertRaises(microfiber.BadRequest) as cm: |
4667 | + client.post({'hello': 'world'}, 'response') |
4668 | + self.assertEqual( |
4669 | + str(cm.exception), |
4670 | + '400 Bad Request Order: POST /response' |
4671 | + ) |
4672 | + |
4673 | + app.state = 'ready' |
4674 | + self.assertEqual(app.state, 'ready') |
4675 | + obj = client.get('challenge') |
4676 | + self.assertEqual(app.state, 'gave_challenge') |
4677 | + self.assertIsInstance(obj, dict) |
4678 | + self.assertEqual(set(obj), set(['challenge'])) |
4679 | + self.assertEqual(local.challenge, decode(obj['challenge'])) |
4680 | + with self.assertRaises(microfiber.BadRequest) as cm: |
4681 | + client.get('challenge') |
4682 | + self.assertEqual( |
4683 | + str(cm.exception), |
4684 | + '400 Bad Request Order: GET /challenge' |
4685 | + ) |
4686 | + self.assertEqual(app.state, 'gave_challenge') |
4687 | + |
4688 | + (nonce, response) = remote.create_response(obj['challenge']) |
4689 | + obj = {'nonce': nonce, 'response': response} |
4690 | + self.assertEqual(client.post(obj, 'response'), {'ok': True}) |
4691 | + self.assertEqual(app.state, 'response_ok') |
4692 | + with self.assertRaises(microfiber.BadRequest) as cm: |
4693 | + client.post(obj, 'response') |
4694 | + self.assertEqual( |
4695 | + str(cm.exception), |
4696 | + '400 Bad Request Order: POST /response' |
4697 | + ) |
4698 | + self.assertEqual(app.state, 'response_ok') |
4699 | + self.assertEqual(q.get(), 'response_ok') |
4700 | + |
4701 | + # Test when an error occurs in put_response() |
4702 | + app.state = 'gave_challenge' |
4703 | + with self.assertRaises(microfiber.ServerError) as cm: |
4704 | + client.post(b'bad json', 'response') |
4705 | + self.assertEqual(app.state, 'in_response') |
4706 | + |
4707 | + # Test with wrong secret |
4708 | + app.state = 'ready' |
4709 | + secret = local.get_secret() |
4710 | + remote.get_secret() |
4711 | + challenge = client.get('challenge')['challenge'] |
4712 | + self.assertEqual(app.state, 'gave_challenge') |
4713 | + (nonce, response) = remote.create_response(challenge) |
4714 | + with self.assertRaises(microfiber.Unauthorized) as cm: |
4715 | + client.post({'nonce': nonce, 'response': response}, 'response') |
4716 | + self.assertEqual(app.state, 'wrong_response') |
4717 | + self.assertFalse(hasattr(local, 'secret')) |
4718 | + self.assertFalse(hasattr(local, 'challenge')) |
4719 | + |
4720 | + # Verify that you can't retry |
4721 | + remote.set_secret(secret) |
4722 | + (nonce, response) = remote.create_response(challenge) |
4723 | + with self.assertRaises(microfiber.BadRequest) as cm: |
4724 | + client.post({'nonce': nonce, 'response': response}, 'response') |
4725 | + self.assertEqual( |
4726 | + str(cm.exception), |
4727 | + '400 Bad Request Order: POST /response' |
4728 | + ) |
4729 | + self.assertEqual(app.state, 'wrong_response') |
4730 | + self.assertEqual(q.get(), 'wrong_response') |
4731 | + |
4732 | + # Test POST /csr |
4733 | + secret = local.get_secret() |
4734 | + remote.set_secret(secret) |
4735 | + app.state = 'response_ok' |
4736 | + with self.assertRaises(microfiber.BadRequest) as cm: |
4737 | + client.post({}, 'csr') |
4738 | + self.assertEqual( |
4739 | + str(cm.exception), |
4740 | + '400 Bad Request Order: POST /csr' |
4741 | + ) |
4742 | + self.assertEqual(app.state, 'response_ok') |
4743 | + |
4744 | + app.state = 'counter_response_ok' |
4745 | + secret = local.get_secret() |
4746 | + remote.set_secret(secret) |
4747 | + pki.create_csr(remote_id) |
4748 | + csr_data = pki.read_csr(remote_id) |
4749 | + obj = { |
4750 | + 'csr': b64encode(csr_data).decode('utf-8'), |
4751 | + 'mac': local.csr_mac(csr_data), |
4752 | + } |
4753 | + # FIXME: Why isn't this working? |
4754 | + #r = client.post(obj, 'csr') |
4755 | + |
4756 | + httpd.shutdown() |
4757 | |
4758 | === modified file 'dmedia/tests/test_startup.py' |
4759 | --- dmedia/tests/test_startup.py 2012-10-03 14:42:56 +0000 |
4760 | +++ dmedia/tests/test_startup.py 2012-10-16 12:35:25 +0000 |
4761 | @@ -68,69 +68,6 @@ |
4762 | self.assertFalse(path.exists(filename + '.tmp')) |
4763 | self.assertEqual(json.load(open(filename, 'r')), config) |
4764 | |
4765 | - def test_get_ssl_config(self): |
4766 | - tmp = TempDir() |
4767 | - pki = PKI(tmp.dir) |
4768 | - machine_id = pki.create_key() |
4769 | - pki.create_ca(machine_id) |
4770 | - pki.create_csr(machine_id) |
4771 | - user_id = pki.create_key() |
4772 | - pki.create_ca(user_id) |
4773 | - pki.issue_cert(machine_id, user_id) |
4774 | - |
4775 | - self.assertIsNone(startup.get_ssl_config(pki)) |
4776 | - |
4777 | - pki.machine = pki.get_cert(machine_id) |
4778 | - self.assertIsNone(startup.get_ssl_config(pki)) |
4779 | - |
4780 | - pki.user = pki.get_ca(user_id) |
4781 | - self.assertEqual( |
4782 | - startup.get_ssl_config(pki), |
4783 | - { |
4784 | - 'check_hostname': False, |
4785 | - 'max_depth': 1, |
4786 | - 'ca_file': pki.path(user_id, 'ca'), |
4787 | - 'cert_file': pki.path(machine_id, 'cert'), |
4788 | - 'key_file': pki.path(machine_id, 'key'), |
4789 | - } |
4790 | - ) |
4791 | - |
4792 | - def test_get_bootstrap_config(self): |
4793 | - tmp = TempDir() |
4794 | - pki = PKI(tmp.dir) |
4795 | - machine_id = pki.create_key() |
4796 | - pki.create_ca(machine_id) |
4797 | - pki.create_csr(machine_id) |
4798 | - user_id = pki.create_key() |
4799 | - pki.create_ca(user_id) |
4800 | - pki.issue_cert(machine_id, user_id) |
4801 | - |
4802 | - self.assertEqual( |
4803 | - startup.get_bootstrap_config(pki), |
4804 | - {'username': 'admin'}, |
4805 | - ) |
4806 | - |
4807 | - pki.machine = pki.get_cert(machine_id) |
4808 | - self.assertEqual( |
4809 | - startup.get_bootstrap_config(pki), |
4810 | - {'username': 'admin'}, |
4811 | - ) |
4812 | - |
4813 | - pki.user = pki.get_ca(user_id) |
4814 | - self.assertEqual( |
4815 | - startup.get_bootstrap_config(pki), |
4816 | - { |
4817 | - 'username': 'admin', |
4818 | - 'replicator': { |
4819 | - 'check_hostname': False, |
4820 | - 'max_depth': 1, |
4821 | - 'ca_file': pki.path(user_id, 'ca'), |
4822 | - 'cert_file': pki.path(machine_id, 'cert'), |
4823 | - 'key_file': pki.path(machine_id, 'key'), |
4824 | - }, |
4825 | - } |
4826 | - ) |
4827 | - |
4828 | |
4829 | class TestDmediaCouch(TestCase): |
4830 | def test_init(self): |
4831 | @@ -180,62 +117,105 @@ |
4832 | tmp = TempDir() |
4833 | inst = startup.DmediaCouch(tmp.dir) |
4834 | self.assertTrue(inst.isfirstrun()) |
4835 | - inst.machine = 'foo' |
4836 | + inst.user = 'foo' |
4837 | self.assertFalse(inst.isfirstrun()) |
4838 | - inst.machine = None |
4839 | + inst.user = None |
4840 | self.assertTrue(inst.isfirstrun()) |
4841 | |
4842 | - def test_firstrun_init(self): |
4843 | - class Subclass(startup.DmediaCouch): |
4844 | - def __init__(self): |
4845 | - pass |
4846 | - |
4847 | - def isfirstrun(self): |
4848 | - return False |
4849 | - |
4850 | - inst = Subclass() |
4851 | - with self.assertRaises(Exception) as cm: |
4852 | - inst.firstrun_init() |
4853 | - self.assertEqual( |
4854 | - str(cm.exception), |
4855 | - 'not first run, cannot call firstrun_init()' |
4856 | - ) |
4857 | - |
4858 | - tmp = TempDir() |
4859 | - inst = startup.DmediaCouch(tmp.dir) |
4860 | - self.assertIsNone(inst.firstrun_init()) |
4861 | - self.assertIsInstance(inst.machine, dict) |
4862 | - self.assertEqual(inst.machine, inst.load_config('machine')) |
4863 | - self.assertIsNone(inst.user) |
4864 | - self.assertIsNone(inst.load_config('user')) |
4865 | - |
4866 | - tmp = TempDir() |
4867 | - inst = startup.DmediaCouch(tmp.dir) |
4868 | - self.assertIsNone(inst.firstrun_init(create_user=True)) |
4869 | - self.assertIsInstance(inst.machine, dict) |
4870 | - self.assertEqual(inst.machine, inst.load_config('machine')) |
4871 | + def test_create_machine(self): |
4872 | + class Subclass(startup.DmediaCouch): |
4873 | + def __init__(self): |
4874 | + self.machine = 'foo' |
4875 | + |
4876 | + inst = Subclass() |
4877 | + with self.assertRaises(Exception) as cm: |
4878 | + inst.create_machine() |
4879 | + self.assertEqual(str(cm.exception), 'machine already exists') |
4880 | + |
4881 | + tmp = TempDir() |
4882 | + inst = startup.DmediaCouch(tmp.dir) |
4883 | + machine_id = inst.create_machine() |
4884 | + self.assertIsInstance(machine_id, str) |
4885 | + self.assertEqual(len(machine_id), 48) |
4886 | + self.assertIsInstance(inst.machine, dict) |
4887 | + self.assertEqual(inst.machine['_id'], machine_id) |
4888 | + self.assertEqual(inst.load_config('machine'), inst.machine) |
4889 | + |
4890 | + with self.assertRaises(Exception) as cm: |
4891 | + inst.create_machine() |
4892 | + self.assertEqual(str(cm.exception), 'machine already exists') |
4893 | + |
4894 | + def test_create_user(self): |
4895 | + class Subclass(startup.DmediaCouch): |
4896 | + def __init__(self): |
4897 | + self.machine = 'foo' |
4898 | + self.user = 'bar' |
4899 | + |
4900 | + inst = Subclass() |
4901 | + with self.assertRaises(Exception) as cm: |
4902 | + inst.create_user() |
4903 | + self.assertEqual(str(cm.exception), 'user already exists') |
4904 | + |
4905 | + tmp = TempDir() |
4906 | + inst = startup.DmediaCouch(tmp.dir) |
4907 | + with self.assertRaises(Exception) as cm: |
4908 | + inst.create_user() |
4909 | + self.assertEqual(str(cm.exception), 'must create machine first') |
4910 | + machine_id = inst.create_machine() |
4911 | + user_id = inst.create_user() |
4912 | + self.assertNotEqual(machine_id, user_id) |
4913 | + self.assertIsInstance(user_id, str) |
4914 | + self.assertEqual(len(user_id), 48) |
4915 | self.assertIsInstance(inst.user, dict) |
4916 | - self.assertEqual(inst.user, inst.load_config('user')) |
4917 | + self.assertEqual(inst.user['_id'], user_id) |
4918 | + self.assertEqual(inst.load_config('user'), inst.user) |
4919 | + |
4920 | + with self.assertRaises(Exception) as cm: |
4921 | + inst.create_user() |
4922 | + self.assertEqual(str(cm.exception), 'user already exists') |
4923 | + |
4924 | + def test_get_ssl_config(self): |
4925 | + tmp = TempDir() |
4926 | + inst = startup.DmediaCouch(tmp.dir) |
4927 | + machine_id = inst.create_machine() |
4928 | + user_id = inst.create_user() |
4929 | + inst.load_pki() |
4930 | + self.assertEqual( |
4931 | + inst.get_ssl_config(), |
4932 | + { |
4933 | + 'check_hostname': False, |
4934 | + 'max_depth': 1, |
4935 | + 'ca_file': inst.pki.path(user_id, 'ca'), |
4936 | + 'cert_file': inst.pki.path(machine_id, 'cert'), |
4937 | + 'key_file': inst.pki.path(machine_id, 'key'), |
4938 | + } |
4939 | + ) |
4940 | + |
4941 | + def test_get_bootstrap_config(self): |
4942 | + tmp = TempDir() |
4943 | + inst = startup.DmediaCouch(tmp.dir) |
4944 | + machine_id = inst.create_machine() |
4945 | + user_id = inst.create_user() |
4946 | + inst.load_pki() |
4947 | + self.assertEqual( |
4948 | + inst.get_bootstrap_config(), |
4949 | + { |
4950 | + 'username': 'admin', |
4951 | + 'replicator': { |
4952 | + 'check_hostname': False, |
4953 | + 'max_depth': 1, |
4954 | + 'ca_file': inst.pki.path(user_id, 'ca'), |
4955 | + 'cert_file': inst.pki.path(machine_id, 'cert'), |
4956 | + 'key_file': inst.pki.path(machine_id, 'key'), |
4957 | + }, |
4958 | + } |
4959 | + ) |
4960 | |
4961 | def test_auto_bootstrap(self): |
4962 | tmp = TempDir() |
4963 | inst = startup.DmediaCouch(tmp.dir) |
4964 | - env = inst.auto_bootstrap() |
4965 | - s = Server(env) |
4966 | - self.assertEqual(s.get()['couchdb'], 'Welcome') |
4967 | - self.assertIsNone(inst.get_ssl_config()) |
4968 | - |
4969 | - tmp = TempDir() |
4970 | - inst = startup.DmediaCouch(tmp.dir) |
4971 | - inst.firstrun_init(create_user=False) |
4972 | - env = inst.auto_bootstrap() |
4973 | - s = Server(env) |
4974 | - self.assertEqual(s.get()['couchdb'], 'Welcome') |
4975 | - self.assertIsNone(inst.get_ssl_config()) |
4976 | - |
4977 | - tmp = TempDir() |
4978 | - inst = startup.DmediaCouch(tmp.dir) |
4979 | - inst.firstrun_init(create_user=True) |
4980 | + inst.create_machine() |
4981 | + inst.create_user() |
4982 | env = inst.auto_bootstrap() |
4983 | s = Server(env) |
4984 | self.assertEqual(s.get()['couchdb'], 'Welcome') |
4985 | @@ -249,3 +229,4 @@ |
4986 | 'key_file': inst.pki.machine.key_file, |
4987 | } |
4988 | ) |
4989 | + |
4990 | |
4991 | === modified file 'run-browse.py' |
4992 | --- run-browse.py 2012-10-09 04:05:45 +0000 |
4993 | +++ run-browse.py 2012-10-16 12:35:25 +0000 |
4994 | @@ -130,7 +130,8 @@ |
4995 | class Browse: |
4996 | def __init__(self): |
4997 | self.couch = DmediaCouch(tempfile.mkdtemp()) |
4998 | - self.couch.firstrun_init(create_user=True) |
4999 | + self.couch.create_machine() |
5000 | + self.couch.create_user() |
Bad form and all, I'm going to self-approve this. This feature is going into 12.10, and so I want to get as much testing as possible before the release. Time to ram this through the daily build.