vibetunnel/linux/pkg/server/app.go
2025-06-21 02:49:38 +02:00

219 lines
6.3 KiB
Go

package server
import (
"fmt"
"log"
"net/http"
"path/filepath"
"time"
"github.com/gorilla/mux"
"github.com/vibetunnel/linux/pkg/ngrok"
"github.com/vibetunnel/linux/pkg/server/middleware"
"github.com/vibetunnel/linux/pkg/server/routes"
"github.com/vibetunnel/linux/pkg/server/services"
"github.com/vibetunnel/linux/pkg/session"
)
// App represents the main server application
type App struct {
router *mux.Router
sessionManager *session.Manager
terminalManager *services.TerminalManager
bufferAggregator *services.BufferAggregator
authMiddleware *middleware.AuthMiddleware
ngrokService *ngrok.Service
remoteRegistry *services.RemoteRegistry
streamWatcher *services.StreamWatcher
controlWatcher *services.ControlDirectoryWatcher
config *Config
}
// Config represents server configuration
type Config struct {
SessionManager *session.Manager
StaticPath string
BasicAuthUsername string
BasicAuthPassword string
Port int
NoSpawn bool
DoNotAllowColumnSet bool
IsHQMode bool
HQClient *services.HQClient
BearerToken string // Token for HQ to authenticate with this remote
}
// NewApp creates a new server application
func NewApp(config *Config) *App {
authConfig := middleware.AuthConfig{
BasicAuthUsername: config.BasicAuthUsername,
BasicAuthPassword: config.BasicAuthPassword,
IsHQMode: config.IsHQMode,
BearerToken: config.BearerToken,
}
app := &App{
router: mux.NewRouter(),
sessionManager: config.SessionManager,
ngrokService: ngrok.NewService(),
config: config,
authMiddleware: middleware.NewAuthMiddleware(authConfig),
streamWatcher: services.NewStreamWatcher(),
}
// Initialize remote registry if in HQ mode
if config.IsHQMode {
app.remoteRegistry = services.NewRemoteRegistry()
}
// Initialize services
app.terminalManager = services.NewTerminalManager(config.SessionManager)
app.terminalManager.SetNoSpawn(config.NoSpawn)
app.terminalManager.SetDoNotAllowColumnSet(config.DoNotAllowColumnSet)
// Initialize buffer aggregator after terminal manager
app.bufferAggregator = services.NewBufferAggregator(&services.BufferAggregatorConfig{
TerminalManager: app.terminalManager,
RemoteRegistry: app.remoteRegistry,
IsHQMode: config.IsHQMode,
})
// Initialize control directory watcher
controlPath := ""
if config.SessionManager != nil {
controlPath = config.SessionManager.GetControlPath()
}
if controlPath != "" {
if watcher, err := services.NewControlDirectoryWatcher(controlPath, config.SessionManager, app.streamWatcher); err == nil {
app.controlWatcher = watcher
app.controlWatcher.Start()
} else {
log.Printf("[WARNING] Failed to create control directory watcher: %v", err)
}
}
// Configure routes
app.configureRoutes()
return app
}
// configureRoutes sets up all application routes
func (app *App) configureRoutes() {
// Health check (no auth needed)
app.router.HandleFunc("/api/health", app.handleHealth).Methods("GET")
// API routes with authentication middleware
apiRouter := app.router.PathPrefix("/api").Subrouter()
apiRouter.Use(app.authMiddleware.Authenticate)
// Session routes with HQ mode support
sessionRoutes := routes.NewSessionRoutes(&routes.SessionRoutesConfig{
TerminalManager: app.terminalManager,
SessionManager: app.sessionManager,
StreamWatcher: app.streamWatcher,
RemoteRegistry: app.remoteRegistry,
IsHQMode: app.config.IsHQMode,
})
sessionRoutes.RegisterRoutes(apiRouter)
// Filesystem routes
filesystemRoutes := routes.NewFilesystemRoutes()
filesystemRoutes.RegisterRoutes(apiRouter)
// Ngrok routes
ngrokRoutes := routes.NewNgrokRoutes(app.ngrokService, app.config.Port)
ngrokRoutes.RegisterRoutes(apiRouter)
// Remote routes (HQ mode only)
if app.config.IsHQMode && app.remoteRegistry != nil {
remoteRoutes := routes.NewRemoteRoutes(app.remoteRegistry, app.config.IsHQMode)
remoteRoutes.RegisterRoutes(apiRouter)
}
// WebSocket endpoint for binary buffer streaming
app.router.HandleFunc("/buffers", app.handleWebSocket).Methods("GET").Headers("Upgrade", "websocket")
// Static file serving
if app.config.StaticPath != "" {
app.router.PathPrefix("/").HandlerFunc(app.serveStaticWithIndex)
}
}
// Handler returns the HTTP handler for the application
func (app *App) Handler() http.Handler {
return app.router
}
// GetNgrokService returns the ngrok service for external control
func (app *App) GetNgrokService() *ngrok.Service {
return app.ngrokService
}
// GetBufferAggregator returns the buffer aggregator service
func (app *App) GetBufferAggregator() *services.BufferAggregator {
return app.bufferAggregator
}
// Stop gracefully stops the application
func (app *App) Stop() {
if app.bufferAggregator != nil {
app.bufferAggregator.Stop()
}
if app.controlWatcher != nil {
app.controlWatcher.Stop()
}
if app.streamWatcher != nil {
app.streamWatcher.Stop()
}
}
func (app *App) handleHealth(w http.ResponseWriter, r *http.Request) {
mode := "remote"
if app.config.IsHQMode {
mode = "hq"
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status":"ok","timestamp":"%s","mode":"%s"}`,
time.Now().Format(time.RFC3339), mode)
}
func (app *App) handleWebSocket(w http.ResponseWriter, r *http.Request) {
app.bufferAggregator.HandleClientConnection(w, r)
}
func (app *App) serveStaticWithIndex(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// Add CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
// Clean the path
if path == "/" {
path = "/index.html"
}
// Try to serve the file
// fullPath := filepath.Join(app.config.StaticPath, filepath.Clean(path))
// Check if it's a directory
info, err := http.Dir(app.config.StaticPath).Open(path)
if err == nil {
defer info.Close()
stat, _ := info.Stat()
if stat != nil && stat.IsDir() {
// Try to serve index.html from the directory
indexPath := filepath.Join(path, "index.html")
if index, err := http.Dir(app.config.StaticPath).Open(indexPath); err == nil {
index.Close()
http.ServeFile(w, r, filepath.Join(app.config.StaticPath, indexPath))
return
}
}
}
// Serve the file or fall back to SPA index.html
fileServer := http.FileServer(http.Dir(app.config.StaticPath))
fileServer.ServeHTTP(w, r)
}