Add ssh server functionality (#150)

* Add ssh server functionality via `wish` library

This adds a `-server` flag which will spin up an ssh server that
executes the slides bubbletea application upon each ssh session.
This commit is contained in:
Ivan Tse
2022-04-13 13:07:06 -04:00
committed by GitHub
parent fb069210e1
commit 93c0f899c4
6 changed files with 243 additions and 29 deletions

89
internal/cmd/serve.go Normal file
View File

@@ -0,0 +1,89 @@
package cmd
import (
"context"
"log"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/maaslalani/slides/internal/model"
"github.com/maaslalani/slides/internal/navigation"
"github.com/maaslalani/slides/internal/server"
"github.com/muesli/coral"
)
var (
host string
port int
keyPath string
err error
fileName string
ServeCmd = &coral.Command{
Use: "serve <file.md>",
Aliases: []string{"server"},
Short: "Start an SSH server to run slides",
Args: coral.ArbitraryArgs,
RunE: func(cmd *coral.Command, args []string) error {
k := os.Getenv("SLIDES_SERVER_KEY_PATH")
if k != "" {
keyPath = k
}
h := os.Getenv("SLIDES_SERVER_HOST")
if h != "" {
host = h
}
p := os.Getenv("SLIDES_SERVER_PORT")
if p != "" {
port, _ = strconv.Atoi(p)
}
if len(args) > 0 {
fileName = args[0]
}
presentation := model.Model{
Page: 0,
Date: time.Now().Format("2006-01-02"),
FileName: fileName,
Search: navigation.NewSearch(),
}
err = presentation.Load()
if err != nil {
return err
}
s, err := server.NewServer(keyPath, host, port, presentation)
if err != nil {
return err
}
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
log.Printf("Starting Slides server on %s:%d", host, port)
go func() {
if err = s.Start(); err != nil {
log.Fatalln(err)
}
}()
<-done
log.Print("Stopping Slides server")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer func() { cancel() }()
if err := s.Shutdown(ctx); err != nil {
return err
}
return err
},
}
)
func init() {
ServeCmd.Flags().StringVar(&keyPath, "keyPath", "slides", "Server private key path")
ServeCmd.Flags().StringVar(&host, "host", "localhost", "Server host to bind to")
ServeCmd.Flags().IntVar(&port, "port", 53531, "Server port to bind to")
}

View File

@@ -0,0 +1,32 @@
package server
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/wish"
bm "github.com/charmbracelet/wish/bubbletea"
"github.com/gliderlabs/ssh"
"github.com/muesli/termenv"
)
func slidesMiddleware(srv *Server) wish.Middleware {
newProg := func(m tea.Model, opts ...tea.ProgramOption) *tea.Program {
p := tea.NewProgram(m, opts...)
return p
}
teaHandler := func(s ssh.Session) *tea.Program {
_, _, active := s.Pty()
if !active {
fmt.Println("no active terminal, skipping")
err := s.Exit(1)
if err != nil {
fmt.Println("Error exiting session")
}
return nil
}
return newProg(srv.presentation, tea.WithInput(s), tea.WithOutput(s), tea.WithAltScreen())
}
return bm.MiddlewareWithProgramHandler(teaHandler, termenv.ANSI256)
}

48
internal/server/server.go Normal file
View File

@@ -0,0 +1,48 @@
package server
import (
"context"
"fmt"
"github.com/charmbracelet/wish"
"github.com/gliderlabs/ssh"
"github.com/maaslalani/slides/internal/model"
)
type Server struct {
host string
port int
srv *ssh.Server
presentation model.Model
}
// NewServer creates a new server.
func NewServer(keyPath, host string, port int, presentation model.Model) (*Server, error) {
s := &Server{
host: host,
port: port,
presentation: presentation,
}
srv, err := wish.NewServer(
wish.WithHostKeyPath(keyPath),
wish.WithAddress(fmt.Sprintf("%s:%d", host, port)),
wish.WithMiddleware(
slidesMiddleware(s),
),
)
if err != nil {
return nil, err
}
s.srv = srv
return s, nil
}
// Start starts the ssh server.
func (s *Server) Start() error {
return s.srv.ListenAndServe()
}
// Shutdown shuts down the server.
func (s *Server) Shutdown(ctx context.Context) error {
return s.srv.Shutdown(ctx)
}