fix: harden DynDNS recovery and publish v1.5.1
This commit is contained in:
1 parent
3383b91e49
commit
81def77e3c
37 files changed
+2415
-250
No files matched your search
@@ -0,0 +1,109 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/model"
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/store"
|
||||
)
|
||||
|
||||
func TestPiHolePageAndRemovalNeverContactUnavailablePiHole(t *testing.T) {
|
||||
t.Parallel()
|
||||
var requests atomic.Int32
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requests.Add(1); http.Error(w, "unavailable", 503) }))
|
||||
defer api.Close()
|
||||
ctx := context.Background()
|
||||
database, err := store.Open(ctx, filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer database.Close()
|
||||
integration, err := database.CreatePiHoleIntegration(ctx, model.PiHoleIntegration{Name: "Pi-hole", BaseURL: api.URL + "/api"}, model.PiHoleCredential{Ciphertext: []byte("secret"), Nonce: []byte("secret")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = database.SyncPiHoleRecordLinks(ctx, integration.ID, []string{"192.168.1.10 nas.lan"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = database.MarkPiHoleSynced(ctx, integration.ID, "connection timed out"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
templates, err := loadTemplates()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := &Server{store: database, templates: templates, logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
|
||||
session := &model.Session{CSRFToken: "csrf", Username: "admin", Role: "admin"}
|
||||
response := httptest.NewRecorder()
|
||||
server.piHolePage(response, httptest.NewRequest(http.MethodGet, "/pihole", nil), session)
|
||||
if response.Code != 200 || !strings.Contains(response.Body.String(), "nas.lan") || !strings.Contains(response.Body.String(), "Lokale Verbindung entfernen") {
|
||||
t.Fatalf("offline page unavailable: %d", response.Code)
|
||||
}
|
||||
id := strconv.FormatInt(integration.ID, 10)
|
||||
request := httptest.NewRequest(http.MethodPost, "/pihole/"+id+"/remove", strings.NewReader(url.Values{"csrf": {"csrf"}}.Encode()))
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
request.SetPathValue("id", id)
|
||||
response = httptest.NewRecorder()
|
||||
server.piHoleRemove(response, request, session)
|
||||
if response.Code != http.StatusSeeOther {
|
||||
t.Fatalf("offline removal failed: %d", response.Code)
|
||||
}
|
||||
integrations, err := database.ListPiHoleIntegrations(ctx)
|
||||
if err != nil || len(integrations) != 0 {
|
||||
t.Fatal("connection not removed")
|
||||
}
|
||||
if requests.Load() != 0 {
|
||||
t.Fatalf("offline page/removal contacted Pi-hole %d times", requests.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiagnosticDownloadExcludesStoredSecretsAndFreeText(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
database, err := store.Open(ctx, filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer database.Close()
|
||||
const secret = "git-hetzner-pihole-cookie-password-secret"
|
||||
if err = database.SaveSoftwareUpdateCredential(ctx, secret, []byte(secret), []byte(secret)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = database.AddAudit(ctx, "arbitrary", secret); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id, err := database.CreateUpdateRun(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = database.FinishUpdateRun(ctx, id, "error", secret, secret, "https://user:"+secret+"@host/?token="+secret); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server := &Server{store: database, logger: slog.New(slog.NewTextHandler(io.Discard, nil))}
|
||||
response := httptest.NewRecorder()
|
||||
server.downloadDiagnostics(response, httptest.NewRequest(http.MethodGet, "/errors/diagnostics", nil), &model.Session{Role: "admin"})
|
||||
if response.Code != 200 || !strings.Contains(response.Header().Get("Content-Disposition"), "attachment") || response.Header().Get("Cache-Control") != "no-store" {
|
||||
t.Fatalf("invalid download: %d", response.Code)
|
||||
}
|
||||
if strings.Contains(response.Body.String(), secret) {
|
||||
t.Fatal("secret included in download")
|
||||
}
|
||||
if !strings.Contains(response.Body.String(), `"status": "error"`) {
|
||||
t.Fatal("run status missing")
|
||||
}
|
||||
anonymous := httptest.NewRecorder()
|
||||
server.Handler().ServeHTTP(anonymous, httptest.NewRequest(http.MethodGet, "/errors/diagnostics", nil))
|
||||
if anonymous.Code == 200 {
|
||||
t.Fatal("anonymous diagnostic access accepted")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user