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