wish

repository·main·Indexed 26 days ago

https://github.com/charmbracelet/wish

A Go library for building custom SSH servers. Wish provides a middleware-based framework to securely serve TUIs via Bubble Tea, Git servers, and other network services over SSH without requiring openssh-server. It includes built-in support for public key authentication, connection logging, access control, and PTY management.

Tokens
4.4K
Snippets
9
Records
28
Agent score
90%

What's inside wish

  1. Overview of Wish

    main
    Wish is a Go library for building custom SSH servers. It allows you to create remotely accessible applications that leverage SSH's secure communication, user identification via SSH keys, and terminal access. Unlike standard SSH setups, Wish does not use openssh-server and does not provide a default shell, making it safe for hosting specific application services.
  2. Deploy a Wish app with systemd

    main

    To run a Wish application as a background service using systemd, create a service unit file (e.g., /etc/systemd/system/myapp.service).

    1. Create the service user:
    useradd --system --user-group --create-home myapp
    1. Define the unit file:
    [Unit]
    Description=My App
    After=network.target
    
    [Service]
    Type=simple
    User=myapp
    Group=myapp
    WorkingDirectory=/home/myapp/
    ExecStart=/usr/bin/myapp
    Restart=on-failure
    
    [Install]
    WantedBy=multi-user.target
    1. Load and start the service:
    sudo systemctl daemon-reload
    sudo systemctl start myapp
  3. Upgrade from Wish v1 to v2

    main

    To upgrade a Wish application to v2, follow these primary steps:

    1. Update Import Paths: Switch from github.com/charmbracelet/* to the charm.land vanity domain with v2 suffixes.
    2. Adopt Bubble Tea v2: Transition from returning string in View() to returning a tea.View struct.
    3. Remove Color Profile Detection: Delete manual calls to bubbletea.MakeRenderer() as color profiles are now handled automatically via messages.
    4. Update Program Options: Move options like tea.WithAltScreen() from the teaHandler return values into the tea.View struct configuration.
  4. Configure local development for SSH

    main

    To avoid polluting your ~/.ssh/known_hosts file with localhost entries while developing Wish applications locally, add the following to your ~/.ssh/config:

    Host localhost
        UserKnownHostsFile /dev/null
  5. Access SSH client environment variables before Init() runs

    main

    If you require access to the client's environment variables before the Bubble Tea Init() method is called, you must manually extract them from the ssh.Session within your handler and pass them into your model's constructor.

    Iterate over s.Environ() from the ssh.Session to build a map of the client's environment variables.

    func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
        // Get client environment variables from the SSH session
        env := make(map[string]string)
        for _, e := range s.Environ() {
            parts := strings.SplitN(e, "=", 2)
            if len(parts) == 2 {
                env[parts[0]] = parts[1]
            }
        }
    
        m := model{
            env: env,  // Pass to model
        }
        return m, bubbletea.MakeOptions(s)
    }
    
    type model struct {
        env map[string]string
    }
    
    func (m model) View() tea.View {
        // Access client's environment
        term := m.env["TERM"]
        lang := m.env["LANG"]
        user := m.env["USER"]
    
        return tea.NewView(fmt.Sprintf("Your TERM: %s", term))
    }
  6. Access SSH client environment variables in Bubble Tea

    main

    When building SSH applications with Wish, you must access the SSH client's environment variables rather than using os.Getenv(), as os.Getenv() returns the server's environment.

    To ensure the client's environment is available to your Bubble Tea model, use bubbletea.MakeOptions(s) in your handler. This automatically passes the client's environment to the model via a tea.EnvMsg.

    Bubble Tea v2 sends a tea.EnvMsg containing the client's environment. You can intercept this in your model's Update method to access specific variables like TERM, LANG, or USER using the .Getenv() method.

    func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
        return model{}, bubbletea.MakeOptions(s) // Passes client environment
    }
    
    type model struct {
        envMsg tea.EnvMsg
    }
    
    func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        switch msg := msg.(type) {
        case tea.EnvMsg:
            m.envMsg = msg
    
            // Access specific CLIENT variables
            term := msg.Getenv("TERM")
            lang := msg.Getenv("LANG")
            user := msg.Getenv("USER")
    
            fmt.Printf("Client TERM: %s\n", term)
        }
        return m, nil
    }
  7. Handle Key, Mouse, and Paste Messages in v2

    main

    Bubble Tea v2 introduces more granular message types for input handling:

    • Key Messages: tea.KeyMsg is replaced by tea.KeyPressMsg and tea.KeyReleaseMsg. Note that key strings are now full names (e.g., "space" instead of " ").
    • Mouse Messages: Split into tea.MouseClickMsg, tea.MouseWheelMsg, and tea.MouseMotionMsg.
    • Paste Events: Use tea.PasteMsg to handle clipboard paste events.
    • Clipboard: Use tea.SetClipboard(string) to write to the clipboard and tea.ReadClipboard() to read from it.
    // Key Press Example
    case tea.KeyPressMsg:
        switch msg.String() {
        case "space":
            // handle space
        case "ctrl+c":
            return m, tea.SetClipboard("Copied text")
        }
    
    // Paste Example
    case tea.PasteMsg:
        m.text += msg.Content
    
    // Mouse Click Example
    case tea.MouseClickMsg:
        if msg.Button == tea.MouseLeft {
            // handle click
        }
  8. Use Git middleware in Wish

    main

    The git middleware adds Git server functionality to your SSH server. It supports repository creation upon the initial push and allows for custom public key-based authentication.

    Requirement: The git binary must be installed on the host server.

  9. Use Bubble Tea with Wish

    main
    The bubbletea middleware allows you to serve Bubble Tea applications over SSH. It automatically creates a unique tea.Program for each SSH session, connecting the SSH pty input and output. It also natively handles client window dimensions and resize messages.
  10. Handle Background Color and Color Profile via Messages

    main

    Instead of querying the terminal renderer during initialization, listen for specific messages in your Update loop to react to terminal changes.

    • Background Color: Listen for tea.BackgroundColorMsg. Use msg.IsDark() to determine the theme.
    • Color Profile: Listen for tea.ColorProfileMsg to get the profile string (e.g., "TrueColor", "ANSI256", "ANSI").

    To trigger the initial check, return tea.RequestBackgroundColor from your Init() method.

    func (m model) Init() tea.Cmd {
        return tea.RequestBackgroundColor
    }
    
    func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        switch msg := msg.(type) {
        case tea.BackgroundColorMsg:
            if msg.IsDark() {
                m.bg = "dark"
            } else {
                m.bg = "light"
            }
        case tea.ColorProfileMsg:
            m.profile = msg.String()
        }
        return m, nil
    }
  11. Use Logging and Access Control middlewares

    main

    Wish provides several middlewares for managing connections:

    • logging: Provides basic connection logging, including remote address, invoked command, TERM setting, window dimensions, and whether authentication was public key-based. It also logs disconnects with the connection duration.
    • activeterm: Restricts access to only allow connections that have an active terminal connected.
    • accesscontrol: Allows you to specify which commands are permitted.
  12. Middleware API Changes in Wish v2

    main

    The middleware API has been simplified and updated:

    • Removed Functions: MakeRenderer(), MiddlewareWithColorProfile(), and QueryTerminalFilter are no longer available.
    • Signature Update: All middleware functions now return the charm.land/wish/v2.Middleware type.
    • Simplified Program Handler: MiddlewareWithProgramHandler no longer requires a termenv.Profile argument; it now only takes the ProgramHandler.

    Logging Middleware: When using logging.StructuredMiddlewareWithLogger, ensure you are importing charm.land/log/v2 for the log.Logger type.

    // Before
    func MiddlewareWithProgramHandler(
        handler ProgramHandler,
        profile termenv.Profile,
    ) wish.Middleware
    
    // After
    func MiddlewareWithProgramHandler(handler ProgramHandler) wish.Middleware