mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
306 lines
10 KiB
Go
306 lines
10 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
)
|
|
|
|
func Test_journalSetsMarker(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := zerolog.New(&buf)
|
|
journal(logger.Info()).Msg("marked")
|
|
if !bytes.Contains(buf.Bytes(), journalMarker) {
|
|
t.Fatalf("journal event lacks the marker: %s", buf.String())
|
|
}
|
|
if !bytes.Contains(buf.Bytes(), []byte(`"level":"info"`)) {
|
|
t.Fatalf("journal event changed its level: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
// journalTestLine has the shape of a serialized event, so the direct write
|
|
// path sees the marker where zerolog puts it.
|
|
func journalTestLine(marked bool) []byte {
|
|
if marked {
|
|
return []byte(`{"level":"info","` + journalField + `":true,"message":"state"}` + "\n")
|
|
}
|
|
return []byte(`{"level":"info","message":"state"}` + "\n")
|
|
}
|
|
|
|
func Test_journalLevelWriterKeepsWarningsAndMarkedLines(t *testing.T) {
|
|
level := zerolog.GlobalLevel()
|
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
t.Cleanup(func() { zerolog.SetGlobalLevel(level) })
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
log func(logger zerolog.Logger)
|
|
kept bool
|
|
}{
|
|
{"info with the marker", func(logger zerolog.Logger) { journal(logger.Info()).Msg("state") }, true},
|
|
{"info without the marker", func(logger zerolog.Logger) { logger.Info().Msg("state") }, false},
|
|
{"warn without the marker", func(logger zerolog.Logger) { logger.Warn().Msg("slow") }, true},
|
|
{"debug with the marker", func(logger zerolog.Logger) { journal(logger.Debug()).Msg("state") }, true},
|
|
{"notice without the marker", func(logger zerolog.Logger) { logger.Notice().Msg("note") }, false},
|
|
{"error without the marker", func(logger zerolog.Logger) { logger.Error().Msg("failed") }, true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
tc.log(zerolog.New(zerolog.MultiLevelWriter(newJournalLevelWriter(&buf))))
|
|
if kept := buf.Len() > 0; kept != tc.kept {
|
|
t.Fatalf("kept = %v, want %v, line: %q", kept, tc.kept, buf.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_journalLevelWriterWriteKeepsMarkedLineOnly(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
writer := newJournalLevelWriter(&buf)
|
|
|
|
marked := journalTestLine(true)
|
|
n, err := writer.Write(marked)
|
|
if err != nil || n != len(marked) {
|
|
t.Fatalf("write of a marked line = (%d, %v), want (%d, nil)", n, err, len(marked))
|
|
}
|
|
if buf.String() != string(marked) {
|
|
t.Fatalf("journal holds %q, want %q", buf.String(), marked)
|
|
}
|
|
|
|
plain := journalTestLine(false)
|
|
n, err = writer.Write(plain)
|
|
if err != nil || n != len(plain) {
|
|
t.Fatalf("write of a plain line = (%d, %v), want (%d, nil)", n, err, len(plain))
|
|
}
|
|
if buf.String() != string(marked) {
|
|
t.Fatalf("journal kept an unmarked line: %q", buf.String())
|
|
}
|
|
}
|
|
|
|
// journalDoHError has the shape of a resolver error text: the DoH URL carries
|
|
// the resolver path and the packed query.
|
|
const journalDoHError = `Get "https://dns.example/org-v1-SECRET0123?dns=pOgBAAAB": dial tcp`
|
|
|
|
// journalRedactedURL is what the sink keeps of journalDoHError.
|
|
const journalRedactedURL = "https://dns.example/[redacted]"
|
|
|
|
// journalTestLogger returns a logger that writes through the journal sink.
|
|
func journalTestLogger(t *testing.T) (*bytes.Buffer, zerolog.Logger) {
|
|
t.Helper()
|
|
level := zerolog.GlobalLevel()
|
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
|
t.Cleanup(func() { zerolog.SetGlobalLevel(level) })
|
|
var buf bytes.Buffer
|
|
return &buf, zerolog.New(zerolog.MultiLevelWriter(newJournalLevelWriter(&buf)))
|
|
}
|
|
|
|
func Test_journalLevelWriterRedactsAKeptLine(t *testing.T) {
|
|
buf, logger := journalTestLogger(t)
|
|
|
|
logger.Warn().Str("error", journalDoHError).Msg("upstream failed")
|
|
|
|
line := buf.String()
|
|
if !strings.Contains(line, journalRedactedURL) {
|
|
t.Fatalf("the sink kept the URL path: %s", line)
|
|
}
|
|
for _, secret := range []string{"SECRET0123", "pOgBAAAB"} {
|
|
if strings.Contains(line, secret) {
|
|
t.Fatalf("the sink kept %q: %s", secret, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Test_journalLevelWriterRedactsTheProvisionSecrets(t *testing.T) {
|
|
origCdUID := cdUID
|
|
t.Cleanup(func() { cdUID = origCdUID })
|
|
cdUID = "org-v1-uidofthisdevice"
|
|
buf, logger := journalTestLogger(t)
|
|
|
|
logger.Warn().Str("detail", "device "+cdUID+" is gone").Msg("provision failed")
|
|
|
|
line := buf.String()
|
|
if strings.Contains(line, cdUID) {
|
|
t.Fatalf("the sink kept the resolver uid: %s", line)
|
|
}
|
|
if !strings.Contains(line, "[redacted]") {
|
|
t.Fatalf("the sink wrote no redaction mark: %s", line)
|
|
}
|
|
}
|
|
|
|
func Test_journalLevelWriterDropsADebugLineWithAURL(t *testing.T) {
|
|
buf, logger := journalTestLogger(t)
|
|
|
|
logger.Debug().Str("error", journalDoHError).Msg("upstream failed")
|
|
|
|
if buf.Len() != 0 {
|
|
t.Fatalf("the sink kept a debug line: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func Test_journalLevelWriterRedactsAMarkedInfoLine(t *testing.T) {
|
|
buf, logger := journalTestLogger(t)
|
|
|
|
journal(logger.Info()).Str("error", journalDoHError).Msg("upstream failed")
|
|
|
|
line := buf.String()
|
|
if !strings.Contains(line, journalRedactedURL) {
|
|
t.Fatalf("the sink kept the URL path: %s", line)
|
|
}
|
|
if !strings.Contains(line, `"level":"info"`) {
|
|
t.Fatalf("the sink changed the level: %s", line)
|
|
}
|
|
}
|
|
|
|
func Test_journalLevelWriterReportsTheOriginalLength(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
writer := newJournalLevelWriter(&buf)
|
|
line := []byte(`{"level":"warn","error":"https://dns.example/org-v1-SECRET0123","message":"state"}` + "\n")
|
|
|
|
n, err := writer.WriteLevel(zerolog.WarnLevel, line)
|
|
|
|
if err != nil || n != len(line) {
|
|
t.Fatalf("write of a kept line = (%d, %v), want (%d, nil)", n, err, len(line))
|
|
}
|
|
if buf.Len() >= len(line) {
|
|
t.Fatalf("the sink wrote no shorter line: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
// startInternalLogging drives the real cd mode setup and returns the program
|
|
// and the directory that holds the debug file and the journal file.
|
|
func startInternalLogging(t *testing.T) (*prog, string) {
|
|
t.Helper()
|
|
origSilent, origCdUID, origHomedir, origVerbose := silent, cdUID, homedir, verbose
|
|
origMainLog, origProxyLog := mainLog.Load(), ctrld.ProxyLogger.Load()
|
|
origLevel := zerolog.GlobalLevel()
|
|
t.Cleanup(func() {
|
|
silent, cdUID, homedir, verbose = origSilent, origCdUID, origHomedir, origVerbose
|
|
mainLog.Store(origMainLog)
|
|
ctrld.ProxyLogger.Store(origProxyLog)
|
|
zerolog.SetGlobalLevel(origLevel)
|
|
})
|
|
if origMainLog == nil {
|
|
discard := zerolog.New(io.Discard)
|
|
mainLog.Store(&discard)
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
homedir = dir
|
|
cdUID = "test-uid"
|
|
silent = false
|
|
verbose = 0
|
|
stubHeaderSnapshotSources(t)
|
|
|
|
p := &prog{cfg: &ctrld.Config{}}
|
|
p.initInternalLogging(nil)
|
|
t.Cleanup(p.closeInternalLogs)
|
|
return p, dir
|
|
}
|
|
|
|
// Test_initInternalLogging_redactsTheJournalFile drives a cd mode run: support
|
|
// downloads the journal file, so it holds the redacted URL, while the debug
|
|
// file stays raw for the developer who reads it on the host.
|
|
func Test_initInternalLogging_redactsTheJournalFile(t *testing.T) {
|
|
_, dir := startInternalLogging(t)
|
|
|
|
mainLog.Load().Warn().Str("error", journalDoHError).Msg("upstream failed")
|
|
|
|
journalText := rotatingFileTestContent(t, filepath.Join(dir, journalLogFileName))
|
|
if !strings.Contains(journalText, journalRedactedURL) {
|
|
t.Fatalf("journal file misses the redacted URL: %s", journalText)
|
|
}
|
|
if strings.Contains(journalText, "SECRET0123") {
|
|
t.Fatalf("journal file kept the resolver path: %s", journalText)
|
|
}
|
|
|
|
debugText := rotatingFileTestContent(t, filepath.Join(dir, logFileName))
|
|
if !strings.Contains(debugText, "org-v1-SECRET0123?dns=pOgBAAAB") {
|
|
t.Fatalf("debug file lost the raw URL: %s", debugText)
|
|
}
|
|
}
|
|
|
|
// Test_stopClosesTheInternalLogFiles proves that a stopped service leaves no
|
|
// open handle on the debug file and the journal file.
|
|
func Test_stopClosesTheInternalLogFiles(t *testing.T) {
|
|
origPin := cdDeactivationPin.Load()
|
|
t.Cleanup(func() { cdDeactivationPin.Store(origPin) })
|
|
cdDeactivationPin.Store(defaultDeactivationPin)
|
|
|
|
p, _ := startInternalLogging(t)
|
|
p.stopCh = make(chan struct{})
|
|
p.dnsWatcherStopCh = make(chan struct{})
|
|
for _, writer := range []*logWriter{p.internalLogWriter, p.internalJournalWriter} {
|
|
if writer.rotating() == nil {
|
|
t.Fatal("the setup opened no file")
|
|
}
|
|
}
|
|
|
|
if err := p.Stop(nil); err != nil {
|
|
t.Fatalf("Stop: %v", err)
|
|
}
|
|
|
|
for name, writer := range map[string]*logWriter{
|
|
logFileName: p.internalLogWriter,
|
|
journalLogFileName: p.internalJournalWriter,
|
|
} {
|
|
if writer.rotating() != nil {
|
|
t.Fatalf("%s stayed open after the stop", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test_redactRetainedLineKeepsAShortClientID puts a two-letter client ID in
|
|
// the resolver UID. The keys of a JSON line hold the same letters, and a
|
|
// redaction of the client ID would break the line.
|
|
func Test_redactRetainedLineKeepsAShortClientID(t *testing.T) {
|
|
origCdUID := cdUID
|
|
t.Cleanup(func() { cdUID = origCdUID })
|
|
cdUID = "uid12345678/os"
|
|
|
|
line := string(redactRetainedLine([]byte(`{"level":"info","os":"darwin","resolver":"uid12345678/os"}`)))
|
|
|
|
if !strings.Contains(line, `"os":"darwin"`) {
|
|
t.Fatalf("the redaction changed the os key: %s", line)
|
|
}
|
|
if strings.Contains(line, "uid12345678") {
|
|
t.Fatalf("the redaction kept the resolver uid: %s", line)
|
|
}
|
|
}
|
|
|
|
// Test_redactRetainedLineStripsEverySchemePath covers the endpoints that carry
|
|
// a token in their path with a scheme other than https.
|
|
func Test_redactRetainedLineStripsEverySchemePath(t *testing.T) {
|
|
for _, tc := range []struct{ in, want string }{
|
|
{`Get "quic://dns.example/org-v1-SECRET0123?dns=pOgBAAAB": timeout`, `Get "quic://dns.example/[redacted]": timeout`},
|
|
{`h3://dns.example/SECRET0123 failed`, `h3://dns.example/[redacted] failed`},
|
|
{`tls://dns.example:853 failed`, `tls://dns.example:853 failed`},
|
|
{`tls://abcdef12.dns.controld.com:853 failed`, `tls://[redacted].dns.controld.com:853 failed`},
|
|
{`quic://abcdef12.dns.controld.dev failed`, `quic://[redacted].dns.controld.dev failed`},
|
|
{`https://freedns.controld.com/p1 failed`, `https://freedns.controld.com/[redacted] failed`},
|
|
} {
|
|
if got := string(redactRetainedLine([]byte(tc.in))); got != tc.want {
|
|
t.Errorf("redactRetainedLine(%q) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test_redactRetainedLineKeepsAShortResolverUID puts a two-letter resolver UID
|
|
// in place. The keys of a JSON line hold the same letters, and a redaction of
|
|
// the UID would break every retained line and the header.
|
|
func Test_redactRetainedLineKeepsAShortResolverUID(t *testing.T) {
|
|
origCdUID := cdUID
|
|
t.Cleanup(func() { cdUID = origCdUID })
|
|
cdUID = "os"
|
|
|
|
const in = `{"level":"info","os":"darwin","message":"loss of service"}`
|
|
if got := string(redactRetainedLine([]byte(in))); got != in {
|
|
t.Fatalf("redactRetainedLine(%q) = %q, want the line unchanged", in, got)
|
|
}
|
|
}
|