Quickstart
A Wijjit SSH server is three things: a factory that builds an app, a host key that gives the server an identity, and an auth policy that decides who gets in. Everything else has a default.
The factory
The factory is the SSH analogue of a Flask view: it runs once per connection and
returns the Wijjit app that client will drive.
from wijjit import Wijjit, render_template_string
from wijjit_ssh import SSHSession, WijjitSSH
def make_app(session: SSHSession) -> Wijjit:
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
The one line that matters is Wijjit(backend=session.backend). That is what
routes the app’s output to the SSH channel and its input to the client’s
keystrokes, rather than to the server process’s own console. Forget it and the
app will try to draw on the server’s stdout.
Everything the factory is told about the client arrives on
SSHSession:
def make_app(session: SSHSession) -> Wijjit:
session.username # who authenticated
session.term_type # their TERM, e.g. "xterm-256color"
session.columns # negotiated width...
session.lines # ...and height
session.peer_ip # where they connected from
session.session_id # correlation id, matching this session's log lines
session.backend # -> Wijjit(backend=...)
session.conn # the asyncssh connection, for advanced use
Each connection gets its own app object and its own state. Two users typing in the same field are not sharing anything - which also means anything you do want shared (a database handle, a cache) should be a module-level object the factory closes over, not something it rebuilds per connection.
Warning
The factory runs after authentication but before the app starts drawing, and
it runs on the event loop. Keep it fast and non-blocking: an await-less
database round trip here stalls every other session in the process. If the
app needs slow setup, do it in an async startup handler inside the app.
A host key
The server’s identity. Clients pin it on first connect and refuse to talk to you if it changes.
from wijjit_ssh import ensure_host_key
host_keys = [ensure_host_key("ssh_host_key")]
Generated on first run, reused forever after. No ssh-keygen detour,
and your known_hosts entry stays valid across restarts.
ssh-keygen -t ed25519 -f /var/lib/myapp/host_key -N ''
from wijjit_ssh import load_host_keys
host_keys = load_host_keys(["/var/lib/myapp/host_key"])
Managed out of band, and a hard failure if it is missing - rather than silently inventing a new identity that breaks every client.
See Host keys for rotation and the permissions rules.
An auth policy
Authentication is fail-closed: WijjitSSH raises
unless you pass an auth policy that actually authenticates, or explicitly
pass allow_anonymous=True.
from wijjit_ssh import AuthorizedKeys
auth = AuthorizedKeys("~/.ssh/authorized_keys")
Public keys are the recommended setup. Passwords, keyboard-interactive, and chained policies are all available - see Authentication.
Putting it together
WijjitSSH(
make_app,
host_keys=[ensure_host_key("ssh_host_key")],
auth=AuthorizedKeys("~/.ssh/authorized_keys"),
).run(port=8022)
ssh -p 8022 you@localhost
A complete, runnable version of this is examples/hello_ssh.py.
Three ways to run it
Which entry point you use depends on who owns the process.
Method |
Use when |
What it does for you |
|---|---|---|
The server is the program |
Blocks. Installs |
|
One coroutine in a larger asyncio app |
Serves until |
|
You want the listener and control of the rest |
Returns as soon as it is bound, handing back the
|
# Embedded: the host application owns signals and logging.
server = WijjitSSH(make_app, host_keys=host_keys, auth=auth)
await server.start()
...
await server.stop()
The rule is that run() owns the process, so it
is the only entry point that touches process-global state. See
Graceful shutdown.
Tuning it
Every knob is a ServerConfig field, and every field
can be passed to WijjitSSH as a keyword:
WijjitSSH(
make_app,
host_keys=host_keys,
auth=auth,
max_sessions=25,
idle_timeout=120.0,
banner="Authorized users only.\n",
).run()
Or build the config up front - from a file, the environment, or argparse - and pass it as an object:
config = ServerConfig(port=2222, max_sessions=25, idle_timeout=120.0)
WijjitSSH(make_app, config, host_keys=host_keys, auth=auth).run()
Unknown names raise TypeError rather than being ignored. That is
deliberate: a typo’d max_session=1 that silently does nothing leaves an
operator believing a server is bounded when it is not.
Next steps
Authentication - the policies, and writing your own
Resource limits - what is bounded, and what is not yet
Graceful shutdown - why draining matters more than it sounds
Logging and metrics - session-bound logs and the metrics hook