mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
88 lines
3.3 KiB
Go
88 lines
3.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"regexp"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
"github.com/Control-D-Inc/ctrld/internal/controld"
|
|
)
|
|
|
|
// journalField marks an event for the retained journal stream. Both packages
|
|
// share the name, so one writer keeps the lines of both.
|
|
const journalField = ctrld.JournalField
|
|
|
|
// journalMarker is the serialized form of the journal field. The journal
|
|
// writer keeps a line below warn level only when the line contains it.
|
|
var journalMarker = []byte(`"` + journalField + `":true`)
|
|
|
|
// retainedURLPath matches the part of a URL that follows the host, for every
|
|
// scheme: a DoH, DoH3, or DoQ endpoint carries its token in the path. The
|
|
// class stops the match at the characters that end a value inside a JSON line.
|
|
var retainedURLPath = regexp.MustCompile(`([a-z][a-z0-9+.-]*://[^/?"\\\s]+)[/?][^"\\\s]*`)
|
|
|
|
// retainedControlDHost matches a Control D resolver host. The first label of
|
|
// the host is the resolver ID, which a DoT or DoQ endpoint carries in place of
|
|
// a path.
|
|
var retainedControlDHost = regexp.MustCompile(`([A-Za-z0-9-]+)(\.dns\.controld\.(?:com|dev))`)
|
|
|
|
// redactRetainedLine strips the secrets of one line that the journal keeps.
|
|
// Support downloads the journal whole, and any error text can carry a DoH URL
|
|
// with the resolver path and the packed query of the user.
|
|
func redactRetainedLine(p []byte) []byte {
|
|
stripped := retainedURLPath.ReplaceAll(p, []byte(`${1}/[redacted]`))
|
|
stripped = retainedControlDHost.ReplaceAll(stripped, []byte(`[redacted]${2}`))
|
|
return []byte(redactSecrets(string(stripped), journalSecrets()...))
|
|
}
|
|
|
|
// journalSecrets names the values that no retained line may hold. The client
|
|
// ID stays out: it is a device label, not a secret. redactSecrets drops the
|
|
// values that are too short to redact.
|
|
func journalSecrets() []string {
|
|
uid, _ := controld.ParseRawUID(cdUID)
|
|
return []string{cdUID, cdOrg, uid}
|
|
}
|
|
|
|
// journal marks an event for the retained journal stream. The event keeps
|
|
// its own level, so an info event stays info on the console.
|
|
func journal(e *zerolog.Event) *zerolog.Event {
|
|
return ctrld.Journal(e)
|
|
}
|
|
|
|
// journalLevelWriter passes on the lines that the journal keeps and drops the
|
|
// rest, so the journal holds a small history that survives a restart.
|
|
type journalLevelWriter struct {
|
|
w io.Writer
|
|
}
|
|
|
|
var _ zerolog.LevelWriter = (*journalLevelWriter)(nil)
|
|
|
|
func newJournalLevelWriter(w io.Writer) *journalLevelWriter {
|
|
return &journalLevelWriter{w: w}
|
|
}
|
|
|
|
// WriteLevel reads the level first, because a compare costs less than a scan
|
|
// of the serialized line.
|
|
func (j *journalLevelWriter) WriteLevel(l zerolog.Level, p []byte) (int, error) {
|
|
if l < zerolog.WarnLevel && !bytes.Contains(p, journalMarker) {
|
|
// A dropped line reports a full write, because zerolog.MultiLevelWriter
|
|
// turns a short write into an error for the whole group of writers.
|
|
return len(p), nil
|
|
}
|
|
if _, err := j.w.Write(redactRetainedLine(p)); err != nil {
|
|
return 0, err
|
|
}
|
|
// The redacted line is shorter than the line the logger handed over, and a
|
|
// short write is an error for the whole group of writers.
|
|
return len(p), nil
|
|
}
|
|
|
|
// Write has no level to read, so the marker alone decides. Every level below
|
|
// warn takes that path.
|
|
func (j *journalLevelWriter) Write(p []byte) (int, error) {
|
|
return j.WriteLevel(zerolog.DebugLevel, p)
|
|
}
|