sonx 0.1.0__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.
sonx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: sonx
3
+ Version: 0.1.0
4
+ Summary: Superior Organised Network - Remote access tool
5
+ Author: son
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/randompasswordis7/son
8
+ Project-URL: Source, https://github.com/randompasswordis7/son
9
+ Keywords: remote-access,ssh,tunnel,terminal,network
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: System :: Networking
21
+ Classifier: Topic :: System :: Systems Administration
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: typer>=0.12.0
25
+ Requires-Dist: rich>=13.0.0
26
+ Requires-Dist: websockets>=12.0
27
+ Provides-Extra: web
28
+ Requires-Dist: fastapi>=0.110.0; extra == "web"
29
+ Requires-Dist: uvicorn>=0.27.0; extra == "web"
30
+ Requires-Dist: httpx>=0.27.0; extra == "web"
31
+
32
+ # son — Superior Organised Network
33
+
34
+ Remote access tool with a daemon-agent architecture. Run `sond` on target machines, connect via `son` CLI or web UI.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install son
40
+ # or with web UI support:
41
+ pip install "son[web]"
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ **On the target machine** (the one you want to access):
47
+
48
+ ```bash
49
+ sond
50
+ ```
51
+
52
+ **On your machine** (the one you connect from):
53
+
54
+ ```bash
55
+ # Discover sond instances on LAN
56
+ son discover
57
+
58
+ # Connect to a remote shell
59
+ son connect 192.168.1.50
60
+
61
+ # Tunnel a local port through the target
62
+ son tunnel 192.168.1.50 -L 8080:localhost:80
63
+
64
+ # Web terminal UI
65
+ son web
66
+ ```
67
+
68
+ ## How it works
69
+
70
+ | Component | Runs on | Purpose |
71
+ |---|---|---|
72
+ | `sond` | Target machine | UDP broadcast (LAN discovery), WebSocket server for shell + tunnels |
73
+ | `son` | Your machine | CLI + web UI, connects to `sond` instances |
74
+
75
+ ## Commands
76
+
77
+ | Command | Description |
78
+ |---|---|
79
+ | `sond` | Start daemon (`-p` port, `--password`, `--no-broadcast`) |
80
+ | `son discover` | Scan LAN for sond instances |
81
+ | `son connect <host>` | Open remote shell (`-s powershell/bash/cmd`) |
82
+ | `son tunnel <host> -L local:remote:port` | Local port forwarding |
83
+ | `son web` | Browser-based terminal at `http://127.0.0.1:8585` |
84
+
85
+ ## License
86
+
87
+ MIT
sonx-0.1.0/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # son — Superior Organised Network
2
+
3
+ Remote access tool with a daemon-agent architecture. Run `sond` on target machines, connect via `son` CLI or web UI.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install son
9
+ # or with web UI support:
10
+ pip install "son[web]"
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ **On the target machine** (the one you want to access):
16
+
17
+ ```bash
18
+ sond
19
+ ```
20
+
21
+ **On your machine** (the one you connect from):
22
+
23
+ ```bash
24
+ # Discover sond instances on LAN
25
+ son discover
26
+
27
+ # Connect to a remote shell
28
+ son connect 192.168.1.50
29
+
30
+ # Tunnel a local port through the target
31
+ son tunnel 192.168.1.50 -L 8080:localhost:80
32
+
33
+ # Web terminal UI
34
+ son web
35
+ ```
36
+
37
+ ## How it works
38
+
39
+ | Component | Runs on | Purpose |
40
+ |---|---|---|
41
+ | `sond` | Target machine | UDP broadcast (LAN discovery), WebSocket server for shell + tunnels |
42
+ | `son` | Your machine | CLI + web UI, connects to `sond` instances |
43
+
44
+ ## Commands
45
+
46
+ | Command | Description |
47
+ |---|---|
48
+ | `sond` | Start daemon (`-p` port, `--password`, `--no-broadcast`) |
49
+ | `son discover` | Scan LAN for sond instances |
50
+ | `son connect <host>` | Open remote shell (`-s powershell/bash/cmd`) |
51
+ | `son tunnel <host> -L local:remote:port` | Local port forwarding |
52
+ | `son web` | Browser-based terminal at `http://127.0.0.1:8585` |
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,46 @@
1
+ [project]
2
+ name = "sonx"
3
+ version = "0.1.0"
4
+ description = "Superior Organised Network - Remote access tool"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = {text = "MIT"}
8
+ authors = [{name = "son"}]
9
+ keywords = ["remote-access", "ssh", "tunnel", "terminal", "network"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Console",
13
+ "Intended Audience :: System Administrators",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: System :: Networking",
22
+ "Topic :: System :: Systems Administration",
23
+ ]
24
+ dependencies = [
25
+ "typer>=0.12.0",
26
+ "rich>=13.0.0",
27
+ "websockets>=12.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ web = ["fastapi>=0.110.0", "uvicorn>=0.27.0", "httpx>=0.27.0"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/randompasswordis7/son"
35
+ Source = "https://github.com/randompasswordis7/son"
36
+
37
+ [project.scripts]
38
+ son = "son.cli.main:app"
39
+ sond = "son.daemon.main:main"
40
+
41
+ [build-system]
42
+ requires = ["setuptools>=68.0"]
43
+ build-backend = "setuptools.build_meta"
44
+
45
+ [tool.setuptools.packages.find]
46
+ include = ["son*", "sond*"]
sonx-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,3 @@
1
+ from son.cli.main import app
2
+
3
+ app()
File without changes
@@ -0,0 +1,77 @@
1
+ import asyncio
2
+ import sys
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.live import Live
8
+ from rich.panel import Panel
9
+
10
+ from son.client.connector import SonConnector
11
+ from son.core.protocol import CONNECTION_PORT
12
+ from son.core.types import get_default_shell
13
+
14
+ app = typer.Typer(help="Connect to a remote sond instance")
15
+ console = Console()
16
+
17
+
18
+ @app.callback(invoke_without_command=True)
19
+ def connect(
20
+ host: str = typer.Argument(..., help="sond hostname or IP address"),
21
+ port: int = typer.Option(CONNECTION_PORT, "--port", "-p", help="sond port"),
22
+ password: str = typer.Option("", "--password", "-P", help="auth password"),
23
+ shell: str = typer.Option("", "--shell", "-s", help="shell type (powershell, cmd, bash)"),
24
+ ):
25
+ target_shell = shell or get_default_shell()
26
+
27
+ async def run():
28
+ connector = SonConnector(host, port, password)
29
+ try:
30
+ await connector.connect()
31
+ except Exception as e:
32
+ console.print(f"[red]connection failed:[/] {e}")
33
+ raise typer.Exit(1)
34
+
35
+ console.print(Panel(
36
+ f"[bold green]connected[/] to [cyan]{host}:{port}[/] via [yellow]{target_shell}[/]\n"
37
+ "[dim]type 'exit' to disconnect[/]",
38
+ border_style="green",
39
+ ))
40
+
41
+ await connector.open_shell(shell=target_shell)
42
+
43
+ async def read_stdin():
44
+ loop = asyncio.get_running_loop()
45
+ while True:
46
+ line = await loop.run_in_executor(None, sys.stdin.readline)
47
+ if not line:
48
+ break
49
+ if line.strip().lower() == "exit":
50
+ break
51
+ await connector.send_input(line)
52
+
53
+ async def handle_output(data: str):
54
+ sys.stdout.write(data)
55
+ sys.stdout.flush()
56
+
57
+ connector.on_output(handle_output)
58
+ connector.on_disconnect(lambda: console.print("\n[red]disconnected[/]"))
59
+
60
+ tasks = [
61
+ asyncio.create_task(connector.listen()),
62
+ asyncio.create_task(read_stdin()),
63
+ ]
64
+
65
+ try:
66
+ await asyncio.gather(*tasks)
67
+ except (asyncio.CancelledError, KeyboardInterrupt):
68
+ pass
69
+ finally:
70
+ await connector.disconnect()
71
+ for t in tasks:
72
+ t.cancel()
73
+
74
+ try:
75
+ asyncio.run(run())
76
+ except KeyboardInterrupt:
77
+ pass
@@ -0,0 +1,66 @@
1
+ import asyncio
2
+ import time
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+ from rich.live import Live
9
+
10
+ from son.client.discovery import DiscoveryListener
11
+ from son.core.types import DiscoveryInfo
12
+
13
+ app = typer.Typer(help="Discover sond instances on the local network")
14
+ console = Console()
15
+
16
+
17
+ @app.callback(invoke_without_command=True)
18
+ def discover(
19
+ timeout: Optional[float] = typer.Option(None, "--timeout", "-t", help="scan duration in seconds"),
20
+ ):
21
+ discovered: list[DiscoveryInfo] = []
22
+ start = time.monotonic()
23
+
24
+ def on_discover(info: DiscoveryInfo):
25
+ for d in discovered:
26
+ if d.ip == info.ip and d.port == info.port:
27
+ return
28
+ discovered.append(info)
29
+ console.print(f" [green]✓[/] found [bold]{info.hostname}[/] at {info.ip}:{info.port}")
30
+
31
+ async def run():
32
+ listener = DiscoveryListener()
33
+ listener.on_discover(on_discover)
34
+ console.print("[dim]scanning for sond instances...[/]")
35
+ task = asyncio.create_task(listener.start())
36
+
37
+ if timeout:
38
+ await asyncio.sleep(timeout)
39
+ else:
40
+ await asyncio.sleep(5)
41
+
42
+ listener.stop()
43
+ task.cancel()
44
+ try:
45
+ await task
46
+ except asyncio.CancelledError:
47
+ pass
48
+
49
+ asyncio.run(run())
50
+
51
+ if not discovered:
52
+ console.print("\n[yellow]no sond instances found[/]")
53
+ return
54
+
55
+ table = Table(title=f"Found {len(discovered)} sond instance(s)")
56
+ table.add_column("Hostname", style="cyan")
57
+ table.add_column("IP", style="green")
58
+ table.add_column("Port", style="yellow")
59
+ table.add_column("OS")
60
+
61
+ for d in discovered:
62
+ table.add_row(d.hostname, d.ip, str(d.port), d.os)
63
+
64
+ console.print("\n")
65
+ console.print(table)
66
+ console.print("\n[dim]connect:[/] [bold]son connect <hostname|ip>[/]")
@@ -0,0 +1,19 @@
1
+ import typer
2
+
3
+ from son.cli import discover, connect, tunnel, web
4
+
5
+ app = typer.Typer(
6
+ name="son",
7
+ help="Superior Organised Network - remote access tool",
8
+ no_args_is_help=True,
9
+ )
10
+
11
+ app.add_typer(discover.app, name="discover")
12
+ app.add_typer(connect.app, name="connect")
13
+ app.add_typer(tunnel.app, name="tunnel")
14
+ app.add_typer(web.app, name="web")
15
+
16
+
17
+ @app.callback()
18
+ def callback():
19
+ pass
@@ -0,0 +1,76 @@
1
+ import asyncio
2
+ import re
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+
9
+ from son.client.connector import SonConnector
10
+ from son.client.tunnel import TunnelManager
11
+ from son.core.protocol import CONNECTION_PORT
12
+
13
+ app = typer.Typer(help="Create tunnels through a sond agent")
14
+ console = Console()
15
+
16
+ LOCAL_FORWARD_RE = re.compile(r"^(?:-L\s+)?(\d+):([^:]+):(\d+)$")
17
+
18
+
19
+ @app.callback(invoke_without_command=True)
20
+ def tunnel(
21
+ host: str = typer.Argument(..., help="sond hostname or IP"),
22
+ port: int = typer.Option(CONNECTION_PORT, "--port", "-p", help="sond port"),
23
+ password: str = typer.Option("", "--password", "-P", help="auth password"),
24
+ local: Optional[str] = typer.Option(
25
+ None, "--local", "-L",
26
+ help="local forward: local_port:remote_host:remote_port",
27
+ ),
28
+ ):
29
+ if not local:
30
+ console.print("[red]specify a tunnel with --local (-L), e.g. -L 8080:localhost:80[/]")
31
+ raise typer.Exit(1)
32
+
33
+ m = LOCAL_FORWARD_RE.match(local)
34
+ if not m:
35
+ console.print("[red]invalid format, use: local_port:remote_host:remote_port[/]")
36
+ raise typer.Exit(1)
37
+
38
+ local_port = int(m.group(1))
39
+ remote_host = m.group(2)
40
+ remote_port = int(m.group(3))
41
+
42
+ async def run():
43
+ connector = SonConnector(host, port, password)
44
+ try:
45
+ await connector.connect()
46
+ except Exception as e:
47
+ console.print(f"[red]connection failed:[/] {e}")
48
+ raise typer.Exit(1)
49
+
50
+ mgr = TunnelManager(connector)
51
+
52
+ await mgr.listen("127.0.0.1", local_port, remote_host, remote_port)
53
+
54
+ console.print(Panel(
55
+ f"[bold green]tunnel active[/]\n"
56
+ f" [cyan]127.0.0.1:{local_port}[/] → [yellow]{remote_host}:{remote_port}[/] via [bold]{host}[/]",
57
+ border_style="green",
58
+ ))
59
+ console.print("[dim]press Ctrl+C to stop[/]\n")
60
+
61
+ listen_task = asyncio.create_task(connector.listen())
62
+
63
+ try:
64
+ await asyncio.Event().wait()
65
+ except (asyncio.CancelledError, KeyboardInterrupt):
66
+ pass
67
+ finally:
68
+ await mgr.stop_all()
69
+ listen_task.cancel()
70
+ await connector.disconnect()
71
+ console.print("[red]tunnel closed[/]")
72
+
73
+ try:
74
+ asyncio.run(run())
75
+ except KeyboardInterrupt:
76
+ pass
@@ -0,0 +1,15 @@
1
+ import typer
2
+ from rich.console import Console
3
+
4
+ app = typer.Typer(help="Start the local web UI")
5
+ console = Console()
6
+
7
+
8
+ @app.callback(invoke_without_command=True)
9
+ def web(
10
+ port: int = typer.Option(8585, "--port", "-p", help="web UI port"),
11
+ host: str = typer.Option("127.0.0.1", "--host", "-H", help="bind address"),
12
+ ):
13
+ from son.web.server import run
14
+ console.print(f"[green]starting web UI at [bold]http://{host}:{port}[/][/]")
15
+ run(host=host, port=port)
File without changes
@@ -0,0 +1,125 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from typing import Callable, Optional
5
+
6
+ import websockets
7
+
8
+ from son.core.protocol import (
9
+ WS_OPEN, WS_INPUT, WS_OUTPUT, WS_RESIZE, WS_CLOSE,
10
+ WS_AUTH, WS_AUTH_OK, WS_AUTH_FAIL,
11
+ WS_TUNNEL_OPEN, WS_TUNNEL_DATA, WS_TUNNEL_CLOSE,
12
+ )
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class SonConnector:
18
+ def __init__(self, host: str, port: int, password: str = ""):
19
+ self._host = host
20
+ self._port = port
21
+ self._password = password
22
+ self._ws = None
23
+ self._shell_open = False
24
+ self._on_output: Optional[Callable] = None
25
+ self._on_disconnect: Optional[Callable] = None
26
+ self._on_tunnel_data: Optional[Callable] = None
27
+ self._on_tunnel_close: Optional[Callable] = None
28
+
29
+ @property
30
+ def connected(self) -> bool:
31
+ return self._ws is not None and not self._ws.close_code
32
+
33
+ def on_output(self, callback: Callable):
34
+ self._on_output = callback
35
+
36
+ def on_disconnect(self, callback: Callable):
37
+ self._on_disconnect = callback
38
+
39
+ def on_tunnel_data(self, callback: Callable):
40
+ self._on_tunnel_data = callback
41
+
42
+ def on_tunnel_close(self, callback: Callable):
43
+ self._on_tunnel_close = callback
44
+
45
+ async def connect(self):
46
+ uri = f"ws://{self._host}:{self._port}"
47
+ self._ws = await websockets.connect(uri)
48
+ if self._password:
49
+ await self._send({"type": WS_AUTH, "password": self._password})
50
+ resp = json.loads(await self._ws.recv())
51
+ if resp.get("type") == WS_AUTH_FAIL:
52
+ raise PermissionError("authentication failed")
53
+ log.info("connected to %s:%s", self._host, self._port)
54
+ return self
55
+
56
+ async def open_shell(self, shell: str = "", cols: int = 120, rows: int = 30):
57
+ await self._send({"type": WS_OPEN, "shell": shell, "cols": cols, "rows": rows})
58
+ self._shell_open = True
59
+
60
+ async def send_input(self, data: str):
61
+ if self._shell_open:
62
+ await self._send({"type": WS_INPUT, "data": data})
63
+
64
+ async def resize(self, cols: int, rows: int):
65
+ if self._shell_open:
66
+ await self._send({"type": WS_RESIZE, "cols": cols, "rows": rows})
67
+
68
+ async def close_shell(self):
69
+ if self._shell_open:
70
+ await self._send({"type": WS_CLOSE})
71
+ self._shell_open = False
72
+
73
+ async def tunnel_open(self, remote_host: str, remote_port: int, tunnel_id: int):
74
+ await self._send({
75
+ "type": WS_TUNNEL_OPEN,
76
+ "tunnel_id": tunnel_id,
77
+ "remote_host": remote_host,
78
+ "remote_port": remote_port,
79
+ })
80
+
81
+ async def tunnel_send(self, tunnel_id: int, data: str):
82
+ await self._send({
83
+ "type": WS_TUNNEL_DATA,
84
+ "tunnel_id": tunnel_id,
85
+ "data": data,
86
+ })
87
+
88
+ async def tunnel_close(self, tunnel_id: int):
89
+ await self._send({"type": WS_TUNNEL_CLOSE, "tunnel_id": tunnel_id})
90
+
91
+ async def _send(self, msg: dict):
92
+ if self._ws:
93
+ try:
94
+ await self._ws.send(json.dumps(msg))
95
+ except websockets.exceptions.ConnectionClosed:
96
+ pass
97
+
98
+ async def listen(self):
99
+ try:
100
+ async for raw in self._ws:
101
+ msg = json.loads(raw)
102
+ msg_type = msg.get("type")
103
+ if msg_type == WS_OUTPUT:
104
+ if self._on_output:
105
+ self._on_output(msg.get("data", ""))
106
+ elif msg_type == WS_TUNNEL_DATA:
107
+ if self._on_tunnel_data:
108
+ self._on_tunnel_data(msg.get("tunnel_id"), msg.get("data", ""))
109
+ elif msg_type == WS_TUNNEL_CLOSE:
110
+ if self._on_tunnel_close:
111
+ self._on_tunnel_close(msg.get("tunnel_id"))
112
+ elif msg_type == WS_CLOSE:
113
+ self._shell_open = False
114
+ break
115
+ except (websockets.exceptions.ConnectionClosed, asyncio.CancelledError):
116
+ pass
117
+ finally:
118
+ if self._on_disconnect:
119
+ self._on_disconnect()
120
+
121
+ async def disconnect(self):
122
+ await self.close_shell()
123
+ if self._ws:
124
+ await self._ws.close()
125
+ log.info("disconnected")
@@ -0,0 +1,72 @@
1
+ import asyncio
2
+ import socket
3
+ import logging
4
+ from typing import Callable
5
+
6
+ from son.core.protocol import DISCOVERY_PORT, DISCOVERY_MAGIC
7
+ from son.core.types import DiscoveryInfo
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+
12
+ class DiscoveryListener:
13
+ def __init__(self):
14
+ self._running = False
15
+ self._on_discover: Callable[[DiscoveryInfo], None] | None = None
16
+ self._seen: set[str] = set()
17
+
18
+ def on_discover(self, callback: Callable[[DiscoveryInfo], None]):
19
+ self._on_discover = callback
20
+
21
+ async def start(self):
22
+ self._running = True
23
+ loop = asyncio.get_running_loop()
24
+ transport: asyncio.DatagramTransport | None = None
25
+
26
+ class Proto(asyncio.DatagramProtocol):
27
+ def connection_made(self, t):
28
+ nonlocal transport
29
+ transport = t
30
+ sock = t.get_extra_info("socket")
31
+ if sock is not None:
32
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
33
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
34
+
35
+ def datagram_received(self, data: bytes, addr):
36
+ if not data.startswith(DISCOVERY_MAGIC):
37
+ return
38
+ payload = data[len(DISCOVERY_MAGIC) + 1:]
39
+ try:
40
+ info = DiscoveryInfo.from_bytes(payload)
41
+ key = f"{info.ip}:{info.port}"
42
+ if key not in self._seen:
43
+ self._seen.add(key)
44
+ log.debug("discovered: %s", info)
45
+ if self._on_discover:
46
+ self._on_discover(info)
47
+ except Exception as e:
48
+ log.debug("bad discovery packet from %s: %s", addr, e)
49
+
50
+ def error_received(self, exc):
51
+ log.warning("discovery listener error: %s", exc)
52
+
53
+ try:
54
+ _, _ = await loop.create_datagram_endpoint(
55
+ Proto,
56
+ local_addr=("0.0.0.0", DISCOVERY_PORT),
57
+ allow_broadcast=True,
58
+ )
59
+ log.info("discovery listener started on port %s", DISCOVERY_PORT)
60
+ while self._running:
61
+ await asyncio.sleep(1)
62
+ except asyncio.CancelledError:
63
+ pass
64
+ finally:
65
+ if transport:
66
+ transport.close()
67
+
68
+ def stop(self):
69
+ self._running = False
70
+
71
+ def reset_seen(self):
72
+ self._seen.clear()