vssh 3.3.0__tar.gz → 3.3.4__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.4}/PKG-INFO +4 -4
- {vssh-3.3.0 → vssh-3.3.4}/README.md +2 -2
- {vssh-3.3.0 → vssh-3.3.4}/pyproject.toml +6 -3
- {vssh-3.3.0 → vssh-3.3.4/vssh.egg-info}/PKG-INFO +4 -4
- {vssh-3.3.0 → vssh-3.3.4}/vssh.egg-info/SOURCES.txt +0 -2
- {vssh-3.3.0 → vssh-3.3.4}/vssh.egg-info/entry_points.txt +3 -0
- {vssh-3.3.0 → vssh-3.3.4}/vssh.egg-info/top_level.txt +0 -2
- {vssh-3.3.0 → vssh-3.3.4}/vssh.py +90 -71
- {vssh-3.3.0 → vssh-3.3.4}/vssh_mcp_server.py +150 -45
- {vssh-3.3.0 → vssh-3.3.4}/vssh_p2p.py +12 -12
- vssh-3.3.0/vssh_p2p_fast.py +0 -500
- vssh-3.3.0/vssh_quic.py +0 -436
- {vssh-3.3.0 → vssh-3.3.4}/LICENSE +0 -0
- {vssh-3.3.0 → vssh-3.3.4}/setup.cfg +0 -0
- {vssh-3.3.0 → vssh-3.3.4}/vssh.egg-info/dependency_links.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.4
|
|
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
|
|
@@ -41,10 +41,10 @@ pip install vssh
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
# Execute remote command
|
|
44
|
-
vssh exec
|
|
44
|
+
vssh exec relay1 "uptime"
|
|
45
45
|
|
|
46
46
|
# Transfer file at 50+ MB/s
|
|
47
|
-
vssh put
|
|
47
|
+
vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
|
|
48
48
|
|
|
49
49
|
# Check connection status
|
|
50
50
|
vssh status
|
|
@@ -18,10 +18,10 @@ pip install vssh
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
# Execute remote command
|
|
21
|
-
vssh exec
|
|
21
|
+
vssh exec relay1 "uptime"
|
|
22
22
|
|
|
23
23
|
# Transfer file at 50+ MB/s
|
|
24
|
-
vssh put
|
|
24
|
+
vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
|
|
25
25
|
|
|
26
26
|
# Check connection status
|
|
27
27
|
vssh status
|
|
@@ -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.4"
|
|
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 = [
|
|
@@ -34,5 +34,8 @@ vssh-mcp = "vssh_mcp_server:main"
|
|
|
34
34
|
Homepage = "https://github.com/meshpop/vssh"
|
|
35
35
|
Repository = "https://github.com/meshpop/vssh"
|
|
36
36
|
|
|
37
|
+
[project.entry-points."meshpop.mcp"]
|
|
38
|
+
vssh = "vssh_mcp_server"
|
|
39
|
+
|
|
37
40
|
[tool.setuptools]
|
|
38
|
-
py-modules = ["vssh", "vssh_p2p", "
|
|
41
|
+
py-modules = ["vssh", "vssh_p2p", "vssh_mcp_server"]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vssh
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.4
|
|
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
|
|
@@ -41,10 +41,10 @@ pip install vssh
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
# Execute remote command
|
|
44
|
-
vssh exec
|
|
44
|
+
vssh exec relay1 "uptime"
|
|
45
45
|
|
|
46
46
|
# Transfer file at 50+ MB/s
|
|
47
|
-
vssh put
|
|
47
|
+
vssh put relay1 ./deploy.tar.gz /opt/deploy.tar.gz
|
|
48
48
|
|
|
49
49
|
# Check connection status
|
|
50
50
|
vssh status
|
|
@@ -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 # e silenced
|
|
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 # wire_err silenced
|
|
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 # e silenced
|
|
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 # e silenced
|
|
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, ValueError, TimeoutError):
|
|
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 # e silenced
|
|
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 # e silenced
|
|
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 # e silenced
|
|
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 # e silenced
|
|
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 # e silenced
|
|
3206
3217
|
|
|
3207
3218
|
result = handle_rpc(method, payload, sender_ip=addr[0])
|
|
3208
3219
|
conn.sendall(f'RPC_RESULT:{len(result)}\n'.encode())
|
|
@@ -3270,6 +3281,14 @@ def server():
|
|
|
3270
3281
|
|
|
3271
3282
|
def main():
|
|
3272
3283
|
args = sys.argv[1:]
|
|
3284
|
+
# Handle --version and --help flags
|
|
3285
|
+
if args and args[0] in ("--version", "-v", "-V"):
|
|
3286
|
+
try:
|
|
3287
|
+
from importlib.metadata import version as _v
|
|
3288
|
+
print(f"vssh v{_v('vssh')}")
|
|
3289
|
+
except Exception:
|
|
3290
|
+
print("vssh v3.3.2")
|
|
3291
|
+
return
|
|
3273
3292
|
if not args:
|
|
3274
3293
|
print('''vssh v3 - Distributed command & file transport daemon for server meshes
|
|
3275
3294
|
|
|
@@ -3457,8 +3476,8 @@ Env: VSSH_SECRET
|
|
|
3457
3476
|
if len(args) > 1:
|
|
3458
3477
|
try:
|
|
3459
3478
|
days = int(args[1])
|
|
3460
|
-
except:
|
|
3461
|
-
pass
|
|
3479
|
+
except (ValueError, TypeError) as e:
|
|
3480
|
+
pass # e silenced
|
|
3462
3481
|
stats = get_transfer_stats(days)
|
|
3463
3482
|
print(f'Transfer Statistics (last {days} days)')
|
|
3464
3483
|
print(f' Total transfers: {stats["total"]}')
|
|
@@ -3622,11 +3641,11 @@ Env: VSSH_SECRET
|
|
|
3622
3641
|
buf += chunk
|
|
3623
3642
|
try:
|
|
3624
3643
|
result['info'] = json.loads(buf.decode())
|
|
3625
|
-
except:
|
|
3626
|
-
pass
|
|
3644
|
+
except (json.JSONDecodeError, ValueError, UnicodeDecodeError) as e:
|
|
3645
|
+
pass # e silenced
|
|
3627
3646
|
s.close()
|
|
3628
|
-
except:
|
|
3629
|
-
pass
|
|
3647
|
+
except OSError:
|
|
3648
|
+
pass # safe to ignore
|
|
3630
3649
|
return result
|
|
3631
3650
|
|
|
3632
3651
|
if full_mode:
|
|
@@ -3712,7 +3731,7 @@ Env: VSSH_SECRET
|
|
|
3712
3731
|
print(f" {r['name']:6s} {r['ip']:18s} ○ offline")
|
|
3713
3732
|
print(f"\nTotal: {online}/{total} online")
|
|
3714
3733
|
else:
|
|
3715
|
-
# Unknown command -> try as PTY session shortcut (vssh
|
|
3734
|
+
# Unknown command -> try as PTY session shortcut (vssh node3 = vssh session node3)
|
|
3716
3735
|
host = resolve_name(cmd)
|
|
3717
3736
|
rc = pty_session(host, cmd)
|
|
3718
3737
|
if rc != 0:
|