package main import ( "crypto/subtle" "log/slog" "net/http" "net/http/httputil" "net/url" "os" "strings" ) func main() { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) slog.SetDefault(logger) target := mustEnv("PZ8_RELAY_TARGET_URL") username := mustEnv("PZ8_RELAY_USERNAME") password := mustEnv("PZ8_RELAY_PASSWORD") addr := os.Getenv("PZ8_RELAY_LISTEN_ADDR") if addr == "" { addr = ":8080" } targetURL, err := url.Parse(target) if err != nil { slog.Error("invalid PZ8_RELAY_TARGET_URL", "err", err) os.Exit(1) } proxy := &httputil.ReverseProxy{ Rewrite: func(preq *httputil.ProxyRequest) { preq.SetURL(targetURL) }, } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { u, p, ok := r.BasicAuth() if !ok || subtle.ConstantTimeCompare([]byte(u), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(p), []byte(password)) != 1 { slog.Warn("unauthorized request", "client", clientIP(r)) w.Header().Set("WWW-Authenticate", `Basic realm="pz8-relay"`) http.Error(w, "unauthorized", http.StatusUnauthorized) return } proxy.ServeHTTP(w, r) }) slog.Info("pz8-relay listening", "addr", addr, "target", targetURL.Redacted()) if err := http.ListenAndServe(addr, nil); err != nil { slog.Error("server stopped", "err", err) os.Exit(1) } } // clientIP returns the original client address, trusting X-Forwarded-For // because this service runs behind Traefik. Do not expose directly. func clientIP(r *http.Request) string { if xff := r.Header.Get("X-Forwarded-For"); xff != "" { if i := strings.IndexByte(xff, ','); i >= 0 { return strings.TrimSpace(xff[:i]) } return strings.TrimSpace(xff) } return r.RemoteAddr } func mustEnv(key string) string { v := os.Getenv(key) if v == "" { slog.Error("missing required env var", "name", key) os.Exit(1) } return v }