sshpeek 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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git init *)",
5
+ "Bash(git add *)"
6
+ ]
7
+ }
8
+ }
@@ -0,0 +1,12 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - uses: actions/checkout@v4
8
+ - uses: actions/setup-python@v5
9
+ with:
10
+ python-version: "3.12"
11
+ - run: pip install -e . pytest
12
+ - run: pytest -q
@@ -0,0 +1,17 @@
1
+ name: release
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+ jobs:
6
+ pypi:
7
+ runs-on: ubuntu-latest
8
+ permissions:
9
+ id-token: write # PyPI trusted publishing, no token needed
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-python@v5
13
+ with:
14
+ python-version: "3.12"
15
+ - run: pip install build
16
+ - run: python -m build
17
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ # personal config stays out of the repo; sshpeek.example.yaml is tracked
10
+ sshpeek.yaml
sshpeek-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thibaut Lamadon
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.
sshpeek-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: sshpeek
3
+ Version: 0.1.0
4
+ Summary: Local web UI for peeking at remote files and ports over SSH
5
+ Project-URL: Homepage, https://github.com/tlamadon/sshpeek
6
+ Project-URL: Issues, https://github.com/tlamadon/sshpeek/issues
7
+ Author: Thibaut Lamadon
8
+ License: MIT License
9
+
10
+ Copyright (c) 2026 Thibaut Lamadon
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: pdf,port-forward,s3,sftp,ssh,tunnel,web-ui
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Environment :: Web Environment
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: Intended Audience :: Science/Research
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Programming Language :: Python :: 3.14
40
+ Classifier: Topic :: Internet :: WWW/HTTP
41
+ Classifier: Topic :: System :: Networking
42
+ Requires-Python: >=3.11
43
+ Requires-Dist: asyncssh>=2.14
44
+ Requires-Dist: httpx>=0.27
45
+ Requires-Dist: pyyaml>=6
46
+ Requires-Dist: starlette>=0.37
47
+ Requires-Dist: uvicorn>=0.29
48
+ Requires-Dist: websockets>=13
49
+ Provides-Extra: s3
50
+ Requires-Dist: boto3>=1.34; extra == 's3'
51
+ Description-Content-Type: text/markdown
52
+
53
+ # sshpeek
54
+
55
+ A small local web UI for peeking at remote machines over SSH: browse and view
56
+ files, live-view PDFs and images that auto-refresh when they change remotely
57
+ (remote LaTeX builds, plotting pipelines), open port tunnels, reach named
58
+ remote web apps at stable `*.localhost` URLs, and browse S3 buckets alongside.
59
+
60
+ One multiplexed SSH connection per host, kept alive in the background.
61
+ Everything that works with plain `ssh <host>` works here: hosts, keys,
62
+ ProxyJump, agents all come from `~/.ssh/config` and your ssh-agent.
63
+
64
+ ## Quickstart
65
+
66
+ ```sh
67
+ pip install sshpeek # or: uv tool install sshpeek
68
+ pip install 'sshpeek[s3]' # with S3 support
69
+ sshpeek # → http://127.0.0.1:8642
70
+ ```
71
+
72
+ Or straight from the repo without installing: `uv run sshpeek`.
73
+
74
+ Zero config works (type any `~/.ssh/config` alias in the host box), but the
75
+ point of sshpeek is the declarative config. Copy `sshpeek.example.yaml` to
76
+ `./sshpeek.yaml` or `~/.config/sshpeek/sshpeek.yaml` (or point `$SSHPEEK_CONFIG` /
77
+ `--config` at it):
78
+
79
+ ```yaml
80
+ listen: { host: 127.0.0.1, port: 8642 }
81
+
82
+ hosts:
83
+ mercury:
84
+ tunnels:
85
+ - 5432 # 127.0.0.1:5432 -> mercury localhost:5432
86
+ - local: 18888
87
+ remote: localhost:8888
88
+ http:
89
+ jupyter: 8888 # http://jupyter.mercury.localhost:8642/
90
+ mlflow: localhost:5000
91
+ homebox:
92
+ http:
93
+ grafana: 3000
94
+ internultra: # just a host chip for browsing
95
+
96
+ s3:
97
+ data: my-bucket # chip "data" -> bucket "my-bucket"
98
+ results:
99
+ bucket: my-results-bucket
100
+ prefix: runs/ # browse only under this key prefix
101
+ profile: work # ~/.aws profile; SSO works too
102
+ ```
103
+
104
+ Declared tunnels and services are established at startup and re-ensured
105
+ every 20s, so they self-heal after connection drops or laptop sleep
106
+ (re-binding the same local ports).
107
+
108
+ ## What it does
109
+
110
+ - **Files** — browse remote directories (sftp over the shared connection),
111
+ view text/logs/PDFs inline, download anything. File-type icons, and a
112
+ toggle for dotfiles (hidden by default).
113
+ - **Live views** — the `live` link next to any PDF or image opens a viewer
114
+ that watches the file over SSE and re-renders when it changes, keeping
115
+ your scroll position. PDF hyperlinks (URLs, citations, TOC) are clickable.
116
+ Refreshes are debounced until the file size is stable across two polls,
117
+ so a `latexmk` mid-write never renders garbage. A "Live views" panel
118
+ lists every open view and can detach one remotely — its tab closes
119
+ itself, handy for trimming after a long session.
120
+ - **S3 sources** — buckets declared under `s3:` appear next to the SSH
121
+ hosts: same browsing, peeking, downloads and live views, with keys
122
+ presented as paths. Credentials use the normal boto3 chain (env vars,
123
+ profiles, SSO). Optional: `pip install 'sshpeek[s3]'`.
124
+ - **Tunnels** — raw TCP binds on `127.0.0.1` to any remote port through the
125
+ SSH connection: databases, dashboards, anything. Declared in YAML (pinned)
126
+ or opened ad hoc from the UI.
127
+ - **HTTP services** — named remote web apps proxied by sshpeek itself under
128
+ `<name>.<host>.localhost:<port>`. Browsers resolve `*.localhost` to
129
+ loopback natively, so the URLs are bookmarkable with no `/etc/hosts`
130
+ edits. Each service lives at the root of its own origin, so apps that
131
+ generate absolute paths (Jupyter, Grafana, ...) work unmodified, cookies
132
+ stay isolated per service, and WebSockets are proxied too.
133
+
134
+ Tunnels vs. services: a tunnel gives you a local TCP port (use for anything
135
+ non-HTTP, or when the app itself must see `127.0.0.1`); a service gives you
136
+ a stable named URL through sshpeek's proxy. Both ride the same SSH connection.
137
+
138
+ ## Notes
139
+
140
+ - Host keys are checked against `~/.ssh/known_hosts`; if you have never
141
+ ssh'd to a host from this machine, do that once first.
142
+ - Binds to 127.0.0.1 by default. Anything running locally can reach the API
143
+ and therefore your remote files — same trust model as your ssh-agent.
144
+ - `*.localhost` resolution is native in Chrome/Firefox/Safari and
145
+ systemd-resolved; curl needs `--resolve` or a hosts entry.
146
+ - `--port` / `--host` / `--config` / `-v` flags on the CLI.
147
+
148
+ ## API
149
+
150
+ ```
151
+ GET /api/hosts configured + connected hosts
152
+ GET /api/services declared HTTP services + proxy URLs
153
+ GET /api/ls?host=&path= directory listing (default: remote $HOME)
154
+ GET /api/file?host=&path=[&dl=1] stream file bytes
155
+ GET /api/events?host=&path= SSE change events (stable-size debounced)
156
+ GET /api/views open live views (one per events stream)
157
+ DELETE /api/views/{id} detach a live view (its tab closes itself)
158
+ GET /api/forwards list tunnels
159
+ POST /api/forwards {"host": "mercury", "port": 8888, "local": 18888}
160
+ DELETE /api/forwards/{id}
161
+ ```
162
+
163
+ ## Roadmap
164
+
165
+ - Log tailing: `/api/file` with an offset param + incremental fetch,
166
+ rendered as a live `<pre>` — cheap `tail -f` for Slurm `.out` files.
167
+ - Browser terminal: xterm.js over a WebSocket to an SSH PTY.
168
+ - Centralized deployment: containerize behind Caddy + Cloudflare Access;
169
+ the subdomain proxy pattern carries over (real subdomains instead of
170
+ `*.localhost`), tunnels become the piece that stays per-machine.
@@ -0,0 +1,118 @@
1
+ # sshpeek
2
+
3
+ A small local web UI for peeking at remote machines over SSH: browse and view
4
+ files, live-view PDFs and images that auto-refresh when they change remotely
5
+ (remote LaTeX builds, plotting pipelines), open port tunnels, reach named
6
+ remote web apps at stable `*.localhost` URLs, and browse S3 buckets alongside.
7
+
8
+ One multiplexed SSH connection per host, kept alive in the background.
9
+ Everything that works with plain `ssh <host>` works here: hosts, keys,
10
+ ProxyJump, agents all come from `~/.ssh/config` and your ssh-agent.
11
+
12
+ ## Quickstart
13
+
14
+ ```sh
15
+ pip install sshpeek # or: uv tool install sshpeek
16
+ pip install 'sshpeek[s3]' # with S3 support
17
+ sshpeek # → http://127.0.0.1:8642
18
+ ```
19
+
20
+ Or straight from the repo without installing: `uv run sshpeek`.
21
+
22
+ Zero config works (type any `~/.ssh/config` alias in the host box), but the
23
+ point of sshpeek is the declarative config. Copy `sshpeek.example.yaml` to
24
+ `./sshpeek.yaml` or `~/.config/sshpeek/sshpeek.yaml` (or point `$SSHPEEK_CONFIG` /
25
+ `--config` at it):
26
+
27
+ ```yaml
28
+ listen: { host: 127.0.0.1, port: 8642 }
29
+
30
+ hosts:
31
+ mercury:
32
+ tunnels:
33
+ - 5432 # 127.0.0.1:5432 -> mercury localhost:5432
34
+ - local: 18888
35
+ remote: localhost:8888
36
+ http:
37
+ jupyter: 8888 # http://jupyter.mercury.localhost:8642/
38
+ mlflow: localhost:5000
39
+ homebox:
40
+ http:
41
+ grafana: 3000
42
+ internultra: # just a host chip for browsing
43
+
44
+ s3:
45
+ data: my-bucket # chip "data" -> bucket "my-bucket"
46
+ results:
47
+ bucket: my-results-bucket
48
+ prefix: runs/ # browse only under this key prefix
49
+ profile: work # ~/.aws profile; SSO works too
50
+ ```
51
+
52
+ Declared tunnels and services are established at startup and re-ensured
53
+ every 20s, so they self-heal after connection drops or laptop sleep
54
+ (re-binding the same local ports).
55
+
56
+ ## What it does
57
+
58
+ - **Files** — browse remote directories (sftp over the shared connection),
59
+ view text/logs/PDFs inline, download anything. File-type icons, and a
60
+ toggle for dotfiles (hidden by default).
61
+ - **Live views** — the `live` link next to any PDF or image opens a viewer
62
+ that watches the file over SSE and re-renders when it changes, keeping
63
+ your scroll position. PDF hyperlinks (URLs, citations, TOC) are clickable.
64
+ Refreshes are debounced until the file size is stable across two polls,
65
+ so a `latexmk` mid-write never renders garbage. A "Live views" panel
66
+ lists every open view and can detach one remotely — its tab closes
67
+ itself, handy for trimming after a long session.
68
+ - **S3 sources** — buckets declared under `s3:` appear next to the SSH
69
+ hosts: same browsing, peeking, downloads and live views, with keys
70
+ presented as paths. Credentials use the normal boto3 chain (env vars,
71
+ profiles, SSO). Optional: `pip install 'sshpeek[s3]'`.
72
+ - **Tunnels** — raw TCP binds on `127.0.0.1` to any remote port through the
73
+ SSH connection: databases, dashboards, anything. Declared in YAML (pinned)
74
+ or opened ad hoc from the UI.
75
+ - **HTTP services** — named remote web apps proxied by sshpeek itself under
76
+ `<name>.<host>.localhost:<port>`. Browsers resolve `*.localhost` to
77
+ loopback natively, so the URLs are bookmarkable with no `/etc/hosts`
78
+ edits. Each service lives at the root of its own origin, so apps that
79
+ generate absolute paths (Jupyter, Grafana, ...) work unmodified, cookies
80
+ stay isolated per service, and WebSockets are proxied too.
81
+
82
+ Tunnels vs. services: a tunnel gives you a local TCP port (use for anything
83
+ non-HTTP, or when the app itself must see `127.0.0.1`); a service gives you
84
+ a stable named URL through sshpeek's proxy. Both ride the same SSH connection.
85
+
86
+ ## Notes
87
+
88
+ - Host keys are checked against `~/.ssh/known_hosts`; if you have never
89
+ ssh'd to a host from this machine, do that once first.
90
+ - Binds to 127.0.0.1 by default. Anything running locally can reach the API
91
+ and therefore your remote files — same trust model as your ssh-agent.
92
+ - `*.localhost` resolution is native in Chrome/Firefox/Safari and
93
+ systemd-resolved; curl needs `--resolve` or a hosts entry.
94
+ - `--port` / `--host` / `--config` / `-v` flags on the CLI.
95
+
96
+ ## API
97
+
98
+ ```
99
+ GET /api/hosts configured + connected hosts
100
+ GET /api/services declared HTTP services + proxy URLs
101
+ GET /api/ls?host=&path= directory listing (default: remote $HOME)
102
+ GET /api/file?host=&path=[&dl=1] stream file bytes
103
+ GET /api/events?host=&path= SSE change events (stable-size debounced)
104
+ GET /api/views open live views (one per events stream)
105
+ DELETE /api/views/{id} detach a live view (its tab closes itself)
106
+ GET /api/forwards list tunnels
107
+ POST /api/forwards {"host": "mercury", "port": 8888, "local": 18888}
108
+ DELETE /api/forwards/{id}
109
+ ```
110
+
111
+ ## Roadmap
112
+
113
+ - Log tailing: `/api/file` with an offset param + incremental fetch,
114
+ rendered as a live `<pre>` — cheap `tail -f` for Slurm `.out` files.
115
+ - Browser terminal: xterm.js over a WebSocket to an SSH PTY.
116
+ - Centralized deployment: containerize behind Caddy + Cloudflare Access;
117
+ the subdomain proxy pattern carries over (real subdomains instead of
118
+ `*.localhost`), tunnels become the piece that stays per-machine.
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "sshpeek"
3
+ version = "0.1.0"
4
+ description = "Local web UI for peeking at remote files and ports over SSH"
5
+ readme = "README.md"
6
+ license = { file = "LICENSE" }
7
+ authors = [{ name = "Thibaut Lamadon" }]
8
+ requires-python = ">=3.11"
9
+ keywords = ["ssh", "sftp", "tunnel", "port-forward", "pdf", "s3", "web-ui"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Web Environment",
13
+ "Intended Audience :: Developers",
14
+ "Intended Audience :: Science/Research",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Topic :: Internet :: WWW/HTTP",
21
+ "Topic :: System :: Networking",
22
+ ]
23
+ dependencies = [
24
+ "asyncssh>=2.14",
25
+ "starlette>=0.37",
26
+ "uvicorn>=0.29",
27
+ "pyyaml>=6",
28
+ "httpx>=0.27",
29
+ "websockets>=13",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/tlamadon/sshpeek"
34
+ Issues = "https://github.com/tlamadon/sshpeek/issues"
35
+
36
+ [project.optional-dependencies]
37
+ s3 = ["boto3>=1.34"]
38
+
39
+ [project.scripts]
40
+ sshpeek = "sshpeek.__main__:main"
41
+
42
+ [build-system]
43
+ requires = ["hatchling"]
44
+ build-backend = "hatchling.build"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/sshpeek"]
@@ -0,0 +1,3 @@
1
+ """sshpeek: local web UI for peeking at remote files and ports over SSH."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+
6
+ import uvicorn
7
+
8
+
9
+ def main() -> None:
10
+ from .config import load_config
11
+
12
+ cfg = load_config()
13
+ ap = argparse.ArgumentParser(
14
+ prog="sshpeek",
15
+ description="Local web UI for peeking at remote files and ports over SSH.",
16
+ )
17
+ ap.add_argument("--host", default=cfg.listen_host,
18
+ help=f"bind address (default: {cfg.listen_host})")
19
+ ap.add_argument("--port", type=int, default=cfg.listen_port,
20
+ help=f"bind port (default: {cfg.listen_port})")
21
+ ap.add_argument("--config", help="path to sshpeek.yaml (also: $SSHPEEK_CONFIG)")
22
+ ap.add_argument("-v", "--verbose", action="store_true", help="debug logging")
23
+ args = ap.parse_args()
24
+
25
+ if args.config:
26
+ import os
27
+ os.environ["SSHPEEK_CONFIG"] = args.config
28
+
29
+ logging.basicConfig(
30
+ level=logging.DEBUG if args.verbose else logging.INFO,
31
+ format="%(asctime)s %(name)s %(levelname)s %(message)s",
32
+ )
33
+ print(f"sshpeek: http://{args.host}:{args.port}")
34
+ uvicorn.run("sshpeek.app:app", host=args.host, port=args.port, log_level="warning")
35
+
36
+
37
+ if __name__ == "__main__":
38
+ main()