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:
+66
-16
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user