Refactor installation and runner scripts for enhanced usability and security

- Updated the installation script to clarify that production wrappers do not include fixed test secrets, improving user guidance for local testing.
- Modified the jiggablend-manager and jiggablend-runner scripts to remove fixed test configurations, emphasizing the use of local test credentials via `make init-test`.
- Enhanced error handling in the runner script to ensure that an API key is provided, improving security and user feedback.
- Added new flags and options for the runner, including sandboxing capabilities and GPU ray tracing control, enhancing flexibility for users.
- Improved README documentation to reflect changes in usage and configuration, ensuring users have clear instructions for setup and execution.
This commit is contained in:
2026-07-12 10:01:15 -05:00
parent a3defe5cf6
commit 1a69fcfd04
47 changed files with 2718 additions and 402 deletions
+66 -16
View File
@@ -173,7 +173,6 @@ func NewManager(db *database.DB, cfg *config.Config, auth *authpkg.Auth, storage
maxUploadSize: cfg.MaxUploadBytes(),
sessionCookieMaxAge: cfg.SessionCookieMaxAgeSec(),
wsUpgrader: websocket.Upgrader{
CheckOrigin: checkWebSocketOrigin,
ReadBufferSize: 1024,
WriteBufferSize: 1024,
},
@@ -199,6 +198,13 @@ func NewManager(db *database.DB, cfg *config.Config, auth *authpkg.Auth, storage
apiRateLimiter = NewRateLimiter(cfg.APIRateLimitPerMinute(), time.Minute)
authRateLimiter = NewRateLimiter(cfg.AuthRateLimitPerMinute(), time.Minute)
// WebSocket origin check uses config production mode (not env-only)
s.wsUpgrader.CheckOrigin = s.checkWebSocketOrigin
// Job tokens must outlive the longest task timeout so long encodes can upload results
ttl := authpkg.ConfigureJobTokenTTLFromTimeouts(cfg.RenderTimeoutSeconds(), cfg.EncodeTimeoutSeconds())
log.Printf("Job token TTL configured to %v (max task timeout + skew)", ttl)
// Check for required external tools
if err := s.checkRequiredTools(); err != nil {
return nil, err
@@ -232,9 +238,9 @@ func (s *Manager) checkRequiredTools() error {
return nil
}
// checkWebSocketOrigin validates WebSocket connection origins
// In production mode, only allows same-origin connections or configured allowed origins
func checkWebSocketOrigin(r *http.Request) bool {
// checkWebSocketOrigin validates WebSocket connection origins using the manager's
// production mode config (single source of truth with cookies/CORS/rate limits).
func (s *Manager) checkWebSocketOrigin(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
// No origin header - allow (could be non-browser client like runner)
@@ -242,14 +248,18 @@ func checkWebSocketOrigin(r *http.Request) bool {
}
// In development mode, allow all origins
// Note: This function doesn't have access to Server, so we use authpkg.IsProductionMode()
// which checks environment variable. The server setup uses s.cfg.IsProductionMode() for consistency.
if !authpkg.IsProductionMode() {
if !s.cfg.IsProductionMode() {
return true
}
// In production, check against allowed origins
allowedOrigins := os.Getenv("ALLOWED_ORIGINS")
// In production, check against configured allowed origins (DB config, then env)
allowedOrigins := ""
if s.cfg != nil {
allowedOrigins = s.cfg.AllowedOrigins()
}
if allowedOrigins == "" {
allowedOrigins = os.Getenv("ALLOWED_ORIGINS")
}
if allowedOrigins == "" {
// Default to same-origin only
host := r.Host
@@ -394,10 +404,23 @@ func rateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
}
}
// securityHeadersMiddleware sets baseline security response headers.
func securityHeadersMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Baseline CSP: allow same-origin scripts/styles for embedded UI
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; frame-ancestors 'none'")
next.ServeHTTP(w, r)
})
}
// setupMiddleware configures middleware
func (s *Manager) setupMiddleware() {
s.router.Use(middleware.Logger)
s.router.Use(middleware.Recoverer)
s.router.Use(securityHeadersMiddleware)
// Note: Timeout middleware is NOT applied globally to avoid conflicts with WebSocket connections
// WebSocket connections are long-lived and should not have HTTP timeouts
@@ -509,7 +532,10 @@ func (s *Manager) setupRoutes() {
r.Post("/local/login", s.handleLocalLogin)
r.Post("/logout", s.handleLogout)
r.Get("/me", s.handleGetMe)
r.Post("/change-password", s.handleChangePassword)
// Password change requires an authenticated session
r.With(func(next http.Handler) http.Handler {
return http.HandlerFunc(s.auth.Middleware(next.ServeHTTP))
}).Post("/change-password", s.handleChangePassword)
})
// Protected routes
@@ -648,7 +674,7 @@ func (s *Manager) createSessionCookie(sessionID string) *http.Cookie {
SameSite: http.SameSiteLaxMode,
}
if authpkg.IsProductionMode() {
if s.cfg.IsProductionMode() {
cookie.Secure = true
}
@@ -726,6 +752,11 @@ func (s *Manager) handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
s.respondError(w, http.StatusBadRequest, "Missing code parameter")
return
}
state := r.URL.Query().Get("state")
if !s.auth.ConsumeOAuthState(state) {
s.respondError(w, http.StatusBadRequest, "Invalid or missing OAuth state")
return
}
session, err := s.auth.GoogleCallback(r.Context(), code)
if err != nil {
@@ -734,6 +765,10 @@ func (s *Manager) handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/?error=registration_disabled", http.StatusFound)
return
}
if strings.Contains(err.Error(), "already exists") {
http.Redirect(w, r, "/?error=account_exists", http.StatusFound)
return
}
s.respondError(w, http.StatusInternalServerError, err.Error())
return
}
@@ -759,6 +794,11 @@ func (s *Manager) handleDiscordCallback(w http.ResponseWriter, r *http.Request)
s.respondError(w, http.StatusBadRequest, "Missing code parameter")
return
}
state := r.URL.Query().Get("state")
if !s.auth.ConsumeOAuthState(state) {
s.respondError(w, http.StatusBadRequest, "Invalid or missing OAuth state")
return
}
session, err := s.auth.DiscordCallback(r.Context(), code)
if err != nil {
@@ -767,6 +807,10 @@ func (s *Manager) handleDiscordCallback(w http.ResponseWriter, r *http.Request)
http.Redirect(w, r, "/?error=registration_disabled", http.StatusFound)
return
}
if strings.Contains(err.Error(), "already exists") {
http.Redirect(w, r, "/?error=account_exists", http.StatusFound)
return
}
s.respondError(w, http.StatusInternalServerError, err.Error())
return
}
@@ -1269,11 +1313,17 @@ func (s *Manager) cleanupOldTempDirectoriesOnce() {
now := time.Now()
cleanedCount := 0
// Check upload sessions to avoid deleting active uploads
// Check upload sessions to avoid deleting active uploads.
// Key by TempDir path (and basename) — not session UUID.
s.uploadSessionsMu.RLock()
activeSessions := make(map[string]bool)
for sessionID := range s.uploadSessions {
activeSessions[sessionID] = true
activeTempDirs := make(map[string]bool)
for _, session := range s.uploadSessions {
if session == nil || session.TempDir == "" {
continue
}
clean := filepath.Clean(session.TempDir)
activeTempDirs[clean] = true
activeTempDirs[filepath.Base(clean)] = true
}
s.uploadSessionsMu.RUnlock()
@@ -1285,7 +1335,7 @@ func (s *Manager) cleanupOldTempDirectoriesOnce() {
entryPath := filepath.Join(tempPath, entry.Name())
// Skip if this directory has an active upload session
if activeSessions[entryPath] {
if activeTempDirs[filepath.Clean(entryPath)] || activeTempDirs[entry.Name()] {
continue
}