2022-04-05 07:07:01 +02:00
package main
import (
2022-10-01 18:50:06 +02:00
"bufio"
2022-09-21 07:28:40 +02:00
"context"
2022-04-05 07:07:01 +02:00
"encoding/json"
"fmt"
2022-10-03 05:14:54 +02:00
"log"
2022-04-05 07:07:01 +02:00
"os"
"strings"
2022-09-05 03:37:46 +02:00
"time"
2022-04-05 07:07:01 +02:00
2022-04-08 05:59:40 +02:00
"github.com/ddworken/hishtory/client/data"
2022-09-21 07:28:40 +02:00
"github.com/ddworken/hishtory/client/hctx"
2022-04-05 07:07:01 +02:00
"github.com/ddworken/hishtory/client/lib"
2022-04-07 03:18:46 +02:00
"github.com/ddworken/hishtory/shared"
2022-04-05 07:07:01 +02:00
)
2022-04-17 05:50:02 +02:00
var GitCommit string = "Unknown"
2022-04-07 07:43:07 +02:00
2022-04-05 07:07:01 +02:00
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" :
2022-09-21 08:30:57 +02:00
ctx := hctx . MakeContext ( )
2022-09-21 07:28:40 +02:00
lib . CheckFatalError ( maybeUploadSkippedHistoryEntries ( ctx ) )
saveHistoryEntry ( ctx )
2022-10-16 18:22:34 +02:00
lib . CheckFatalError ( lib . ProcessDeletionRequests ( ctx ) )
2022-04-05 07:07:01 +02:00
case "query" :
2022-09-21 08:30:57 +02:00
ctx := hctx . MakeContext ( )
2022-10-16 18:22:34 +02:00
lib . CheckFatalError ( lib . ProcessDeletionRequests ( ctx ) )
2022-09-21 07:28:40 +02:00
query ( ctx , strings . Join ( os . Args [ 2 : ] , " " ) )
2022-10-16 18:22:34 +02:00
case "tquery" :
ctx := hctx . MakeContext ( )
lib . CheckFatalError ( lib . ProcessDeletionRequests ( ctx ) )
2022-10-24 00:25:02 +02:00
lib . CheckFatalError ( lib . TuiQuery ( ctx , GitCommit , strings . Join ( os . Args [ 2 : ] , " " ) ) )
2022-04-05 07:07:01 +02:00
case "export" :
2022-09-21 08:30:57 +02:00
ctx := hctx . MakeContext ( )
2022-10-16 18:22:34 +02:00
lib . CheckFatalError ( lib . ProcessDeletionRequests ( ctx ) )
2022-09-21 07:28:40 +02:00
export ( ctx , strings . Join ( os . Args [ 2 : ] , " " ) )
2022-09-18 18:14:34 +02:00
case "redact" :
fallthrough
case "delete" :
2022-09-21 08:30:57 +02:00
ctx := hctx . MakeContext ( )
2022-10-16 18:22:34 +02:00
lib . CheckFatalError ( lib . RetrieveAdditionalEntriesFromRemote ( ctx ) )
lib . CheckFatalError ( lib . ProcessDeletionRequests ( ctx ) )
2022-09-20 07:49:48 +02:00
query := strings . Join ( os . Args [ 2 : ] , " " )
force := false
if os . Args [ 2 ] == "--force" {
query = strings . Join ( os . Args [ 3 : ] , " " )
force = true
}
2022-09-21 07:28:40 +02:00
lib . CheckFatalError ( lib . Redact ( ctx , query , force ) )
2022-04-05 07:07:01 +02:00
case "init" :
2022-10-01 18:50:06 +02:00
db , err := hctx . OpenLocalSqliteDb ( )
lib . CheckFatalError ( err )
data , err := data . Search ( 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? [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
}
}
2022-09-21 08:30:57 +02:00
lib . CheckFatalError ( lib . Setup ( os . Args ) )
2022-04-05 07:07:01 +02:00
case "install" :
2022-09-21 08:30:57 +02:00
lib . CheckFatalError ( lib . Install ( ) )
2022-09-17 20:21:42 +02:00
if os . Getenv ( "HISHTORY_TEST" ) == "" {
2022-10-21 08:42:08 +02:00
db , err := hctx . OpenLocalSqliteDb ( )
2022-09-17 20:49:31 +02:00
lib . CheckFatalError ( err )
2022-10-21 08:42:08 +02:00
data , err := data . Search ( db , "" , 10 )
lib . CheckFatalError ( err )
if len ( data ) < 10 {
fmt . Println ( "Importing existing shell history..." )
ctx := hctx . MakeContext ( )
numImported , err := lib . ImportHistory ( ctx , false )
lib . CheckFatalError ( err )
if numImported > 0 {
fmt . Printf ( "Imported %v history entries from your existing shell history\n" , numImported )
}
2022-09-18 07:45:07 +02:00
}
2022-09-17 20:21:42 +02:00
}
case "import" :
2022-09-21 08:30:57 +02:00
ctx := hctx . MakeContext ( )
2022-10-16 18:51:52 +02:00
numImported , err := lib . ImportHistory ( ctx , true )
2022-09-17 20:49:31 +02:00
lib . CheckFatalError ( err )
2022-09-18 07:45:07 +02:00
if numImported > 0 {
2022-09-22 04:13:53 +02:00
fmt . Printf ( "Imported %v history entries from your existing shell history\n" , numImported )
2022-09-18 07:45:07 +02:00
}
2022-04-05 07:07:01 +02:00
case "enable" :
2022-09-21 08:30:57 +02:00
ctx := hctx . MakeContext ( )
2022-09-21 07:28:40 +02:00
lib . CheckFatalError ( lib . Enable ( ctx ) )
2022-04-05 07:07:01 +02:00
case "disable" :
2022-09-21 08:30:57 +02:00
ctx := hctx . MakeContext ( )
2022-09-21 07:28:40 +02:00
lib . CheckFatalError ( lib . Disable ( ctx ) )
2022-04-12 07:56:23 +02:00
case "version" :
fallthrough
2022-04-05 07:07:01 +02:00
case "status" :
2022-09-21 08:30:57 +02:00
ctx := hctx . MakeContext ( )
2022-09-21 07:28:40 +02:00
config := hctx . GetConf ( ctx )
2022-09-25 00:30:51 +02:00
fmt . Printf ( "hiSHtory: v0.%s\nEnabled: %v\n" , lib . Version , config . IsEnabled )
2022-04-16 19:46:57 +02:00
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 )
2022-05-02 04:37:26 +02:00
printDumpStatus ( config )
2022-04-16 19:46:57 +02:00
}
fmt . Printf ( "Commit Hash: %s\n" , GitCommit )
2022-04-05 07:07:01 +02:00
case "update" :
2022-10-03 05:14:54 +02:00
err := lib . Update ( hctx . MakeContext ( ) )
if err != nil {
log . Fatalf ( "Failed to update hishtory: %v" , err )
}
2022-10-16 18:29:14 +02:00
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 )
2022-10-26 09:41:18 +02:00
case "displayed-columns" :
// TODO: better formatting for the below
fmt . Printf ( "%#v" , config . DisplayedColumns )
case "custom-columns" :
// TODO: better formatting for this
fmt . Printf ( "%#v" , config . CustomColumns )
2022-10-16 18:29:14 +02:00
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" :
2022-10-26 09:41:18 +02:00
val := os . Args [ 3 ]
2022-10-16 18:29:14 +02:00
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 ) )
2022-10-26 09:41:18 +02:00
case "displayed-columns" :
vals := os . Args [ 3 : ]
config . DisplayedColumns = vals
lib . CheckFatalError ( hctx . SetConfig ( config ) )
2022-10-26 09:56:46 +02:00
case "custom-columns" :
log . Fatalf ( "Please use config-add and config-delete to interact with custom-columns" )
2022-10-16 18:29:14 +02:00
default :
log . Fatalf ( "Unrecognized config key: %s" , key )
}
2022-10-26 09:56:46 +02:00
case "config-add" :
2022-10-26 09:30:00 +02:00
ctx := hctx . MakeContext ( )
config := hctx . GetConf ( ctx )
2022-10-26 09:56:46 +02:00
key := os . Args [ 2 ]
switch key {
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 )
2022-10-26 09:30:00 +02:00
}
2022-10-26 09:56:46 +02:00
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 )
}
2022-10-09 21:13:05 +02:00
case "reupload" :
// Purposefully undocumented since this command is generally not necessary to run
2022-10-10 02:10:11 +02:00
lib . CheckFatalError ( lib . Reupload ( hctx . MakeContext ( ) ) )
2022-06-06 03:26:02 +02:00
case "-h" :
fallthrough
case "help" :
2022-09-25 00:30:51 +02:00
fmt . Print ( ` hiSHtory : Better shell history
2022-06-06 03:26:02 +02:00
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 ' .
2022-09-21 06:42:09 +02:00
' 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 ' .
2022-06-06 03:26:02 +02:00
' 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 help ' : View this help page
2022-10-26 09:41:18 +02:00
` ) // TODO; Update ^ to document the config-get and config-set options
2022-04-05 07:07:01 +02:00
default :
lib . CheckFatalError ( fmt . Errorf ( "unknown command: %s" , os . Args [ 1 ] ) )
}
}
2022-09-21 07:28:40 +02:00
func printDumpStatus ( config hctx . ClientConfig ) {
2022-05-23 04:45:46 +02:00
dumpRequests , err := getDumpRequests ( config )
lib . CheckFatalError ( err )
fmt . Printf ( "Dump Requests: " )
for _ , d := range dumpRequests {
fmt . Printf ( "%#v, " , * d )
}
fmt . Print ( "\n" )
}
2022-09-21 07:28:40 +02:00
func getDumpRequests ( config hctx . ClientConfig ) ( [ ] * shared . DumpRequest , error ) {
2022-05-23 04:45:46 +02:00
resp , err := lib . ApiGet ( "/api/v1/get-dump-requests?user_id=" + data . UserId ( config . UserSecret ) + "&device_id=" + config . DeviceId )
2022-09-18 18:42:24 +02:00
if lib . IsOfflineError ( err ) {
return [ ] * shared . DumpRequest { } , nil
}
2022-05-02 04:37:26 +02:00
if err != nil {
2022-05-23 04:45:46 +02:00
return nil , err
2022-05-02 04:37:26 +02:00
}
var dumpRequests [ ] * shared . DumpRequest
2022-05-23 04:45:46 +02:00
err = json . Unmarshal ( resp , & dumpRequests )
return dumpRequests , err
2022-05-02 04:37:26 +02:00
}
2022-09-21 07:28:40 +02:00
func query ( ctx * context . Context , query string ) {
db := hctx . GetDb ( ctx )
2022-10-16 18:22:34 +02:00
err := lib . RetrieveAdditionalEntriesFromRemote ( ctx )
2022-05-02 04:37:26 +02:00
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 )
}
}
2022-09-21 07:28:40 +02:00
lib . CheckFatalError ( displayBannerIfSet ( ctx ) )
2022-04-08 05:59:40 +02:00
data , err := data . Search ( db , query , 25 )
2022-04-05 07:07:01 +02:00
lib . CheckFatalError ( err )
2022-10-24 05:54:46 +02:00
lib . CheckFatalError ( lib . DisplayResults ( ctx , data ) )
2022-04-05 07:07:01 +02:00
}
2022-09-21 07:28:40 +02:00
func displayBannerIfSet ( ctx * context . Context ) error {
2022-10-16 18:22:34 +02:00
respBody , err := lib . GetBanner ( ctx , GitCommit )
2022-09-18 18:42:24 +02:00
if lib . IsOfflineError ( err ) {
return nil
}
2022-04-07 07:43:07 +02:00
if err != nil {
2022-04-16 09:44:47 +02:00
return err
2022-04-07 07:43:07 +02:00
}
2022-04-16 09:44:47 +02:00
if len ( respBody ) > 0 {
fmt . Println ( string ( respBody ) )
2022-04-07 07:43:07 +02:00
}
return nil
}
2022-09-21 07:28:40 +02:00
func maybeUploadSkippedHistoryEntries ( ctx * context . Context ) error {
config := hctx . GetConf ( ctx )
2022-09-05 03:37:46 +02:00
if ! config . HaveMissedUploads {
return nil
}
// Upload the missing entries
2022-09-21 07:28:40 +02:00
db := hctx . GetDb ( ctx )
2022-09-05 03:37:46 +02:00
query := fmt . Sprintf ( "after:%s" , time . Unix ( config . MissedUploadTimestamp , 0 ) . Format ( "2006-01-02" ) )
entries , err := data . Search ( db , query , 0 )
if err != nil {
return fmt . Errorf ( "failed to retrieve history entries that haven't been uploaded yet: %v" , err )
}
2022-09-21 07:28:40 +02:00
hctx . GetLogger ( ) . Printf ( "Uploading %d history entries that previously failed to upload (query=%#v)\n" , len ( entries ) , query )
2022-10-10 02:13:40 +02:00
jsonValue , err := lib . EncryptAndMarshal ( config , entries )
if err != nil {
return err
}
_ , err = lib . ApiPost ( "/api/v1/submit?source_device_id=" + config . DeviceId , "application/json" , jsonValue )
if err != nil {
// Failed to upload the history entry, so we must still be offline. So just return nil and we'll try again later.
return nil
2022-09-05 03:37:46 +02:00
}
// Mark down that we persisted it
config . HaveMissedUploads = false
config . MissedUploadTimestamp = 0
2022-09-21 07:28:40 +02:00
err = hctx . SetConfig ( config )
2022-09-05 03:37:46 +02:00
if err != nil {
return fmt . Errorf ( "failed to mark a history entry as uploaded: %v" , err )
}
return nil
}
2022-09-21 07:28:40 +02:00
func saveHistoryEntry ( ctx * context . Context ) {
config := hctx . GetConf ( ctx )
2022-04-05 07:07:01 +02:00
if ! config . IsEnabled {
2022-09-21 07:28:40 +02:00
hctx . GetLogger ( ) . Printf ( "Skipping saving a history entry because hishtory is disabled\n" )
2022-04-05 07:07:01 +02:00
return
}
2022-09-21 07:28:40 +02:00
entry , err := lib . BuildHistoryEntry ( ctx , os . Args )
2022-04-05 07:07:01 +02:00
lib . CheckFatalError ( err )
2022-04-15 09:04:49 +02:00
if entry == nil {
2022-10-24 00:40:30 +02:00
hctx . GetLogger ( ) . Printf ( "Skipping saving a history entry because we did not build a history entry (was the command prefixed with a space and/or empty?)\n" )
2022-04-15 09:04:49 +02:00
return
}
2022-04-05 07:07:01 +02:00
// Persist it locally
2022-09-21 07:28:40 +02:00
db := hctx . GetDb ( ctx )
2022-09-23 03:09:51 +02:00
err = lib . ReliableDbCreate ( db , * entry )
2022-06-05 07:06:50 +02:00
lib . CheckFatalError ( err )
2022-04-05 07:07:01 +02:00
// Persist it remotely
2022-10-10 02:10:11 +02:00
jsonValue , err := lib . EncryptAndMarshal ( config , [ ] * data . HistoryEntry { entry } )
2022-04-05 07:07:01 +02:00
lib . CheckFatalError ( err )
2022-10-09 21:13:05 +02:00
_ , err = lib . ApiPost ( "/api/v1/submit?source_device_id=" + config . DeviceId , "application/json" , jsonValue )
2022-04-28 18:51:01 +02:00
if err != nil {
2022-05-02 04:37:26 +02:00
if lib . IsOfflineError ( err ) {
2022-09-21 07:28:40 +02:00
hctx . GetLogger ( ) . Printf ( "Failed to remotely persist hishtory entry because the device is offline!" )
2022-09-05 03:37:46 +02:00
if ! config . HaveMissedUploads {
config . HaveMissedUploads = true
config . MissedUploadTimestamp = time . Now ( ) . Unix ( )
2022-09-21 07:28:40 +02:00
lib . CheckFatalError ( hctx . SetConfig ( config ) )
2022-09-05 03:37:46 +02:00
}
2022-04-28 18:51:01 +02:00
} else {
lib . CheckFatalError ( err )
}
}
2022-04-28 20:46:14 +02:00
// Check if there is a pending dump request and reply to it if so
2022-05-23 04:45:46 +02:00
dumpRequests , err := getDumpRequests ( config )
2022-05-02 04:37:26 +02:00
if err != nil {
if lib . IsOfflineError ( err ) {
// It is fine to just ignore this, the next command will retry the API and eventually we will respond to any pending dump requests
2022-05-23 04:45:46 +02:00
dumpRequests = [ ] * shared . DumpRequest { }
2022-09-21 07:28:40 +02:00
hctx . GetLogger ( ) . Printf ( "Failed to check for dump requests because the device is offline!" )
2022-05-02 04:37:26 +02:00
} else {
lib . CheckFatalError ( err )
}
}
2022-04-28 20:46:14 +02:00
if len ( dumpRequests ) > 0 {
2022-10-16 18:22:34 +02:00
lib . CheckFatalError ( lib . RetrieveAdditionalEntriesFromRemote ( ctx ) )
2022-04-28 20:46:14 +02:00
entries , err := data . Search ( db , "" , 0 )
lib . CheckFatalError ( err )
var encEntries [ ] * shared . EncHistoryEntry
for _ , entry := range entries {
enc , err := data . EncryptHistoryEntry ( config . UserSecret , * entry )
lib . CheckFatalError ( err )
encEntries = append ( encEntries , & enc )
}
reqBody , err := json . Marshal ( encEntries )
lib . CheckFatalError ( err )
for _ , dumpRequest := range dumpRequests {
2022-05-02 04:37:26 +02:00
_ , err := lib . ApiPost ( "/api/v1/submit-dump?user_id=" + dumpRequest . UserId + "&requesting_device_id=" + dumpRequest . RequestingDeviceId + "&source_device_id=" + config . DeviceId , "application/json" , reqBody )
2022-04-28 20:46:14 +02:00
lib . CheckFatalError ( err )
}
}
2022-04-05 07:07:01 +02:00
}
2022-09-21 07:28:40 +02:00
func export ( ctx * context . Context , query string ) {
db := hctx . GetDb ( ctx )
2022-10-16 18:22:34 +02:00
err := lib . RetrieveAdditionalEntriesFromRemote ( ctx )
2022-05-02 04:37:26 +02:00
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 )
}
}
2022-06-06 03:05:06 +02:00
data , err := data . Search ( db , query , 0 )
2022-04-05 07:07:01 +02:00
lib . CheckFatalError ( err )
2022-04-07 08:17:58 +02:00
for i := len ( data ) - 1 ; i >= 0 ; i -- {
2022-04-07 08:05:30 +02:00
fmt . Println ( data [ i ] . Command )
2022-04-05 07:07:01 +02:00
}
}
2022-09-24 10:01:32 +02:00
// TODO(feature): Add a session_id column that corresponds to the shell session the command was run in