Files
go-ssh-server/main.go

236 lines
5.7 KiB
Go
Raw Normal View History

package main
import (
2025-11-06 23:31:41 +01:00
"context"
"crypto/ed25519"
"crypto/rand"
2025-11-06 20:12:28 +01:00
"encoding/pem"
2025-11-06 23:31:41 +01:00
"errors"
"log/slog"
"net"
"os"
2025-11-06 23:31:41 +01:00
"os/signal"
"syscall"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
"github.com/charmbracelet/ssh"
"github.com/charmbracelet/wish"
"github.com/charmbracelet/wish/activeterm"
"github.com/charmbracelet/wish/bubbletea"
"github.com/charmbracelet/wish/logging"
"github.com/charmbracelet/wish/recover"
gossh "golang.org/x/crypto/ssh"
)
2025-11-06 23:31:41 +01:00
type Theme struct {
Bg lipgloss.Color
Fg lipgloss.Color
Red lipgloss.Color
Green lipgloss.Color
Yellow lipgloss.Color
Blue lipgloss.Color
Purple lipgloss.Color
Aqua lipgloss.Color
Orange lipgloss.Color
Gray lipgloss.Color
}
2025-11-06 23:31:41 +01:00
func gruvboxDark() Theme {
return Theme{
Bg: lipgloss.Color("#282828"),
Fg: lipgloss.Color("#ebdbb2"),
Red: lipgloss.Color("#cc241d"),
Green: lipgloss.Color("#98971a"),
Yellow: lipgloss.Color("#d79921"),
Blue: lipgloss.Color("#458588"),
Purple: lipgloss.Color("#b16286"),
Aqua: lipgloss.Color("#689d6a"),
Orange: lipgloss.Color("#d65d0e"),
Gray: lipgloss.Color("#928374"),
}
}
2025-11-06 23:31:41 +01:00
type model struct {
showWelcome bool
focus int
showHelp bool
theme Theme
blink bool
blinkCount int
}
const numBoxes = 4
func (m model) Init() tea.Cmd {
return tea.Tick(time.Millisecond*70, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
type tickMsg time.Time
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tickMsg:
m.blinkCount++
m.blink = (m.blinkCount / 3) % 2 == 0
return m, tea.Tick(time.Millisecond*70, func(t time.Time) tea.Msg {
return tickMsg(t)
})
case tea.KeyMsg:
if m.showWelcome {
m.showWelcome = false
return m, nil
}
if m.showHelp {
switch msg.String() {
case "q", "esc", "?", "enter", "backspace":
m.showHelp = false
2025-11-06 22:23:59 +01:00
}
2025-11-06 23:31:41 +01:00
return m, nil
}
switch msg.String() {
case "h", "left":
m.focus = (m.focus + 3) % numBoxes
case "l", "right":
m.focus = (m.focus + 1) % numBoxes
case "j", "down":
if m.focus < 2 {
m.focus += 2
2025-11-06 22:23:59 +01:00
} else {
2025-11-06 23:31:41 +01:00
m.focus -= 2
2025-11-06 22:23:59 +01:00
}
2025-11-06 23:31:41 +01:00
case "k", "up":
if m.focus >= 2 {
m.focus -= 2
} else {
m.focus += 2
2025-11-06 22:23:59 +01:00
}
2025-11-06 23:31:41 +01:00
case "?":
m.showHelp = true
case "q", "esc":
return m, tea.Quit
}
}
2025-11-06 23:31:41 +01:00
return m, nil
}
2025-11-06 23:31:41 +01:00
func (m model) View() string {
if m.showWelcome {
fg := lipgloss.NewStyle().Foreground(m.theme.Fg).Background(m.theme.Bg)
accentFg := lipgloss.NewStyle().Foreground(m.theme.Blue).Background(m.theme.Bg)
if m.blink {
accentFg = lipgloss.NewStyle().Foreground(m.theme.Bg).Background(m.theme.Bg)
}
welcome := fg.Render("Welcome to ") +
lipgloss.NewStyle().Foreground(m.theme.Orange).Background(m.theme.Bg).Render("dcorral.com") +
fg.Render("!") + "\n" +
fg.Render("press ") + accentFg.Render("any key") + fg.Render(" to continue")
return welcome
}
if m.showHelp {
return lipgloss.NewStyle().Foreground(m.theme.Fg).Background(m.theme.Bg).Render(
"Navigation:\n" +
"h / ←: Move left\n" +
"l / →: Move right\n" +
"j / ↓: Move down\n" +
"k / ↑: Move up\n" +
"? : Show help\n" +
"q / Esc: Quit",
)
}
2025-11-06 23:31:41 +01:00
// Render the boxes
boxes := make([]string, numBoxes)
for i := range boxes {
var style lipgloss.Style
if i == m.focus {
style = lipgloss.NewStyle().Background(m.theme.Blue).Foreground(m.theme.Bg).Border(lipgloss.NormalBorder()).Padding(0, 1)
} else {
style = lipgloss.NewStyle().Background(m.theme.Bg).Foreground(m.theme.Fg).Border(lipgloss.NormalBorder()).Padding(0, 1)
}
boxes[i] = style.Render("Box " + string(rune('0'+i)))
}
grid := lipgloss.JoinHorizontal(lipgloss.Top, boxes[0], " ", boxes[1]) + "\n" +
lipgloss.JoinHorizontal(lipgloss.Top, boxes[2], " ", boxes[3]) + "\n" +
lipgloss.NewStyle().Foreground(m.theme.Fg).Background(m.theme.Bg).Render("Use hjkl or arrows to navigate, ? for help, q to quit")
return grid
}
func main() {
// Generate host key
_, key, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
2025-11-06 23:31:41 +01:00
log.Error("Failed to generate host key", "error", err)
os.Exit(1)
}
2025-11-06 23:31:41 +01:00
block, err := gossh.MarshalPrivateKey(key, "")
if err != nil {
2025-11-06 23:31:41 +01:00
log.Error("Failed to marshal host key", "error", err)
os.Exit(1)
}
privateKeyBytes := pem.EncodeToMemory(block)
log.Info("Generated host key")
ctx, cancel := context.WithCancel(context.Background())
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-signalChan
cancel()
}()
port := os.Getenv("PORT")
if port == "" {
port = "22"
}
2025-11-06 23:31:41 +01:00
s, err := wish.NewServer(
wish.WithAddress(net.JoinHostPort("0.0.0.0", port)),
wish.WithHostKeyPEM(privateKeyBytes),
wish.WithMiddleware(
recover.Middleware(
bubbletea.Middleware(teaHandler),
activeterm.Middleware(),
logging.Middleware(),
),
),
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
return true
}),
wish.WithKeyboardInteractiveAuth(
func(ctx ssh.Context, challenger gossh.KeyboardInteractiveChallenge) bool {
return true
},
),
)
if err != nil {
log.Error("Could not start server", "error", err)
os.Exit(1)
}
2025-11-06 23:31:41 +01:00
log.Info("Starting SSH server", "port", port)
go func() {
if err = s.ListenAndServe(); err != nil && !errors.Is(err, ssh.ErrServerClosed) {
log.Error("Could not start server", "error", err)
cancel()
}
}()
<-ctx.Done()
s.Shutdown(ctx)
slog.Info("Shutting down server")
2025-11-06 16:21:54 +01:00
}
2025-11-06 22:23:59 +01:00
2025-11-06 23:31:41 +01:00
func teaHandler(s ssh.Session) (tea.Model, []tea.ProgramOption) {
return model{
showWelcome: true,
focus: 0,
showHelp: false,
theme: gruvboxDark(),
blink: false,
blinkCount: 0,
}, []tea.ProgramOption{tea.WithAltScreen()}
}