repositories / bugabinga.net
bugabinga.net
personal infrastructure for bugabinga!
owned by admin
services/luci/internal/logs/sanitize.go
Rawpackage logs
import (
"bytes"
"io"
"sort"
"sync"
)
const masked = "[MASKED]"
// MaxLogBytes bounds live log output, including the truncation marker.
const MaxLogBytes = 16 << 20
const truncationMarker = "\n[luci] log output truncated\n"
type Sanitizer struct {
limiter *limitWriter
mu sync.Mutex
writer io.Writer
secrets [][]byte
maxLen int
pending []byte
ansiMode byte
closed bool
}
func NewSanitizer(writer io.Writer, secrets [][]byte) *Sanitizer {
return newSanitizer(writer, secrets, nil)
}
// NewBoundedSanitizer strips ANSI sequences and masks secrets while retaining
// at most MaxLogBytes. Further output is discarded after one visible marker.
func NewBoundedSanitizer(writer io.Writer, secrets [][]byte) *Sanitizer {
limiter := &limitWriter{writer: writer, limit: MaxLogBytes}
return newSanitizer(limiter, secrets, limiter)
}
func newSanitizer(writer io.Writer, secrets [][]byte, limiter *limitWriter) *Sanitizer {
unique := map[string]struct{}{}
for _, secret := range secrets {
if len(secret) > 0 {
unique[string(secret)] = struct{}{}
}
}
values := make([][]byte, 0, len(unique))
for secret := range unique {
values = append(values, []byte(secret))
}
sort.Slice(values, func(i, j int) bool { return len(values[i]) > len(values[j]) })
maxLen := 1
if len(values) > 0 {
maxLen = len(values[0])
}
return &Sanitizer{writer: writer, secrets: values, maxLen: maxLen, limiter: limiter}
}
// Truncated reports whether NewBoundedSanitizer discarded output.
func (s *Sanitizer) Truncated() bool {
return s.limiter != nil && s.limiter.Truncated()
}
func (s *Sanitizer) Write(data []byte) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return 0, io.ErrClosedPipe
}
for _, value := range data {
s.consumeANSI(value)
}
if err := s.flush(false); err != nil {
return 0, err
}
return len(data), nil
}
func (s *Sanitizer) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return nil
}
s.closed = true
return s.flush(true)
}
func (s *Sanitizer) consumeANSI(value byte) {
switch s.ansiMode {
case 0:
if value == 0x1b {
s.ansiMode = 1
return
}
s.pending = append(s.pending, value)
case 1:
switch value {
case '[':
s.ansiMode = 2
case ']', 'P', 'X', '^', '_':
s.ansiMode = 3
default:
if value >= 0x20 && value <= 0x2f {
s.ansiMode = 4
} else {
s.ansiMode = 0
}
}
case 2:
if value >= 0x40 && value <= 0x7e {
s.ansiMode = 0
}
case 3:
if value == 0x07 {
s.ansiMode = 0
} else if value == 0x1b {
s.ansiMode = 5
}
case 4:
if value >= 0x30 && value <= 0x7e {
s.ansiMode = 0
}
case 5:
if value == '\\' {
s.ansiMode = 0
} else if value != 0x1b {
s.ansiMode = 3
}
}
}
func (s *Sanitizer) flush(all bool) error {
limit := len(s.pending)
if !all {
limit -= s.maxLen - 1
if limit < 0 {
limit = 0
}
}
position, plainStart := 0, 0
for position < limit {
match := s.secretAt(position)
if match == 0 {
position++
continue
}
if plainStart < position {
if _, err := s.writer.Write(s.pending[plainStart:position]); err != nil {
return err
}
}
if _, err := io.WriteString(s.writer, masked); err != nil {
return err
}
position += match
plainStart = position
}
if plainStart < position {
if _, err := s.writer.Write(s.pending[plainStart:position]); err != nil {
return err
}
}
s.pending = append(s.pending[:0], s.pending[position:]...)
return nil
}
func (s *Sanitizer) secretAt(position int) int {
for _, secret := range s.secrets {
if bytes.HasPrefix(s.pending[position:], secret) {
return len(secret)
}
}
return 0
}
type limitWriter struct {
mu sync.Mutex
writer io.Writer
limit int
written int
truncated bool
}
func (w *limitWriter) Write(data []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
if w.truncated {
return len(data), nil
}
available := w.limit - len(truncationMarker) - w.written
if available >= len(data) {
n, err := w.writer.Write(data)
w.written += n
return n, err
}
if available > 0 {
n, err := w.writer.Write(data[:available])
w.written += n
if err != nil {
return n, err
}
if n != available {
return n, io.ErrShortWrite
}
}
n, err := io.WriteString(w.writer, truncationMarker)
w.written += n
if err != nil {
return 0, err
}
if n != len(truncationMarker) {
return 0, io.ErrShortWrite
}
w.truncated = true
return len(data), nil
}
func (w *limitWriter) Truncated() bool {
w.mu.Lock()
defer w.mu.Unlock()
return w.truncated
}