package main import ( "fmt" "inou/lib" "net/http" "regexp" "strings" "time" ) // Matches 16-char hex IDs (inou format) or standard UUIDs var guidPattern = regexp.MustCompile(`[0-9a-fA-F]{16}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`) const tarpitDuration = 30 * time.Second var tarpitPatterns = []string{ // PHP/CMS "php", "wp-", "xmlrpc", "phpmyadmin", "pma", "adminer", "joomla", "drupal", "magento", "administrator", // Config/Secrets ".env", ".git", ".svn", ".htaccess", ".htpasswd", ".aws", ".docker", ".bak", ".sql", // Java/Spring "actuator", "jolokia", "console", // ColdFusion "cfide", "cf_scripts", // Shells/Exploits "shell", "cgi-bin", "elfinder", "filemanager", "fckeditor", // Infrastructure "/manager/", "jenkins", "jmx-console", "web-console", "solr", "telescope", "elmah", // Network devices "goform", "hnap", "stssys", } var validPaths = []string{ "/", "/static/", "/robots.txt", "/sitemap.xml", "/favicon.ico", "/apple-touch-icon.png", "/apple-touch-icon-precomposed.png", "/app-ads.txt", "/llms.txt", "/send-code", "/verify", "/onboard", "/logout", "/dashboard", "/connect", "/install", "/start", "/invite", "/set-lang", "/privacy-policy", "/security", "/legal/dpa", "/legal/terms", "/demo", "/oauth/authorize", "/oauth/token", "/oauth/userinfo", "/oauth/revoke", "/.well-known/", "/register", "/styleguide", "/pricing", "/faq", "/dossier/", "/viewer/", "/data/", "/image/", "/contact-sheet.webp/", "/download/", // API - specific routes only (no broad /api prefix) "/api/docs", "/api/openapi.yaml", "/api/token/", "/api/version", "/api/dossier", // covers /api/dossier and /api/dossiers "/api/studies", "/api/series", "/api/slices", "/api/labs/", "/api/access", "/api/audit", "/api/entries", "/api/trackers", "/api/v1/auth/", "/api/v1/dashboard", "/api/v1/trackers", "/api/v1/health", "/api/v1/token", "/api/v1/dossiers", "/api/v1/images/", "/api/v1/categories", } var whitelistedIPs = map[string]bool{ "82.22.36.202": true, // our vulnerability scanner } func isLocalIP(ip string) bool { return strings.HasPrefix(ip, "192.168.") } func getIP(r *http.Request) string { if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { return strings.Split(fwd, ",")[0] } return strings.Split(r.RemoteAddr, ":")[0] } func isValidPath(path string) bool { if path == "/api" || path == "/mcp" { return true } for _, p := range validPaths { if p == "/" && path == "/" { return true } if p != "/" && strings.HasPrefix(path, p) { return true } } return false } func shouldTarpit(path string) bool { lower := strings.ToLower(path) if strings.HasSuffix(lower, ".js") && lower != "/static/upload.js" && lower != "/static/carousel.js" && lower != "/static/viewer.js" { return true } for _, pattern := range tarpitPatterns { if strings.Contains(lower, pattern) { return true } } return false } // hasGUID checks if path or query contains a GUID-like token // This indicates a legitimate client with invalid/expired credentials func hasGUID(r *http.Request) bool { // Check path if guidPattern.MatchString(r.URL.Path) { return true } // Check query params (token=xxx, dossier=xxx, etc.) if guidPattern.MatchString(r.URL.RawQuery) { return true } // Check Authorization header if auth := r.Header.Get("Authorization"); auth != "" { if guidPattern.MatchString(auth) { return true } } return false } // tarpit holds connection silent then closes - no HTTP response func tarpit(w http.ResponseWriter, r *http.Request, label string) { ip := getIP(r) logAccess("\033[33m%s %s %s %s %s\033[0m", time.Now().Format("2006-01-02 15:04:05"), ip, label, r.Method, r.URL.RequestURI()) w.WriteHeader(444) } func defense(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path ip := getIP(r) // Known bad patterns - tarpit immediately (unless GUID present) if shouldTarpit(strings.ToLower(path)) { if hasGUID(r) { // Looks like a real client with bad/expired token - give proper error logAccess("\033[33m%s %s INVALID_TOKEN %s %s\033[0m", time.Now().Format("2006-01-02 15:04:05"), ip, r.Method, r.URL.RequestURI()) http.Error(w, "Invalid or expired token", http.StatusUnauthorized) return } tarpit(w, r, "TARPIT") return } // MCP without Bearer token - not a legitimate client if path == "/mcp" && !isLocalIP(ip) { if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") { tarpit(w, r, "MCP_NO_AUTH") return } } // Unknown paths from non-local IPs if !isValidPath(path) && !isLocalIP(ip) { if hasGUID(r) { // Looks like a real client with bad/expired token - give proper error logAccess("\033[33m%s %s INVALID_TOKEN %s %s\033[0m", time.Now().Format("2006-01-02 15:04:05"), ip, r.Method, r.URL.RequestURI()) http.Error(w, "Invalid or expired token", http.StatusUnauthorized) return } tarpit(w, r, "PROBE") return } next.ServeHTTP(w, r) }) } type statusCapture struct { http.ResponseWriter status int r *http.Request } func (s *statusCapture) WriteHeader(code int) { s.status = code if code == 404 && s.r.URL.Path != "/favicon.ico" { ip := getIP(s.r) if !whitelistedIPs[ip] { lib.SendSignal(fmt.Sprintf("404: %s %s", ip, s.r.URL.Path)) } } s.ResponseWriter.WriteHeader(code) } func notifyOn404(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { capture := &statusCapture{ResponseWriter: w, status: 200, r: r} next.ServeHTTP(capture, r) }) } func normalizeQuery(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { raw := r.URL.RawQuery raw = strings.ReplaceAll(raw, "&%253B", "&") raw = strings.ReplaceAll(raw, "&%3B", "&") raw = strings.ReplaceAll(raw, "&", "&") raw = strings.ReplaceAll(raw, "%26amp%3B", "&") raw = strings.ReplaceAll(raw, "%26amp;", "&") r.URL.RawQuery = raw next(w, r) } } // httpsRedirectMiddleware redirects HTTP requests to HTTPS // Respects X-Forwarded-Proto header for deployments behind reverse proxy func httpsRedirectMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Skip redirect for health checks and local development if r.URL.Path == "/api/v1/health" || isLocalIP(getIP(r)) { next.ServeHTTP(w, r) return } // Check if request is already HTTPS (direct or via proxy) isHTTPS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" if !isHTTPS { // Build HTTPS URL target := "https://" + r.Host + r.URL.RequestURI() http.Redirect(w, r, target, http.StatusMovedPermanently) return } next.ServeHTTP(w, r) }) } // hstsMiddleware adds HTTP Strict Transport Security headers func hstsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Only add HSTS header for HTTPS requests isHTTPS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" if isHTTPS { // max-age=1 year (31536000 seconds), include subdomains, preload ready w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") } next.ServeHTTP(w, r) }) }