651 lines
19 KiB
Go
651 lines
19 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"pdns_admin/internal/appdb"
|
|
"pdns_admin/internal/pdns"
|
|
)
|
|
|
|
type fakeClient struct {
|
|
deletedRRSetType string
|
|
createdRRSet pdns.RRSet
|
|
replacedRRSet pdns.RRSet
|
|
replacedZoneID string
|
|
zone pdns.Zone
|
|
}
|
|
|
|
type fakeAuth struct {
|
|
allowed bool
|
|
err error
|
|
username string
|
|
password string
|
|
}
|
|
|
|
func (f *fakeAuth) Authenticate(_ context.Context, username, password string) (bool, error) {
|
|
f.username = username
|
|
f.password = password
|
|
return f.allowed, f.err
|
|
}
|
|
|
|
func (fakeClient) GetServer(context.Context) (pdns.Server, error) {
|
|
return pdns.Server{}, nil
|
|
}
|
|
|
|
func (fakeClient) ListZones(context.Context) ([]pdns.Zone, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (fakeClient) CreateZone(context.Context, pdns.Zone) (pdns.Zone, error) {
|
|
return pdns.Zone{}, nil
|
|
}
|
|
|
|
func (fakeClient) DeleteZone(context.Context, string) error {
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeClient) GetZone(context.Context, string) (pdns.Zone, error) {
|
|
return f.zone, nil
|
|
}
|
|
|
|
func (f *fakeClient) CreateRRSet(_ context.Context, _ string, rrset pdns.RRSet) error {
|
|
f.createdRRSet = rrset
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeClient) ReplaceRRSet(_ context.Context, zoneID string, rrset pdns.RRSet) error {
|
|
f.replacedZoneID = zoneID
|
|
f.replacedRRSet = rrset
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeClient) DeleteRRSet(_ context.Context, _, _, recordType string) error {
|
|
f.deletedRRSetType = recordType
|
|
return nil
|
|
}
|
|
|
|
type fakeDynamicStore struct {
|
|
records map[string]appdb.DynamicRecord
|
|
}
|
|
|
|
func (f *fakeDynamicStore) ListDynamicRecords(context.Context, string) ([]appdb.DynamicRecord, error) {
|
|
records := make([]appdb.DynamicRecord, 0, len(f.records))
|
|
for _, record := range f.records {
|
|
if record.Enabled {
|
|
records = append(records, record)
|
|
}
|
|
}
|
|
return records, nil
|
|
}
|
|
|
|
func (f *fakeDynamicStore) FindDynamicRecordByNameAndTokenHash(_ context.Context, name, tokenHash string) (appdb.DynamicRecord, error) {
|
|
for _, record := range f.records {
|
|
if record.Name == name && record.TokenHash == tokenHash && record.Enabled {
|
|
return record, nil
|
|
}
|
|
}
|
|
return appdb.DynamicRecord{}, appdb.ErrNotFound
|
|
}
|
|
|
|
func (f *fakeDynamicStore) GetDynamicRecord(_ context.Context, zoneID, name, recordType string) (appdb.DynamicRecord, error) {
|
|
record, ok := f.records[dynamicRecordKey(name, recordType)]
|
|
if !ok || record.ZoneID != zoneID || !record.Enabled {
|
|
return appdb.DynamicRecord{}, appdb.ErrNotFound
|
|
}
|
|
return record, nil
|
|
}
|
|
|
|
func (f *fakeDynamicStore) UpsertDynamicRecord(_ context.Context, record appdb.DynamicRecord) error {
|
|
if f.records == nil {
|
|
f.records = make(map[string]appdb.DynamicRecord)
|
|
}
|
|
record.Enabled = true
|
|
f.records[dynamicRecordKey(record.Name, record.Type)] = record
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeDynamicStore) DisableDynamicRecord(_ context.Context, _, name, recordType string) error {
|
|
record, ok := f.records[dynamicRecordKey(name, recordType)]
|
|
if ok {
|
|
record.Enabled = false
|
|
f.records[dynamicRecordKey(name, recordType)] = record
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestNewRequiresClient(t *testing.T) {
|
|
if _, err := New(Config{}, nil, nil); err == nil {
|
|
t.Fatal("expected error for missing client")
|
|
}
|
|
}
|
|
|
|
func TestNewBuildsServer(t *testing.T) {
|
|
srv, err := New(Config{Addr: ":0"}, &fakeClient{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
if srv.addr != ":0" {
|
|
t.Fatalf("unexpected addr: %s", srv.addr)
|
|
}
|
|
}
|
|
|
|
func TestAuthCanBeDisabled(t *testing.T) {
|
|
srv, err := New(Config{}, &fakeClient{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestServesEmbeddedTablerAssets(t *testing.T) {
|
|
srv, err := New(Config{}, &fakeClient{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/static/vendor/tabler.min.css", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "Tabler v1.4.0") {
|
|
t.Fatal("expected vendored Tabler CSS")
|
|
}
|
|
}
|
|
|
|
func TestAuthRedirectsProtectedRoutesToLogin(t *testing.T) {
|
|
srv, err := New(Config{Authenticator: &fakeAuth{allowed: true}}, &fakeClient{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/zones", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
if got := rec.Header().Get("Location"); got != "/login?next=%2Fzones" {
|
|
t.Fatalf("unexpected location: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestLoginCreatesSession(t *testing.T) {
|
|
auth := &fakeAuth{allowed: true}
|
|
srv, err := New(Config{Authenticator: auth}, &fakeClient{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
body := strings.NewReader("username=alice&password=secret&next=/zones")
|
|
req := httptest.NewRequest(http.MethodPost, "/login", body)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
if auth.username != "alice" || auth.password != "secret" {
|
|
t.Fatalf("unexpected credentials: %q %q", auth.username, auth.password)
|
|
}
|
|
|
|
cookies := rec.Result().Cookies()
|
|
if len(cookies) == 0 || cookies[0].Name != sessionCookieName {
|
|
t.Fatalf("expected session cookie, got %#v", cookies)
|
|
}
|
|
if !cookies[0].HttpOnly {
|
|
t.Fatal("session cookie must be HttpOnly")
|
|
}
|
|
if !cookies[0].Secure {
|
|
t.Fatal("session cookie must be Secure")
|
|
}
|
|
if cookies[0].SameSite != http.SameSiteStrictMode {
|
|
t.Fatalf("unexpected SameSite policy: %v", cookies[0].SameSite)
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/zones", nil)
|
|
req.AddCookie(cookies[0])
|
|
rec = httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected authenticated status: %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestLoginReportsBackendErrors(t *testing.T) {
|
|
srv, err := New(Config{Authenticator: &fakeAuth{err: errors.New("ldap down")}}, &fakeClient{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
body := strings.NewReader("username=alice&password=secret")
|
|
req := httptest.NewRequest(http.MethodPost, "/login", body)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
if !strings.HasPrefix(rec.Header().Get("Location"), "/login?error=") {
|
|
t.Fatalf("unexpected location: %q", rec.Header().Get("Location"))
|
|
}
|
|
}
|
|
|
|
func TestLogoutClearsSession(t *testing.T) {
|
|
srv, err := New(Config{Authenticator: &fakeAuth{allowed: true}}, &fakeClient{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
token, err := srv.createSession("alice")
|
|
if err != nil {
|
|
t.Fatalf("createSession returned error: %v", err)
|
|
}
|
|
csrfToken := srv.sessions[token].CSRFToken
|
|
|
|
body := strings.NewReader("csrf_token=" + csrfToken)
|
|
req := httptest.NewRequest(http.MethodPost, "/logout", body)
|
|
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: token})
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
if _, ok := srv.sessions[token]; ok {
|
|
t.Fatal("session was not deleted")
|
|
}
|
|
}
|
|
|
|
func TestLogoutRequiresCSRF(t *testing.T) {
|
|
srv, err := New(Config{Authenticator: &fakeAuth{allowed: true}}, &fakeClient{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
token, err := srv.createSession("alice")
|
|
if err != nil {
|
|
t.Fatalf("createSession returned error: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
|
|
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: token})
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestSecurityHeadersAreSet(t *testing.T) {
|
|
srv, err := New(Config{}, &fakeClient{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
for _, header := range []string{
|
|
"Content-Security-Policy",
|
|
"Referrer-Policy",
|
|
"Strict-Transport-Security",
|
|
"X-Content-Type-Options",
|
|
"X-Frame-Options",
|
|
} {
|
|
if rec.Header().Get(header) == "" {
|
|
t.Fatalf("expected %s header", header)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseLinesSkipsBlankLines(t *testing.T) {
|
|
values := parseLines("192.0.2.1\n\n192.0.2.2\n")
|
|
if len(values) != 2 {
|
|
t.Fatalf("unexpected values: %#v", values)
|
|
}
|
|
}
|
|
|
|
func TestDeleteRRSetRejectsSOA(t *testing.T) {
|
|
client := &fakeClient{}
|
|
srv, err := New(Config{}, client, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
body := strings.NewReader("name=example.org.&type=SOA")
|
|
req := httptest.NewRequest(http.MethodPost, "/zones/example.org./rrsets/delete", body)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
if client.deletedRRSetType != "" {
|
|
t.Fatalf("SOA delete reached client")
|
|
}
|
|
}
|
|
|
|
func TestDeleteRRSetDisablesDynamicDNS(t *testing.T) {
|
|
client := &fakeClient{}
|
|
store := &fakeDynamicStore{records: map[string]appdb.DynamicRecord{}}
|
|
store.records[dynamicRecordKey("www.example.org.", "A")] = appdb.DynamicRecord{
|
|
ZoneID: "example.org.",
|
|
Name: "www.example.org.",
|
|
Type: "A",
|
|
TTL: 300,
|
|
TokenHash: tokenHash("secret"),
|
|
Enabled: true,
|
|
}
|
|
srv, err := New(Config{DynamicStore: store}, client, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
body := strings.NewReader("name=www.example.org.&type=A")
|
|
req := httptest.NewRequest(http.MethodPost, "/zones/example.org./rrsets/delete", body)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
record := store.records[dynamicRecordKey("www.example.org.", "A")]
|
|
if record.Enabled {
|
|
t.Fatalf("dynamic record was not disabled: %#v", record)
|
|
}
|
|
}
|
|
|
|
func TestSaveEditedRRSetUsesQueryIdentity(t *testing.T) {
|
|
client := &fakeClient{}
|
|
srv, err := New(Config{}, client, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
body := strings.NewReader("name=evil.example.org.&type=AAAA&ttl=300&records=192.0.2.10")
|
|
req := httptest.NewRequest(http.MethodPost, "/zones/example.org./rrsets/edit?name=www.example.org.&type=A", body)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
if client.replacedRRSet.Name != "www.example.org." {
|
|
t.Fatalf("name came from form: %#v", client.replacedRRSet)
|
|
}
|
|
if client.replacedRRSet.Type != "A" {
|
|
t.Fatalf("type came from form: %#v", client.replacedRRSet)
|
|
}
|
|
}
|
|
|
|
func TestSaveRRSetEnablesDynamicDNS(t *testing.T) {
|
|
client := &fakeClient{}
|
|
store := &fakeDynamicStore{}
|
|
srv, err := New(Config{DynamicStore: store}, client, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("name", "www.example.org.")
|
|
form.Set("type", "A")
|
|
form.Set("ttl", "300")
|
|
form.Set("records", "192.0.2.10")
|
|
form.Set("dynamic_dns", "on")
|
|
req := httptest.NewRequest(http.MethodPost, "/zones/example.org./rrsets", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
record, err := store.GetDynamicRecord(context.Background(), "example.org.", "www.example.org.", "A")
|
|
if err != nil {
|
|
t.Fatalf("GetDynamicRecord returned error: %v", err)
|
|
}
|
|
if record.TokenHash == "" || record.TTL != 300 {
|
|
t.Fatalf("unexpected dynamic record: %#v", record)
|
|
}
|
|
if len(rec.Result().Cookies()) == 0 {
|
|
t.Fatal("expected flash cookie")
|
|
}
|
|
}
|
|
|
|
func TestRegenerateDynamicToken(t *testing.T) {
|
|
store := &fakeDynamicStore{records: map[string]appdb.DynamicRecord{}}
|
|
store.records[dynamicRecordKey("www.example.org.", "A")] = appdb.DynamicRecord{
|
|
ZoneID: "example.org.",
|
|
Name: "www.example.org.",
|
|
Type: "A",
|
|
TTL: 300,
|
|
TokenHash: tokenHash("old-token"),
|
|
Enabled: true,
|
|
}
|
|
srv, err := New(Config{DynamicStore: store}, &fakeClient{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("name", "www.example.org.")
|
|
form.Set("type", "A")
|
|
req := httptest.NewRequest(http.MethodPost, "/zones/example.org./rrsets/dyndns/regenerate", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusSeeOther {
|
|
t.Fatalf("unexpected status: %d", rec.Code)
|
|
}
|
|
record := store.records[dynamicRecordKey("www.example.org.", "A")]
|
|
if record.TokenHash == tokenHash("old-token") || record.TokenHash == "" {
|
|
t.Fatalf("token hash was not regenerated: %#v", record)
|
|
}
|
|
}
|
|
|
|
func TestDynamicUpdateEndpointUpdatesRecord(t *testing.T) {
|
|
client := &fakeClient{}
|
|
store := &fakeDynamicStore{records: map[string]appdb.DynamicRecord{}}
|
|
store.records[dynamicRecordKey("www.example.org.", "A")] = appdb.DynamicRecord{
|
|
ZoneID: "example.org.",
|
|
Name: "www.example.org.",
|
|
Type: "A",
|
|
TTL: 120,
|
|
TokenHash: tokenHash("secret"),
|
|
Enabled: true,
|
|
}
|
|
srv, err := New(Config{DynamicStore: store}, client, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
body := strings.NewReader(`{"name":"www.example.org","token":"secret"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/dyndns", body)
|
|
req.RemoteAddr = "198.51.100.20:12345"
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if client.replacedZoneID != "example.org." {
|
|
t.Fatalf("unexpected zone id: %q", client.replacedZoneID)
|
|
}
|
|
if client.replacedRRSet.Name != "www.example.org." || client.replacedRRSet.Type != "A" || client.replacedRRSet.TTL != 120 {
|
|
t.Fatalf("unexpected rrset: %#v", client.replacedRRSet)
|
|
}
|
|
if got := client.replacedRRSet.Records[0].Content; got != "198.51.100.20" {
|
|
t.Fatalf("unexpected update address: %q", got)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), `"status":"OK"`) {
|
|
t.Fatalf("unexpected response: %s", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDynamicUpdateEndpointUsesForwardedFor(t *testing.T) {
|
|
client := &fakeClient{}
|
|
store := &fakeDynamicStore{records: map[string]appdb.DynamicRecord{}}
|
|
store.records[dynamicRecordKey("www.example.org.", "A")] = appdb.DynamicRecord{
|
|
ZoneID: "example.org.",
|
|
Name: "www.example.org.",
|
|
Type: "A",
|
|
TTL: 120,
|
|
TokenHash: tokenHash("secret"),
|
|
Enabled: true,
|
|
}
|
|
srv, err := New(Config{DynamicStore: store}, client, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
body := strings.NewReader(`{"name":"www.example.org","token":"secret"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/dyndns", body)
|
|
req.RemoteAddr = "10.0.0.10:12345"
|
|
req.Header.Set("X-Forwarded-For", "198.51.100.30, 10.0.0.10")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if got := client.replacedRRSet.Records[0].Content; got != "198.51.100.30" {
|
|
t.Fatalf("unexpected update address: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDynamicUpdateEndpointUsesRealIP(t *testing.T) {
|
|
client := &fakeClient{}
|
|
store := &fakeDynamicStore{records: map[string]appdb.DynamicRecord{}}
|
|
store.records[dynamicRecordKey("www.example.org.", "A")] = appdb.DynamicRecord{
|
|
ZoneID: "example.org.",
|
|
Name: "www.example.org.",
|
|
Type: "A",
|
|
TTL: 120,
|
|
TokenHash: tokenHash("secret"),
|
|
Enabled: true,
|
|
}
|
|
srv, err := New(Config{DynamicStore: store}, client, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
body := strings.NewReader(`{"name":"www.example.org","token":"secret"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/dyndns", body)
|
|
req.RemoteAddr = "10.0.0.10:12345"
|
|
req.Header.Set("X-Real-IP", "198.51.100.31")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if got := client.replacedRRSet.Records[0].Content; got != "198.51.100.31" {
|
|
t.Fatalf("unexpected update address: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDynamicUpdateEndpointAddressOverridesForwardedHeaders(t *testing.T) {
|
|
client := &fakeClient{}
|
|
store := &fakeDynamicStore{records: map[string]appdb.DynamicRecord{}}
|
|
store.records[dynamicRecordKey("www.example.org.", "A")] = appdb.DynamicRecord{
|
|
ZoneID: "example.org.",
|
|
Name: "www.example.org.",
|
|
Type: "A",
|
|
TTL: 120,
|
|
TokenHash: tokenHash("secret"),
|
|
Enabled: true,
|
|
}
|
|
srv, err := New(Config{DynamicStore: store}, client, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
body := strings.NewReader(`{"name":"www.example.org","token":"secret","address":"198.51.100.40"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/dyndns", body)
|
|
req.RemoteAddr = "10.0.0.10:12345"
|
|
req.Header.Set("X-Forwarded-For", "198.51.100.30")
|
|
req.Header.Set("X-Real-IP", "198.51.100.31")
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if got := client.replacedRRSet.Records[0].Content; got != "198.51.100.40" {
|
|
t.Fatalf("unexpected update address: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDynamicUpdateEndpointRejectsAddressTypeMismatch(t *testing.T) {
|
|
client := &fakeClient{}
|
|
store := &fakeDynamicStore{records: map[string]appdb.DynamicRecord{}}
|
|
store.records[dynamicRecordKey("www.example.org.", "A")] = appdb.DynamicRecord{
|
|
ZoneID: "example.org.",
|
|
Name: "www.example.org.",
|
|
Type: "A",
|
|
TTL: 120,
|
|
TokenHash: tokenHash("secret"),
|
|
Enabled: true,
|
|
}
|
|
srv, err := New(Config{DynamicStore: store}, client, nil)
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
|
|
body := strings.NewReader(`{"name":"www.example.org","token":"secret","address":"2001:db8::1"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/dyndns", body)
|
|
rec := httptest.NewRecorder()
|
|
|
|
srv.routes().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
if client.replacedRRSet.Name != "" {
|
|
t.Fatalf("unexpected PowerDNS update: %#v", client.replacedRRSet)
|
|
}
|
|
}
|