internal/server/proxy_test.go
Rama main · réplica de lectura del repositorio original.
package server
import (
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"strings"
"testing"
"time"
"armdash/internal/auth"
"armdash/internal/config"
)
func TestClientsBehindATrustedProxyAreCountedApart(t *testing.T) {
t.Setenv("AD_CORE_TRUSTED_PROXIES", "127.0.0.1, ::1")
s := newAuthServer(t)
s.limiter = auth.NewLimiter(2, time.Minute, time.Minute)
from := func(client, password string) *http.Request {
r := loginRequest("owner", password, "/")
r.RemoteAddr = "127.0.0.1:40000"
r.Header.Set("X-Forwarded-For", client)
return r
}
do(s, from("203.0.113.7", "wrong horse"))
do(s, from("203.0.113.7", "wrong horse"))
if rec := do(s, from("203.0.113.7", "correct horse")); rec.Code != http.StatusTooManyRequests {
t.Fatalf("the locked client = %d, want 429", rec.Code)
}
if rec := do(s, from("203.0.113.8", "correct horse")); rec.Code != http.StatusSeeOther {
t.Errorf("another client behind the same proxy = %d, want it unaffected", rec.Code)
}
if rec := do(s, from("203.0.113.8, 203.0.113.7", "correct horse")); rec.Code != http.StatusTooManyRequests {
t.Errorf("a client prepending an address got around the lock: %d", rec.Code)
}
}
func TestSessionCookieFollowsATrustedProxysScheme(t *testing.T) {
t.Setenv("AD_CORE_TRUSTED_PROXIES", "127.0.0.1")
s := newAuthServer(t)
for _, c := range []struct {
peer, proto string
secure bool
}{
{"127.0.0.1:40000", "https", true},
{"127.0.0.1:40000", "http", false},
{"127.0.0.1:40000", "wss", false},
{"192.168.178.20:5555", "https", false},
} {
r := loginRequest("owner", "correct horse", "/")
r.RemoteAddr = c.peer
r.Header.Set("X-Forwarded-Proto", c.proto)
if got := sessionFrom(t, do(s, r)).Secure; got != c.secure {
t.Errorf("%s from %s: Secure = %v, want %v", c.proto, c.peer, got, c.secure)
}
}
}
func TestBadTrustedProxiesStopStartup(t *testing.T) {
t.Setenv("AD_CORE_TRUSTED_PROXIES", "127.0.0.1,caddy")
cfg, err := config.Load()
if err != nil {
t.Fatal(err)
}
_, err = New(cfg, slog.New(slog.NewTextHandler(io.Discard, nil)), nil)
if err == nil || !strings.Contains(err.Error(), "AD_CORE_TRUSTED_PROXIES") || !strings.Contains(err.Error(), "caddy") {
t.Errorf("err = %v, want it to name the variable and the entry", err)
}
}
func TestFramingIsSavedWithALink(t *testing.T) {
s, _ := newStoreServer(t)
c := setUp(t, s)
form := url.Values{"rows": {"1"}, "new.0": {"1"}, "title.0": {"Bugs"}, "url.0": {"http://127.0.0.1:8000"},
"mode.0": {"proxy"}, "framing.0": {"sameorigin"}}
if rec := do(s, post("/settings/links", form), c); rec.Code != http.StatusSeeOther {
t.Fatalf("saving = %d: %s", rec.Code, rec.Body.String())
}
if got := s.cfg.Get("link.bugs.framing"); got != "sameorigin" {
t.Errorf("framing = %q", got)
}
if body := do(s, httptest.NewRequest(http.MethodGet, "/settings", nil), c).Body.String(); !strings.Contains(body, `value="sameorigin" title="Allow framing by this page only, proxy mode"
aria-label="Allow framing" checked`) {
t.Error("the settings page does not show framing ticked")
}
form.Set("mode.0", "frame")
form.Del("new.0")
form.Set("id.0", "bugs")
if rec := do(s, post("/settings/links", form), c); rec.Code != http.StatusUnprocessableEntity {
t.Errorf("framing on a framed link = %d, want 422", rec.Code)
}
}
// newOpenServer has the login off and a fresh data directory at path.
func newOpenServer(t *testing.T, path string) *Server {
t.Helper()
t.Setenv("AD_CORE_AUTH_MODE", "none")
t.Setenv("AD_CORE_AUTH_USER", "")
t.Setenv("AD_CORE_AUTH_PASSWORD_HASH", "")
cfg, err := config.Load()
if err != nil {
t.Fatal(err)
}
if err := cfg.UseStore(path); err != nil {
t.Fatal(err)
}
s, err := New(cfg, slog.New(slog.NewTextHandler(io.Discard, nil)), nil)
if err != nil {
t.Fatal(err)
}
return s
}
func TestLoginOffLetsEveryoneEdit(t *testing.T) {
s := newOpenServer(t, filepath.Join(t.TempDir(), "settings.json"))
if !s.LoginOff() {
t.Fatal("LoginOff = false")
}
if rec := serve(s, http.MethodGet, "/s/nothing/"); rec.Header().Get("Location") == "/settings" {
t.Error("a fresh data directory still asks for a login to be set up")
}
page := serve(s, http.MethodGet, "/settings")
if page.Code != http.StatusOK {
t.Fatalf("settings = %d", page.Code)
}
body := page.Body.String()
for _, unwanted := range []string{">Log in<", "Log out", `action="/settings/setup"`, `action="/settings/login"`} {
if strings.Contains(body, unwanted) {
t.Errorf("settings show %s", unwanted)
}
}
if !strings.Contains(body, "set by AD_CORE_AUTH_MODE") {
t.Error("the Login box does not say the login is off")
}
form := url.Values{"core.prometheus_url": {"http://127.0.0.1:9090"}}
if rec := do(s, post("/settings/core", form)); rec.Code != http.StatusSeeOther {
t.Fatalf("saving without a cookie = %d: %s", rec.Code, rec.Body.String())
}
if s.cfg.Get("core.prometheus_url") != "http://127.0.0.1:9090" {
t.Error("the save did not apply")
}
ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })
rec := httptest.NewRecorder()
s.api("fritzhome", ok).ServeHTTP(rec, s.withSession(httptest.NewRequest(http.MethodPost, "/s/fritzhome/api/positions", nil)))
if rec.Code != http.StatusNoContent {
t.Errorf("a system write without a cookie = %d, want it through", rec.Code)
}
if rec := serve(s, http.MethodGet, "/settings/status"); rec.Code != http.StatusOK {
t.Errorf("status = %d", rec.Code)
}
}
// Without a login the cross-origin check is all that stops another site from
// posting to the settings through the visitor's browser.
func TestLoginOffStillRefusesCrossSiteWrites(t *testing.T) {
s := newOpenServer(t, filepath.Join(t.TempDir(), "settings.json"))
form := url.Values{"core.prometheus_url": {"http://evil.example"}}
r := post("/settings/core", form)
r.Header.Set("Sec-Fetch-Site", "cross-site")
if rec := do(s, r); rec.Code != http.StatusForbidden {
t.Errorf("cross-site save = %d, want 403", rec.Code)
}
r = post("http://admin.example/settings/core", form)
r.Header.Set("Origin", "https://evil.example")
if rec := do(s, r); rec.Code != http.StatusForbidden {
t.Errorf("save from a foreign Origin = %d, want 403", rec.Code)
}
api := httptest.NewRequest(http.MethodPost, "/s/fritzhome/api/positions", nil)
api.Header.Set("Sec-Fetch-Site", "cross-site")
rec := httptest.NewRecorder()
s.api("fritzhome", http.NotFoundHandler()).ServeHTTP(rec, s.withSession(api))
if rec.Code != http.StatusForbidden {
t.Errorf("cross-site system write = %d, want 403", rec.Code)
}
if s.cfg.Has("core.prometheus_url") && s.cfg.Get("core.prometheus_url") == "http://evil.example" {
t.Error("a cross-site save went through")
}
}
func TestLoginOffRemovesTheLoginEndpoints(t *testing.T) {
s := newOpenServer(t, filepath.Join(t.TempDir(), "settings.json"))
if rec := serve(s, http.MethodGet, "/login"); rec.Code != http.StatusFound || rec.Header().Get("Location") != "/" {
t.Errorf("GET /login = %d to %q, want a redirect to /", rec.Code, rec.Header().Get("Location"))
}
for _, path := range []string{"/login", "/logout", "/settings/login", "/settings/setup"} {
if rec := do(s, post(path, url.Values{"user": {"x"}, "password": {"correct horse"}, "confirm": {"correct horse"}})); rec.Code != http.StatusNotFound {
t.Errorf("POST %s = %d, want 404", path, rec.Code)
}
}
}
func TestLoginOffKeepsTheStoredLogin(t *testing.T) {
s, path := newStoreServer(t)
setUp(t, s)
open := newOpenServer(t, path)
if open.hasLogin() {
t.Error("the stored login is in force with the login off")
}
if body := serve(open, http.MethodGet, "/settings").Body.String(); !strings.Contains(body, "The stored login, owner, is kept") {
t.Error("the settings page does not mention the kept login")
}
t.Setenv("AD_CORE_AUTH_MODE", "login")
cfg, err := config.Load()
if err != nil {
t.Fatal(err)
}
if err := cfg.UseStore(path); err != nil {
t.Fatal(err)
}
back, err := New(cfg, slog.New(slog.NewTextHandler(io.Discard, nil)), nil)
if err != nil {
t.Fatal(err)
}
if rec := serve(back, http.MethodGet, "/settings"); rec.Code != http.StatusFound {
t.Errorf("settings after switching back = %d, want the login redirect", rec.Code)
}
}
func TestAuthModeConflictsStopStartup(t *testing.T) {
for name, env := range map[string]map[string]string{
"unknown mode": {"AD_CORE_AUTH_MODE": "off"},
"none with an env user": {"AD_CORE_AUTH_MODE": "none", "AD_CORE_AUTH_USER": "owner"},
"none with an env hash": {"AD_CORE_AUTH_MODE": "none", "AD_CORE_AUTH_PASSWORD_HASH": testHash()},
} {
t.Run(name, func(t *testing.T) {
t.Setenv("AD_CORE_AUTH_USER", "")
t.Setenv("AD_CORE_AUTH_PASSWORD_HASH", "")
for k, v := range env {
t.Setenv(k, v)
}
cfg, err := config.Load()
if err != nil {
t.Fatal(err)
}
if _, err := New(cfg, slog.New(slog.NewTextHandler(io.Discard, nil)), nil); err == nil {
t.Error("startup succeeded")
}
})
}
}