Claude Code session 1.
This commit is contained in:
222
manager/main.go
222
manager/main.go
@@ -33,6 +33,10 @@ var (
|
||||
m map[string]*Session
|
||||
}{m: make(map[string]*Session)}
|
||||
logger *Logger
|
||||
|
||||
// Rate limiters
|
||||
authRateLimiter *RateLimiter // Aggressive limit for auth endpoints
|
||||
apiRateLimiter *RateLimiter // Moderate limit for API endpoints
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
@@ -49,6 +53,7 @@ func main() {
|
||||
dyfiPass := flag.String("dyfi-pass", os.Getenv("DYFI_PASS"), "dy.fi password")
|
||||
email := flag.String("email", os.Getenv("ACME_EMAIL"), "Email for Let's Encrypt notifications")
|
||||
logFile := flag.String("log", os.Getenv("LOG_FILE"), "Path to log file for fail2ban")
|
||||
enableGateway := flag.Bool("enable-gateway", false, "Enable gateway/proxy mode for external workers")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
@@ -76,6 +81,28 @@ func main() {
|
||||
|
||||
store = NewUserStore("users_data", crypto)
|
||||
|
||||
// Initialize worker store and health poller
|
||||
workerStore = NewWorkerStore("workers_data.json")
|
||||
healthPoller = NewHealthPoller(workerStore, 60*time.Second)
|
||||
healthPoller.Start()
|
||||
logger.Info("Worker health poller started (60s interval)")
|
||||
|
||||
// Initialize gateway components (if enabled)
|
||||
if *enableGateway {
|
||||
apiKeyStore = NewAPIKeyStore("apikeys_data", crypto)
|
||||
proxyManager = NewProxyManager(workerStore)
|
||||
logger.Info("Gateway mode enabled - API key auth and proxy available")
|
||||
} else {
|
||||
logger.Info("Gateway mode disabled (use --enable-gateway to enable)")
|
||||
}
|
||||
|
||||
// Initialize rate limiters
|
||||
// Auth endpoints: 10 requests per minute (aggressive)
|
||||
authRateLimiter = NewRateLimiter(10, 1*time.Minute)
|
||||
// API endpoints: 100 requests per minute (moderate)
|
||||
apiRateLimiter = NewRateLimiter(100, 1*time.Minute)
|
||||
logger.Info("Rate limiters initialized (auth: 10/min, api: 100/min)")
|
||||
|
||||
// --- BACKGROUND TASKS ---
|
||||
// Reload user store from disk periodically
|
||||
go func() {
|
||||
@@ -97,7 +124,7 @@ func main() {
|
||||
|
||||
// dy.fi Dynamic DNS Updater
|
||||
if *domain != "" && *dyfiUser != "" {
|
||||
startDyfiUpdater(*domain, *dyfiUser, *dyfiPass)
|
||||
startDyfiUpdater(*domain, *dyfiUser, *dyfiPass, *port)
|
||||
}
|
||||
|
||||
// --- CLI COMMANDS ---
|
||||
@@ -119,6 +146,13 @@ func main() {
|
||||
// --- ROUTES ---
|
||||
// Routes must be defined BEFORE the server starts
|
||||
|
||||
// Public health endpoint (no auth required) for monitoring and dy.fi failover
|
||||
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"status":"healthy"}`))
|
||||
})
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if session := getValidSession(r, crypto); session != nil {
|
||||
http.Redirect(w, r, "/app", http.StatusSeeOther)
|
||||
@@ -128,6 +162,25 @@ func main() {
|
||||
})
|
||||
|
||||
http.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// Redirect to dashboard
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
http.HandleFunc("/dashboard", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
handleDashboard(w, r)
|
||||
})
|
||||
|
||||
http.HandleFunc("/rest-client", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
@@ -152,6 +205,47 @@ func main() {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
})
|
||||
|
||||
// API: Worker management endpoints
|
||||
http.HandleFunc("/api/workers/list", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
handleAPIWorkersList(w, r)
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/workers/register", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
handleAPIWorkersRegister(w, r)
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/workers/remove", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
handleAPIWorkersRemove(w, r)
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/workers/get", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
handleAPIWorkersGet(w, r)
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/request", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
@@ -177,8 +271,64 @@ func main() {
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
http.HandleFunc("/verify-user", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Gateway endpoints (API key auth) - only if gateway is enabled
|
||||
if *enableGateway {
|
||||
http.HandleFunc("/api/gateway/target", APIKeyAuthMiddleware(apiKeyStore, handleGatewayTarget))
|
||||
http.HandleFunc("/api/gateway/result", APIKeyAuthMiddleware(apiKeyStore, handleGatewayResult))
|
||||
http.HandleFunc("/api/gateway/stats", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
handleGatewayStats(w, r)
|
||||
})
|
||||
|
||||
// API key management endpoints (TOTP auth - admin only)
|
||||
http.HandleFunc("/api/apikeys/generate", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
handleAPIKeyGenerate(w, r)
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/apikeys/list", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
handleAPIKeyList(w, r)
|
||||
})
|
||||
|
||||
http.HandleFunc("/api/apikeys/revoke", func(w http.ResponseWriter, r *http.Request) {
|
||||
session := getValidSession(r, crypto)
|
||||
if session == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
handleAPIKeyRevoke(w, r)
|
||||
})
|
||||
|
||||
logger.Info("Gateway routes registered")
|
||||
}
|
||||
|
||||
http.HandleFunc("/verify-user", RateLimitMiddleware(authRateLimiter, func(w http.ResponseWriter, r *http.Request) {
|
||||
userID := strings.TrimSpace(r.FormValue("userid"))
|
||||
|
||||
// Input validation
|
||||
if !ValidateInput(userID, 100) {
|
||||
logger.Warn("AUTH_FAILURE: Invalid user ID format from IP %s", getIP(r))
|
||||
tmpl.Execute(w, map[string]interface{}{"Step2": false, "Error": "Invalid input"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := store.GetUser(userID)
|
||||
if err != nil || user == nil {
|
||||
// FAIL2BAN TRIGGER
|
||||
@@ -204,9 +354,9 @@ func main() {
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
tmpl.Execute(w, map[string]interface{}{"Step2": true})
|
||||
})
|
||||
}))
|
||||
|
||||
http.HandleFunc("/verify-totp", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.HandleFunc("/verify-totp", RateLimitMiddleware(authRateLimiter, func(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("temp_session")
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
@@ -226,6 +376,13 @@ func main() {
|
||||
user, _ := store.GetUser(session.UserID)
|
||||
totpCode := strings.TrimSpace(r.FormValue("totp"))
|
||||
|
||||
// Input validation for TOTP code
|
||||
if !ValidateInput(totpCode, 10) {
|
||||
logger.Warn("AUTH_FAILURE: Invalid TOTP format for user %s from IP %s", session.UserID, getIP(r))
|
||||
tmpl.Execute(w, map[string]interface{}{"Step2": true, "Error": "Invalid input"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the TOTP code
|
||||
if !totp.Validate(totpCode, user.TOTPSecret) {
|
||||
// --- FAIL2BAN TRIGGER ---
|
||||
@@ -260,7 +417,7 @@ func main() {
|
||||
|
||||
// Redirect to the main application
|
||||
http.Redirect(w, r, "/app", http.StatusSeeOther)
|
||||
})
|
||||
}))
|
||||
|
||||
// --- SERVER STARTUP ---
|
||||
|
||||
@@ -280,12 +437,38 @@ func main() {
|
||||
log.Fatal(http.ListenAndServe(":80", certManager.HTTPHandler(nil)))
|
||||
}()
|
||||
|
||||
// Create base handler with security headers and size limits
|
||||
baseHandler := SecurityHeadersMiddleware(
|
||||
MaxBytesMiddleware(10*1024*1024, http.DefaultServeMux), // 10MB max request size
|
||||
)
|
||||
|
||||
// Configure TLS with strong cipher suites
|
||||
tlsConfig := certManager.TLSConfig()
|
||||
tlsConfig.MinVersion = tls.VersionTLS12
|
||||
tlsConfig.PreferServerCipherSuites = true
|
||||
tlsConfig.CipherSuites = []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + *port,
|
||||
TLSConfig: certManager.TLSConfig(),
|
||||
Addr: ":" + *port,
|
||||
Handler: baseHandler,
|
||||
TLSConfig: tlsConfig,
|
||||
ReadTimeout: 15 * time.Second, // Time to read request headers + body
|
||||
WriteTimeout: 30 * time.Second, // Time to write response
|
||||
IdleTimeout: 120 * time.Second, // Time to keep connection alive
|
||||
// Protect against slowloris attacks
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20, // 1MB max header size
|
||||
}
|
||||
|
||||
logger.Info("Secure Server starting with Let's Encrypt on https://%s", *domain)
|
||||
logger.Info("Security: Rate limiting enabled, headers hardened, timeouts configured")
|
||||
log.Fatal(server.ListenAndServeTLS("", "")) // Certs provided by autocert
|
||||
} else {
|
||||
// Fallback to Self-Signed Certs
|
||||
@@ -295,14 +478,35 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Create base handler with security headers and size limits
|
||||
baseHandler := SecurityHeadersMiddleware(
|
||||
MaxBytesMiddleware(10*1024*1024, http.DefaultServeMux), // 10MB max request size
|
||||
)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + *port,
|
||||
Addr: ":" + *port,
|
||||
Handler: baseHandler,
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
PreferServerCipherSuites: true,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
},
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20, // 1MB
|
||||
}
|
||||
|
||||
logger.Info("Secure Server starting with self-signed certs on https://localhost:%s", *port)
|
||||
logger.Info("Security: Rate limiting enabled, headers hardened, timeouts configured")
|
||||
log.Fatal(server.ListenAndServeTLS(certFile, keyFile))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user