Monorepo for Tangled
tangled.org
1package ssh
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7
8 "github.com/charmbracelet/ssh"
9 "github.com/charmbracelet/wish"
10 tea "github.com/charmbracelet/wish/bubbletea"
11 "tangled.org/core/appview/config"
12 "tangled.org/core/appview/db"
13)
14
15type Server struct {
16 db *db.DB
17 config *config.Config
18 logger *slog.Logger
19}
20
21func New(db *db.DB, cfg *config.Config, logger *slog.Logger) *Server {
22 return &Server{db: db, config: cfg, logger: logger}
23}
24
25func (s *Server) ListenAndServe(ctx context.Context) error {
26 opts := []ssh.Option{
27 wish.WithAddress(s.config.SSH.ListenAddr),
28 wish.WithMiddleware(
29 tea.Middleware(s.teaHandler),
30 requirePty,
31 ),
32 }
33
34 if s.config.SSH.HostKeyPath != "" {
35 opts = append(opts, wish.WithHostKeyPath(s.config.SSH.HostKeyPath))
36 }
37
38 srv, err := wish.NewServer(opts...)
39 if err != nil {
40 return err
41 }
42
43 go func() {
44 <-ctx.Done()
45 s.logger.Info("shutting down SSH log server")
46 srv.Close()
47 }()
48
49 s.logger.Info("SSH log server listening", "address", s.config.SSH.ListenAddr)
50 if err := srv.ListenAndServe(); err != ssh.ErrServerClosed {
51 return err
52 }
53 s.logger.Info("SSH log server stopped")
54 return nil
55}
56
57// requirePty is a middleware that rejects connections without a PTY and tells the user to pass -t.
58func requirePty(next ssh.Handler) ssh.Handler {
59 return func(sess ssh.Session) {
60 _, _, ok := sess.Pty()
61 if !ok {
62 fmt.Fprintf(sess.Stderr(), "error: no terminal allocated\nhint: use `ssh -t` to force PTY allocation\n")
63 sess.Exit(1)
64 return
65 }
66 next(sess)
67 }
68}