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.

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

run()

The server is the program

Blocks. Installs SIGINT/SIGTERM handlers and configures stderr logging.

run_async()

One coroutine in a larger asyncio app

Serves until stop() or cancellation. Touches neither signals nor logging.

start()

You want the listener and control of the rest

Returns as soon as it is bound, handing back the SSHAcceptor (bind port 0 and read the assigned port off it - this is what the tests do).

# 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