hishtory/hishtory.go

389 lines
13 KiB
Go
Raw Normal View History

package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"strings"
"time"
"github.com/ddworken/hishtory/client/data"
"github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/client/lib"
)
var GitCommit string = "Unknown"
func main() {
if len(os.Args) == 1 {
fmt.Println("Must specify a command! Do you mean `hishtory query`?")
return
}
switch os.Args[1] {
case "saveHistoryEntry":
ctx := hctx.MakeContext()
saveHistoryEntry(ctx)
case "query":
ctx := hctx.MakeContext()
lib.CheckFatalError(lib.ProcessDeletionRequests(ctx))
query(ctx, strings.Join(os.Args[2:], " "))
case "tquery":
ctx := hctx.MakeContext()
lib.CheckFatalError(lib.TuiQuery(ctx, GitCommit, strings.Join(os.Args[2:], " ")))
case "export":
ctx := hctx.MakeContext()
lib.CheckFatalError(lib.ProcessDeletionRequests(ctx))
export(ctx, strings.Join(os.Args[2:], " "))
case "redact":
fallthrough
case "delete":
ctx := hctx.MakeContext()
lib.CheckFatalError(lib.RetrieveAdditionalEntriesFromRemote(ctx))
lib.CheckFatalError(lib.ProcessDeletionRequests(ctx))
query := strings.Join(os.Args[2:], " ")
force := false
if os.Args[2] == "--force" {
query = strings.Join(os.Args[3:], " ")
force = true
}
lib.CheckFatalError(lib.Redact(ctx, query, force))
case "init":
db, err := hctx.OpenLocalSqliteDb()
lib.CheckFatalError(err)
data, err := lib.Search(nil, db, "", 10)
lib.CheckFatalError(err)
if len(data) > 0 {
fmt.Printf("Your current hishtory profile has saved history entries, are you sure you want to run `init` and reset?\nNote: This won't clear any imported history entries from your existing shell\n[y/N]")
reader := bufio.NewReader(os.Stdin)
resp, err := reader.ReadString('\n')
lib.CheckFatalError(err)
if strings.TrimSpace(resp) != "y" {
fmt.Printf("Aborting init per user response of %#v\n", strings.TrimSpace(resp))
return
}
}
lib.CheckFatalError(lib.Setup(os.Args))
if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" {
fmt.Println("Importing existing shell history...")
ctx := hctx.MakeContext()
numImported, err := lib.ImportHistory(ctx, false, false)
lib.CheckFatalError(err)
if numImported > 0 {
fmt.Printf("Imported %v history entries from your existing shell history\n", numImported)
}
}
case "install":
lib.CheckFatalError(lib.Install())
if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" {
db, err := hctx.OpenLocalSqliteDb()
lib.CheckFatalError(err)
data, err := lib.Search(nil, db, "", 10)
lib.CheckFatalError(err)
if len(data) < 10 {
fmt.Println("Importing existing shell history...")
ctx := hctx.MakeContext()
numImported, err := lib.ImportHistory(ctx, false, false)
lib.CheckFatalError(err)
if numImported > 0 {
fmt.Printf("Imported %v history entries from your existing shell history\n", numImported)
}
}
}
case "uninstall":
fmt.Printf("Are you sure you want to uninstall hiSHtory and delete all locally saved history data [y/N]")
reader := bufio.NewReader(os.Stdin)
resp, err := reader.ReadString('\n')
lib.CheckFatalError(err)
if strings.TrimSpace(resp) != "y" {
fmt.Printf("Aborting uninstall per user response of %#v\n", strings.TrimSpace(resp))
return
}
lib.CheckFatalError(lib.Uninstall(hctx.MakeContext()))
case "import":
ctx := hctx.MakeContext()
numImported, err := lib.ImportHistory(ctx, true, true)
lib.CheckFatalError(err)
if numImported > 0 {
fmt.Printf("Imported %v history entries from your existing shell history\n", numImported)
}
case "enable":
ctx := hctx.MakeContext()
lib.CheckFatalError(lib.Enable(ctx))
case "disable":
ctx := hctx.MakeContext()
lib.CheckFatalError(lib.Disable(ctx))
case "version":
fallthrough
case "status":
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
fmt.Printf("hiSHtory: v0.%s\nEnabled: %v\n", lib.Version, config.IsEnabled)
fmt.Printf("Secret Key: %s\n", config.UserSecret)
if len(os.Args) == 3 && os.Args[2] == "-v" {
fmt.Printf("User ID: %s\n", data.UserId(config.UserSecret))
fmt.Printf("Device ID: %s\n", config.DeviceId)
printDumpStatus(config)
}
fmt.Printf("Commit Hash: %s\n", GitCommit)
case "update":
err := lib.Update(hctx.MakeContext())
if err != nil {
log.Fatalf("Failed to update hishtory: %v", err)
}
case "config-get":
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
key := os.Args[2]
switch key {
case "enable-control-r":
fmt.Printf("%v", config.ControlRSearchEnabled)
case "filter-duplicate-commands":
fmt.Printf("%v", config.FilterDuplicateCommands)
case "displayed-columns":
for _, col := range config.DisplayedColumns {
if strings.Contains(col, " ") {
fmt.Printf("%q ", col)
} else {
fmt.Print(col + " ")
}
}
fmt.Print("\n")
case "custom-columns":
for _, cc := range config.CustomColumns {
fmt.Println(cc.ColumnName + ": " + cc.ColumnCommand)
}
default:
log.Fatalf("Unrecognized config key: %s", key)
}
case "config-set":
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
key := os.Args[2]
switch key {
case "enable-control-r":
val := os.Args[3]
if val != "true" && val != "false" {
log.Fatalf("Unexpected config value %s, must be one of: true, false", val)
}
config.ControlRSearchEnabled = (val == "true")
lib.CheckFatalError(hctx.SetConfig(config))
case "filter-duplicate-commands":
val := os.Args[3]
if val != "true" && val != "false" {
log.Fatalf("Unexpected config value %s, must be one of: true, false", val)
}
config.FilterDuplicateCommands = (val == "true")
lib.CheckFatalError(hctx.SetConfig(config))
case "displayed-columns":
vals := os.Args[3:]
config.DisplayedColumns = vals
lib.CheckFatalError(hctx.SetConfig(config))
case "timestamp-format":
val := os.Args[3]
config.TimestampFormat = val
lib.CheckFatalError(hctx.SetConfig(config))
case "custom-columns":
log.Fatalf("Please use config-add and config-delete to interact with custom-columns")
default:
log.Fatalf("Unrecognized config key: %s", key)
}
case "config-add":
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
key := os.Args[2]
switch key {
case "custom-column":
fallthrough
case "custom-columns":
columnName := os.Args[3]
command := os.Args[4]
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
if config.CustomColumns == nil {
config.CustomColumns = make([]hctx.CustomColumnDefinition, 0)
}
config.CustomColumns = append(config.CustomColumns, hctx.CustomColumnDefinition{ColumnName: columnName, ColumnCommand: command})
lib.CheckFatalError(hctx.SetConfig(config))
case "displayed-columns":
vals := os.Args[3:]
config.DisplayedColumns = append(config.DisplayedColumns, vals...)
lib.CheckFatalError(hctx.SetConfig(config))
default:
log.Fatalf("Unrecognized config key: %s", key)
}
case "config-delete":
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
key := os.Args[2]
switch key {
case "custom-columns":
columnName := os.Args[2]
ctx := hctx.MakeContext()
config := hctx.GetConf(ctx)
if config.CustomColumns == nil {
return
}
newColumns := make([]hctx.CustomColumnDefinition, 0)
deletedColumns := false
for _, c := range config.CustomColumns {
if c.ColumnName != columnName {
newColumns = append(newColumns, c)
deletedColumns = true
}
}
if !deletedColumns {
log.Fatalf("Did not find a column with name %#v to delete (current columns = %#v)", columnName, config.CustomColumns)
}
config.CustomColumns = newColumns
lib.CheckFatalError(hctx.SetConfig(config))
case "displayed-columns":
deletedColumns := os.Args[3:]
newColumns := make([]string, 0)
for _, c := range config.DisplayedColumns {
isDeleted := false
for _, d := range deletedColumns {
if c == d {
isDeleted = true
}
}
if !isDeleted {
newColumns = append(newColumns, c)
}
}
config.DisplayedColumns = newColumns
lib.CheckFatalError(hctx.SetConfig(config))
default:
log.Fatalf("Unrecognized config key: %s", key)
}
case "reupload":
// Purposefully undocumented since this command is generally not necessary to run
lib.CheckFatalError(lib.Reupload(hctx.MakeContext()))
case "-h":
fallthrough
case "help":
fmt.Print(`hiSHtory: Better shell history
Supported commands:
'hishtory query': Query for matching commands and display them in a table. Examples:
'hishtory query apt-get' # Find shell commands containing 'apt-get'
'hishtory query apt-get install' # Find shell commands containing 'apt-get' and 'install'
'hishtory query curl cwd:/tmp/' # Find shell commands containing 'curl' run in '/tmp/'
'hishtory query curl user:david' # Find shell commands containing 'curl' run by 'david'
'hishtory query curl host:x1' # Find shell commands containing 'curl' run on 'x1'
'hishtory query exit_code:1' # Find shell commands that exited with status code 1
'hishtory query before:2022-02-01' # Find shell commands run before 2022-02-01
'hishtory export': Query for matching commands and display them in list without any other
metadata. Supports the same query format as 'hishtory query'.
'hishtory redact': Query for matching commands and remove them from your shell history (on the
current machine and on all remote machines). Supports the same query format as 'hishtory query'.
'hishtory update': Securely update hishtory to the latest version.
'hishtory disable': Stop recording shell commands
'hishtory enable': Start recording shell commands
'hishtory status': View status info including the secret key which is needed to sync shell
history from another machine.
'hishtory init': Set the secret key to enable syncing shell commands from another
machine with a matching secret key.
'hishtory config-get', 'hishtory config-set', 'hishtory config-add', 'hishtory config-delete': Edit the config. See the README for details on each of the config options.
'hishtory uninstall': Permanently uninstall hishtory
'hishtory help': View this help page
`)
default:
lib.CheckFatalError(fmt.Errorf("unknown command: %s", os.Args[1]))
}
}
func printDumpStatus(config hctx.ClientConfig) {
fmt.Print("\n")
}
func query(ctx *context.Context, query string) {
db := hctx.GetDb(ctx)
err := lib.RetrieveAdditionalEntriesFromRemote(ctx)
if err != nil {
if lib.IsOfflineError(err) {
fmt.Println("Warning: hishtory is offline so this may be missing recent results from your other machines!")
} else {
lib.CheckFatalError(err)
}
}
lib.CheckFatalError(displayBannerIfSet(ctx))
numResults := 25
data, err := lib.Search(ctx, db, query, numResults*5)
lib.CheckFatalError(err)
lib.CheckFatalError(lib.DisplayResults(ctx, data, numResults))
}
func displayBannerIfSet(ctx *context.Context) error {
respBody, err := lib.GetBanner(ctx, GitCommit)
if lib.IsOfflineError(err) {
return nil
}
if err != nil {
return err
}
if len(respBody) > 0 {
fmt.Println(string(respBody))
}
return nil
}
func saveHistoryEntry(ctx *context.Context) {
config := hctx.GetConf(ctx)
if !config.IsEnabled {
hctx.GetLogger().Infof("Skipping saving a history entry because hishtory is disabled\n")
return
}
entry, err := lib.BuildHistoryEntry(ctx, os.Args)
lib.CheckFatalError(err)
if entry == nil {
hctx.GetLogger().Infof("Skipping saving a history entry because we did not build a history entry (was the command prefixed with a space and/or empty?)\n")
return
}
// Persist it locally
db := hctx.GetDb(ctx)
err = lib.ReliableDbCreate(db, *entry)
lib.CheckFatalError(err)
// Persist it remotely
if !config.IsOffline {
jsonValue, err := lib.EncryptAndMarshal(config, []*data.HistoryEntry{entry})
lib.CheckFatalError(err)
_, err = lib.ApiPost("/api/v1/submit?source_device_id="+config.DeviceId, "application/json", jsonValue)
if err != nil {
if lib.IsOfflineError(err) {
hctx.GetLogger().Infof("Failed to remotely persist hishtory entry because we failed to connect to the remote server! This is likely because the device is offline, but also could be because the remote server is having reliability issues. Original error: %v", err)
if !config.HaveMissedUploads {
config.HaveMissedUploads = true
config.MissedUploadTimestamp = time.Now().Unix()
lib.CheckFatalError(hctx.SetConfig(config))
}
} else {
lib.CheckFatalError(err)
}
}
}
}
func export(ctx *context.Context, query string) {
db := hctx.GetDb(ctx)
err := lib.RetrieveAdditionalEntriesFromRemote(ctx)
if err != nil {
if lib.IsOfflineError(err) {
fmt.Println("Warning: hishtory is offline so this may be missing recent results from your other machines!")
} else {
lib.CheckFatalError(err)
}
}
data, err := lib.Search(ctx, db, query, 0)
lib.CheckFatalError(err)
for i := len(data) - 1; i >= 0; i-- {
fmt.Println(data[i].Command)
}
}
// TODO(feature): Add a session_id column that corresponds to the shell session the command was run in