zscams 2.0.1__py2.py3-none-any.whl

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.
Files changed (37) hide show
  1. zscams/__init__.py +0 -0
  2. zscams/__main__.py +101 -0
  3. zscams/agent/__init__.py +0 -0
  4. zscams/agent/certificates/.gitkeep +0 -0
  5. zscams/agent/config.yaml +121 -0
  6. zscams/agent/keys/autoport.key +27 -0
  7. zscams/agent/src/__init__.py +0 -0
  8. zscams/agent/src/core/__init__.py +1 -0
  9. zscams/agent/src/core/api/backend/bootstrap.py +21 -0
  10. zscams/agent/src/core/api/backend/client.py +242 -0
  11. zscams/agent/src/core/api/backend/exceptions.py +10 -0
  12. zscams/agent/src/core/api/backend/update_machine_info.py +16 -0
  13. zscams/agent/src/core/prerequisites.py +36 -0
  14. zscams/agent/src/core/service_health_check.py +49 -0
  15. zscams/agent/src/core/services.py +86 -0
  16. zscams/agent/src/core/tunnel/__init__.py +144 -0
  17. zscams/agent/src/core/tunnel/tls.py +56 -0
  18. zscams/agent/src/core/tunnels.py +55 -0
  19. zscams/agent/src/services/__init__.py +0 -0
  20. zscams/agent/src/services/reverse_ssh.py +73 -0
  21. zscams/agent/src/services/ssh_forwarder.py +75 -0
  22. zscams/agent/src/services/system_monitor.py +264 -0
  23. zscams/agent/src/support/__init__.py +0 -0
  24. zscams/agent/src/support/configuration.py +53 -0
  25. zscams/agent/src/support/filesystem.py +41 -0
  26. zscams/agent/src/support/logger.py +40 -0
  27. zscams/agent/src/support/mac.py +18 -0
  28. zscams/agent/src/support/network.py +49 -0
  29. zscams/agent/src/support/openssl.py +100 -0
  30. zscams/libs/getmac/__init__.py +3 -0
  31. zscams/libs/getmac/__main__.py +123 -0
  32. zscams/libs/getmac/getmac.py +1900 -0
  33. zscams/libs/getmac/shutilwhich.py +67 -0
  34. zscams-2.0.1.dist-info/METADATA +118 -0
  35. zscams-2.0.1.dist-info/RECORD +37 -0
  36. zscams-2.0.1.dist-info/WHEEL +4 -0
  37. zscams-2.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,86 @@
1
+ """
2
+ Service launcher utilities for TLS Tunnel Client
3
+
4
+ - Generic solution: any service can receive parameters via `SERVICE_PARAMS` env variable
5
+ - Starts services asynchronously using asyncio
6
+ - Supports both Python scripts and executables
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ import os
12
+ from zscams.agent.src.support.logger import get_logger
13
+ from zscams.agent.src.core.prerequisites import check_prerequisites
14
+ from zscams.agent.src.core import running_services
15
+
16
+ logger = get_logger("service_launcher")
17
+
18
+
19
+ async def start_service(service_cfg, config_dir=None):
20
+ """
21
+ Launch an external script/service asynchronously.
22
+
23
+ Args:
24
+ service_cfg (dict): Service configuration from config.yaml
25
+ - name: service name
26
+ - script: path to script/executable
27
+ - port: associated port (optional)
28
+ - args: list of extra arguments
29
+ - params: dict of arbitrary parameters (SERVICE_PARAMS)
30
+ config_dir (str, optional): Base directory to resolve relative script paths
31
+
32
+ Returns:
33
+ asyncio.subprocess.Process or None
34
+ """
35
+ base_dir = config_dir or os.getcwd()
36
+ script_path = os.path.join(base_dir, service_cfg["script"])
37
+ prereqs = service_cfg.get("prerequisites")
38
+ name = service_cfg["name"]
39
+ # Wait for prerequisites before starting
40
+ logger.info("Checking prerequisites for %s...", name)
41
+ ok = check_prerequisites(prereqs)
42
+ if not ok:
43
+ logger.error("Prerequisites not met for %s. Skipping start.", name)
44
+ return
45
+
46
+ if not os.path.exists(script_path):
47
+ logger.error("Service script not found: %s", script_path)
48
+ return None
49
+
50
+ env = os.environ.copy()
51
+ params = service_cfg.get("params")
52
+ if params:
53
+ # Pass generic parameters to the service via JSON environment variable
54
+ env["SERVICE_PARAMS"] = json.dumps(params)
55
+
56
+ cmd = ["python", script_path] + service_cfg.get("args", [])
57
+ logger.info(
58
+ "Starting service %s on port %d: %s",
59
+ service_cfg.get("name"),
60
+ service_cfg.get("port", 0),
61
+ cmd,
62
+ )
63
+
64
+ try:
65
+ process = await asyncio.create_subprocess_exec(*cmd, env=env)
66
+ running_services.add(name)
67
+ return process
68
+ except Exception as e:
69
+ logger.error("Failed to start service %s: %s", service_cfg.get("name"), e)
70
+ return None
71
+
72
+
73
+ async def start_all_services(services_cfg_list, config_dir=None):
74
+ """
75
+ Start all services defined in the configuration concurrently.
76
+
77
+ Args:
78
+ services_cfg_list (list): List of service configuration dictionaries
79
+ config_dir (str, optional): Base directory to resolve relative script paths
80
+ """
81
+ tasks = []
82
+ for svc in services_cfg_list:
83
+ tasks.append(start_service(svc, config_dir=config_dir))
84
+
85
+ # Wait for all services to start
86
+ await asyncio.gather(*tasks)
@@ -0,0 +1,144 @@
1
+ """
2
+ Forwarding utilities for TLS Tunnel Client with:
3
+ - Auto-reconnect with exponential backoff
4
+ - Health checks
5
+ """
6
+
7
+ import asyncio
8
+ from zscams.agent.src.support.logger import get_logger
9
+
10
+ READ_BUFFER_SIZE = 4096
11
+
12
+ logger = get_logger("tls_forward")
13
+
14
+
15
+ async def forward(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
16
+ """
17
+ Forward data from reader to writer until connection closes.
18
+ Handles Windows-specific connection resets gracefully.
19
+ """
20
+ try:
21
+ while True:
22
+ data = await reader.read(READ_BUFFER_SIZE)
23
+ if not data:
24
+ break
25
+ writer.write(data)
26
+ await writer.drain()
27
+ except (ConnectionResetError, OSError) as err:
28
+ # Handle Windows WinError 64 and other disconnects gracefully
29
+ logger.debug("Connection closed by peer (expected): %s", err)
30
+ except Exception as err:
31
+ logger.error("Unexpected forwarding error: %s", err)
32
+ finally:
33
+ try:
34
+ writer.close()
35
+ await writer.wait_closed()
36
+ except Exception:
37
+ pass # ignore errors on close
38
+
39
+
40
+ async def handle_client(
41
+ local_reader, local_writer, remote_host, remote_port, sni_hostname, ssl_context
42
+ ): # pylint: disable=too-many-arguments,too-many-positional-arguments
43
+ """
44
+ Forward a single local connection to the remote TLS server with SNI.
45
+ Handles abrupt disconnects gracefully.
46
+ """
47
+ try:
48
+ remote_reader, remote_writer = await asyncio.open_connection(
49
+ host=remote_host,
50
+ port=remote_port,
51
+ ssl=ssl_context,
52
+ server_hostname=sni_hostname,
53
+ )
54
+ logger.debug(
55
+ "New connection: local -> %s:%s (SNI=%s)",
56
+ remote_host,
57
+ remote_port,
58
+ sni_hostname,
59
+ )
60
+
61
+ await asyncio.gather(
62
+ forward(local_reader, remote_writer), forward(remote_reader, local_writer)
63
+ )
64
+
65
+ except (ConnectionResetError, OSError) as e:
66
+ logger.debug("Connection closed unexpectedly: %s", e)
67
+ except Exception as e:
68
+ logger.error("Unexpected client handling error: %s", e)
69
+ finally:
70
+ try:
71
+ local_writer.close()
72
+ await local_writer.wait_closed()
73
+ logger.debug(
74
+ f"Connection closed: local -> {remote_host}:{remote_port} (SNI={sni_hostname})"
75
+ )
76
+ except Exception as e:
77
+ logger.error("Unexpected client handling error: %s", e)
78
+
79
+
80
+ async def start_tunnel(
81
+ local_port,
82
+ remote_host,
83
+ remote_port,
84
+ sni_hostname,
85
+ ssl_context,
86
+ reconnect_max_delay=60,
87
+ ready_event=None,
88
+ ): # pylint: disable=too-many-arguments,too-many-positional-arguments
89
+ """
90
+ Start a TCP listener on local_port and forward connections to the remote TLS server.
91
+ Automatically reconnects on failure with exponential backoff and performs periodic health checks.
92
+
93
+ Args:
94
+ local_port (int)
95
+ remote_host (str)
96
+ remote_port (int)
97
+ sni_hostname (str)
98
+ ssl_context (ssl.SSLContext)
99
+ reconnect_max_delay (int): Maximum delay between reconnect attempts
100
+ """
101
+ backoff = 1 # initial delay in seconds
102
+
103
+ while True:
104
+ try:
105
+ server = await asyncio.start_server(
106
+ lambda r, w: handle_client(
107
+ r, w, remote_host, remote_port, sni_hostname, ssl_context
108
+ ),
109
+ "127.0.0.1",
110
+ local_port,
111
+ )
112
+ addr = server.sockets[0].getsockname()
113
+ logger.info(
114
+ "Listening on :%s -> %s:%s (SNI=%s)",
115
+ addr[1],
116
+ remote_host,
117
+ remote_port,
118
+ sni_hostname,
119
+ )
120
+
121
+ # Health check task: logs tunnel is alive every 30 seconds
122
+ async def health_check():
123
+ while True:
124
+ await asyncio.sleep(600)
125
+ logger.info(
126
+ "Tunnel on local port %s is healthy and running", local_port
127
+ )
128
+
129
+ asyncio.create_task(health_check())
130
+
131
+ # Signal ready if event is provided
132
+ if ready_event:
133
+ ready_event.set()
134
+
135
+ async with server:
136
+ await server.serve_forever()
137
+
138
+ except Exception as e:
139
+ logger.error("Tunnel error on local port %s: %s", local_port, e)
140
+ logger.info("Reconnecting in %s seconds...", backoff)
141
+ await asyncio.sleep(backoff)
142
+ backoff = min(backoff * 2, reconnect_max_delay) # exponential backoff
143
+ else:
144
+ backoff = 1 # reset backoff if successful
@@ -0,0 +1,56 @@
1
+ import ssl
2
+ import os
3
+ from typing import Optional
4
+ from zscams.agent.src.support.configuration import RemoteConfig
5
+ from zscams.agent.src.support.filesystem import resolve_path
6
+ from zscams.agent.src.support.logger import get_logger
7
+
8
+ logger = get_logger("tls_utils")
9
+
10
+
11
+ def create_ssl_context(
12
+ remote_cfg: RemoteConfig, config_dir: Optional[str] = None
13
+ ) -> ssl.SSLContext:
14
+ """
15
+ Create an SSL context for TLS connections based on configuration.
16
+
17
+ Args:
18
+ remote_cfg (dict): Remote server configuration including:
19
+ - verify_cert (bool)
20
+ - ca_cert (str)
21
+ - client_cert (str)
22
+ - client_key (str)
23
+ config_dir (str, optional): Directory to resolve relative paths from.
24
+
25
+ Returns:
26
+ ssl.SSLContext: Configured SSL context
27
+ """
28
+ context = ssl.create_default_context()
29
+
30
+ ca_cert = resolve_path(remote_cfg.get("ca_cert"), config_dir)
31
+ client_cert = resolve_path(remote_cfg.get("client_cert"), config_dir)
32
+ client_key = resolve_path(remote_cfg.get("client_key"), config_dir)
33
+
34
+ if not remote_cfg.get("verify_cert", True):
35
+ context.check_hostname = False
36
+ context.verify_mode = ssl.CERT_NONE
37
+ logger.warning("Certificate verification is disabled!")
38
+ else:
39
+ if ca_cert and os.path.exists(ca_cert):
40
+ context.load_verify_locations(cafile=ca_cert)
41
+ logger.info("Loaded CA certificate from %s", ca_cert)
42
+ else:
43
+ logger.warning("CA certificate not found or not provided: %s", ca_cert)
44
+
45
+ if client_cert and client_key:
46
+ if os.path.exists(client_cert) and os.path.exists(client_key):
47
+ context.load_cert_chain(certfile=client_cert, keyfile=client_key)
48
+ logger.info(
49
+ "Loaded client certificate/key: %s / %s", client_cert, client_key
50
+ )
51
+ else:
52
+ raise FileNotFoundError(
53
+ f"Client certificate or key not found: {client_cert}, {client_key}"
54
+ )
55
+
56
+ return context
@@ -0,0 +1,55 @@
1
+ """
2
+ Utilities to start TLS tunnels asynchronously
3
+
4
+ - Supports multiple forwards
5
+ - Signals readiness via asyncio.Event
6
+ - Auto-reconnect and logging
7
+ """
8
+
9
+ import asyncio
10
+ from zscams.agent.src.core.tunnel import start_tunnel
11
+ from zscams.agent.src.support.logger import get_logger
12
+
13
+ logger = get_logger("tunnel_launcher")
14
+
15
+
16
+ async def start_all_tunnels(forwards_cfg_list, remote_host, remote_port, ssl_context):
17
+ """
18
+ Start all TLS tunnels concurrently and wait for readiness.
19
+
20
+ Args:
21
+ forwards_cfg_list (list): List of forward dictionaries from config
22
+ Each dict should have:
23
+ - local_port
24
+ - sni_hostname
25
+ remote_host (str): Remote host to connect to
26
+ remote_port (int): Remote port to connect to
27
+ ssl_context (ssl.SSLContext): SSL context for the TLS tunnel
28
+
29
+ Returns:
30
+ list of asyncio.Tasks: tunnel tasks running indefinitely
31
+ """
32
+ tasks = []
33
+ ready_events = []
34
+
35
+ for forward_cfg in forwards_cfg_list:
36
+ ready_event = asyncio.Event()
37
+ ready_events.append(ready_event)
38
+ tasks.append(
39
+ asyncio.create_task(
40
+ start_tunnel(
41
+ local_port=forward_cfg["local_port"],
42
+ remote_host=remote_host,
43
+ remote_port=remote_port,
44
+ sni_hostname=forward_cfg["sni_hostname"],
45
+ ssl_context=ssl_context,
46
+ reconnect_max_delay=60,
47
+ ready_event=ready_event,
48
+ )
49
+ )
50
+ )
51
+
52
+ # Wait for all tunnels to signal readiness
53
+ await asyncio.gather(*(event.wait() for event in ready_events))
54
+ logger.info("[*] %d tunnel(s) ready", len(tasks))
55
+ return tasks
File without changes
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import sys
5
+ from zscams.agent.src.support.logger import get_logger
6
+
7
+ logger = get_logger("autossh_service")
8
+
9
+ # Load service-specific params from environment
10
+ params_env = os.environ.get("SERVICE_PARAMS")
11
+ if not params_env:
12
+ logger.error("SERVICE_PARAMS environment variable not set")
13
+ sys.exit(1)
14
+
15
+ params = json.loads(params_env)
16
+
17
+ LOCAL_PORT = params.get("local_port", 4422)
18
+ SERVER_SSH_USER = params.get("server_ssh_user", "ssh_user")
19
+ REVERSE_PORT = params.get("reverse_port", 2222)
20
+ PRIVATE_KEY = params.get("private_key") # Path to RSA key
21
+ SSH_OPTIONS = params.get("ssh_options", [])
22
+ CHECK_INTERVAL = 120 #
23
+
24
+
25
+ async def run():
26
+ backoff = 1
27
+ max_backoff = 60
28
+ ssh_cmd = [
29
+ "ssh",
30
+ "-p",
31
+ f"{LOCAL_PORT}",
32
+ "-R",
33
+ f"*:{REVERSE_PORT}:localhost:22",
34
+ f"{SERVER_SSH_USER}@localhost",
35
+ "-N",
36
+ ]
37
+
38
+ # Use key file if provided
39
+ if PRIVATE_KEY:
40
+ ssh_cmd += ["-i", PRIVATE_KEY]
41
+
42
+ # Add additional SSH options
43
+ ssh_cmd += SSH_OPTIONS
44
+
45
+ logger.info(f"Starting reverse SSH tunnel: {' '.join(ssh_cmd)}")
46
+
47
+ while True:
48
+ try:
49
+ process = await asyncio.create_subprocess_exec(
50
+ *ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
51
+ )
52
+ logger.info(f"SSH tunnel started (PID={process.pid})")
53
+ stdout, stderr = await process.communicate()
54
+ if stdout:
55
+ logger.info(stdout.decode())
56
+ if stderr:
57
+ logger.warning(stderr.decode())
58
+ returncode = process.returncode
59
+ logger.warning(f"SSH tunnel exited with code {returncode}")
60
+ except Exception as e:
61
+ logger.error(f"SSH tunnel failed: {e}")
62
+
63
+ logger.info(f"Reconnecting in {backoff} seconds...")
64
+ await asyncio.sleep(backoff)
65
+ backoff = min(backoff * 2, max_backoff)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ try:
70
+ asyncio.run(run())
71
+ except KeyboardInterrupt:
72
+ logger.info("Exiting autossh_service.py")
73
+ sys.exit(0)
@@ -0,0 +1,75 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import sys
5
+ from zscams.agent.src.support.logger import get_logger
6
+
7
+ logger = get_logger("autossh_service")
8
+
9
+ # Load service-specific params from environment
10
+ params_env = os.environ.get("SERVICE_PARAMS")
11
+ if not params_env:
12
+ logger.error("SERVICE_PARAMS environment variable not set")
13
+ sys.exit(1)
14
+
15
+ params = json.loads(params_env)
16
+
17
+ FORWARDER_PORT = params.get("forwarder_port", 4422)
18
+ LOCAL_PORT = params.get("local_port", 4422)
19
+ REMOTE_PORT = params.get("remote_port", 4422)
20
+ REMOTE_HOST = params.get("remote_host", "localhost")
21
+ SERVER_SSH_USER = params.get("server_ssh_user", "ssh_user")
22
+ PRIVATE_KEY = params.get("private_key") # Path to RSA key
23
+ SSH_OPTIONS = params.get("ssh_options", [])
24
+ CHECK_INTERVAL = 120 #
25
+
26
+
27
+ async def run():
28
+ backoff = 1
29
+ max_backoff = 60
30
+ ssh_cmd = [
31
+ "ssh",
32
+ "-p",
33
+ f"{LOCAL_PORT}",
34
+ "-L",
35
+ f"{FORWARDER_PORT}:{REMOTE_HOST}:{REMOTE_PORT}",
36
+ f"{SERVER_SSH_USER}@localhost",
37
+ "-N", # No command execution
38
+ ]
39
+
40
+ # Use key file if provided
41
+ if PRIVATE_KEY:
42
+ ssh_cmd += ["-i", PRIVATE_KEY]
43
+
44
+ # Add additional SSH options
45
+ ssh_cmd += SSH_OPTIONS
46
+
47
+ logger.info(f"Starting reverse SSH tunnel: {' '.join(ssh_cmd)}")
48
+
49
+ while True:
50
+ try:
51
+ process = await asyncio.create_subprocess_exec(
52
+ *ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
53
+ )
54
+ logger.info(f"SSH tunnel started (PID={process.pid})")
55
+ stdout, stderr = await process.communicate()
56
+ if stdout:
57
+ logger.info(stdout.decode())
58
+ if stderr:
59
+ logger.warning(stderr.decode())
60
+ returncode = process.returncode
61
+ logger.warning(f"SSH tunnel exited with code {returncode}")
62
+ except Exception as e:
63
+ logger.error(f"SSH tunnel failed: {e}")
64
+
65
+ logger.info(f"Reconnecting in {backoff} seconds...")
66
+ await asyncio.sleep(backoff)
67
+ backoff = min(backoff * 2, max_backoff)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ try:
72
+ asyncio.run(run())
73
+ except KeyboardInterrupt:
74
+ logger.info("Exiting autossh_service.py")
75
+ sys.exit(0)