2026-05-06 00:11:40 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/subtle"
|
2026-05-06 01:45:38 +02:00
|
|
|
"log/slog"
|
2026-05-06 00:11:40 +02:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httputil"
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
2026-05-06 01:40:28 +02:00
|
|
|
"strings"
|
2026-05-06 00:11:40 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func main() {
|
2026-05-06 01:48:00 +02:00
|
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
2026-05-06 01:45:38 +02:00
|
|
|
slog.SetDefault(logger)
|
|
|
|
|
|
2026-05-06 00:11:40 +02:00
|
|
|
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 {
|
2026-05-06 01:45:38 +02:00
|
|
|
slog.Error("invalid PZ8_RELAY_TARGET_URL", "err", err)
|
|
|
|
|
os.Exit(1)
|
2026-05-06 00:11:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-05-06 01:45:38 +02:00
|
|
|
slog.Warn("unauthorized request", "client", clientIP(r))
|
2026-05-06 00:11:40 +02:00
|
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="pz8-relay"`)
|
|
|
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
proxy.ServeHTTP(w, r)
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-06 01:45:38 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2026-05-06 00:11:40 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-06 01:40:28 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 00:11:40 +02:00
|
|
|
func mustEnv(key string) string {
|
|
|
|
|
v := os.Getenv(key)
|
|
|
|
|
if v == "" {
|
2026-05-06 01:45:38 +02:00
|
|
|
slog.Error("missing required env var", "name", key)
|
|
|
|
|
os.Exit(1)
|
2026-05-06 00:11:40 +02:00
|
|
|
}
|
|
|
|
|
return v
|
|
|
|
|
}
|