vssh 3.3.0__tar.gz → 3.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vssh-3.3.0/vssh.egg-info → vssh-3.3.1}/PKG-INFO +2 -2
- {vssh-3.3.0 → vssh-3.3.1}/pyproject.toml +2 -2
- {vssh-3.3.0 → vssh-3.3.1/vssh.egg-info}/PKG-INFO +2 -2
- {vssh-3.3.0 → vssh-3.3.1}/vssh.py +81 -70
- {vssh-3.3.0 → vssh-3.3.1}/vssh_mcp_server.py +10 -10
- {vssh-3.3.0 → vssh-3.3.1}/vssh_p2p.py +10 -10
- {vssh-3.3.0 → vssh-3.3.1}/vssh_p2p_fast.py +6 -6
- {vssh-3.3.0 → vssh-3.3.1}/vssh_quic.py +1 -1
- {vssh-3.3.0 → vssh-3.3.1}/LICENSE +0 -0
- {vssh-3.3.0 → vssh-3.3.1}/README.md +0 -0
- {vssh-3.3.0 → vssh-3.3.1}/setup.cfg +0 -0
- {vssh-3.3.0 → vssh-3.3.1}/vssh.egg-info/SOURCES.txt +0 -0
- {vssh-3.3.0 → vssh-3.3.1}/vssh.egg-info/dependency_links.txt +0 -0
- {vssh-3.3.0 → vssh-3.3.1}/vssh.egg-info/entry_points.txt +0 -0
- {vssh-3.3.0 → vssh-3.3.1}/vssh.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vssh
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
|
|
5
|
-
Author-email: MeshPOP <
|
|
5
|
+
Author-email: MeshPOP <mpop@mpop.dev>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/meshpop/vssh
|
|
8
8
|
Project-URL: Repository, https://github.com/meshpop/vssh
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vssh"
|
|
7
|
-
version = "3.3.
|
|
7
|
+
version = "3.3.1"
|
|
8
8
|
description = "Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
11
11
|
requires-python = ">=3.8"
|
|
12
12
|
authors = [
|
|
13
|
-
{name = "MeshPOP", email = "
|
|
13
|
+
{name = "MeshPOP", email = "mpop@mpop.dev"}
|
|
14
14
|
]
|
|
15
15
|
keywords = ["ssh", "scp", "tailscale", "p2p", "vpn", "mcp", "remote"]
|
|
16
16
|
classifiers = [
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vssh
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
|
|
5
|
-
Author-email: MeshPOP <
|
|
5
|
+
Author-email: MeshPOP <mpop@mpop.dev>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/meshpop/vssh
|
|
8
8
|
Project-URL: Repository, https://github.com/meshpop/vssh
|
|
@@ -50,7 +50,7 @@ def _load_tailscale_map() -> dict:
|
|
|
50
50
|
"""Load Wire VPN IP → Tailscale IP mapping"""
|
|
51
51
|
ts_map = {}
|
|
52
52
|
|
|
53
|
-
# 1. From config: TAILSCALE.10.99.
|
|
53
|
+
# 1. From config: TAILSCALE.10.99.85.143 = 100.80.191.6
|
|
54
54
|
for k, v in VSSH_CONFIG.items():
|
|
55
55
|
if k.startswith('TAILSCALE.'):
|
|
56
56
|
wire_ip = k[len('TAILSCALE.'):]
|
|
@@ -67,8 +67,8 @@ def _load_tailscale_map() -> dict:
|
|
|
67
67
|
parts = line.split()
|
|
68
68
|
if len(parts) >= 2:
|
|
69
69
|
ts_map[parts[0]] = parts[1] # wire_ip tailscale_ip
|
|
70
|
-
except:
|
|
71
|
-
pass
|
|
70
|
+
except (OSError, ValueError) as e:
|
|
71
|
+
pass # TODO: log error
|
|
72
72
|
|
|
73
73
|
return ts_map
|
|
74
74
|
|
|
@@ -92,7 +92,7 @@ def vssh_connect(host: str, timeout: float = 5.0) -> socket.socket:
|
|
|
92
92
|
sock.settimeout(timeout)
|
|
93
93
|
sock.connect((ts_ip, PORT))
|
|
94
94
|
return sock
|
|
95
|
-
except:
|
|
95
|
+
except (OSError, socket.error):
|
|
96
96
|
# Tailscale also failed — clear cache, try Wire again
|
|
97
97
|
del _FAILOVER_ACTIVE[host]
|
|
98
98
|
|
|
@@ -103,7 +103,7 @@ def vssh_connect(host: str, timeout: float = 5.0) -> socket.socket:
|
|
|
103
103
|
sock.connect((host, PORT))
|
|
104
104
|
return sock
|
|
105
105
|
except (ConnectionRefusedError, ConnectionError, OSError, socket.timeout) as wire_err:
|
|
106
|
-
pass
|
|
106
|
+
pass # TODO: log error
|
|
107
107
|
|
|
108
108
|
# Fallback: try Tailscale IP
|
|
109
109
|
ts_ip = TAILSCALE_MAP.get(host)
|
|
@@ -148,7 +148,7 @@ def log_transfer(op: str, host: str, path: str, size: int, duration: float, stat
|
|
|
148
148
|
_rotate_log(LOG_FILE)
|
|
149
149
|
with open(LOG_FILE, 'a') as f:
|
|
150
150
|
f.write(line)
|
|
151
|
-
except:
|
|
151
|
+
except OSError:
|
|
152
152
|
pass # Don't fail on logging errors
|
|
153
153
|
|
|
154
154
|
def log_event(op: str, host: str, detail: str, status: str = 'OK'):
|
|
@@ -167,8 +167,8 @@ def log_event(op: str, host: str, detail: str, status: str = 'OK'):
|
|
|
167
167
|
_rotate_log(HISTORY_FILE)
|
|
168
168
|
with open(HISTORY_FILE, 'a') as f:
|
|
169
169
|
f.write(line)
|
|
170
|
-
except:
|
|
171
|
-
pass
|
|
170
|
+
except OSError:
|
|
171
|
+
pass # safe to ignore
|
|
172
172
|
|
|
173
173
|
def get_history(count: int = 20, op_filter: str = None, host_filter: str = None) -> list:
|
|
174
174
|
"""Get recent history entries, optionally filtered by op or host"""
|
|
@@ -226,7 +226,7 @@ def get_transfer_stats(days: int = 7) -> dict:
|
|
|
226
226
|
stats['success'] += 1
|
|
227
227
|
else:
|
|
228
228
|
stats['fail'] += 1
|
|
229
|
-
except:
|
|
229
|
+
except Exception:
|
|
230
230
|
continue
|
|
231
231
|
|
|
232
232
|
return stats
|
|
@@ -295,7 +295,7 @@ def verify_auth(received: str, expected_secret: str) -> bool:
|
|
|
295
295
|
try:
|
|
296
296
|
ts = int(parts[1])
|
|
297
297
|
received_hmac = parts[2]
|
|
298
|
-
except:
|
|
298
|
+
except (ValueError, IndexError):
|
|
299
299
|
return False
|
|
300
300
|
|
|
301
301
|
# Check timestamp within window
|
|
@@ -364,8 +364,8 @@ def get_local_ips() -> list:
|
|
|
364
364
|
ip = parts[1].split('/')[0]
|
|
365
365
|
if not ip.startswith('10.99.'):
|
|
366
366
|
ips.append(ip)
|
|
367
|
-
except:
|
|
368
|
-
pass
|
|
367
|
+
except (OSError, subprocess.SubprocessError):
|
|
368
|
+
pass # safe to ignore
|
|
369
369
|
return ips
|
|
370
370
|
|
|
371
371
|
def get_public_ip() -> str:
|
|
@@ -377,10 +377,10 @@ def get_public_ip() -> str:
|
|
|
377
377
|
try:
|
|
378
378
|
with urllib.request.urlopen(url, timeout=3) as r:
|
|
379
379
|
return r.read().decode().strip()
|
|
380
|
-
except:
|
|
380
|
+
except (urllib.error.URLError, OSError):
|
|
381
381
|
continue
|
|
382
|
-
except:
|
|
383
|
-
pass
|
|
382
|
+
except Exception as e:
|
|
383
|
+
pass # TODO: log error
|
|
384
384
|
return ''
|
|
385
385
|
|
|
386
386
|
def get_network_info_local() -> dict:
|
|
@@ -434,8 +434,8 @@ def is_safe_network(host: str) -> Tuple[bool, str]:
|
|
|
434
434
|
second = int(host.split('.')[1])
|
|
435
435
|
if 16 <= second <= 31:
|
|
436
436
|
return True, "lan"
|
|
437
|
-
except:
|
|
438
|
-
pass
|
|
437
|
+
except (ValueError, IndexError) as e:
|
|
438
|
+
pass # TODO: log error
|
|
439
439
|
|
|
440
440
|
# Everything else is public IP - UNSAFE!
|
|
441
441
|
return False, "public_ip_unencrypted"
|
|
@@ -462,7 +462,7 @@ def check_transfer_safety(host: str) -> bool:
|
|
|
462
462
|
|
|
463
463
|
|
|
464
464
|
# Wire VPN coordinator servers for name resolution
|
|
465
|
-
WIRE_SERVERS = [
|
|
465
|
+
WIRE_SERVERS = ["10.99.85.143", "158.247.247.115", "10.99.74.131", "10.99.249.158"]
|
|
466
466
|
_name_cache = {} # name -> vpn_ip
|
|
467
467
|
_name_cache_time = 0
|
|
468
468
|
|
|
@@ -499,7 +499,7 @@ def resolve_name(host: str) -> str:
|
|
|
499
499
|
if host in _name_cache:
|
|
500
500
|
return _name_cache[host]
|
|
501
501
|
break # got peers, just no match
|
|
502
|
-
except:
|
|
502
|
+
except (OSError, subprocess.SubprocessError):
|
|
503
503
|
continue
|
|
504
504
|
return host
|
|
505
505
|
|
|
@@ -556,7 +556,7 @@ def resolve_best_ip(host: str, timeout: float = 0.5) -> str:
|
|
|
556
556
|
|
|
557
557
|
sock.close()
|
|
558
558
|
except Exception as e:
|
|
559
|
-
pass
|
|
559
|
+
pass # safe to ignore
|
|
560
560
|
|
|
561
561
|
return host
|
|
562
562
|
|
|
@@ -568,7 +568,7 @@ def try_connect(host: str, port: int, timeout: float = 0.5) -> bool:
|
|
|
568
568
|
sock.connect((host, port))
|
|
569
569
|
sock.close()
|
|
570
570
|
return True
|
|
571
|
-
except:
|
|
571
|
+
except OSError:
|
|
572
572
|
return False
|
|
573
573
|
|
|
574
574
|
# ========== Persistent Session ==========
|
|
@@ -591,8 +591,8 @@ def cleanup_sessions():
|
|
|
591
591
|
for sid in expired:
|
|
592
592
|
try:
|
|
593
593
|
PERSISTENT_SESSIONS[sid]['conn'].close()
|
|
594
|
-
except:
|
|
595
|
-
pass
|
|
594
|
+
except OSError:
|
|
595
|
+
pass # safe to ignore
|
|
596
596
|
del PERSISTENT_SESSIONS[sid]
|
|
597
597
|
print(f'[SESSION] Expired: {sid[:8]}...')
|
|
598
598
|
return len(expired)
|
|
@@ -644,8 +644,10 @@ def _rpc_get_disk(payload: dict) -> dict:
|
|
|
644
644
|
"""Return disk usage"""
|
|
645
645
|
import subprocess
|
|
646
646
|
path = payload.get('path', '/')
|
|
647
|
+
if any(c in path for c in [';', '|', '&', '`', '$', '(', ')', '\n']):
|
|
648
|
+
return {'error': 'invalid path'}
|
|
647
649
|
try:
|
|
648
|
-
r = subprocess.run(
|
|
650
|
+
r = subprocess.run(['df', '-B1', path], capture_output=True, text=True, timeout=10)
|
|
649
651
|
lines = r.stdout.strip().split('\n')
|
|
650
652
|
if len(lines) >= 2:
|
|
651
653
|
parts = lines[1].split()
|
|
@@ -665,7 +667,7 @@ def _rpc_get_memory(payload: dict) -> dict:
|
|
|
665
667
|
"""Return memory status"""
|
|
666
668
|
import subprocess
|
|
667
669
|
try:
|
|
668
|
-
r = subprocess.run('free -b',
|
|
670
|
+
r = subprocess.run(['free', '-b'], capture_output=True, text=True, timeout=10)
|
|
669
671
|
lines = r.stdout.strip().split('\n')
|
|
670
672
|
if len(lines) >= 2:
|
|
671
673
|
parts = lines[1].split()
|
|
@@ -691,7 +693,7 @@ def _rpc_get_load(payload: dict) -> dict:
|
|
|
691
693
|
'load15': float(parts[2]),
|
|
692
694
|
'processes': parts[3]
|
|
693
695
|
}
|
|
694
|
-
except:
|
|
696
|
+
except (OSError, ValueError, IndexError):
|
|
695
697
|
return {'error': 'load read failed'}
|
|
696
698
|
|
|
697
699
|
@rpc_method('get_processes', 'read')
|
|
@@ -741,17 +743,17 @@ def _rpc_get_logs(payload: dict) -> dict:
|
|
|
741
743
|
if service not in log_paths:
|
|
742
744
|
try:
|
|
743
745
|
r = subprocess.run(
|
|
744
|
-
|
|
745
|
-
|
|
746
|
+
['journalctl', '-u', service, '-n', str(lines), '--no-pager'],
|
|
747
|
+
capture_output=True, text=True, timeout=10
|
|
746
748
|
)
|
|
747
749
|
return {'service': service, 'lines': r.stdout.strip().split('\n')}
|
|
748
|
-
except:
|
|
749
|
-
pass
|
|
750
|
+
except (subprocess.SubprocessError, OSError) as e:
|
|
751
|
+
pass # TODO: log error
|
|
750
752
|
|
|
751
753
|
path = log_paths.get(service)
|
|
752
754
|
if path and os.path.exists(path):
|
|
753
755
|
try:
|
|
754
|
-
r = subprocess.run(
|
|
756
|
+
r = subprocess.run(['tail', f'-{lines}', path], capture_output=True, text=True, timeout=10)
|
|
755
757
|
return {'service': service, 'path': path, 'lines': r.stdout.strip().split('\n')}
|
|
756
758
|
except Exception as e:
|
|
757
759
|
return {'error': str(e)}
|
|
@@ -772,7 +774,7 @@ def _rpc_restart_service(payload: dict) -> dict:
|
|
|
772
774
|
return {'error': f'service not allowed: {service}', 'allowed': allowed}
|
|
773
775
|
|
|
774
776
|
try:
|
|
775
|
-
r = subprocess.run(
|
|
777
|
+
r = subprocess.run(['systemctl', 'restart', service], shell=False, capture_output=True, text=True, timeout=30)
|
|
776
778
|
if r.returncode == 0:
|
|
777
779
|
return {'success': True, 'service': service}
|
|
778
780
|
else:
|
|
@@ -789,7 +791,7 @@ def _rpc_service_status(payload: dict) -> dict:
|
|
|
789
791
|
return {'error': 'service required'}
|
|
790
792
|
|
|
791
793
|
try:
|
|
792
|
-
r = subprocess.run(
|
|
794
|
+
r = subprocess.run(['systemctl', 'is-active', service], shell=False, capture_output=True, text=True, timeout=10)
|
|
793
795
|
status = r.stdout.strip()
|
|
794
796
|
return {'service': service, 'status': status, 'active': status == 'active'}
|
|
795
797
|
except Exception as e:
|
|
@@ -1035,8 +1037,8 @@ def setup_socket(sock, is_send=True):
|
|
|
1035
1037
|
# Linux TCP_QUICKACK
|
|
1036
1038
|
try:
|
|
1037
1039
|
sock.setsockopt(socket.IPPROTO_TCP, 12, 1) # TCP_QUICKACK = 12
|
|
1038
|
-
except:
|
|
1039
|
-
pass
|
|
1040
|
+
except OSError:
|
|
1041
|
+
pass # safe to ignore
|
|
1040
1042
|
sock.settimeout(600)
|
|
1041
1043
|
|
|
1042
1044
|
def file_hash(path: Path) -> str:
|
|
@@ -1464,8 +1466,8 @@ def rsync_put(local: str, remote: str, lan: bool = True) -> bool:
|
|
|
1464
1466
|
finally:
|
|
1465
1467
|
try:
|
|
1466
1468
|
sock.close()
|
|
1467
|
-
except:
|
|
1468
|
-
pass
|
|
1469
|
+
except OSError:
|
|
1470
|
+
pass # safe to ignore
|
|
1469
1471
|
|
|
1470
1472
|
# ========== put_fast: Auto-select best method ==========
|
|
1471
1473
|
FAST_THRESHOLD = 500 * 1024 * 1024 # 500MB for parallel transfer
|
|
@@ -2290,7 +2292,7 @@ def rpc(host: str, method: str, payload: dict = None) -> dict:
|
|
|
2290
2292
|
# Error response
|
|
2291
2293
|
try:
|
|
2292
2294
|
return json.loads(resp.replace(b'__END__', b'').decode())
|
|
2293
|
-
except:
|
|
2295
|
+
except (json.JSONDecodeError, ValueError, UnicodeDecodeError):
|
|
2294
2296
|
return {'error': resp.decode().strip()}
|
|
2295
2297
|
|
|
2296
2298
|
# Send payload
|
|
@@ -2346,8 +2348,8 @@ def pty_session(host: str, name: str = ''):
|
|
|
2346
2348
|
import shutil
|
|
2347
2349
|
ts = shutil.get_terminal_size()
|
|
2348
2350
|
cols, rows = ts.columns, ts.lines
|
|
2349
|
-
except:
|
|
2350
|
-
pass
|
|
2351
|
+
except Exception:
|
|
2352
|
+
pass # safe to ignore
|
|
2351
2353
|
|
|
2352
2354
|
sock = vssh_connect(host, timeout=10)
|
|
2353
2355
|
try:
|
|
@@ -2383,8 +2385,8 @@ def pty_session(host: str, name: str = ''):
|
|
|
2383
2385
|
fd = sys.stdin.fileno()
|
|
2384
2386
|
old_settings = _termios.tcgetattr(fd)
|
|
2385
2387
|
_tty.setraw(fd)
|
|
2386
|
-
except:
|
|
2387
|
-
pass
|
|
2388
|
+
except Exception:
|
|
2389
|
+
pass # safe to ignore
|
|
2388
2390
|
|
|
2389
2391
|
sock.setblocking(False)
|
|
2390
2392
|
|
|
@@ -2397,7 +2399,7 @@ def pty_session(host: str, name: str = ''):
|
|
|
2397
2399
|
while True:
|
|
2398
2400
|
try:
|
|
2399
2401
|
readable, _, _ = _select.select([sys.stdin, sock], [], [], 0.1)
|
|
2400
|
-
except:
|
|
2402
|
+
except (OSError, ValueError):
|
|
2401
2403
|
break
|
|
2402
2404
|
|
|
2403
2405
|
for r in readable:
|
|
@@ -2418,20 +2420,20 @@ def pty_session(host: str, name: str = ''):
|
|
|
2418
2420
|
raise EOFError
|
|
2419
2421
|
sys.stdout.buffer.write(data)
|
|
2420
2422
|
sys.stdout.buffer.flush()
|
|
2421
|
-
except (BlockingIOError):
|
|
2422
|
-
pass
|
|
2423
|
+
except (BlockingIOError) as e:
|
|
2424
|
+
pass # TODO: log error
|
|
2423
2425
|
except (EOFError, ConnectionError):
|
|
2424
2426
|
raise EOFError
|
|
2425
|
-
except (EOFError, KeyboardInterrupt, ConnectionError, BrokenPipeError):
|
|
2426
|
-
pass
|
|
2427
|
+
except (EOFError, KeyboardInterrupt, ConnectionError, BrokenPipeError) as e:
|
|
2428
|
+
pass # TODO: log error
|
|
2427
2429
|
finally:
|
|
2428
2430
|
# Restore terminal
|
|
2429
2431
|
if old_settings:
|
|
2430
2432
|
try:
|
|
2431
2433
|
import termios as _termios
|
|
2432
2434
|
_termios.tcsetattr(sys.stdin.fileno(), _termios.TCSADRAIN, old_settings)
|
|
2433
|
-
except:
|
|
2434
|
-
pass
|
|
2435
|
+
except Exception:
|
|
2436
|
+
pass # safe to ignore
|
|
2435
2437
|
sock.close()
|
|
2436
2438
|
print() # newline after raw mode
|
|
2437
2439
|
|
|
@@ -2585,8 +2587,8 @@ class VsshSession:
|
|
|
2585
2587
|
try:
|
|
2586
2588
|
self.sock.sendall(f"CLOSE:{self.session_id}\n".encode())
|
|
2587
2589
|
self.sock.recv(32) # CLOSE_OK
|
|
2588
|
-
except:
|
|
2589
|
-
pass
|
|
2590
|
+
except OSError as e:
|
|
2591
|
+
pass # TODO: log error
|
|
2590
2592
|
finally:
|
|
2591
2593
|
self.sock.close()
|
|
2592
2594
|
self.sock = None
|
|
@@ -2949,10 +2951,15 @@ def server():
|
|
|
2949
2951
|
|
|
2950
2952
|
elif cmd == 'SSH':
|
|
2951
2953
|
cmdline = ':'.join(parts[2:])
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2954
|
+
_dangerous = ['rm -rf /', 'mkfs ', 'dd if=/dev/zero']
|
|
2955
|
+
if any(d in cmdline for d in _dangerous):
|
|
2956
|
+
conn.sendall(b'FAIL\n')
|
|
2957
|
+
print(f'[SSH] BLOCKED dangerous: {cmdline}')
|
|
2958
|
+
else:
|
|
2959
|
+
conn.sendall(b'OK\n')
|
|
2960
|
+
print(f'[SSH] {addr[0]}: {cmdline}')
|
|
2961
|
+
result = subprocess.run(cmdline, shell=True, capture_output=True, timeout=60)
|
|
2962
|
+
conn.sendall(result.stdout + result.stderr + b'__END__')
|
|
2956
2963
|
|
|
2957
2964
|
elif cmd == 'INFO':
|
|
2958
2965
|
conn.sendall(b'OK\n')
|
|
@@ -2961,7 +2968,7 @@ def server():
|
|
|
2961
2968
|
try:
|
|
2962
2969
|
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=10)
|
|
2963
2970
|
return r.stdout.strip() or r.stderr.strip() or '-'
|
|
2964
|
-
except:
|
|
2971
|
+
except (subprocess.SubprocessError, OSError):
|
|
2965
2972
|
return '-'
|
|
2966
2973
|
|
|
2967
2974
|
info = {
|
|
@@ -3011,11 +3018,15 @@ def server():
|
|
|
3011
3018
|
# Pipe command output to stdout: PIPE_DOWN:auth:cmd
|
|
3012
3019
|
cmdline = ':'.join(parts[2:])
|
|
3013
3020
|
print(f'[PIPE_DOWN] {addr[0]}: {cmdline}')
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3021
|
+
_dangerous = ['rm -rf /', 'mkfs ', 'dd if=/dev/zero']
|
|
3022
|
+
if any(d in cmdline for d in _dangerous):
|
|
3023
|
+
conn.sendall(b'FAIL\n')
|
|
3024
|
+
print(f'[PIPE_DOWN] BLOCKED dangerous: {cmdline}')
|
|
3025
|
+
else:
|
|
3026
|
+
result = subprocess.run(cmdline, shell=True, capture_output=True, timeout=600)
|
|
3027
|
+
output = result.stdout
|
|
3028
|
+
conn.sendall(f'OK:{len(output)}\n'.encode())
|
|
3029
|
+
conn.sendall(output)
|
|
3019
3030
|
|
|
3020
3031
|
elif cmd == 'MPUT':
|
|
3021
3032
|
# Multiplexed put: MPUT:auth:file_count
|
|
@@ -3113,7 +3124,7 @@ def server():
|
|
|
3113
3124
|
payload_data += chunk
|
|
3114
3125
|
try:
|
|
3115
3126
|
payload = json.loads(payload_data.decode())
|
|
3116
|
-
except:
|
|
3127
|
+
except (json.JSONDecodeError, ValueError, UnicodeDecodeError):
|
|
3117
3128
|
payload = {}
|
|
3118
3129
|
|
|
3119
3130
|
print(f'[RPC] {addr[0]}: {method}({payload})')
|
|
@@ -3201,8 +3212,8 @@ def server():
|
|
|
3201
3212
|
payload_data += chunk
|
|
3202
3213
|
try:
|
|
3203
3214
|
payload = json.loads(payload_data.decode())
|
|
3204
|
-
except:
|
|
3205
|
-
pass
|
|
3215
|
+
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
3216
|
+
pass # TODO: log error
|
|
3206
3217
|
|
|
3207
3218
|
result = handle_rpc(method, payload, sender_ip=addr[0])
|
|
3208
3219
|
conn.sendall(f'RPC_RESULT:{len(result)}\n'.encode())
|
|
@@ -3457,8 +3468,8 @@ Env: VSSH_SECRET
|
|
|
3457
3468
|
if len(args) > 1:
|
|
3458
3469
|
try:
|
|
3459
3470
|
days = int(args[1])
|
|
3460
|
-
except:
|
|
3461
|
-
pass
|
|
3471
|
+
except (ValueError, TypeError) as e:
|
|
3472
|
+
pass # TODO: log error
|
|
3462
3473
|
stats = get_transfer_stats(days)
|
|
3463
3474
|
print(f'Transfer Statistics (last {days} days)')
|
|
3464
3475
|
print(f' Total transfers: {stats["total"]}')
|
|
@@ -3622,11 +3633,11 @@ Env: VSSH_SECRET
|
|
|
3622
3633
|
buf += chunk
|
|
3623
3634
|
try:
|
|
3624
3635
|
result['info'] = json.loads(buf.decode())
|
|
3625
|
-
except:
|
|
3626
|
-
pass
|
|
3636
|
+
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
3637
|
+
pass # TODO: log error
|
|
3627
3638
|
s.close()
|
|
3628
|
-
except:
|
|
3629
|
-
pass
|
|
3639
|
+
except OSError:
|
|
3640
|
+
pass # safe to ignore
|
|
3630
3641
|
return result
|
|
3631
3642
|
|
|
3632
3643
|
if full_mode:
|
|
@@ -43,7 +43,7 @@ def load_config():
|
|
|
43
43
|
try:
|
|
44
44
|
with open(CONFIG_PATH) as f:
|
|
45
45
|
return json.load(f)
|
|
46
|
-
except:
|
|
46
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
47
47
|
return {}
|
|
48
48
|
|
|
49
49
|
def get_servers():
|
|
@@ -108,8 +108,8 @@ def _get_tailscale_map():
|
|
|
108
108
|
parts = line.split()
|
|
109
109
|
if len(parts) >= 2:
|
|
110
110
|
_ts_map_cache[parts[0]] = parts[1]
|
|
111
|
-
except:
|
|
112
|
-
pass
|
|
111
|
+
except OSError as e:
|
|
112
|
+
pass # TODO: log error
|
|
113
113
|
return _ts_map_cache
|
|
114
114
|
|
|
115
115
|
def vssh_connect(ip: str, timeout: int = 5) -> socket.socket:
|
|
@@ -126,7 +126,7 @@ def vssh_connect(ip: str, timeout: int = 5) -> socket.socket:
|
|
|
126
126
|
sock.settimeout(timeout)
|
|
127
127
|
sock.connect((ts_ip, VSSH_PORT))
|
|
128
128
|
return sock
|
|
129
|
-
except:
|
|
129
|
+
except OSError as e:
|
|
130
130
|
del _failover_cache[ip]
|
|
131
131
|
|
|
132
132
|
# Primary: Wire VPN
|
|
@@ -135,8 +135,8 @@ def vssh_connect(ip: str, timeout: int = 5) -> socket.socket:
|
|
|
135
135
|
sock.settimeout(timeout)
|
|
136
136
|
sock.connect((ip, VSSH_PORT))
|
|
137
137
|
return sock
|
|
138
|
-
except:
|
|
139
|
-
pass
|
|
138
|
+
except OSError as e:
|
|
139
|
+
pass # TODO: log error
|
|
140
140
|
|
|
141
141
|
# Fallback: Tailscale
|
|
142
142
|
ts_map = _get_tailscale_map()
|
|
@@ -291,7 +291,7 @@ def tool_vssh_status():
|
|
|
291
291
|
"ip": ip,
|
|
292
292
|
"latency_ms": round(latency, 1)
|
|
293
293
|
}
|
|
294
|
-
except:
|
|
294
|
+
except OSError as e:
|
|
295
295
|
results[name] = {
|
|
296
296
|
"status": "offline",
|
|
297
297
|
"ip": ip
|
|
@@ -426,7 +426,7 @@ def tool_vssh_p2p_status():
|
|
|
426
426
|
"external_port": external[1] if external else 0,
|
|
427
427
|
"nat_type": "cone" if external else "unknown"
|
|
428
428
|
}
|
|
429
|
-
except:
|
|
429
|
+
except Exception as e:
|
|
430
430
|
return {
|
|
431
431
|
"p2p_available": False,
|
|
432
432
|
"note": "P2P module not loaded"
|
|
@@ -504,8 +504,8 @@ def tool_vssh_keys():
|
|
|
504
504
|
sock = vssh_connect(ip, timeout=2)
|
|
505
505
|
sock.close()
|
|
506
506
|
keys["servers_with_vssh"].append(name)
|
|
507
|
-
except:
|
|
508
|
-
pass
|
|
507
|
+
except OSError as e:
|
|
508
|
+
pass # safe to ignore
|
|
509
509
|
|
|
510
510
|
return keys
|
|
511
511
|
|
|
@@ -292,7 +292,7 @@ def hole_punch_udp(peer_ip: str, peer_port: int, local_port: int, duration: int
|
|
|
292
292
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
293
293
|
try:
|
|
294
294
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
295
|
-
except:
|
|
295
|
+
except OSError as e:
|
|
296
296
|
pass # Not available on all platforms
|
|
297
297
|
sock.bind(('', local_port))
|
|
298
298
|
sock.setblocking(False)
|
|
@@ -307,7 +307,7 @@ def hole_punch_udp(peer_ip: str, peer_port: int, local_port: int, duration: int
|
|
|
307
307
|
try:
|
|
308
308
|
sock.sendto(msg, peer)
|
|
309
309
|
sent += 1
|
|
310
|
-
except:
|
|
310
|
+
except Exception: pass # TODO: log error
|
|
311
311
|
|
|
312
312
|
try:
|
|
313
313
|
data, addr = sock.recvfrom(1024)
|
|
@@ -318,7 +318,7 @@ def hole_punch_udp(peer_ip: str, peer_port: int, local_port: int, duration: int
|
|
|
318
318
|
sock.close()
|
|
319
319
|
return True
|
|
320
320
|
except BlockingIOError: pass
|
|
321
|
-
except:
|
|
321
|
+
except Exception: pass # TODO: log error
|
|
322
322
|
|
|
323
323
|
time.sleep(0.05)
|
|
324
324
|
|
|
@@ -338,8 +338,8 @@ def tcp_simultaneous_open(peer_ip: str, peer_port: int, local_port: int, timeout
|
|
|
338
338
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
339
339
|
try:
|
|
340
340
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
341
|
-
except:
|
|
342
|
-
pass
|
|
341
|
+
except OSError as e:
|
|
342
|
+
pass # TODO: log error
|
|
343
343
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
344
344
|
sock.bind(('', local_port))
|
|
345
345
|
sock.settimeout(1)
|
|
@@ -414,8 +414,8 @@ def wrap_p2p_tls(sock: socket.socket, is_server: bool) -> ssl.SSLSocket:
|
|
|
414
414
|
# Prefer ChaCha20 for speed on non-AES-NI hardware
|
|
415
415
|
try:
|
|
416
416
|
ctx.set_ciphers('ECDHE+CHACHA20:ECDHE+AESGCM:@STRENGTH')
|
|
417
|
-
except:
|
|
418
|
-
pass
|
|
417
|
+
except Exception as e:
|
|
418
|
+
pass # TODO: log error
|
|
419
419
|
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
420
420
|
tls_sock = ctx.wrap_socket(sock, server_side=True)
|
|
421
421
|
else:
|
|
@@ -424,8 +424,8 @@ def wrap_p2p_tls(sock: socket.socket, is_server: bool) -> ssl.SSLSocket:
|
|
|
424
424
|
ctx.verify_mode = ssl.CERT_NONE # Self-signed, verified by HMAC below
|
|
425
425
|
try:
|
|
426
426
|
ctx.set_ciphers('ECDHE+CHACHA20:ECDHE+AESGCM:@STRENGTH')
|
|
427
|
-
except:
|
|
428
|
-
pass
|
|
427
|
+
except OSError as e:
|
|
428
|
+
pass # TODO: log error
|
|
429
429
|
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
|
430
430
|
tls_sock = ctx.wrap_socket(sock, server_hostname='vssh-p2p')
|
|
431
431
|
|
|
@@ -917,7 +917,7 @@ def main():
|
|
|
917
917
|
sock.connect((relay_ip, VSSH_PORT))
|
|
918
918
|
sock.close()
|
|
919
919
|
print(f" OK: {relay_ip}:{VSSH_PORT}")
|
|
920
|
-
except:
|
|
920
|
+
except OSError as e:
|
|
921
921
|
print(f" FAILED: Cannot connect to relay")
|
|
922
922
|
return
|
|
923
923
|
|
|
@@ -44,8 +44,8 @@ def setup_fast_socket(sock):
|
|
|
44
44
|
# Try to set TCP quickack (Linux only)
|
|
45
45
|
try:
|
|
46
46
|
sock.setsockopt(socket.IPPROTO_TCP, 12, 1) # TCP_QUICKACK
|
|
47
|
-
except:
|
|
48
|
-
pass
|
|
47
|
+
except OSError as e:
|
|
48
|
+
pass # TODO: log error
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def p2p_send_stream(stream_id: int, sock: socket.socket, path: Path,
|
|
@@ -387,8 +387,8 @@ def p2p_receive_fast(ctrl_sock: socket.socket) -> bool:
|
|
|
387
387
|
# Clean up temp dir
|
|
388
388
|
try:
|
|
389
389
|
temp_dir.rmdir()
|
|
390
|
-
except:
|
|
391
|
-
pass
|
|
390
|
+
except OSError as e:
|
|
391
|
+
pass # TODO: log error
|
|
392
392
|
|
|
393
393
|
print(f" Saved {final_path} ({size/elapsed/1024/1024:.1f}MB/s)")
|
|
394
394
|
ctrl_sock.sendall(b'OK\n')
|
|
@@ -398,8 +398,8 @@ def p2p_receive_fast(ctrl_sock: socket.socket) -> bool:
|
|
|
398
398
|
print(f" Receive error: {e}")
|
|
399
399
|
try:
|
|
400
400
|
ctrl_sock.sendall(f'ERROR:{e}\n'.encode())
|
|
401
|
-
except:
|
|
402
|
-
pass
|
|
401
|
+
except (ValueError, TypeError) as e:
|
|
402
|
+
pass # TODO: log error
|
|
403
403
|
return False
|
|
404
404
|
|
|
405
405
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|