2022-04-05 07:07:01 +02:00
package lib
2022-01-09 06:59:28 +01:00
import (
2022-09-17 20:21:42 +02:00
"bufio"
2022-03-30 06:56:28 +02:00
"bytes"
2022-01-09 20:00:53 +01:00
"encoding/json"
2022-04-20 06:05:54 +02:00
"errors"
2022-01-09 06:59:28 +01:00
"fmt"
2022-01-10 00:48:20 +01:00
"io"
"io/ioutil"
2022-01-09 20:00:53 +01:00
"log"
2022-06-05 07:06:50 +02:00
"math/rand"
2022-04-07 03:18:46 +02:00
"net/http"
2022-01-09 06:59:28 +01:00
"os"
2022-01-10 00:48:20 +01:00
"os/exec"
2022-01-09 06:59:28 +01:00
"os/user"
"path"
2022-09-17 20:21:42 +02:00
"path/filepath"
2022-09-02 08:22:53 +02:00
"regexp"
2022-04-12 07:36:52 +02:00
"runtime"
2022-01-09 06:59:28 +01:00
"strconv"
"strings"
2022-04-07 03:18:46 +02:00
"syscall"
2022-01-09 06:59:28 +01:00
"time"
2022-04-04 06:27:32 +02:00
2022-04-12 07:36:52 +02:00
_ "embed" // for embedding config.sh
2022-09-21 07:03:15 +02:00
// an alternate non-cgo-requiring sqlite driver
2022-04-07 07:44:10 +02:00
"gorm.io/gorm"
2022-04-06 08:31:24 +02:00
2022-01-09 06:59:28 +01:00
"github.com/fatih/color"
"github.com/google/uuid"
"github.com/rodaine/table"
2022-09-21 07:03:15 +02:00
"github.com/ddworken/hishtory/client/ctx"
2022-04-08 05:59:40 +02:00
"github.com/ddworken/hishtory/client/data"
2022-04-05 07:07:01 +02:00
"github.com/ddworken/hishtory/shared"
2022-01-09 06:59:28 +01:00
)
2022-04-08 06:40:22 +02:00
//go:embed config.sh
var ConfigShContents string
2022-01-10 01:39:13 +01:00
2022-04-15 05:18:49 +02:00
//go:embed test_config.sh
var TestConfigShContents string
2022-04-18 04:54:17 +02:00
//go:embed config.zsh
var ConfigZshContents string
2022-04-22 07:25:24 +02:00
//go:embed test_config.zsh
var TestConfigZshContents string
2022-04-17 05:50:02 +02:00
var Version string = "Unknown"
2022-09-17 20:21:42 +02:00
// 256KB ought to be enough for any reasonable cmd
var maxSupportedLineLengthForImport = 256_000
2022-09-08 08:20:31 +02:00
func getCwd ( ) ( string , string , error ) {
2022-01-09 06:59:28 +01:00
cwd , err := os . Getwd ( )
if err != nil {
2022-09-08 08:20:31 +02:00
return "" , "" , fmt . Errorf ( "failed to get cwd for last command: %v" , err )
2022-01-09 06:59:28 +01:00
}
homedir , err := os . UserHomeDir ( )
if err != nil {
2022-09-08 08:20:31 +02:00
return "" , "" , fmt . Errorf ( "failed to get user's home directory: %v" , err )
2022-01-09 06:59:28 +01:00
}
2022-03-30 06:56:28 +02:00
if cwd == homedir {
2022-09-08 08:20:31 +02:00
return "~/" , homedir , nil
2022-03-30 06:56:28 +02:00
}
2022-01-09 06:59:28 +01:00
if strings . HasPrefix ( cwd , homedir ) {
2022-09-08 08:20:31 +02:00
return strings . Replace ( cwd , homedir , "~" , 1 ) , homedir , nil
2022-01-09 06:59:28 +01:00
}
2022-09-08 08:20:31 +02:00
return cwd , homedir , nil
2022-01-09 06:59:28 +01:00
}
2022-04-08 05:59:40 +02:00
func BuildHistoryEntry ( args [ ] string ) ( * data . HistoryEntry , error ) {
2022-05-28 08:05:28 +02:00
if len ( args ) < 6 {
2022-09-21 07:03:15 +02:00
ctx . GetLogger ( ) . Printf ( "BuildHistoryEntry called with args=%#v, which has too few entries! This can happen in specific edge cases for newly opened terminals and is likely not a problem." , args )
2022-05-28 08:05:28 +02:00
return nil , nil
}
2022-04-18 04:54:17 +02:00
shell := args [ 2 ]
2022-05-28 08:05:28 +02:00
2022-04-08 05:59:40 +02:00
var entry data . HistoryEntry
2022-01-09 06:59:28 +01:00
// exitCode
2022-04-18 04:54:17 +02:00
exitCode , err := strconv . Atoi ( args [ 3 ] )
2022-01-09 06:59:28 +01:00
if err != nil {
return nil , fmt . Errorf ( "failed to build history entry: %v" , err )
}
entry . ExitCode = exitCode
// user
user , err := user . Current ( )
if err != nil {
return nil , fmt . Errorf ( "failed to build history entry: %v" , err )
}
entry . LocalUsername = user . Username
2022-09-08 08:20:31 +02:00
// cwd and homedir
cwd , homedir , err := getCwd ( )
2022-01-09 06:59:28 +01:00
if err != nil {
return nil , fmt . Errorf ( "failed to build history entry: %v" , err )
}
entry . CurrentWorkingDirectory = cwd
2022-09-08 08:20:31 +02:00
entry . HomeDirectory = homedir
2022-01-09 06:59:28 +01:00
2022-01-10 01:39:13 +01:00
// start time
2022-04-19 07:36:57 +02:00
seconds , err := parseCrossPlatformInt ( args [ 5 ] )
2022-01-10 01:39:13 +01:00
if err != nil {
2022-04-18 04:54:17 +02:00
return nil , fmt . Errorf ( "failed to parse start time %s as int: %v" , args [ 5 ] , err )
2022-01-10 01:39:13 +01:00
}
2022-04-19 07:36:57 +02:00
entry . StartTime = time . Unix ( seconds , 0 )
2022-01-09 06:59:28 +01:00
// end time
entry . EndTime = time . Now ( )
// command
2022-04-18 04:54:17 +02:00
if shell == "bash" {
cmd , err := getLastCommand ( args [ 4 ] )
if err != nil {
return nil , fmt . Errorf ( "failed to build history entry: %v" , err )
}
shouldBeSkipped , err := shouldSkipHiddenCommand ( args [ 4 ] )
if err != nil {
return nil , fmt . Errorf ( "failed to check if command was hidden: %v" , err )
}
2022-04-19 06:41:49 +02:00
if shouldBeSkipped || strings . HasPrefix ( cmd , " " ) {
2022-04-18 06:04:44 +02:00
// Don't save commands that start with a space
2022-04-18 04:54:17 +02:00
return nil , nil
}
2022-09-08 08:20:31 +02:00
cmd , err = maybeSkipBashHistTimePrefix ( cmd )
if err != nil {
return nil , err
}
entry . Command = cmd
2022-04-18 06:04:44 +02:00
} else if shell == "zsh" {
cmd := strings . TrimSuffix ( strings . TrimSuffix ( args [ 4 ] , "\n" ) , " " )
2022-04-18 04:54:17 +02:00
if strings . HasPrefix ( cmd , " " ) {
// Don't save commands that start with a space
return nil , nil
}
entry . Command = cmd
} else {
return nil , fmt . Errorf ( "tried to save a hishtory entry from an unsupported shell=%#v" , shell )
2022-04-15 09:04:49 +02:00
}
2022-01-09 06:59:28 +01:00
// hostname
hostname , err := os . Hostname ( )
if err != nil {
return nil , fmt . Errorf ( "failed to build history entry: %v" , err )
}
entry . Hostname = hostname
2022-04-06 08:31:24 +02:00
2022-05-02 04:37:26 +02:00
// device ID
2022-09-21 07:03:15 +02:00
config , err := ctx . GetConfig ( )
2022-05-02 04:37:26 +02:00
if err != nil {
return nil , fmt . Errorf ( "failed to get device ID when building history entry: %v" , err )
}
entry . DeviceId = config . DeviceId
2022-01-09 06:59:28 +01:00
return & entry , nil
}
2022-09-02 08:22:53 +02:00
func buildRegexFromTimeFormat ( timeFormat string ) string {
expectedRegex := ""
lastCharWasPercent := false
for _ , char := range timeFormat {
if lastCharWasPercent {
if char == '%' {
expectedRegex += regexp . QuoteMeta ( string ( char ) )
lastCharWasPercent = false
continue
} else if char == 't' {
expectedRegex += "\t"
} else if char == 'F' {
expectedRegex += buildRegexFromTimeFormat ( "%Y-%m-%d" )
} else if char == 'Y' {
expectedRegex += "[0-9]{4}"
} else if char == 'G' {
expectedRegex += "[0-9]{4}"
} else if char == 'g' {
expectedRegex += "[0-9]{2}"
} else if char == 'C' {
expectedRegex += "[0-9]{2}"
2022-09-08 08:32:25 +02:00
} else if char == 'u' || char == 'w' {
expectedRegex += "[0-9]"
2022-09-02 08:22:53 +02:00
} else if char == 'm' {
expectedRegex += "[0-9]{2}"
} else if char == 'd' {
expectedRegex += "[0-9]{2}"
} else if char == 'D' {
expectedRegex += buildRegexFromTimeFormat ( "%m/%d/%y" )
} else if char == 'T' {
expectedRegex += buildRegexFromTimeFormat ( "%H:%M:%S" )
2022-09-08 08:32:25 +02:00
} else if char == 'H' || char == 'I' || char == 'U' || char == 'V' || char == 'W' || char == 'y' || char == 'Y' {
2022-09-02 08:22:53 +02:00
expectedRegex += "[0-9]{2}"
} else if char == 'M' {
expectedRegex += "[0-9]{2}"
} else if char == 'j' {
expectedRegex += "[0-9]{3}"
2022-09-08 08:32:25 +02:00
} else if char == 'S' || char == 'm' {
2022-09-02 08:22:53 +02:00
expectedRegex += "[0-9]{2}"
} else if char == 'c' {
// Note: Specific to the POSIX locale
expectedRegex += buildRegexFromTimeFormat ( "%a %b %e %H:%M:%S %Y" )
} else if char == 'a' {
// Note: Specific to the POSIX locale
expectedRegex += "(Sun|Mon|Tue|Wed|Thu|Fri|Sat)"
} else if char == 'b' || char == 'h' {
// Note: Specific to the POSIX locale
expectedRegex += "(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"
2022-09-08 08:32:25 +02:00
} else if char == 'e' || char == 'k' || char == 'l' {
2022-09-02 08:22:53 +02:00
expectedRegex += "[0-9 ]{2}"
2022-09-08 08:32:25 +02:00
} else if char == 'n' {
expectedRegex += "\n"
} else if char == 'p' {
expectedRegex += "(AM|PM)"
} else if char == 'P' {
expectedRegex += "(am|pm)"
} else if char == 's' {
expectedRegex += "\\d+"
} else if char == 'z' {
expectedRegex += "[+-][0-9]{4}"
} else if char == 'r' {
expectedRegex += buildRegexFromTimeFormat ( "%I:%M:%S %p" )
} else if char == 'R' {
expectedRegex += buildRegexFromTimeFormat ( "%H:%M" )
} else if char == 'x' {
expectedRegex += buildRegexFromTimeFormat ( "%m/%d/%y" )
} else if char == 'X' {
expectedRegex += buildRegexFromTimeFormat ( "%H:%M:%S" )
2022-09-02 08:22:53 +02:00
} else {
panic ( fmt . Sprintf ( "buildRegexFromTimeFormat doesn't support %%%v, please open a bug against github.com/ddworken/hishtory" , string ( char ) ) )
}
} else if char != '%' {
expectedRegex += regexp . QuoteMeta ( string ( char ) )
}
lastCharWasPercent = false
if char == '%' {
lastCharWasPercent = true
}
}
return expectedRegex
}
2022-09-08 08:20:31 +02:00
func maybeSkipBashHistTimePrefix ( cmdLine string ) ( string , error ) {
2022-06-13 06:28:19 +02:00
format := os . Getenv ( "HISTTIMEFORMAT" )
if format == "" {
2022-09-08 08:20:31 +02:00
return cmdLine , nil
2022-06-13 06:28:19 +02:00
}
2022-09-02 08:22:53 +02:00
re , err := regexp . Compile ( "^" + buildRegexFromTimeFormat ( format ) )
if err != nil {
2022-09-08 08:20:31 +02:00
return "" , fmt . Errorf ( "failed to parse regex for HISTTIMEFORMAT variable: %v" , err )
2022-06-13 06:28:19 +02:00
}
2022-09-08 08:20:31 +02:00
return re . ReplaceAllLiteralString ( cmdLine , "" ) , nil
2022-06-13 06:28:19 +02:00
}
2022-04-17 20:09:30 +02:00
func parseCrossPlatformInt ( data string ) ( int64 , error ) {
data = strings . TrimSuffix ( data , "N" )
return strconv . ParseInt ( data , 10 , 64 )
}
2022-01-09 06:59:28 +01:00
func getLastCommand ( history string ) ( string , error ) {
2022-04-19 07:19:51 +02:00
return strings . SplitN ( strings . SplitN ( strings . TrimSpace ( history ) , " " , 2 ) [ 1 ] , " " , 2 ) [ 1 ] , nil
2022-04-15 09:04:49 +02:00
}
func shouldSkipHiddenCommand ( historyLine string ) ( bool , error ) {
2022-09-21 07:03:15 +02:00
config , err := ctx . GetConfig ( )
2022-04-15 09:04:49 +02:00
if err != nil {
return false , err
}
if config . LastSavedHistoryLine == historyLine {
return true , nil
}
config . LastSavedHistoryLine = historyLine
2022-09-21 07:03:15 +02:00
err = ctx . SetConfig ( config )
2022-04-15 09:04:49 +02:00
if err != nil {
return false , err
}
return false , nil
2022-01-09 06:59:28 +01:00
}
func GetUserSecret ( ) ( string , error ) {
2022-09-21 07:03:15 +02:00
config , err := ctx . GetConfig ( )
2022-01-09 06:59:28 +01:00
if err != nil {
2022-01-09 20:00:53 +01:00
return "" , err
2022-01-09 06:59:28 +01:00
}
2022-01-09 20:00:53 +01:00
return config . UserSecret , nil
2022-01-09 06:59:28 +01:00
}
2022-04-04 06:00:46 +02:00
func Setup ( args [ ] string ) error {
2022-01-09 06:59:28 +01:00
userSecret := uuid . Must ( uuid . NewRandom ( ) ) . String ( )
if len ( args ) > 2 && args [ 2 ] != "" {
userSecret = args [ 2 ]
}
fmt . Println ( "Setting secret hishtory key to " + string ( userSecret ) )
2022-04-07 07:44:10 +02:00
// Create and set the config
2022-09-21 07:03:15 +02:00
var config ctx . ClientConfig
2022-01-09 20:00:53 +01:00
config . UserSecret = userSecret
config . IsEnabled = true
2022-04-04 06:00:46 +02:00
config . DeviceId = uuid . Must ( uuid . NewRandom ( ) ) . String ( )
2022-09-21 07:03:15 +02:00
err := ctx . SetConfig ( config )
2022-04-04 06:27:32 +02:00
if err != nil {
return fmt . Errorf ( "failed to persist config to disk: %v" , err )
}
2022-04-07 07:44:10 +02:00
// Drop all existing data
2022-09-21 07:03:15 +02:00
db , err := ctx . OpenLocalSqliteDb ( )
2022-04-07 07:43:07 +02:00
if err != nil {
return fmt . Errorf ( "failed to open DB: %v" , err )
}
db . Exec ( "DELETE FROM history_entries" )
// Bootstrap from remote date
2022-04-28 19:56:59 +02:00
_ , err = ApiGet ( "/api/v1/register?user_id=" + data . UserId ( userSecret ) + "&device_id=" + config . DeviceId )
2022-04-04 06:27:32 +02:00
if err != nil {
return fmt . Errorf ( "failed to register device with backend: %v" , err )
}
2022-04-06 08:31:24 +02:00
2022-04-28 19:56:59 +02:00
respBody , err := ApiGet ( "/api/v1/bootstrap?user_id=" + data . UserId ( userSecret ) + "&device_id=" + config . DeviceId )
2022-04-06 08:31:24 +02:00
if err != nil {
return fmt . Errorf ( "failed to bootstrap device from the backend: %v" , err )
}
var retrievedEntries [ ] * shared . EncHistoryEntry
2022-04-08 05:59:40 +02:00
err = json . Unmarshal ( respBody , & retrievedEntries )
2022-04-06 08:31:24 +02:00
if err != nil {
return fmt . Errorf ( "failed to load JSON response: %v" , err )
}
for _ , entry := range retrievedEntries {
2022-04-08 05:59:40 +02:00
decEntry , err := data . DecryptHistoryEntry ( userSecret , * entry )
2022-04-06 08:31:24 +02:00
if err != nil {
return fmt . Errorf ( "failed to decrypt history entry from server: %v" , err )
}
AddToDbIfNew ( db , decEntry )
}
2022-04-07 03:18:46 +02:00
return nil
2022-01-09 06:59:28 +01:00
}
2022-04-08 05:59:40 +02:00
func AddToDbIfNew ( db * gorm . DB , entry data . HistoryEntry ) {
2022-04-06 08:31:24 +02:00
tx := db . Where ( "local_username = ?" , entry . LocalUsername )
tx = tx . Where ( "hostname = ?" , entry . Hostname )
tx = tx . Where ( "command = ?" , entry . Command )
tx = tx . Where ( "current_working_directory = ?" , entry . CurrentWorkingDirectory )
2022-09-08 08:20:31 +02:00
tx = tx . Where ( "home_directory = ?" , entry . HomeDirectory )
2022-04-06 08:31:24 +02:00
tx = tx . Where ( "exit_code = ?" , entry . ExitCode )
tx = tx . Where ( "start_time = ?" , entry . StartTime )
tx = tx . Where ( "end_time = ?" , entry . EndTime )
2022-04-08 05:59:40 +02:00
var results [ ] data . HistoryEntry
2022-04-06 08:31:24 +02:00
tx . Limit ( 1 ) . Find ( & results )
if len ( results ) == 0 {
2022-09-05 00:40:30 +02:00
db . Create ( entry )
2022-04-06 08:31:24 +02:00
}
}
2022-04-09 23:52:10 +02:00
func DisplayResults ( results [ ] * data . HistoryEntry ) {
2022-01-09 06:59:28 +01:00
headerFmt := color . New ( color . FgGreen , color . Underline ) . SprintfFunc ( )
2022-04-09 23:52:10 +02:00
tbl := table . New ( "Hostname" , "CWD" , "Timestamp" , "Runtime" , "Exit Code" , "Command" )
2022-01-09 06:59:28 +01:00
tbl . WithHeaderFormatter ( headerFmt )
for _ , result := range results {
2022-01-10 01:39:13 +01:00
timestamp := result . StartTime . Format ( "Jan 2 2006 15:04:05 MST" )
duration := result . EndTime . Sub ( result . StartTime ) . Round ( time . Millisecond ) . String ( )
2022-06-13 05:28:40 +02:00
tbl . AddRow ( result . Hostname , result . CurrentWorkingDirectory , timestamp , duration , result . ExitCode , result . Command )
2022-01-09 06:59:28 +01:00
}
tbl . Print ( )
}
2022-01-09 20:00:53 +01:00
func IsEnabled ( ) ( bool , error ) {
2022-09-21 07:03:15 +02:00
config , err := ctx . GetConfig ( )
2022-01-09 20:00:53 +01:00
if err != nil {
return false , err
}
return config . IsEnabled , nil
}
func Enable ( ) error {
2022-09-21 07:03:15 +02:00
config , err := ctx . GetConfig ( )
2022-01-09 20:00:53 +01:00
if err != nil {
return err
}
config . IsEnabled = true
2022-09-21 07:03:15 +02:00
return ctx . SetConfig ( config )
2022-01-09 20:00:53 +01:00
}
func Disable ( ) error {
2022-09-21 07:03:15 +02:00
config , err := ctx . GetConfig ( )
2022-01-09 20:00:53 +01:00
if err != nil {
return err
}
config . IsEnabled = false
2022-09-21 07:03:15 +02:00
return ctx . SetConfig ( config )
2022-01-09 20:00:53 +01:00
}
func CheckFatalError ( err error ) {
if err != nil {
2022-04-12 07:36:52 +02:00
_ , filename , line , _ := runtime . Caller ( 1 )
log . Fatalf ( "hishtory fatal error at %s:%d: %v" , filename , line , err )
2022-01-09 20:00:53 +01:00
}
}
2022-01-10 00:48:20 +01:00
2022-09-17 20:49:31 +02:00
func ImportHistory ( ) ( int , error ) {
2022-09-21 07:03:15 +02:00
config , err := ctx . GetConfig ( )
2022-09-17 20:21:42 +02:00
if err != nil {
2022-09-17 20:49:31 +02:00
return 0 , err
2022-09-17 20:21:42 +02:00
}
if config . HaveCompletedInitialImport {
// Don't run an import if we already have run one. This avoids importing the same entry multiple times.
2022-09-17 20:49:31 +02:00
return 0 , nil
2022-09-17 20:21:42 +02:00
}
homedir , err := os . UserHomeDir ( )
if err != nil {
2022-09-17 20:49:31 +02:00
return 0 , fmt . Errorf ( "failed to get user's home directory: %v" , err )
2022-09-17 20:21:42 +02:00
}
historyEntries , err := parseBashHistory ( homedir )
if err != nil {
2022-09-17 20:49:31 +02:00
return 0 , fmt . Errorf ( "failed to parse bash history: %v" , err )
2022-09-17 20:21:42 +02:00
}
extraEntries , err := parseZshHistory ( homedir )
if err != nil {
2022-09-17 20:49:31 +02:00
return 0 , fmt . Errorf ( "failed to parse zsh history: %v" , err )
2022-09-17 20:21:42 +02:00
}
historyEntries = append ( historyEntries , extraEntries ... )
2022-09-21 07:03:15 +02:00
db , err := ctx . OpenLocalSqliteDb ( )
2022-09-17 20:21:42 +02:00
if err != nil {
2022-09-17 20:49:31 +02:00
return 0 , nil
2022-09-17 20:21:42 +02:00
}
currentUser , err := user . Current ( )
if err != nil {
2022-09-17 20:49:31 +02:00
return 0 , err
2022-09-17 20:21:42 +02:00
}
hostname , err := os . Hostname ( )
if err != nil {
2022-09-17 20:49:31 +02:00
return 0 , err
2022-09-17 20:21:42 +02:00
}
for _ , cmd := range historyEntries {
startTime := time . Now ( )
endTime := time . Now ( )
err = ReliableDbCreate ( db , data . HistoryEntry {
LocalUsername : currentUser . Name ,
Hostname : hostname ,
Command : cmd ,
CurrentWorkingDirectory : "Unknown" ,
HomeDirectory : homedir ,
ExitCode : 0 , // Unknown, but assumed
StartTime : startTime ,
EndTime : endTime ,
DeviceId : config . DeviceId ,
} )
if err != nil {
2022-09-17 20:49:31 +02:00
return 0 , err
2022-09-17 20:21:42 +02:00
}
}
config . HaveCompletedInitialImport = true
2022-09-21 07:03:15 +02:00
err = ctx . SetConfig ( config )
2022-09-17 20:21:42 +02:00
if err != nil {
2022-09-17 20:49:31 +02:00
return 0 , fmt . Errorf ( "failed to mark initial import as completed, this may lead to duplicate history entries: %v" , err )
2022-09-17 20:21:42 +02:00
}
2022-09-17 20:49:31 +02:00
return len ( historyEntries ) , nil
2022-09-17 20:21:42 +02:00
}
func parseBashHistory ( homedir string ) ( [ ] string , error ) {
return readFileToArray ( filepath . Join ( homedir , ".bash_history" ) )
}
func readFileToArray ( path string ) ( [ ] string , error ) {
2022-09-17 21:22:29 +02:00
if _ , err := os . Stat ( path ) ; errors . Is ( err , os . ErrNotExist ) {
return [ ] string { } , nil
}
2022-09-17 20:21:42 +02:00
file , err := os . Open ( path )
if err != nil {
return nil , err
}
defer file . Close ( )
scanner := bufio . NewScanner ( file )
buf := make ( [ ] byte , maxSupportedLineLengthForImport )
scanner . Buffer ( buf , maxSupportedLineLengthForImport )
lines := make ( [ ] string , 0 )
for scanner . Scan ( ) {
lines = append ( lines , scanner . Text ( ) )
}
if err := scanner . Err ( ) ; err != nil {
return nil , err
}
return lines , nil
}
func parseZshHistory ( homedir string ) ( [ ] string , error ) {
histfile := os . Getenv ( "HISTFILE" )
if histfile == "" {
histfile = filepath . Join ( homedir , ".zsh_history" )
}
return readFileToArray ( histfile )
}
2022-01-10 00:48:20 +01:00
func Install ( ) error {
homedir , err := os . UserHomeDir ( )
if err != nil {
return fmt . Errorf ( "failed to get user's home directory: %v" , err )
}
2022-04-05 07:07:01 +02:00
clientDir := path . Join ( homedir , shared . HISHTORY_PATH )
2022-04-08 06:40:22 +02:00
err = os . MkdirAll ( clientDir , 0 o744 )
2022-04-03 07:27:20 +02:00
if err != nil {
return fmt . Errorf ( "failed to create folder for hishtory binary: %v" , err )
}
2022-01-10 00:48:20 +01:00
path , err := installBinary ( homedir )
if err != nil {
return err
}
2022-01-10 01:39:13 +01:00
err = configureBashrc ( homedir , path )
if err != nil {
return err
}
2022-04-18 04:54:17 +02:00
err = configureZshrc ( homedir , path )
if err != nil {
return err
}
2022-09-21 07:03:15 +02:00
_ , err = ctx . GetConfig ( )
2022-01-10 01:39:13 +01:00
if err != nil {
// No config, so set up a new installation
2022-04-04 06:00:46 +02:00
return Setup ( os . Args )
2022-01-10 01:39:13 +01:00
}
return nil
2022-01-10 00:48:20 +01:00
}
2022-04-18 04:54:17 +02:00
func configureZshrc ( homedir , binaryPath string ) error {
// Create the file we're going to source in our zshrc. Do this no matter what in case there are updates to it.
zshConfigPath := path . Join ( homedir , shared . HISHTORY_PATH , "config.zsh" )
2022-04-22 07:25:24 +02:00
configContents := ConfigZshContents
if os . Getenv ( "HISHTORY_TEST" ) != "" {
configContents = TestConfigZshContents
}
err := ioutil . WriteFile ( zshConfigPath , [ ] byte ( configContents ) , 0 o644 )
2022-04-18 04:54:17 +02:00
if err != nil {
return fmt . Errorf ( "failed to write config.zsh file: %v" , err )
}
// Check if we need to configure the zshrc
2022-04-20 06:05:54 +02:00
zshIsConfigured , err := isZshConfigured ( homedir )
2022-04-18 04:54:17 +02:00
if err != nil {
2022-04-20 06:05:54 +02:00
return fmt . Errorf ( "failed to check ~/.zshrc: %v" , err )
2022-04-18 04:54:17 +02:00
}
2022-04-20 06:05:54 +02:00
if zshIsConfigured {
2022-04-18 04:54:17 +02:00
return nil
}
// Add to zshrc
2022-04-20 06:05:54 +02:00
f , err := os . OpenFile ( path . Join ( homedir , ".zshrc" ) , os . O_APPEND | os . O_WRONLY | os . O_CREATE , 0 o644 )
2022-04-18 04:54:17 +02:00
if err != nil {
return fmt . Errorf ( "failed to append to zshrc: %v" , err )
}
defer f . Close ( )
_ , err = f . WriteString ( "\n# Hishtory Config:\nexport PATH=\"$PATH:" + path . Join ( homedir , shared . HISHTORY_PATH ) + "\"\nsource " + zshConfigPath + "\n" )
if err != nil {
return fmt . Errorf ( "failed to append to zshrc: %v" , err )
}
return nil
}
2022-04-20 06:05:54 +02:00
func isZshConfigured ( homedir string ) ( bool , error ) {
_ , err := os . Stat ( path . Join ( homedir , ".zshrc" ) )
if errors . Is ( err , os . ErrNotExist ) {
return false , nil
}
bashrc , err := ioutil . ReadFile ( path . Join ( homedir , ".zshrc" ) )
if err != nil {
return false , fmt . Errorf ( "failed to read zshrc: %v" , err )
}
return strings . Contains ( string ( bashrc ) , "# Hishtory Config:" ) , nil
}
2022-01-10 00:48:20 +01:00
func configureBashrc ( homedir , binaryPath string ) error {
2022-03-30 06:56:28 +02:00
// Create the file we're going to source in our bashrc. Do this no matter what in case there are updates to it.
2022-04-18 04:54:17 +02:00
bashConfigPath := path . Join ( homedir , shared . HISHTORY_PATH , "config.sh" )
2022-04-15 05:18:49 +02:00
configContents := ConfigShContents
if os . Getenv ( "HISHTORY_TEST" ) != "" {
configContents = TestConfigShContents
}
err := ioutil . WriteFile ( bashConfigPath , [ ] byte ( configContents ) , 0 o644 )
2022-03-30 06:56:28 +02:00
if err != nil {
return fmt . Errorf ( "failed to write config.sh file: %v" , err )
}
2022-01-10 01:39:13 +01:00
// Check if we need to configure the bashrc
2022-04-20 06:05:54 +02:00
bashIsConfigured , err := isBashConfigured ( homedir )
2022-01-10 00:48:20 +01:00
if err != nil {
2022-04-20 06:05:54 +02:00
return fmt . Errorf ( "failed to check ~/.bashrc: %v" , err )
2022-01-10 00:48:20 +01:00
}
2022-04-20 06:05:54 +02:00
if bashIsConfigured {
2022-01-10 00:48:20 +01:00
return nil
}
2022-01-10 01:39:13 +01:00
// Add to bashrc
2022-04-20 06:05:54 +02:00
f , err := os . OpenFile ( path . Join ( homedir , ".bashrc" ) , os . O_APPEND | os . O_WRONLY | os . O_CREATE , 0 o644 )
2022-01-10 00:48:20 +01:00
if err != nil {
return fmt . Errorf ( "failed to append to bashrc: %v" , err )
}
defer f . Close ( )
2022-04-18 04:54:17 +02:00
_ , err = f . WriteString ( "\n# Hishtory Config:\nexport PATH=\"$PATH:" + path . Join ( homedir , shared . HISHTORY_PATH ) + "\"\nsource " + bashConfigPath + "\n" )
2022-01-10 00:48:20 +01:00
if err != nil {
return fmt . Errorf ( "failed to append to bashrc: %v" , err )
}
return nil
}
2022-04-20 06:05:54 +02:00
func isBashConfigured ( homedir string ) ( bool , error ) {
_ , err := os . Stat ( path . Join ( homedir , ".bashrc" ) )
if errors . Is ( err , os . ErrNotExist ) {
return false , nil
}
bashrc , err := ioutil . ReadFile ( path . Join ( homedir , ".bashrc" ) )
if err != nil {
return false , fmt . Errorf ( "failed to read bashrc: %v" , err )
}
return strings . Contains ( string ( bashrc ) , "# Hishtory Config:" ) , nil
}
2022-01-10 00:48:20 +01:00
func installBinary ( homedir string ) ( string , error ) {
clientPath , err := exec . LookPath ( "hishtory" )
if err != nil {
2022-04-05 07:07:01 +02:00
clientPath = path . Join ( homedir , shared . HISHTORY_PATH , "hishtory" )
2022-01-10 00:48:20 +01:00
}
2022-04-14 06:30:27 +02:00
if _ , err := os . Stat ( clientPath ) ; err == nil {
err = syscall . Unlink ( clientPath )
if err != nil {
return "" , fmt . Errorf ( "failed to unlink %s for install: %v" , clientPath , err )
}
}
2022-01-10 00:48:20 +01:00
err = copyFile ( os . Args [ 0 ] , clientPath )
if err != nil {
return "" , fmt . Errorf ( "failed to copy hishtory binary to $PATH: %v" , err )
}
2022-04-08 06:40:22 +02:00
err = os . Chmod ( clientPath , 0 o700 )
2022-01-10 00:48:20 +01:00
if err != nil {
return "" , fmt . Errorf ( "failed to set permissions on hishtory binary: %v" , err )
}
return clientPath , nil
}
func copyFile ( src , dst string ) error {
sourceFileStat , err := os . Stat ( src )
if err != nil {
return err
}
if ! sourceFileStat . Mode ( ) . IsRegular ( ) {
return fmt . Errorf ( "%s is not a regular file" , src )
}
source , err := os . Open ( src )
if err != nil {
return err
}
defer source . Close ( )
destination , err := os . Create ( dst )
if err != nil {
return err
}
defer destination . Close ( )
_ , err = io . Copy ( destination , source )
return err
}
2022-03-30 06:56:28 +02:00
2022-06-05 07:27:04 +02:00
func GetDownloadData ( ) ( shared . UpdateInfo , error ) {
2022-04-17 05:50:02 +02:00
respBody , err := ApiGet ( "/api/v1/download" )
if err != nil {
2022-06-05 07:27:04 +02:00
return shared . UpdateInfo { } , fmt . Errorf ( "failed to download update info: %v" , err )
2022-04-17 05:50:02 +02:00
}
var downloadData shared . UpdateInfo
err = json . Unmarshal ( respBody , & downloadData )
if err != nil {
2022-06-05 07:27:04 +02:00
return shared . UpdateInfo { } , fmt . Errorf ( "failed to parse update info: %v" , err )
}
return downloadData , nil
}
func Update ( ) error {
// Download the binary
downloadData , err := GetDownloadData ( )
if err != nil {
return err
2022-04-17 05:50:02 +02:00
}
2022-04-17 06:39:40 +02:00
if downloadData . Version == "v0." + Version {
2022-04-17 05:50:02 +02:00
fmt . Printf ( "Latest version (v0.%s) is already installed\n" , Version )
2022-04-17 06:54:34 +02:00
return nil
2022-04-17 05:50:02 +02:00
}
2022-04-17 21:30:46 +02:00
err = downloadFiles ( downloadData )
2022-04-17 05:50:02 +02:00
if err != nil {
return err
2022-04-07 03:18:46 +02:00
}
2022-04-17 01:02:07 +02:00
// Verify the SLSA attestation
2022-05-27 08:45:08 +02:00
if runtime . GOOS == "darwin" {
err = verifyBinaryMac ( "/tmp/hishtory-client" , downloadData )
} else {
2022-06-05 06:31:16 +02:00
err = verifyBinary ( "/tmp/hishtory-client" , "/tmp/hishtory-client.intoto.jsonl" , downloadData . Version )
2022-05-27 08:45:08 +02:00
}
2022-04-17 01:02:07 +02:00
if err != nil {
2022-09-17 08:02:52 +02:00
return fmt . Errorf ( "failed to verify SLSA provenance of the updated binary, aborting update (to bypass, set `export HISHTORY_DISABLE_SLSA_ATTESTATION=true`): %v" , err )
2022-04-17 01:02:07 +02:00
}
2022-04-17 05:50:02 +02:00
// Unlink the existing binary so we can overwrite it even though it is still running
2022-04-21 06:57:37 +02:00
if runtime . GOOS == "linux" {
homedir , err := os . UserHomeDir ( )
if err != nil {
return fmt . Errorf ( "failed to get user's home directory: %v" , err )
}
err = syscall . Unlink ( path . Join ( homedir , shared . HISHTORY_PATH , "hishtory" ) )
if err != nil {
return fmt . Errorf ( "failed to unlink %s for update: %v" , path . Join ( homedir , shared . HISHTORY_PATH , "hishtory" ) , err )
}
2022-04-07 03:18:46 +02:00
}
2022-04-17 05:50:02 +02:00
2022-04-17 01:02:07 +02:00
// Install the new one
2022-04-17 05:50:02 +02:00
cmd := exec . Command ( "chmod" , "+x" , "/tmp/hishtory-client" )
2022-04-21 06:57:37 +02:00
var stdout bytes . Buffer
cmd . Stdout = & stdout
var stderr bytes . Buffer
cmd . Stderr = & stderr
2022-04-17 05:50:02 +02:00
err = cmd . Run ( )
if err != nil {
2022-04-21 06:57:37 +02:00
return fmt . Errorf ( "failed to chmod +x the update (out=%#v, err=%#v): %v" , stdout . String ( ) , stderr . String ( ) , err )
2022-04-17 05:50:02 +02:00
}
2022-04-07 03:18:46 +02:00
cmd = exec . Command ( "/tmp/hishtory-client" , "install" )
2022-04-22 07:52:38 +02:00
stdout = bytes . Buffer { }
stderr = bytes . Buffer { }
2022-04-07 03:18:46 +02:00
err = cmd . Run ( )
if err != nil {
2022-04-22 07:52:38 +02:00
return fmt . Errorf ( "failed to install update (out=%#v, err=%#v): %v" , stdout . String ( ) , stderr . String ( ) , err )
2022-03-30 06:56:28 +02:00
}
2022-04-17 05:50:02 +02:00
fmt . Printf ( "Successfully updated hishtory from v0.%s to %s\n" , Version , downloadData . Version )
2022-03-30 06:56:28 +02:00
return nil
}
2022-04-05 07:07:01 +02:00
2022-05-27 08:45:08 +02:00
func verifyBinaryMac ( binaryPath string , downloadData shared . UpdateInfo ) error {
// On Mac, binary verification is a bit more complicated since mac binaries are code
// signed. To verify a signed binary, we:
// 1. Download the unsigned binary
// 2. Strip the real signature from the signed binary and the ad-hoc signature from the unsigned binary
// 3. Assert that those binaries match
// 4. Use SLSA to verify the unsigned binary (pre-strip)
// Yes, this is complicated. But AFAICT, it is the only solution here.
// Step 1: Download the "unsigned" binary that actually has an ad-hoc signature from the
// go compiler.
unsignedBinaryPath := binaryPath + "-unsigned"
var err error = nil
if runtime . GOOS == "darwin" && runtime . GOARCH == "amd64" {
err = downloadFile ( unsignedBinaryPath , downloadData . DarwinAmd64UnsignedUrl )
} else if runtime . GOOS == "darwin" && runtime . GOARCH == "arm64" {
err = downloadFile ( unsignedBinaryPath , downloadData . DarwinArm64UnsignedUrl )
} else {
2022-06-05 07:29:07 +02:00
err = fmt . Errorf ( "verifyBinaryMac() called for the unhandled branch GOOS=%s, GOARCH=%s" , runtime . GOOS , runtime . GOARCH )
2022-05-27 08:45:08 +02:00
}
if err != nil {
return err
}
// Step 2: Create the .nosig files that have no signatures whatsoever
noSigSuffix := ".nosig"
err = stripCodeSignature ( binaryPath , binaryPath + noSigSuffix )
if err != nil {
return err
}
err = stripCodeSignature ( unsignedBinaryPath , unsignedBinaryPath + noSigSuffix )
if err != nil {
return err
}
// Step 3: Compare the binaries
err = assertIdenticalBinaries ( binaryPath + noSigSuffix , unsignedBinaryPath + noSigSuffix )
if err != nil {
return err
}
// Step 4: Use SLSA to verify the unsigned binary
2022-06-05 06:31:16 +02:00
return verifyBinary ( unsignedBinaryPath , "/tmp/hishtory-client.intoto.jsonl" , downloadData . Version )
2022-05-27 08:45:08 +02:00
}
func assertIdenticalBinaries ( bin1Path , bin2Path string ) error {
bin1 , err := os . ReadFile ( bin1Path )
if err != nil {
return err
}
bin2 , err := os . ReadFile ( bin2Path )
if err != nil {
return err
}
if len ( bin1 ) != len ( bin2 ) {
return fmt . Errorf ( "unsigned binaries have different lengths (len(%s)=%d, len(%s)=%d)" , bin1Path , len ( bin1 ) , bin2Path , len ( bin2 ) )
}
differences := make ( [ ] string , 0 )
2022-05-28 07:41:52 +02:00
for i := range bin1 {
2022-05-27 08:45:08 +02:00
b1 := bin1 [ i ]
b2 := bin2 [ i ]
if b1 != b2 {
differences = append ( differences , fmt . Sprintf ( "diff at index %d: %s[%d]=%x, %s[%d]=%x" , i , bin1Path , i , b1 , bin2Path , i , b2 ) )
}
}
2022-09-21 07:03:15 +02:00
logger := ctx . GetLogger ( )
2022-05-27 08:45:08 +02:00
for _ , d := range differences {
logger . Printf ( "comparing binaries: %#v\n" , d )
}
2022-05-28 07:41:52 +02:00
if len ( differences ) > 5 {
2022-05-27 08:45:08 +02:00
return fmt . Errorf ( "found %d differences in the binary" , len ( differences ) )
}
return nil
}
func stripCodeSignature ( inPath , outPath string ) error {
_ , err := exec . LookPath ( "codesign_allocate" )
if err != nil {
return fmt . Errorf ( "your system is missing the codesign_allocate tool, so we can't verify the SLSA attestation (you can bypass this by setting `export HISHTORY_DISABLE_SLSA_ATTESTATION=true` in your shell)" )
}
cmd := exec . Command ( "codesign_allocate" , "-i" , inPath , "-o" , outPath , "-r" )
var stdout bytes . Buffer
cmd . Stdout = & stdout
var stderr bytes . Buffer
cmd . Stderr = & stderr
err = cmd . Run ( )
if err != nil {
2022-09-17 20:21:42 +02:00
return fmt . Errorf ( "failed to use codesign_allocate to strip signatures on binary=%v (stdout=%#v, stderr%#v): %v" , inPath , stdout . String ( ) , stderr . String ( ) , err )
2022-05-27 08:45:08 +02:00
}
return nil
}
2022-04-17 21:30:46 +02:00
func downloadFiles ( updateInfo shared . UpdateInfo ) error {
clientUrl := ""
clientProvenanceUrl := ""
if runtime . GOOS == "linux" && runtime . GOARCH == "amd64" {
clientUrl = updateInfo . LinuxAmd64Url
clientProvenanceUrl = updateInfo . LinuxAmd64AttestationUrl
} else if runtime . GOOS == "darwin" && runtime . GOARCH == "amd64" {
clientUrl = updateInfo . DarwinAmd64Url
clientProvenanceUrl = updateInfo . DarwinAmd64AttestationUrl
} else if runtime . GOOS == "darwin" && runtime . GOARCH == "arm64" {
clientUrl = updateInfo . DarwinArm64Url
clientProvenanceUrl = updateInfo . DarwinArm64AttestationUrl
} else {
return fmt . Errorf ( "no update info found for GOOS=%s, GOARCH=%s" , runtime . GOOS , runtime . GOARCH )
}
err := downloadFile ( "/tmp/hishtory-client" , clientUrl )
if err != nil {
return err
}
err = downloadFile ( "/tmp/hishtory-client.intoto.jsonl" , clientProvenanceUrl )
if err != nil {
return err
}
return nil
}
2022-04-17 05:50:02 +02:00
func downloadFile ( filename , url string ) error {
2022-09-17 20:21:42 +02:00
// Download the data
2022-04-17 05:50:02 +02:00
resp , err := http . Get ( url )
if err != nil {
return fmt . Errorf ( "failed to download file at %s to %s: %v" , url , filename , err )
}
defer resp . Body . Close ( )
2022-04-19 06:28:41 +02:00
if resp . StatusCode != 200 {
return fmt . Errorf ( "failed to download file at %s due to resp_code=%d" , url , resp . StatusCode )
}
2022-04-17 05:50:02 +02:00
2022-09-17 20:21:42 +02:00
// Delete the file if it already exists. This is necessary due to https://openradar.appspot.com/FB8735191
if _ , err := os . Stat ( filename ) ; err == nil {
err = os . Remove ( filename )
if err != nil {
return fmt . Errorf ( "failed to delete file %v when trying to download a new version" , filename )
}
}
// Create the file
2022-04-17 05:50:02 +02:00
out , err := os . Create ( filename )
if err != nil {
return fmt . Errorf ( "failed to save file to %s: %v" , filename , err )
}
defer out . Close ( )
_ , err = io . Copy ( out , resp . Body )
2022-04-19 06:28:41 +02:00
2022-04-17 05:50:02 +02:00
return err
}
2022-04-16 09:44:47 +02:00
func getServerHostname ( ) string {
2022-04-05 07:07:01 +02:00
if server := os . Getenv ( "HISHTORY_SERVER" ) ; server != "" {
return server
}
2022-04-06 08:31:24 +02:00
return "https://api.hishtory.dev"
2022-04-07 03:18:46 +02:00
}
2022-04-07 07:43:07 +02:00
2022-04-16 09:44:47 +02:00
func ApiGet ( path string ) ( [ ] byte , error ) {
2022-09-05 03:37:46 +02:00
if os . Getenv ( "HISHTORY_SIMULATE_NETWORK_ERROR" ) != "" {
return nil , fmt . Errorf ( "simulated network error: dial tcp: lookup api.hishtory.dev" )
}
2022-04-16 09:44:47 +02:00
start := time . Now ( )
resp , err := http . Get ( getServerHostname ( ) + path )
if err != nil {
2022-05-02 04:37:26 +02:00
return nil , fmt . Errorf ( "failed to GET %s%s: %v" , getServerHostname ( ) , path , err )
2022-04-16 09:44:47 +02:00
}
defer resp . Body . Close ( )
if resp . StatusCode != 200 {
2022-05-02 04:37:26 +02:00
return nil , fmt . Errorf ( "failed to GET %s%s: status_code=%d" , getServerHostname ( ) , path , resp . StatusCode )
2022-04-16 09:44:47 +02:00
}
respBody , err := ioutil . ReadAll ( resp . Body )
if err != nil {
2022-05-02 04:37:26 +02:00
return nil , fmt . Errorf ( "failed to read response body from GET %s%s: %v" , getServerHostname ( ) , path , err )
2022-04-16 09:44:47 +02:00
}
duration := time . Since ( start )
2022-09-21 07:03:15 +02:00
ctx . GetLogger ( ) . Printf ( "ApiGet(%#v): %s\n" , path , duration . String ( ) )
2022-04-16 09:44:47 +02:00
return respBody , nil
}
func ApiPost ( path , contentType string , data [ ] byte ) ( [ ] byte , error ) {
2022-09-05 03:37:46 +02:00
if os . Getenv ( "HISHTORY_SIMULATE_NETWORK_ERROR" ) != "" {
return nil , fmt . Errorf ( "simulated network error: dial tcp: lookup api.hishtory.dev" )
}
2022-04-16 09:44:47 +02:00
start := time . Now ( )
resp , err := http . Post ( getServerHostname ( ) + path , contentType , bytes . NewBuffer ( data ) )
if err != nil {
return nil , fmt . Errorf ( "failed to POST %s: %v" , path , err )
}
defer resp . Body . Close ( )
if resp . StatusCode != 200 {
return nil , fmt . Errorf ( "failed to POST %s: status_code=%d" , path , resp . StatusCode )
}
respBody , err := ioutil . ReadAll ( resp . Body )
if err != nil {
return nil , fmt . Errorf ( "failed to read response body from POST %s: %v" , path , err )
}
duration := time . Since ( start )
2022-09-21 07:03:15 +02:00
ctx . GetLogger ( ) . Printf ( "ApiPost(%#v): %s\n" , path , duration . String ( ) )
2022-04-16 09:44:47 +02:00
return respBody , nil
}
2022-04-26 06:42:28 +02:00
2022-05-02 04:37:26 +02:00
func IsOfflineError ( err error ) bool {
2022-09-18 18:42:24 +02:00
if err == nil {
return false
}
2022-09-11 03:07:45 +02:00
return strings . Contains ( err . Error ( ) , "dial tcp: lookup api.hishtory.dev" ) || strings . Contains ( err . Error ( ) , "read: connection reset by peer" ) || strings . Contains ( err . Error ( ) , ": EOF" )
2022-05-02 04:37:26 +02:00
}
2022-06-05 07:06:50 +02:00
func ReliableDbCreate ( db * gorm . DB , entry interface { } ) error {
var err error = nil
i := 0
for i = 0 ; i < 10 ; i ++ {
result := db . Create ( entry )
err = result . Error
if err != nil {
errMsg := err . Error ( )
if errMsg == "database is locked (5) (SQLITE_BUSY)" || errMsg == "database is locked (261)" {
time . Sleep ( time . Duration ( i * rand . Intn ( 100 ) ) * time . Millisecond )
continue
}
if strings . Contains ( errMsg , "constraint failed: UNIQUE constraint failed" ) {
if i == 0 {
return err
} else {
return nil
}
}
return err
}
if err != nil && err . Error ( ) != "database is locked (5) (SQLITE_BUSY)" {
return err
}
}
return fmt . Errorf ( "failed to create DB entry even with %d retries: %v" , i , err )
}
2022-09-05 03:37:46 +02:00
2022-09-21 07:03:15 +02:00
func EncryptAndMarshal ( config ctx . ClientConfig , entry * data . HistoryEntry ) ( [ ] byte , error ) {
2022-09-05 03:37:46 +02:00
encEntry , err := data . EncryptHistoryEntry ( config . UserSecret , * entry )
if err != nil {
return nil , fmt . Errorf ( "failed to encrypt history entry" )
}
encEntry . DeviceId = config . DeviceId
jsonValue , err := json . Marshal ( [ ] shared . EncHistoryEntry { encEntry } )
if err != nil {
return jsonValue , fmt . Errorf ( "failed to marshal encrypted history entry: %v" , err )
}
return jsonValue , nil
}
2022-09-20 07:49:48 +02:00
func Redact ( db * gorm . DB , query string , force bool ) error {
tx , err := data . MakeWhereQueryFromSearch ( db , query )
if err != nil {
return err
}
var historyEntries [ ] * data . HistoryEntry
res := tx . Find ( & historyEntries )
if res . Error != nil {
return res . Error
}
if force {
fmt . Printf ( "Permanently deleting %d entries" , len ( historyEntries ) )
} else {
// TODO: Find a way to test the prompting
fmt . Printf ( "This will permanently delete %d entries, are you sure? [y/N]" , len ( historyEntries ) )
reader := bufio . NewReader ( os . Stdin )
resp , err := reader . ReadString ( '\n' )
if err != nil {
return fmt . Errorf ( "failed to read response: %v" , err )
}
if strings . TrimSpace ( resp ) != "y" {
fmt . Printf ( "Aborting delete per user response of %#v\n" , strings . TrimSpace ( resp ) )
return nil
}
}
tx , err = data . MakeWhereQueryFromSearch ( db , query )
if err != nil {
return err
}
res = tx . Delete ( & data . HistoryEntry { } )
if res . Error != nil {
return res . Error
}
if res . RowsAffected != int64 ( len ( historyEntries ) ) {
return fmt . Errorf ( "DB deleted %d rows, when we only expected to delete %d rows, something may have gone wrong" , res . RowsAffected , len ( historyEntries ) )
}
err = deleteOnRemoteInstances ( historyEntries )
if err != nil {
return err
}
return nil
}
func deleteOnRemoteInstances ( historyEntries [ ] * data . HistoryEntry ) error {
2022-09-21 07:03:15 +02:00
config , err := ctx . GetConfig ( )
2022-09-20 07:49:48 +02:00
if err != nil {
return err
}
var deletionRequest shared . DeletionRequest
deletionRequest . SendTime = time . Now ( )
deletionRequest . UserId = data . UserId ( config . UserSecret )
for _ , entry := range historyEntries {
deletionRequest . Messages . Ids = append ( deletionRequest . Messages . Ids , shared . MessageIdentifier { Date : entry . EndTime , DeviceId : entry . DeviceId } )
}
data , err := json . Marshal ( deletionRequest )
if err != nil {
return err
}
_ , err = ApiPost ( "/api/v1/add-deletion-request" , "application/json" , data )
if err != nil {
return fmt . Errorf ( "failed to send deletion request to backend service, this may cause commands to not get deleted on other instances of hishtory: %v" , err )
}
return nil
}