init versions pre-split

This commit is contained in:
David Dworken 2022-01-08 20:27:18 -08:00
commit 6fbad3a194
6 changed files with 401 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
client
server
!client/
!server/

207
client/client.go Normal file
View File

@ -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
}

11
go.mod Normal file
View File

@ -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
)

36
go.sum Normal file
View File

@ -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=

129
server/server.go Normal file
View File

@ -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))
}

14
shared/data.go Normal file
View File

@ -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"`
}