repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

commit
72d930b
parent
2c4edfe
author
Antonio Mika
date
2025-03-11 16:45:10 -0400 EDT
More updates
17 files changed,  +353, -374
M go.mod
M go.sum
M feeds/cli.go
+17, -18
 1@@ -5,7 +5,6 @@ import (
 2 	"text/tabwriter"
 3 	"time"
 4 
 5-	"github.com/charmbracelet/wish"
 6 	"github.com/picosh/pico/db"
 7 	"github.com/picosh/pico/pssh"
 8 	"github.com/picosh/pico/shared"
 9@@ -80,11 +79,10 @@ func WishMiddleware(dbpool db.DB, cfg *shared.ConfigSite) pssh.SSHServerMiddlewa
10 						post.Data.Attempts,
11 					)
12 				}
13-				writer.Flush()
14-				return
15+				return writer.Flush()
16 			} else if cmd == "rm" {
17 				filename := args[1]
18-				wish.Printf(sesh, "removing digest post %s\n", filename)
19+				fmt.Fprintf(sesh, "removing digest post %s\n", filename)
20 				write := false
21 				if len(args) > 2 {
22 					writeRaw := args[2]
23@@ -95,41 +93,42 @@ func WishMiddleware(dbpool db.DB, cfg *shared.ConfigSite) pssh.SSHServerMiddlewa
24 
25 				post, err := dbpool.FindPostWithFilename(filename, user.ID, "feeds")
26 				if err != nil {
27-					wish.Errorln(sesh, err)
28-					return
29+					fmt.Fprintln(sesh.Stderr(), err)
30+					return err
31 				}
32 				if write {
33 					err = dbpool.RemovePosts([]string{post.ID})
34 					if err != nil {
35-						wish.Errorln(sesh, err)
36+						fmt.Fprintln(sesh.Stderr(), err)
37 					}
38 				}
39-				wish.Printf(sesh, "digest post removed %s\n", filename)
40+				fmt.Fprintf(sesh, "digest post removed %s\n", filename)
41 				if !write {
42-					wish.Println(sesh, "WARNING: *must* append with `--write` for the changes to persist.")
43+					fmt.Fprintln(sesh, "WARNING: *must* append with `--write` for the changes to persist.")
44 				}
45-				return
46+				return err
47 			} else if cmd == "run" {
48 				if len(args) < 2 {
49-					wish.Errorln(sesh, "must provide filename of post to run")
50-					return
51+					err := fmt.Errorf("must provide filename of post to run")
52+					fmt.Fprintln(sesh.Stderr(), err)
53+					return err
54 				}
55 				filename := args[1]
56 				post, err := dbpool.FindPostWithFilename(filename, user.ID, "feeds")
57 				if err != nil {
58-					wish.Errorln(sesh, err)
59-					return
60+					fmt.Fprintln(sesh.Stderr(), err)
61+					return err
62 				}
63-				wish.Printf(sesh, "running feed post: %s\n", filename)
64+				fmt.Fprintf(sesh, "running feed post: %s\n", filename)
65 				fetcher := NewFetcher(dbpool, cfg)
66 				err = fetcher.RunPost(logger, user, post, true)
67 				if err != nil {
68-					wish.Errorln(sesh, err)
69+					fmt.Fprintln(sesh.Stderr(), err)
70 				}
71-				return
72+				return err
73 			}
74 
75-			next(sesh)
76+			return next(sesh)
77 		}
78 	}
79 }
M feeds/scp_hooks.go
+3, -3
 1@@ -8,9 +8,9 @@ import (
 2 	"strings"
 3 	"time"
 4 
 5-	"github.com/charmbracelet/ssh"
 6 	"github.com/picosh/pico/db"
 7 	"github.com/picosh/pico/filehandlers"
 8+	"github.com/picosh/pico/pssh"
 9 	"github.com/picosh/pico/shared"
10 	"github.com/picosh/utils"
11 )
12@@ -20,7 +20,7 @@ type FeedHooks struct {
13 	Db  db.DB
14 }
15 
16-func (p *FeedHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData) (bool, error) {
17+func (p *FeedHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) (bool, error) {
18 	if !utils.IsTextFile(string(data.Text)) {
19 		err := fmt.Errorf(
20 			"WARNING: (%s) invalid file must be plain text (utf-8), skipping",
21@@ -73,7 +73,7 @@ func (p *FeedHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData)
22 	return true, nil
23 }
24 
25-func (p *FeedHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
26+func (p *FeedHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
27 	if data.Data.LastDigest == nil {
28 		now := time.Now()
29 		// let it run on the next loop
M feeds/ssh.go
+48, -63
  1@@ -2,71 +2,34 @@ package feeds
  2 
  3 import (
  4 	"context"
  5-	"fmt"
  6 	"os"
  7 	"os/signal"
  8 	"syscall"
  9-	"time"
 10 
 11-	"github.com/charmbracelet/promwish"
 12-	"github.com/charmbracelet/ssh"
 13-	"github.com/charmbracelet/wish"
 14 	"github.com/picosh/pico/db/postgres"
 15 	"github.com/picosh/pico/filehandlers"
 16+	"github.com/picosh/pico/pssh"
 17 	"github.com/picosh/pico/shared"
 18-	wsh "github.com/picosh/pico/wish"
 19 	"github.com/picosh/send/auth"
 20 	"github.com/picosh/send/list"
 21 	"github.com/picosh/send/pipe"
 22-	wishrsync "github.com/picosh/send/protocols/rsync"
 23+	"github.com/picosh/send/protocols/rsync"
 24 	"github.com/picosh/send/protocols/scp"
 25 	"github.com/picosh/send/protocols/sftp"
 26-	"github.com/picosh/send/proxy"
 27 	"github.com/picosh/utils"
 28+	"golang.org/x/crypto/ssh"
 29 )
 30 
 31-func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
 32-	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
 33-		return []wish.Middleware{
 34-			pipe.Middleware(handler, ".txt"),
 35-			list.Middleware(handler),
 36-			scp.Middleware(handler),
 37-			wishrsync.Middleware(handler),
 38-			auth.Middleware(handler),
 39-			wsh.PtyMdw(wsh.DeprecatedNotice()),
 40-			WishMiddleware(handler.DBPool, handler.Cfg),
 41-			wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool),
 42-		}
 43-	}
 44-}
 45-
 46-func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
 47-	return func(server *ssh.Server) error {
 48-		err := sftp.SSHOption(handler)(server)
 49-		if err != nil {
 50-			return err
 51-		}
 52-
 53-		newSubsystemHandlers := map[string]ssh.SubsystemHandler{}
 54-
 55-		for name, subsystemHandler := range server.SubsystemHandlers {
 56-			newSubsystemHandlers[name] = func(s ssh.Session) {
 57-				wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool)(ssh.Handler(subsystemHandler))(s)
 58-			}
 59-		}
 60-
 61-		server.SubsystemHandlers = newSubsystemHandlers
 62-
 63-		return proxy.WithProxy(createRouter(handler), otherMiddleware...)(server)
 64-	}
 65-}
 66-
 67 func StartSshServer() {
 68 	host := utils.GetEnv("LISTS_HOST", "0.0.0.0")
 69 	port := utils.GetEnv("LISTS_SSH_PORT", "2222")
 70-	promPort := utils.GetEnv("LISTS_PROM_PORT", "9222")
 71+	// promPort := utils.GetEnv("LISTS_PROM_PORT", "9222")
 72 	cfg := NewConfigSite("feeds-ssh")
 73 	logger := cfg.Logger
 74+
 75+	ctx, cancel := context.WithCancel(context.Background())
 76+	defer cancel()
 77+
 78 	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
 79 	defer dbh.Close()
 80 
 81@@ -81,34 +44,56 @@ func StartSshServer() {
 82 	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
 83 
 84 	sshAuth := shared.NewSshAuthHandler(dbh, logger)
 85-	s, err := wish.NewServer(
 86-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
 87-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
 88-		wish.WithPublicKeyAuth(sshAuth.PubkeyAuthHandler),
 89-		withProxy(
 90-			handler,
 91-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "feeds-ssh"),
 92-		),
 93-	)
 94+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{
 95+		ListenAddr: "localhost:2222",
 96+		ServerConfig: &ssh.ServerConfig{
 97+			PublicKeyCallback: sshAuth.PubkeyAuthHandler,
 98+		},
 99+		Middleware: []pssh.SSHServerMiddleware{
100+			pipe.Middleware(handler, ".txt"),
101+			list.Middleware(handler),
102+			scp.Middleware(handler),
103+			rsync.Middleware(handler),
104+			auth.Middleware(handler),
105+			pssh.PtyMdw(pssh.DeprecatedNotice()),
106+			pssh.LogMiddleware(handler, dbh),
107+		},
108+		SubsystemMiddleware: []pssh.SSHServerMiddleware{
109+			sftp.Middleware(handler),
110+			pssh.LogMiddleware(handler, dbh),
111+		},
112+	})
113+
114+	pemBytes, err := os.ReadFile("ssh_data/term_info_ed25519")
115+	if err != nil {
116+		logger.Error("failed to read private key file", "error", err)
117+		return
118+	}
119+
120+	signer, err := ssh.ParsePrivateKey(pemBytes)
121 	if err != nil {
122-		logger.Error(err.Error())
123+		logger.Error("failed to parse private key", "error", err)
124 		return
125 	}
126 
127+	server.Config.AddHostKey(signer)
128+
129 	done := make(chan os.Signal, 1)
130+
131 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
132 	logger.Info("Starting SSH server", "host", host, "port", port)
133 	go func() {
134-		if err = s.ListenAndServe(); err != nil {
135-			logger.Error(err.Error())
136+		if err = server.ListenAndServe(); err != nil {
137+			logger.Error("serve", "err", err.Error())
138+			os.Exit(1)
139 		}
140 	}()
141 
142-	<-done
143-	logger.Info("Stopping SSH server")
144-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
145-	defer func() { cancel() }()
146-	if err := s.Shutdown(ctx); err != nil {
147-		logger.Error(err.Error())
148+	exit := func() {
149+		logger.Info("stopping ssh server")
150+		cancel()
151 	}
152+
153+	<-done
154+	exit()
155 }
M go.mod
+0, -1
1@@ -29,7 +29,6 @@ require (
2 	github.com/antoniomika/syncmap v1.0.0
3 	github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
4 	github.com/charmbracelet/lipgloss v1.0.0
5-	github.com/charmbracelet/promwish v0.7.0
6 	github.com/charmbracelet/ssh v0.0.0-20250213143314-8712ec3ff3ef
7 	github.com/charmbracelet/wish v1.4.6
8 	github.com/containerd/console v1.0.4
M go.sum
+0, -2
1@@ -160,8 +160,6 @@ github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O
2 github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
3 github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
4 github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
5-github.com/charmbracelet/promwish v0.7.0 h1:oaMH+ey6W4DDIv1xucS8jL1ik/Q46qxjNXlh6XxEm+s=
6-github.com/charmbracelet/promwish v0.7.0/go.mod h1:WbRJN9irg8LmsBU8G2rFF8md9O3rSg63qrnqquP/+cs=
7 github.com/charmbracelet/ssh v0.0.0-20250213143314-8712ec3ff3ef h1:dNZwn4is5svUd+sQEGsrXtp7VwD2ipYaCkKMzcpAEIE=
8 github.com/charmbracelet/ssh v0.0.0-20250213143314-8712ec3ff3ef/go.mod h1:hg+I6gvlMl16nS9ZzQNgBIrrCasGwEw0QiLsDcP01Ko=
9 github.com/charmbracelet/wish v1.4.6 h1:27WRqMTUmyFoZASoaAaEe78Je7LTU4VqyoBxnl4d9XA=
M pastes/scp_hooks.go
+3, -3
 1@@ -7,9 +7,9 @@ import (
 2 	"time"
 3 
 4 	"github.com/araddon/dateparse"
 5-	"github.com/charmbracelet/ssh"
 6 	"github.com/picosh/pico/db"
 7 	"github.com/picosh/pico/filehandlers"
 8+	"github.com/picosh/pico/pssh"
 9 	"github.com/picosh/pico/shared"
10 	"github.com/picosh/utils"
11 )
12@@ -21,7 +21,7 @@ type FileHooks struct {
13 	Db  db.DB
14 }
15 
16-func (p *FileHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData) (bool, error) {
17+func (p *FileHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) (bool, error) {
18 	if !utils.IsTextFile(string(data.Text)) {
19 		err := fmt.Errorf(
20 			"ERROR: (%s) invalid file must be plain text (utf-8), skipping",
21@@ -42,7 +42,7 @@ func (p *FileHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData)
22 	return true, nil
23 }
24 
25-func (p *FileHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
26+func (p *FileHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
27 	data.Title = utils.ToUpper(data.Slug)
28 	// we want the slug to be the filename for pastes
29 	data.Slug = data.Filename
M pastes/ssh.go
+48, -57
  1@@ -2,64 +2,34 @@ package pastes
  2 
  3 import (
  4 	"context"
  5-	"fmt"
  6 	"os"
  7 	"os/signal"
  8 	"syscall"
  9-	"time"
 10 
 11-	"github.com/charmbracelet/promwish"
 12-	"github.com/charmbracelet/ssh"
 13-	"github.com/charmbracelet/wish"
 14 	"github.com/picosh/pico/db/postgres"
 15 	"github.com/picosh/pico/filehandlers"
 16+	"github.com/picosh/pico/pssh"
 17 	"github.com/picosh/pico/shared"
 18-	wsh "github.com/picosh/pico/wish"
 19 	"github.com/picosh/send/auth"
 20 	"github.com/picosh/send/list"
 21 	"github.com/picosh/send/pipe"
 22-	wishrsync "github.com/picosh/send/protocols/rsync"
 23+	"github.com/picosh/send/protocols/rsync"
 24 	"github.com/picosh/send/protocols/scp"
 25-	"github.com/picosh/send/proxy"
 26+	"github.com/picosh/send/protocols/sftp"
 27 	"github.com/picosh/utils"
 28+	"golang.org/x/crypto/ssh"
 29 )
 30 
 31-func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
 32-	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
 33-		return []wish.Middleware{
 34-			pipe.Middleware(handler, ""),
 35-			list.Middleware(handler),
 36-			scp.Middleware(handler),
 37-			wishrsync.Middleware(handler),
 38-			auth.Middleware(handler),
 39-			wsh.PtyMdw(wsh.DeprecatedNotice()),
 40-			wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool),
 41-		}
 42-	}
 43-}
 44-
 45-func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
 46-	return func(server *ssh.Server) error {
 47-		newSubsystemHandlers := map[string]ssh.SubsystemHandler{}
 48-
 49-		for name, subsystemHandlers := range server.SubsystemHandlers {
 50-			newSubsystemHandlers[name] = func(s ssh.Session) {
 51-				wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool)(ssh.Handler(subsystemHandlers))
 52-			}
 53-		}
 54-
 55-		server.SubsystemHandlers = newSubsystemHandlers
 56-
 57-		return proxy.WithProxy(createRouter(handler), otherMiddleware...)(server)
 58-	}
 59-}
 60-
 61 func StartSshServer() {
 62 	host := utils.GetEnv("PASTES_HOST", "0.0.0.0")
 63 	port := utils.GetEnv("PASTES_SSH_PORT", "2222")
 64-	promPort := utils.GetEnv("PASTES_PROM_PORT", "9222")
 65+	// promPort := utils.GetEnv("PASTES_PROM_PORT", "9222")
 66 	cfg := NewConfigSite("pastes-ssh")
 67 	logger := cfg.Logger
 68+
 69+	ctx, cancel := context.WithCancel(context.Background())
 70+	defer cancel()
 71+
 72 	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
 73 	defer dbh.Close()
 74 	hooks := &FileHooks{
 75@@ -72,34 +42,55 @@ func StartSshServer() {
 76 	}
 77 	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
 78 	sshAuth := shared.NewSshAuthHandler(dbh, logger)
 79-	s, err := wish.NewServer(
 80-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
 81-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
 82-		wish.WithPublicKeyAuth(sshAuth.PubkeyAuthHandler),
 83-		withProxy(
 84-			handler,
 85-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pastes-ssh"),
 86-		),
 87-	)
 88+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{
 89+		ListenAddr: "localhost:2222",
 90+		ServerConfig: &ssh.ServerConfig{
 91+			PublicKeyCallback: sshAuth.PubkeyAuthHandler,
 92+		},
 93+		Middleware: []pssh.SSHServerMiddleware{
 94+			pipe.Middleware(handler, ""),
 95+			list.Middleware(handler),
 96+			scp.Middleware(handler),
 97+			rsync.Middleware(handler),
 98+			auth.Middleware(handler),
 99+			pssh.PtyMdw(pssh.DeprecatedNotice()),
100+			pssh.LogMiddleware(handler, dbh),
101+		},
102+		SubsystemMiddleware: []pssh.SSHServerMiddleware{
103+			sftp.Middleware(handler),
104+			pssh.LogMiddleware(handler, dbh),
105+		},
106+	})
107+
108+	pemBytes, err := os.ReadFile("ssh_data/term_info_ed25519")
109 	if err != nil {
110-		logger.Error(err.Error())
111+		logger.Error("failed to read private key file", "error", err)
112 		return
113 	}
114 
115+	signer, err := ssh.ParsePrivateKey(pemBytes)
116+	if err != nil {
117+		logger.Error("failed to parse private key", "error", err)
118+		return
119+	}
120+
121+	server.Config.AddHostKey(signer)
122+
123 	done := make(chan os.Signal, 1)
124 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
125 	logger.Info("Starting SSH server", "host", host, "port", port)
126 	go func() {
127-		if err = s.ListenAndServe(); err != nil {
128-			logger.Error(err.Error())
129+		if err = server.ListenAndServe(); err != nil {
130+			logger.Error("serve", "err", err.Error())
131+			os.Exit(1)
132 		}
133 	}()
134 
135-	<-done
136-	logger.Info("Stopping SSH server")
137-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
138-	defer func() { cancel() }()
139-	if err := s.Shutdown(ctx); err != nil {
140-		logger.Error(err.Error())
141+	exit := func() {
142+		logger.Info("stopping ssh server")
143+		cancel()
144 	}
145+
146+	<-done
147+	exit()
148 }
M pgs/ssh.go
+0, -1
1@@ -48,7 +48,6 @@ func StartSshServer(cfg *PgsConfig, killCh chan error) {
2 			PublicKeyCallback: sshAuth.PubkeyAuthHandler,
3 		},
4 		Middleware: []pssh.SSHServerMiddleware{
5-			sftp.Middleware(handler),
6 			pipe.Middleware(handler, ""),
7 			list.Middleware(handler),
8 			scp.Middleware(handler),
M pico/cli.go
+31, -31
  1@@ -8,16 +8,15 @@ import (
  2 	"log/slog"
  3 	"strings"
  4 
  5-	"github.com/charmbracelet/ssh"
  6-	"github.com/charmbracelet/wish"
  7 	"github.com/picosh/pico/db"
  8+	"github.com/picosh/pico/pssh"
  9 	"github.com/picosh/pico/shared"
 10 	"github.com/picosh/utils"
 11 
 12 	pipeLogger "github.com/picosh/utils/pipe/log"
 13 )
 14 
 15-func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
 16+func getUser(s *pssh.SSHServerConnSession, dbpool db.DB) (*db.User, error) {
 17 	if s.PublicKey() == nil {
 18 		return nil, fmt.Errorf("key not found")
 19 	}
 20@@ -38,7 +37,7 @@ func getUser(s ssh.Session, dbpool db.DB) (*db.User, error) {
 21 
 22 type Cmd struct {
 23 	User       *db.User
 24-	SshSession ssh.Session
 25+	SshSession *pssh.SSHServerConnSession
 26 	Session    utils.CmdSession
 27 	Log        *slog.Logger
 28 	Dbpool     db.DB
 29@@ -77,7 +76,7 @@ func (c *Cmd) logs(ctx context.Context) error {
 30 		user := utils.AnyToStr(parsedData, "user")
 31 		userId := utils.AnyToStr(parsedData, "userId")
 32 		if user == c.User.Name || userId == c.User.ID {
 33-			wish.Println(c.SshSession, line)
 34+			fmt.Fprintln(c.SshSession, line)
 35 		}
 36 	}
 37 	return scanner.Err()
 38@@ -88,34 +87,34 @@ type CliHandler struct {
 39 	Logger *slog.Logger
 40 }
 41 
 42-func WishMiddleware(handler *CliHandler) wish.Middleware {
 43+func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
 44 	dbpool := handler.DBPool
 45 	log := handler.Logger
 46 
 47-	return func(next ssh.Handler) ssh.Handler {
 48-		return func(sesh ssh.Session) {
 49+	return func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
 50+		return func(sesh *pssh.SSHServerConnSession) error {
 51 			args := sesh.Command()
 52 			if len(args) == 0 {
 53-				next(sesh)
 54-				return
 55+				return next(sesh)
 56 			}
 57 
 58 			user, err := getUser(sesh, dbpool)
 59 			if err != nil {
 60-				wish.Errorf(sesh, "detected ssh command: %s\n", args)
 61+				fmt.Fprintf(sesh.Stderr(), "detected ssh command: %s\n", args)
 62 				s := fmt.Errorf("error: you need to create an account before using the remote cli: %w", err)
 63-				wish.Fatalln(sesh, s)
 64-				return
 65+				sesh.Fatal(s)
 66+				return s
 67 			}
 68 
 69 			if len(args) > 0 && args[0] == "chat" {
 70 				_, _, hasPty := sesh.Pty()
 71 				if !hasPty {
 72-					wish.Fatalln(
 73-						sesh,
 74-						"In order to render chat you need to enable PTY with the `ssh -t` flag",
 75+					err := fmt.Errorf(
 76+						"in order to render chat you need to enable PTY with the `ssh -t` flag",
 77 					)
 78-					return
 79+
 80+					sesh.Fatal(err)
 81+					return err
 82 				}
 83 
 84 				ff, err := dbpool.FindFeatureForUser(user.ID, "plus")
 85@@ -124,29 +123,30 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
 86 					ff, err = dbpool.FindFeatureForUser(user.ID, "bouncer")
 87 					if err != nil {
 88 						handler.Logger.Error("Unable to find bouncer feature flag", "err", err, "user", user, "command", args)
 89-						wish.Fatalln(sesh, "Unable to find plus or bouncer feature flag")
 90-						return
 91+						sesh.Fatal(err)
 92+						return err
 93 					}
 94 				}
 95 
 96 				if ff == nil {
 97-					wish.Fatalln(sesh, "Unable to find plus or bouncer feature flag")
 98-					return
 99+					err = fmt.Errorf("unable to find plus or bouncer feature flag")
100+					sesh.Fatal(err)
101+					return err
102 				}
103 
104 				pass, err := dbpool.UpsertToken(user.ID, "pico-chat")
105 				if err != nil {
106-					wish.Fatalln(sesh, err)
107-					return
108+					sesh.Fatal(err)
109+					return err
110 				}
111 				app, err := shared.NewSenpaiApp(sesh, user.Name, pass)
112 				if err != nil {
113-					wish.Fatalln(sesh, err)
114-					return
115+					sesh.Fatal(err)
116+					return err
117 				}
118 				app.Run()
119 				app.Close()
120-				return
121+				return err
122 			}
123 
124 			opts := Cmd{
125@@ -162,20 +162,20 @@ func WishMiddleware(handler *CliHandler) wish.Middleware {
126 			if len(args) == 1 {
127 				if cmd == "help" {
128 					opts.help()
129-					return
130+					return nil
131 				} else if cmd == "logs" {
132 					err = opts.logs(sesh.Context())
133 					if err != nil {
134-						wish.Fatalln(sesh, err)
135+						sesh.Fatal(err)
136 					}
137-					return
138+					return nil
139 				} else {
140 					next(sesh)
141-					return
142+					return nil
143 				}
144 			}
145 
146-			next(sesh)
147+			return next(sesh)
148 		}
149 	}
150 }
M pico/ssh.go
+62, -68
  1@@ -2,73 +2,37 @@ package pico
  2 
  3 import (
  4 	"context"
  5-	"fmt"
  6 	"os"
  7 	"os/signal"
  8 	"syscall"
  9-	"time"
 10 
 11 	"git.sr.ht/~rockorager/vaxis"
 12-	"github.com/charmbracelet/promwish"
 13-	"github.com/charmbracelet/ssh"
 14-	"github.com/charmbracelet/wish"
 15 	"github.com/picosh/pico/db/postgres"
 16+	"github.com/picosh/pico/pssh"
 17 	"github.com/picosh/pico/shared"
 18 	"github.com/picosh/pico/tui"
 19-	wsh "github.com/picosh/pico/wish"
 20 	"github.com/picosh/send/auth"
 21 	"github.com/picosh/send/list"
 22 	"github.com/picosh/send/pipe"
 23-	wishrsync "github.com/picosh/send/protocols/rsync"
 24+	"github.com/picosh/send/protocols/rsync"
 25 	"github.com/picosh/send/protocols/scp"
 26 	"github.com/picosh/send/protocols/sftp"
 27-	"github.com/picosh/send/proxy"
 28 	"github.com/picosh/utils"
 29+	"golang.org/x/crypto/ssh"
 30 )
 31 
 32-func createRouterVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHandler) proxy.Router {
 33-	return func(sh ssh.Handler, sesh ssh.Session) []wish.Middleware {
 34-		shrd := &tui.SharedModel{
 35-			Session: sesh,
 36-			Cfg:     cfg,
 37-			Dbpool:  handler.DBPool,
 38-			Logger:  cfg.Logger,
 39-		}
 40-		return []wish.Middleware{
 41-			pipe.Middleware(handler, ""),
 42-			list.Middleware(handler),
 43-			scp.Middleware(handler),
 44-			wishrsync.Middleware(handler),
 45-			auth.Middleware(handler),
 46-			wsh.PtyMdw(createTui(shrd)),
 47-			WishMiddleware(cliHandler),
 48-			wsh.LogMiddleware(handler.GetLogger(sesh), handler.DBPool),
 49-		}
 50-	}
 51-}
 52-
 53-func withProxyVaxis(cfg *shared.ConfigSite, handler *UploadHandler, cliHandler *CliHandler, otherMiddleware ...wish.Middleware) ssh.Option {
 54-	return func(server *ssh.Server) error {
 55-		err := sftp.SSHOption(handler)(server)
 56-		if err != nil {
 57-			return err
 58-		}
 59-
 60-		return proxy.WithProxy(createRouterVaxis(cfg, handler, cliHandler), otherMiddleware...)(server)
 61-	}
 62-}
 63-
 64-func createTui(shrd *tui.SharedModel) wish.Middleware {
 65-	return func(next ssh.Handler) ssh.Handler {
 66-		return func(sesh ssh.Session) {
 67+func createTui(shrd *tui.SharedModel) pssh.SSHServerMiddleware {
 68+	return func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
 69+		return func(sesh *pssh.SSHServerConnSession) error {
 70 			vty, err := shared.NewVConsole(sesh)
 71 			if err != nil {
 72-				panic(err)
 73+				return err
 74 			}
 75 			opts := vaxis.Options{
 76 				WithConsole: vty,
 77 			}
 78 			tui.NewTui(opts, shrd)
 79+			return nil
 80 		}
 81 	}
 82 }
 83@@ -76,9 +40,13 @@ func createTui(shrd *tui.SharedModel) wish.Middleware {
 84 func StartSshServer() {
 85 	host := utils.GetEnv("PICO_HOST", "0.0.0.0")
 86 	port := utils.GetEnv("PICO_SSH_PORT", "2222")
 87-	promPort := utils.GetEnv("PICO_PROM_PORT", "9222")
 88+	// promPort := utils.GetEnv("PICO_PROM_PORT", "9222")
 89 	cfg := NewConfigSite("pico-ssh")
 90 	logger := cfg.Logger
 91+
 92+	ctx, cancel := context.WithCancel(context.Background())
 93+	defer cancel()
 94+
 95 	dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
 96 	defer dbpool.Close()
 97 
 98@@ -86,47 +54,73 @@ func StartSshServer() {
 99 		dbpool,
100 		cfg,
101 	)
102+
103 	cliHandler := &CliHandler{
104 		Logger: logger,
105 		DBPool: dbpool,
106 	}
107 
108 	sshAuth := shared.NewSshAuthHandler(dbpool, logger)
109-	s, err := wish.NewServer(
110-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
111-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
112-		wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
113-			sshAuth.PubkeyAuthHandler(ctx, key)
114-			return true
115-		}),
116-		withProxyVaxis(
117-			cfg,
118-			handler,
119-			cliHandler,
120-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pico-ssh"),
121-		),
122-	)
123+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{
124+		ListenAddr: "localhost:2222",
125+		ServerConfig: &ssh.ServerConfig{
126+			PublicKeyCallback: sshAuth.PubkeyAuthHandler,
127+		},
128+		Middleware: []pssh.SSHServerMiddleware{
129+			pipe.Middleware(handler, ""),
130+			list.Middleware(handler),
131+			scp.Middleware(handler),
132+			rsync.Middleware(handler),
133+			auth.Middleware(handler),
134+			func(next pssh.SSHServerHandler) pssh.SSHServerHandler {
135+				return func(sesh *pssh.SSHServerConnSession) error {
136+					shrd := &tui.SharedModel{
137+						Session: sesh,
138+						Cfg:     cfg,
139+						Dbpool:  handler.DBPool,
140+						Logger:  cfg.Logger,
141+					}
142+					return pssh.PtyMdw(createTui(shrd))(next)(sesh)
143+				}
144+			},
145+			WishMiddleware(cliHandler),
146+			pssh.LogMiddleware(handler, dbpool),
147+		},
148+		SubsystemMiddleware: []pssh.SSHServerMiddleware{
149+			sftp.Middleware(handler),
150+			pssh.LogMiddleware(handler, dbpool),
151+		},
152+	})
153+
154+	pemBytes, err := os.ReadFile("ssh_data/term_info_ed25519")
155 	if err != nil {
156-		logger.Error(err.Error())
157+		logger.Error("failed to read private key file", "error", err)
158 		return
159 	}
160 
161+	signer, err := ssh.ParsePrivateKey(pemBytes)
162+	if err != nil {
163+		logger.Error("failed to parse private key", "error", err)
164+		return
165+	}
166+
167+	server.Config.AddHostKey(signer)
168+
169 	done := make(chan os.Signal, 1)
170 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
171 	logger.Info("starting SSH server on", "host", host, "port", port)
172 	go func() {
173-		if err = s.ListenAndServe(); err != nil {
174+		if err = server.ListenAndServe(); err != nil {
175 			logger.Error("serve", "err", err.Error())
176 			os.Exit(1)
177 		}
178 	}()
179 
180-	<-done
181-	logger.Info("stopping SSH server")
182-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
183-	defer func() { cancel() }()
184-	if err := s.Shutdown(ctx); err != nil {
185-		logger.Error("shutdown", "err", err.Error())
186-		os.Exit(1)
187+	exit := func() {
188+		logger.Info("stopping ssh server")
189+		cancel()
190 	}
191+
192+	<-done
193+	exit()
194 }
M pipe/cli.go
+20, -23
  1@@ -13,8 +13,6 @@ import (
  2 	"time"
  3 
  4 	"github.com/antoniomika/syncmap"
  5-	"github.com/charmbracelet/ssh"
  6-	"github.com/charmbracelet/wish"
  7 	"github.com/google/uuid"
  8 	"github.com/picosh/pico/db"
  9 	"github.com/picosh/pico/pssh"
 10@@ -23,7 +21,7 @@ import (
 11 	gossh "golang.org/x/crypto/ssh"
 12 )
 13 
 14-func flagSet(cmdName string, sesh ssh.Session) *flag.FlagSet {
 15+func flagSet(cmdName string, sesh *pssh.SSHServerConnSession) *flag.FlagSet {
 16 	cmd := flag.NewFlagSet(cmdName, flag.ContinueOnError)
 17 	cmd.SetOutput(sesh)
 18 	cmd.Usage = func() {
 19@@ -102,6 +100,9 @@ type CliHandler struct {
 20 	Access  *syncmap.Map[string, []string]
 21 }
 22 
 23+func (h *CliHandler) GetLogger(s *pssh.SSHServerConnSession) *slog.Logger {
 24+}
 25+
 26 func toSshCmd(cfg *shared.ConfigSite) string {
 27 	port := ""
 28 	if cfg.PortOverride != "22" {
 29@@ -120,7 +121,7 @@ func parseArgList(arg string) []string {
 30 }
 31 
 32 // checkAccess checks if the user has access to a topic based on an access list.
 33-func checkAccess(accessList []string, userName string, sesh ssh.Session) bool {
 34+func checkAccess(accessList []string, userName string, sesh *pssh.SSHServerConnSession) bool {
 35 	for _, acc := range accessList {
 36 		if acc == userName {
 37 			return true
 38@@ -192,10 +193,7 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
 39 			} else if cmd == "ls" {
 40 				if userName == "public" {
 41 					err := fmt.Errorf("access denied")
 42-					fmt.Fprintln(sesh.Stderr(), err)
 43-					fmt.Fprintf(sesh.Stderr(), "\r")
 44-					_ = sesh.Exit(1)
 45-					_ = sesh.Close()
 46+					sesh.Fatal(err)
 47 					return err
 48 				}
 49 
 50@@ -275,8 +273,7 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
 51 					_, _ = sesh.Write([]byte(outputData))
 52 				}
 53 
 54-				next(sesh)
 55-				return
 56+				return next(sesh)
 57 			}
 58 
 59 			topic := ""
 60@@ -308,7 +305,7 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
 61 				clean := pubCmd.Bool("c", false, "Don't send status messages")
 62 
 63 				if !flagCheck(pubCmd, topic, cmdArgs) {
 64-					return
 65+					return err
 66 				}
 67 
 68 				if pubCmd.NArg() == 1 && topic == "" {
 69@@ -384,7 +381,7 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
 70 				}
 71 
 72 				if !*clean {
 73-					wish.Printf(
 74+					fmt.Fprintf(
 75 						sesh,
 76 						"subscribe to this channel:\n  ssh %s sub %s%s\n",
 77 						toSshCmd(handler.Cfg),
 78@@ -456,7 +453,7 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
 79 							cancel()
 80 
 81 							if !*clean {
 82-								wish.Fatalln(sesh, "timeout reached, exiting ...")
 83+								sesh.Fatal(fmt.Errorf("timeout reached, exiting ..."))
 84 							} else {
 85 								err = sesh.Exit(1)
 86 								if err != nil {
 87@@ -506,7 +503,7 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
 88 				}
 89 
 90 				if err != nil && !*clean {
 91-					wish.Errorln(sesh, err)
 92+					fmt.Fprintln(sesh.Stderr(), err)
 93 				}
 94 			} else if cmd == "sub" {
 95 				subCmd := flagSet("sub", sesh)
 96@@ -516,7 +513,7 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
 97 				clean := subCmd.Bool("c", false, "Don't send status messages")
 98 
 99 				if !flagCheck(subCmd, topic, cmdArgs) {
100-					return
101+					return err
102 				}
103 
104 				if subCmd.NArg() == 1 && topic == "" {
105@@ -571,8 +568,8 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
106 					} else if !*public {
107 						name = toTopic(userName, withoutUser)
108 					} else {
109-						wish.Errorln(sesh, "access denied")
110-						return
111+						fmt.Fprintln(sesh.Stderr(), "access denied")
112+						return err
113 					}
114 				}
115 
116@@ -587,7 +584,7 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
117 				)
118 
119 				if err != nil && !*clean {
120-					wish.Errorln(sesh, err)
121+					fmt.Fprintln(sesh.Stderr(), err)
122 				}
123 			} else if cmd == "pipe" {
124 				pipeCmd := flagSet("pipe", sesh)
125@@ -597,7 +594,7 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
126 				clean := pipeCmd.Bool("c", false, "Don't send status messages")
127 
128 				if !flagCheck(pipeCmd, topic, cmdArgs) {
129-					return
130+					return err
131 				}
132 
133 				if pipeCmd.NArg() == 1 && topic == "" {
134@@ -665,7 +662,7 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
135 				}
136 
137 				if isCreator && !*clean {
138-					wish.Printf(
139+					fmt.Fprintf(
140 						sesh,
141 						"subscribe to this topic:\n  ssh %s sub %s%s\n",
142 						toSshCmd(handler.Cfg),
143@@ -685,15 +682,15 @@ func WishMiddleware(handler *CliHandler) pssh.SSHServerMiddleware {
144 				)
145 
146 				if readErr != nil && !*clean {
147-					wish.Errorln(sesh, "error reading from pipe", readErr)
148+					fmt.Fprintln(sesh.Stderr(), "error reading from pipe", readErr)
149 				}
150 
151 				if writeErr != nil && !*clean {
152-					wish.Errorln(sesh, "error writing to pipe", writeErr)
153+					fmt.Fprintln(sesh.Stderr(), "error writing to pipe", writeErr)
154 				}
155 			}
156 
157-			next(sesh)
158+			return next(sesh)
159 		}
160 	}
161 }
M pipe/ssh.go
+36, -28
  1@@ -2,30 +2,30 @@ package pipe
  2 
  3 import (
  4 	"context"
  5-	"fmt"
  6 	"os"
  7 	"os/signal"
  8 	"syscall"
  9-	"time"
 10 
 11 	"github.com/antoniomika/syncmap"
 12-	"github.com/charmbracelet/promwish"
 13-	"github.com/charmbracelet/ssh"
 14-	"github.com/charmbracelet/wish"
 15 	"github.com/picosh/pico/db/postgres"
 16+	"github.com/picosh/pico/pssh"
 17 	"github.com/picosh/pico/shared"
 18-	wsh "github.com/picosh/pico/wish"
 19 	psub "github.com/picosh/pubsub"
 20 	"github.com/picosh/utils"
 21+	"golang.org/x/crypto/ssh"
 22 )
 23 
 24 func StartSshServer() {
 25 	host := utils.GetEnv("PIPE_HOST", "0.0.0.0")
 26 	port := utils.GetEnv("PIPE_SSH_PORT", "2222")
 27 	portOverride := utils.GetEnv("PIPE_SSH_PORT_OVERRIDE", port)
 28-	promPort := utils.GetEnv("PIPE_PROM_PORT", "9222")
 29+	// promPort := utils.GetEnv("PIPE_PROM_PORT", "9222")
 30 	cfg := NewConfigSite("pipe-ssh")
 31 	logger := cfg.Logger
 32+
 33+	ctx, cancel := context.WithCancel(context.Background())
 34+	defer cancel()
 35+
 36 	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
 37 	defer dbh.Close()
 38 
 39@@ -43,38 +43,46 @@ func StartSshServer() {
 40 	}
 41 
 42 	sshAuth := shared.NewSshAuthHandler(dbh, logger)
 43-	s, err := wish.NewServer(
 44-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
 45-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
 46-		wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
 47-			sshAuth.PubkeyAuthHandler(ctx, key)
 48-			return true
 49-		}),
 50-		wish.WithMiddleware(
 51+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{
 52+		ListenAddr: "localhost:2222",
 53+		ServerConfig: &ssh.ServerConfig{
 54+			PublicKeyCallback: sshAuth.PubkeyAuthHandler,
 55+		},
 56+		Middleware: []pssh.SSHServerMiddleware{
 57 			WishMiddleware(handler),
 58-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "pipe-ssh"),
 59-			wsh.LogMiddleware(logger, dbh),
 60-		),
 61-	)
 62+			pssh.LogMiddleware(handler, dbh),
 63+		},
 64+	})
 65+
 66+	pemBytes, err := os.ReadFile("ssh_data/term_info_ed25519")
 67 	if err != nil {
 68-		logger.Error("wish server", "err", err.Error())
 69+		logger.Error("failed to read private key file", "error", err)
 70 		return
 71 	}
 72 
 73+	signer, err := ssh.ParsePrivateKey(pemBytes)
 74+	if err != nil {
 75+		logger.Error("failed to parse private key", "error", err)
 76+		return
 77+	}
 78+
 79+	server.Config.AddHostKey(signer)
 80+
 81 	done := make(chan os.Signal, 1)
 82 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
 83 	logger.Info("Starting SSH server", "host", host, "port", port)
 84 	go func() {
 85-		if err = s.ListenAndServe(); err != nil {
 86-			logger.Error("listen", "err", err.Error())
 87+		if err = server.ListenAndServe(); err != nil {
 88+			logger.Error("serve", "err", err.Error())
 89+			os.Exit(1)
 90 		}
 91 	}()
 92 
 93-	<-done
 94-	logger.Info("Stopping SSH server")
 95-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 96-	defer func() { cancel() }()
 97-	if err := s.Shutdown(ctx); err != nil {
 98-		logger.Error("shutdown", "err", err.Error())
 99+	exit := func() {
100+		logger.Info("stopping ssh server")
101+		cancel()
102 	}
103+
104+	<-done
105+	exit()
106 }
M prose/scp_hooks.go
+3, -3
 1@@ -6,9 +6,9 @@ import (
 2 
 3 	"slices"
 4 
 5-	"github.com/charmbracelet/ssh"
 6 	"github.com/picosh/pico/db"
 7 	"github.com/picosh/pico/filehandlers"
 8+	"github.com/picosh/pico/pssh"
 9 	"github.com/picosh/pico/shared"
10 	"github.com/picosh/utils"
11 	pipeUtil "github.com/picosh/utils/pipe"
12@@ -20,7 +20,7 @@ type MarkdownHooks struct {
13 	Pipe *pipeUtil.ReconnectReadWriteCloser
14 }
15 
16-func (p *MarkdownHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaData) (bool, error) {
17+func (p *MarkdownHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) (bool, error) {
18 	if !utils.IsTextFile(data.Text) {
19 		err := fmt.Errorf(
20 			"ERROR: (%s) invalid file must be plain text (utf-8), skipping",
21@@ -57,7 +57,7 @@ func (p *MarkdownHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaD
22 	return true, nil
23 }
24 
25-func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
26+func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
27 	parsedText, err := shared.ParseText(data.Text)
28 	if err != nil {
29 		return fmt.Errorf("%s: %w", data.Filename, err)
M prose/ssh.go
+47, -62
  1@@ -2,72 +2,36 @@ package prose
  2 
  3 import (
  4 	"context"
  5-	"fmt"
  6 	"os"
  7 	"os/signal"
  8 	"syscall"
  9-	"time"
 10 
 11-	"github.com/charmbracelet/promwish"
 12-	"github.com/charmbracelet/ssh"
 13-	"github.com/charmbracelet/wish"
 14 	"github.com/picosh/pico/db/postgres"
 15 	"github.com/picosh/pico/filehandlers"
 16 	uploadimgs "github.com/picosh/pico/filehandlers/imgs"
 17+	"github.com/picosh/pico/pssh"
 18 	"github.com/picosh/pico/shared"
 19 	"github.com/picosh/pico/shared/storage"
 20-	wsh "github.com/picosh/pico/wish"
 21 	"github.com/picosh/send/auth"
 22 	"github.com/picosh/send/list"
 23 	"github.com/picosh/send/pipe"
 24-	wishrsync "github.com/picosh/send/protocols/rsync"
 25+	"github.com/picosh/send/protocols/rsync"
 26 	"github.com/picosh/send/protocols/scp"
 27 	"github.com/picosh/send/protocols/sftp"
 28-	"github.com/picosh/send/proxy"
 29 	"github.com/picosh/utils"
 30+	"golang.org/x/crypto/ssh"
 31 )
 32 
 33-func createRouter(handler *filehandlers.FileHandlerRouter) proxy.Router {
 34-	return func(sh ssh.Handler, s ssh.Session) []wish.Middleware {
 35-		return []wish.Middleware{
 36-			pipe.Middleware(handler, ".md"),
 37-			list.Middleware(handler),
 38-			scp.Middleware(handler),
 39-			wishrsync.Middleware(handler),
 40-			auth.Middleware(handler),
 41-			wsh.PtyMdw(wsh.DeprecatedNotice()),
 42-			wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool),
 43-		}
 44-	}
 45-}
 46-
 47-func withProxy(handler *filehandlers.FileHandlerRouter, otherMiddleware ...wish.Middleware) ssh.Option {
 48-	return func(server *ssh.Server) error {
 49-		err := sftp.SSHOption(handler)(server)
 50-		if err != nil {
 51-			return err
 52-		}
 53-
 54-		newSubsystemHandlers := map[string]ssh.SubsystemHandler{}
 55-
 56-		for name, subsystemHandler := range server.SubsystemHandlers {
 57-			newSubsystemHandlers[name] = func(s ssh.Session) {
 58-				wsh.LogMiddleware(handler.GetLogger(s), handler.DBPool)(ssh.Handler(subsystemHandler))(s)
 59-			}
 60-		}
 61-
 62-		server.SubsystemHandlers = newSubsystemHandlers
 63-
 64-		return proxy.WithProxy(createRouter(handler), otherMiddleware...)(server)
 65-	}
 66-}
 67-
 68 func StartSshServer() {
 69 	host := utils.GetEnv("PROSE_HOST", "0.0.0.0")
 70 	port := utils.GetEnv("PROSE_SSH_PORT", "2222")
 71-	promPort := utils.GetEnv("PROSE_PROM_PORT", "9222")
 72+	// promPort := utils.GetEnv("PROSE_PROM_PORT", "9222")
 73 	cfg := NewConfigSite("prose-ssh")
 74 	logger := cfg.Logger
 75+
 76+	ctx, cancel := context.WithCancel(context.Background())
 77+	defer cancel()
 78+
 79 	dbh := postgres.NewDB(cfg.DbURL, cfg.Logger)
 80 	defer dbh.Close()
 81 
 82@@ -97,34 +61,55 @@ func StartSshServer() {
 83 	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
 84 
 85 	sshAuth := shared.NewSshAuthHandler(dbh, logger)
 86-	s, err := wish.NewServer(
 87-		wish.WithAddress(fmt.Sprintf("%s:%s", host, port)),
 88-		wish.WithHostKeyPath("ssh_data/term_info_ed25519"),
 89-		wish.WithPublicKeyAuth(sshAuth.PubkeyAuthHandler),
 90-		withProxy(
 91-			handler,
 92-			promwish.Middleware(fmt.Sprintf("%s:%s", host, promPort), "prose-ssh"),
 93-		),
 94-	)
 95+	server := pssh.NewSSHServer(ctx, logger, &pssh.SSHServerConfig{
 96+		ListenAddr: "localhost:2222",
 97+		ServerConfig: &ssh.ServerConfig{
 98+			PublicKeyCallback: sshAuth.PubkeyAuthHandler,
 99+		},
100+		Middleware: []pssh.SSHServerMiddleware{
101+			pipe.Middleware(handler, ".md"),
102+			list.Middleware(handler),
103+			scp.Middleware(handler),
104+			rsync.Middleware(handler),
105+			auth.Middleware(handler),
106+			pssh.PtyMdw(pssh.DeprecatedNotice()),
107+			pssh.LogMiddleware(handler, dbh),
108+		},
109+		SubsystemMiddleware: []pssh.SSHServerMiddleware{
110+			sftp.Middleware(handler),
111+			pssh.LogMiddleware(handler, dbh),
112+		},
113+	})
114+
115+	pemBytes, err := os.ReadFile("ssh_data/term_info_ed25519")
116 	if err != nil {
117-		logger.Error("wish server", "err", err.Error())
118+		logger.Error("failed to read private key file", "error", err)
119 		return
120 	}
121 
122+	signer, err := ssh.ParsePrivateKey(pemBytes)
123+	if err != nil {
124+		logger.Error("failed to parse private key", "error", err)
125+		return
126+	}
127+
128+	server.Config.AddHostKey(signer)
129+
130 	done := make(chan os.Signal, 1)
131 	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
132 	logger.Info("Starting SSH server", "host", host, "port", port)
133 	go func() {
134-		if err = s.ListenAndServe(); err != nil {
135-			logger.Error(err.Error())
136+		if err = server.ListenAndServe(); err != nil {
137+			logger.Error("serve", "err", err.Error())
138+			os.Exit(1)
139 		}
140 	}()
141 
142-	<-done
143-	logger.Info("Stopping SSH server")
144-	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
145-	defer func() { cancel() }()
146-	if err := s.Shutdown(ctx); err != nil {
147-		logger.Error(err.Error())
148+	exit := func() {
149+		logger.Info("stopping ssh server")
150+		cancel()
151 	}
152+
153+	<-done
154+	exit()
155 }
M pssh/server.go
+22, -2
 1@@ -3,8 +3,10 @@ package pssh
 2 import (
 3 	"context"
 4 	"errors"
 5+	"fmt"
 6 	"log/slog"
 7 	"net"
 8+	"os"
 9 	"strings"
10 	"sync"
11 	"time"
12@@ -119,20 +121,38 @@ func (s *SSHServerConnSession) Exit(code int) error {
13 	return err
14 }
15 
16+func (s *SSHServerConnSession) Fatal(err error) {
17+	fmt.Fprintln(s.Stderr(), err)
18+	fmt.Fprintf(s.Stderr(), "\r")
19+	_ = s.Exit(1)
20+	_ = s.Close()
21+}
22+
23 type Window struct {
24-	Width  int
25-	Height int
26+	Width        int
27+	Height       int
28+	HeightPixels int
29+	WidthPixels  int
30 }
31 
32 type Pty struct {
33 	Term   string
34 	Window Window
35+	Slave  os.File
36 }
37 
38 func (s *SSHServerConnSession) Pty() (Pty, <-chan Window, bool) {
39 	return Pty{}, nil, false
40 }
41 
42+func (p Pty) Resize(width, height int) error {
43+	return nil
44+}
45+
46+func (p Pty) Name() string {
47+	return ""
48+}
49+
50 var _ context.Context = &SSHServerConnSession{}
51 
52 func (sc *SSHServerConn) Handle(chans <-chan ssh.NewChannel, reqs <-chan *ssh.Request) error {
M shared/senpai.go
+11, -7
 1@@ -6,8 +6,8 @@ import (
 2 	"sync"
 3 
 4 	"git.sr.ht/~delthas/senpai"
 5-	"github.com/charmbracelet/ssh"
 6 	"github.com/containerd/console"
 7+	"github.com/picosh/pico/pssh"
 8 )
 9 
10 type consoleData struct {
11@@ -16,13 +16,13 @@ type consoleData struct {
12 }
13 
14 type VConsole struct {
15-	ssh.Session
16-	pty ssh.Pty
17+	Session *pssh.SSHServerConnSession
18+	pty     pssh.Pty
19 
20 	sizeEnableOnce sync.Once
21 
22 	windowMu      sync.Mutex
23-	currentWindow ssh.Window
24+	currentWindow pssh.Window
25 
26 	readReq  chan []byte
27 	dataChan chan consoleData
28@@ -94,7 +94,7 @@ func (v *VConsole) DisableEcho() error {
29 }
30 
31 func (v *VConsole) Reset() error {
32-	_, err := v.Write([]byte("\033[?25h\033[0 q\033[34h\033[?25h\033[39;49m\033[m^O\033[H\033[J\033[?1049l\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l"))
33+	_, err := v.Session.Write([]byte("\033[?25h\033[0 q\033[34h\033[?25h\033[39;49m\033[m^O\033[H\033[J\033[?1049l\033[?1l\033>\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l"))
34 	return err
35 }
36 
37@@ -123,7 +123,11 @@ func (v *VConsole) Close() error {
38 	return err
39 }
40 
41-func NewVConsole(sesh ssh.Session) (*VConsole, error) {
42+func (v *VConsole) Write(p []byte) (int, error) {
43+	return v.Session.Write(p)
44+}
45+
46+func NewVConsole(sesh *pssh.SSHServerConnSession) (*VConsole, error) {
47 	pty, win, ok := sesh.Pty()
48 	if !ok {
49 		return nil, fmt.Errorf("PTY not found")
50@@ -178,7 +182,7 @@ func NewVConsole(sesh ssh.Session) (*VConsole, error) {
51 	return vty, nil
52 }
53 
54-func NewSenpaiApp(sesh ssh.Session, username, pass string) (*senpai.App, error) {
55+func NewSenpaiApp(sesh *pssh.SSHServerConnSession, username, pass string) (*senpai.App, error) {
56 	vty, err := NewVConsole(sesh)
57 	if err != nil {
58 		slog.Error("PTY not found")
M tui/ui.go
+2, -2
 1@@ -9,8 +9,8 @@ import (
 2 	"git.sr.ht/~rockorager/vaxis"
 3 	"git.sr.ht/~rockorager/vaxis/vxfw"
 4 	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
 5-	"github.com/charmbracelet/ssh"
 6 	"github.com/picosh/pico/db"
 7+	"github.com/picosh/pico/pssh"
 8 	"github.com/picosh/pico/shared"
 9 	"github.com/picosh/utils"
10 )
11@@ -19,7 +19,7 @@ var HOME = "dash"
12 
13 type SharedModel struct {
14 	Logger             *slog.Logger
15-	Session            ssh.Session
16+	Session            *pssh.SSHServerConnSession
17 	Cfg                *shared.ConfigSite
18 	Dbpool             db.DB
19 	User               *db.User