initial
This commit is contained in:
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
*.7z
|
||||
*.bak
|
||||
*.deb
|
||||
*.jar
|
||||
*.rar
|
||||
*.run
|
||||
*.sig
|
||||
*.tar
|
||||
*.tar.bz2
|
||||
*.tar.gz
|
||||
*.tar.xz
|
||||
*.tbz
|
||||
*.tbz2
|
||||
*.tgz
|
||||
*.txz
|
||||
*.zip
|
||||
.*.swp
|
||||
.editix
|
||||
|
||||
# https://github.com/github/gitignore/blob/master/Go.gitignore
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
#*.test
|
||||
test.sh
|
||||
/test/
|
||||
|
||||
# Built binaries
|
||||
bin/*
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Example configs.
|
||||
_exampledata/
|
||||
|
||||
# Don't include rendered doc
|
||||
/README.html
|
||||
|
||||
# Example code.
|
||||
_demo/
|
||||
118
build.sh
Executable file
118
build.sh
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# This is not portable. It has bashisms.
|
||||
|
||||
# Like this (bash >= 4.x)
|
||||
declare -A os_sfx=(
|
||||
["linux"]='bin'
|
||||
["windows"]='exe'
|
||||
["darwin"]='app'
|
||||
)
|
||||
|
||||
BUILD_TIME="$(date '+%s')"
|
||||
BUILD_USER="$(whoami)"
|
||||
BUILD_SUDO_USER="${SUDO_USER}"
|
||||
BUILD_HOST="$(hostname)"
|
||||
|
||||
# Check to make sure git is available.
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "Git is not available; automatic version handling unsupported."
|
||||
echo "You must build by calling 'go build' directly in the respective directories."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check git directory/repository.
|
||||
if ! git rev-parse --is-inside-work-tree &> /dev/null; then
|
||||
echo "Not running inside a git work tree; automatic version handling unsupported/build script unsupported."
|
||||
echo "You must build by calling 'go build' directly in the respective directories instead."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If it has a tag in the path of the current HEAD that matches a version string...
|
||||
# I wish git describe supported regex. It does not; only globs. Gross.
|
||||
# If there's a bug anywhere, it's here.
|
||||
if git describe --tags --abbrev=0 --match "v[0-9]*" HEAD &> /dev/null; then
|
||||
# It has a tag we can use.
|
||||
CURRENT_VER="$(git describe --tags --abbrev=0 --match "v[0-9]*" HEAD)"
|
||||
COMMITS_SINCE="$(git rev-list --count ${CURRENT_VER}..HEAD)"
|
||||
else
|
||||
# No tag available.
|
||||
CURRENT_VER=""
|
||||
COMMITS_SINCE=""
|
||||
fi
|
||||
|
||||
# If it's dirty (staged but not committed or unstaged files)...
|
||||
if ! git diff-index --quiet HEAD; then
|
||||
# It's dirty.
|
||||
IS_DIRTY="1"
|
||||
else
|
||||
# It's clean.
|
||||
IS_DIRTY="0"
|
||||
fi
|
||||
|
||||
# Get the commit hash of the *most recent* commit in the path of current HEAD...
|
||||
CURRENT_HASH="$(git rev-parse --verify HEAD)"
|
||||
# The same as above, but abbreviated.
|
||||
CURRENT_SHORT="$(git rev-parse --verify --short HEAD)"
|
||||
|
||||
# Get the module name.
|
||||
MODPATH="$(sed -n -re 's@^\s*module\s+(.*)(//.*)?$@\1@p' go.mod)"
|
||||
|
||||
# Build the ldflags string.
|
||||
# BEHOLD! BASH WITCHCRAFT.
|
||||
LDFLAGS_STR="\
|
||||
-X '${MODPATH}/version.version=${CURRENT_VER}' \
|
||||
-X '${MODPATH}/version.commitHash=${CURRENT_HASH}' \
|
||||
-X '${MODPATH}/version.commitShort=${CURRENT_SHORT}' \
|
||||
-X '${MODPATH}/version.numCommitsAfterTag=${COMMITS_SINCE}' \
|
||||
-X '${MODPATH}/version.isDirty=${IS_DIRTY}' \
|
||||
-X '${MODPATH}/version.buildTime=${BUILD_TIME}' \
|
||||
-X '${MODPATH}/version.buildUser=${BUILD_USER}' \
|
||||
-X '${MODPATH}/version.buildSudoUser=${BUILD_SUDO_USER}' \
|
||||
-X '${MODPATH}/version.buildHost=${BUILD_HOST}'"
|
||||
|
||||
set -u
|
||||
|
||||
# And finally build.
|
||||
mkdir -p ./bin/
|
||||
export CGO_ENABLED=0
|
||||
_origdir="$(pwd)"
|
||||
_pfx=''
|
||||
#for cmd in 'discord' 'irc' 'msteams' 'slack' 'xmpp'; do
|
||||
for cmd_dir in cmd/*; do
|
||||
cmd="$(basename "${cmd_dir}")"
|
||||
echo "${cmd}..."
|
||||
if [ ! -f "${cmd_dir}/main.go" ]; then
|
||||
continue
|
||||
fi
|
||||
cd "${cmd_dir}"
|
||||
_bin="${_pfx}${cmd}"
|
||||
for ga in 'amd64' 'arm64'; do
|
||||
as="${ga}"
|
||||
if [[ "${ga}" == 'amd64' ]]; then
|
||||
as='x86_64'
|
||||
fi
|
||||
echo -e "\t${as}..."
|
||||
for osnm in "${!os_sfx[@]}"; do
|
||||
echo -e "\t\t${osnm}: "
|
||||
_sfx="${os_sfx[$osnm]}"
|
||||
bin="${_origdir}/bin/${_bin}-${as}-${CURRENT_VER:-dev}.${_sfx}"
|
||||
export GOOS="${osnm}"
|
||||
export GOARCH="${ga}"
|
||||
echo -e -n "\t\tBuilding '${bin}'..."
|
||||
go build \
|
||||
-o "${bin}" \
|
||||
-ldflags \
|
||||
"${LDFLAGS_STR}"
|
||||
# "${LDFLAGS_STR}" \
|
||||
# *.go
|
||||
echo " Done."
|
||||
done
|
||||
echo -e "\tDone."
|
||||
done
|
||||
echo "Done."
|
||||
done
|
||||
|
||||
echo -e "\nBuild complete."
|
||||
70
cmd/kill_totp_secrets/main.go
Normal file
70
cmd/kill_totp_secrets/main.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`log`
|
||||
`os`
|
||||
|
||||
`update_vault_totp/internal`
|
||||
|
||||
"golang.org/x/term"
|
||||
"r00t2.io/sysutils/envs"
|
||||
)
|
||||
|
||||
func getToken() (tok string, err error) {
|
||||
|
||||
var p1 []byte
|
||||
var oldState *term.State
|
||||
|
||||
if envs.HasEnv(internal.VaultTokEnv) {
|
||||
tok = os.Getenv(internal.VaultTokEnv)
|
||||
return
|
||||
}
|
||||
|
||||
// Prompt for it instead
|
||||
fmt.Println("Vault token needed.\nVault token (will not be echoed back):")
|
||||
if oldState, err = term.GetState(internal.TermFd); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err = term.Restore(internal.TermFd, oldState); err != nil {
|
||||
log.Println("restore failed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if p1, err = term.ReadPassword(internal.TermFd); err != nil {
|
||||
return
|
||||
}
|
||||
tok = string(p1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var errs []error
|
||||
var tok string
|
||||
var c *internal.Client
|
||||
|
||||
if tok, err = getToken(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if c, err = internal.New(tok, internal.DefAddr, internal.TgtMnt, internal.CollNm); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
c.DeleteAllVaultKeys()
|
||||
|
||||
if err = c.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if errs = c.Errors(); len(errs) > 0 {
|
||||
for _, err = range errs {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Done.")
|
||||
}
|
||||
70
cmd/update_vault_totp/main.go
Normal file
70
cmd/update_vault_totp/main.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`fmt`
|
||||
`log`
|
||||
`os`
|
||||
|
||||
`update_vault_totp/internal`
|
||||
|
||||
"golang.org/x/term"
|
||||
"r00t2.io/sysutils/envs"
|
||||
)
|
||||
|
||||
func getToken() (tok string, err error) {
|
||||
|
||||
var p1 []byte
|
||||
var oldState *term.State
|
||||
|
||||
if envs.HasEnv(internal.VaultTokEnv) {
|
||||
tok = os.Getenv(internal.VaultTokEnv)
|
||||
return
|
||||
}
|
||||
|
||||
// Prompt for it instead
|
||||
fmt.Println("Vault token needed.\nVault token (will not be echoed back):")
|
||||
if oldState, err = term.GetState(internal.TermFd); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err = term.Restore(internal.TermFd, oldState); err != nil {
|
||||
log.Println("restore failed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if p1, err = term.ReadPassword(internal.TermFd); err != nil {
|
||||
return
|
||||
}
|
||||
tok = string(p1)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
var errs []error
|
||||
var tok string
|
||||
var c *internal.Client
|
||||
|
||||
if tok, err = getToken(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if c, err = internal.New(tok, internal.DefAddr, internal.TgtMnt, internal.CollNm); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
c.Sync()
|
||||
|
||||
if err = c.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if errs = c.Errors(); len(errs) > 0 {
|
||||
for _, err = range errs {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Done.")
|
||||
}
|
||||
27
cmd/vault_totp_util/consts.go
Normal file
27
cmd/vault_totp_util/consts.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
`update_vault_totp/internal`
|
||||
|
||||
sysdUtil "github.com/coreos/go-systemd/v22/util"
|
||||
`github.com/jessevdk/go-flags`
|
||||
`r00t2.io/goutils/logging`
|
||||
)
|
||||
|
||||
var (
|
||||
isSystemd bool = sysdUtil.IsRunningSystemd()
|
||||
)
|
||||
|
||||
const (
|
||||
logFlags int = log.LstdFlags | log.Lmsgprefix
|
||||
logFlagsDebug int = logFlags | log.Llongfile
|
||||
)
|
||||
|
||||
var (
|
||||
logger *logging.MultiLogger
|
||||
args *internal.Args = new(internal.Args)
|
||||
logFlagsRuntime int = logFlags
|
||||
parser *flags.Parser = flags.NewParser(args, flags.Default)
|
||||
)
|
||||
72
cmd/vault_totp_util/main.go
Normal file
72
cmd/vault_totp_util/main.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`fmt`
|
||||
`log`
|
||||
`os`
|
||||
|
||||
`update_vault_totp/version`
|
||||
|
||||
`github.com/davecgh/go-spew/spew`
|
||||
`github.com/jessevdk/go-flags`
|
||||
`r00t2.io/goutils/logging`
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
parser.EnvNamespaceDelimiter = ""
|
||||
if _, err = parser.Parse(); err != nil {
|
||||
var flagsErr *flags.Error
|
||||
switch {
|
||||
case errors.As(err, &flagsErr):
|
||||
switch {
|
||||
case errors.Is(flagsErr.Type, flags.ErrHelp), errors.Is(flagsErr.Type, flags.ErrCommandRequired), errors.Is(flagsErr.Type, flags.ErrRequired): // These print their relevant messages by themselves.
|
||||
return
|
||||
default:
|
||||
log.Panicln(err)
|
||||
}
|
||||
default:
|
||||
log.Panicln(err)
|
||||
}
|
||||
}
|
||||
|
||||
if version.Ver, err = version.Version(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
// If args.Version or args.DetailVersion are true, just print them and exit.
|
||||
if args.DetailVersion || args.Version {
|
||||
if args.Version {
|
||||
fmt.Println(version.Ver.Short())
|
||||
return
|
||||
} else if args.DetailVersion {
|
||||
fmt.Println(version.Ver.Detail())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// We want to set up logging before anything else.
|
||||
if args.DoDebug {
|
||||
logFlagsRuntime = logFlagsDebug
|
||||
}
|
||||
logger = logging.GetMultiLogger(args.DoDebug, "ZorkBot")
|
||||
if err = logger.AddDefaultLogger(
|
||||
"default",
|
||||
logFlagsRuntime,
|
||||
"/var/log/zorkbot/zorkbot.log", "~/logs/zorkbot.log",
|
||||
); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
if err = logger.Setup(); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
defer logger.Shutdown()
|
||||
logger.Info("main: ZorkBot version %v", version.Ver.Short())
|
||||
logger.Debug("main: ZorkBot version (extended):\n%v", version.Ver.Detail())
|
||||
logger.Debug("main: Invocation args:\n%s", spew.Sdump(args))
|
||||
}
|
||||
31
go.mod
Normal file
31
go.mod
Normal file
@@ -0,0 +1,31 @@
|
||||
module update_vault_totp
|
||||
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/creachadair/otp v0.5.2
|
||||
github.com/hashicorp/vault-client-go v0.4.3
|
||||
github.com/jessevdk/go-flags v1.6.1
|
||||
golang.org/x/mod v0.23.0
|
||||
golang.org/x/term v0.38.0
|
||||
r00t2.io/gosecret v1.1.5
|
||||
r00t2.io/goutils v1.13.0
|
||||
r00t2.io/sysutils v1.15.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/creachadair/wirepb v0.0.0-20251201055919-954ef89c4c71 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
)
|
||||
66
go.sum
Normal file
66
go.sum
Normal file
@@ -0,0 +1,66 @@
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creachadair/mds v0.25.2 h1:xc0S0AfDq5GX9KUR5sLvi5XjA61/P6S5e0xFs1vA18Q=
|
||||
github.com/creachadair/mds v0.25.2/go.mod h1:+s4CFteFRj4eq2KcGHW8Wei3u9NyzSPzNV32EvjyK/Q=
|
||||
github.com/creachadair/otp v0.5.2 h1:3krOkIHA8X1YAnv9lGZi2NlyCZepcsAQkZwKGK5L+9c=
|
||||
github.com/creachadair/otp v0.5.2/go.mod h1:0cUM4kl9cQmePbIEy1J1O+esh+S3zDM9s2Foo71KzKA=
|
||||
github.com/creachadair/wirepb v0.0.0-20251201055919-954ef89c4c71 h1:H3EV2OFbtKgFAcxGJiLTNar9krJ9yfsgpE37/QY9vN8=
|
||||
github.com/creachadair/wirepb v0.0.0-20251201055919-954ef89c4c71/go.mod h1:dHvCVZSsk3Y/fhY0mUex0U2XBZszomOe7PzlghOtHnQ=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc=
|
||||
github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY=
|
||||
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
|
||||
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
r00t2.io/gosecret v1.1.5 h1:pmlsCR8VKeI9+BN7uvUqThP/BtgoSch580XOWin+qH4=
|
||||
r00t2.io/gosecret v1.1.5/go.mod h1:24jo8x5lOmWAJZJZ1J3FxhoT9+KOXSZ5pQWWzFdvdtM=
|
||||
r00t2.io/goutils v1.13.0 h1:103QR2eUu42TNF7h9r6YgahbSgRAJ8VtFxSzw9rMaXI=
|
||||
r00t2.io/goutils v1.13.0/go.mod h1:/0M+6fW2VAgdtAvwGE9oNV259MoEZeJ84rLCZsxe8uI=
|
||||
r00t2.io/sysutils v1.15.0 h1:FSnREfbXDhBQEO7LMpnRQeKlPshozxk9XHw3YgWRgRg=
|
||||
r00t2.io/sysutils v1.15.0/go.mod h1:28qB0074EIRQ8Sy/ybaA5jC3qA32iW2aYLkMCRhyAFM=
|
||||
26
internal/args.go
Normal file
26
internal/args.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package internal
|
||||
|
||||
type (
|
||||
Args struct {
|
||||
Version bool `short:"v" long:"version" description:"Print the version and exit."`
|
||||
DetailVersion bool `short:"V" long:"detail" description:"Print detailed version info and exit."`
|
||||
DoDebug bool `env:"VTOTP_DEBUG" short:"d" long:"debug" description:"If specified, enable debug logging. This may log potentially sensitive information, so be careful."`
|
||||
VaultToken string `env:"VAULT_TOKEN" short:"t" long:"vault-token" description:"Vault token to use. If empty/not defined, you will be securely prompted."`
|
||||
VaultAddr string `env:"VAULT_ADDR" short:"a" long:"vault-addr" default:"https://clandestine.r00t2.io/" description:"Vault address to use."`
|
||||
QrImgPath string `env:"VTOTP_QR" short:"q" long:"qr-img" description:"Path to QR image to extract OTPAuth URLs from. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, and/or -x/--explicit must be specified." validate:"required_without=OtpFile OtpUrl OtpExplicit,filepath"`
|
||||
OtpFile string `env:"VTOTP_FILE" short:"f" long:"otp-file" description:"Path to file containing OTPAuth URLs in plaintext, one per line. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, and/or -x/--explicit must be specified." validate:"required_without=QrImgPath OtpUrl OtpExplicit,filepath"`
|
||||
OtpUrl string `env:"VTOTP_URL" short:"u" long:"otp-url" description:"Explicit OTPAuth URL. Either -q/--qr-img, -f/--otp-file, -u/--otp-url, and/or -x/--explicit must be specified." validate:"required_without=QrImgPath OtpFile OtpExplicit,url"`
|
||||
OtpExplicit bool `short:"x" long:"otp-explicit" description:"If specified, use the explicit OTP specification under the EXPLICIT TOTP group."`
|
||||
ExplicitOtp ExplicitOtp `group:"EXPLICIT TOTP" env-namespace:"VTOTP_X"`
|
||||
}
|
||||
ExplicitOtp struct {
|
||||
Type string `env:"TYP" short:"y" long:"type" choice:"totp" choice:"hotp" default:"totp" hidden:"true" description:"The OTP type." validate:"required_with=OtpExplicit,oneof=totp hotp"`
|
||||
Counter uint64 `env:CTR" short:"c" long:"counter" hidden:"true" description:"The initial counter value (if -y/--type='hotp')." validate:"required_with=OtpExplicit,required_if=Type hotp"`
|
||||
Account string `env:"ACCT" short:"n" long:"name" description:"Name of the TOTP account (should be just the username)." validate:"required_with=OtpExplicit"`
|
||||
Issuer string `env:"ISS" short:"i" long:"issuer" description:"Issuer of the TOTP (this is generally the service name you're authing to)." validate:"required_with=OtpExplicit"`
|
||||
Secret string `env:"SSKEY" short:"s" long:"shared-secret" description:"The shared secret key in Base32 string format (with no padding)." validate:"required_with=OtpExplicit,base32"`
|
||||
Algorithm string `env:"ALGO" short:"g" long:"algo" choice:"md5" choice:"sha1" choice:"sha256" choice:"sha512" description:"The hashing/checksum algorithm." validate:"required_with=OtpExplicit,oneof=md5 sha1 sha256 sha512"`
|
||||
Digits int `env:"DIG" short:"l" long:"digits" choice:"6" choice:"8" description:"Number of digits for the generated code." validate:"required_with=OtpExplicit,oneof=6 8"`
|
||||
Period time.Duration `env:TIME" short:"p" long:"period" default:"30s" description:"The period that a generated code is valid for." validate:"required_with=OtpExplicit,required_if=Type totp"`
|
||||
}
|
||||
)
|
||||
32
internal/consts.go
Normal file
32
internal/consts.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
`os`
|
||||
)
|
||||
|
||||
const (
|
||||
DefAddr string = "https://clandestine.r00t2.io/"
|
||||
VaultTokEnv string = "VAULT_TOKEN"
|
||||
CollNm string = "OTP"
|
||||
TgtMnt string = "totp_bts.work"
|
||||
// These attrs need to be added to new SecretService TOTP secrets
|
||||
// ~/.local/share/gnome-shell/extensions/totp@dkosmari.github.com/schemas/org.gnome.shell.extensions.totp.gschema.xml
|
||||
SsSchemaName string = "xdg:schema"
|
||||
SsSchemaVal string = "org.gnome.shell.extensions.totp" // also the gosecret.ItemType
|
||||
)
|
||||
|
||||
var (
|
||||
TermFd int = int(os.Stdin.Fd())
|
||||
)
|
||||
|
||||
var (
|
||||
/*
|
||||
These map from one form to the other using introspection.
|
||||
ext: The GNOME Shell extension, ~/.local/share/gnome-shell/extensions/totp@dkosmari.github.com/secretUtils.js (makeSchema())
|
||||
Also found at https://github.com/dkosmari/gnome-shell-extension-totp/blob/master/secretUtils.js#L33-L45
|
||||
vault: A https://pkg.go.dev/github.com/hashicorp/vault-client-go@/schema#TotpCreateKeyRequest
|
||||
(reading a key returns a map[string]interface{} for ...some reason)
|
||||
url: A https://pkg.go.dev/github.com/creachadair/otp/otpauth#URL
|
||||
*/
|
||||
// TODO?
|
||||
)
|
||||
103
internal/funcs.go
Normal file
103
internal/funcs.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
`context`
|
||||
`sync`
|
||||
|
||||
`github.com/hashicorp/vault-client-go`
|
||||
`r00t2.io/gosecret`
|
||||
`r00t2.io/goutils/multierr`
|
||||
)
|
||||
|
||||
func New(vaultTok, vaultAddr, vaultMnt, collNm string) (c *Client, err error) {
|
||||
|
||||
c = &Client{
|
||||
// lastIdx: 0,
|
||||
vtok: vaultTok,
|
||||
vaddr: vaultAddr,
|
||||
scollNm: collNm,
|
||||
vmnt: vaultMnt,
|
||||
errsDone: make(chan bool, 1),
|
||||
errChan: make(chan error),
|
||||
// vc: nil,
|
||||
wg: sync.WaitGroup{},
|
||||
ctx: context.Background(),
|
||||
// ssvc: nil,
|
||||
// scoll: nil,
|
||||
mErr: multierr.NewMultiError(nil),
|
||||
// inSS: nil,
|
||||
// inVault: nil,
|
||||
}
|
||||
|
||||
if c.vc, err = vault.New(vault.WithAddress(c.vaddr)); err != nil {
|
||||
return
|
||||
}
|
||||
if err = c.vc.SetToken(c.vtok); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if c.ssvc, err = gosecret.NewService(); err != nil {
|
||||
return
|
||||
}
|
||||
if c.scoll, err = c.ssvc.GetCollection(collNm); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go c.readErrs()
|
||||
|
||||
c.wg.Add(2)
|
||||
go c.getSS()
|
||||
go c.getVault()
|
||||
|
||||
c.wg.Wait()
|
||||
|
||||
if !c.mErr.IsEmpty() {
|
||||
err = c.mErr
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func normalizeVaultNm(nm string) (normalized string) {
|
||||
|
||||
var c rune
|
||||
var idx int
|
||||
var last rune
|
||||
var repl rune = '_'
|
||||
var reduced []rune = make([]rune, 0)
|
||||
var norm []rune = make([]rune, 0, len(nm))
|
||||
|
||||
for _, c = range nm {
|
||||
// If it's "safe" chars, it's fine
|
||||
if (c == '-' || c == '.') || // 0x2d, 0x2e
|
||||
(c >= '0' && c <= '9') || // 0x30 to 0x39
|
||||
(c == '@') || // 0x40
|
||||
(c >= 'A' && c <= 'Z') || // 0x41 to 0x5a
|
||||
(c == '_') || // 0x5f
|
||||
(c >= 'a' && c <= 'z') { // 0x61 to 0x7a
|
||||
norm = append(norm, c)
|
||||
continue
|
||||
}
|
||||
// Otherwise normalize it to a safe char
|
||||
norm = append(norm, repl)
|
||||
}
|
||||
|
||||
// And remove repeating sequential replacers.
|
||||
for idx, c = range norm[:] {
|
||||
if idx == 0 {
|
||||
last = c
|
||||
reduced = append(reduced, c)
|
||||
continue
|
||||
}
|
||||
if c == last && last == repl {
|
||||
continue
|
||||
}
|
||||
reduced = append(reduced, c)
|
||||
last = c
|
||||
}
|
||||
|
||||
normalized = string(reduced)
|
||||
|
||||
return
|
||||
}
|
||||
269
internal/funcs_client.go
Normal file
269
internal/funcs_client.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`fmt`
|
||||
`math`
|
||||
`net/http`
|
||||
`strconv`
|
||||
`strings`
|
||||
|
||||
`github.com/creachadair/otp/otpauth`
|
||||
`github.com/hashicorp/vault-client-go`
|
||||
`github.com/hashicorp/vault-client-go/schema`
|
||||
`r00t2.io/gosecret`
|
||||
`r00t2.io/goutils/multierr`
|
||||
)
|
||||
|
||||
func (c *Client) Close() (err error) {
|
||||
|
||||
if err = c.ssvc.Close(); err != nil {
|
||||
c.errChan <- err
|
||||
}
|
||||
|
||||
close(c.errChan)
|
||||
|
||||
<-c.errsDone
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) DeleteAllVaultKeys() {
|
||||
|
||||
var name string
|
||||
|
||||
for name, _ = range c.inVault {
|
||||
c.wg.Add(1)
|
||||
go func(nm string) {
|
||||
var vErr error
|
||||
var resp *vault.Response[map[string]interface{}]
|
||||
|
||||
defer c.wg.Done()
|
||||
|
||||
if resp, vErr = c.vc.Secrets.TotpDeleteKey(
|
||||
c.ctx,
|
||||
nm,
|
||||
vault.WithMountPath(c.vmnt),
|
||||
); vErr != nil {
|
||||
c.errChan <- vErr
|
||||
return
|
||||
}
|
||||
_ = resp
|
||||
}(name)
|
||||
}
|
||||
|
||||
c.wg.Wait()
|
||||
}
|
||||
|
||||
func (c *Client) Errors() (errs []error) {
|
||||
|
||||
errs = make([]error, 0)
|
||||
|
||||
if c.mErr == nil || c.mErr.IsEmpty() {
|
||||
return
|
||||
}
|
||||
|
||||
errs = c.mErr.Errors
|
||||
c.mErr = multierr.NewMultiError(nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// currently it doesn't update out-of-sync keys and only syncs from SecretService to Vault. TODO.
|
||||
func (c *Client) Sync() {
|
||||
|
||||
c.addVault()
|
||||
|
||||
c.wg.Wait()
|
||||
}
|
||||
|
||||
func (c *Client) addVault() {
|
||||
|
||||
var name string
|
||||
var wsec wrappedSsSecret
|
||||
|
||||
for name, wsec = range c.inSS {
|
||||
c.wg.Add(1)
|
||||
go func(nm string, sec wrappedSsSecret) {
|
||||
var vErr error
|
||||
var i int
|
||||
var ok bool
|
||||
var digSize int
|
||||
var numSeconds int
|
||||
// var digSize int32
|
||||
var otpUrl *otpauth.URL
|
||||
var req schema.TotpCreateKeyRequest
|
||||
var resp *vault.Response[map[string]interface{}]
|
||||
|
||||
defer c.wg.Done()
|
||||
|
||||
if _, ok = c.inVault[nm]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
if i, vErr = strconv.Atoi(sec.secret.Attrs["digits"]); vErr != nil {
|
||||
c.errChan <- vErr
|
||||
return
|
||||
}
|
||||
if i > math.MaxInt32 {
|
||||
c.errChan <- fmt.Errorf("digits too large (%d > %d)", i, math.MaxInt32)
|
||||
return
|
||||
}
|
||||
// digSize = int32(i)
|
||||
digSize = i
|
||||
|
||||
if i, vErr = strconv.Atoi(sec.secret.Attrs["period"]); vErr != nil {
|
||||
c.errChan <- vErr
|
||||
return
|
||||
}
|
||||
if i > math.MaxInt32 {
|
||||
c.errChan <- fmt.Errorf("period too large (%d > %d)", i, math.MaxInt32)
|
||||
return
|
||||
}
|
||||
numSeconds = i
|
||||
|
||||
otpUrl = &otpauth.URL{
|
||||
Type: "TOTP", // TODO: https://github.com/dkosmari/gnome-shell-extension-totp/issues/15
|
||||
Issuer: sec.secret.Attrs["issuer"],
|
||||
Account: sec.secret.Attrs["name"],
|
||||
RawSecret: string(sec.secret.Secret.Value),
|
||||
Algorithm: strings.ToUpper(strings.ReplaceAll(sec.secret.Attrs["algorithm"], "-", "")),
|
||||
Digits: digSize,
|
||||
Period: numSeconds,
|
||||
Counter: 0,
|
||||
}
|
||||
fmt.Printf("Adding '%s' to '%s'\n", nm, c.vmnt)
|
||||
// https://developer.hashicorp.com/vault/api-docs/secret/totp#create-key
|
||||
// https://pkg.go.dev/github.com/hashicorp/vault-client-go/schema#TotpCreateKeyRequest
|
||||
req = schema.TotpCreateKeyRequest{
|
||||
// AccountName: sec.secret.Attrs["name"],
|
||||
// Algorithm: strings.ToUpper(strings.ReplaceAll(sec.secret.Attrs["algorithm"], "-", "")),
|
||||
// Digits: digSize,
|
||||
// Exported: false,
|
||||
// Generate: false,
|
||||
// Issuer: sec.secret.Attrs["issuer"],
|
||||
// Key: string(sec.secret.Secret.Value),
|
||||
// // KeySize: 0,
|
||||
// Period: sec.secret.Attrs["period"],
|
||||
// // QrSize: 0,
|
||||
// // Skew: 0,
|
||||
Url: otpUrl.String(),
|
||||
}
|
||||
if resp, vErr = c.vc.Secrets.TotpCreateKey(
|
||||
c.ctx,
|
||||
nm,
|
||||
req,
|
||||
vault.WithMountPath(c.vmnt),
|
||||
); vErr != nil {
|
||||
c.errChan <- fmt.Errorf("error adding '%s': %v", nm, vErr)
|
||||
return
|
||||
}
|
||||
_ = resp
|
||||
}(name, wsec)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) getSS() {
|
||||
|
||||
var err error
|
||||
var idx int
|
||||
var nm string
|
||||
var spl []string
|
||||
var item *gosecret.Item
|
||||
var secrets []*gosecret.Item
|
||||
|
||||
defer c.wg.Done()
|
||||
|
||||
if secrets, err = c.scoll.Items(); err != nil {
|
||||
c.errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
c.inSS = make(map[string]wrappedSsSecret)
|
||||
for _, item = range secrets {
|
||||
// Unlike KVv2 names, Vault is *very* picky about key names. Boo.
|
||||
// nm = fmt.Sprintf("%s/%s", item.Attrs["issuer"], item.Attrs["name"])
|
||||
nm = fmt.Sprintf(
|
||||
"%s"+
|
||||
"."+
|
||||
"%s",
|
||||
normalizeVaultNm(item.Attrs["issuer"]),
|
||||
normalizeVaultNm(item.Attrs["name"]),
|
||||
)
|
||||
fmt.Printf("'%s' from '%s' => '%s'\n", item.Attrs["name"], item.Attrs["issuer"], nm)
|
||||
spl = strings.SplitN(item.LabelName, ":", 2)
|
||||
if idx, err = strconv.Atoi(spl[0]); err != nil {
|
||||
c.errChan <- err
|
||||
continue
|
||||
}
|
||||
c.inSS[nm] = wrappedSsSecret{
|
||||
id: idx,
|
||||
strippedNm: nm,
|
||||
nm: item.LabelName,
|
||||
secret: item,
|
||||
}
|
||||
if idx > c.lastIdx {
|
||||
c.lastIdx = idx
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) getVault() {
|
||||
|
||||
var err error
|
||||
var nm string
|
||||
var nms []string
|
||||
var vErr *vault.ResponseError
|
||||
var totpKey *vault.Response[map[string]interface{}]
|
||||
|
||||
defer c.wg.Done()
|
||||
|
||||
var resp *vault.Response[schema.StandardListResponse]
|
||||
|
||||
resp, err = c.vc.Secrets.TotpListKeys(
|
||||
c.ctx,
|
||||
vault.WithMountPath(c.vmnt),
|
||||
)
|
||||
if err != nil {
|
||||
// no TOTP yet
|
||||
if errors.As(err, &vErr) && vErr.StatusCode == http.StatusNotFound {
|
||||
c.inVault = make(map[string]map[string]interface{})
|
||||
err = nil
|
||||
return
|
||||
}
|
||||
c.errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
nms = resp.Data.Keys
|
||||
c.inVault = make(map[string]map[string]interface{})
|
||||
|
||||
for _, nm = range nms {
|
||||
if totpKey, err = c.vc.Secrets.TotpReadKey(
|
||||
c.ctx,
|
||||
nm,
|
||||
vault.WithMountPath(c.vmnt),
|
||||
); err != nil {
|
||||
c.errChan <- err
|
||||
continue
|
||||
}
|
||||
c.inVault[nm] = totpKey.Data
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) readErrs() {
|
||||
|
||||
var err error
|
||||
|
||||
for err = range c.errChan {
|
||||
if err != nil {
|
||||
c.mErr.AddError(err)
|
||||
}
|
||||
}
|
||||
|
||||
c.errsDone <- true
|
||||
}
|
||||
36
internal/types.go
Normal file
36
internal/types.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
`context`
|
||||
`sync`
|
||||
|
||||
`github.com/hashicorp/vault-client-go`
|
||||
`r00t2.io/gosecret`
|
||||
`r00t2.io/goutils/multierr`
|
||||
)
|
||||
|
||||
type (
|
||||
Client struct {
|
||||
lastIdx int
|
||||
vtok string
|
||||
vaddr string
|
||||
scollNm string
|
||||
vmnt string
|
||||
errsDone chan bool
|
||||
errChan chan error
|
||||
vc *vault.Client
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
ssvc *gosecret.Service
|
||||
scoll *gosecret.Collection
|
||||
mErr *multierr.MultiError
|
||||
inSS map[string]wrappedSsSecret
|
||||
inVault map[string]map[string]interface{}
|
||||
}
|
||||
wrappedSsSecret struct {
|
||||
id int
|
||||
strippedNm string
|
||||
nm string
|
||||
secret *gosecret.Item
|
||||
}
|
||||
)
|
||||
32
version/consts.go
Normal file
32
version/consts.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
/*
|
||||
These variables are automatically handled by the build script.
|
||||
|
||||
DO NOT MODIFY THESE VARIABLES.
|
||||
Refer to /build.sh for how these are generated at build time and populated.
|
||||
*/
|
||||
var (
|
||||
sourceControl string = "git"
|
||||
version string = "(unknown)"
|
||||
commitHash string
|
||||
commitShort string
|
||||
numCommitsAfterTag string
|
||||
isDirty string
|
||||
buildTime string
|
||||
buildUser string
|
||||
buildSudoUser string
|
||||
buildHost string
|
||||
)
|
||||
|
||||
var (
|
||||
patchRe *regexp.Regexp = regexp.MustCompile(`^(?P<patch>[0-9+])(?P<pre>-[0-9A-Za-z.-]+)?(?P<build>\+[0-9A-Za-z.-]+)?$`)
|
||||
patchReIsolated *regexp.Regexp = regexp.MustCompile(`^([0-9]+)(?:[-+](.*)?)?$`)
|
||||
)
|
||||
|
||||
// Ver is populated by main() from the build script and used in other places.
|
||||
var Ver *BuildInfo
|
||||
144
version/funcs.go
Normal file
144
version/funcs.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// Version returns the build information. See build.sh.
|
||||
func Version() (b *BuildInfo, err error) {
|
||||
|
||||
var n int
|
||||
var s string
|
||||
var sb strings.Builder
|
||||
var ok bool
|
||||
var canonical string
|
||||
var build strings.Builder
|
||||
// Why a map?
|
||||
// I forget but I had a reason for it once upon a time.
|
||||
var raw map[string]string = map[string]string{
|
||||
"sourceControl": sourceControl,
|
||||
"tag": version,
|
||||
"hash": commitHash,
|
||||
"shortHash": commitShort,
|
||||
"postTagCommits": numCommitsAfterTag,
|
||||
"dirty": isDirty,
|
||||
"time": buildTime,
|
||||
"user": buildUser,
|
||||
"sudoUser": buildSudoUser,
|
||||
"host": buildHost,
|
||||
}
|
||||
var i BuildInfo = BuildInfo{
|
||||
SourceControl: raw["sourceControl"],
|
||||
TagVersion: raw["tag"],
|
||||
// PostTagCommits: 0,
|
||||
CommitHash: raw["hash"],
|
||||
CommitId: raw["shortHash"],
|
||||
BuildUser: raw["user"],
|
||||
RealBuildUser: raw["sudoUser"],
|
||||
// BuildTime: time.Time{},
|
||||
BuildHost: raw["host"],
|
||||
Dirty: false,
|
||||
isDefined: false,
|
||||
raw: raw,
|
||||
}
|
||||
|
||||
if s, ok = raw["postTagCommits"]; ok && strings.TrimSpace(s) != "" {
|
||||
if n, err = strconv.Atoi(s); err == nil {
|
||||
i.PostTagCommits = uint(n)
|
||||
}
|
||||
}
|
||||
|
||||
if s, ok = raw["time"]; ok && strings.TrimSpace(s) != "" {
|
||||
if n, err = strconv.Atoi(s); err == nil {
|
||||
i.BuildTime = time.Unix(int64(n), 0).UTC()
|
||||
}
|
||||
}
|
||||
|
||||
switch strings.ToLower(raw["dirty"]) {
|
||||
case "1":
|
||||
i.Dirty = true
|
||||
case "0", "":
|
||||
i.Dirty = false
|
||||
}
|
||||
|
||||
// Build the short form. We use this for both BuildInfo.short and BuildInfo.verSplit.
|
||||
if i.TagVersion == "" {
|
||||
sb.WriteString(i.SourceControl)
|
||||
} else {
|
||||
sb.WriteString(i.TagVersion)
|
||||
}
|
||||
/*
|
||||
Now the mess. In order to conform to SemVer 2.0 (the spec this code targets):
|
||||
|
||||
1.) MAJOR.
|
||||
2.) MINOR.
|
||||
3.) PATCH
|
||||
4.) -PRERELEASE (OPTIONAL)
|
||||
(git commit, if building against a commit made past 1-3. Always included if untagged.)
|
||||
5.) +BUILDINFO (OPTIONAL)
|
||||
("+x[.y]", where x is # of commits past 4, or tag commit if 4 is empty. 0 is valid.
|
||||
y is optional, and is the string "dirty" if it is a "dirty" build - that is, uncommitted/unstaged changes.
|
||||
if x and y would be 0 and empty, respectively, then 5 is not included.)
|
||||
|
||||
1-3 are already written, or the source control software used if not.
|
||||
|
||||
Technically 4 and 5 are only included if 3 is present. We force patch to 0 if it's a tagged release and patch isn't present --
|
||||
so this is not relevant.
|
||||
*/
|
||||
// PRERELEASE
|
||||
if i.TagVersion == "" || i.PostTagCommits > 0 {
|
||||
// We use the full commit hash for git versions, short identifier for tagged releases.
|
||||
if i.TagVersion == "" {
|
||||
i.Pre = i.CommitHash
|
||||
} else {
|
||||
i.Pre = i.CommitId
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("-%v", i.Pre))
|
||||
}
|
||||
// BUILD
|
||||
if i.PostTagCommits > 0 || i.Dirty {
|
||||
build.WriteString(strconv.Itoa(int(i.PostTagCommits)))
|
||||
if i.Dirty {
|
||||
build.WriteString(".dirty")
|
||||
}
|
||||
i.Build = build.String()
|
||||
sb.WriteString(fmt.Sprintf("+%v", i.Build))
|
||||
}
|
||||
|
||||
i.short = sb.String()
|
||||
if semver.IsValid(i.short) {
|
||||
// DON'T DO THIS. It strips the prerelease and build info.
|
||||
// i.short = semver.Canonical(i.short)
|
||||
// Do this instead.
|
||||
canonical = semver.Canonical(i.short)
|
||||
// Numeric versions...
|
||||
if n, err = strconv.Atoi(strings.TrimPrefix(semver.Major(canonical), "v")); err != nil {
|
||||
err = nil
|
||||
} else {
|
||||
i.Major = uint(n)
|
||||
}
|
||||
if n, err = strconv.Atoi(strings.Split(semver.MajorMinor(canonical), ".")[1]); err != nil {
|
||||
err = nil
|
||||
} else {
|
||||
i.Minor = uint(n)
|
||||
}
|
||||
if n, err = strconv.Atoi(patchReIsolated.FindStringSubmatch(strings.Split(canonical, ".")[2])[1]); err != nil {
|
||||
err = nil
|
||||
} else {
|
||||
i.Patch = uint(n)
|
||||
}
|
||||
// The other tag assignments were performed above.
|
||||
}
|
||||
// The default is 0 for the numerics, so no big deal.
|
||||
|
||||
i.isDefined = true
|
||||
|
||||
b = &i
|
||||
|
||||
return
|
||||
}
|
||||
103
version/funcs_buildinfo.go
Normal file
103
version/funcs_buildinfo.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// Detail returns a multiline string containing every possible piece of information we collect.
|
||||
func (b *BuildInfo) Detail() (ver string) {
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%v\n\n", b.short))
|
||||
sb.WriteString(fmt.Sprintf("====\nSource Control: %v\n", b.SourceControl))
|
||||
if b.TagVersion != "" {
|
||||
if b.PostTagCommits > 0 {
|
||||
sb.WriteString(fmt.Sprintf("Version Base: %v\nCommit Hash: %v\n", b.TagVersion, b.CommitHash))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("Version: %v\n", b.TagVersion))
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("Version: (Unversioned)\nCommit Hash: %v\n", b.CommitHash))
|
||||
}
|
||||
|
||||
// Post-commits
|
||||
if b.TagVersion != "" {
|
||||
sb.WriteString(fmt.Sprintf("# of Commits Since %v: %v\n", b.TagVersion, b.PostTagCommits))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("# of Commits Since %v: %v\n", b.CommitId, b.PostTagCommits))
|
||||
}
|
||||
|
||||
sb.WriteString("Uncommitted/Unstaged Changes: ")
|
||||
if b.Dirty {
|
||||
sb.WriteString("yes (dirty/monkeypatched build)\n")
|
||||
} else {
|
||||
sb.WriteString("no (clean build)\n")
|
||||
}
|
||||
|
||||
if b.TagVersion != "" {
|
||||
sb.WriteString(
|
||||
fmt.Sprintf(
|
||||
"====\nMajor: %v\nMinor: %v\nPatch: %v\n",
|
||||
b.Major, b.Minor, b.Patch,
|
||||
),
|
||||
)
|
||||
}
|
||||
sb.WriteString("====\n")
|
||||
sb.WriteString(b.Meta())
|
||||
|
||||
ver = sb.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Short returns a uniquely identifiable version string.
|
||||
func (b *BuildInfo) Short() (ver string) {
|
||||
|
||||
ver = b.short
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Meta returns the build/compile-time info.
|
||||
func (b *BuildInfo) Meta() (meta string) {
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
if b.RealBuildUser != b.BuildUser && b.RealBuildUser != "" {
|
||||
sb.WriteString(fmt.Sprintf("Real Build User: %v\n", b.RealBuildUser))
|
||||
sb.WriteString(fmt.Sprintf("Sudo Build User: %v\n", b.BuildUser))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("Build User: %v\n", b.BuildUser))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Build Time: %v\nBuild Host: %v\n", b.BuildTime, b.BuildHost))
|
||||
|
||||
meta = sb.String()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getReMap gets a regex map of map[pattern]match.
|
||||
func (b *BuildInfo) getReMap() (matches map[string]string) {
|
||||
|
||||
var s string = b.Short()
|
||||
var sections []string
|
||||
|
||||
if !semver.IsValid(s) {
|
||||
return
|
||||
}
|
||||
|
||||
sections = strings.Split(s, ".")
|
||||
|
||||
// The split should contain everything in the third element.
|
||||
// Or, if using a "simplified" semver, the last element.
|
||||
matches = make(map[string]string)
|
||||
for idx, str := range patchRe.FindStringSubmatch(sections[len(sections)-1]) {
|
||||
matches[patchRe.SubexpNames()[idx]] = str
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
51
version/types.go
Normal file
51
version/types.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// BuildInfo contains nativized version information.
|
||||
type BuildInfo struct {
|
||||
// TagVersion is the most recent tag name on the current branch.
|
||||
TagVersion string
|
||||
// PostTagCommits is the number of commits after BuildInfo.TagVersion's commit on the current branch.
|
||||
PostTagCommits uint
|
||||
// CommitHash is the full commit hash.
|
||||
CommitHash string
|
||||
// CommitId is the "short" version of BuildInfo.CommitHash.
|
||||
CommitId string
|
||||
// BuildUser is the user the program was compiled under.
|
||||
BuildUser string
|
||||
// If compiled under sudo, BuildInfo.RealBuildUser is the user that called sudo.
|
||||
RealBuildUser string
|
||||
// BuildTime is the time and date of the program's build time.
|
||||
BuildTime time.Time
|
||||
// BuildHost is the host the binary was compiled on.
|
||||
BuildHost string
|
||||
// Dirty specifies if the source was "dirty" (uncommitted/unstaged etc. files) at the time of compilation.
|
||||
Dirty bool
|
||||
// SourceControl is the source control version used. Only relevant if not a "clean" build or untagged.
|
||||
SourceControl string
|
||||
// Major is the major version, expressed as an uint per spec.
|
||||
Major uint
|
||||
// Minor is the minor version, expressed as an uint per spec.
|
||||
Minor uint
|
||||
// Patch is the patch version, expressed as an uint per spec.
|
||||
Patch uint
|
||||
// Pre
|
||||
Pre string
|
||||
// Build
|
||||
Build string
|
||||
// isDefined specifies if this version was retrieved from the built-in values.
|
||||
isDefined bool
|
||||
// raw is the raw variable values.
|
||||
raw map[string]string
|
||||
/*
|
||||
verSplit is a slice of []string{Major, Minor, Patch, PreRelease, Build}
|
||||
|
||||
If using an actual point release, PreRelease and Build are probably blank.
|
||||
*/
|
||||
verSplit [5]string
|
||||
// short is the condensed version of verSplit.
|
||||
short string
|
||||
}
|
||||
Reference in New Issue
Block a user