package main import ( "crypto/rand" "encoding/json" "fmt" "html/template" "log" "net" "net/http" "os" "path/filepath" "strconv" "strings" "sync" "time" ) const ( passwordLength = 32 chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" startTimeKey contextKey = "startTime" ) var ( debug = false templates = make(map[string]*template.Template) AppVersion = "development" counterFile = "/data/counter.txt" mu sync.Mutex ) type contextKey string type responseWriter struct { http.ResponseWriter statusCode int } func (rw *responseWriter) WriteHeader(code int) { rw.statusCode = code rw.ResponseWriter.WriteHeader(code) } func newResponseWriter(w http.ResponseWriter) *responseWriter { return &responseWriter{w, http.StatusOK} // Default 200 OK } func initConfig() { if envFile := os.Getenv("COUNTER_FILE"); envFile != "" { counterFile = envFile log.Printf("counterFile st to %s, by ENV\n", envFile) // Prüfen, ob das Verzeichnis für die Datei existiert dir := filepath.Dir(counterFile) if _, err := os.Stat(dir); os.IsNotExist(err) { log.Printf("WARNUNG: Verzeichnis %s existiert nicht. Counter wird evtl. fehlschlagen.", dir) } } envDebug := strings.ToLower(os.Getenv("DEBUG")) if envDebug == "true" || envDebug == "1" { debug = true log.Println("DEBUG-Modus ist aktiviert") } } func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rw := newResponseWriter(w) next.ServeHTTP(rw, r) duration := time.Since(start) clientIP := getClientIP(r) userAgent := r.UserAgent() // Format: IP - - [Datum] "Method Path Proto" Status Duration User-Agent // "Combined Log Format" log.Printf("%s - - [%s] \"%s %s %s\" %d %v \"%s\"\n", clientIP, time.Now().Format("02/Jan/2006:15:04:05 -0700"), r.Method, r.URL.Path, r.Proto, rw.statusCode, duration, userAgent, ) }) } func getClientIP(r *http.Request) string { // 1. Prüfe den X-Forwarded-For Header (Standard für Proxies) xForwardedFor := r.Header.Get("X-Forwarded-For") if xForwardedFor != "" { // Der Header kann eine Liste von IPs sein (Client, Proxy1, Proxy2) // Die erste IP in der Liste ist die echte Client-IP ips := strings.Split(xForwardedFor, ",") return strings.TrimSpace(ips[0]) } // 2. Fallback auf X-Real-IP (oft von Traefik/Nginx gesetzt) xRealIP := r.Header.Get("X-Real-IP") if xRealIP != "" { return xRealIP } // 3. Letzter Ausweg: Die direkte IP (wird in deinem Fall die Traefik-IP sein) // RemoteAddr enthält oft auch den Port (z.B. "127.0.0.1:1234") ip, _, _ := net.SplitHostPort(r.RemoteAddr) return ip } // Diese Funktion wird nur intern aufgerufen, wenn der Mutex bereits gesperrt ist func getCount() int { data, err := os.ReadFile(counterFile) if err != nil { return 0 } count, _ := strconv.Atoi(strings.TrimSpace(string(data))) return count } // Öffentliche Funktion für das Template (mit Lock) func GetPasswordCount() int { mu.Lock() defer mu.Unlock() return getCount() } // Öffentliche Funktion zum Erhöhen (mit Lock) func IncrementPasswordCount() { mu.Lock() defer mu.Unlock() // Wir rufen jetzt die interne Funktion auf, die NICHT versucht, // den Mutex erneut zu sperren count := getCount() count++ os.WriteFile(counterFile, []byte(strconv.Itoa(count)), 0644) } func loadTemplates() { funcMap := template.FuncMap{ "getAppVersion": func() string { return AppVersion }, "getPassCount": func() int { return GetPasswordCount() }, "isDebug": func() bool { return debug }, "dt": func(startTime time.Time) string { duration := time.Since(startTime) // Gibt die Zeit in Millisekunden mit 2 Nachkommastellen aus, z.B. "1.45ms" return fmt.Sprintf("%.2fms", float64(duration.Nanoseconds())/1e6) }, } templates["index.html"] = template.Must(template.New("base.html").Funcs(funcMap).ParseFiles( "templates/base.html", "templates/index.html", )) templates["help.html"] = template.Must(template.New("base.html").Funcs(funcMap).ParseFiles( "templates/base.html", "templates/help.html", )) log.Printf("Alle Templates erfolgreich geladen") } func generatePassword() string { if debug { log.Printf("called generatePassword\n") } password := make([]byte, passwordLength) _, err := rand.Read(password) if err != nil { log.Fatal(err) } for i := 0; i < passwordLength; i++ { password[i] = chars[int(password[i])%len(chars)] } IncrementPasswordCount() return string(password) } func passwordHandler(w http.ResponseWriter, r *http.Request) { if debug { log.Printf("called passwordHandler\n") } password := generatePassword() currentCount := GetPasswordCount() response := map[string]interface{}{ "password": password, "count": currentCount, } w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(response) if err != nil { log.Printf("Fehler beim Senden des JSON: %v", err) http.Error(w, "Interner Fehler", http.StatusInternalServerError) return } } func passwordAPIHandler(w http.ResponseWriter, r *http.Request) { if debug { log.Printf("called passwordHandler\n") } password := generatePassword() w.Header().Set("Content-Type", "text/plain") w.Write([]byte(password)) } func indexHandler(w http.ResponseWriter, r *http.Request) { if debug { log.Printf("call indexHandler: Request %s %s\n", r.Method, r.URL) } startTime, ok := r.Context().Value(startTimeKey).(time.Time) if !ok { startTime = time.Now() // Fallback, falls die Middleware mal fehlt } password := generatePassword() data := struct { Password string StartTime time.Time Request *http.Request RealIP string }{ Password: password, StartTime: startTime, Request: r, RealIP: getClientIP(r), } if debug { log.Printf("prepare template for index\n") } err := templates["index.html"].ExecuteTemplate(w, "base.html", data) if err != nil { log.Printf("Fehler beim Rendern des Templates: %v", err) http.Error(w, "Interner Serverfehler", http.StatusInternalServerError) } } func helpHandler(w http.ResponseWriter, r *http.Request) { if debug { log.Printf("call helpHandler\n") } err := templates["help.html"].ExecuteTemplate(w, "base.html", nil) if err != nil { log.Printf("Fehler beim Rendern des Templates: %v", err) http.Error(w, "Interner Serverfehler", http.StatusInternalServerError) } } func main() { initConfig() loadTemplates() mux := http.NewServeMux() fs := http.FileServer(http.Dir("static")) mux.Handle("/static/", http.StripPrefix("/static/", fs)) mux.HandleFunc("/", indexHandler) mux.HandleFunc("/api/password", passwordAPIHandler) mux.HandleFunc("/json/password", passwordHandler) mux.HandleFunc("/help", helpHandler) loggingRouter := LoggingMiddleware(mux) log.Println("Server läuft auf http://localhost:8080") log.Fatal(http.ListenAndServe(":8080", loggingRouter)) }