feat: add Fiber HTTPS server with embedded static files
This commit is contained in:
34
internal/server/net.go
Normal file
34
internal/server/net.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetLANIP returns the first non-loopback IPv4 address.
|
||||||
|
func GetLANIP() (string, error) {
|
||||||
|
addrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil {
|
||||||
|
return ipNet.IP.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no LAN IP found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken creates a cryptographically random token for WS auth.
|
||||||
|
func GenerateToken() string {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
slog.Error("failed to generate token", "error", err)
|
||||||
|
// Fallback to a less secure but functional token
|
||||||
|
return "voicepaste-fallback-token"
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
91
internal/server/server.go
Normal file
91
internal/server/server.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/gofiber/fiber/v3/middleware/static"
|
||||||
|
|
||||||
|
"github.com/imbytecat/voicepaste/internal/config"
|
||||||
|
vpTLS "github.com/imbytecat/voicepaste/internal/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server holds the Fiber app and related state.
|
||||||
|
type Server struct {
|
||||||
|
app *fiber.App
|
||||||
|
token string
|
||||||
|
lanIP string
|
||||||
|
webFS fs.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Server instance.
|
||||||
|
func New(token, lanIP string, webFS fs.FS) *Server {
|
||||||
|
app := fiber.New(fiber.Config{
|
||||||
|
AppName: "VoicePaste",
|
||||||
|
})
|
||||||
|
|
||||||
|
s := &Server{
|
||||||
|
app: app,
|
||||||
|
token: token,
|
||||||
|
lanIP: lanIP,
|
||||||
|
webFS: webFS,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.setupRoutes()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupRoutes configures HTTP routes.
|
||||||
|
func (s *Server) setupRoutes() {
|
||||||
|
// Health check
|
||||||
|
s.app.Get("/health", func(c fiber.Ctx) error {
|
||||||
|
return c.SendString("ok")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Static files from embedded FS
|
||||||
|
s.app.Use("/", static.New("", static.Config{
|
||||||
|
FS: s.webFS,
|
||||||
|
Browse: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// App returns the underlying Fiber app for WebSocket route registration.
|
||||||
|
func (s *Server) App() *fiber.App {
|
||||||
|
return s.app
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token returns the auth token.
|
||||||
|
func (s *Server) Token() string {
|
||||||
|
return s.token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the HTTPS server.
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
cfg := config.Get()
|
||||||
|
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
||||||
|
|
||||||
|
if cfg.Server.TLSAuto {
|
||||||
|
tlsCfg, err := vpTLS.GetTLSConfig(s.lanIP)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("TLS setup failed: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("starting HTTPS server", "addr", addr)
|
||||||
|
return s.app.Listen(addr, fiber.ListenConfig{
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
Certificates: tlsCfg.Certificates,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("starting HTTP server (no TLS)", "addr", addr)
|
||||||
|
return s.app.Listen(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown gracefully shuts down the server.
|
||||||
|
func (s *Server) Shutdown() error {
|
||||||
|
return s.app.Shutdown()
|
||||||
|
}
|
||||||
119
main.go
Normal file
119
main.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"github.com/imbytecat/voicepaste/internal/asr"
|
||||||
|
"github.com/imbytecat/voicepaste/internal/config"
|
||||||
|
"github.com/imbytecat/voicepaste/internal/paste"
|
||||||
|
"github.com/imbytecat/voicepaste/internal/server"
|
||||||
|
"github.com/imbytecat/voicepaste/internal/ws"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:web
|
||||||
|
var webFS embed.FS
|
||||||
|
var version = "dev"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelInfo,
|
||||||
|
})))
|
||||||
|
|
||||||
|
slog.Info("VoicePaste", "version", version)
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
cfg, err := config.Load("")
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to load config", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start config hot-reload watcher
|
||||||
|
config.WatchAndReload("")
|
||||||
|
|
||||||
|
// Initialize clipboard
|
||||||
|
if err := paste.Init(); err != nil {
|
||||||
|
slog.Warn("clipboard init failed, paste will be unavailable", "err", err)
|
||||||
|
}
|
||||||
|
// Detect LAN IP
|
||||||
|
lanIP, err := server.GetLANIP()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to detect LAN IP", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate auth token
|
||||||
|
token := server.GenerateToken()
|
||||||
|
|
||||||
|
// Build URL
|
||||||
|
scheme := "https"
|
||||||
|
if !cfg.Server.TLSAuto {
|
||||||
|
scheme = "http"
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("%s://%s:%d/?token=%s", scheme, lanIP, cfg.Server.Port, token)
|
||||||
|
|
||||||
|
// Print connection info
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("╔══════════════════════════════════════╗")
|
||||||
|
fmt.Println("║ VoicePaste Ready ║")
|
||||||
|
fmt.Println("╚══════════════════════════════════════╝")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf(" URL: %s\n", url)
|
||||||
|
fmt.Println()
|
||||||
|
printQRCode(url)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(" Scan QR code with your phone to connect.")
|
||||||
|
fmt.Println(" Press Ctrl+C to stop.")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Create and start server
|
||||||
|
webContent, _ := fs.Sub(webFS, "web")
|
||||||
|
srv := server.New(token, lanIP, webContent)
|
||||||
|
|
||||||
|
// Build ASR factory from config
|
||||||
|
asrCfg := asr.Config{
|
||||||
|
AppKey: cfg.Doubao.AppKey,
|
||||||
|
AccessKey: cfg.Doubao.AccessKey,
|
||||||
|
ResourceID: cfg.Doubao.ResourceID,
|
||||||
|
}
|
||||||
|
asrFactory := func(resultCh chan<- ws.ServerMsg) (func([]byte), func(), error) {
|
||||||
|
client, err := asr.Dial(asrCfg, resultCh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
sendAudio := func(pcm []byte) {
|
||||||
|
if err := client.SendAudio(pcm, false); err != nil {
|
||||||
|
slog.Warn("send audio to asr", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleanup := func() {
|
||||||
|
// Send last empty frame to signal end
|
||||||
|
_ = client.SendAudio(nil, true)
|
||||||
|
client.Close()
|
||||||
|
}
|
||||||
|
return sendAudio, cleanup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register WebSocket handler
|
||||||
|
wsHandler := ws.NewHandler(token, paste.Paste, asrFactory)
|
||||||
|
wsHandler.Register(srv.App())
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
go func() {
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sigCh
|
||||||
|
slog.Info("shutting down...")
|
||||||
|
srv.Shutdown()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := srv.Start(); err != nil {
|
||||||
|
slog.Error("server error", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
qrcode.go
Normal file
18
qrcode.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/mdp/qrterminal/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// printQRCode prints a QR code to the terminal.
|
||||||
|
func printQRCode(url string) {
|
||||||
|
qrterminal.GenerateWithConfig(url, qrterminal.Config{
|
||||||
|
Level: qrterminal.L,
|
||||||
|
Writer: os.Stdout,
|
||||||
|
BlackChar: qrterminal.BLACK,
|
||||||
|
WhiteChar: qrterminal.WHITE,
|
||||||
|
QuietZone: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user