main pico / pkg / apps / prose / ssh.go
Antonio Mika  ·  2026-10-22
  1package prose
  2
  3import (
  4	"context"
  5	"os"
  6	"os/signal"
  7	"syscall"
  8
  9	"github.com/picosh/pico/pkg/db/postgres"
 10	"github.com/picosh/pico/pkg/pssh"
 11	"github.com/picosh/pico/pkg/send/auth"
 12	"github.com/picosh/pico/pkg/send/list"
 13	"github.com/picosh/pico/pkg/send/pipe"
 14	"github.com/picosh/pico/pkg/send/protocols/rsync"
 15	"github.com/picosh/pico/pkg/send/protocols/scp"
 16	"github.com/picosh/pico/pkg/send/protocols/sftp"
 17	"github.com/picosh/pico/pkg/shared"
 18	"github.com/picosh/pico/pkg/storage"
 19)
 20
 21const help = `prose.sh: a blog you publish over ssh
 22
 23  rsync ~/blog/* prose.sh:/                  publish posts and images; scp, sftp and sshfs work too
 24  cat post.md | ssh prose.sh hello-world.md  upload one post
 25  cat post.md | ssh prose.sh                 upload with a random name
 26
 27Run ssh pico.sh for your dashboard. Docs: https://pico.sh/prose
 28`
 29
 30func StartSshServer() {
 31	appName := "prose-ssh"
 32
 33	host := shared.GetEnv("PROSE_HOST", "0.0.0.0")
 34	port := shared.GetEnv("PROSE_SSH_PORT", "2222")
 35	promPort := shared.GetEnv("PROSE_PROM_PORT", "9222")
 36	cfg := NewConfig(appName)
 37	logger := cfg.Logger
 38
 39	ctx, cancel := context.WithCancel(context.Background())
 40	defer cancel()
 41
 42	dbh := NewDB(postgres.NewDB(cfg.DbURL, cfg.Logger))
 43	defer func() {
 44		_ = dbh.Close()
 45	}()
 46
 47	adapter := storage.GetStorageTypeFromEnv()
 48	st, err := storage.NewStorage(cfg.Logger, adapter)
 49	if err != nil {
 50		logger.Error("loading storage", "err", err)
 51		return
 52	}
 53
 54	handler := NewFileHandler(cfg, dbh, st)
 55
 56	sshAuth := shared.NewSshAuthHandler(dbh, logger, "prose")
 57
 58	// Create a new SSH server
 59	server, err := pssh.NewSSHServerWithConfig(
 60		ctx,
 61		logger,
 62		appName,
 63		host,
 64		port,
 65		promPort,
 66		"ssh_data/term_info_ed25519",
 67		sshAuth.PubkeyAuthHandler,
 68		[]pssh.SSHServerMiddleware{
 69			pipe.Middleware(handler, ".md"),
 70			list.Middleware(handler),
 71			scp.Middleware(handler),
 72			rsync.Middleware(handler),
 73			auth.Middleware(handler),
 74			pssh.PtyMdw(pssh.HelpNotice(help)),
 75			pssh.LogMiddleware(handler, dbh),
 76		},
 77		[]pssh.SSHServerMiddleware{
 78			sftp.Middleware(handler),
 79			pssh.LogMiddleware(handler, dbh),
 80		},
 81		nil,
 82	)
 83
 84	if err != nil {
 85		logger.Error("failed to create ssh server", "err", err.Error())
 86		os.Exit(1)
 87	}
 88
 89	done := make(chan os.Signal, 1)
 90	signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
 91	logger.Info("Starting SSH server", "addr", server.Config.ListenAddr)
 92	go func() {
 93		if err = server.ListenAndServe(); err != nil {
 94			logger.Error("serve", "err", err.Error())
 95			os.Exit(1)
 96		}
 97	}()
 98
 99	exit := func() {
100		logger.Info("stopping ssh server")
101		cancel()
102	}
103
104	<-done
105	exit()
106}