wijjit_ssh.server
A “Flask for SSH apps” server: expose a Wijjit app over SSH.
WijjitSSH wraps asyncssh so that each incoming SSH connection gets
its own Wijjit application, driven through a
RemoteTerminalBackend. You supply a factory - the
SSH analogue of a Flask view - that builds an app per connection:
>>> from wijjit import Wijjit, render_template_string
>>> from wijjit_ssh import WijjitSSH
>>> from wijjit_ssh.auth import AuthorizedKeys
>>>
>>> def make_app(session):
... app = Wijjit(backend=session.backend)
... @app.view("main", default=True)
... def main():
... return render_template_string(
... "{% frame %}{% text %}Hi {{ who }}!{% endtext %}{% endframe %}",
... who=session.username,
... )
... return app
>>>
>>> WijjitSSH(
... make_app,
... host_keys=[ensure_host_key("ssh_host_key")],
... auth=AuthorizedKeys("~/.ssh/authorized_keys"),
... ).run(port=8022)
Then ssh -p 8022 you@localhost drops the client straight into the TUI.
Authentication is fail-closed: constructing WijjitSSH without an
auth policy raises unless allow_anonymous=True is passed explicitly. See
wijjit_ssh.auth.
Resources are bounded by default: concurrent sessions, connections per IP,
idle time, and login time all have limits without being asked for. See
ServerConfig to tune them and wijjit_ssh.limits
for how they are enforced.
Not yet hardened
No backpressure handling: a client that stops reading buffers frames in asyncssh without bound (M5).
Blocking sync handlers stall that session’s frames; give each app an executor (
EXECUTOR) for CPU-bound work.
- class wijjit_ssh.server.SSHSession(username, term_type, columns, lines, backend, conn, session_id='', peer_ip='')[source]
Bases:
objectContext passed to the per-connection app factory.
- Variables:
username (str) – Username the client authenticated as.
term_type (str) – Client’s
TERM(e.g."xterm-256color").columns (int) – Negotiated terminal width.
lines (int) – Negotiated terminal height.
backend (RemoteTerminalBackend) – The transport for this connection. Pass it to the app:
Wijjit(backend=session.backend)- this is what routes the app’s I/O to the SSH channel instead of the server’s console.conn (asyncssh.SSHServerConnection) – The underlying connection, for advanced use.
session_id (str) – Short correlation id, matching the one in this session’s log lines. Worth surfacing in an app’s own logs or error messages: it is what ties a user’s report back to the server-side record.
peer_ip (str) – Client address, e.g. for per-user rate limiting inside the app.
- Parameters:
username (str)
term_type (str)
columns (int)
lines (int)
backend (RemoteTerminalBackend)
conn (SSHServerConnection)
session_id (str)
peer_ip (str)
- class wijjit_ssh.server.WijjitSSH(app_factory, config=None, **overrides)[source]
Bases:
objectServe a Wijjit app over SSH, one app instance per connection.
- Parameters:
app_factory (Callable[[SSHSession], Wijjit]) – Builds the app for each connection (the SSH analogue of a Flask view).
config (ServerConfig, optional) – Every knob the server takes; see
ServerConfig. Defaults are used when omitted.**overrides –
Any
ServerConfigfield, as a keyword. Applied on top ofconfig, so the common case needs no config object at all:WijjitSSH(make_app, host_keys=[key], auth=policy, max_sessions=10)
Unknown names raise
TypeErrorrather than being ignored - a typo’dmax_session=1that silently does nothing would leave a server the operator believes is bounded and which is not.
- Variables:
config (ServerConfig) – The resolved configuration, after overrides and validation.
- Raises:
ValueError – If the server would run unauthenticated and
allow_anonymousis not True - whether that is because noauthpolicy was given, or because the one given waives authentication (OpenAuth, or aChainAuthcontaining one). Serving an unauthenticated SSH server is a decision that has to be typed out, not one you inherit by forgetting an argument - so the default fails closed rather than silently accepting every client on the internet. Also raised for an out-of-range config value, or an unreadable host key.TypeError – If an override is not a config field.
Examples
>>> from wijjit_ssh import AuthorizedKeys, ensure_host_key, WijjitSSH >>> WijjitSSH( ... make_app, ... host_keys=[ensure_host_key("ssh_host_key")], ... auth=AuthorizedKeys("~/.ssh/authorized_keys"), ... ).run()
Or build the config up front, e.g. from a file or argparse:
>>> config = ServerConfig(port=2222, max_sessions=10) >>> WijjitSSH(make_app, config, host_keys=[key], auth=policy).run()
- async start(host=None, port=None)[source]
Bind the listener and start accepting connections.
Returns as soon as the server is listening, so callers can drive it (tests bind port 0 and read the assigned port off the acceptor). Use
run_async()to start and then serve forever.Does not configure logging or install signal handlers: this entry point may be one coroutine inside a larger application, which owns both. Use
run()when the server owns the process.- Parameters:
- Returns:
The listening server; call
close()on it to stop accepting.- Return type:
- Raises:
ValueError – If no host keys were configured. asyncssh would refuse every connection with an opaque handshake failure, so say it plainly here.
- async stop(*, grace=None)[source]
Stop accepting, drain live sessions, and close the listener.
Idempotent and safe to call concurrently: a second caller awaits the first rather than racing it. Safe to call on a server that never started.
The order is deliberate. Accepting stops first, so the drain is not chasing a moving target. Then sessions are asked to end and given
graceto do it, because a session that ends cleanly runs the app’s teardown and restores the client’s terminal, while one that is cancelled leaves a real person in the alternate screen buffer. Only then does the listener close.- Parameters:
grace (float, optional) – Seconds to allow for a clean exit, overriding
config.shutdown_grace.- Return type:
None
Examples
>>> server = WijjitSSH(make_app, host_keys=[key], auth=policy) >>> await server.start() >>> await server.stop()
- async run_async(host=None, port=None)[source]
Start the SSH server and serve until
stop()is called.Like
start(), this configures no logging and installs no signal handlers - it may be embedded in a host application that owns both. A host that wants signal handling should install its own and callstop(), or userun().
- run(host=None, port=None)[source]
Serve until interrupted, draining cleanly. Blocking; owns the process.
The entry point for “this process is the server”, as opposed to
run_async(), which may be one coroutine inside a larger application. That ownership is the whole distinction: this is the only method that configures logging or installs signal handlers, because a library coroutine has no business doing either to somebody else’s process. (It is the same reasoning that makes the backend setowns_terminal = False.)On SIGINT/SIGTERM the server stops accepting, gives live sessions
config.shutdown_graceto exit cleanly - which is what restores each client’s terminal - and then exits.- Parameters:
- Return type:
None
Notes
Signal handling on Windows is best-effort: SIGTERM is never delivered there (
TerminateProcessdoes not run handlers), so only Ctrl+C drains. The deployment targets in the README are systemd and Docker, both POSIX.