mirror of
https://github.com/arcanericky/totp.git
synced 2026-05-17 07:55:45 +03:00
Pull dev into main (#24)
This commit is contained in:
8
.codecov.yml
Normal file
8
.codecov.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 80%
|
||||
patch:
|
||||
default:
|
||||
target: 80%
|
||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
24
.github/workflows/builder.yml
vendored
24
.github/workflows/builder.yml
vendored
@@ -2,31 +2,41 @@ on: [push, pull_request]
|
||||
name: Build
|
||||
jobs:
|
||||
test:
|
||||
name: Build and Test
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.16.x]
|
||||
go-version: [1.18.x]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Code format
|
||||
run: diff -u <(echo -n) <(gofmt -d -s .)
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
|
||||
- name: Unit tests
|
||||
run: go test -race -coverprofile=coverage.out ./...
|
||||
|
||||
- name: Function coverage
|
||||
run: go tool cover "-func=coverage.out"
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.out
|
||||
- name: Build and Execute
|
||||
|
||||
- name: Build and Test
|
||||
run: |
|
||||
go build -o totp-test -ldflags "-X main.version=$(./scripts/get-version.sh)" ./totp/...
|
||||
./totp-test --help
|
||||
./scripts/totp-test.sh
|
||||
|
||||
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@@ -7,21 +7,22 @@ on:
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
name: GoReleaser
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18.x
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
args: release --rm-dist
|
||||
env:
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
.vscode/
|
||||
bin/
|
||||
dist/
|
||||
cmd/testcollection.json
|
||||
coverage.*
|
||||
settings.json
|
||||
dist/
|
||||
|
||||
testcollection.json
|
||||
totp
|
||||
|
||||
7
.golangci.yml
Normal file
7
.golangci.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
linters:
|
||||
enable:
|
||||
- gosec
|
||||
linters-settings:
|
||||
gosec:
|
||||
excludes:
|
||||
- G404
|
||||
@@ -4,10 +4,10 @@ before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
builds:
|
||||
- main: ./totp
|
||||
- main: ./cmd
|
||||
binary: totp
|
||||
ldflags:
|
||||
- -s -w -X github.com/arcanericky/totp/cmd.versionText={{.Version}}
|
||||
- -s -w -X github.com/arcanericky/totp/commands.versionText={{.Version}}
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Ricky Pike
|
||||
Copyright (c) 2022 Ricky Pike
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
16
Makefile
16
Makefile
@@ -1,7 +1,7 @@
|
||||
VERSION=1.0.0-incubation
|
||||
VERSION_INJECT=github.com/arcanericky/totp/cmd.versionText
|
||||
SRCS=*.go totp/*.go cmd/*.go
|
||||
|
||||
SRCS=*.go cmd/*.go commands/*.go
|
||||
MAIN=./cmd/...
|
||||
EXECUTABLE=bin/totp
|
||||
|
||||
LINUX=$(EXECUTABLE)-linux
|
||||
@@ -31,23 +31,23 @@ linux-arm: $(LINUX_ARM32)
|
||||
linux-arm64: $(LINUX_ARM64)
|
||||
|
||||
test:
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic . ./cmd
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic . ./commands
|
||||
go tool cover -html=coverage.txt -o coverage.html
|
||||
|
||||
$(WINDOWS_AMD64): $(SRCS)
|
||||
GOOS=windows GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" github.com/arcanericky/totp/totp
|
||||
GOOS=windows GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" $(MAIN)
|
||||
|
||||
$(LINUX_AMD64): $(SRCS)
|
||||
GOOS=linux GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" github.com/arcanericky/totp/totp
|
||||
GOOS=linux GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" $(MAIN)
|
||||
|
||||
$(DARWIN_AMD64): $(SRCS)
|
||||
GOOS=darwin GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" github.com/arcanericky/totp/totp
|
||||
GOOS=darwin GOARCH=amd64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" $(MAIN)
|
||||
|
||||
$(LINUX_ARM32): $(SRCS)
|
||||
GOOS=linux GOARCH=arm go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" github.com/arcanericky/totp/totp
|
||||
GOOS=linux GOARCH=arm go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" $(MAIN)
|
||||
|
||||
$(LINUX_ARM64): $(SRCS)
|
||||
GOOS=linux GOARCH=arm64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" github.com/arcanericky/totp/totp
|
||||
GOOS=linux GOARCH=arm64 go build -o $@ -ldflags "-X $(VERSION_INJECT)=$(shell sh scripts/get-version.sh)" $(MAIN)
|
||||
|
||||
clean:
|
||||
rm -rf bin
|
||||
|
||||
42
README.md
42
README.md
@@ -1,6 +1,6 @@
|
||||
# TOTP
|
||||
|
||||
A time-based one-time password (TOTP) code generator written in Go. Basically a command-line interface that's [Google Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US) or [Authy](https://authy.com/) for your Windows, macOS, or Linux machine.
|
||||
A time-based one-time password (TOTP) code generator written in Go. A command-line interface that's like [Google Authenticator](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_US) or [Authy](https://authy.com/) for your Windows, macOS, or Linux machine.
|
||||
|
||||
[](https://github.com/arcanericky/totp/actions/workflows/builder.yml)
|
||||
[](https://codecov.io/gh/arcanericky/totp)
|
||||
@@ -13,7 +13,7 @@ It generates TOTP codes used for two-factor authentication at sites such as Goog
|
||||
**Warning**
|
||||
Every copy of your two-factor credentials increases your risk profile. Using this utility is no exception. This utility will store your TOTP secrets unencrypted on your filesystem. The only protection offered is to store these secrets in a file readable by only your user and protected by the operating system only.
|
||||
|
||||
## How to Use
|
||||
## Quick Start
|
||||
|
||||
**Add TOTP secrets** to the TOTP configuration file with the `config add` option, specifying the name and secret value. Note the secret names are **case sensitive**.
|
||||
|
||||
@@ -21,7 +21,7 @@ Every copy of your two-factor credentials increases your risk profile. Using thi
|
||||
totp config add mysecretname seed
|
||||
```
|
||||
|
||||
**Generate TOTP codes** using the `totp` command to specify the secret name. Note that because `totp` reserves the use of the words `config` and `version`, don't use them to name a secret.
|
||||
**Generate TOTP codes** using the `totp` command to specify the secret name. Note that because `totp` reserves the use of the words `config` and `version` for commands, don't use them to name a secret. If you've generated and installed `totp` completions for for your shell, pressing tab on a partially completed secret name will trigger autocomplete.
|
||||
|
||||
```sh
|
||||
totp mysecretname
|
||||
@@ -33,6 +33,8 @@ totp mysecretname
|
||||
totp config list
|
||||
```
|
||||
|
||||
Aliases are `ls` and `l`.
|
||||
|
||||
**Update secret entries** using the `config update` command. Note that `config update` and `config add` are actually the same command and can be used interchangeably.
|
||||
|
||||
```sh
|
||||
@@ -45,12 +47,16 @@ totp config update mysecretname newseed
|
||||
totp config rename mysecretname mynewname
|
||||
```
|
||||
|
||||
Aliases are `ren` and `mv`.
|
||||
|
||||
**Delete secret entries** with the `config delete` command
|
||||
|
||||
```sh
|
||||
totp config delete mynewname
|
||||
```
|
||||
|
||||
Aliases are `remove`, `erase`, `rm`, and `del`.
|
||||
|
||||
**Remove all the secrets** and start over using the `config reset` command
|
||||
|
||||
```sh
|
||||
@@ -76,17 +82,29 @@ totp --help
|
||||
totp config --help
|
||||
```
|
||||
|
||||
**Bash completion** can be enabled by using `config completion`.
|
||||
**Shell completion** can be enabled by using the `completion` command.
|
||||
|
||||
Bash
|
||||
|
||||
```sh
|
||||
. <(totp config completion)
|
||||
. <(totp completion bash)
|
||||
```
|
||||
|
||||
Powershell
|
||||
|
||||
```powershell
|
||||
. totp completion powershell | Out-String | Invoke-Expression
|
||||
```
|
||||
|
||||
## TOTP Data Location
|
||||
|
||||
The location for saved data is extracted from the `LOCALAPPDATA` environment variable in Windows and the `HOME` environment for Linux/MacOS and in the file `totp-config.json`. This can be customized using the `--file` option or by setting the `TOTP_CONFIG` environment variable.
|
||||
|
||||
## Using the Time Machine
|
||||
|
||||
`totp` has the `--time`, `--forward`, and `--backward` options that are used to manipulate the time for which the TOTP code is generated. This is useful if `totp` is being used on a machine with the incorrect time.
|
||||
`totp` implements the `--time`, `--forward`, and `--backward` options to manipulate the time for which the TOTP code is generated. This is useful if `totp` is being used on a machine with the incorrect time.
|
||||
|
||||
The `--time` option takes an [RFC3339 formatted time string](https://tools.ietf.org/html/rfc3339) as its argument and uses it to generate the TOTP code. Note that the `--forward` and `--backward` options will modify this option value.
|
||||
The `--time` option takes an [RFC3339 formatted time string](https://tools.ietf.org/html/rfc3339) as its argument and uses it to generate the TOTP code. Note that the `--forward` and `--backward` options will internally modify this option value.
|
||||
|
||||
Examples with `--time`:
|
||||
|
||||
@@ -114,6 +132,8 @@ The `--follow` option is also compatible with the time machine.
|
||||
|
||||
```sh
|
||||
totp --time 2001-10-31T20:00:00-05:00 --follow --secret seed
|
||||
877737
|
||||
208737
|
||||
```
|
||||
|
||||
## Using the Stdio Option
|
||||
@@ -123,7 +143,7 @@ If storing secrets in the clear isn't ideal for you, `totp` supports streaming t
|
||||
The `totp <secret name>` and `totp config list` commands support loading the collection via standard input. The
|
||||
`totp config update`, `totp config delete`, and `totp config rename` commands support loading via standard input and sending the modified collection to standard output. Experiment with the `--stdio` option to observe how this works.
|
||||
|
||||
**Learning with Cleartext Data**
|
||||
### Learning with Plaintext Data
|
||||
|
||||
Note the `--file` option can achieve the same results as this example. This is meant to teach how stdio works with `totp`.
|
||||
|
||||
@@ -145,7 +165,7 @@ Generate a TOTP code
|
||||
totp secretname --stdio < totp.json
|
||||
```
|
||||
|
||||
**Encrypting Shared Secret Collection**
|
||||
### Encrypting Shared Secret Collection
|
||||
|
||||
Using what was learned above, a contrived example for encrypting data with [GnuPG](https://gnupg.org/) follows.
|
||||
|
||||
@@ -186,7 +206,7 @@ gpg --quiet --batch --passphrase mypassphrase --decrypt totp-collection.gpg | to
|
||||
|
||||
## Building
|
||||
|
||||
`totp` is mostly developed using Go 1.12.x on Debian based systems. Only `go` is required but to use the automated actions the `Makefile` provides, `make` must be installed.
|
||||
`totp` is mostly developed using Go 1.18.x on Debian based systems. Only `go` is required but to use the automated actions the `Makefile` provides, `make` must be installed.
|
||||
|
||||
To build everything:
|
||||
|
||||
@@ -228,4 +248,4 @@ My [ga-cmd project](https://github.com/arcanericky/ga-cmd) is more popular than
|
||||
|
||||
## Credits
|
||||
|
||||
This utility uses the [otp package by pquerna](https://github.com/pquerna/otp). Without this library, I probably wouldn't have bothered creating this.
|
||||
This utility uses the [otp package by pquerna](https://github.com/pquerna/otp). Without this library, I probably wouldn't have bothered creating this front-end.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Configure totp",
|
||||
Long: `Configure totp`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(configCmd)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// completionCmd represents the completion command
|
||||
var completionCmd = &cobra.Command{
|
||||
Use: "completion",
|
||||
Short: "Generates bash completion scripts",
|
||||
Long: `To load completion run
|
||||
|
||||
. <(totp config completion)
|
||||
|
||||
To configure your bash shell to load completions for each session add to your bashrc
|
||||
|
||||
# ~/.bashrc or ~/.profile
|
||||
. <(totp config completion)
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
rootCmd.GenBashCompletion(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(completionCmd)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigCompletion(t *testing.T) {
|
||||
completionCmd.Run(nil, []string{})
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var configDeleteCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Aliases: []string{"remove", "erase", "rm", "del"},
|
||||
Short: "Delete a secret",
|
||||
Long: `Delete a secret`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
fmt.Fprintln(os.Stderr, "Must provide a secret name to delete.")
|
||||
return
|
||||
}
|
||||
|
||||
deleteSecret(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
func deleteSecret(name string) {
|
||||
s, err := collectionFile.loader()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error loading settings", err)
|
||||
} else {
|
||||
_, err := s.DeleteSecret(name)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error deleting secret", err)
|
||||
} else {
|
||||
s.Save()
|
||||
printResultf("Deleted secret %s\n", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(configDeleteCmd)
|
||||
configDeleteCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/arcanericky/totp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var configListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List secrets",
|
||||
Long: `List secrets`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
listSecrets(cmd)
|
||||
},
|
||||
}
|
||||
|
||||
func titleLine(len int) string {
|
||||
var builder strings.Builder
|
||||
builder.Grow(len)
|
||||
|
||||
for i := 0; i < len; i++ {
|
||||
builder.WriteString("-")
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func listSecretNames(secrets []totp.Secret) {
|
||||
for _, s := range secrets {
|
||||
fmt.Println(s.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func listAllInfo(secrets []totp.Secret) {
|
||||
nameTitle := "Name"
|
||||
secretTitle := "Secret"
|
||||
addedDateTitle := "Date Added"
|
||||
modifiedDateTitle := "Date Modified"
|
||||
|
||||
maxNameLen := len(nameTitle)
|
||||
maxSecretLen := len(secretTitle)
|
||||
for _, s := range secrets {
|
||||
nameLen := len(s.Name)
|
||||
if nameLen > maxNameLen {
|
||||
maxNameLen = nameLen
|
||||
}
|
||||
|
||||
secretLen := len(s.Value)
|
||||
if secretLen > maxSecretLen {
|
||||
maxSecretLen = secretLen
|
||||
}
|
||||
}
|
||||
|
||||
timeFormat := "Jan _2 2006 15:04:05"
|
||||
timeFormatLen := len(timeFormat)
|
||||
timeFormatLine := titleLine(len(timeFormat))
|
||||
fmt.Printf("%-*s %-*s %-*s %-*s\n",
|
||||
maxNameLen, nameTitle,
|
||||
maxSecretLen, secretTitle,
|
||||
timeFormatLen, addedDateTitle,
|
||||
timeFormatLen, modifiedDateTitle)
|
||||
fmt.Printf("%s %s %s %s\n", titleLine(maxNameLen), titleLine(maxSecretLen), timeFormatLine, timeFormatLine)
|
||||
for _, s := range secrets {
|
||||
fmt.Printf("%-*s %-*s %s %s\n", maxNameLen, s.Name, maxSecretLen, s.Value, s.DateAdded.Format(timeFormat), s.DateModified.Format(timeFormat))
|
||||
}
|
||||
}
|
||||
|
||||
func listSecrets(cmd *cobra.Command) {
|
||||
c, err := collectionFile.loader()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error loading collection", err)
|
||||
} else {
|
||||
names, err := cmd.Flags().GetBool("names")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error getting names option", err)
|
||||
return
|
||||
}
|
||||
|
||||
secrets := c.GetSecrets()
|
||||
|
||||
if names == true {
|
||||
listSecretNames(secrets)
|
||||
} else {
|
||||
listAllInfo(secrets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(configListCmd)
|
||||
configListCmd.Flags().BoolP("names", "n", false, "list only secret names")
|
||||
configListCmd.Flags().BoolP(optionStdio, "", false, "load data from stdin")
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var configRenameCmd = &cobra.Command{
|
||||
Use: "rename",
|
||||
Aliases: []string{"ren", "mv"},
|
||||
Short: "Rename a secret",
|
||||
Long: `Rename a secret`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "Must provide source and target.")
|
||||
return
|
||||
}
|
||||
|
||||
renameSecret(args[0], args[1])
|
||||
},
|
||||
}
|
||||
|
||||
func renameSecret(source, target string) {
|
||||
if isReservedCommand(target) {
|
||||
fmt.Fprintln(os.Stderr, "The name \""+target+"\" is reserved for the "+target+" command")
|
||||
return
|
||||
}
|
||||
|
||||
s, _ := collectionFile.loader()
|
||||
_, err := s.RenameSecret(source, target)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error renaming secret:", err)
|
||||
} else {
|
||||
s.Save()
|
||||
printResultf("Renamed secret %s to %s\n", source, target)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(configRenameCmd)
|
||||
configRenameCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
|
||||
configRenameCmd.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [old secret name] [new secret name]", 1))
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var configResetCmd = &cobra.Command{
|
||||
Use: "reset",
|
||||
Short: "Reset the TOTP colllection",
|
||||
Long: "Reset the TOTP colllection",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
configReset()
|
||||
},
|
||||
}
|
||||
|
||||
func configReset() {
|
||||
os.Remove(collectionFile.filename)
|
||||
fmt.Println("Collection file removed")
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(configResetCmd)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigReset(t *testing.T) {
|
||||
collectionFile.filename = "testcollection.json"
|
||||
|
||||
createTestData(t)
|
||||
|
||||
configResetCmd.Run(nil, []string{})
|
||||
|
||||
_, err := os.Stat(collectionFile.filename)
|
||||
if !os.IsNotExist(err) {
|
||||
t.Error("Failed to remove the collection file")
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var configUpdateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Aliases: []string{"add"},
|
||||
Short: "Add or update a secret",
|
||||
Long: `Add or update a secret`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "Must provide name and secret")
|
||||
return
|
||||
}
|
||||
|
||||
updateSecret(args[0], args[1])
|
||||
},
|
||||
}
|
||||
|
||||
func updateSecret(name, value string) {
|
||||
if isReservedCommand(name) {
|
||||
fmt.Fprintln(os.Stderr, "The name \""+name+"\" is reserved for the "+name+" command")
|
||||
return
|
||||
}
|
||||
|
||||
s, _ := collectionFile.loader()
|
||||
secret, err := s.UpdateSecret(name, value)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error updating secret:", err)
|
||||
} else {
|
||||
s.Save()
|
||||
|
||||
action := "Updated"
|
||||
if secret.DateAdded == secret.DateModified {
|
||||
action = "Added"
|
||||
}
|
||||
|
||||
printResultf("%s secret %s\n", action, name)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(configUpdateCmd)
|
||||
configUpdateCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
|
||||
configUpdateCmd.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [secret name] [secret value]", 1))
|
||||
}
|
||||
285
cmd/root.go
285
cmd/root.go
@@ -1,285 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
api "github.com/arcanericky/totp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
optionBackward = "backward"
|
||||
optionFile = "file"
|
||||
optionFollow = "follow"
|
||||
optionForward = "forward"
|
||||
optionSecret = "secret"
|
||||
optionStdio = "stdio"
|
||||
optionTime = "time"
|
||||
)
|
||||
|
||||
type generateCodesAPI func(time.Duration, time.Duration, time.Duration, func(time.Duration), string, string)
|
||||
|
||||
var generateCodesService generateCodesAPI
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "totp",
|
||||
Short: "TOTP Generator",
|
||||
Long: `TOTP Generator`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
if cmd.Flags().Changed(optionFile) {
|
||||
cfgFile, err := cmd.Flags().GetString(optionFile)
|
||||
if err != nil {
|
||||
fmt.Println("Error processing collection file option", err)
|
||||
return
|
||||
}
|
||||
|
||||
collectionFile.filename = cfgFile
|
||||
}
|
||||
|
||||
if cmd.Flags().Lookup(optionStdio) != nil {
|
||||
useStdio, err := cmd.Flags().GetBool(optionStdio)
|
||||
if err != nil {
|
||||
fmt.Println("Error processing stdio option", err)
|
||||
return
|
||||
}
|
||||
|
||||
if useStdio == true {
|
||||
collectionFile.loader = loadCollectionFromStdin
|
||||
collectionFile.useStdio = true
|
||||
}
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Process the secret option
|
||||
secret, err := cmd.Flags().GetString(optionSecret)
|
||||
if err != nil {
|
||||
fmt.Println("Error getting secret", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Process the backward option
|
||||
backward, err := cmd.Flags().GetDuration(optionBackward)
|
||||
if err != nil {
|
||||
fmt.Println("Error processing backward option", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Process the forward option
|
||||
forward, err := cmd.Flags().GetDuration(optionForward)
|
||||
if err != nil {
|
||||
fmt.Println("Error processing forward option", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Process the time option
|
||||
timeString, err := cmd.Flags().GetString(optionTime)
|
||||
if err != nil {
|
||||
fmt.Println("Error processing time option", err)
|
||||
return
|
||||
}
|
||||
|
||||
follow, err := cmd.Flags().GetBool(optionFollow)
|
||||
if err != nil {
|
||||
fmt.Println("Error processing follow option", err)
|
||||
return
|
||||
}
|
||||
|
||||
var codeTime time.Time
|
||||
|
||||
// Override if time was given
|
||||
if len(timeString) > 0 {
|
||||
codeTime, err = time.Parse(time.RFC3339, timeString)
|
||||
if err != nil {
|
||||
fmt.Println("Error parsing the time option", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
codeTime = time.Now()
|
||||
// codeOffset is 0
|
||||
}
|
||||
|
||||
secretLen := len(secret)
|
||||
argsLen := len(args)
|
||||
|
||||
// No secret, no secret name
|
||||
if secretLen == 0 && argsLen == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Secret name or secret is required.\n\n")
|
||||
cmd.Help()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Secret given but additional arguments were also given
|
||||
if secretLen > 0 && argsLen > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Secret was given so additional arguments are not needed.\n\n")
|
||||
cmd.Help()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// No secret given and too many args
|
||||
if secretLen == 0 && argsLen > 1 {
|
||||
fmt.Fprintf(os.Stderr, "Too many arguments. Only one secret name is required.\n\n")
|
||||
cmd.Help()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Load the secret name
|
||||
secretName := ""
|
||||
if argsLen == 1 {
|
||||
secretName = args[0]
|
||||
}
|
||||
|
||||
// If here then a stored shared secret is wanted
|
||||
generateCode(secretName, secret, codeTime.Add(forward-backward))
|
||||
|
||||
if follow {
|
||||
generateCodesService(codeTime.Sub(time.Now())-backward+forward, 0, 30*time.Second, time.Sleep, secretName, secret)
|
||||
}
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) != 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return getSecretNamesForCompletion(toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
}
|
||||
|
||||
func getSecretNamesForCompletion(toComplete string) []string {
|
||||
var secretNames []string
|
||||
var err error
|
||||
var c *api.Collection
|
||||
|
||||
c, err = collectionFile.loader()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error loading collection", err)
|
||||
} else {
|
||||
secrets := c.GetSecrets()
|
||||
for _, s := range secrets {
|
||||
if strings.HasPrefix(s.Name, toComplete) {
|
||||
secretNames = append(secretNames, s.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return secretNames
|
||||
}
|
||||
|
||||
func generateCode(name string, secret string, t time.Time) error {
|
||||
var code string
|
||||
var err error
|
||||
var c *api.Collection
|
||||
|
||||
if len(secret) != 0 {
|
||||
code, err = totp.GenerateCode(secret, t)
|
||||
} else {
|
||||
c, err = collectionFile.loader()
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error loading collection", err)
|
||||
} else {
|
||||
code, err = c.GenerateCodeWithTime(name, t)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error generating code", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
fmt.Println(code)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func durationToNextInterval(now time.Time) time.Duration {
|
||||
var sleepSeconds int
|
||||
|
||||
s := now.Second()
|
||||
switch {
|
||||
case s == 0, s < 30:
|
||||
sleepSeconds = 30 - s
|
||||
case s >= 30:
|
||||
sleepSeconds = 60 - s
|
||||
}
|
||||
|
||||
return time.Duration(sleepSeconds)*time.Second -
|
||||
time.Duration(now.Nanosecond())*time.Nanosecond
|
||||
|
||||
}
|
||||
|
||||
func callOnInterval(runtime time.Duration, interval time.Duration, exec func() bool) {
|
||||
stopper := make(chan bool)
|
||||
|
||||
if runtime > 0 {
|
||||
go func() {
|
||||
time.Sleep(runtime)
|
||||
stopper <- true
|
||||
}()
|
||||
}
|
||||
|
||||
if exec != nil && exec() {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopper:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if exec != nil && exec() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateCodes(timeOffset time.Duration, durationToRun time.Duration, intervalTime time.Duration, sleep func(time.Duration), secretName, secret string) {
|
||||
sleep(durationToNextInterval(time.Now().Add(timeOffset)) + 10*time.Millisecond)
|
||||
|
||||
callOnInterval(durationToRun, intervalTime,
|
||||
func() bool {
|
||||
generateCode(secretName, secret, time.Now().Add(timeOffset))
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() int {
|
||||
retVal := 0
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
retVal = 1
|
||||
}
|
||||
|
||||
return retVal
|
||||
}
|
||||
|
||||
func init() {
|
||||
var duration time.Duration
|
||||
|
||||
generateCodesService = generateCodes
|
||||
|
||||
rootCmd.PersistentFlags().StringP(optionFile, "f", "", "secret collection file")
|
||||
|
||||
rootCmd.Flags().StringP(optionSecret, "s", "", "TOTP secret value")
|
||||
rootCmd.Flags().BoolP(optionStdio, "", false, "load with stdin")
|
||||
rootCmd.Flags().StringP(optionTime, "", "", "RFC3339 time for TOTP (2019-06-23T20:00:00-05:00)")
|
||||
rootCmd.Flags().DurationP(optionBackward, "", duration, "move time backward (ex. \"30s\")")
|
||||
rootCmd.Flags().DurationP(optionForward, "", duration, "move time forward (ex. \"1m\")")
|
||||
rootCmd.Flags().BoolP(optionFollow, "", false, "continuous output")
|
||||
|
||||
rootCmd.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [secret name]", 1))
|
||||
}
|
||||
209
cmd/root_test.go
209
cmd/root_test.go
@@ -1,209 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type flagValue struct{}
|
||||
|
||||
func (f flagValue) Set(s string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f flagValue) Type() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f flagValue) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestRoot(t *testing.T) {
|
||||
collectionFile.filename = "testcollection.json"
|
||||
|
||||
secretList := createTestData(t)
|
||||
|
||||
// No parameters
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
|
||||
// Valid entry and secret
|
||||
rootCmd.Run(rootCmd, []string{secretList[0].name})
|
||||
|
||||
// Non-existing entry
|
||||
rootCmd.Run(rootCmd, []string{"invalidsecret"})
|
||||
|
||||
// Test follow condition
|
||||
savedGenerateCodesService := generateCodesService
|
||||
generateCodesService = func(time.Duration, time.Duration, time.Duration, func(time.Duration), string, string) {}
|
||||
rootCmd.Flags().Lookup(optionFollow).Value.Set("true")
|
||||
rootCmd.Run(rootCmd, []string{"name0"})
|
||||
generateCodesService = savedGenerateCodesService
|
||||
rootCmd.Flags().Lookup(optionFollow).Value.Set("false")
|
||||
|
||||
// Completion
|
||||
rootCmd.ValidArgsFunction(rootCmd, []string{}, "na")
|
||||
|
||||
// Completion with args
|
||||
rootCmd.ValidArgsFunction(rootCmd, []string{"secret"}, "na")
|
||||
|
||||
// No collections file
|
||||
os.Remove(collectionFile.filename)
|
||||
rootCmd.Run(rootCmd, []string{secretList[0].name})
|
||||
|
||||
// Completion without collections
|
||||
rootCmd.ValidArgsFunction(rootCmd, []string{}, "na")
|
||||
|
||||
// Excessive args
|
||||
rootCmd.Run(rootCmd, []string{"secretname", "extraarg"})
|
||||
|
||||
// Provide secret option
|
||||
rootCmd.Flags().Set(optionSecret, "seed")
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
|
||||
// Provide invalid secret option
|
||||
rootCmd.Flags().Set(optionSecret, "seed1")
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
|
||||
// File option
|
||||
rootCmd.Flags().Set(optionFile, collectionFile.filename)
|
||||
rootCmd.Flags().Lookup(optionFile).Changed = true
|
||||
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
|
||||
|
||||
// Stdio option
|
||||
rootCmd.Flags().Set(optionStdio, "true")
|
||||
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
|
||||
collectionFile.loader = loadCollectionFromDefaultFile
|
||||
collectionFile.useStdio = false
|
||||
rootCmd.Flags().Set(optionStdio, "")
|
||||
|
||||
// Time option
|
||||
rootCmd.Flags().Set(optionTime, "2019-06-01T20:00:00-05:00")
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
|
||||
// Give secret and secret name
|
||||
rootCmd.Flags().Set(optionSecret, "seed")
|
||||
rootCmd.Run(rootCmd, []string{"secretname"})
|
||||
rootCmd.Flags().Set(optionSecret, "")
|
||||
|
||||
// Invalid time option
|
||||
rootCmd.Flags().Set(optionTime, "invalidtime")
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
rootCmd.Flags().Set(optionTime, "")
|
||||
|
||||
var f *pflag.Flag
|
||||
var savedFlagValue pflag.Value
|
||||
|
||||
// optionFile error
|
||||
f = rootCmd.Flags().Lookup(optionFile)
|
||||
savedFlagValue = f.Value
|
||||
f.Value = new(flagValue)
|
||||
f.Changed = true
|
||||
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
|
||||
f.Value = savedFlagValue
|
||||
|
||||
// optionStdio error
|
||||
f = rootCmd.Flags().Lookup(optionStdio)
|
||||
savedFlagValue = f.Value
|
||||
f.Value = new(flagValue)
|
||||
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
|
||||
f.Value = savedFlagValue
|
||||
|
||||
// optionSecret error
|
||||
f = rootCmd.Flags().Lookup(optionSecret)
|
||||
savedFlagValue = f.Value
|
||||
f.Value = new(flagValue)
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
f.Value = savedFlagValue
|
||||
|
||||
// optionBackward error
|
||||
f = rootCmd.Flags().Lookup(optionBackward)
|
||||
savedFlagValue = f.Value
|
||||
f.Value = new(flagValue)
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
f.Value = savedFlagValue
|
||||
|
||||
// optionForward error
|
||||
f = rootCmd.Flags().Lookup(optionForward)
|
||||
savedFlagValue = f.Value
|
||||
f.Value = new(flagValue)
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
f.Value = savedFlagValue
|
||||
|
||||
// optionTime error
|
||||
f = rootCmd.Flags().Lookup(optionTime)
|
||||
savedFlagValue = f.Value
|
||||
f.Value = new(flagValue)
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
f.Value = savedFlagValue
|
||||
|
||||
// optionFollow error
|
||||
f = rootCmd.Flags().Lookup(optionFollow)
|
||||
savedFlagValue = f.Value
|
||||
f.Value = new(flagValue)
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
f.Value = savedFlagValue
|
||||
|
||||
Execute()
|
||||
savedArgs := os.Args
|
||||
os.Args = []string{"totp", "--invalidoption"}
|
||||
Execute()
|
||||
os.Args = savedArgs
|
||||
}
|
||||
|
||||
func TestExecOnInterval(t *testing.T) {
|
||||
execCount := 0
|
||||
preAndExecNormal := func() bool { return false }
|
||||
preAndExecEarlyExit := func() bool { execCount++; return execCount == 2 }
|
||||
|
||||
// Normal execution
|
||||
execCount = 0
|
||||
callOnInterval(2*time.Millisecond, 1*time.Millisecond, preAndExecNormal)
|
||||
|
||||
// Exit at preExec
|
||||
execCount = 1
|
||||
callOnInterval(2*time.Millisecond, 1*time.Millisecond, preAndExecNormal)
|
||||
|
||||
// Exit at top exec
|
||||
execCount = 1
|
||||
callOnInterval(2*time.Millisecond, 1*time.Millisecond, preAndExecEarlyExit)
|
||||
|
||||
// Exit at loop exec
|
||||
execCount = 0
|
||||
callOnInterval(2*time.Millisecond, 1*time.Millisecond, preAndExecEarlyExit)
|
||||
|
||||
// No callbacks
|
||||
callOnInterval(2*time.Millisecond, 1*time.Millisecond, nil)
|
||||
}
|
||||
|
||||
func TestDurationToNextInterval(t *testing.T) {
|
||||
now, _ := time.Parse(time.RFC3339, "2019-06-23T20:00:01-05:00")
|
||||
expectedResult := time.Duration(29 * time.Second)
|
||||
actualResult := durationToNextInterval(now)
|
||||
if expectedResult != actualResult {
|
||||
t.Errorf("durationToNextInterval(%s) expected %s but returned %s", now, expectedResult, actualResult)
|
||||
}
|
||||
|
||||
now, _ = time.Parse(time.RFC3339, "2019-06-23T20:00:31-05:00")
|
||||
expectedResult = time.Duration(29 * time.Second)
|
||||
actualResult = durationToNextInterval(now)
|
||||
if expectedResult != actualResult {
|
||||
t.Errorf("durationToNextInterval(%s) expected %s but returned %s", now, expectedResult, actualResult)
|
||||
}
|
||||
|
||||
now, _ = time.Parse(time.RFC3339, "2019-06-23T20:00:31.001-05:00")
|
||||
expectedResult = time.Duration(28*time.Second + 999*time.Millisecond)
|
||||
actualResult = durationToNextInterval(now)
|
||||
if expectedResult != actualResult {
|
||||
t.Errorf("durationToNextInterval(%s) expected %s but returned %s", now, expectedResult, actualResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateCodes(t *testing.T) {
|
||||
var d time.Duration
|
||||
generateCodes(d, 2*time.Millisecond, 1*time.Millisecond,
|
||||
func(d time.Duration) {}, "", "seed")
|
||||
}
|
||||
11
cmd/totp.go
Normal file
11
cmd/totp.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/arcanericky/totp/commands"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(commands.Execute())
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var versionText = "unspecified"
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show topt version",
|
||||
Long: "Show topt version",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("totp version %s %s/%s\n", versionText, runtime.GOOS, runtime.GOARCH)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
197
collection.go
197
collection.go
@@ -4,30 +4,38 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
var errSecretNotFound = errors.New("secret not found")
|
||||
var errNoFilename = errors.New("no save target")
|
||||
var errSecretNameEmpty = errors.New("secret name empty")
|
||||
var errSecretValueEmpty = errors.New("secret value empty")
|
||||
var ErrSecretNotFound = errors.New("secret not found")
|
||||
var ErrNoFilename = errors.New("no save target")
|
||||
var ErrSecretNameEmpty = errors.New("secret name empty")
|
||||
var ErrSecretValueEmpty = errors.New("secret value empty")
|
||||
|
||||
// Secret is a struct containing data necessary for working with secrets,
|
||||
// namely, the name of the secret name and the secret value
|
||||
type Secret struct {
|
||||
DateAdded time.Time
|
||||
// DateAdded is the date a secret was added to the collection
|
||||
DateAdded time.Time
|
||||
|
||||
// DateModified is the date a secret was last modified
|
||||
DateModified time.Time
|
||||
Name string
|
||||
Value string
|
||||
|
||||
// Name is the name of the secret used for retrieval
|
||||
Name string
|
||||
|
||||
// Value is the secret (seed) value
|
||||
Value string
|
||||
}
|
||||
|
||||
// Collection is a struct that holds TOTP data
|
||||
type Collection struct {
|
||||
Secrets map[string]Secret
|
||||
// Secrets is a map of secrets using the secret name as the key
|
||||
Secrets map[string]Secret
|
||||
|
||||
filename string
|
||||
writer io.Writer
|
||||
}
|
||||
@@ -45,73 +53,66 @@ type CollectionInterface interface {
|
||||
// Save serializes (marshals) the Collections struct and writes it to
|
||||
// a file
|
||||
func (c *Collection) Save() error {
|
||||
var err error
|
||||
var writerErr error
|
||||
|
||||
serializedSettings, err := c.Serialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if c.writer == nil && len(c.filename) == 0 {
|
||||
err = errNoFilename
|
||||
}
|
||||
|
||||
if c.writer != nil {
|
||||
_, writerErr = c.writer.Write(serializedSettings)
|
||||
}
|
||||
|
||||
if len(c.filename) != 0 {
|
||||
err = ioutil.WriteFile(c.filename, serializedSettings, 0600)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = writerErr
|
||||
}
|
||||
if c.writer != nil {
|
||||
_, err = c.writer.Write(serializedSettings)
|
||||
} else if len(c.filename) != 0 {
|
||||
err = os.WriteFile(c.filename, serializedSettings, 0600)
|
||||
} else {
|
||||
err = ErrNoFilename
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSecret deletes an Entry by name
|
||||
// DeleteSecret deletes an entry by name
|
||||
func (c *Collection) DeleteSecret(name string) (Secret, error) {
|
||||
var err error
|
||||
|
||||
retSecret, ok := c.Secrets[name]
|
||||
|
||||
if ok {
|
||||
delete(c.Secrets, name)
|
||||
} else {
|
||||
err = errSecretNotFound
|
||||
if !ok {
|
||||
return Secret{}, ErrSecretNotFound
|
||||
}
|
||||
|
||||
return retSecret, err
|
||||
delete(c.Secrets, name)
|
||||
|
||||
return retSecret, nil
|
||||
}
|
||||
|
||||
// UpdateSecret updates (if it exists) or adds a new Entry with the
|
||||
// UpdateSecret updates (if it exists) or adds a new entry with the
|
||||
// name and value given
|
||||
func (c *Collection) UpdateSecret(name, value string) (Secret, error) {
|
||||
var retSecret Secret
|
||||
var err error
|
||||
var ok bool
|
||||
|
||||
if len(name) == 0 {
|
||||
err = errSecretNameEmpty
|
||||
} else if len(value) == 0 {
|
||||
err = errSecretValueEmpty
|
||||
return Secret{}, ErrSecretNameEmpty
|
||||
}
|
||||
|
||||
if len(value) == 0 {
|
||||
return Secret{}, ErrSecretValueEmpty
|
||||
}
|
||||
|
||||
_, err := totp.GenerateCode(value, time.Now())
|
||||
if err != nil {
|
||||
return Secret{}, err
|
||||
}
|
||||
|
||||
retSecret, ok := c.Secrets[name]
|
||||
if ok {
|
||||
// entry indicates an update
|
||||
retSecret.Value = value
|
||||
retSecret.DateModified = time.Now()
|
||||
c.Secrets[name] = retSecret
|
||||
} else {
|
||||
_, err = totp.GenerateCode(value, time.Now())
|
||||
if err == nil {
|
||||
retSecret, ok = c.Secrets[name]
|
||||
if ok {
|
||||
retSecret.Value = value
|
||||
retSecret.DateModified = time.Now()
|
||||
c.Secrets[name] = retSecret
|
||||
} else {
|
||||
dateAdded := time.Now()
|
||||
newSecret := Secret{Name: name, Value: value, DateAdded: dateAdded, DateModified: dateAdded}
|
||||
c.Secrets[name] = newSecret
|
||||
retSecret = newSecret
|
||||
}
|
||||
// no entry indicates an add
|
||||
dateAdded := time.Now()
|
||||
retSecret = Secret{
|
||||
Name: name,
|
||||
Value: value,
|
||||
DateAdded: dateAdded,
|
||||
DateModified: dateAdded,
|
||||
}
|
||||
c.Secrets[name] = retSecret
|
||||
}
|
||||
|
||||
return retSecret, err
|
||||
@@ -119,37 +120,31 @@ func (c *Collection) UpdateSecret(name, value string) (Secret, error) {
|
||||
|
||||
// RenameSecret renames a secret
|
||||
func (c *Collection) RenameSecret(oldName, newName string) (Secret, error) {
|
||||
var retSecret Secret
|
||||
var ok bool
|
||||
var err error
|
||||
|
||||
if len(newName) != 0 {
|
||||
retSecret, ok = c.Secrets[oldName]
|
||||
if ok {
|
||||
retSecret.Name = newName
|
||||
retSecret.DateModified = time.Now()
|
||||
c.Secrets[newName] = retSecret
|
||||
delete(c.Secrets, oldName)
|
||||
} else {
|
||||
err = errSecretNotFound
|
||||
}
|
||||
} else {
|
||||
err = errSecretNameEmpty
|
||||
if len(newName) == 0 {
|
||||
return Secret{}, ErrSecretNameEmpty
|
||||
}
|
||||
|
||||
return retSecret, err
|
||||
retSecret, ok := c.Secrets[oldName]
|
||||
if !ok {
|
||||
return Secret{}, ErrSecretNotFound
|
||||
}
|
||||
|
||||
retSecret.Name = newName
|
||||
retSecret.DateModified = time.Now()
|
||||
c.Secrets[newName] = retSecret
|
||||
delete(c.Secrets, oldName)
|
||||
|
||||
return retSecret, nil
|
||||
}
|
||||
|
||||
// GetSecret returns an Secret with the name argument
|
||||
// GetSecret returns a secret with the name argument
|
||||
func (c *Collection) GetSecret(name string) (Secret, error) {
|
||||
var err error
|
||||
|
||||
retSecret, ok := c.Secrets[name]
|
||||
if !ok {
|
||||
err = errSecretNotFound
|
||||
return Secret{}, ErrSecretNotFound
|
||||
}
|
||||
|
||||
return retSecret, err
|
||||
return retSecret, nil
|
||||
}
|
||||
|
||||
// GetSecrets returns a slice containing all the secrets
|
||||
@@ -164,14 +159,13 @@ func (c *Collection) GetSecrets() []Secret {
|
||||
|
||||
// GenerateCodeWithTime creates a TOTP code with the named secret's value
|
||||
func (c *Collection) GenerateCodeWithTime(name string, time time.Time) (string, error) {
|
||||
var code string
|
||||
|
||||
secret, err := c.GetSecret(name)
|
||||
if err == nil {
|
||||
code, err = totp.GenerateCode(secret.Value, time)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return code, err
|
||||
return totp.GenerateCode(secret.Value, time)
|
||||
|
||||
}
|
||||
|
||||
// GenerateCode creates a TOTP code with the named secret's value
|
||||
@@ -211,15 +205,13 @@ func NewCollection() *Collection {
|
||||
// NewCollectionWithData creates a new Collection instance with data from a byte slice
|
||||
func NewCollectionWithData(data []byte) (*Collection, error) {
|
||||
c := NewCollection()
|
||||
err := c.Deserialize(data)
|
||||
|
||||
return c, err
|
||||
return c, c.Deserialize(data)
|
||||
}
|
||||
|
||||
// NewCollectionWithReader creates a new collection from a Reader interface
|
||||
func NewCollectionWithReader(reader io.Reader) (*Collection, error) {
|
||||
data, err := ioutil.ReadAll(reader)
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return NewCollection(), err
|
||||
}
|
||||
@@ -227,16 +219,27 @@ func NewCollectionWithReader(reader io.Reader) (*Collection, error) {
|
||||
return NewCollectionWithData(data)
|
||||
}
|
||||
|
||||
// NewCollectionWithFile creates a new Collection instance with data from a file
|
||||
func NewCollectionWithFile(filename string) (*Collection, error) {
|
||||
c := NewCollection()
|
||||
|
||||
// NewCollectionWithFile creates a new Collection instance with data from a file.
|
||||
// If the file open fails, a new Collection instance is returned along with the
|
||||
// file open error, which guarantees a usable but empty collection is returned.
|
||||
//
|
||||
// Returning data to be used along with an error is bad design but changing this
|
||||
// would be a breaking API change.
|
||||
func NewCollectionWithFile(filename string) (c *Collection, err error) {
|
||||
f, err := os.Open(filename)
|
||||
|
||||
if err == nil {
|
||||
c, err = NewCollectionWithReader(f)
|
||||
if err != nil {
|
||||
c := NewCollection()
|
||||
c.filename = filename
|
||||
return c, err
|
||||
}
|
||||
f.Close()
|
||||
|
||||
defer func() {
|
||||
if e := f.Close(); e != nil && err == nil {
|
||||
err = e
|
||||
}
|
||||
}()
|
||||
|
||||
c, err = NewCollectionWithReader(f)
|
||||
c.filename = filename
|
||||
|
||||
return c, err
|
||||
|
||||
1025
collection_test.go
1025
collection_test.go
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -19,11 +19,12 @@ func createTestData(t *testing.T) []secretItem {
|
||||
|
||||
// Create some test data
|
||||
secretList := []secretItem{
|
||||
{name: "name0", value: "seed"},
|
||||
{name: "name1", value: "seed"},
|
||||
{name: "name2", value: "seedseed"},
|
||||
{name: "name3", value: "seed"},
|
||||
{name: "name4", value: "seed"},
|
||||
{name: "name0", value: "SEED"},
|
||||
{name: "name1", value: "SEED"},
|
||||
{name: "name2", value: "SEEDSEED"},
|
||||
{name: "name3", value: "SEED"},
|
||||
{name: "name4", value: "SEED"},
|
||||
{name: "testname", value: "TESTSECRET"},
|
||||
}
|
||||
|
||||
for _, i := range secretList {
|
||||
@@ -33,7 +34,7 @@ func createTestData(t *testing.T) []secretItem {
|
||||
}
|
||||
}
|
||||
|
||||
c.Save()
|
||||
_ = c.Save()
|
||||
|
||||
return secretList
|
||||
}
|
||||
21
commands/config.go
Normal file
21
commands/config.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func getConfigCmd(rootCmd *cobra.Command) *cobra.Command {
|
||||
var cobraCmd = &cobra.Command{
|
||||
Use: cmdConfig,
|
||||
Short: "Configure totp",
|
||||
Long: `Configure totp`,
|
||||
}
|
||||
|
||||
cobraCmd.AddCommand(getConfigListCmd())
|
||||
cobraCmd.AddCommand(getConfigRenameCmd(rootCmd))
|
||||
cobraCmd.AddCommand(getConfigUpdateCmd(rootCmd))
|
||||
cobraCmd.AddCommand(getConfigDeleteCmd())
|
||||
cobraCmd.AddCommand(getConfigResetCmd())
|
||||
|
||||
return cobraCmd
|
||||
}
|
||||
73
commands/configdelete.go
Normal file
73
commands/configdelete.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func deleteSecret(name string) {
|
||||
s, err := collectionFile.loader()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error loading settings:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.DeleteSecret(name); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error deleting secret:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Save(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error saving settings:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := printResultf("Deleted secret %s\n", name); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigDeleteCmd() *cobra.Command {
|
||||
var (
|
||||
confirmAll bool
|
||||
cobraCmd = &cobra.Command{
|
||||
Use: "delete",
|
||||
Aliases: []string{"remove", "erase", "rm", "del"},
|
||||
Short: "Delete a secret",
|
||||
Long: `Delete a secret`,
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
fmt.Fprintln(os.Stderr, "Must provide a secret name to delete.")
|
||||
return
|
||||
}
|
||||
|
||||
secretName := args[0]
|
||||
|
||||
if !confirmAll {
|
||||
confirm, err := userConfirm(bufio.NewReader(os.Stdin),
|
||||
fmt.Sprintf("This will delete secret %s.", secretName))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error getting response:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !confirm {
|
||||
fmt.Println("Skipping delete")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
deleteSecret(secretName)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
cobraCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
|
||||
cobraCmd.Flags().BoolVarP(&confirmAll, optionYes, "y", false, "confirm all prompts")
|
||||
|
||||
return cobraCmd
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -8,10 +8,16 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigDelete(t *testing.T) {
|
||||
defaults()
|
||||
collectionFile.filename = "testcollection.json"
|
||||
|
||||
secretList := createTestData(t)
|
||||
|
||||
configDeleteCmd := getConfigDeleteCmd()
|
||||
configDeleteCmd.Run(nil, []string{"secret"})
|
||||
|
||||
_ = configDeleteCmd.Flags().Set(optionYes, "true")
|
||||
|
||||
// Secret does not exit
|
||||
configDeleteCmd.Run(nil, []string{"secret"})
|
||||
|
||||
124
commands/configlist.go
Normal file
124
commands/configlist.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/arcanericky/totp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func titleLine(len int) string {
|
||||
var builder strings.Builder
|
||||
builder.Grow(len)
|
||||
|
||||
for i := 0; i < len; i++ {
|
||||
builder.WriteString("-")
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func listSecretNames(writer io.Writer, secrets []totp.Secret) {
|
||||
for _, s := range secrets {
|
||||
fmt.Fprintln(writer, s.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func listInfo(writer io.Writer, secrets []totp.Secret, all bool) {
|
||||
const (
|
||||
nameTitle = "Name"
|
||||
secretTitle = "Secret"
|
||||
addedDateTitle = "Date Added"
|
||||
modifiedDateTitle = "Date Modified"
|
||||
)
|
||||
|
||||
maxNameLen := len(nameTitle)
|
||||
maxSecretLen := len(secretTitle)
|
||||
for _, s := range secrets {
|
||||
nameLen := len(s.Name)
|
||||
if nameLen > maxNameLen {
|
||||
maxNameLen = nameLen
|
||||
}
|
||||
|
||||
secretLen := len(s.Value)
|
||||
if secretLen > maxSecretLen {
|
||||
maxSecretLen = secretLen
|
||||
}
|
||||
}
|
||||
|
||||
const timeFormat = "Jan _2 2006 15:04:05"
|
||||
timeFormatLen := len(timeFormat)
|
||||
timeFormatLine := titleLine(len(timeFormat))
|
||||
|
||||
if all {
|
||||
fmt.Fprintf(writer, "%-*s %-*s %-*s %-*s\n",
|
||||
maxNameLen, nameTitle,
|
||||
maxSecretLen, secretTitle,
|
||||
timeFormatLen, addedDateTitle,
|
||||
timeFormatLen, modifiedDateTitle)
|
||||
fmt.Fprintf(writer, "%s %s %s %s\n", titleLine(maxNameLen), titleLine(maxSecretLen), timeFormatLine, timeFormatLine)
|
||||
for _, s := range secrets {
|
||||
fmt.Fprintf(writer, "%-*s %-*s %s %s\n", maxNameLen, s.Name, maxSecretLen, s.Value, s.DateAdded.Format(timeFormat), s.DateModified.Format(timeFormat))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "%-*s %-*s %-*s\n",
|
||||
maxNameLen, nameTitle,
|
||||
timeFormatLen, addedDateTitle,
|
||||
timeFormatLen, modifiedDateTitle)
|
||||
fmt.Fprintf(writer, "%s %s %s\n", titleLine(maxNameLen), timeFormatLine, timeFormatLine)
|
||||
for _, s := range secrets {
|
||||
fmt.Fprintf(writer, "%-*s %s %s\n", maxNameLen, s.Name, s.DateAdded.Format(timeFormat), s.DateModified.Format(timeFormat))
|
||||
}
|
||||
}
|
||||
|
||||
func listSecrets(names, all bool) {
|
||||
c, err := collectionFile.loader()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error loading collection", err)
|
||||
return
|
||||
}
|
||||
|
||||
secrets := c.GetSecrets()
|
||||
sort.Slice(secrets, func(i, j int) bool {
|
||||
return secrets[i].Name < secrets[j].Name
|
||||
})
|
||||
|
||||
if names {
|
||||
listSecretNames(os.Stdout, secrets)
|
||||
} else {
|
||||
listInfo(os.Stdout, secrets, all)
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigListCmd() *cobra.Command {
|
||||
var (
|
||||
names bool
|
||||
all bool
|
||||
cobraCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls", "l"},
|
||||
Short: "List secrets",
|
||||
Long: `List secrets`,
|
||||
Run: func(listCmd *cobra.Command, _ []string) {
|
||||
if names && all {
|
||||
fmt.Fprintln(os.Stderr, "Only one of --names or --all can be used.")
|
||||
return
|
||||
}
|
||||
listSecrets(names, all)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
cobraCmd.Flags().BoolVarP(&names, "names", "n", false, "list only secret names")
|
||||
cobraCmd.Flags().BoolVarP(&all, "all", "a", false, "list all secret info")
|
||||
|
||||
cobraCmd.Flags().BoolP(optionStdio, "", false, "load data from stdin")
|
||||
|
||||
return cobraCmd
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -10,9 +10,23 @@ func TestConfigList(t *testing.T) {
|
||||
|
||||
createTestData(t)
|
||||
|
||||
configListCmd := getConfigListCmd()
|
||||
|
||||
configListCmd.Run(configListCmd, []string{})
|
||||
|
||||
configListCmd.Flags().Set("names", "true")
|
||||
// names only
|
||||
_ = configListCmd.Flags().Set("names", "true")
|
||||
configListCmd.Run(configListCmd, []string{})
|
||||
|
||||
// all
|
||||
configListCmd = getConfigListCmd()
|
||||
_ = configListCmd.Flags().Set("all", "true")
|
||||
configListCmd.Run(configListCmd, []string{})
|
||||
|
||||
// names and all
|
||||
configListCmd = getConfigListCmd()
|
||||
_ = configListCmd.Flags().Set("all", "true")
|
||||
_ = configListCmd.Flags().Set("names", "true")
|
||||
configListCmd.Run(configListCmd, []string{})
|
||||
|
||||
configListCmd.ResetFlags()
|
||||
53
commands/configrename.go
Normal file
53
commands/configrename.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func renameSecret(source, target string) {
|
||||
if isReservedCommand(target) {
|
||||
fmt.Fprintln(os.Stderr, "The name \""+target+"\" is reserved for the "+target+" command")
|
||||
return
|
||||
}
|
||||
|
||||
s, _ := collectionFile.loader()
|
||||
if _, err := s.RenameSecret(source, target); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error renaming secret:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Save(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error saving settings:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := printResultf("Renamed secret %s to %s\n", source, target); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigRenameCmd(rootCmd *cobra.Command) *cobra.Command {
|
||||
var cobraCmd = &cobra.Command{
|
||||
Use: "rename",
|
||||
Aliases: []string{"ren", "mv"},
|
||||
Short: "Rename a secret",
|
||||
Long: `Rename a secret`,
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "Must provide source and target.")
|
||||
return
|
||||
}
|
||||
|
||||
renameSecret(args[0], args[1])
|
||||
},
|
||||
}
|
||||
|
||||
cobraCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
|
||||
cobraCmd.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [old secret name] [new secret name]", 1))
|
||||
return cobraCmd
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -12,6 +12,8 @@ func TestConfigRename(t *testing.T) {
|
||||
|
||||
secrets := createTestData(t)
|
||||
|
||||
configRenameCmd := getConfigRenameCmd(getRootCmd())
|
||||
|
||||
configRenameCmd.Run(nil, []string{})
|
||||
|
||||
// Valid parameters
|
||||
@@ -28,15 +30,16 @@ func TestConfigRename(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test rename to config
|
||||
configRenameCmd.Run(nil, []string{newName, configCmd.Use})
|
||||
configCmdUse := "config"
|
||||
configRenameCmd.Run(nil, []string{newName, configCmdUse})
|
||||
c, err = totp.NewCollectionWithFile(collectionFile.filename)
|
||||
if err != nil {
|
||||
t.Error("Could not load collection for rename test from file")
|
||||
}
|
||||
|
||||
_, err = c.GetSecret(configCmd.Use)
|
||||
_, err = c.GetSecret(configCmdUse)
|
||||
if err == nil {
|
||||
t.Error("Secret should not have been renamed to \"" + configCmd.Use + "\"")
|
||||
t.Error("Secret should not have been renamed to \"" + configCmdUse + "\"")
|
||||
}
|
||||
|
||||
// No collections file
|
||||
50
commands/configreset.go
Normal file
50
commands/configreset.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func configReset(filename string) error {
|
||||
if err := os.Remove(filename); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error removing collection file %s: %s\n", filename, err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Collection file %s removed\n", filename)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getConfigResetCmd() *cobra.Command {
|
||||
var (
|
||||
confirmAll bool
|
||||
cobraCmd = &cobra.Command{
|
||||
Use: "reset",
|
||||
Short: "Reset the TOTP colllection",
|
||||
Long: "Reset the TOTP colllection",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
if !confirmAll {
|
||||
confirm, err := userConfirm(bufio.NewReader(os.Stdin), "This will remove all secrets.")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error getting response:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !confirm {
|
||||
fmt.Println("Skipping reset")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_ = configReset(collectionFile.filename)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
cobraCmd.Flags().BoolVarP(&confirmAll, optionYes, "y", false, "confirm all prompts")
|
||||
|
||||
return cobraCmd
|
||||
}
|
||||
27
commands/configreset_test.go
Normal file
27
commands/configreset_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigReset(t *testing.T) {
|
||||
collectionFile.filename = "testcollection.json"
|
||||
|
||||
createTestData(t)
|
||||
configResetCmd := getConfigResetCmd()
|
||||
_ = configResetCmd.Flags().Set(optionYes, "true")
|
||||
configResetCmd.Run(nil, []string{})
|
||||
|
||||
_, err := os.Stat(collectionFile.filename)
|
||||
if !os.IsNotExist(err) {
|
||||
t.Error("Failed to remove the collection file")
|
||||
}
|
||||
|
||||
configResetCmd = getConfigResetCmd()
|
||||
configResetCmd.Run(nil, []string{})
|
||||
|
||||
if err := configReset("nosuchfilename.example"); err == nil {
|
||||
t.Error("Failed to generate error removing invalid collection file")
|
||||
}
|
||||
}
|
||||
61
commands/configupdate.go
Normal file
61
commands/configupdate.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func updateSecret(name, value string) {
|
||||
if isReservedCommand(name) {
|
||||
fmt.Fprintln(os.Stderr, "The name \""+name+"\" is reserved for the "+name+" command")
|
||||
return
|
||||
}
|
||||
|
||||
// ignore error because file may not exist
|
||||
s, _ := collectionFile.loader()
|
||||
|
||||
secret, err := s.UpdateSecret(name, value)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error updating secret:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.Save(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error saving settings:", err)
|
||||
return
|
||||
}
|
||||
|
||||
action := "Updated"
|
||||
if secret.DateAdded == secret.DateModified {
|
||||
action = "Added"
|
||||
}
|
||||
|
||||
if _, err := printResultf("%s secret %s\n", action, name); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigUpdateCmd(rootCmd *cobra.Command) *cobra.Command {
|
||||
var cobraCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Aliases: []string{"add"},
|
||||
Short: "Add or update a secret",
|
||||
Long: `Add or update a secret`,
|
||||
Run: func(_ *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "Must provide name and secret")
|
||||
return
|
||||
}
|
||||
|
||||
updateSecret(args[0], args[1])
|
||||
},
|
||||
}
|
||||
|
||||
cobraCmd.Flags().BoolP(optionStdio, "", false, "load with stdin, save with stdout")
|
||||
cobraCmd.SetUsageTemplate(strings.Replace(rootCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [secret name] [secret value]", 1))
|
||||
return cobraCmd
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -12,6 +12,8 @@ func TestConfigUpdate(t *testing.T) {
|
||||
|
||||
createTestData(t)
|
||||
|
||||
configUpdateCmd := getConfigUpdateCmd(getRootCmd())
|
||||
|
||||
// Valid parameters
|
||||
secretName := "testsecret"
|
||||
configUpdateCmd.Run(nil, []string{secretName, "seed"})
|
||||
@@ -26,7 +28,7 @@ func TestConfigUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test update secret
|
||||
newSecret := "seedseed"
|
||||
newSecret := "SEEDSEED"
|
||||
configUpdateCmd.Run(nil, []string{secretName, newSecret})
|
||||
c, err = totp.NewCollectionWithFile(collectionFile.filename)
|
||||
if err != nil {
|
||||
@@ -39,7 +41,7 @@ func TestConfigUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test using secret named 'config'
|
||||
secretName = configCmd.Use
|
||||
secretName = "config"
|
||||
configUpdateCmd.Run(nil, []string{secretName, "seed"})
|
||||
c, err = totp.NewCollectionWithFile(collectionFile.filename)
|
||||
if err != nil {
|
||||
@@ -48,7 +50,7 @@ func TestConfigUpdate(t *testing.T) {
|
||||
|
||||
secret, err = c.GetSecret(secretName)
|
||||
if err == nil {
|
||||
t.Error("Secret named \"" + configCmd.Use + "\" should not have been saved")
|
||||
t.Error("Secret named \"" + secretName + "\" should not have been saved")
|
||||
}
|
||||
|
||||
// No parameters passed
|
||||
26
commands/confirm.go
Normal file
26
commands/confirm.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func userConfirm(reader *bufio.Reader, prompt string) (bool, error) {
|
||||
for {
|
||||
fmt.Printf("%s Continue? [y/n]: ", prompt)
|
||||
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
|
||||
if response == "y" || response == "yes" {
|
||||
return true, nil
|
||||
} else if response == "n" || response == "no" {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
68
commands/confirm_test.go
Normal file
68
commands/confirm_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_userConfirm(t *testing.T) {
|
||||
type args struct {
|
||||
reader *bufio.Reader
|
||||
prompt string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "yes",
|
||||
args: args{
|
||||
reader: bufio.NewReader(strings.NewReader("yes\n")),
|
||||
prompt: "",
|
||||
},
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "y",
|
||||
args: args{
|
||||
reader: bufio.NewReader(strings.NewReader("y\n")),
|
||||
prompt: "",
|
||||
},
|
||||
want: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no",
|
||||
args: args{
|
||||
reader: bufio.NewReader(strings.NewReader("no\n")),
|
||||
prompt: "",
|
||||
},
|
||||
want: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "n",
|
||||
args: args{
|
||||
reader: bufio.NewReader(strings.NewReader("n\n")),
|
||||
prompt: "",
|
||||
},
|
||||
want: false,
|
||||
wantErr: false,
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := userConfirm(tt.args.reader, tt.args.prompt)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("userConfirm() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("userConfirm() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -8,7 +8,13 @@ import (
|
||||
"github.com/arcanericky/totp"
|
||||
)
|
||||
|
||||
const defaultBaseCollectionFile = "totp-config.json"
|
||||
const (
|
||||
defaultBaseCollectionFile = "totp-config.json"
|
||||
|
||||
cmdVersion = "version"
|
||||
cmdConfig = "config"
|
||||
cmdCompletion = "completion"
|
||||
)
|
||||
|
||||
var collectionFile struct {
|
||||
filename string
|
||||
@@ -28,14 +34,20 @@ func loadCollectionFromDefaultFile() (*totp.Collection, error) {
|
||||
}
|
||||
|
||||
func setCollectionFile(goos string) {
|
||||
if totpFile := os.Getenv("TOTP_CONFIG"); totpFile != "" {
|
||||
collectionFile.filename = totpFile
|
||||
return
|
||||
}
|
||||
|
||||
if goos == "windows" {
|
||||
collectionFile.filename = filepath.Join(os.Getenv("LOCALAPPDATA"), defaultBaseCollectionFile)
|
||||
} else {
|
||||
collectionFile.filename = filepath.Join(os.Getenv("HOME"), "."+defaultBaseCollectionFile)
|
||||
return
|
||||
}
|
||||
|
||||
collectionFile.filename = filepath.Join(os.Getenv("HOME"), "."+defaultBaseCollectionFile)
|
||||
}
|
||||
|
||||
var reservedCommands = []string{configCmd.Use, versionCmd.Use}
|
||||
var reservedCommands = []string{cmdConfig, cmdVersion, cmdCompletion}
|
||||
|
||||
func isReservedCommand(name string) bool {
|
||||
for _, c := range reservedCommands {
|
||||
@@ -47,7 +59,7 @@ func isReservedCommand(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func init() {
|
||||
func defaults() {
|
||||
setCollectionFile(runtime.GOOS)
|
||||
collectionFile.loader = loadCollectionFromDefaultFile
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
)
|
||||
|
||||
func TestDefaults(t *testing.T) {
|
||||
// Doesn't set and check exactly under on Linux but good enough for test
|
||||
setCollectionFile("windows")
|
||||
if collectionFile.filename != filepath.Join(os.Getenv("LOCALAPPDATA"), defaultBaseCollectionFile) {
|
||||
t.Error("Windows collection file not set properly")
|
||||
@@ -18,8 +17,14 @@ func TestDefaults(t *testing.T) {
|
||||
t.Error("Runtime OS collection file not set properly")
|
||||
}
|
||||
|
||||
os.Setenv("TOTP_CONFIG", "testcollectionfile.json")
|
||||
setCollectionFile("windows")
|
||||
if collectionFile.filename != os.Getenv("TOTP_CONFIG") {
|
||||
t.Error("Collection file not set properly with environment variable")
|
||||
}
|
||||
|
||||
// Not sure how to unit test but at least run it for now
|
||||
loadCollectionFromStdin()
|
||||
_, _ = loadCollectionFromStdin()
|
||||
|
||||
for _, c := range reservedCommands {
|
||||
if isReservedCommand(c) != true {
|
||||
@@ -1,9 +1,9 @@
|
||||
package cmd
|
||||
package commands
|
||||
|
||||
import "fmt"
|
||||
|
||||
func printResultf(format string, a ...interface{}) (n int, err error) {
|
||||
if collectionFile.useStdio == false {
|
||||
if !collectionFile.useStdio {
|
||||
return fmt.Printf(format, a...)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
func TestPrintResult(t *testing.T) {
|
||||
text := "test text"
|
||||
printResultf(text)
|
||||
_, _ = printResultf(text)
|
||||
collectionFile.useStdio = true
|
||||
printResultf(text)
|
||||
_, _ = printResultf(text)
|
||||
}
|
||||
63
commands/qrcode.go
Normal file
63
commands/qrcode.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
func getQrString(name, secret string) string {
|
||||
secret = strings.ToUpper(secret)
|
||||
|
||||
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
// otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
|
||||
return fmt.Sprintf("otpauth://totp/%s?secret=%s&issuer=%s", name, secret, name)
|
||||
}
|
||||
|
||||
func outputQrCode(writer io.Writer, name, secret string) error {
|
||||
qrString := getQrString(name, secret)
|
||||
q, err := qrcode.New(qrString, qrcode.Medium)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error generating qr code:", err)
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(writer, q.ToSmallString(false))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func qrCode(writer io.Writer, name, secret string) error {
|
||||
if len(name) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Name required for QR code generation")
|
||||
return errors.New("name required")
|
||||
}
|
||||
|
||||
if len(secret) != 0 {
|
||||
_, err := totp.GenerateCode(secret, time.Now())
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Invalid secret:", err)
|
||||
return err
|
||||
}
|
||||
return outputQrCode(writer, name, secret)
|
||||
}
|
||||
|
||||
c, err := collectionFile.loader()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error loading collection:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
s, err := c.GetSecret(name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to get collection entry for %s: %s\n", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return outputQrCode(writer, s.Name, s.Value)
|
||||
}
|
||||
246
commands/qrcode_test.go
Normal file
246
commands/qrcode_test.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testQrCode = `4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI
|
||||
4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI4paI
|
||||
4paI4paI4paICuKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKW
|
||||
iOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKW
|
||||
iOKWiOKWiOKWiOKWiOKWiOKWiArilojilojilojilogg4paE4paE4paE4paE4paEIOKWiOKWhOKW
|
||||
hOKWgOKWiOKWiCDiloDiloQg4paI4paE4paA4paI4paA4paE4paE4paA4paIIOKWhOKWhOKWhOKW
|
||||
hOKWhCDilojilojilojilogK4paI4paI4paI4paIIOKWiCAgIOKWiCDilojiloDiloAg4paI4paA
|
||||
4paI4paEIOKWgOKWhOKWgOKWiOKWgOKWhOKWiCAg4paIIOKWiCAgIOKWiCDilojilojilojilogK
|
||||
4paI4paI4paI4paIIOKWiOKWhOKWhOKWhOKWiCDilogg4paEIOKWhOKWgCDilojiloDiloTiloTi
|
||||
loTiloDilojilojiloTiloTilojilogg4paI4paE4paE4paE4paIIOKWiOKWiOKWiOKWiAriloji
|
||||
lojilojilojiloTiloTiloTiloTiloTiloTiloTilogg4paAIOKWiCDilojiloTilojiloTiloAg
|
||||
4paA4paE4paAIOKWiOKWhOKWiOKWhOKWhOKWhOKWhOKWhOKWhOKWhOKWiOKWiOKWiOKWiAriloji
|
||||
lojilojilojiloTilojiloDilogg4paI4paE4paEIOKWiOKWgOKWiOKWiOKWiOKWhCDiloAg4paA
|
||||
4paE4paE4paE4paI4paA4paI4paE4paE4paE4paEIOKWgOKWiCDilojilojilojilogK4paI4paI
|
||||
4paI4paIIOKWgCAgIOKWgOKWhOKWgOKWiOKWiOKWiOKWgOKWhCDiloTiloQg4paA4paE4paA4paA
|
||||
IOKWiOKWhCDilojiloAg4paE4paE4paIIOKWiOKWiOKWiOKWiOKWiArilojilojilojilojiloDi
|
||||
loTilojiloTilogg4paE4paA4paE4paA4paIIOKWhOKWhOKWgOKWiOKWhOKWhOKWgOKWiOKWiCDi
|
||||
loQg4paI4paAICDilojiloTiloDilojiloTilojilojilojilogK4paI4paI4paI4paI4paIIOKW
|
||||
hCDiloDiloTiloTilojiloQg4paE4paE4paI4paIICDiloAgIOKWiCDiloDilogg4paE4paIIOKW
|
||||
gOKWgOKWiOKWhCDiloDilojilojilojilogK4paI4paI4paI4paI4paI4paE4paA4paI4paE4paA
|
||||
4paE4paI4paA4paA4paE4paIIOKWiOKWgOKWgCDiloTiloTiloTiloTiloQgIOKWhCDiloTiloDi
|
||||
loTiloQg4paA4paA4paI4paI4paI4paICuKWiOKWiOKWiOKWiOKWiOKWgOKWiCAg4paE4paE4paA
|
||||
4paEIOKWgOKWhCDilojiloDiloTiloAg4paA4paI4paAIOKWiCDiloAg4paI4paEIOKWhOKWiOKW
|
||||
hOKWiOKWiOKWiOKWiOKWiArilojilojilojilogg4paE4paA4paE4paI4paA4paE4paI4paA4paA
|
||||
4paIIOKWiOKWhOKWiOKWiOKWgCDiloDiloTiloTiloQg4paA4paE4paIIOKWgOKWiOKWgOKWgOKW
|
||||
gOKWhOKWiOKWiOKWiOKWiArilojilojilojilojilojilojilojiloTiloTiloTiloTiloTilogg
|
||||
4paE4paE4paE4paA4paA4paI4paE4paAIOKWiOKWgCDiloDiloTiloAgIOKWiCDiloTilogg4paI
|
||||
4paI4paI4paI4paICuKWiOKWiOKWiOKWiOKWhOKWhOKWiOKWiOKWiOKWiOKWhOKWiCAgIOKWiOKW
|
||||
gCDilogg4paAIOKWiOKWhOKWiOKWhOKWhOKWgCDiloTiloTiloQg4paEIOKWhOKWgOKWiOKWiOKW
|
||||
iOKWiArilojilojilojilogg4paE4paE4paE4paE4paEIOKWiOKWhOKWiOKWiCAg4paI4paAIOKW
|
||||
iCAg4paA4paE4paAIOKWiCDilojiloTilogg4paI4paIIOKWiOKWiOKWiOKWiOKWiArilojiloji
|
||||
lojilogg4paIICAg4paIIOKWiOKWhOKWiCDiloTilogg4paI4paI4paI4paE4paA4paE4paI4paE
|
||||
4paA4paA4paEICAgIOKWhOKWgOKWiOKWiOKWiOKWiOKWiOKWiArilojilojilojilogg4paI4paE
|
||||
4paE4paE4paIIOKWiOKWiOKWhOKWiOKWiCDiloAgIOKWiOKWgOKWgOKWiCAg4paI4paAIOKWgOKW
|
||||
hOKWiCDilojiloTilojilojilojilojilojilogK4paI4paI4paI4paI4paE4paE4paE4paE4paE
|
||||
4paE4paE4paI4paE4paI4paE4paE4paE4paE4paE4paI4paI4paE4paI4paI4paI4paE4paE4paI
|
||||
4paE4paE4paE4paE4paI4paI4paE4paI4paE4paI4paI4paI4paICuKWiOKWiOKWiOKWiOKWiOKW
|
||||
iOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKW
|
||||
iOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiOKWiAriloDiloDi
|
||||
loDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDi
|
||||
loDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDiloDi
|
||||
loAK`
|
||||
|
||||
func RandStringBytes(n int) string {
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func Test_getQrString(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
secret string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
name: "testname",
|
||||
secret: "testsecret",
|
||||
},
|
||||
want: "otpauth://totp/testname?secret=TESTSECRET&issuer=testname",
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
name: "testname",
|
||||
secret: "TESTSECRET",
|
||||
},
|
||||
want: "otpauth://totp/testname?secret=TESTSECRET&issuer=testname",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getQrString(tt.args.name, tt.args.secret); got != tt.want {
|
||||
t.Errorf("getQrString() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_qrCode(t *testing.T) {
|
||||
collectionFile.filename = "testcollection.json"
|
||||
createTestData(t)
|
||||
collectionFile.loader = loadCollectionFromDefaultFile
|
||||
decoded, _ := base64.StdEncoding.DecodeString(testQrCode)
|
||||
type args struct {
|
||||
name string
|
||||
secret string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
args args
|
||||
wantWriter string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "name and secret",
|
||||
filename: "testcollection.json",
|
||||
args: args{
|
||||
name: "testname",
|
||||
secret: "testsecret",
|
||||
},
|
||||
wantWriter: string(decoded),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "name from collection",
|
||||
filename: "testcollection.json",
|
||||
args: args{
|
||||
name: "testname",
|
||||
secret: "",
|
||||
},
|
||||
wantWriter: string(decoded),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
filename: "testcollection.json",
|
||||
args: args{
|
||||
name: "",
|
||||
secret: "testsecret",
|
||||
},
|
||||
wantWriter: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no collecton entry",
|
||||
filename: "testcollection.json",
|
||||
args: args{
|
||||
name: "invalidname",
|
||||
secret: "",
|
||||
},
|
||||
wantWriter: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no collecton file",
|
||||
filename: "invalidcollectionfile.json",
|
||||
args: args{
|
||||
name: "invalidname",
|
||||
secret: "",
|
||||
},
|
||||
wantWriter: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid secret",
|
||||
filename: "testcollection.json",
|
||||
args: args{
|
||||
name: "testname",
|
||||
secret: "seed0",
|
||||
},
|
||||
wantWriter: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
collectionFile.filename = tt.filename
|
||||
writer := &bytes.Buffer{}
|
||||
if err := qrCode(writer, tt.args.name, tt.args.secret); (err != nil) != tt.wantErr {
|
||||
t.Errorf("qrCode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotWriter := writer.String(); gotWriter != tt.wantWriter {
|
||||
t.Errorf("qrCode() = %v, want %v", gotWriter, tt.wantWriter)
|
||||
}
|
||||
})
|
||||
}
|
||||
os.Remove(collectionFile.filename)
|
||||
}
|
||||
|
||||
func Test_outputQrCode(t *testing.T) {
|
||||
decoded, _ := base64.StdEncoding.DecodeString(testQrCode)
|
||||
|
||||
type args struct {
|
||||
name string
|
||||
secret string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantWriter string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid name and secret",
|
||||
args: args{
|
||||
name: "testname",
|
||||
secret: "TESTSECRET",
|
||||
},
|
||||
wantWriter: string(decoded),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid name and lowercase secret",
|
||||
args: args{
|
||||
name: "testname",
|
||||
secret: "testsecret",
|
||||
},
|
||||
wantWriter: string(decoded),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "data too long",
|
||||
args: args{
|
||||
name: RandStringBytes(2048),
|
||||
secret: RandStringBytes(2048),
|
||||
},
|
||||
wantWriter: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
writer := &bytes.Buffer{}
|
||||
if err := outputQrCode(writer, tt.args.name, tt.args.secret); (err != nil) != tt.wantErr {
|
||||
t.Errorf("outputQrCode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotWriter := writer.String(); gotWriter != tt.wantWriter {
|
||||
t.Errorf("outputQrCode() = %v, want %v", gotWriter, tt.wantWriter)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
291
commands/root.go
Normal file
291
commands/root.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
api "github.com/arcanericky/totp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
optionBackward = "backward"
|
||||
optionFile = "file"
|
||||
optionFollow = "follow"
|
||||
optionForward = "forward"
|
||||
optionQr = "qrcode"
|
||||
optionSecret = "secret"
|
||||
optionStdio = "stdio"
|
||||
optionTime = "time"
|
||||
optionYes = "yes"
|
||||
)
|
||||
|
||||
type generateCodesAPI func(time.Duration, time.Duration, time.Duration, func(time.Duration), string, string)
|
||||
|
||||
type runVars struct {
|
||||
secret string
|
||||
backward time.Duration
|
||||
forward time.Duration
|
||||
timeString string
|
||||
follow bool
|
||||
useStdio bool
|
||||
cfgFile string
|
||||
qr bool
|
||||
}
|
||||
|
||||
var generateCodesService generateCodesAPI
|
||||
|
||||
func getSecretNamesForCompletion(toComplete string) []string {
|
||||
var (
|
||||
secretNames []string
|
||||
err error
|
||||
c *api.Collection
|
||||
)
|
||||
|
||||
c, err = collectionFile.loader()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error loading collection:", err)
|
||||
} else {
|
||||
secrets := c.GetSecrets()
|
||||
for _, s := range secrets {
|
||||
if strings.HasPrefix(s.Name, toComplete) {
|
||||
secretNames = append(secretNames, s.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return secretNames
|
||||
}
|
||||
|
||||
func generateCode(writer io.Writer, name string, secret string, t time.Time) error {
|
||||
const errGen = "Error generating code:"
|
||||
if len(secret) != 0 {
|
||||
code, err := totp.GenerateCode(secret, t)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, errGen, err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(writer, code)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := collectionFile.loader()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error loading collection:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
code, err := c.GenerateCodeWithTime(name, t)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, errGen, err)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(writer, code)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func durationToNextInterval(now time.Time) time.Duration {
|
||||
var sleepSeconds int
|
||||
|
||||
s := now.Second()
|
||||
switch {
|
||||
case s == 0, s < 30:
|
||||
sleepSeconds = 30 - s
|
||||
case s >= 30:
|
||||
sleepSeconds = 60 - s
|
||||
}
|
||||
|
||||
return time.Duration(sleepSeconds)*time.Second -
|
||||
time.Duration(now.Nanosecond())*time.Nanosecond
|
||||
|
||||
}
|
||||
|
||||
func callOnInterval(runtime time.Duration, interval time.Duration, exec func() bool) {
|
||||
stopper := make(chan bool)
|
||||
|
||||
if runtime > 0 {
|
||||
go func() {
|
||||
time.Sleep(runtime)
|
||||
stopper <- true
|
||||
}()
|
||||
}
|
||||
|
||||
if exec != nil && exec() {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopper:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if exec != nil && exec() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateCodes(timeOffset time.Duration, durationToRun time.Duration, intervalTime time.Duration, sleep func(time.Duration), secretName, secret string) {
|
||||
sleep(durationToNextInterval(time.Now().Add(timeOffset)) + 10*time.Millisecond)
|
||||
|
||||
callOnInterval(durationToRun, intervalTime,
|
||||
func() bool {
|
||||
if err := generateCode(os.Stdout, secretName, secret, time.Now().Add(timeOffset)); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func run(cmd *cobra.Command, args []string, cfg runVars) {
|
||||
// var err error
|
||||
|
||||
secretLen := len(cfg.secret)
|
||||
argsLen := len(args)
|
||||
|
||||
errMsg := ""
|
||||
switch {
|
||||
// No secret, no secret name
|
||||
case secretLen == 0 && argsLen == 0:
|
||||
errMsg = "Secret name or secret is required."
|
||||
// Secret given but additional arguments were also given
|
||||
case !cfg.qr && secretLen > 0 && argsLen > 0:
|
||||
errMsg = "Secret was given so additional arguments are not needed."
|
||||
// No secret given and too many args
|
||||
case secretLen == 0 && argsLen > 1:
|
||||
errMsg = "Too many arguments. Only one secret name is required."
|
||||
}
|
||||
|
||||
if errMsg != "" {
|
||||
fmt.Fprintf(os.Stderr, errMsg+"\n\n")
|
||||
if err := cmd.Help(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.qr {
|
||||
secretName := ""
|
||||
if argsLen == 1 {
|
||||
secretName = args[0]
|
||||
}
|
||||
|
||||
_ = qrCode(os.Stdout, secretName, cfg.secret)
|
||||
return
|
||||
}
|
||||
|
||||
// Override if time was given
|
||||
var (
|
||||
codeTime time.Time
|
||||
err error
|
||||
)
|
||||
if len(cfg.timeString) > 0 {
|
||||
codeTime, err = time.Parse(time.RFC3339, cfg.timeString)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error parsing the time option:", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
codeTime = time.Now()
|
||||
// codeOffset is 0
|
||||
}
|
||||
|
||||
// Load the secret name
|
||||
secretName := ""
|
||||
if argsLen == 1 {
|
||||
secretName = args[0]
|
||||
}
|
||||
|
||||
// If here then a stored shared secret is wanted
|
||||
if err := generateCode(os.Stdout, secretName, cfg.secret, codeTime.Add(cfg.forward-cfg.backward)); err != nil {
|
||||
// generateCode will output error text
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.follow {
|
||||
generateCodesService(time.Until(codeTime)-cfg.backward+cfg.forward, 0, 30*time.Second, time.Sleep, secretName, cfg.secret)
|
||||
}
|
||||
}
|
||||
|
||||
func getRootCmd() *cobra.Command {
|
||||
var cfg runVars
|
||||
|
||||
var cobraCmd = &cobra.Command{
|
||||
Use: "totp",
|
||||
Short: "TOTP Generator",
|
||||
Long: `TOTP Generator`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
if cmd.Flags().Changed(optionFile) {
|
||||
collectionFile.filename = cfg.cfgFile
|
||||
}
|
||||
|
||||
if cmd.Flags().Lookup(optionStdio) != nil {
|
||||
if cfg.useStdio {
|
||||
collectionFile.loader = loadCollectionFromStdin
|
||||
collectionFile.useStdio = true
|
||||
}
|
||||
}
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) != 0 {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return getSecretNamesForCompletion(toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
run(cmd, args, cfg)
|
||||
},
|
||||
}
|
||||
|
||||
var duration time.Duration
|
||||
|
||||
generateCodesService = generateCodes
|
||||
|
||||
cobraCmd.PersistentFlags().StringVarP(&cfg.cfgFile, optionFile, "f", "", "secret collection file")
|
||||
|
||||
cobraCmd.Flags().StringVarP(&cfg.secret, optionSecret, "s", "", "TOTP secret value")
|
||||
cobraCmd.Flags().BoolVarP(&cfg.useStdio, optionStdio, "", false, "load with stdin")
|
||||
cobraCmd.Flags().StringVarP(&cfg.timeString, optionTime, "", "", "RFC3339 time for TOTP (2019-06-23T20:00:00-05:00)")
|
||||
cobraCmd.Flags().DurationVarP(&cfg.backward, optionBackward, "", duration, "move time backward (ex. \"30s\")")
|
||||
cobraCmd.Flags().DurationVarP(&cfg.forward, optionForward, "", duration, "move time forward (ex. \"1m\")")
|
||||
cobraCmd.Flags().BoolVarP(&cfg.follow, optionFollow, "", false, "continuous output")
|
||||
cobraCmd.Flags().BoolVarP(&cfg.qr, optionQr, "", false, "output QR code")
|
||||
|
||||
cobraCmd.SetUsageTemplate(strings.Replace(cobraCmd.UsageTemplate(), "{{.UseLine}}", "{{.UseLine}}\n {{.CommandPath}} [secret name]", 1))
|
||||
|
||||
cobraCmd.AddCommand(getVersionCmd())
|
||||
cobraCmd.AddCommand(getConfigCmd(cobraCmd))
|
||||
|
||||
return cobraCmd
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() int {
|
||||
retVal := 0
|
||||
|
||||
defaults()
|
||||
rootCmd := getRootCmd()
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
retVal = 1
|
||||
}
|
||||
|
||||
return retVal
|
||||
}
|
||||
421
commands/root_test.go
Normal file
421
commands/root_test.go
Normal file
@@ -0,0 +1,421 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestRoot(t *testing.T) {
|
||||
Execute()
|
||||
collectionFile.filename = "testcollection.json"
|
||||
|
||||
secretList := createTestData(t)
|
||||
|
||||
rootCmd := getRootCmd()
|
||||
|
||||
// No parameters
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
|
||||
// Valid entry and secret
|
||||
rootCmd.Run(rootCmd, []string{secretList[0].name})
|
||||
|
||||
// Non-existing entry
|
||||
rootCmd.Run(rootCmd, []string{"invalidsecret"})
|
||||
|
||||
// Test follow condition
|
||||
savedGenerateCodesService := generateCodesService
|
||||
generateCodesService = func(time.Duration, time.Duration, time.Duration, func(time.Duration), string, string) {}
|
||||
_ = rootCmd.Flags().Lookup(optionFollow).Value.Set("true")
|
||||
rootCmd.Run(rootCmd, []string{"name0"})
|
||||
generateCodesService = savedGenerateCodesService
|
||||
_ = rootCmd.Flags().Lookup(optionFollow).Value.Set("false")
|
||||
|
||||
// Completion
|
||||
rootCmd.ValidArgsFunction(rootCmd, []string{}, "na")
|
||||
|
||||
// Completion with args
|
||||
rootCmd.ValidArgsFunction(rootCmd, []string{"secret"}, "na")
|
||||
|
||||
// No collections file
|
||||
os.Remove(collectionFile.filename)
|
||||
rootCmd.Run(rootCmd, []string{secretList[0].name})
|
||||
|
||||
// Completion without collections
|
||||
rootCmd.ValidArgsFunction(rootCmd, []string{}, "na")
|
||||
|
||||
// Excessive args
|
||||
rootCmd.Run(rootCmd, []string{"secretname", "extraarg"})
|
||||
|
||||
// Provide secret option
|
||||
_ = rootCmd.Flags().Set(optionSecret, "seed")
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
|
||||
// Provide invalid secret option
|
||||
_ = rootCmd.Flags().Set(optionSecret, "seed1")
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
|
||||
// File option
|
||||
_ = rootCmd.Flags().Set(optionFile, collectionFile.filename)
|
||||
rootCmd.Flags().Lookup(optionFile).Changed = true
|
||||
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
|
||||
|
||||
// Stdio option
|
||||
_ = rootCmd.Flags().Set(optionStdio, "true")
|
||||
rootCmd.PersistentPreRun(rootCmd, []string{"secret"})
|
||||
collectionFile.loader = loadCollectionFromDefaultFile
|
||||
collectionFile.useStdio = false
|
||||
_ = rootCmd.Flags().Set(optionStdio, "")
|
||||
|
||||
// Time option
|
||||
_ = rootCmd.Flags().Set(optionTime, "2019-06-01T20:00:00-05:00")
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
|
||||
// Give secret and secret name
|
||||
_ = rootCmd.Flags().Set(optionSecret, "seed")
|
||||
rootCmd.Run(rootCmd, []string{"secretname"})
|
||||
_ = rootCmd.Flags().Set(optionSecret, "")
|
||||
|
||||
// Invalid time option
|
||||
_ = rootCmd.Flags().Set(optionTime, "invalidtime")
|
||||
rootCmd.Run(rootCmd, []string{})
|
||||
_ = rootCmd.Flags().Set(optionTime, "")
|
||||
os.Remove(collectionFile.filename)
|
||||
}
|
||||
|
||||
func Test_run(t *testing.T) {
|
||||
collectionFile.filename = "testcollection.json"
|
||||
_ = createTestData(t)
|
||||
type args struct {
|
||||
cmd *cobra.Command
|
||||
args []string
|
||||
cfg runVars
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "qr code",
|
||||
args: args{
|
||||
cmd: &cobra.Command{},
|
||||
args: []string{"testname"},
|
||||
cfg: runVars{qr: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret name or secret required",
|
||||
args: args{
|
||||
cmd: &cobra.Command{},
|
||||
args: []string{},
|
||||
cfg: runVars{secret: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret + additional args",
|
||||
args: args{
|
||||
cmd: &cobra.Command{},
|
||||
args: []string{"name"},
|
||||
cfg: runVars{
|
||||
qr: false,
|
||||
secret: "seed",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "too many arguments",
|
||||
args: args{
|
||||
cmd: &cobra.Command{},
|
||||
args: []string{"name", "extra-arg"},
|
||||
cfg: runVars{
|
||||
qr: false,
|
||||
secret: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret from collection",
|
||||
args: args{
|
||||
cmd: &cobra.Command{},
|
||||
args: []string{"name3"},
|
||||
cfg: runVars{
|
||||
qr: false,
|
||||
secret: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secret from collection now found",
|
||||
args: args{
|
||||
cmd: &cobra.Command{},
|
||||
args: []string{"invalidsecretname"},
|
||||
cfg: runVars{
|
||||
qr: false,
|
||||
secret: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "time parse error",
|
||||
args: args{
|
||||
cmd: &cobra.Command{},
|
||||
args: []string{"testname"},
|
||||
cfg: runVars{
|
||||
timeString: "invalidtime",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
run(tt.args.cmd, tt.args.args, tt.args.cfg)
|
||||
})
|
||||
}
|
||||
|
||||
os.Remove(collectionFile.filename)
|
||||
}
|
||||
|
||||
func Test_generateCodes(t *testing.T) {
|
||||
var duration time.Duration
|
||||
type args struct {
|
||||
timeOffset time.Duration
|
||||
durationToRun time.Duration
|
||||
intervalTime time.Duration
|
||||
sleep func(time.Duration)
|
||||
secretName string
|
||||
secret string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "valid seed",
|
||||
args: args{
|
||||
timeOffset: duration,
|
||||
durationToRun: 2 * time.Millisecond,
|
||||
intervalTime: 1 * time.Millisecond,
|
||||
sleep: func(d time.Duration) {},
|
||||
secretName: "",
|
||||
secret: "seed",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid seed",
|
||||
args: args{
|
||||
timeOffset: duration,
|
||||
durationToRun: 2 * time.Millisecond,
|
||||
intervalTime: 1 * time.Millisecond,
|
||||
sleep: func(d time.Duration) {},
|
||||
secretName: "",
|
||||
secret: "invalidseed",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
generateCodes(tt.args.timeOffset, tt.args.durationToRun, tt.args.intervalTime, tt.args.sleep, tt.args.secretName, tt.args.secret)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_callOnInterval(t *testing.T) {
|
||||
execCount := 0
|
||||
preAndExecNormal := func() bool { return false }
|
||||
preAndExecEarlyExit := func() bool { execCount++; return execCount == 2 }
|
||||
type args struct {
|
||||
runtime time.Duration
|
||||
interval time.Duration
|
||||
exec func() bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
startExecCount int
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "normal execution",
|
||||
startExecCount: 0,
|
||||
args: args{
|
||||
runtime: 2 * time.Millisecond,
|
||||
interval: 1 * time.Millisecond,
|
||||
exec: preAndExecNormal,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exit at preExec",
|
||||
startExecCount: 1,
|
||||
args: args{
|
||||
runtime: 2 * time.Millisecond,
|
||||
interval: 1 * time.Millisecond,
|
||||
exec: preAndExecNormal,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exit at top exec",
|
||||
startExecCount: 1,
|
||||
args: args{
|
||||
runtime: 2 * time.Millisecond,
|
||||
interval: 1 * time.Millisecond,
|
||||
exec: preAndExecEarlyExit,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exit at loop exec",
|
||||
startExecCount: 0,
|
||||
args: args{
|
||||
runtime: 2 * time.Millisecond,
|
||||
interval: 1 * time.Millisecond,
|
||||
exec: preAndExecEarlyExit,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no callbacks",
|
||||
startExecCount: 0,
|
||||
args: args{
|
||||
runtime: 2 * time.Millisecond,
|
||||
interval: 1 * time.Millisecond,
|
||||
exec: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
execCount = tt.startExecCount
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
callOnInterval(tt.args.runtime, tt.args.interval, tt.args.exec)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_durationToNextInterval(t *testing.T) {
|
||||
type args struct {
|
||||
now string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
name: "29 seconds",
|
||||
args: args{
|
||||
now: "2019-06-23T20:00:01-05:00",
|
||||
},
|
||||
want: time.Duration(29 * time.Second),
|
||||
},
|
||||
{
|
||||
name: "29 seconds 2",
|
||||
args: args{
|
||||
now: "2019-06-23T20:00:31-05:00",
|
||||
},
|
||||
want: time.Duration(29 * time.Second),
|
||||
},
|
||||
{
|
||||
name: "28 seconds",
|
||||
args: args{
|
||||
now: "2019-06-23T20:00:31.001-05:00",
|
||||
},
|
||||
want: time.Duration(28*time.Second + 999*time.Millisecond),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
now, _ := time.Parse(time.RFC3339, tt.args.now)
|
||||
if got := durationToNextInterval(now); got != tt.want {
|
||||
t.Errorf("durationToNextInterval() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_generateCode(t *testing.T) {
|
||||
now, _ := time.Parse(time.RFC3339, "2019-06-23T20:00:01-05:00")
|
||||
type args struct {
|
||||
name string
|
||||
secret string
|
||||
t time.Time
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantWriter string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid secret",
|
||||
args: args{
|
||||
name: "name",
|
||||
secret: "seed",
|
||||
t: now,
|
||||
},
|
||||
wantWriter: "335072\n",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid secret",
|
||||
args: args{
|
||||
name: "name",
|
||||
secret: "seed0",
|
||||
t: now,
|
||||
},
|
||||
wantWriter: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
writer := &bytes.Buffer{}
|
||||
if err := generateCode(writer, tt.args.name, tt.args.secret, tt.args.t); (err != nil) != tt.wantErr {
|
||||
t.Errorf("generateCode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotWriter := writer.String(); gotWriter != tt.wantWriter {
|
||||
t.Errorf("generateCode() = %v, want %v", gotWriter, tt.wantWriter)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSecretNamesForCompletion(t *testing.T) {
|
||||
collectionFile.filename = "testcollection.json"
|
||||
collectionFile.loader = loadCollectionFromDefaultFile
|
||||
_ = createTestData(t)
|
||||
type args struct {
|
||||
toComplete string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "names for completion",
|
||||
args: args{
|
||||
toComplete: "n",
|
||||
},
|
||||
want: []string{"name0", "name1", "name2", "name3", "name4"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := getSecretNamesForCompletion(tt.args.toComplete)
|
||||
for _, want := range tt.want {
|
||||
i := 0
|
||||
match := false
|
||||
for i = range got {
|
||||
if want == got[i] {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
t.Errorf("want: %s, got: %s", tt.want, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
os.Remove(collectionFile.filename)
|
||||
}
|
||||
23
commands/version.go
Normal file
23
commands/version.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var versionText = "unspecified"
|
||||
|
||||
func getVersionCmd() *cobra.Command {
|
||||
var cobraCmd = &cobra.Command{
|
||||
Use: cmdVersion,
|
||||
Short: "Show totp version",
|
||||
Long: "Show totp version",
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
fmt.Printf("totp version %s %s/%s\n", versionText, runtime.GOOS, runtime.GOARCH)
|
||||
},
|
||||
}
|
||||
|
||||
return cobraCmd
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package cmd
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
versionCmd := getVersionCmd()
|
||||
versionCmd.Run(nil, []string{})
|
||||
}
|
||||
11
go.mod
11
go.mod
@@ -3,8 +3,13 @@ module github.com/arcanericky/totp
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/spf13/cobra v1.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
)
|
||||
|
||||
567
go.sum
567
go.sum
@@ -1,575 +1,24 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
|
||||
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
|
||||
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
|
||||
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
187
scripts/totp-test.sh
Executable file
187
scripts/totp-test.sh
Executable file
@@ -0,0 +1,187 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
TOTP=./totp-test
|
||||
COLLECTION=testcollection.json
|
||||
TEST_NBR=1
|
||||
|
||||
# Build
|
||||
echo "Building ${TOTP}"
|
||||
go build -o ${TOTP} -ldflags "-X main.version=$(./scripts/get-version.sh)" ./cmd/...
|
||||
|
||||
# Basic commands
|
||||
echo "${TEST_NBR}: Testing basic commands"
|
||||
${TOTP}
|
||||
${TOTP} --help
|
||||
|
||||
# Test version
|
||||
RESULT=$(${TOTP} version | head --bytes=12)
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} = "totp version" ]]; then
|
||||
echo "FAIL: Version command"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test secret name or secret required
|
||||
RESULT=$(${TOTP} 2>&1 | head --lines=1)
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} = "Secret name or secret is required." ]]; then
|
||||
echo "FAIL: Secret name or secret required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test secret was given so additional arguments are not needed
|
||||
RESULT=$(${TOTP} --secret SEED additional arguments 2>&1 | head --lines=1)
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} = "Secret was given so additional arguments are not needed." ]]; then
|
||||
echo "FAIL: Secret was given so additional arguments are not needed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test too many arguments. Only one secret name is required
|
||||
RESULT=$(${TOTP} too many arguments 2>&1 | head --lines=1)
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} = "Too many arguments. Only one secret name is required." ]]; then
|
||||
echo "FAIL: Too many arguments. Only one secret name is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test completion output
|
||||
RESULT=$(${TOTP} completion bash | head --lines=1)
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} = "# bash completion V2 for totp -*- shell-script -*-" ]]; then
|
||||
echo "FAIL: Completion output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test collection reset
|
||||
((TEST_NBR++))
|
||||
echo "${TEST_NBR}: Testing config reset"
|
||||
touch ${COLLECTION}
|
||||
${TOTP} config reset --file ${COLLECTION} --yes
|
||||
if test -f "${COLLECTION}"; then
|
||||
echo "FAIL: ${TEST_NBR}. Collection file not removed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test generate TOTP with secret
|
||||
((TEST_NBR++))
|
||||
echo "${TEST_NBR}: Testing generate TOTP w/ secret on CLI"
|
||||
TIME="2019-06-23T20:00:00-05:00"
|
||||
SECRET=SEED
|
||||
RESULT=$(${TOTP} --time ${TIME} --secret ${SECRET})
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} =~ ^335072$ ]]; then
|
||||
echo "FAIL: ${TEST_NBR}. Incorrect TOTP generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test add secret
|
||||
((TEST_NBR++))
|
||||
ENTRY=entryname
|
||||
SECRET=SEED
|
||||
|
||||
echo "${TEST_NBR}: Testing config update for adding"
|
||||
${TOTP} config update --file ${COLLECTION} ${ENTRY} ${SECRET}
|
||||
RESULT=$(${TOTP} config list --all --file ${COLLECTION} | tail -1)
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} =~ ^${ENTRY}\ ${SECRET}\ ]]; then
|
||||
echo "FAIL: ${TEST_NBR}. Entry not added"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test generate TOTP
|
||||
((TEST_NBR++))
|
||||
echo "${TEST_NBR}: Testing generate TOTP"
|
||||
TIME="2019-06-23T20:00:00-05:00"
|
||||
RESULT=$(${TOTP} --file ${COLLECTION} --time "${TIME}" ${ENTRY})
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} =~ ^335072$ ]]; then
|
||||
echo "FAIL: ${TEST_NBR}. Incorrect TOTP generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test generate backward TOTP
|
||||
((TEST_NBR++))
|
||||
echo "${TEST_NBR}: Testing backward"
|
||||
TIME="2019-06-23T20:00:00-05:00"
|
||||
RESULT=$(${TOTP} --file ${COLLECTION} --time "${TIME}" --backward 300s ${ENTRY})
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} =~ ^962630$ ]]; then
|
||||
echo "FAIL: Incorrect backward TOTP generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test generate forward TOTP
|
||||
((TEST_NBR++))
|
||||
echo "${TEST_NBR}: Testing forward"
|
||||
TIME="2019-06-23T20:00:00-05:00"
|
||||
RESULT=$(${TOTP} --file ${COLLECTION} --time "${TIME}" --forward 300s ${ENTRY})
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} =~ ^869438$ ]]; then
|
||||
echo "FAIL: ${TEST_NBR}. Incorrect forward TOTP generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Test stdin config
|
||||
((TEST_NBR++))
|
||||
echo "${TEST_NBR}: Testing stdin"
|
||||
ENTRY=entryname
|
||||
TIME="2019-06-23T20:00:00-05:00"
|
||||
RESULT=$(cat ${COLLECTION} | ${TOTP} --stdio --time ${TIME} entryname)
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} =~ ^335072$ ]]; then
|
||||
echo "FAIL: ${TEST_NBR}. Incorrect TOTP generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test update secret
|
||||
((TEST_NBR++))
|
||||
ENTRY=entryname
|
||||
SECRET=SEEDSEED
|
||||
|
||||
echo "${TEST_NBR}: Testing config update for updating"
|
||||
${TOTP} config update --file ${COLLECTION} ${ENTRY} ${SECRET}
|
||||
RESULT=$(${TOTP} config list --all --file ${COLLECTION} | tail -1)
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} =~ ^${ENTRY}\ ${SECRET}\ ]]; then
|
||||
echo "FAIL: ${TEST_NBR}. Entry not added"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test rename secret
|
||||
((TEST_NBR++))
|
||||
ENTRY=entryname
|
||||
NEWENTRY=newentryname
|
||||
SECRET=SEEDSEED
|
||||
|
||||
echo "${TEST_NBR}: Testing config rename"
|
||||
${TOTP} config rename --file ${COLLECTION} ${ENTRY} ${NEWENTRY}
|
||||
RESULT=$(${TOTP} config list --all --file ${COLLECTION} | tail -1)
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} =~ ^${NEWENTRY}\ ${SECRET}\ ]]; then
|
||||
echo "FAIL: ${TEST_NBR}. Entry not added"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test delete secret
|
||||
((TEST_NBR++))
|
||||
ENTRY=newentryname
|
||||
|
||||
echo "${TEST_NBR}: Testing config delete"
|
||||
${TOTP} config delete --file ${COLLECTION} --yes ${ENTRY}
|
||||
RESULT=$(${TOTP} config list --file ${COLLECTION} | wc -l)
|
||||
echo "Result: ${RESULT}"
|
||||
if [[ ! ${RESULT} =~ ^2 ]]; then
|
||||
echo "FAIL: ${TEST_NBR}. Entry not deleted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Removing ${COLLECTION}"
|
||||
${TOTP} config reset --file ${COLLECTION} --yes
|
||||
echo "Removing ${TOTP}"
|
||||
rm ${TOTP}
|
||||
|
||||
echo Success
|
||||
11
totp/totp.go
11
totp/totp.go
@@ -1,11 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/arcanericky/totp/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(cmd.Execute())
|
||||
}
|
||||
Reference in New Issue
Block a user