feat: WireGuard VPN-Funktionalität hinzugefügt und Versionsnummer auf 1.0.9 erhöht
This commit is contained in:
364
app.py
364
app.py
@@ -61,6 +61,8 @@ DEFAULT_CONFIG = {
|
||||
# WiFi
|
||||
'wifi_ssid': '', 'wifi_password': '',
|
||||
'ap_ssid': 'PiCopy', 'ap_password': 'PiCopy,',
|
||||
# WireGuard
|
||||
'wireguard_auto': False,
|
||||
}
|
||||
|
||||
# ── Persistenter Kopierstatus ───────────────────────────────────────────────
|
||||
@@ -268,6 +270,146 @@ def wifi_monitor():
|
||||
|
||||
time.sleep(30)
|
||||
|
||||
# ── WireGuard VPN ─────────────────────────────────────────────────────────────
|
||||
|
||||
WG_CONF = Path('/etc/wireguard/picopy.conf')
|
||||
WG_IFACE = 'picopy'
|
||||
|
||||
def wg_is_installed():
|
||||
return shutil.which('wg-quick') is not None
|
||||
|
||||
|
||||
wg_state = {
|
||||
'connected': False,
|
||||
'ip': '',
|
||||
'peer': '',
|
||||
'error': None,
|
||||
'has_config': False,
|
||||
'installed': False,
|
||||
'pkg_running': False,
|
||||
'pkg_action': '',
|
||||
'pkg_error': None,
|
||||
}
|
||||
wg_lock = threading.Lock()
|
||||
|
||||
|
||||
def wg_update_state():
|
||||
inst = wg_is_installed()
|
||||
has_conf = WG_CONF.exists()
|
||||
if not inst:
|
||||
with wg_lock:
|
||||
wg_state.update(installed=False, connected=False, ip='', peer='',
|
||||
has_config=has_conf)
|
||||
return
|
||||
r = subprocess.run(['wg', 'show', WG_IFACE],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if r.returncode != 0:
|
||||
with wg_lock:
|
||||
wg_state.update(installed=True, connected=False, ip='', peer='',
|
||||
has_config=has_conf)
|
||||
return
|
||||
ip_r = subprocess.run(['ip', '-4', 'addr', 'show', WG_IFACE],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
ip = ''
|
||||
for line in ip_r.stdout.splitlines():
|
||||
if line.strip().startswith('inet '):
|
||||
ip = line.strip().split()[1].split('/')[0]
|
||||
break
|
||||
peer = ''
|
||||
for line in r.stdout.splitlines():
|
||||
if line.startswith('peer:'):
|
||||
peer = line.split(':', 1)[-1].strip()
|
||||
break
|
||||
with wg_lock:
|
||||
wg_state.update(installed=True, connected=True, ip=ip, peer=peer,
|
||||
error=None, has_config=has_conf)
|
||||
|
||||
|
||||
def wg_connect():
|
||||
if not WG_CONF.exists():
|
||||
with wg_lock:
|
||||
wg_state['error'] = 'Keine Konfiguration vorhanden'
|
||||
return False
|
||||
r = subprocess.run(['wg-quick', 'up', WG_IFACE],
|
||||
capture_output=True, text=True, timeout=30)
|
||||
if r.returncode == 0:
|
||||
time.sleep(1)
|
||||
wg_update_state()
|
||||
log.info('WireGuard verbunden')
|
||||
return True
|
||||
err = (r.stderr.strip().splitlines()[-1]
|
||||
if r.stderr.strip() else 'Unbekannter Fehler')
|
||||
with wg_lock:
|
||||
wg_state.update(connected=False, error=err)
|
||||
log.error(f'WireGuard Fehler: {err}')
|
||||
return False
|
||||
|
||||
|
||||
def wg_disconnect():
|
||||
r = subprocess.run(['wg-quick', 'down', WG_IFACE],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
with wg_lock:
|
||||
wg_state.update(connected=False, ip='', peer='', error=None)
|
||||
log.info('WireGuard getrennt')
|
||||
return r.returncode == 0
|
||||
|
||||
|
||||
def _wg_apt(action: str, packages: list):
|
||||
"""Führt apt-get install/remove aus und aktualisiert pkg_state."""
|
||||
with wg_lock:
|
||||
if wg_state['pkg_running']:
|
||||
return
|
||||
wg_state.update(pkg_running=True, pkg_action=action, pkg_error=None)
|
||||
try:
|
||||
cmd = ['apt-get', action, '-y'] + packages
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=300,
|
||||
env={**os.environ, 'DEBIAN_FRONTEND': 'noninteractive'})
|
||||
if r.returncode != 0:
|
||||
err = (r.stderr.strip().splitlines()[-1]
|
||||
if r.stderr.strip() else f'apt-get {action} fehlgeschlagen')
|
||||
log.error(f'WireGuard apt {action}: {err}')
|
||||
with wg_lock:
|
||||
wg_state['pkg_error'] = err
|
||||
else:
|
||||
log.info(f'WireGuard apt {action} abgeschlossen')
|
||||
except Exception as e:
|
||||
with wg_lock:
|
||||
wg_state['pkg_error'] = str(e)
|
||||
finally:
|
||||
with wg_lock:
|
||||
wg_state['pkg_running'] = False
|
||||
wg_state['pkg_action'] = ''
|
||||
wg_update_state()
|
||||
|
||||
|
||||
def wg_install():
|
||||
_wg_apt('install', ['wireguard', 'wireguard-tools'])
|
||||
|
||||
|
||||
def wg_uninstall():
|
||||
wg_disconnect()
|
||||
_wg_apt('remove', ['wireguard', 'wireguard-tools'])
|
||||
|
||||
|
||||
def wg_save_config(content: str):
|
||||
try:
|
||||
WG_CONF.parent.mkdir(parents=True, exist_ok=True)
|
||||
WG_CONF.write_text(content, encoding='utf-8')
|
||||
WG_CONF.chmod(0o600)
|
||||
return True, ''
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def wg_monitor():
|
||||
while True:
|
||||
try:
|
||||
wg_update_state()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
# ── USB Geräteerkennung ───────────────────────────────────────────────────────
|
||||
|
||||
def usb_port_of(dev_name):
|
||||
@@ -774,7 +916,9 @@ def r_status():
|
||||
cs = dict(copy_state)
|
||||
with wifi_lock:
|
||||
ws = dict(wifi_state)
|
||||
return jsonify(copy=cs, wifi=ws)
|
||||
with wg_lock:
|
||||
wgs = dict(wg_state)
|
||||
return jsonify(copy=cs, wifi=ws, vpn=wgs)
|
||||
|
||||
@app.route('/api/copy/start', methods=['POST'])
|
||||
def r_start():
|
||||
@@ -860,6 +1004,63 @@ def r_wifi_status():
|
||||
return jsonify(dict(wifi_state))
|
||||
|
||||
|
||||
# ── WireGuard Routes ─────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/api/wireguard/config', methods=['GET', 'POST'])
|
||||
def r_wg_config():
|
||||
if request.method == 'POST':
|
||||
data = request.get_json(force=True)
|
||||
content = data.get('content', '')
|
||||
if not content.strip():
|
||||
return jsonify(error='Konfiguration ist leer'), 400
|
||||
ok, err = wg_save_config(content)
|
||||
if not ok:
|
||||
return jsonify(error=err), 500
|
||||
auto = data.get('auto')
|
||||
if auto is not None:
|
||||
c = load_cfg()
|
||||
c['wireguard_auto'] = bool(auto)
|
||||
save_cfg(c)
|
||||
with wg_lock:
|
||||
wg_state['has_config'] = True
|
||||
return jsonify(ok=True)
|
||||
if WG_CONF.exists():
|
||||
content = WG_CONF.read_text(encoding='utf-8')
|
||||
masked = re.sub(r'(PrivateKey\s*=\s*)(.+)', r'\1****', content)
|
||||
return jsonify(exists=True, config=masked)
|
||||
return jsonify(exists=False, config='')
|
||||
|
||||
|
||||
@app.route('/api/wireguard/connect', methods=['POST'])
|
||||
def r_wg_connect():
|
||||
threading.Thread(target=wg_connect, daemon=True).start()
|
||||
return jsonify(ok=True, msg='Verbindungsversuch gestartet')
|
||||
|
||||
|
||||
@app.route('/api/wireguard/disconnect', methods=['POST'])
|
||||
def r_wg_disconnect():
|
||||
ok = wg_disconnect()
|
||||
return jsonify(ok=ok)
|
||||
|
||||
|
||||
@app.route('/api/wireguard/install', methods=['POST'])
|
||||
def r_wg_install():
|
||||
with wg_lock:
|
||||
if wg_state['pkg_running']:
|
||||
return jsonify(error='Bereits aktiv'), 400
|
||||
threading.Thread(target=wg_install, daemon=True).start()
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
@app.route('/api/wireguard/uninstall', methods=['POST'])
|
||||
def r_wg_uninstall():
|
||||
with wg_lock:
|
||||
if wg_state['pkg_running']:
|
||||
return jsonify(error='Bereits aktiv'), 400
|
||||
threading.Thread(target=wg_uninstall, daemon=True).start()
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
# ── Upload Routes ──────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route('/api/upload/targets', methods=['GET'])
|
||||
@@ -1332,6 +1533,9 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
.sec{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--sub);padding:.1rem 0;margin:.7rem 0 .5rem;display:flex;align-items:center;gap:.5rem}
|
||||
.sec::after{content:'';flex:1;height:1px;background:var(--brd)}
|
||||
.empty{color:var(--sub);font-size:.85rem;padding:.3rem 0}
|
||||
|
||||
/* ── WireGuard VPN ── */
|
||||
.wdot.vpn{background:var(--pur);box-shadow:0 0 6px var(--pur)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1350,6 +1554,11 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
<span id="wifi-label">Verbinde…</span>
|
||||
<span id="wifi-ip"></span>
|
||||
</div>
|
||||
<div id="vpn-pill" class="topbar-wifi" style="display:none">
|
||||
<div class="wdot d" id="vpn-dot"></div>
|
||||
<span id="vpn-label" style="font-weight:600;color:var(--txt)">VPN</span>
|
||||
<span id="vpn-ip" style="color:var(--sub);font-family:monospace;font-size:.76rem"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="page">
|
||||
@@ -1618,6 +1827,72 @@ body{background:var(--bg);color:var(--txt);font-family:-apple-system,BlinkMacSys
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── WireGuard VPN ── -->
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
<div class="card-icon pur">⚿</div>
|
||||
<span class="card-title">WireGuard VPN</span>
|
||||
<span class="card-sub" id="wg-status-sub"></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Paket nicht installiert -->
|
||||
<div id="wg-not-installed" style="display:none">
|
||||
<div style="display:flex;align-items:center;gap:.75rem;padding:.75rem .9rem;background:rgba(251,191,36,.07);border:1px solid rgba(251,191,36,.28);border-radius:.5rem;margin-bottom:.6rem">
|
||||
<span style="font-size:1.3rem;flex-shrink:0">📦</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:600;font-size:.87rem;color:var(--ylw)">WireGuard nicht installiert</div>
|
||||
<div style="font-size:.76rem;color:var(--sub);margin-top:.15rem">wireguard + wireguard-tools werden per apt-get auf dem Pi installiert</div>
|
||||
</div>
|
||||
<button class="btn pri" onclick="wgInstall()" style="flex-shrink:0">Installieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paketoperation läuft -->
|
||||
<div id="wg-pkg-progress" style="display:none">
|
||||
<div style="display:flex;align-items:center;gap:.75rem;padding:.75rem .9rem;background:rgba(79,142,247,.07);border:1px solid rgba(79,142,247,.25);border-radius:.5rem;margin-bottom:.6rem">
|
||||
<span style="font-size:1.3rem;flex-shrink:0" id="wg-pkg-icon">⏳</span>
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:.87rem" id="wg-pkg-title">Installiere WireGuard…</div>
|
||||
<div style="font-size:.76rem;color:var(--sub);margin-top:.1rem">apt-get läuft – bitte warten (bis 60 s)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hauptbereich (wenn installiert) -->
|
||||
<div id="wg-installed-ui" style="display:none">
|
||||
|
||||
<!-- Verbindungsstatus -->
|
||||
<div style="display:flex;align-items:center;gap:.75rem;margin-bottom:.85rem;padding:.65rem .85rem;background:var(--bg2);border-radius:.5rem;border:1px solid var(--brd)">
|
||||
<div id="wg-dot" class="wdot d"></div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div id="wg-label" style="font-weight:600;font-size:.87rem">Getrennt</div>
|
||||
<div id="wg-detail" style="font-size:.74rem;color:var(--sub);font-family:monospace;margin-top:.1rem"></div>
|
||||
</div>
|
||||
<button id="wg-btn-connect" class="btn grn sm" onclick="wgConnect()" style="display:none">⚿ Verbinden</button>
|
||||
<button id="wg-btn-disconnect" class="btn danger sm" onclick="wgDisconnect()" style="display:none">✕ Trennen</button>
|
||||
</div>
|
||||
|
||||
<!-- Konfiguration -->
|
||||
<div class="sec" style="margin-top:0">Konfiguration</div>
|
||||
<div style="font-size:.8rem;color:var(--sub);margin-bottom:.65rem;line-height:1.5">
|
||||
WireGuard .conf einfügen — wird als <code style="background:var(--bg2);padding:.1rem .3rem;border-radius:.25rem">/etc/wireguard/picopy.conf</code> gespeichert (Permissions 600). Der private Schlüssel wird maskiert angezeigt.
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>WireGuard Konfiguration (.conf)</label>
|
||||
<textarea id="wg-config" rows="9" placeholder="[Interface] PrivateKey = ... Address = 10.x.x.x/32 DNS = ... [Peer] PublicKey = ... Endpoint = mein-nas.dyndns.org:51820 AllowedIPs = 192.168.1.0/24" style="font-family:ui-monospace,monospace;font-size:.77rem;line-height:1.6"></textarea>
|
||||
</div>
|
||||
<label class="tog"><input type="checkbox" id="wg-auto"><span>Beim Start automatisch verbinden</span></label>
|
||||
<div class="btn-row">
|
||||
<button class="btn pri" onclick="wgSaveConfig()">✓ Konfiguration speichern</button>
|
||||
<button class="btn danger" onclick="wgUninstall()" style="margin-left:auto" title="wireguard-Paket entfernen">✕ Deinstallieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="wg-flash" class="flash" style="margin-top:.4rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── System ── -->
|
||||
<div class="card">
|
||||
<div class="card-head">
|
||||
@@ -1957,7 +2232,44 @@ function fmtSpd(bps){
|
||||
// ── Poll ──────────────────────────────────────────────────────────────────────
|
||||
async function poll(){
|
||||
try{
|
||||
const {copy:c,wifi:w}=await api('/status');
|
||||
const {copy:c,wifi:w,vpn:v}=await api('/status');
|
||||
// VPN Topbar + Card
|
||||
if(v){
|
||||
const vp=$('vpn-pill'),vdot=$('vpn-dot'),vl=$('vpn-label'),vi=$('vpn-ip');
|
||||
const ni=$('wg-not-installed'),pp=$('wg-pkg-progress'),ui=$('wg-installed-ui');
|
||||
if(v.pkg_running){
|
||||
ni.style.display='none'; pp.style.display='block'; ui.style.display='none';
|
||||
const act=v.pkg_action==='remove'?'Deinstalliere':'Installiere';
|
||||
$('wg-pkg-title').textContent=act+' WireGuard…';
|
||||
$('wg-pkg-icon').textContent='⏳';
|
||||
$('wg-status-sub').textContent=act+'…';
|
||||
vp.style.display='none';
|
||||
} else if(!v.installed){
|
||||
ni.style.display='block'; pp.style.display='none'; ui.style.display='none';
|
||||
$('wg-status-sub').textContent='Nicht installiert';
|
||||
vp.style.display='none';
|
||||
if(v.pkg_error) flash('wg-flash','err',v.pkg_error);
|
||||
} else {
|
||||
ni.style.display='none'; pp.style.display='none'; ui.style.display='block';
|
||||
const wgd=$('wg-dot'),wgl=$('wg-label'),wgdet=$('wg-detail');
|
||||
const bc=$('wg-btn-connect'),bd=$('wg-btn-disconnect');
|
||||
if(v.connected){
|
||||
vp.style.display='flex'; vdot.className='wdot c';
|
||||
vl.textContent='VPN aktiv'; vi.textContent=v.ip||'';
|
||||
wgd.className='wdot c'; wgl.textContent='Verbunden';
|
||||
wgdet.textContent=v.ip?(v.ip+(v.peer?' · peer …'+v.peer.slice(-8):'')):'';
|
||||
bc.style.display='none'; bd.style.display=''; bd.disabled=false;
|
||||
$('wg-status-sub').textContent=v.ip||'';
|
||||
} else {
|
||||
vp.style.display=v.has_config?'flex':'none';
|
||||
vdot.className='wdot d'; vl.textContent='VPN'; vi.textContent='';
|
||||
wgd.className='wdot d'; wgl.textContent='Getrennt';
|
||||
wgdet.textContent=v.error||'';
|
||||
bc.style.display=v.has_config?'':'none'; bc.disabled=false; bd.style.display='none';
|
||||
$('wg-status-sub').textContent=v.has_config?'Konfiguriert':'Nicht konfiguriert';
|
||||
}
|
||||
}
|
||||
}
|
||||
// WiFi
|
||||
const wd=$('wdot'),wl=$('wifi-label'),wi=$('wifi-ip');
|
||||
if(w.mode==='client'){wd.className='wdot c';wl.textContent=w.ssid||'Verbunden';wi.textContent=w.ip||'';}
|
||||
@@ -2120,6 +2432,49 @@ async function rebootDevice() {
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// ── WireGuard VPN ─────────────────────────────────────────────────────────────
|
||||
async function wgInstall(){
|
||||
if(!confirm('wireguard + wireguard-tools jetzt per apt-get installieren?\n\nDauer: ca. 30–90 Sekunden.'))return;
|
||||
flash('wg-flash','ok','Starte Installation…');
|
||||
const r=await api('/wireguard/install','POST');
|
||||
if(r.error) flash('wg-flash','err',r.error);
|
||||
}
|
||||
async function wgUninstall(){
|
||||
if(!confirm('WireGuard wirklich deinstallieren?\n\nDer aktive VPN-Tunnel wird vorher getrennt.\nDie Konfigurationsdatei bleibt erhalten.'))return;
|
||||
flash('wg-flash','ok','Deinstalliere…');
|
||||
const r=await api('/wireguard/uninstall','POST');
|
||||
if(r.error) flash('wg-flash','err',r.error);
|
||||
}
|
||||
async function wgConnect(){
|
||||
$('wg-btn-connect').disabled=true;
|
||||
flash('wg-flash','ok','Verbinde VPN…');
|
||||
await api('/wireguard/connect','POST');
|
||||
}
|
||||
async function wgDisconnect(){
|
||||
$('wg-btn-disconnect').disabled=true;
|
||||
flash('wg-flash','ok','Trenne VPN…');
|
||||
const r=await api('/wireguard/disconnect','POST');
|
||||
if(!r.ok) flash('wg-flash','err','Trennen fehlgeschlagen');
|
||||
}
|
||||
async function wgSaveConfig(){
|
||||
const content=$('wg-config').value.trim();
|
||||
if(!content){flash('wg-flash','err','Konfiguration ist leer');return;}
|
||||
if(!content.includes('[Interface]')){flash('wg-flash','err','Ungültige Konfiguration – [Interface] fehlt');return;}
|
||||
const auto=$('wg-auto').checked;
|
||||
flash('wg-flash','ok','Speichere…');
|
||||
const r=await api('/wireguard/config','POST',{content,auto});
|
||||
if(r.error){flash('wg-flash','err',r.error);return;}
|
||||
flash('wg-flash','ok','✓ Konfiguration gespeichert');
|
||||
}
|
||||
async function loadWgConfig(){
|
||||
try{
|
||||
const r=await api('/wireguard/config');
|
||||
if(r.exists && r.config) $('wg-config').value=r.config;
|
||||
const c=await api('/config');
|
||||
$('wg-auto').checked=!!c.wireguard_auto;
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
function flash(id,cls,msg){
|
||||
const el=$(id); el.className='flash '+cls; el.textContent=msg; el.style.display='block';
|
||||
if(cls==='ok') setTimeout(()=>el.style.display='none',3500);
|
||||
@@ -2129,6 +2484,7 @@ function flash(id,cls,msg){
|
||||
await loadCfg();
|
||||
await refreshDevices();
|
||||
await loadUTs();
|
||||
await loadWgConfig();
|
||||
expl.load('');
|
||||
setInterval(poll,1500);
|
||||
setInterval(refreshDevices,8000);
|
||||
@@ -2144,8 +2500,12 @@ function flash(id,cls,msg){
|
||||
if __name__ == '__main__':
|
||||
cleanup_stale_mounts()
|
||||
load_state()
|
||||
wg_update_state()
|
||||
threading.Thread(target=usb_monitor, daemon=True).start()
|
||||
threading.Thread(target=wifi_monitor, daemon=True).start()
|
||||
threading.Thread(target=wg_monitor, daemon=True).start()
|
||||
threading.Thread(target=update_check_loop, daemon=True).start()
|
||||
if load_cfg().get('wireguard_auto') and WG_CONF.exists():
|
||||
threading.Thread(target=wg_connect, daemon=True).start()
|
||||
log.info(f'PiCopy v{VERSION} läuft auf http://0.0.0.0:8080')
|
||||
app.run(host='0.0.0.0', port=8080, debug=False, use_reloader=False)
|
||||
|
||||
Reference in New Issue
Block a user