Ctrl K
ComunidadGuías…

internal/server/proxy_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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")
			}
		})
	}
}