gopass/pkg/debug/debug.go
Dominik Schulz 9666b0a850
Correctly handle new multiline secrets (#2625)
* Correctly handle new multiline secrets

This commit fixes as small issue in how multi-line secrets are handled.
Before they were always written in to the secret body completly ignoring
the first line that contains the password. Now we do respect that
correctly. To implement that properly we need to have some additional
code to satisfy the io.Writer assumptions around the AKV secret type.

Also this fixes some non-hermetic tests that showed up during testing of
this change.

Fixes #2614

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>

* Fix typo

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>

* Ditch the MultiWriter approach in favor of a pass-through writer

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>

* Format

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>

---------

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
2023-07-30 18:57:43 +02:00

266 lines
5.0 KiB
Go

package debug
import (
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"runtime"
"strings"
)
var (
// Stdout is exported for tests.
Stdout io.Writer = os.Stdout
// Stderr is exported for tests.
Stderr io.Writer = os.Stderr
// LogWriter is exposed for consuming extra command output, if needed.
LogWriter io.Writer = io.Discard
logSecrets bool
)
var opts struct {
logger *log.Logger
funcs map[string]bool
files map[string]bool
logFile *os.File
}
var logFn = doNotLog
// make sure all initializations happens before the init func.
var enabled = initDebug()
func initDebug() bool {
if opts.logFile != nil {
_ = opts.logFile.Close()
}
if os.Getenv("GOPASS_DEBUG") == "" && os.Getenv("GOPASS_DEBUG_LOG") == "" {
logFn = doNotLog
return false
}
// we need to explicitly set logSecrets to false in case tests run under an environment
// where GOPASS_DEBUG_LOG_SECRETS is true. Otherwise setting it to false in the test
// context won't have any effect.
logSecrets = false
if sv := os.Getenv("GOPASS_DEBUG_LOG_SECRETS"); sv != "" && sv != "false" {
logSecrets = true
}
initDebugLogger()
initDebugTags()
logFn = doLog
return true
}
func initDebugLogger() {
debugfile := os.Getenv("GOPASS_DEBUG_LOG")
if debugfile == "" {
opts.logger = log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)
LogWriter = os.Stderr
return
}
f, err := os.OpenFile(debugfile, os.O_WRONLY|os.O_APPEND, 0o600)
if err == nil {
// seek to the end of the file (offset, whence [2 = end])
_, err := f.Seek(0, 2)
if err != nil {
_ = f.Close()
fmt.Fprintf(Stderr, "unable to seek to end of %v: %v\n", debugfile, err)
os.Exit(3)
}
}
if err != nil && os.IsNotExist(err) {
f, err = os.OpenFile(debugfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
}
if err != nil {
fmt.Fprintf(Stderr, "unable to open debug log file %v: %v\n", debugfile, err)
os.Exit(2)
}
opts.logFile = f
opts.logger = log.New(f, "", log.Ldate|log.Lmicroseconds)
LogWriter = f
}
func parseFilter(envname string, pad func(string) string) map[string]bool {
filter := make(map[string]bool)
env := os.Getenv(envname)
if env == "" {
return filter
}
for _, fn := range strings.Split(env, ",") {
t := pad(strings.TrimSpace(fn))
val := true
if t[0] == '-' {
val = false
t = t[1:]
} else if t[0] == '+' {
val = true
t = t[1:]
}
// test pattern
_, err := path.Match(t, "")
if err != nil {
fmt.Fprintf(Stderr, "error: invalid pattern %q: %v\n", t, err)
os.Exit(5)
}
filter[t] = val
}
return filter
}
func padFunc(s string) string {
if s == "all" {
return s
}
return s
}
func padFile(s string) string {
if s == "all" {
return s
}
if !strings.Contains(s, "/") {
s = "*/" + s
}
if !strings.Contains(s, ":") {
s += ":*"
}
return s
}
func initDebugTags() {
opts.funcs = parseFilter("GOPASS_DEBUG_FUNCS", padFunc)
opts.files = parseFilter("GOPASS_DEBUG_FILES", padFile)
}
func getPosition(offset int) (fn, dir, file string, line int) { //nolint:nonamedreturns
pc, file, line, ok := runtime.Caller(3 + offset)
if !ok {
return "", "", "", 0
}
dirname := filepath.Base(filepath.Dir(file))
filename := filepath.Base(file)
f := runtime.FuncForPC(pc)
return path.Base(f.Name()), dirname, filename, line
}
func checkFilter(filter map[string]bool, key string) bool {
// check if exact match
if v, ok := filter[key]; ok {
return v
}
// check globbing
for k, v := range filter {
if m, _ := path.Match(k, key); m {
return v
}
}
// check if tag "all" is enabled
if v, ok := filter["all"]; ok && v {
return true
}
return false
}
// Log logs a statement to Stderr (unless filtered) and the
// debug log file (if enabled).
func Log(f string, args ...any) {
logFn(0, f, args...)
}
// LogN logs a statement to Stderr (unless filtered) and the
// debug log file (if enabled). The offset will be applied to
// the runtime position.
func LogN(offset int, f string, args ...any) {
logFn(offset, f, args...)
}
func doNotLog(offset int, f string, args ...any) {}
func doLog(offset int, f string, args ...any) {
fn, dir, file, line := getPosition(offset)
if len(f) == 0 || f[len(f)-1] != '\n' {
f += "\n"
}
type Shortener interface {
Str() string
}
type Safer interface {
SafeStr() string
}
argsi := make([]any, len(args))
for i, item := range args {
argsi[i] = item
if secreter, ok := item.(Safer); ok && !logSecrets {
argsi[i] = secreter.SafeStr()
continue
}
if shortener, ok := item.(Shortener); ok {
argsi[i] = shortener.Str()
}
}
pos := fmt.Sprintf("%s/%s:%d", dir, file, line)
formatString := fmt.Sprintf("%s\t%s\t%s", pos, fn, f)
dbgprint := func() {
fmt.Fprintf(Stderr, formatString, argsi...)
}
if opts.logger != nil {
opts.logger.Printf(formatString, argsi...)
}
filename := fmt.Sprintf("%s/%s:%d", dir, file, line)
if checkFilter(opts.files, filename) {
dbgprint()
return
}
if checkFilter(opts.funcs, fn) {
dbgprint()
}
}
// IsEnabled returns true if debug logging was enabled.
func IsEnabled() bool {
return enabled
}