109 lines
3.4 KiB
Go
109 lines
3.4 KiB
Go
package serve
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/charmbracelet/ssh"
|
|
"github.com/charmbracelet/wish"
|
|
"github.com/charmbracelet/wish/activeterm"
|
|
"github.com/charmbracelet/wish/logging"
|
|
)
|
|
|
|
// Setup default logger to append or create a new log file `log` in the current
|
|
// working directory containing all the logs reported by the ssh server.
|
|
func setupLogging() {
|
|
file, err := os.OpenFile("log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
panic(fmt.Errorf("Could not create or open log file. err: %w", err))
|
|
}
|
|
log.SetDefault(log.NewWithOptions(file, log.Options{ReportTimestamp: true}))
|
|
}
|
|
|
|
// Configure ssh server with the provided arguments. An empty `users` map
|
|
// will disable user authentication. At least one user is required for user
|
|
// authentication via ssh-public keys. Each map entry has a pair of *username*
|
|
// and *pub-key value*. The ssh server will serve every authenticated user (or
|
|
// all if no user is given) the application `name` with the provided arguments
|
|
// `args` as the ssh session. The ssh session will always allocate an pty
|
|
// (necessary for tui applications).
|
|
func setupSshServer(host string, port string, host_key_path string, users map[string]string, name string, args []string) (*ssh.Server, error) {
|
|
return wish.NewServer(
|
|
wish.WithAddress(net.JoinHostPort(host, port)),
|
|
wish.WithHostKeyPath(host_key_path),
|
|
wish.WithPublicKeyAuth(func(_ ssh.Context, key ssh.PublicKey) bool {
|
|
if len(users) == 0 {
|
|
// no users provided, meaning there is no user authentication, everyone is allowed to connect
|
|
return true
|
|
}
|
|
for _, pubkey := range users {
|
|
parsed, _, _, _, _ := ssh.ParseAuthorizedKey([]byte(pubkey))
|
|
if ssh.KeysEqual(key, parsed) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}),
|
|
ssh.AllocatePty(),
|
|
wish.WithMiddleware(
|
|
func(next ssh.Handler) ssh.Handler {
|
|
return func(s ssh.Session) {
|
|
cmd := wish.Command(s, name, args...)
|
|
file, err := os.OpenFile("log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
wish.Fatalln(s, err)
|
|
}
|
|
cmd.SetStderr(file)
|
|
if err := cmd.Run(); err != nil {
|
|
wish.Fatalln(s, err)
|
|
}
|
|
next(s)
|
|
}
|
|
},
|
|
activeterm.Middleware(),
|
|
logging.Middleware(),
|
|
),
|
|
)
|
|
}
|
|
|
|
// Serve an ssh application using the provided arguments.
|
|
func Serve(host string, port string, host_key_path string, users map[string]string, name string, args []string) {
|
|
setupLogging()
|
|
log.Info("Running Serve (v2.8)...") // TODO make version string automatically the git tag (otherwise commit-hash)?
|
|
defer log.Info("Stopping Serve...")
|
|
|
|
srv, err := setupSshServer(host, port, host_key_path, users, name, args)
|
|
if err != nil {
|
|
log.Error("Could not start server", "error", err)
|
|
}
|
|
|
|
done := make(chan os.Signal, 1)
|
|
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
go func() {
|
|
log.Info("Starting SSH server...")
|
|
if err = srv.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
|
|
// We ignore ErrServerClosed because it is expected.
|
|
log.Error("Could not start server", "error", err)
|
|
done <- nil
|
|
}
|
|
}()
|
|
|
|
<-done
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer func() { cancel() }()
|
|
|
|
log.Info("Stopping SSH server...")
|
|
if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
|
|
log.Error("Could not stop server", "error", err)
|
|
}
|
|
}
|