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", "/mcp", "/styleguide", "/pricing", "/dossier/", "/viewer/", "/data/", "/image/", "/contact-sheet.webp/", "/download/", "/api", } 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 { 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") && !strings.HasSuffix(lower, "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 } // 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) 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) } }