From 6fbad3a19492bf9d4638a4a55a788e80e0abd532 Mon Sep 17 00:00:00 2001 From: David Dworken Date: Sat, 8 Jan 2022 20:27:18 -0800 Subject: [PATCH] init versions pre-split --- .gitignore | 4 + client/client.go | 207 +++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 11 +++ go.sum | 36 +++++++++ server/server.go | 129 +++++++++++++++++++++++++++++ shared/data.go | 14 ++++ 6 files changed, 401 insertions(+) create mode 100644 .gitignore create mode 100644 client/client.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 server/server.go create mode 100644 shared/data.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04f05e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +client +server +!client/ +!server/ diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..ac75ddb --- /dev/null +++ b/client/client.go @@ -0,0 +1,207 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/user" + "path" + "strconv" + "strings" + "time" + + "github.com/fatih/color" + "github.com/google/uuid" + "github.com/rodaine/table" + + "github.com/ddworken/hishtory/shared" +) + +func main() { + switch os.Args[1] { + case "upload": + upload() + case "query": + query() + case "init": + setup() + } +} + +func setup() { + userSecret := uuid.Must(uuid.NewRandom()).String() + if len(os.Args) > 2 && os.Args[2] != "" { + userSecret = os.Args[2] + } + fmt.Println("Setting secret hishtory key to " + string(userSecret)) + + homedir, err := os.UserHomeDir() + if err != nil { + panic(err) + } + err = os.WriteFile(path.Join(homedir, ".hishtory.secret"), []byte(userSecret), 0400) + if err != nil { + panic(err) + } +} + +func getUserSecret() (string, error) { + homedir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to read secret hishtory key: %v", err) + } + secret, err := os.ReadFile(path.Join(homedir, ".hishtory.secret")) + if err != nil { + return "", fmt.Errorf("failed to read secret hishtory key: %v", err) + } + return string(secret), nil +} + +func getServerHostname() string { + if server := os.Getenv("HISHTORY_SERVER"); server != "" { + return server + } + return "http://localhost:8080" +} + +func query() { + userSecret, err := getUserSecret() + if err != nil { + panic(err) + } + + req, err := http.NewRequest("GET", getServerHostname()+"/api/v1/search", nil) + if err != nil { + panic(err) + } + + q := req.URL.Query() + q.Add("query", strings.Join(os.Args[2:], " ")) + q.Add("user_secret", userSecret) + q.Add("limit", "25") + req.URL.RawQuery = q.Encode() + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + resp_body, err := ioutil.ReadAll(resp.Body) + if err != nil { + panic(err) + } + if resp.Status != "200 OK" { + panic("search API returned invalid result. status=" + resp.Status) + } + + var data []*shared.HistoryEntry + err = json.Unmarshal(resp_body, &data) + if err != nil { + panic(err) + } + display(data) +} + +func display(results []*shared.HistoryEntry) { + headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc() + tbl := table.New("Hostname", "CWD", "Timestamp", "Exit Code", "Command") + tbl.WithHeaderFormatter(headerFmt) + + for _, result := range results { + tbl.AddRow(result.Hostname, result.CurrentWorkingDirectory, result.EndTime.Format("Jan 2 2006 15:04:05 MST"), result.ExitCode, result.Command) + } + + tbl.Print() +} + +func upload() { + var entry shared.HistoryEntry + + // exitCode + exitCode, err := strconv.Atoi(os.Args[2]) + if err != nil { + panic(err) + } + entry.ExitCode = exitCode + + // user + user, err := user.Current() + if err != nil { + panic(err) + } + entry.LocalUsername = user.Username + + // cwd + cwd, err := getCwd() + if err != nil { + panic(err) + } + entry.CurrentWorkingDirectory = cwd + + // TODO(ddworken): start time + + // end time + entry.EndTime = time.Now() + + // command + cmd, err := getLastCommand(os.Args[3]) + if err != nil { + panic(err) + } + entry.Command = cmd + + // hostname + hostname, err := os.Hostname() + if err != nil { + panic(err) + } + entry.Hostname = hostname + + // user secret + userSecret, err := getUserSecret() + if err != nil { + panic(err) + } + entry.UserSecret = userSecret + + err = send(entry) + if err != nil { + panic(err) + } +} + +func getCwd() (string, error) { + cwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get cwd for last command: %v", err) + } + homedir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get user's home directory: %v", err) + } + if strings.HasPrefix(cwd, homedir) { + return strings.Replace(cwd, homedir, "~", 1), nil + } + return cwd, nil +} + +func getLastCommand(history string) (string, error) { + return strings.TrimSpace(strings.SplitN(strings.TrimSpace(history), " ", 2)[1]), nil +} + +func send(entry shared.HistoryEntry) error { + jsonValue, err := json.Marshal(entry) + if err != nil { + return fmt.Errorf("failed to marshal HistoryEntry as json: %v", err) + } + + _, err = http.Post(getServerHostname()+"/api/v1/submit", "application/json", bytes.NewBuffer(jsonValue)) + if err != nil { + return fmt.Errorf("failed to send HistoryEntry to api: %v", err) + } + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7a08116 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/ddworken/hishtory + +go 1.13 + +require ( + github.com/fatih/color v1.13.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/rodaine/table v1.0.1 // indirect + gorm.io/driver/sqlite v1.2.6 // indirect + gorm.io/gorm v1.22.4 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..646d6c5 --- /dev/null +++ b/go.sum @@ -0,0 +1,36 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.3 h1:PlHq1bSCSZL9K0wUhbm2pGLoTWs2GwVhsP6emvGV/ZI= +github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ= +github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4= +gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY= +gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/gorm v1.22.4 h1:8aPcyEJhY0MAt8aY6Dc524Pn+pO29K+ydu+e/cXSpQM= +gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk= diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..361b3da --- /dev/null +++ b/server/server.go @@ -0,0 +1,129 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path" + "strconv" + "strings" + + "github.com/ddworken/hishtory/shared" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func openDB() (*gorm.DB, error) { + homedir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get user's home directory: %v", err) + } + db, err := gorm.Open(sqlite.Open(path.Join(homedir, ".hishtory.db")), &gorm.Config{}) + if err != nil { + panic("failed to connect database") + } + db.AutoMigrate(&shared.HistoryEntry{}) + return db, nil +} + +func persist(entry shared.HistoryEntry) error { + log.Printf("Saving %#v to the DB\n", entry) + db, err := openDB() + if err != nil { + return err + } + conn, err := db.DB() + defer conn.Close() + db.Create(&entry).Commit() + return nil +} + +func search(db *gorm.DB, userSecret, query string, limit int) ([]*shared.HistoryEntry, error) { + fmt.Println("Received search query: " + query) + tokens, err := tokenize(query) + if err != nil { + return nil, fmt.Errorf("failed to tokenize query: %v") + } + tx := db.Debug().Where("user_secret = ?", userSecret) + for _, token := range tokens { + if strings.Contains(token, ":") { + splitToken := strings.SplitN(token, ":", 2) + field := splitToken[0] + val := splitToken[1] + // tx = tx.Where() + panic("TODO(ddworken): Use " + field + val) + } else { + wildcardedToken := "%" + token + "%" + tx = tx.Where("(command LIKE ? OR hostname LIKE ? OR current_working_directory LIKE ?)", wildcardedToken, wildcardedToken, wildcardedToken) + } + } + tx = tx.Order("end_time DESC") + if limit > 0 { + tx = tx.Limit(limit) + } + var historyEntries []*shared.HistoryEntry + result := tx.Find(&historyEntries) + if result.Error != nil { + return nil, fmt.Errorf("DB query error: %v", result.Error) + } + return historyEntries, nil +} + +func tokenize(query string) ([]string, error) { + return strings.Split(query, " "), nil +} + +func apiSubmitHandler(w http.ResponseWriter, r *http.Request) { + decoder := json.NewDecoder(r.Body) + var entry shared.HistoryEntry + err := decoder.Decode(&entry) + if err != nil { + panic(err) + } + err = persist(entry) + if err != nil { + panic(err) + } +} + +func apiSearchHandler(w http.ResponseWriter, r *http.Request) { + userSecret := r.URL.Query().Get("user_secret") + if userSecret == "" { + panic("cannot search without specifying a user secret") + } + query := r.URL.Query().Get("query") + limitStr := r.URL.Query().Get("limit") + limit, err := strconv.Atoi(limitStr) + if err != nil { + limit = 0 + } + db, err := openDB() + if err != nil { + panic(err) + } + entries, err := search(db, userSecret, query, limit) + if err != nil { + panic(err) + } + for _, entry := range entries { + entry.UserSecret = "" + } + resp, err := json.Marshal(entries) + if err != nil { + panic(err) + } + w.Write(resp) +} + +func main() { + _, err := openDB() + if err != nil { + panic(err) + } + fmt.Println("Listening on localhost:8080") + http.HandleFunc("/api/v1/submit", apiSubmitHandler) + http.HandleFunc("/api/v1/search", apiSearchHandler) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/shared/data.go b/shared/data.go new file mode 100644 index 0000000..5a7080e --- /dev/null +++ b/shared/data.go @@ -0,0 +1,14 @@ +package shared + +import "time" + +type HistoryEntry struct { + UserSecret string `json:"user_secret"` + LocalUsername string `json:"local_username"` + Hostname string `json:"hostname"` + Command string `json:"command"` + CurrentWorkingDirectory string `json:"current_working_directory"` + ExitCode int `json:"exit_code"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` +}