vssh 3.2.3__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.1/PKG-INFO ADDED
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: vssh
3
+ Version: 3.3.1
4
+ Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
5
+ Author-email: MeshPOP <mpop@mpop.dev>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/meshpop/vssh
8
+ Project-URL: Repository, https://github.com/meshpop/vssh
9
+ Keywords: ssh,scp,tailscale,p2p,vpn,mcp,remote
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: System :: Networking
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # vssh
25
+
26
+ **Distributed SSH transport and file transfer daemon with zero external dependencies.**
27
+
28
+ Part of [MeshPOP](https://mpop.dev) — Layer 2 (Transport)
29
+
30
+ - Zero external dependencies — pure Python stdlib
31
+ - 50+ MB/s file transfer across WireGuard mesh
32
+ - MCP server for AI agent integration
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install vssh
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ # Execute remote command
44
+ vssh exec v1 "uptime"
45
+
46
+ # Transfer file at 50+ MB/s
47
+ vssh put v1 ./deploy.tar.gz /opt/deploy.tar.gz
48
+
49
+ # Check connection status
50
+ vssh status
51
+ ```
52
+
53
+ ## MCP Setup
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "vssh": { "command": "vssh-mcp" }
59
+ }
60
+ }
61
+ ```
62
+
63
+ Gives AI agents: `vssh_exec`, `vssh_put`, `vssh_get`, `vssh_status`, `vssh_keys`, `vssh_speed_test`
64
+
65
+ ## Links
66
+
67
+ - Main project: [github.com/meshpop/mpop](https://github.com/meshpop/mpop)
68
+ - Website: [mpop.dev](https://mpop.dev)
69
+ - PyPI: [pypi.org/project/vssh](https://pypi.org/project/vssh/)
70
+
71
+ ## License
72
+
73
+ MIT
vssh-3.3.1/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # vssh
2
+
3
+ **Distributed SSH transport and file transfer daemon with zero external dependencies.**
4
+
5
+ Part of [MeshPOP](https://mpop.dev) — Layer 2 (Transport)
6
+
7
+ - Zero external dependencies — pure Python stdlib
8
+ - 50+ MB/s file transfer across WireGuard mesh
9
+ - MCP server for AI agent integration
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install vssh
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```bash
20
+ # Execute remote command
21
+ vssh exec v1 "uptime"
22
+
23
+ # Transfer file at 50+ MB/s
24
+ vssh put v1 ./deploy.tar.gz /opt/deploy.tar.gz
25
+
26
+ # Check connection status
27
+ vssh status
28
+ ```
29
+
30
+ ## MCP Setup
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "vssh": { "command": "vssh-mcp" }
36
+ }
37
+ }
38
+ ```
39
+
40
+ Gives AI agents: `vssh_exec`, `vssh_put`, `vssh_get`, `vssh_status`, `vssh_keys`, `vssh_speed_test`
41
+
42
+ ## Links
43
+
44
+ - Main project: [github.com/meshpop/mpop](https://github.com/meshpop/mpop)
45
+ - Website: [mpop.dev](https://mpop.dev)
46
+ - PyPI: [pypi.org/project/vssh](https://pypi.org/project/vssh/)
47
+
48
+ ## License
49
+
50
+ MIT
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vssh"
7
- version = "3.2.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 = "hello@meshpop.dev"}
13
+ {name = "MeshPOP", email = "mpop@mpop.dev"}
14
14
  ]
15
15
  keywords = ["ssh", "scp", "tailscale", "p2p", "vpn", "mcp", "remote"]
16
16
  classifiers = [
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: vssh
3
+ Version: 3.3.1
4
+ Summary: Secure SSH/SCP tool with Tailscale failover, P2P transport, and MCP server
5
+ Author-email: MeshPOP <mpop@mpop.dev>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/meshpop/vssh
8
+ Project-URL: Repository, https://github.com/meshpop/vssh
9
+ Keywords: ssh,scp,tailscale,p2p,vpn,mcp,remote
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: System :: Networking
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # vssh
25
+
26
+ **Distributed SSH transport and file transfer daemon with zero external dependencies.**
27
+
28
+ Part of [MeshPOP](https://mpop.dev) — Layer 2 (Transport)
29
+
30
+ - Zero external dependencies — pure Python stdlib
31
+ - 50+ MB/s file transfer across WireGuard mesh
32
+ - MCP server for AI agent integration
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install vssh
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ # Execute remote command
44
+ vssh exec v1 "uptime"
45
+
46
+ # Transfer file at 50+ MB/s
47
+ vssh put v1 ./deploy.tar.gz /opt/deploy.tar.gz
48
+
49
+ # Check connection status
50
+ vssh status
51
+ ```
52
+
53
+ ## MCP Setup
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "vssh": { "command": "vssh-mcp" }
59
+ }
60
+ }
61
+ ```
62
+
63
+ Gives AI agents: `vssh_exec`, `vssh_put`, `vssh_get`, `vssh_status`, `vssh_keys`, `vssh_speed_test`
64
+
65
+ ## Links
66
+
67
+ - Main project: [github.com/meshpop/mpop](https://github.com/meshpop/mpop)
68
+ - Website: [mpop.dev](https://mpop.dev)
69
+ - PyPI: [pypi.org/project/vssh](https://pypi.org/project/vssh/)
70
+
71
+ ## License
72
+
73
+ MIT
@@ -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.x.x = 100.x.x.x
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 = [] # Loaded from ~/.vssh/config or auto-discovered
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(f'df -B1 {path}', shell=True, capture_output=True, text=True, timeout=10)
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', shell=True, capture_output=True, text=True, timeout=10)
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
- f'journalctl -u {service} -n {lines} --no-pager',
745
- shell=True, capture_output=True, text=True, timeout=10
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(f'tail -{lines} {path}', shell=True, capture_output=True, text=True, timeout=10)
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(f'systemctl restart {service}', shell=True, capture_output=True, text=True, timeout=30)
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(f'systemctl is-active {service}', shell=True, capture_output=True, text=True, timeout=10)
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
- conn.sendall(b'OK\n')
2953
- print(f'[SSH] {addr[0]}: {cmdline}')
2954
- result = subprocess.run(cmdline, shell=True, capture_output=True, timeout=60)
2955
- conn.sendall(result.stdout + result.stderr + b'__END__')
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
- result = subprocess.run(cmdline, shell=True, capture_output=True, timeout=600)
3016
- output = result.stdout
3017
- conn.sendall(f'OK:{len(output)}\n'.encode())
3018
- conn.sendall(output)
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: