github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/status/log_send.go (about)

     1  // Copyright 2019 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package status
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"mime/multipart"
    11  	"os"
    12  	"regexp"
    13  	"strings"
    14  	"time"
    15  
    16  	humanize "github.com/dustin/go-humanize"
    17  	"github.com/keybase/client/go/libkb"
    18  	"github.com/keybase/client/go/protocol/keybase1"
    19  )
    20  
    21  const (
    22  	// After gzipping the logs we compress by this factor on avg. We use this
    23  	// to calculate the amount of raw log bytes we should read when sending.
    24  	AvgCompressionRatio        = 5
    25  	LogSendDefaultBytesDesktop = 1024 * 1024 * 16
    26  	// NOTE: On mobile we may store less than the number of bytes we attempt to
    27  	// send. See go/libkb/env.go:Env.GetLogFileConfig
    28  	LogSendDefaultBytesMobileWifi   = 1024 * 1024 * 10
    29  	LogSendDefaultBytesMobileNoWifi = 1024 * 1024 * 1
    30  	LogSendMaxBytes                 = 1024 * 1024 * 128
    31  )
    32  
    33  // Logs is the struct to specify the path of log files
    34  type Logs struct {
    35  	GUI        string
    36  	Kbfs       string
    37  	Service    string
    38  	EK         string
    39  	Perf       string
    40  	KbfsPerf   string
    41  	GitPerf    string
    42  	Updater    string
    43  	Start      string
    44  	Install    string
    45  	System     string
    46  	Git        string
    47  	Trace      string
    48  	CPUProfile string
    49  	Watchdog   string
    50  	Processes  string
    51  }
    52  
    53  // LogSendContext for LogSend
    54  type LogSendContext struct {
    55  	libkb.Contextified
    56  
    57  	InstallID        libkb.InstallID
    58  	UID              keybase1.UID
    59  	StatusJSON       string
    60  	NetworkStatsJSON string
    61  	Feedback         string
    62  
    63  	Logs Logs
    64  
    65  	kbfsLog          string
    66  	svcLog           string
    67  	ekLog            string
    68  	perfLog          string
    69  	desktopLog       string
    70  	updaterLog       string
    71  	startLog         string
    72  	installLog       string
    73  	systemLog        string
    74  	gitLog           string
    75  	traceBundle      []byte
    76  	cpuProfileBundle []byte
    77  	watchdogLog      string
    78  	processesLog     string
    79  }
    80  
    81  var noncharacterRxx = regexp.MustCompile(`[^\w]`)
    82  
    83  const redactedReplacer = "[REDACTED]"
    84  const serialPaperKeyWordThreshold = 6
    85  
    86  func redactPotentialPaperKeys(s string) string {
    87  	doubleDelimited := noncharacterRxx.ReplaceAllFunc([]byte(s), func(x []byte) []byte {
    88  		return []byte{'~', '~', '~', x[0], '~', '~', '~'} // regexp is single char so we can take first elem
    89  	})
    90  	allWords := strings.Split(string(doubleDelimited), "~~~")
    91  	var checkWords []string
    92  	var checkWordLocations []int // keep track of each checkWord's index in allWords
    93  	for idx, word := range allWords {
    94  		if !(len(word) == 1 && noncharacterRxx.MatchString(word)) {
    95  			checkWords = append(checkWords, word)
    96  			checkWordLocations = append(checkWordLocations, idx)
    97  		}
    98  	}
    99  	didRedact := false
   100  	start := -1
   101  	for idx, word := range checkWords {
   102  		if !libkb.ValidSecWord(word) {
   103  			start = -1
   104  			continue
   105  		}
   106  		switch {
   107  		case start == -1:
   108  			start = idx
   109  		case idx-start+1 == serialPaperKeyWordThreshold:
   110  			for jdx := start; jdx <= idx; jdx++ {
   111  				allWords[checkWordLocations[jdx]] = redactedReplacer
   112  			}
   113  			didRedact = true
   114  		case idx-start+1 > serialPaperKeyWordThreshold:
   115  			allWords[checkWordLocations[idx]] = redactedReplacer
   116  		}
   117  	}
   118  	if didRedact {
   119  		return "[redacted feedback follows] " + strings.Join(allWords, "")
   120  	}
   121  	return s
   122  }
   123  
   124  func NewLogSendContext(g *libkb.GlobalContext, fstatus *keybase1.FullStatus, statusJSON, networkStatsJSON, feedback string) *LogSendContext {
   125  	logs := logFilesFromStatus(g, fstatus)
   126  
   127  	var uid keybase1.UID
   128  	if fstatus != nil && fstatus.CurStatus.User != nil {
   129  		uid = fstatus.CurStatus.User.Uid
   130  	} else {
   131  		uid = g.Env.GetUID()
   132  	}
   133  	if uid.IsNil() {
   134  		g.Log.Info("Not sending up a UID for logged in user; none found")
   135  	}
   136  
   137  	return &LogSendContext{
   138  		Contextified:     libkb.NewContextified(g),
   139  		UID:              uid,
   140  		InstallID:        g.Env.GetInstallID(),
   141  		StatusJSON:       statusJSON,
   142  		NetworkStatsJSON: networkStatsJSON,
   143  		Feedback:         feedback,
   144  		Logs:             logs,
   145  	}
   146  }
   147  
   148  func (l *LogSendContext) post(mctx libkb.MetaContext) (keybase1.LogSendID, error) {
   149  	mctx.Debug("sending status + logs to keybase")
   150  
   151  	var body bytes.Buffer
   152  	mpart := multipart.NewWriter(&body)
   153  
   154  	if l.Feedback != "" {
   155  		feedback := redactPotentialPaperKeys(l.Feedback)
   156  		err := mpart.WriteField("feedback", feedback)
   157  		if err != nil {
   158  			return "", err
   159  		}
   160  	}
   161  
   162  	if len(l.InstallID) > 0 {
   163  		err := mpart.WriteField("install_id", string(l.InstallID))
   164  		if err != nil {
   165  			return "", err
   166  		}
   167  	}
   168  
   169  	if !l.UID.IsNil() {
   170  		err := mpart.WriteField("uid", l.UID.String())
   171  		if err != nil {
   172  			return "", err
   173  		}
   174  	}
   175  
   176  	if err := addGzippedFile(mpart, "status_gz", "status.gz", l.StatusJSON); err != nil {
   177  		return "", err
   178  	}
   179  	if err := addGzippedFile(mpart, "network_stats_gz", "network_stats.gz", l.NetworkStatsJSON); err != nil {
   180  		return "", err
   181  	}
   182  	if err := addGzippedFile(mpart, "kbfs_log_gz", "kbfs_log.gz", l.kbfsLog); err != nil {
   183  		return "", err
   184  	}
   185  	if err := addGzippedFile(mpart, "keybase_log_gz", "keybase_log.gz", l.svcLog); err != nil {
   186  		return "", err
   187  	}
   188  	if err := addGzippedFile(mpart, "ek_log_gz", "ek_log.gz", l.ekLog); err != nil {
   189  		return "", err
   190  	}
   191  	if err := addGzippedFile(mpart, "perf_log_gz", "perf_log.gz", l.perfLog); err != nil {
   192  		return "", err
   193  	}
   194  	if err := addGzippedFile(mpart, "updater_log_gz", "updater_log.gz", l.updaterLog); err != nil {
   195  		return "", err
   196  	}
   197  	if err := addGzippedFile(mpart, "gui_log_gz", "gui_log.gz", l.desktopLog); err != nil {
   198  		return "", err
   199  	}
   200  	if err := addGzippedFile(mpart, "start_log_gz", "start_log.gz", l.startLog); err != nil {
   201  		return "", err
   202  	}
   203  	if err := addGzippedFile(mpart, "install_log_gz", "install_log.gz", l.installLog); err != nil {
   204  		return "", err
   205  	}
   206  	if err := addGzippedFile(mpart, "system_log_gz", "system_log.gz", l.systemLog); err != nil {
   207  		return "", err
   208  	}
   209  	if err := addGzippedFile(mpart, "git_log_gz", "git_log.gz", l.gitLog); err != nil {
   210  		return "", err
   211  	}
   212  	if err := addGzippedFile(mpart, "watchdog_log_gz", "watchdog_log.gz", l.watchdogLog); err != nil {
   213  		return "", err
   214  	}
   215  	if err := addGzippedFile(mpart, "processes_log_gz", "processes_log.gz", l.processesLog); err != nil {
   216  		return "", err
   217  	}
   218  
   219  	if len(l.traceBundle) > 0 {
   220  		mctx.Debug("trace bundle size: %d", len(l.traceBundle))
   221  		if err := addFile(mpart, "trace_tar_gz", "trace.tar.gz", l.traceBundle); err != nil {
   222  			return "", err
   223  		}
   224  	}
   225  
   226  	if len(l.cpuProfileBundle) > 0 {
   227  		mctx.Debug("CPU profile bundle size: %d", len(l.cpuProfileBundle))
   228  		if err := addFile(mpart, "cpu_profile_tar_gz", "cpu_profile.tar.gz", l.cpuProfileBundle); err != nil {
   229  			return "", err
   230  		}
   231  	}
   232  
   233  	if err := mpart.Close(); err != nil {
   234  		return "", err
   235  	}
   236  
   237  	mctx.Debug("body size: %s", humanize.Bytes(uint64(body.Len())))
   238  
   239  	arg := libkb.APIArg{
   240  		Endpoint:        "logdump/send",
   241  		SessionType:     libkb.APISessionTypeOPTIONAL,
   242  		RetryCount:      2,
   243  		RetryMultiplier: 1.3,
   244  		InitialTimeout:  5 * time.Minute,
   245  	}
   246  
   247  	resp, err := mctx.G().API.PostRaw(mctx, arg, mpart.FormDataContentType(), &body)
   248  	if err != nil {
   249  		mctx.Debug("post error: %s", err)
   250  		return "", err
   251  	}
   252  
   253  	id, err := resp.Body.AtKey("logdump_id").GetString()
   254  	if err != nil {
   255  		return "", err
   256  	}
   257  
   258  	return keybase1.LogSendID(id), nil
   259  }
   260  
   261  // LogSend sends the tails of log files to kb, and also the last few trace
   262  // output files.
   263  func (l *LogSendContext) LogSend(sendLogs bool, numBytes int, mergeExtendedStatus, addNetworkStats bool) (id keybase1.LogSendID, err error) {
   264  	if numBytes < 1 {
   265  		numBytes = LogSendDefaultBytesDesktop
   266  	} else if numBytes > LogSendMaxBytes {
   267  		numBytes = LogSendMaxBytes
   268  	}
   269  	mctx := libkb.NewMetaContextBackground(l.G()).WithLogTag("LOGSEND")
   270  	defer mctx.Trace(fmt.Sprintf("LogSend sendLogs: %v numBytes: %s",
   271  		sendLogs, humanize.Bytes(uint64(numBytes))), &err)()
   272  
   273  	logs := l.Logs
   274  	// So far, install logs are Windows only
   275  	if logs.Install != "" {
   276  		defer os.Remove(logs.Install)
   277  	}
   278  	// So far, watchdog logs are Windows only
   279  	if logs.Watchdog != "" {
   280  		defer os.Remove(logs.Watchdog)
   281  	}
   282  
   283  	if sendLogs {
   284  		// Increase some log files by the average compression ratio size
   285  		// so we have more comprehensive coverage there.
   286  		l.svcLog = tail(l.G().Log, "service", logs.Service, numBytes*AvgCompressionRatio)
   287  		l.ekLog = tail(l.G().Log, "ek", logs.EK, numBytes)
   288  
   289  		{
   290  			// Scope these logs so they can be GC'd after this block.
   291  			servicePerfLog := tail(l.G().Log, "perf", logs.Perf, numBytes)
   292  			kbfsPerfLog := tail(l.G().Log, "kbfsPerf", logs.KbfsPerf, numBytes)
   293  			gitPerfLog := tail(l.G().Log, "gitPerf", logs.GitPerf, numBytes)
   294  			l.perfLog = zipLogs(
   295  				numBytes, servicePerfLog, kbfsPerfLog, gitPerfLog)
   296  		}
   297  
   298  		l.kbfsLog = tail(l.G().Log, "kbfs", logs.Kbfs, numBytes*AvgCompressionRatio)
   299  		l.desktopLog = tail(l.G().Log, "gui", logs.GUI, numBytes)
   300  		l.updaterLog = tail(l.G().Log, "updater", logs.Updater, numBytes)
   301  		// We don't use the systemd journal to store regular logs, since on
   302  		// some systems (e.g. Ubuntu 16.04) it's not persisted across boots.
   303  		// However we do use it for startup logs, since that's the only place
   304  		// to get them in systemd mode.
   305  		if l.G().Env.WantsSystemd() {
   306  			l.startLog = tailSystemdJournal(l.G().Log, []string{
   307  				"keybase.service",
   308  				"kbfs.service",
   309  				"keybase.gui.service",
   310  				"keybase-redirector.service",
   311  			}, numBytes)
   312  		} else {
   313  			l.startLog = tail(l.G().Log, "start", logs.Start, numBytes)
   314  		}
   315  		l.installLog = tail(l.G().Log, "install", logs.Install, numBytes)
   316  		l.systemLog = tail(l.G().Log, "system", logs.System, numBytes)
   317  		l.gitLog = tail(l.G().Log, "git", logs.Git, numBytes)
   318  		l.watchdogLog = tail(l.G().Log, "watchdog", logs.Watchdog, numBytes)
   319  		if logs.Trace != "" {
   320  			l.traceBundle = getTraceBundle(l.G().Log, logs.Trace)
   321  		}
   322  		if logs.CPUProfile != "" {
   323  			l.cpuProfileBundle = getCPUProfileBundle(l.G().Log, logs.CPUProfile)
   324  		}
   325  		// Only add extended status if we're sending logs
   326  		if mergeExtendedStatus {
   327  			l.StatusJSON = l.mergeExtendedStatus(l.StatusJSON)
   328  		}
   329  		if addNetworkStats {
   330  			localStats, err := mctx.G().LocalNetworkInstrumenterStorage.Stats(mctx.Ctx())
   331  			if err != nil {
   332  				return "", err
   333  			}
   334  			remoteStats, err := mctx.G().RemoteNetworkInstrumenterStorage.Stats(mctx.Ctx())
   335  			if err != nil {
   336  				return "", err
   337  			}
   338  			networkStatsJSON, err := json.Marshal(libkb.NetworkStatsJSON{
   339  				Local:  localStats,
   340  				Remote: remoteStats,
   341  			})
   342  			if err != nil {
   343  				return "", err
   344  			}
   345  			l.NetworkStatsJSON = string(networkStatsJSON)
   346  		}
   347  		l.processesLog = keybaseProcessList()
   348  	}
   349  
   350  	return l.post(mctx)
   351  }
   352  
   353  // mergeExtendedStatus adds the extended status to the given status json blob.
   354  // If any errors occur the original status is returned unmodified.
   355  func (l *LogSendContext) mergeExtendedStatus(status string) string {
   356  	extStatus, err := GetExtendedStatus(libkb.NewMetaContextTODO(l.G()))
   357  	if err != nil {
   358  		return status
   359  	}
   360  	return MergeStatusJSON(extStatus, "extstatus", status)
   361  }
   362  
   363  // Clear removes any log data that we don't want to stick around until the
   364  // next time LogSend is called, in case sendLogs is false the next time.
   365  func (l *LogSendContext) Clear() {
   366  	l.svcLog = ""
   367  	l.ekLog = ""
   368  	l.perfLog = ""
   369  	l.kbfsLog = ""
   370  	l.desktopLog = ""
   371  	l.updaterLog = ""
   372  	l.startLog = ""
   373  	l.installLog = ""
   374  	l.systemLog = ""
   375  	l.gitLog = ""
   376  	l.watchdogLog = ""
   377  	l.traceBundle = []byte{}
   378  	l.cpuProfileBundle = []byte{}
   379  	l.processesLog = ""
   380  	l.StatusJSON = ""
   381  	l.NetworkStatsJSON = ""
   382  }