mirror of
https://github.com/gopasspw/gopass.git
synced 2025-12-08 19:24:54 +00:00
* [chore] Add PID to the debug logs This helps differentiate between gopass foreground and background (e.g. agent) processes. Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> * [chore] Adjust logging severities and improve pretty printing Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org> --------- Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
393 lines
9.3 KiB
Go
393 lines
9.3 KiB
Go
// Copyright 2021 The gopass Authors. All rights reserved.
|
|
// Use of this source code is governed by the MIT license,
|
|
// that can be found in the LICENSE file.
|
|
|
|
// Gopass implements the gopass command line tool.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"runtime"
|
|
rdebug "runtime/debug"
|
|
"runtime/pprof"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/blang/semver/v4"
|
|
"github.com/fatih/color"
|
|
ap "github.com/gopasspw/gopass/internal/action"
|
|
"github.com/gopasspw/gopass/internal/action/exit"
|
|
"github.com/gopasspw/gopass/internal/action/pwgen"
|
|
_ "github.com/gopasspw/gopass/internal/backend/crypto"
|
|
"github.com/gopasspw/gopass/internal/backend/crypto/gpg"
|
|
_ "github.com/gopasspw/gopass/internal/backend/storage"
|
|
"github.com/gopasspw/gopass/internal/config"
|
|
"github.com/gopasspw/gopass/internal/hook"
|
|
"github.com/gopasspw/gopass/internal/out"
|
|
"github.com/gopasspw/gopass/internal/queue"
|
|
"github.com/gopasspw/gopass/internal/store/leaf"
|
|
"github.com/gopasspw/gopass/pkg/ctxutil"
|
|
"github.com/gopasspw/gopass/pkg/debug"
|
|
"github.com/gopasspw/gopass/pkg/protect"
|
|
"github.com/gopasspw/gopass/pkg/termio"
|
|
colorable "github.com/mattn/go-colorable"
|
|
"github.com/mattn/go-isatty"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
const (
|
|
name = "gopass"
|
|
)
|
|
|
|
// Version is the released version of gopass.
|
|
var version string
|
|
|
|
func main() {
|
|
// important: execute the func now but the returned func only on defer!
|
|
// Example: https://go.dev/play/p/8214zCX6hVq.
|
|
defer writeCPUProfile()()
|
|
|
|
if err := protect.Pledge("stdio rpath wpath cpath tty proc exec fattr"); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// trap Ctrl+C and call cancel on the context
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt)
|
|
|
|
defer func() {
|
|
signal.Stop(sigChan)
|
|
cancel()
|
|
}()
|
|
|
|
go func(ctx context.Context) {
|
|
select {
|
|
case <-sigChan:
|
|
cancel()
|
|
case <-ctx.Done():
|
|
}
|
|
}(ctx)
|
|
|
|
cli.ErrWriter = errorWriter{ //nolint:reassign
|
|
out: colorable.NewColorableStderr(),
|
|
}
|
|
sv := getVersion()
|
|
cli.VersionPrinter = makeVersionPrinter(os.Stdout, sv)
|
|
|
|
debug.Log("gopass %s starting. Args: %v", sv.String(), os.Args)
|
|
|
|
// run the app
|
|
q := queue.New(ctx)
|
|
ctx = queue.WithQueue(ctx, q)
|
|
ctx, app := setupApp(ctx, sv)
|
|
|
|
if err := app.RunContext(ctx, os.Args); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// process all pending queue items
|
|
_ = q.Close(ctx)
|
|
|
|
writeMemProfile()
|
|
|
|
debug.Log("gopass %s shutting down ...\n\n", sv.String())
|
|
}
|
|
|
|
//nolint:wrapcheck
|
|
func setupApp(ctx context.Context, sv semver.Version) (context.Context, *cli.App) {
|
|
// try to read config (if it exists)
|
|
cfg := config.New()
|
|
|
|
// set config values
|
|
ctx = initContext(ctx, cfg)
|
|
|
|
// initialize action handlers
|
|
action, err := ap.New(cfg, sv)
|
|
if err != nil {
|
|
out.Errorf(ctx, "failed to initialize gopass: %s", err)
|
|
os.Exit(exit.Unknown)
|
|
}
|
|
|
|
// set some action callbacks
|
|
if !config.AsBool(cfg.Get("core.autoimport")) {
|
|
ctx = ctxutil.WithImportFunc(ctx, termio.AskForKeyImport)
|
|
}
|
|
|
|
ctx = leaf.WithFsckFunc(ctx, termio.AskForConfirmation)
|
|
|
|
app := cli.NewApp()
|
|
|
|
app.Name = name
|
|
app.Version = sv.String()
|
|
app.Usage = "The standard unix password manager - rewritten in Go"
|
|
app.UseShortOptionHandling = true
|
|
app.EnableBashCompletion = true
|
|
app.BashComplete = func(c *cli.Context) {
|
|
cli.DefaultAppComplete(c)
|
|
action.Complete(c)
|
|
}
|
|
|
|
app.Flags = ap.ShowFlags()
|
|
app.Action = func(c *cli.Context) error {
|
|
if err := action.IsInitialized(c); err != nil {
|
|
return err
|
|
}
|
|
|
|
if c.Args().Present() {
|
|
return action.Show(c)
|
|
}
|
|
|
|
return action.REPL(c)
|
|
}
|
|
|
|
app.Commands = getCommands(action, app)
|
|
|
|
return ctx, app
|
|
}
|
|
|
|
func getCommands(action *ap.Action, app *cli.App) []*cli.Command {
|
|
cmds := []*cli.Command{
|
|
{
|
|
Name: "completion",
|
|
Usage: "Bash and ZSH completion",
|
|
Description: "" +
|
|
"Source the output of this command with bash or zsh to get auto completion",
|
|
Subcommands: []*cli.Command{{
|
|
Name: "bash",
|
|
Usage: "Source for auto completion in bash",
|
|
Action: action.CompletionBash,
|
|
}, {
|
|
Name: "zsh",
|
|
Usage: "Source for auto completion in zsh",
|
|
Action: func(c *cli.Context) error {
|
|
return action.CompletionZSH(app) //nolint:wrapcheck
|
|
},
|
|
}, {
|
|
Name: "fish",
|
|
Usage: "Source for auto completion in fish",
|
|
Action: func(c *cli.Context) error {
|
|
return action.CompletionFish(app) //nolint:wrapcheck
|
|
},
|
|
}, {
|
|
Name: "openbsdksh",
|
|
Usage: "Source for auto completion in OpenBSD's ksh",
|
|
Action: func(c *cli.Context) error {
|
|
return action.CompletionOpenBSDKsh(app) //nolint:wrapcheck
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
|
|
cmds = append(cmds, action.GetCommands()...)
|
|
cmds = append(cmds, pwgen.GetCommands()...)
|
|
sort.Slice(cmds, func(i, j int) bool { return cmds[i].Name < cmds[j].Name })
|
|
|
|
for i, cmd := range cmds {
|
|
// fmt.Printf("[%6d - %10s] Before: %p - After %p\n", i, cmds[i].Name, cmds[i].Before, cmds[i].After)
|
|
cmds[i].Before = mkHookFn("core.pre-hook", cmd.Name, action.Store, cmd.Before)
|
|
cmds[i].After = mkHookFn("core.post-hook", cmd.Name, action.Store, cmd.After)
|
|
// fmt.Printf("[%6d - %10s] Before: %p - After %p\n", i, cmds[i].Name, cmds[i].Before, cmds[i].After)
|
|
// fmt.Println()
|
|
}
|
|
|
|
return cmds
|
|
}
|
|
|
|
type pathGetter interface {
|
|
Path() string
|
|
}
|
|
|
|
func mkHookFn(hookName, cmdName string, s pathGetter, fn func(c *cli.Context) error) func(c *cli.Context) error {
|
|
if fn == nil {
|
|
return func(c *cli.Context) error {
|
|
dir := config.String(c.Context, "mounts.path")
|
|
|
|
return hook.Invoke(c.Context, hookName, dir, cmdName)
|
|
}
|
|
}
|
|
|
|
return func(c *cli.Context) error {
|
|
if err := fn(c); err != nil {
|
|
return err
|
|
}
|
|
|
|
return hook.Invoke(c.Context, hookName, s.Path(), cmdName, c.Args().First())
|
|
}
|
|
}
|
|
|
|
func parseBuildInfo() (string, string, string) {
|
|
bi, ok := rdebug.ReadBuildInfo()
|
|
if !ok {
|
|
return "HEAD", "", ""
|
|
}
|
|
|
|
var (
|
|
commit string
|
|
date string
|
|
dirty string
|
|
)
|
|
|
|
for _, v := range bi.Settings {
|
|
switch v.Key {
|
|
case "vcs.revision":
|
|
commit = v.Value[len(v.Value)-8:]
|
|
case "vcs.time":
|
|
if bt, err := time.Parse("2006-01-02T15:04:05Z", date); err == nil {
|
|
date = bt.Format("2006-01-02 15:04:05")
|
|
}
|
|
case "vcs.modified":
|
|
if v.Value == "true" {
|
|
dirty = " (dirty)"
|
|
}
|
|
}
|
|
}
|
|
|
|
return commit, date, dirty
|
|
}
|
|
|
|
func makeVersionPrinter(out io.Writer, sv semver.Version) func(c *cli.Context) {
|
|
return func(c *cli.Context) {
|
|
commit, buildtime, dirty := parseBuildInfo()
|
|
buildInfo := ""
|
|
|
|
if commit != "" {
|
|
buildInfo = commit + dirty
|
|
}
|
|
|
|
if buildtime != "" {
|
|
if buildInfo != "" {
|
|
buildInfo += " "
|
|
}
|
|
|
|
buildInfo += buildtime
|
|
}
|
|
|
|
if buildInfo != "" {
|
|
buildInfo = "(" + buildInfo + ") "
|
|
}
|
|
|
|
fmt.Fprintf(
|
|
out,
|
|
"%s %s %s%s %s %s\n",
|
|
name,
|
|
sv.String(),
|
|
buildInfo,
|
|
runtime.Version(),
|
|
runtime.GOOS,
|
|
runtime.GOARCH,
|
|
)
|
|
}
|
|
}
|
|
|
|
type errorWriter struct {
|
|
out io.Writer
|
|
}
|
|
|
|
func (e errorWriter) Write(p []byte) (int, error) {
|
|
return e.out.Write([]byte("\n" + color.RedString("Error: %s", p))) //nolint:wrapcheck
|
|
}
|
|
|
|
func initContext(ctx context.Context, cfg *config.Config) context.Context {
|
|
// initialize from config, may be overridden by env vars
|
|
ctx = cfg.WithConfig(ctx)
|
|
|
|
// always trust
|
|
ctx = gpg.WithAlwaysTrust(ctx, true)
|
|
|
|
// only emit color codes when stdout is a terminal
|
|
if !isatty.IsTerminal(os.Stdout.Fd()) {
|
|
color.NoColor = true
|
|
ctx = ctxutil.WithTerminal(ctx, false)
|
|
ctx = ctxutil.WithInteractive(ctx, false)
|
|
}
|
|
|
|
// reading from stdin?
|
|
if info, err := os.Stdin.Stat(); err == nil && info.Mode()&os.ModeCharDevice == 0 {
|
|
ctx = ctxutil.WithInteractive(ctx, false)
|
|
ctx = ctxutil.WithStdin(ctx, true)
|
|
}
|
|
|
|
// enable following references based on the configuration
|
|
ctx = ctxutil.WithFollowRef(ctx, config.AsBool(cfg.Get("core.follow-references")))
|
|
|
|
// disable colored output on windows since cmd.exe doesn't support ANSI color
|
|
// codes. Other terminal may do, but until we can figure that out better
|
|
// disable this for all terms on this platform
|
|
if sv := os.Getenv("NO_COLOR"); runtime.GOOS == "windows" || sv == "true" {
|
|
color.NoColor = true
|
|
} else {
|
|
// on all other platforms we should be able to use color. Only set
|
|
// this if it's in the config.
|
|
if cfg.IsSet("core.nocolor") {
|
|
color.NoColor = config.AsBool(cfg.Get("core.nocolor"))
|
|
}
|
|
}
|
|
|
|
// using a password callback for age identity file or not?
|
|
if pw, isSet := os.LookupEnv("GOPASS_AGE_PASSWORD"); isSet {
|
|
ctx = ctxutil.WithPasswordCallback(ctx, func(_ string, _ bool) ([]byte, error) {
|
|
debug.Log("using age password callback from env variable GOPASS_AGE_PASSWORD")
|
|
|
|
return []byte(pw), nil
|
|
})
|
|
}
|
|
|
|
return ctx
|
|
}
|
|
|
|
func writeCPUProfile() func() {
|
|
cp := os.Getenv("GOPASS_CPU_PROFILE")
|
|
if cp == "" {
|
|
return func() {}
|
|
}
|
|
|
|
f, err := os.Create(cp)
|
|
if err != nil {
|
|
log.Fatalf("could not create CPU profile at %s: %s", cp, err)
|
|
}
|
|
|
|
if err := pprof.StartCPUProfile(f); err != nil {
|
|
log.Fatalf("could not start CPU profile: %s", err)
|
|
}
|
|
|
|
return func() {
|
|
pprof.StopCPUProfile()
|
|
|
|
_ = f.Close()
|
|
|
|
debug.Log("wrote CPU profile to %s", cp)
|
|
}
|
|
}
|
|
|
|
func writeMemProfile() {
|
|
mp := os.Getenv("GOPASS_MEM_PROFILE")
|
|
if mp == "" {
|
|
return
|
|
}
|
|
|
|
f, err := os.Create(mp)
|
|
if err != nil {
|
|
log.Fatalf("could not write mem profile to %s: %s", mp, err)
|
|
}
|
|
|
|
defer func() {
|
|
_ = f.Close()
|
|
}()
|
|
|
|
runtime.GC() // get up-to-date statistics
|
|
|
|
if err := pprof.WriteHeapProfile(f); err != nil {
|
|
log.Fatalf("could not write heap profile: %s", err)
|
|
}
|
|
|
|
debug.Log("wrote heap profile to %s", mp)
|
|
}
|