Add encrypted configuration backup and planned restore
This commit is contained in:
@@ -136,6 +136,8 @@ class Server(ThreadingHTTPServer):
|
||||
session=self.sessions.get(token)
|
||||
return bool(record and session and session['expires']>time.monotonic() and secrets.compare_digest(session['revision'],record['password']['hash']))
|
||||
self.endpoint.video_browser_auth=video_browser_auth
|
||||
from backup import Backup
|
||||
self.backup=Backup(self)
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args):
|
||||
@@ -288,7 +290,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return self.respond({'error':'Anmeldung erforderlich.'},401)
|
||||
if not self.authenticated() and self.path == '/':
|
||||
return self.respond((ROOT/'login.html').read_bytes(), mime='text/html; charset=utf-8')
|
||||
routes = {'/dashboard-ui.js':('dashboard-ui.js','text/javascript'),'/dashboard.css':('dashboard.css','text/css'),'/themes.css':('themes.css','text/css'),'/video-ui.js':('video-ui.js','text/javascript'),'/stt-ui.js':('stt-ui.js','text/javascript'),'/tts-ui.js':('tts-ui.js','text/javascript'),'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')}
|
||||
routes = {'/backup-ui.js':('backup-ui.js','text/javascript'),'/dashboard-ui.js':('dashboard-ui.js','text/javascript'),'/dashboard.css':('dashboard.css','text/css'),'/themes.css':('themes.css','text/css'),'/video-ui.js':('video-ui.js','text/javascript'),'/stt-ui.js':('stt-ui.js','text/javascript'),'/tts-ui.js':('tts-ui.js','text/javascript'),'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')}
|
||||
if self.path in routes:
|
||||
name, mime = routes[self.path]
|
||||
return self.respond((ROOT/name).read_bytes(), mime=mime)
|
||||
@@ -297,6 +299,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
public_endpoint={key:endpoint[key] for key in ('state','port','counts','active_requests')}
|
||||
return self.respond(dict(name='Athena Deck', version='0.7.0', state='ready', uptime_seconds=round(time.time()-self.server.started), mode='isolated', location=os.environ.get('DECK_LOCATION', 'Athena · Debian-Server'), endpoint=public_endpoint))
|
||||
if self.path == '/api/v1/auto-tests':return self.respond(self.server.auto_tests.status())
|
||||
if self.path == '/api/v1/backup':return self.respond(self.server.backup.status())
|
||||
if self.path == '/api/v1/chat-tests':return self.respond(self.server.chat_tests.status())
|
||||
if self.path == '/api/v1/endpoint':return self.respond(self.server.endpoint.status())
|
||||
if self.path == '/api/v1/docker':return self.respond(self.server.docker.status())
|
||||
@@ -396,6 +399,36 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return self.login()
|
||||
if not self.authenticated():
|
||||
return self.respond({'error':'Anmeldung oder gültiger API-Token erforderlich.'},401)
|
||||
if self.server.backup.busy() and not self.path.startswith('/api/v1/backup/') and self.path != '/api/v1/logout':
|
||||
return self.respond({'error':'Wiederherstellung läuft. Änderungen und Tests sind vorübergehend gesperrt.'},409)
|
||||
if self.path.startswith('/api/v1/backup/'):
|
||||
try:
|
||||
if not self.authenticated(session_only=True):return self.respond({'error':'Administratorsitzung erforderlich.'},401)
|
||||
action=self.path.rsplit('/',1)[1]
|
||||
if action=='export':
|
||||
data=self.read_json()
|
||||
if set(data)!={'password','theme','history'} or type(data['history']) is not bool:raise ValueError('Backup-Kennwort, Theme und History-Auswahl erforderlich.')
|
||||
body=self.server.backup.export(data['password'],data['theme'],data['history'])
|
||||
self.send_response(200);self.send_header('Content-Type','application/octet-stream');self.send_header('Content-Length',str(len(body)));self.send_header('Content-Disposition','attachment; filename="athena-deck-'+time.strftime('%Y%m%d-%H%M%S')+'.adbackup"');self.send_header('Cache-Control','no-store');self.end_headers();self.wfile.write(body);return
|
||||
if action=='inspect':
|
||||
from backup_codec import MAX
|
||||
self.connection.settimeout(60)
|
||||
length=int(self.headers.get('Content-Length','0'))
|
||||
if self.headers.get('Transfer-Encoding') or self.headers.get('Content-Type')!='application/octet-stream' or not 0<length<=MAX:raise ValueError('Backup-Datei erwartet; maximal 64 MiB.')
|
||||
raw=self.rfile.read(length)
|
||||
if len(raw)!=length:raise ValueError('Backup-Upload unvollständig.')
|
||||
return self.respond(self.server.backup.inspect(raw,__import__('base64').b64decode(self.headers.get('X-Backup-Passphrase',''),validate=True).decode('utf-8')))
|
||||
if action=='checkpoint':
|
||||
data=self.read_json()
|
||||
if set(data)!={'name'}:raise ValueError('Rückfallstand erforderlich.')
|
||||
return self.respond(self.server.backup.inspect_checkpoint(data['name']))
|
||||
if action=='restore':return self.respond(self.server.backup.start(self.read_json()),202)
|
||||
if action=='cancel':
|
||||
if self.read_json():raise ValueError('Keine Parameter erwartet.')
|
||||
return self.respond(self.server.backup.stop())
|
||||
raise ValueError('Unbekannte Backup-Aktion.')
|
||||
except ValueError as exc:return self.respond({'error':str(exc)},400)
|
||||
except Exception:return self.respond({'error':'Backup-Aktion fehlgeschlagen. Dateiformat, freien Speicher und Systemhelfer prüfen.'},503)
|
||||
if self.path == '/api/v1/audio-policy':
|
||||
try:return self.respond(self.server.audio_policy.configure(self.read_json()))
|
||||
except ValueError as exc:return self.respond({'error':str(exc)},400)
|
||||
@@ -603,6 +636,7 @@ def main():
|
||||
try:
|
||||
server.serve_forever()
|
||||
finally:
|
||||
server.backup.stop()
|
||||
server.dashboard.close()
|
||||
server.auto_tests.stop()
|
||||
server.chat_tests.stop()
|
||||
|
||||
Reference in New Issue
Block a user