torrus 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.
- torrus-0.1.0/LICENSE +21 -0
- torrus-0.1.0/PKG-INFO +82 -0
- torrus-0.1.0/README.md +52 -0
- torrus-0.1.0/pyproject.toml +46 -0
- torrus-0.1.0/setup.cfg +4 -0
- torrus-0.1.0/src/torrus/__init__.py +3 -0
- torrus-0.1.0/src/torrus/cli.py +40 -0
- torrus-0.1.0/src/torrus/server.py +249 -0
- torrus-0.1.0/src/torrus/ssh_manager.py +564 -0
- torrus-0.1.0/src/torrus/static/assets/favicon-CSRr8MAM.svg +16 -0
- torrus-0.1.0/src/torrus/static/assets/index-Cqx9HX2H.css +32 -0
- torrus-0.1.0/src/torrus/static/assets/index-frz8ykZJ.js +11 -0
- torrus-0.1.0/src/torrus/static/assets/vendor-react-59tfzSPj.js +24 -0
- torrus-0.1.0/src/torrus/static/assets/vendor-socket-BcxXcwBL.js +1 -0
- torrus-0.1.0/src/torrus/static/assets/vendor-ui-413r9PKx.js +120 -0
- torrus-0.1.0/src/torrus/static/assets/vendor-xterm-B5rsJnKL.js +9 -0
- torrus-0.1.0/src/torrus/static/fonts/JetBrainsMono-Bold.woff2 +0 -0
- torrus-0.1.0/src/torrus/static/fonts/JetBrainsMono-Italic.woff2 +0 -0
- torrus-0.1.0/src/torrus/static/fonts/JetBrainsMono-Regular.woff2 +0 -0
- torrus-0.1.0/src/torrus/static/index.html +18 -0
- torrus-0.1.0/src/torrus.egg-info/PKG-INFO +82 -0
- torrus-0.1.0/src/torrus.egg-info/SOURCES.txt +24 -0
- torrus-0.1.0/src/torrus.egg-info/dependency_links.txt +1 -0
- torrus-0.1.0/src/torrus.egg-info/entry_points.txt +2 -0
- torrus-0.1.0/src/torrus.egg-info/requires.txt +6 -0
- torrus-0.1.0/src/torrus.egg-info/top_level.txt +1 -0
torrus-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anudeep Dhavaleswarapu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
torrus-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: torrus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Web-based SSH terminal that works behind any reverse proxy
|
|
5
|
+
Author-email: Anudeep Dhavaleswarapu <anudeepd2@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/anudeepd/torrus
|
|
8
|
+
Project-URL: Repository, https://github.com/anudeepd/torrus
|
|
9
|
+
Project-URL: Issues, https://github.com/anudeepd/torrus/issues
|
|
10
|
+
Keywords: ssh,terminal,web,xterm,remote
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
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: Topic :: System :: Networking
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: fastapi>=0.110.0
|
|
24
|
+
Requires-Dist: python-socketio>=5.11.0
|
|
25
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
26
|
+
Requires-Dist: paramiko>=3.4.0
|
|
27
|
+
Requires-Dist: aiofiles>=23.2.0
|
|
28
|
+
Requires-Dist: click>=8.1.0
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img src="https://raw.githubusercontent.com/anudeepd/torrus/main/assets/logo.svg" alt="Torrus" width="120"/>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<h1 align="center">Torrus</h1>
|
|
36
|
+
|
|
37
|
+
<p align="center">A web-based SSH terminal that works behind any reverse proxy. Install it, run it, use it.</p>
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Web-based SSH terminal** with full xterm.js emulation
|
|
42
|
+
- **Multi-tab support** — open multiple SSH sessions side by side
|
|
43
|
+
- **Saved servers** — save, edit, import, and export connection configs
|
|
44
|
+
- **Works behind reverse proxies** — uses Socket.IO for reliable transport
|
|
45
|
+
- **Session sidebar** — quick-connect to saved servers
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install torrus
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
torrus serve
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Opens the terminal in your browser. Connect to any SSH server from there.
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
--host TEXT Bind host. [default: 0.0.0.0]
|
|
65
|
+
--port INTEGER Bind port. [default: 8080]
|
|
66
|
+
--no-browser Don't open the browser automatically.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
Requires [uv](https://github.com/astral-sh/uv).
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
git clone https://github.com/anudeepd/torrus
|
|
75
|
+
cd torrus
|
|
76
|
+
uv sync
|
|
77
|
+
make dev
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
torrus-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/anudeepd/torrus/main/assets/logo.svg" alt="Torrus" width="120"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Torrus</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">A web-based SSH terminal that works behind any reverse proxy. Install it, run it, use it.</p>
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Web-based SSH terminal** with full xterm.js emulation
|
|
12
|
+
- **Multi-tab support** — open multiple SSH sessions side by side
|
|
13
|
+
- **Saved servers** — save, edit, import, and export connection configs
|
|
14
|
+
- **Works behind reverse proxies** — uses Socket.IO for reliable transport
|
|
15
|
+
- **Session sidebar** — quick-connect to saved servers
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install torrus
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
torrus serve
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Opens the terminal in your browser. Connect to any SSH server from there.
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
--host TEXT Bind host. [default: 0.0.0.0]
|
|
35
|
+
--port INTEGER Bind port. [default: 8080]
|
|
36
|
+
--no-browser Don't open the browser automatically.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
Requires [uv](https://github.com/astral-sh/uv).
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/anudeepd/torrus
|
|
45
|
+
cd torrus
|
|
46
|
+
uv sync
|
|
47
|
+
make dev
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "torrus"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Web-based SSH terminal that works behind any reverse proxy"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Anudeep Dhavaleswarapu", email = "anudeepd2@gmail.com" }]
|
|
13
|
+
keywords = ["ssh", "terminal", "web", "xterm", "remote"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Web Environment",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: System Administrators",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: System :: Networking",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"fastapi>=0.110.0",
|
|
27
|
+
"python-socketio>=5.11.0",
|
|
28
|
+
"uvicorn[standard]>=0.29.0",
|
|
29
|
+
"paramiko>=3.4.0",
|
|
30
|
+
"aiofiles>=23.2.0",
|
|
31
|
+
"click>=8.1.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/anudeepd/torrus"
|
|
36
|
+
Repository = "https://github.com/anudeepd/torrus"
|
|
37
|
+
Issues = "https://github.com/anudeepd/torrus/issues"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
torrus = "torrus.cli:main"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
torrus = ["static/**/*", "static/*"]
|
torrus-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import webbrowser
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import uvicorn
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def main():
|
|
11
|
+
"""torrus — web-based SSH terminal."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@main.command()
|
|
16
|
+
@click.option("--host", default="0.0.0.0", show_default=True, help="Bind host")
|
|
17
|
+
@click.option("--port", default=8080, show_default=True, help="Bind port")
|
|
18
|
+
@click.option("--no-browser", is_flag=True, default=False, help="Don't open browser on startup")
|
|
19
|
+
@click.option("--reload", is_flag=True, default=False, hidden=True, help="Dev auto-reload")
|
|
20
|
+
def serve(host, port, no_browser, reload):
|
|
21
|
+
"""Start the torrus SSH web terminal."""
|
|
22
|
+
logging.basicConfig(
|
|
23
|
+
level=logging.INFO,
|
|
24
|
+
format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
|
|
25
|
+
datefmt="%H:%M:%S",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if not no_browser:
|
|
29
|
+
browse_host = "127.0.0.1" if host == "0.0.0.0" else host
|
|
30
|
+
url = f"http://{browse_host}:{port}"
|
|
31
|
+
# Open after a short delay so uvicorn has time to bind
|
|
32
|
+
threading.Timer(1.5, webbrowser.open, args=[url]).start()
|
|
33
|
+
|
|
34
|
+
uvicorn.run(
|
|
35
|
+
"torrus.server:app",
|
|
36
|
+
host=host,
|
|
37
|
+
port=port,
|
|
38
|
+
reload=reload,
|
|
39
|
+
log_level="info",
|
|
40
|
+
)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""FastAPI + Socket.IO ASGI application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from importlib.resources import files
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import socketio
|
|
13
|
+
from fastapi import FastAPI
|
|
14
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
15
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
16
|
+
from fastapi.staticfiles import StaticFiles
|
|
17
|
+
|
|
18
|
+
from torrus.ssh_manager import SSHManager
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("torrus.server")
|
|
21
|
+
|
|
22
|
+
_SAFE_ID = re.compile(r'^[a-zA-Z0-9_\-]+$')
|
|
23
|
+
_DEV_MODE = bool(os.getenv("TORRUS_DEV"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _safe_int(value, default: int) -> int:
|
|
27
|
+
"""Parse an integer from untrusted input, returning default on failure."""
|
|
28
|
+
try:
|
|
29
|
+
return int(value)
|
|
30
|
+
except (TypeError, ValueError):
|
|
31
|
+
return default
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _valid_id(value: str) -> bool:
|
|
35
|
+
"""Check that an ID contains only safe characters."""
|
|
36
|
+
return bool(value and _SAFE_ID.match(value))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _static_dir() -> Path | None:
|
|
40
|
+
try:
|
|
41
|
+
p = Path(str(files("torrus").joinpath("static")))
|
|
42
|
+
return p if p.exists() else None
|
|
43
|
+
except Exception:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Socket.IO + FastAPI setup
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
sio = socketio.AsyncServer(
|
|
52
|
+
async_mode="asgi",
|
|
53
|
+
cors_allowed_origins=(
|
|
54
|
+
["http://localhost:5173", "http://127.0.0.1:5173"]
|
|
55
|
+
if _DEV_MODE
|
|
56
|
+
else []
|
|
57
|
+
),
|
|
58
|
+
max_http_buffer_size=10_000_000,
|
|
59
|
+
ping_timeout=60,
|
|
60
|
+
ping_interval=25,
|
|
61
|
+
logger=False,
|
|
62
|
+
engineio_logger=False,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
fastapi_app = FastAPI(title="torrus", docs_url=None, redoc_url=None)
|
|
66
|
+
|
|
67
|
+
# CORS for Vite dev server
|
|
68
|
+
if _DEV_MODE:
|
|
69
|
+
fastapi_app.add_middleware(
|
|
70
|
+
CORSMiddleware,
|
|
71
|
+
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
|
72
|
+
allow_credentials=True,
|
|
73
|
+
allow_methods=["*"],
|
|
74
|
+
allow_headers=["*"],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Combined ASGI app — uvicorn runs this
|
|
78
|
+
app = socketio.ASGIApp(sio, other_asgi_app=fastapi_app)
|
|
79
|
+
|
|
80
|
+
ssh_manager = SSHManager(sio)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Static files + SPA fallback (only when built frontend exists)
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
_static = _static_dir()
|
|
88
|
+
|
|
89
|
+
if _static:
|
|
90
|
+
# Serve /assets/ from Vite build output
|
|
91
|
+
assets_dir = _static / "assets"
|
|
92
|
+
if assets_dir.exists():
|
|
93
|
+
fastapi_app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
|
|
94
|
+
|
|
95
|
+
# Serve /fonts/ from Python static dir (JetBrains Mono)
|
|
96
|
+
fonts_dir = _static / "fonts"
|
|
97
|
+
if fonts_dir.exists():
|
|
98
|
+
fastapi_app.mount("/fonts", StaticFiles(directory=str(fonts_dir)), name="fonts")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@fastapi_app.get("/{full_path:path}", include_in_schema=False)
|
|
102
|
+
async def spa_fallback(full_path: str):
|
|
103
|
+
if full_path.startswith("socket.io"):
|
|
104
|
+
return JSONResponse(status_code=404, content={"detail": "Not found"})
|
|
105
|
+
if _static:
|
|
106
|
+
index = _static / "index.html"
|
|
107
|
+
if index.exists():
|
|
108
|
+
return FileResponse(str(index))
|
|
109
|
+
return JSONResponse(
|
|
110
|
+
status_code=503,
|
|
111
|
+
content={"detail": "Frontend not built. Run: cd frontend && npm run build"},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Socket.IO lifecycle
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
@sio.on("connect")
|
|
120
|
+
async def on_connect(sid, environ):
|
|
121
|
+
remote = environ.get("REMOTE_ADDR", "unknown")
|
|
122
|
+
logger.info("Client connected: %s (from %s)", sid, remote)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@sio.on("disconnect")
|
|
126
|
+
async def on_disconnect(sid):
|
|
127
|
+
ssh_manager.unmap_sid(sid)
|
|
128
|
+
logger.info("Client disconnected: %s", sid)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Session registration / recovery
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
@sio.on("session:register")
|
|
136
|
+
async def on_session_register(sid, data):
|
|
137
|
+
session_id = data.get("session_id", "")
|
|
138
|
+
tab_id = data.get("tab_id", "")
|
|
139
|
+
if not _valid_id(session_id) or not _valid_id(tab_id):
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
status = await ssh_manager.restore_session(sid, session_id, tab_id)
|
|
143
|
+
await sio.emit("session:restored", {"tab_id": tab_id, "status": status}, to=sid)
|
|
144
|
+
# Force shell redraw AFTER session:restored so the frontend clears the
|
|
145
|
+
# viewport first, then the fresh prompt from SIGWINCH overwrites it.
|
|
146
|
+
if status == "active":
|
|
147
|
+
await ssh_manager.force_redraw(session_id, tab_id)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# SSH events
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
@sio.on("ssh:connect")
|
|
155
|
+
async def on_ssh_connect(sid, data):
|
|
156
|
+
host = data.get("host", "").strip()
|
|
157
|
+
port = _safe_int(data.get("port", 22), 22)
|
|
158
|
+
username = data.get("username", "").strip()
|
|
159
|
+
password = data.get("password", "")
|
|
160
|
+
session_id = data.get("session_id", "")
|
|
161
|
+
tab_id = data.get("tab_id", "")
|
|
162
|
+
cols = _safe_int(data.get("cols", 220), 220)
|
|
163
|
+
rows = _safe_int(data.get("rows", 50), 50)
|
|
164
|
+
|
|
165
|
+
if not host or not username or not _valid_id(session_id) or not _valid_id(tab_id):
|
|
166
|
+
await sio.emit(
|
|
167
|
+
"ssh:error",
|
|
168
|
+
{"tab_id": tab_id, "message": "Missing required fields.", "code": "invalid_request"},
|
|
169
|
+
to=sid,
|
|
170
|
+
)
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
await ssh_manager.connect(
|
|
174
|
+
sid=sid,
|
|
175
|
+
session_id=session_id,
|
|
176
|
+
tab_id=tab_id,
|
|
177
|
+
host=host,
|
|
178
|
+
port=port,
|
|
179
|
+
username=username,
|
|
180
|
+
password=password,
|
|
181
|
+
cols=cols,
|
|
182
|
+
rows=rows,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@sio.on("ssh:input")
|
|
187
|
+
async def on_ssh_input(sid, data):
|
|
188
|
+
await ssh_manager.handle_input(
|
|
189
|
+
data.get("session_id", ""),
|
|
190
|
+
data.get("tab_id", ""),
|
|
191
|
+
data.get("data", ""),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@sio.on("terminal:resize")
|
|
196
|
+
async def on_terminal_resize(sid, data):
|
|
197
|
+
await ssh_manager.handle_resize(
|
|
198
|
+
data.get("session_id", ""),
|
|
199
|
+
data.get("tab_id", ""),
|
|
200
|
+
_safe_int(data.get("cols", 80), 80),
|
|
201
|
+
_safe_int(data.get("rows", 24), 24),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@sio.on("ssh:disconnect")
|
|
206
|
+
async def on_ssh_disconnect(sid, data):
|
|
207
|
+
await ssh_manager.disconnect_session(
|
|
208
|
+
data.get("session_id", ""),
|
|
209
|
+
data.get("tab_id", ""),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@sio.on("ssh:clone")
|
|
214
|
+
async def on_ssh_clone(sid, data):
|
|
215
|
+
session_id = data.get("session_id", "")
|
|
216
|
+
source_tab_id = data.get("source_tab_id", "")
|
|
217
|
+
new_tab_id = data.get("new_tab_id", "")
|
|
218
|
+
cols = _safe_int(data.get("cols", 220), 220)
|
|
219
|
+
rows = _safe_int(data.get("rows", 50), 50)
|
|
220
|
+
|
|
221
|
+
if not _valid_id(session_id) or not _valid_id(source_tab_id) or not _valid_id(new_tab_id):
|
|
222
|
+
await sio.emit(
|
|
223
|
+
"ssh:error",
|
|
224
|
+
{"tab_id": new_tab_id, "message": "Missing required fields.", "code": "invalid_request"},
|
|
225
|
+
to=sid,
|
|
226
|
+
)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
await ssh_manager.clone(
|
|
230
|
+
sid=sid,
|
|
231
|
+
session_id=session_id,
|
|
232
|
+
source_tab_id=source_tab_id,
|
|
233
|
+
new_tab_id=new_tab_id,
|
|
234
|
+
cols=cols,
|
|
235
|
+
rows=rows,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
# Startup
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
@asynccontextmanager
|
|
244
|
+
async def lifespan(app):
|
|
245
|
+
ssh_manager.start_background_tasks()
|
|
246
|
+
yield
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
fastapi_app.router.lifespan_context = lifespan
|