mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-01 10:35:56 +00:00
219 lines
6.3 KiB
Go
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)
|
|
}
|