From 77bb3166c41761247973f6155de1b85675eea5dd Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 16 Jun 2025 19:17:52 -0400 Subject: [PATCH 01/17] starting sqlite rewrite - adding db connector - starting rewrite of commands - WIP --- README.md | 9 +- client/mcr.go | 29 ++--- cmd/clear.go | 27 ++--- cmd/config.go | 70 +++--------- cmd/delete.go | 17 +-- cmd/login.go | 44 ++++++-- cmd/run.go | 15 ++- cmd/save.go | 19 ++-- cmd/view.go | 39 ++++--- database/commands.go | 250 +++++++++++++++++++++++++++++++++++++++++++ go.mod | 8 +- go.sum | 26 +++-- model/data.go | 16 +++ models/data.go | 4 - 14 files changed, 421 insertions(+), 152 deletions(-) create mode 100644 database/commands.go create mode 100644 model/data.go delete mode 100644 models/data.go diff --git a/README.md b/README.md index fbd3f07..b93815c 100644 --- a/README.md +++ b/README.md @@ -119,4 +119,11 @@ mctl utilizes [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
# Development -this repo is currently in development and may encounter breaking changes, use a tag to prevent any surprises \ No newline at end of file +this repo is currently in development and may encounter breaking changes, use a tag to prevent any surprises + +# TODO +1. continue sqlite update +2. migrate the credentials to sqlite as well +3. login too +4. MCR client also has viper stuff, gut it +5. Since we are now using sqlite should we just encode the pwd and insert \ No newline at end of file diff --git a/client/mcr.go b/client/mcr.go index 292c687..e58db39 100644 --- a/client/mcr.go +++ b/client/mcr.go @@ -1,11 +1,11 @@ package client import ( + "encoding/base64" "fmt" - "code.jakeyoungdev.com/jake/mctl/cryptography" + "code.jakeyoungdev.com/jake/mctl/database" "github.com/jake-young-dev/mcr" - "github.com/spf13/viper" ) /* @@ -23,22 +23,25 @@ type IClient interface { } // creates a new mcr client using saved credentials and decrypted password -func New() (*Client, error) { - //grab saved credentials - server := viper.Get("server").(string) - password := viper.Get("password").(string) - port := viper.Get("port").(int) - fmt.Printf("Logging into %s on port %d\n", server, port) - - //decrypt password - pt, err := cryptography.DecryptPassword([]byte(password)) +func New(name string) (*Client, error) { + db, err := database.New() if err != nil { return nil, err } + defer db.Close() + + srv, err := db.GetServer(name) + if err != nil { + return nil, err + } + fmt.Printf("Logging into %s on port %d\n", srv.Server, srv.Port) + + var p []byte + _, err = base64.StdEncoding.Decode(p, []byte(srv.Password)) //connect to game server - cli := mcr.NewClient(server, mcr.WithPort(port)) - err = cli.Connect(string(pt)) + cli := mcr.NewClient(srv.Server, mcr.WithPort(srv.Port)) + err = cli.Connect(string(p)) if err != nil { return nil, err } diff --git a/cmd/clear.go b/cmd/clear.go index 5dd05de..692dc2e 100644 --- a/cmd/clear.go +++ b/cmd/clear.go @@ -5,36 +5,25 @@ package cmd import ( "fmt" - "os" - "code.jakeyoungdev.com/jake/mctl/models" + "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // clearCmd represents the clear command var clearCmd = &cobra.Command{ Use: "clear", Short: "Clear config file", - Long: `Clears all configuration values for mctl, all server configuration will be lost`, + Long: `Clears all configuration values for mctl. [WARNING] all server configuration will be lost`, Run: func(cmd *cobra.Command, args []string) { - home, err := os.UserHomeDir() + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + err = db.Destroy() cobra.CheckErr(err) - viper.AddConfigPath(home) - viper.SetConfigType("yaml") - viper.SetConfigName(".mctl") - - err = viper.ReadInConfig() - if err == nil { - //clear values if file exists - for _, v := range models.ConfigFields { - viper.Set(v, "") - } - err := viper.WriteConfig() - cobra.CheckErr(err) - fmt.Println("Config file cleared, use 'config' command to re-populate it") - } + fmt.Println("Configuration is cleared, the 'config' command must be run again.") }, } diff --git a/cmd/config.go b/cmd/config.go index 25f500c..d299d13 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -4,26 +4,26 @@ Copyright © 2025 Jake jake.young.dev@gmail.com package cmd import ( - "crypto/rand" + "encoding/base64" "fmt" - "io" "os" - "code.jakeyoungdev.com/jake/mctl/cryptography" + "code.jakeyoungdev.com/jake/mctl/database" + "code.jakeyoungdev.com/jake/mctl/model" "github.com/spf13/cobra" - "github.com/spf13/viper" "golang.org/x/term" ) var ( cfgserver string cfgport int + cfgname string ) // configCmd represents the config command var configCmd = &cobra.Command{ Use: "config", - Example: "mctl config -s x.x.x.x -p 61695", + Example: "mctl config -n serverAlias -s x.x.x.x -p 61695", Short: "Create and populate config file", Long: `Creates the .mctl file in the user home directory populating it with the server address, rcon password, and @@ -34,68 +34,32 @@ var configCmd = &cobra.Command{ ps, err := term.ReadPassword(int(os.Stdin.Fd())) cobra.CheckErr(err) - //generate and apply random nonce - nonce := make([]byte, 12) - _, err = io.ReadFull(rand.Reader, nonce) + db, err := database.New() cobra.CheckErr(err) - viper.Set("nonce", string(nonce)) + defer db.Close() - //encrypt password - ciphert, err := cryptography.EncryptPassword(ps) + err = db.Init() cobra.CheckErr(err) - //update config file with new values - viper.Set("server", cfgserver) - viper.Set("password", string(ciphert)) - viper.Set("port", cfgport) - err = viper.WriteConfig() + err = db.SaveServer(model.Server{ + Name: cfgname, + Server: cfgserver, + Port: cfgport, + Password: base64.StdEncoding.EncodeToString(ps), + }) cobra.CheckErr(err) - fmt.Println() - fmt.Println("Config file updated!") }, } func init() { - initConfig() configCmd.Flags().StringVarP(&cfgserver, "server", "s", "", "server address") err := configCmd.MarkFlagRequired("server") cobra.CheckErr(err) configCmd.Flags().IntVarP(&cfgport, "port", "p", 0, "server rcon port") err = configCmd.MarkFlagRequired("port") cobra.CheckErr(err) + configCmd.Flags().StringVarP(&cfgname, "name", "n", "", "server alias") + err = configCmd.MarkFlagRequired("name") + cobra.CheckErr(err) rootCmd.AddCommand(configCmd) } - -// init config sets viper config and checks for config file, creating it if it doesn't exist -func initConfig() { - home, err := os.UserHomeDir() - cobra.CheckErr(err) - - viper.AddConfigPath(home) - viper.SetConfigType("yaml") - viper.SetConfigName(".mctl") - viper.AutomaticEnv() - err = viper.ReadInConfig() - - if err != nil { - //file does not exist, create it - viper.Set("server", cfgserver) - viper.Set("password", "") - viper.Set("port", cfgport) - viper.Set("nonce", "") - - //generate psuedo-random key - uu := make([]byte, 32) - _, err := rand.Read(uu) - cobra.CheckErr(err) - - //create custom command map - cmdMap := make(map[string]any, 0) - - //write config - viper.Set("customcmd", cmdMap) - viper.Set("device", string(uu)) - err = viper.SafeWriteConfig() - cobra.CheckErr(err) - } -} diff --git a/cmd/delete.go b/cmd/delete.go index dcee881..0944d79 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -5,9 +5,10 @@ package cmd import ( "errors" + "fmt" + "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // deleteCmd represents the delete command @@ -17,13 +18,13 @@ var deleteCmd = &cobra.Command{ Short: "Delete a saved command", Long: `Deletes a command stored using the save command`, Run: func(cmd *cobra.Command, args []string) { - if viper.IsSet("customcmd") { - cmdMap := viper.Get("customcmd").(map[string]any) - delete(cmdMap, args[0]) - viper.Set("customcmd", cmdMap) - err := viper.WriteConfig() - cobra.CheckErr(err) - } + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + err = db.DeleteCmd(args[0]) + cobra.CheckErr(err) + fmt.Println("Command deleted") }, PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { diff --git a/cmd/login.go b/cmd/login.go index 84e22f9..33e78ea 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -10,28 +10,36 @@ import ( "os" "code.jakeyoungdev.com/jake/mctl/client" + "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" - "github.com/spf13/viper" +) + +var ( + name string ) // loginCmd represents the login command var loginCmd = &cobra.Command{ Use: "login", - Example: "mctl login", + Example: "mctl login ", SilenceUsage: true, Short: "Login to server and send commands", Long: `Login to server using saved config and enter command loop sending commands to server and printing the response.`, Run: func(cmd *cobra.Command, args []string) { - //grab saved credentials - server := viper.Get("server").(string) - cli, err := client.New() + server := args[0] + cli, err := client.New(server) cobra.CheckErr(err) defer cli.Close() //start command loop fmt.Println("Connected! Type 'mctl' to close") scanner := bufio.NewScanner(os.Stdin) + + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + var runningCmd string for runningCmd != "mctl" { fmt.Printf("RCON@%s /> ", server) @@ -48,6 +56,11 @@ var loginCmd = &cobra.Command{ break } + if runningCmd == ".run" { + } + //hmm this gets weird af tbh + dbcmd, err := db.GetCmd(runningCmd) + res, err := cli.Command(runningCmd) cobra.CheckErr(err) fmt.Printf("\n%s\n", res) @@ -57,9 +70,24 @@ var loginCmd = &cobra.Command{ fmt.Printf("Disconnected from %s\n", server) }, PreRunE: func(cmd *cobra.Command, args []string) error { - //ensure config command has been run - if !viper.IsSet("server") || !viper.IsSet("password") || !viper.IsSet("port") { - return errors.New("the 'config' command must be run before you can interact with servers") + if len(args) == 0 { + return errors.New("must specify which server to login to") + } + srv := args[0] + + db, err := database.New() + if err != nil { + return err + } + defer db.Close() + + server, err := db.GetServer(srv) + if err != nil { + return err + } + + if server.Name == "" { + return fmt.Errorf("server %s not found", server.Name) } return nil diff --git a/cmd/run.go b/cmd/run.go index bf749c6..4c33e2d 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -4,11 +4,13 @@ Copyright © 2025 Jake jake.young.dev@gmail.com package cmd import ( + "database/sql" "errors" "fmt" "strings" "code.jakeyoungdev.com/jake/mctl/client" + "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -22,13 +24,16 @@ var runCmd = &cobra.Command{ Long: `Loads a saved command, injects the supplied arguments into the command, and sends the command to the remove server printing the response`, Run: func(cmd *cobra.Command, args []string) { - cm := viper.Get("customcmd").(map[string]any) - //is this an existing command - cmdRun, ok := cm[args[0]] - if !ok { + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + sc, err := db.GetCmd(args[0]) + if err == sql.ErrNoRows { fmt.Printf("command %s not found", args[0]) return } + cobra.CheckErr(err) //convert arguments to interface var nargs []any @@ -37,7 +42,7 @@ var runCmd = &cobra.Command{ } //inject arguments - fixed := fmt.Sprintf(cmdRun.(string), nargs...) + fixed := fmt.Sprintf(sc, nargs...) fmt.Printf("Running saved command %s\n", fixed) //create client and send command diff --git a/cmd/save.go b/cmd/save.go index 2f5e6fb..0cbc4d3 100644 --- a/cmd/save.go +++ b/cmd/save.go @@ -9,8 +9,8 @@ import ( "fmt" "os" + "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // saveCmd represents the save command @@ -21,22 +21,17 @@ var saveCmd = &cobra.Command{ Long: `Saves supplied command using alias to allow the command to be executed using the run command. The %s placeholder can be used as a wildcard to be injected when running the command`, Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Use %s as a wildcard in your command\n", "%s") //this is ugly, have to use printf to stop issues with compiler + fmt.Printf("Use %s as a wildcard in your command\n", "%s") //this is ugly fmt.Printf("Command: ") sc := bufio.NewScanner(os.Stdin) if sc.Scan() { txt := sc.Text() if txt != "" { - var cmdMap map[string]any - cm := viper.Get("customcmd") - if cmdMap == nil { - cmdMap = make(map[string]any, 0) - } else { - cmdMap = cm.(map[string]any) - } - cmdMap[args[0]] = txt - viper.Set("customcmd", cmdMap) - err := viper.WriteConfig() + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + err = db.SaveCmd(args[0], txt) cobra.CheckErr(err) fmt.Println("\nSaved!") } diff --git a/cmd/view.go b/cmd/view.go index 1c95609..929bed5 100644 --- a/cmd/view.go +++ b/cmd/view.go @@ -8,8 +8,8 @@ import ( "fmt" "strings" + "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // viewCmd represents the view command @@ -19,32 +19,29 @@ var viewCmd = &cobra.Command{ Short: "View saved commands", Long: `Load command using the supplied name and displays it in the terminal, 'all' will list every saved command`, Run: func(cmd *cobra.Command, args []string) { - var cm map[string]any - cmdMap := viper.Get("customcmd") - if cmdMap == nil { - fmt.Println("no custom commands found") - return - } - - cm = cmdMap.(map[string]any) - + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() if strings.EqualFold(args[0], "all") { - //show all commands + cmds, err := db.GetAllCmds() + cobra.CheckErr(err) + fmt.Println("\nCommands: ") - for k, v := range cm { - fmt.Printf("%s - %s\n", k, v) + for _, c := range cmds { + fmt.Printf("%s - %s\n", c.Name, c.Command) } fmt.Println() - return - } + } else { + cmds, err := db.GetCmd(args[0]) + cobra.CheckErr(err) - custom, ok := cm[args[0]] - if !ok { - fmt.Println("command not found") - return - } + if cmds == "" { + fmt.Println("Command not found") + return + } - fmt.Printf("Command: %s\n", custom.(string)) + fmt.Printf("Command: %s\n", cmds) + } }, PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { diff --git a/database/commands.go b/database/commands.go new file mode 100644 index 0000000..80cc041 --- /dev/null +++ b/database/commands.go @@ -0,0 +1,250 @@ +package database + +import ( + "fmt" + "os" + + "code.jakeyoungdev.com/jake/mctl/model" + "github.com/jmoiron/sqlx" + _ "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" +) + +type database struct { + *sqlx.DB +} + +type Database interface { + Init() error + Destroy() error + Close() error + //command methods + GetCmd(name string) (string, error) + GetAllCmds() ([]model.Command, error) + SaveCmd(name, cmd string) error + UpdateCmd(name, cmd string) error + DeleteCmd(name string) error + //server methods + GetServer(name string) (model.Server, error) + GetAllServers() ([]model.Server, error) + SaveServer(srv model.Server) error + UpdateServer(name, password string) error + DeleteServer(name string) error +} + +func New() (Database, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + db, err := sqlx.Open("sqlite3", fmt.Sprintf("file:%s/.mctl", home)) + if err != nil { + return nil, err + } + + return &database{ + db, + }, nil +} + +func (d *database) Init() error { + query := ` + CREATE TABLE IF NOT EXISTS commands( + name TEXT PRIMARY KEY, + command TEXT + ) + + CREATE TABLE IF NOT EXISTS servers( + name TEXT PRIMARY KEY, + server TEXT, + password TEXT, + port NUMBER + ) + ` + + _, err := d.Exec(query) + return err +} + +func (d *database) Destroy() error { + query := ` + DROP TABLE commands + DROP TABLE servers + ` + + _, err := d.Exec(query) + return err +} + +func (d *database) Close() error { + return d.Close() +} + +func (d *database) GetCmd(name string) (string, error) { + query := ` + SELECT + command + FROM + commands + WHERE + name = ? + ` + + var cmd string + err := d.QueryRowx(query, name).Scan(&cmd) + if err != nil { + return "", err + } + + return name, nil +} + +func (d *database) GetAllCmds() ([]model.Command, error) { + query := ` + SELECT + name, + command + FROM + commands + ` + + rows, err := d.Queryx(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var res []model.Command + for rows.Next() { + var r model.Command + err := rows.StructScan(&r) + if err != nil { + return nil, err + } + + res = append(res, r) + } + + return res, nil +} + +func (d *database) SaveCmd(name, cmd string) error { + query := ` + INSERT INTO commands(name, command) + VALUES(?, ?) + ` + + _, err := d.Exec(query, name, cmd) + return err +} + +func (d *database) UpdateCmd(name, cmd string) error { + query := ` + UPDATE + commands + SET + cmd = ? + WHERE + name = ? + ` + + _, err := d.Exec(query, cmd, name) + return err +} + +func (d *database) DeleteCmd(name string) error { + query := ` + DELETE FROM commands + WHERE name = ? + ` + + _, err := d.Exec(query, name) + return err +} + +func (d *database) GetServer(name string) (model.Server, error) { + query := ` + SELECT + name, + server, + password, + port + FROM + servers + WHERE + name = ? + ` + + var s model.Server + err := d.QueryRowx(query, name).StructScan(&s) + if err != nil { + return model.Server{}, err + } + return s, nil +} + +func (d *database) GetAllServers() ([]model.Server, error) { + query := ` + SELECT + name, + server, + password, + port + FROM + servers + ` + + rows, err := d.Queryx(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var res []model.Server + for rows.Next() { + var r model.Server + err := rows.StructScan(&r) + if err != nil { + return nil, err + } + + res = append(res, r) + } + + return res, nil +} + +func (d *database) SaveServer(srv model.Server) error { + query := ` + INSERT INTO servers(name, server, password, port) + VALUES(?, ?, ?, ?) + ` + + _, err := d.Exec(query, srv.Name, srv.Server, srv.Password, srv.Port) + return err +} + +// updates server password, if anymore fields need updated the entry should be deleted and recreated +func (d *database) UpdateServer(name, password string) error { + query := ` + UPDATE servers + SET + password = ? + WHERE + name = ? + ` + + _, err := d.Exec(query, password, name) + return err +} + +func (d *database) DeleteServer(name string) error { + query := ` + DELETE FROM servers + WHERE + name = ? + ` + + _, err := d.Exec(query, name) + return err +} diff --git a/go.mod b/go.mod index fed923e..68fac98 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.24.2 require ( github.com/jake-young-dev/mcr v1.4.0 + github.com/jmoiron/sqlx v1.4.0 + github.com/ncruces/go-sqlite3 v0.26.1 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 golang.org/x/term v0.31.0 @@ -13,6 +15,7 @@ require ( github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/ncruces/julianday v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -20,9 +23,10 @@ require ( github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4c6f616..6fee408 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -6,20 +8,30 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jake-young-dev/mcr v1.3.1 h1:ELJsrJHwQsMiM09o+q8auUaiGXXX3DWIgh/TfZQc0B0= -github.com/jake-young-dev/mcr v1.3.1/go.mod h1:74yZHGf9h3tLUDUpInA17grKLrNp9lVesWvisCFCXKY= github.com/jake-young-dev/mcr v1.4.0 h1:cXZImkfI8aNIiVPrONE6qP+nfblTGsD2iXpPKTcA25U= github.com/jake-young-dev/mcr v1.4.0/go.mod h1:74yZHGf9h3tLUDUpInA17grKLrNp9lVesWvisCFCXKY= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/ncruces/go-sqlite3 v0.26.1 h1:lBXmbmucH1Bsj57NUQR6T84UoMN7jnNImhF+ibEITJU= +github.com/ncruces/go-sqlite3 v0.26.1/go.mod h1:XFTPtFIo1DmGCh+XVP8KGn9b/o2f+z0WZuT09x2N6eo= +github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= +github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -47,16 +59,18 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/model/data.go b/model/data.go new file mode 100644 index 0000000..38841a6 --- /dev/null +++ b/model/data.go @@ -0,0 +1,16 @@ +package model + +//list of all fields kept in config file +// var ConfigFields = [6]string{"customcmd", "device", "nonce", "port", "server", "password"} + +type Command struct { + Name string `db:"name"` + Command string `db:"command"` +} + +type Server struct { + Name string `db:"name"` + Server string `db:"server"` + Password string `db:"password"` + Port int `db:"port"` +} diff --git a/models/data.go b/models/data.go deleted file mode 100644 index e282c91..0000000 --- a/models/data.go +++ /dev/null @@ -1,4 +0,0 @@ -package models - -//list of all fields kept in config file -var ConfigFields = [6]string{"customcmd", "device", "nonce", "port", "server", "password"} From fe37cac2da8b52d2eaf597ff567d929c74aadfb6 Mon Sep 17 00:00:00 2001 From: jake Date: Tue, 17 Jun 2025 23:12:49 -0400 Subject: [PATCH 02/17] lots o progress - added 'command' subcommand - removed viper - setup commands added - still a WIP - readme TODO update --- README.md | 53 +++++++++---------- client/mcr.go | 3 +- cmd/clear.go | 32 ------------ cmd/command/add.go | 50 ++++++++++++++++++ cmd/command/command.go | 22 ++++++++ cmd/{ => command}/delete.go | 19 ++++--- cmd/command/view.go | 35 +++++++++++++ cmd/config.go | 65 ----------------------- cmd/destroy.go | 32 ++++++++++++ cmd/init.go | 30 +++++++++++ cmd/login.go | 55 ++++++------------- cmd/root.go | 4 ++ cmd/run.go | 87 ------------------------------- cmd/save.go | 51 ------------------ cmd/server/add.go | 70 +++++++++++++++++++++++++ cmd/server/delete.go | 36 +++++++++++++ cmd/server/server.go | 21 ++++++++ cmd/server/update.go | 48 +++++++++++++++++ cmd/server/view.go | 37 +++++++++++++ cmd/view.go | 57 -------------------- cryptography/aes.go | 52 ------------------ database/{commands.go => sqlx.go} | 25 ++++++--- go.mod | 13 ----- go.sum | 44 ---------------- model/data.go | 3 -- 25 files changed, 456 insertions(+), 488 deletions(-) delete mode 100644 cmd/clear.go create mode 100644 cmd/command/add.go create mode 100644 cmd/command/command.go rename cmd/{ => command}/delete.go (59%) create mode 100644 cmd/command/view.go delete mode 100644 cmd/config.go create mode 100644 cmd/destroy.go create mode 100644 cmd/init.go delete mode 100644 cmd/run.go delete mode 100644 cmd/save.go create mode 100644 cmd/server/add.go create mode 100644 cmd/server/delete.go create mode 100644 cmd/server/server.go create mode 100644 cmd/server/update.go create mode 100644 cmd/server/view.go delete mode 100644 cmd/view.go delete mode 100644 cryptography/aes.go rename database/{commands.go => sqlx.go} (88%) diff --git a/README.md b/README.md index b93815c..c24324b 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ mctl is a terminal-friendly remote console client # Index 1. [Installation](#installation) -2. [Setup](#setup) -3. [Documentation](#documentation) +2. [Use](#use) 4. [Security](#security) 5. [Development](#development) @@ -13,18 +12,31 @@ mctl is a terminal-friendly remote console client # Installation Install mctl using golang ```bash -go install code.jakeyoungdev.com/jake/mctl@main #it is recommended to use a tagged version +go install code.jakeyoungdev.com/jake/mctl@main ```
-# Setup +# Use ### Configuring mctl -mctl requires a one-time setup via the 'config' command before interacting with any servers, password is entered securely from the terminal and encrypted +mctl requires a one-time setup via the 'init' command before using any other commands ```bash -mctl config -s -p +mctl init ``` +### Add a server +A + + + + + + + + + + + ### Connecting to server Once the client has been configured commands can be sent to the server using the 'login' command. This will authenticate you with the game server and enter the command loop, the session is logged out when the command loop is broken @@ -94,23 +106,6 @@ mctl clear
-# Documentation -### Commands -|Command|Description| -|---|---| -|config|used to update the config file| -|login|makes connection request to the server using saved configuration and enters command loop| -|save \|saves specific command for reuse| -|view \|displays saved command| -|delete \|deletes saved command| -|run \ args...|runs saved command filling placeholders with supplied args| -|clear|clears config file| - -### Configuration file -All configuration data will be kept in the home directory and any sensitive data is encrypted for added security - -
- # Security RCon is an inherently insecure protocol, passwords are sent in plaintext and, if possible, the port should not be exposed to the internet. It is best to keep these connections local or over a VPN. @@ -122,8 +117,10 @@ mctl utilizes [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck this repo is currently in development and may encounter breaking changes, use a tag to prevent any surprises # TODO -1. continue sqlite update -2. migrate the credentials to sqlite as well -3. login too -4. MCR client also has viper stuff, gut it -5. Since we are now using sqlite should we just encode the pwd and insert \ No newline at end of file +6. Do we use CheckErr or use RunE +7. update readme, the commands are all broken +9. do saved commands run on default server only? Yes, but add -s + mctl command run tp nutlift -s server +10. RUN COMMAND +11. Command descriptions, examples, and silence usage +12. lol "default" server functionality (which means setting which one w default command) \ No newline at end of file diff --git a/client/mcr.go b/client/mcr.go index e58db39..ea9452b 100644 --- a/client/mcr.go +++ b/client/mcr.go @@ -9,8 +9,7 @@ import ( ) /* - This is a simple wrapper for the MCR client to provide easy use of mcr without having to manually - decrypt the password/hit viper each time. + This is a simple wrapper for the MCR client to provide easy use of rcon */ type Client struct { diff --git a/cmd/clear.go b/cmd/clear.go deleted file mode 100644 index 692dc2e..0000000 --- a/cmd/clear.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright © 2025 Jake jake.young.dev@gmail.com -*/ -package cmd - -import ( - "fmt" - - "code.jakeyoungdev.com/jake/mctl/database" - "github.com/spf13/cobra" -) - -// clearCmd represents the clear command -var clearCmd = &cobra.Command{ - Use: "clear", - Short: "Clear config file", - Long: `Clears all configuration values for mctl. [WARNING] all server configuration will be lost`, - Run: func(cmd *cobra.Command, args []string) { - db, err := database.New() - cobra.CheckErr(err) - defer db.Close() - - err = db.Destroy() - cobra.CheckErr(err) - - fmt.Println("Configuration is cleared, the 'config' command must be run again.") - }, -} - -func init() { - rootCmd.AddCommand(clearCmd) -} diff --git a/cmd/command/add.go b/cmd/command/add.go new file mode 100644 index 0000000..f54080e --- /dev/null +++ b/cmd/command/add.go @@ -0,0 +1,50 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package command + +import ( + "bufio" + "fmt" + "os" + + "code.jakeyoungdev.com/jake/mctl/database" + "github.com/spf13/cobra" +) + +var addCmd = &cobra.Command{ + Use: "add", + Example: "mctl command add", + // Short: "Saves a new server configuration", + // Long: `Saves server address, alias, port, and password.`, + Run: func(cmd *cobra.Command, args []string) { + scanner := bufio.NewScanner(os.Stdin) + + //get server information + var cfgname string + fmt.Printf("Command alias: ") + if scanner.Scan() { + cfgname = scanner.Text() + } + + var cfgcmd string + fmt.Printf("Command: ") + if scanner.Scan() { + cfgcmd = scanner.Text() + } + + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + err = db.Init() + cobra.CheckErr(err) + + err = db.SaveCmd(cfgname, cfgcmd) + cobra.CheckErr(err) + }, +} + +func init() { + CommandCmd.AddCommand(addCmd) +} diff --git a/cmd/command/command.go b/cmd/command/command.go new file mode 100644 index 0000000..a295960 --- /dev/null +++ b/cmd/command/command.go @@ -0,0 +1,22 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package command + +import ( + "github.com/spf13/cobra" +) + +// CommandCmd is such a cool name lol +var CommandCmd = &cobra.Command{ + Use: "command", + Example: "mctl command ", + // Short: "A remote console client", + // Long: `mctl is a terminal-friendly remote console client made to manage game servers.`, + // Version: "v0.3.4", + // Run: func(cmd *cobra.Command, args []string) { }, +} + +func init() { + // rootCmd.AddCommand() +} diff --git a/cmd/delete.go b/cmd/command/delete.go similarity index 59% rename from cmd/delete.go rename to cmd/command/delete.go index 0944d79..d6640e8 100644 --- a/cmd/delete.go +++ b/cmd/command/delete.go @@ -1,7 +1,7 @@ /* Copyright © 2025 Jake jake.young.dev@gmail.com */ -package cmd +package command import ( "errors" @@ -11,12 +11,11 @@ import ( "github.com/spf13/cobra" ) -// deleteCmd represents the delete command var deleteCmd = &cobra.Command{ - Use: "delete ", - Example: "mctl delete newcmd", - Short: "Delete a saved command", - Long: `Deletes a command stored using the save command`, + Use: "delete", + Example: "mctl command delete ", + // Short: "Deletes a server", + // Long: `Deletes server configuration`, Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) @@ -24,17 +23,17 @@ var deleteCmd = &cobra.Command{ err = db.DeleteCmd(args[0]) cobra.CheckErr(err) - fmt.Println("Command deleted") + + fmt.Println("Command deleted!") }, PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - return errors.New("name argument is required") + return errors.New("name parameter missing") } - return nil }, } func init() { - rootCmd.AddCommand(deleteCmd) + CommandCmd.AddCommand(deleteCmd) } diff --git a/cmd/command/view.go b/cmd/command/view.go new file mode 100644 index 0000000..85fb091 --- /dev/null +++ b/cmd/command/view.go @@ -0,0 +1,35 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package command + +import ( + "fmt" + + "code.jakeyoungdev.com/jake/mctl/database" + "github.com/spf13/cobra" +) + +var viewCmd = &cobra.Command{ + Use: "view", + Example: "mctl command view", + // Short: "view all commands", + Run: func(cmd *cobra.Command, args []string) { + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + ts, err := db.GetAllCmds() + cobra.CheckErr(err) + + for _, s := range ts { + fmt.Println("-----") + fmt.Printf("Name: %s\n", s.Name) + fmt.Printf("Command: %s\n", s.Command) + } + }, +} + +func init() { + CommandCmd.AddCommand(viewCmd) +} diff --git a/cmd/config.go b/cmd/config.go deleted file mode 100644 index d299d13..0000000 --- a/cmd/config.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright © 2025 Jake jake.young.dev@gmail.com -*/ -package cmd - -import ( - "encoding/base64" - "fmt" - "os" - - "code.jakeyoungdev.com/jake/mctl/database" - "code.jakeyoungdev.com/jake/mctl/model" - "github.com/spf13/cobra" - "golang.org/x/term" -) - -var ( - cfgserver string - cfgport int - cfgname string -) - -// configCmd represents the config command -var configCmd = &cobra.Command{ - Use: "config", - Example: "mctl config -n serverAlias -s x.x.x.x -p 61695", - Short: "Create and populate config file", - Long: `Creates the .mctl file in the user home directory - populating it with the server address, rcon password, and - rcon port to be pulled when using Login command`, - Run: func(cmd *cobra.Command, args []string) { - //read in password using term to keep it secure/hidden from bash history - fmt.Printf("Password: ") - ps, err := term.ReadPassword(int(os.Stdin.Fd())) - cobra.CheckErr(err) - - db, err := database.New() - cobra.CheckErr(err) - defer db.Close() - - err = db.Init() - cobra.CheckErr(err) - - err = db.SaveServer(model.Server{ - Name: cfgname, - Server: cfgserver, - Port: cfgport, - Password: base64.StdEncoding.EncodeToString(ps), - }) - cobra.CheckErr(err) - }, -} - -func init() { - configCmd.Flags().StringVarP(&cfgserver, "server", "s", "", "server address") - err := configCmd.MarkFlagRequired("server") - cobra.CheckErr(err) - configCmd.Flags().IntVarP(&cfgport, "port", "p", 0, "server rcon port") - err = configCmd.MarkFlagRequired("port") - cobra.CheckErr(err) - configCmd.Flags().StringVarP(&cfgname, "name", "n", "", "server alias") - err = configCmd.MarkFlagRequired("name") - cobra.CheckErr(err) - rootCmd.AddCommand(configCmd) -} diff --git a/cmd/destroy.go b/cmd/destroy.go new file mode 100644 index 0000000..e574e29 --- /dev/null +++ b/cmd/destroy.go @@ -0,0 +1,32 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package cmd + +import ( + "fmt" + + "code.jakeyoungdev.com/jake/mctl/database" + "github.com/spf13/cobra" +) + +// destroyCmd represents the destroy command +var destroyCmd = &cobra.Command{ + Use: "destroy", + // Short: "Clear all configuration", + // Long: `Clears all configuration values for mctl. [WARNING] all server configuration will be lost`, + Run: func(cmd *cobra.Command, args []string) { + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + err = db.Destroy() + cobra.CheckErr(err) + + fmt.Println("Configuration is cleared, the 'init' command must be run again.") + }, +} + +func init() { + rootCmd.AddCommand(destroyCmd) +} diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000..698a296 --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,30 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package cmd + +import ( + "code.jakeyoungdev.com/jake/mctl/database" + "github.com/spf13/cobra" +) + +var initCmd = &cobra.Command{ + Use: "init", + Example: "mctl init", + SilenceUsage: true, + // Short: "Login to server and send commands", + // Long: `Login to default server and enter command loop. The default server + // is used unless the server flag is set`, + Run: func(cmd *cobra.Command, args []string) { + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + err = db.Init() + cobra.CheckErr(err) + }, +} + +func init() { + rootCmd.AddCommand(loginCmd) +} diff --git a/cmd/login.go b/cmd/login.go index 33e78ea..8ae96ee 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -5,7 +5,6 @@ package cmd import ( "bufio" - "errors" "fmt" "os" @@ -15,19 +14,30 @@ import ( ) var ( - name string + server string ) // loginCmd represents the login command var loginCmd = &cobra.Command{ Use: "login", - Example: "mctl login ", + Example: "mctl login -s ", SilenceUsage: true, Short: "Login to server and send commands", - Long: `Login to server using saved config and enter command loop - sending commands to server and printing the response.`, + Long: `Login to default server and enter command loop. The default server + is used unless the server flag is set`, Run: func(cmd *cobra.Command, args []string) { - server := args[0] + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + //get default server + if server == "" { + ds, err := db.GetServer("default") + cobra.CheckErr(err) + + server = ds.Name + } + cli, err := client.New(server) cobra.CheckErr(err) defer cli.Close() @@ -36,10 +46,6 @@ var loginCmd = &cobra.Command{ fmt.Println("Connected! Type 'mctl' to close") scanner := bufio.NewScanner(os.Stdin) - db, err := database.New() - cobra.CheckErr(err) - defer db.Close() - var runningCmd string for runningCmd != "mctl" { fmt.Printf("RCON@%s /> ", server) @@ -56,11 +62,6 @@ var loginCmd = &cobra.Command{ break } - if runningCmd == ".run" { - } - //hmm this gets weird af tbh - dbcmd, err := db.GetCmd(runningCmd) - res, err := cli.Command(runningCmd) cobra.CheckErr(err) fmt.Printf("\n%s\n", res) @@ -69,31 +70,9 @@ var loginCmd = &cobra.Command{ fmt.Printf("Disconnected from %s\n", server) }, - PreRunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errors.New("must specify which server to login to") - } - srv := args[0] - - db, err := database.New() - if err != nil { - return err - } - defer db.Close() - - server, err := db.GetServer(srv) - if err != nil { - return err - } - - if server.Name == "" { - return fmt.Errorf("server %s not found", server.Name) - } - - return nil - }, } func init() { + loginCmd.Flags().StringVarP(&server, "server", "s", "", "server alias") rootCmd.AddCommand(loginCmd) } diff --git a/cmd/root.go b/cmd/root.go index 3431bea..166eecd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,8 @@ package cmd import ( "os" + "code.jakeyoungdev.com/jake/mctl/cmd/command" + srv "code.jakeyoungdev.com/jake/mctl/cmd/server" "github.com/spf13/cobra" ) @@ -31,4 +33,6 @@ func init() { rootCmd.CompletionOptions = cobra.CompletionOptions{ DisableDefaultCmd: true, } + rootCmd.AddCommand(srv.ServerCmd) + rootCmd.AddCommand(command.CommandCmd) //the word command is in this four times, that can't be good. } diff --git a/cmd/run.go b/cmd/run.go deleted file mode 100644 index 4c33e2d..0000000 --- a/cmd/run.go +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright © 2025 Jake jake.young.dev@gmail.com -*/ -package cmd - -import ( - "database/sql" - "errors" - "fmt" - "strings" - - "code.jakeyoungdev.com/jake/mctl/client" - "code.jakeyoungdev.com/jake/mctl/database" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -// runCmd represents the run command -var runCmd = &cobra.Command{ - Use: "run args...", - Example: "mctl run savedcmd 63 jake", - SilenceUsage: true, - Short: "Runs a previously saved command with supplied arguments on remote server", - Long: `Loads a saved command, injects the supplied arguments into the command, and sends the command to the remove server - printing the response`, - Run: func(cmd *cobra.Command, args []string) { - db, err := database.New() - cobra.CheckErr(err) - defer db.Close() - - sc, err := db.GetCmd(args[0]) - if err == sql.ErrNoRows { - fmt.Printf("command %s not found", args[0]) - return - } - cobra.CheckErr(err) - - //convert arguments to interface - var nargs []any - for _, a := range args[1:] { - nargs = append(nargs, a) - } - - //inject arguments - fixed := fmt.Sprintf(sc, nargs...) - fmt.Printf("Running saved command %s\n", fixed) - - //create client and send command - cli, err := client.New() - cobra.CheckErr(err) - defer cli.Close() - - res, err := cli.Command(fixed) - cobra.CheckErr(err) - - fmt.Println(res) - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - //ensure config command has been run - if !viper.IsSet("server") || !viper.IsSet("password") || !viper.IsSet("port") { - return errors.New("the 'config' command must be run before you can interact with servers") - } - - if !viper.IsSet("customcmd") { - return errors.New("no saved commands to run") - } - - //ensure we have a command name - al := len(args) - if al == 0 { - return errors.New("name argument is required") - } - - cmdMap := viper.Get("customcmd").(map[string]any) - count := strings.Count(cmdMap[args[0]].(string), "%s") - - //make sure enough arguments are sent to fill command placeholders - if al < count+1 { - return fmt.Errorf("not enough arguments to populate command. Supplied: %d, Needed: %d", al-1, count) - } - return nil - }, -} - -func init() { - rootCmd.AddCommand(runCmd) -} diff --git a/cmd/save.go b/cmd/save.go deleted file mode 100644 index 0cbc4d3..0000000 --- a/cmd/save.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright © 2025 Jake jake.young.dev@gmail.com -*/ -package cmd - -import ( - "bufio" - "errors" - "fmt" - "os" - - "code.jakeyoungdev.com/jake/mctl/database" - "github.com/spf13/cobra" -) - -// saveCmd represents the save command -var saveCmd = &cobra.Command{ - Use: "save ", - Example: "mctl save newcmd", - Short: "Saves a server command under an alias for quick execution", - Long: `Saves supplied command using alias to allow the command to be executed using the run command. The %s placeholder can be - used as a wildcard to be injected when running the command`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Use %s as a wildcard in your command\n", "%s") //this is ugly - fmt.Printf("Command: ") - sc := bufio.NewScanner(os.Stdin) - if sc.Scan() { - txt := sc.Text() - if txt != "" { - db, err := database.New() - cobra.CheckErr(err) - defer db.Close() - - err = db.SaveCmd(args[0], txt) - cobra.CheckErr(err) - fmt.Println("\nSaved!") - } - } - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errors.New("name argument is required") - } - - return nil - }, -} - -func init() { - rootCmd.AddCommand(saveCmd) -} diff --git a/cmd/server/add.go b/cmd/server/add.go new file mode 100644 index 0000000..8f7bbb6 --- /dev/null +++ b/cmd/server/add.go @@ -0,0 +1,70 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package server + +import ( + "bufio" + "encoding/base64" + "fmt" + "os" + "strconv" + + "code.jakeyoungdev.com/jake/mctl/database" + "code.jakeyoungdev.com/jake/mctl/model" + "github.com/spf13/cobra" + "golang.org/x/term" +) + +var addCmd = &cobra.Command{ + Use: "add", + Example: "mctl server add", + Short: "Saves a new server configuration", + Long: `Saves server address, alias, port, and password.`, + Run: func(cmd *cobra.Command, args []string) { + scanner := bufio.NewScanner(os.Stdin) + + //get server information + var cfgname string + fmt.Printf("Server alias: ") + if scanner.Scan() { + cfgname = scanner.Text() + } + + var cfgaddress string + fmt.Printf("Server address: ") + if scanner.Scan() { + cfgaddress = scanner.Text() + } + + var cfgport string + fmt.Printf("Server port: ") + if scanner.Scan() { + cfgport = scanner.Text() + } + + //read in password using term to keep it secure/hidden from bash history + fmt.Printf("Password: ") + ps, err := term.ReadPassword(int(os.Stdin.Fd())) + cobra.CheckErr(err) + + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + fp, err := strconv.Atoi(cfgport) + cobra.CheckErr(err) + + err = db.SaveServer(model.Server{ + Name: cfgname, + Server: cfgaddress, + Port: fp, + Password: base64.StdEncoding.EncodeToString(ps), + }) + cobra.CheckErr(err) + }, +} + +func init() { + ServerCmd.AddCommand(addCmd) +} diff --git a/cmd/server/delete.go b/cmd/server/delete.go new file mode 100644 index 0000000..0b94987 --- /dev/null +++ b/cmd/server/delete.go @@ -0,0 +1,36 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package server + +import ( + "errors" + + "code.jakeyoungdev.com/jake/mctl/database" + "github.com/spf13/cobra" +) + +var deleteCmd = &cobra.Command{ + Use: "delete", + Example: "mctl server delete ", + // Short: "Deletes a server", + // Long: `Deletes server configuration`, + Run: func(cmd *cobra.Command, args []string) { + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + err = db.DeleteServer(args[0]) + cobra.CheckErr(err) + }, + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("server parameter missing") + } + return nil + }, +} + +func init() { + ServerCmd.AddCommand(deleteCmd) +} diff --git a/cmd/server/server.go b/cmd/server/server.go new file mode 100644 index 0000000..1c07002 --- /dev/null +++ b/cmd/server/server.go @@ -0,0 +1,21 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package server + +import ( + "github.com/spf13/cobra" +) + +var ServerCmd = &cobra.Command{ + Use: "server", + Example: "mctl server ", + // Short: "A remote console client", + // Long: `mctl is a terminal-friendly remote console client made to manage game servers.`, + // Version: "v0.3.4", + // Run: func(cmd *cobra.Command, args []string) { }, +} + +func init() { + // rootCmd.AddCommand() +} diff --git a/cmd/server/update.go b/cmd/server/update.go new file mode 100644 index 0000000..dfa68cf --- /dev/null +++ b/cmd/server/update.go @@ -0,0 +1,48 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package server + +import ( + "encoding/base64" + "errors" + "fmt" + "os" + + "code.jakeyoungdev.com/jake/mctl/database" + "github.com/spf13/cobra" + "golang.org/x/term" +) + +var updateCmd = &cobra.Command{ + Use: "update", + Example: "mctl server update ", + // Short: "Saves a new server configuration", + // Long: `Saves server address, alias, port, and password.`, + Run: func(cmd *cobra.Command, args []string) { + //read in password using term to keep it secure/hidden from bash history + fmt.Printf("Password: ") + ps, err := term.ReadPassword(int(os.Stdin.Fd())) + cobra.CheckErr(err) + + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + err = db.UpdateServer(args[0], base64.StdEncoding.EncodeToString(ps)) + cobra.CheckErr(err) + + fmt.Printf("%s password updated!", args[0]) + }, + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("name command required") + } + + return nil + }, +} + +func init() { + ServerCmd.AddCommand(updateCmd) +} diff --git a/cmd/server/view.go b/cmd/server/view.go new file mode 100644 index 0000000..34516dd --- /dev/null +++ b/cmd/server/view.go @@ -0,0 +1,37 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package server + +import ( + "fmt" + + "code.jakeyoungdev.com/jake/mctl/database" + "github.com/spf13/cobra" +) + +var viewCmd = &cobra.Command{ + Use: "view", + Example: "mctl server view", + Short: "view all servers", + Run: func(cmd *cobra.Command, args []string) { + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + ts, err := db.GetAllServers() + cobra.CheckErr(err) + + for _, s := range ts { + fmt.Println("-----") + fmt.Printf("Name: %s\n", s.Name) + fmt.Printf("Address: %s\n", s.Server) + fmt.Printf("Port: %d\n", s.Port) + fmt.Printf("Password: %s\n", s.Password) + } + }, +} + +func init() { + ServerCmd.AddCommand(viewCmd) +} diff --git a/cmd/view.go b/cmd/view.go deleted file mode 100644 index 929bed5..0000000 --- a/cmd/view.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright © 2025 Jake jake.young.dev@gmail.com -*/ -package cmd - -import ( - "errors" - "fmt" - "strings" - - "code.jakeyoungdev.com/jake/mctl/database" - "github.com/spf13/cobra" -) - -// viewCmd represents the view command -var viewCmd = &cobra.Command{ - Use: "view ", - Example: "mctl view test", - Short: "View saved commands", - Long: `Load command using the supplied name and displays it in the terminal, 'all' will list every saved command`, - Run: func(cmd *cobra.Command, args []string) { - db, err := database.New() - cobra.CheckErr(err) - defer db.Close() - if strings.EqualFold(args[0], "all") { - cmds, err := db.GetAllCmds() - cobra.CheckErr(err) - - fmt.Println("\nCommands: ") - for _, c := range cmds { - fmt.Printf("%s - %s\n", c.Name, c.Command) - } - fmt.Println() - } else { - cmds, err := db.GetCmd(args[0]) - cobra.CheckErr(err) - - if cmds == "" { - fmt.Println("Command not found") - return - } - - fmt.Printf("Command: %s\n", cmds) - } - }, - PreRunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errors.New("name argument is required") - } - - return nil - }, -} - -func init() { - rootCmd.AddCommand(viewCmd) -} diff --git a/cryptography/aes.go b/cryptography/aes.go deleted file mode 100644 index d5f8012..0000000 --- a/cryptography/aes.go +++ /dev/null @@ -1,52 +0,0 @@ -package cryptography - -import ( - "crypto/aes" - "crypto/cipher" - - "github.com/spf13/viper" -) - -func EncryptPassword(b []byte) ([]byte, error) { - nonce := viper.Get("nonce").(string) - dev := viper.Get("device").(string) - - block, err := aes.NewCipher([]byte(dev)) - if err != nil { - return nil, err - } - - aesg, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - //adding #nosec trigger here since gosec interprets this as a hardcoded nonce value. The nonce is calculated using crypto/rand when the - //config command is ran and is pulled from memory when used any times after, for now we must prevent the scan from catching here until gosec - //is updated to account for this properly - ct := aesg.Seal(nil, []byte(nonce), []byte(b), nil) // #nosec - return ct, nil -} - -func DecryptPassword(b []byte) (string, error) { - nonce := viper.Get("nonce").(string) - password := viper.Get("password").(string) - dev := viper.Get("device").(string) - - block, err := aes.NewCipher([]byte(dev)) - if err != nil { - return "", err - } - - aesg, err := cipher.NewGCM(block) - if err != nil { - return "", err - } - - op, err := aesg.Open(nil, []byte(nonce), []byte(password), nil) - if err != nil { - return "", err - } - - return string(op), nil -} diff --git a/database/commands.go b/database/sqlx.go similarity index 88% rename from database/commands.go rename to database/sqlx.go index 80cc041..159bc95 100644 --- a/database/commands.go +++ b/database/sqlx.go @@ -10,6 +10,10 @@ import ( _ "github.com/ncruces/go-sqlite3/embed" ) +/* +all sqlx methods for CRUD functionalities of commands and servers +*/ + type database struct { *sqlx.DB } @@ -32,6 +36,8 @@ type Database interface { DeleteServer(name string) error } +// creates a new sqlite connection to the mctl database. Database files are +// kept in the the user home directory func New() (Database, error) { home, err := os.UserHomeDir() if err != nil { @@ -47,29 +53,32 @@ func New() (Database, error) { }, nil } +// intitial database setup, creates commands and servers tables func (d *database) Init() error { query := ` CREATE TABLE IF NOT EXISTS commands( name TEXT PRIMARY KEY, command TEXT - ) + ); CREATE TABLE IF NOT EXISTS servers( name TEXT PRIMARY KEY, server TEXT, password TEXT, - port NUMBER - ) + port NUMBER, + --active TEXT? bit? or something + ); ` _, err := d.Exec(query) return err } +// drops commands and servers tables func (d *database) Destroy() error { query := ` - DROP TABLE commands - DROP TABLE servers + DROP TABLE commands; + DROP TABLE servers; ` _, err := d.Exec(query) @@ -77,9 +86,10 @@ func (d *database) Destroy() error { } func (d *database) Close() error { - return d.Close() + return d.DB.Close() } +// gets command using name func (d *database) GetCmd(name string) (string, error) { query := ` SELECT @@ -99,6 +109,7 @@ func (d *database) GetCmd(name string) (string, error) { return name, nil } +// gets all saved commands func (d *database) GetAllCmds() ([]model.Command, error) { query := ` SELECT @@ -128,6 +139,7 @@ func (d *database) GetAllCmds() ([]model.Command, error) { return res, nil } +// save a new command func (d *database) SaveCmd(name, cmd string) error { query := ` INSERT INTO commands(name, command) @@ -138,6 +150,7 @@ func (d *database) SaveCmd(name, cmd string) error { return err } +// DO WE NEED THIS? func (d *database) UpdateCmd(name, cmd string) error { query := ` UPDATE diff --git a/go.mod b/go.mod index 68fac98..9a37cd3 100644 --- a/go.mod +++ b/go.mod @@ -7,26 +7,13 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/ncruces/go-sqlite3 v0.26.1 github.com/spf13/cobra v1.9.1 - github.com/spf13/viper v1.20.1 golang.org/x/term v0.31.0 ) require ( - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ncruces/julianday v1.0.0 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/subosito/gotenv v1.6.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6fee408..8adc870 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,14 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jake-young-dev/mcr v1.4.0 h1:cXZImkfI8aNIiVPrONE6qP+nfblTGsD2iXpPKTcA25U= github.com/jake-young-dev/mcr v1.4.0/go.mod h1:74yZHGf9h3tLUDUpInA17grKLrNp9lVesWvisCFCXKY= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -32,39 +17,13 @@ github.com/ncruces/go-sqlite3 v0.26.1 h1:lBXmbmucH1Bsj57NUQR6T84UoMN7jnNImhF+ibE github.com/ncruces/go-sqlite3 v0.26.1/go.mod h1:XFTPtFIo1DmGCh+XVP8KGn9b/o2f+z0WZuT09x2N6eo= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= @@ -72,7 +31,4 @@ golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/model/data.go b/model/data.go index 38841a6..71c3d9d 100644 --- a/model/data.go +++ b/model/data.go @@ -1,8 +1,5 @@ package model -//list of all fields kept in config file -// var ConfigFields = [6]string{"customcmd", "device", "nonce", "port", "server", "password"} - type Command struct { Name string `db:"name"` Command string `db:"command"` From c15c16be8de2382bd8d230023c056a77b857b473 Mon Sep 17 00:00:00 2001 From: jake Date: Wed, 18 Jun 2025 18:38:01 -0400 Subject: [PATCH 03/17] finishing sqlite changes - finished default/active server logic - dev done - needs testing --- README.md | 7 +-- client/mcr.go | 15 ++++-- cmd/command/add.go | 3 +- cmd/command/command.go | 8 +-- cmd/command/delete.go | 3 +- cmd/command/run.go | 75 +++++++++++++++++++++++++++++ cmd/command/view.go | 2 +- cmd/destroy.go | 30 ++++++++---- cmd/init.go | 11 ++--- cmd/login.go | 2 +- cmd/root.go | 3 +- cmd/server/active.go | 35 ++++++++++++++ cmd/server/add.go | 2 +- cmd/server/delete.go | 3 +- cmd/server/server.go | 8 +-- cmd/server/update.go | 3 +- cmd/server/view.go | 5 +- database/sqlx.go | 107 +++++++++++++++++++++++++++++++++++------ model/data.go | 1 + 19 files changed, 254 insertions(+), 69 deletions(-) create mode 100644 cmd/command/run.go create mode 100644 cmd/server/active.go diff --git a/README.md b/README.md index c24324b..95573cf 100644 --- a/README.md +++ b/README.md @@ -117,10 +117,5 @@ mctl utilizes [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck this repo is currently in development and may encounter breaking changes, use a tag to prevent any surprises # TODO -6. Do we use CheckErr or use RunE 7. update readme, the commands are all broken -9. do saved commands run on default server only? Yes, but add -s - mctl command run tp nutlift -s server -10. RUN COMMAND -11. Command descriptions, examples, and silence usage -12. lol "default" server functionality (which means setting which one w default command) \ No newline at end of file +8. Maybe move off of fmt \ No newline at end of file diff --git a/client/mcr.go b/client/mcr.go index ea9452b..afc93df 100644 --- a/client/mcr.go +++ b/client/mcr.go @@ -5,6 +5,7 @@ import ( "fmt" "code.jakeyoungdev.com/jake/mctl/database" + "code.jakeyoungdev.com/jake/mctl/model" "github.com/jake-young-dev/mcr" ) @@ -29,9 +30,17 @@ func New(name string) (*Client, error) { } defer db.Close() - srv, err := db.GetServer(name) - if err != nil { - return nil, err + var srv model.Server + if name != "" { + srv, err = db.GetServer(name) + if err != nil { + return nil, err + } + } else { + srv, err = db.GetActiveServer() + if err != nil { + return nil, err + } } fmt.Printf("Logging into %s on port %d\n", srv.Server, srv.Port) diff --git a/cmd/command/add.go b/cmd/command/add.go index f54080e..63975ad 100644 --- a/cmd/command/add.go +++ b/cmd/command/add.go @@ -15,8 +15,7 @@ import ( var addCmd = &cobra.Command{ Use: "add", Example: "mctl command add", - // Short: "Saves a new server configuration", - // Long: `Saves server address, alias, port, and password.`, + Short: "Saves a new command to the database", Run: func(cmd *cobra.Command, args []string) { scanner := bufio.NewScanner(os.Stdin) diff --git a/cmd/command/command.go b/cmd/command/command.go index a295960..600466b 100644 --- a/cmd/command/command.go +++ b/cmd/command/command.go @@ -11,12 +11,6 @@ import ( var CommandCmd = &cobra.Command{ Use: "command", Example: "mctl command ", - // Short: "A remote console client", - // Long: `mctl is a terminal-friendly remote console client made to manage game servers.`, - // Version: "v0.3.4", - // Run: func(cmd *cobra.Command, args []string) { }, } -func init() { - // rootCmd.AddCommand() -} +func init() {} diff --git a/cmd/command/delete.go b/cmd/command/delete.go index d6640e8..499c8f0 100644 --- a/cmd/command/delete.go +++ b/cmd/command/delete.go @@ -14,8 +14,7 @@ import ( var deleteCmd = &cobra.Command{ Use: "delete", Example: "mctl command delete ", - // Short: "Deletes a server", - // Long: `Deletes server configuration`, + Short: "Deletes a command from the database", Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) diff --git a/cmd/command/run.go b/cmd/command/run.go new file mode 100644 index 0000000..8ff9a2b --- /dev/null +++ b/cmd/command/run.go @@ -0,0 +1,75 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package command + +import ( + "errors" + "fmt" + "strings" + + "code.jakeyoungdev.com/jake/mctl/client" + "code.jakeyoungdev.com/jake/mctl/database" + "github.com/spf13/cobra" +) + +var ( + cfgserver string +) + +var runCmd = &cobra.Command{ + Use: "run", + Example: "mctl command run -s args...", + Short: "Runs a saved command on a server", + Long: `Runs the named command with the provided args on the default/active server unless -s is specified`, + RunE: func(cmd *cobra.Command, args []string) error { + cname := args[0] + + db, err := database.New() + if err != nil { + return err + } + defer db.Close() + + crun, err := db.GetCmd(cname) + if err != nil { + return err + } + + count := strings.Count(crun, "%s") + l := len(args) + if l-1 != count { + return fmt.Errorf("not enough arguments to fill all command placeholders, required: %d - have: %d", count, l) + } + + cli, err := client.New(cfgserver) + cobra.CheckErr(err) + defer cli.Close() + + var fargs []any + for _, x := range args[1:] { + fargs = append(fargs, x) + } + + res, err := cli.Command(fmt.Sprintf(crun, fargs...)) + if err != nil { + return err + } + + fmt.Println(res) + + return nil + }, + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("name parameter required") + } + + return nil + }, +} + +func init() { + runCmd.Flags().StringVarP(&cfgserver, "server", "s", "", "server name") + CommandCmd.AddCommand(runCmd) +} diff --git a/cmd/command/view.go b/cmd/command/view.go index 85fb091..ec9006c 100644 --- a/cmd/command/view.go +++ b/cmd/command/view.go @@ -13,7 +13,7 @@ import ( var viewCmd = &cobra.Command{ Use: "view", Example: "mctl command view", - // Short: "view all commands", + Short: "view all saved commands", Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) diff --git a/cmd/destroy.go b/cmd/destroy.go index e574e29..c793b9e 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -4,7 +4,10 @@ Copyright © 2025 Jake jake.young.dev@gmail.com package cmd import ( + "bufio" "fmt" + "os" + "strings" "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" @@ -12,18 +15,27 @@ import ( // destroyCmd represents the destroy command var destroyCmd = &cobra.Command{ - Use: "destroy", - // Short: "Clear all configuration", - // Long: `Clears all configuration values for mctl. [WARNING] all server configuration will be lost`, + Use: "destroy", + Short: "clears all configuration", + Long: `clear all data and drop database tables, this will delete all previously saved data and the 'inti' command + must be run again before use`, Run: func(cmd *cobra.Command, args []string) { - db, err := database.New() - cobra.CheckErr(err) - defer db.Close() + scanner := bufio.NewScanner(os.Stdin) + fmt.Printf("Are you sure you want to destroy your config? (yes|no): ") + if scanner.Scan() { + if strings.EqualFold(scanner.Text(), "yes") { + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() - err = db.Destroy() - cobra.CheckErr(err) + err = db.Destroy() + cobra.CheckErr(err) - fmt.Println("Configuration is cleared, the 'init' command must be run again.") + fmt.Println("Configuration is cleared, the 'init' command must be run again.") + } + } else { + return + } }, } diff --git a/cmd/init.go b/cmd/init.go index 698a296..b9b144d 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -9,12 +9,9 @@ import ( ) var initCmd = &cobra.Command{ - Use: "init", - Example: "mctl init", - SilenceUsage: true, - // Short: "Login to server and send commands", - // Long: `Login to default server and enter command loop. The default server - // is used unless the server flag is set`, + Use: "init", + Example: "mctl init", + Short: "initializes the database tables, must be run before mctl can be used", Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) @@ -26,5 +23,5 @@ var initCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(loginCmd) + rootCmd.AddCommand(initCmd) } diff --git a/cmd/login.go b/cmd/login.go index 8ae96ee..a9da7ab 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -32,7 +32,7 @@ var loginCmd = &cobra.Command{ //get default server if server == "" { - ds, err := db.GetServer("default") + ds, err := db.GetActiveServer() cobra.CheckErr(err) server = ds.Name diff --git a/cmd/root.go b/cmd/root.go index 166eecd..5fd9de1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -16,8 +16,7 @@ var rootCmd = &cobra.Command{ Use: "mctl", Short: "A remote console client", Long: `mctl is a terminal-friendly remote console client made to manage game servers.`, - Version: "v0.3.4", - // Run: func(cmd *cobra.Command, args []string) { }, + Version: "v0.3.4", //change version number } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cmd/server/active.go b/cmd/server/active.go new file mode 100644 index 0000000..58763c0 --- /dev/null +++ b/cmd/server/active.go @@ -0,0 +1,35 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package server + +import ( + "errors" + + "code.jakeyoungdev.com/jake/mctl/database" + "github.com/spf13/cobra" +) + +var activeCmd = &cobra.Command{ + Use: "active", + Example: "mctl server active ", + Short: "sets the active server to run commands on", + Run: func(cmd *cobra.Command, args []string) { + db, err := database.New() + cobra.CheckErr(err) + defer db.Close() + + err = db.SetActiveServer(args[0]) + cobra.CheckErr(err) + }, + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("server parameter missing") + } + return nil + }, +} + +func init() { + ServerCmd.AddCommand(activeCmd) +} diff --git a/cmd/server/add.go b/cmd/server/add.go index 8f7bbb6..27c2d89 100644 --- a/cmd/server/add.go +++ b/cmd/server/add.go @@ -20,7 +20,7 @@ var addCmd = &cobra.Command{ Use: "add", Example: "mctl server add", Short: "Saves a new server configuration", - Long: `Saves server address, alias, port, and password.`, + Long: `Saves server address, alias, port, and password to the database.`, Run: func(cmd *cobra.Command, args []string) { scanner := bufio.NewScanner(os.Stdin) diff --git a/cmd/server/delete.go b/cmd/server/delete.go index 0b94987..0fac751 100644 --- a/cmd/server/delete.go +++ b/cmd/server/delete.go @@ -13,8 +13,7 @@ import ( var deleteCmd = &cobra.Command{ Use: "delete", Example: "mctl server delete ", - // Short: "Deletes a server", - // Long: `Deletes server configuration`, + Short: "deletes a server from the database", Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) diff --git a/cmd/server/server.go b/cmd/server/server.go index 1c07002..d2bc87e 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -10,12 +10,6 @@ import ( var ServerCmd = &cobra.Command{ Use: "server", Example: "mctl server ", - // Short: "A remote console client", - // Long: `mctl is a terminal-friendly remote console client made to manage game servers.`, - // Version: "v0.3.4", - // Run: func(cmd *cobra.Command, args []string) { }, } -func init() { - // rootCmd.AddCommand() -} +func init() {} diff --git a/cmd/server/update.go b/cmd/server/update.go index dfa68cf..f2782c8 100644 --- a/cmd/server/update.go +++ b/cmd/server/update.go @@ -17,8 +17,7 @@ import ( var updateCmd = &cobra.Command{ Use: "update", Example: "mctl server update ", - // Short: "Saves a new server configuration", - // Long: `Saves server address, alias, port, and password.`, + Short: "updates a saved servers password in the database", Run: func(cmd *cobra.Command, args []string) { //read in password using term to keep it secure/hidden from bash history fmt.Printf("Password: ") diff --git a/cmd/server/view.go b/cmd/server/view.go index 34516dd..f54c5ed 100644 --- a/cmd/server/view.go +++ b/cmd/server/view.go @@ -13,7 +13,7 @@ import ( var viewCmd = &cobra.Command{ Use: "view", Example: "mctl server view", - Short: "view all servers", + Short: "view all saved servers", Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) @@ -27,7 +27,8 @@ var viewCmd = &cobra.Command{ fmt.Printf("Name: %s\n", s.Name) fmt.Printf("Address: %s\n", s.Server) fmt.Printf("Port: %d\n", s.Port) - fmt.Printf("Password: %s\n", s.Password) + fmt.Println("Password: [REDACTED]") + fmt.Printf("Default: %t\n", s.Default) } }, } diff --git a/database/sqlx.go b/database/sqlx.go index 159bc95..f3993a8 100644 --- a/database/sqlx.go +++ b/database/sqlx.go @@ -1,8 +1,11 @@ package database import ( + "context" + "database/sql" "fmt" "os" + "time" "code.jakeyoungdev.com/jake/mctl/model" "github.com/jmoiron/sqlx" @@ -11,9 +14,14 @@ import ( ) /* -all sqlx methods for CRUD functionalities of commands and servers +all sqlx methods for CRUD functionalities of commands and servers. All database methods should use the internal +methods query, exec, queryrow -- these wrapper functions handle timeouts and other configuration */ +const ( + DB_TIMEOUT = time.Second * 1 +) + type database struct { *sqlx.DB } @@ -22,6 +30,11 @@ type Database interface { Init() error Destroy() error Close() error + //internals + timeout() (context.Context, context.CancelFunc) + query(query string, args ...any) (*sqlx.Rows, error) + queryRow(query string, args ...any) *sqlx.Row + exec(query string, args ...any) (sql.Result, error) //command methods GetCmd(name string) (string, error) GetAllCmds() ([]model.Command, error) @@ -30,7 +43,9 @@ type Database interface { DeleteCmd(name string) error //server methods GetServer(name string) (model.Server, error) + GetActiveServer() (model.Server, error) GetAllServers() ([]model.Server, error) + SetActiveServer(name string) error SaveServer(srv model.Server) error UpdateServer(name, password string) error DeleteServer(name string) error @@ -65,12 +80,12 @@ func (d *database) Init() error { name TEXT PRIMARY KEY, server TEXT, password TEXT, - port NUMBER, - --active TEXT? bit? or something + port INTEGER, + active INTEGER NOT NULL DEFAULT 0 ); ` - _, err := d.Exec(query) + _, err := d.exec(query) return err } @@ -81,7 +96,7 @@ func (d *database) Destroy() error { DROP TABLE servers; ` - _, err := d.Exec(query) + _, err := d.exec(query) return err } @@ -89,6 +104,31 @@ func (d *database) Close() error { return d.DB.Close() } +func (d *database) timeout() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), DB_TIMEOUT) +} + +func (d *database) query(query string, args ...any) (*sqlx.Rows, error) { + ctx, cl := d.timeout() + defer cl() + + return d.QueryxContext(ctx, query, args...) +} + +func (d *database) queryRow(query string, args ...any) *sqlx.Row { + ctx, cl := d.timeout() + defer cl() + + return d.QueryRowxContext(ctx, query, args...) +} + +func (d *database) exec(query string, args ...any) (sql.Result, error) { + ctx, cl := d.timeout() + defer cl() + + return d.ExecContext(ctx, query, args...) +} + // gets command using name func (d *database) GetCmd(name string) (string, error) { query := ` @@ -101,7 +141,7 @@ func (d *database) GetCmd(name string) (string, error) { ` var cmd string - err := d.QueryRowx(query, name).Scan(&cmd) + err := d.queryRow(query, name).Scan(&cmd) if err != nil { return "", err } @@ -119,7 +159,7 @@ func (d *database) GetAllCmds() ([]model.Command, error) { commands ` - rows, err := d.Queryx(query) + rows, err := d.query(query) if err != nil { return nil, err } @@ -146,7 +186,7 @@ func (d *database) SaveCmd(name, cmd string) error { VALUES(?, ?) ` - _, err := d.Exec(query, name, cmd) + _, err := d.exec(query, name, cmd) return err } @@ -161,7 +201,7 @@ func (d *database) UpdateCmd(name, cmd string) error { name = ? ` - _, err := d.Exec(query, cmd, name) + _, err := d.exec(query, cmd, name) return err } @@ -171,7 +211,7 @@ func (d *database) DeleteCmd(name string) error { WHERE name = ? ` - _, err := d.Exec(query, name) + _, err := d.exec(query, name) return err } @@ -189,13 +229,31 @@ func (d *database) GetServer(name string) (model.Server, error) { ` var s model.Server - err := d.QueryRowx(query, name).StructScan(&s) + err := d.queryRow(query, name).StructScan(&s) if err != nil { return model.Server{}, err } return s, nil } +func (d *database) GetActiveServer() (model.Server, error) { + query := ` + SELECT + name, + server, + password, + port + FROM + servers + WHERE + active = 1 + ` + + var s model.Server + err := d.queryRow(query).StructScan(&s) + return s, err +} + func (d *database) GetAllServers() ([]model.Server, error) { query := ` SELECT @@ -207,7 +265,7 @@ func (d *database) GetAllServers() ([]model.Server, error) { servers ` - rows, err := d.Queryx(query) + rows, err := d.query(query) if err != nil { return nil, err } @@ -233,7 +291,26 @@ func (d *database) SaveServer(srv model.Server) error { VALUES(?, ?, ?, ?) ` - _, err := d.Exec(query, srv.Name, srv.Server, srv.Password, srv.Port) + _, err := d.exec(query, srv.Name, srv.Server, srv.Password, srv.Port) + return err +} + +func (d *database) SetActiveServer(name string) error { + query := ` + UPDATE + servers + SET + active = 0; + + UPDATE + servers + SET + active = 1 + WHERE + name = ? + ` + + _, err := d.exec(query, name) return err } @@ -247,7 +324,7 @@ func (d *database) UpdateServer(name, password string) error { name = ? ` - _, err := d.Exec(query, password, name) + _, err := d.exec(query, password, name) return err } @@ -258,6 +335,6 @@ func (d *database) DeleteServer(name string) error { name = ? ` - _, err := d.Exec(query, name) + _, err := d.exec(query, name) return err } diff --git a/model/data.go b/model/data.go index 71c3d9d..ae87d52 100644 --- a/model/data.go +++ b/model/data.go @@ -10,4 +10,5 @@ type Server struct { Server string `db:"server"` Password string `db:"password"` Port int `db:"port"` + Default bool `db:"active"` } From e4984c8941ac1d4d3a874f62ad92fb81de53b401 Mon Sep 17 00:00:00 2001 From: jake Date: Wed, 18 Jun 2025 23:57:16 -0400 Subject: [PATCH 04/17] timeout changes --- database/sqlx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/sqlx.go b/database/sqlx.go index f3993a8..3345c55 100644 --- a/database/sqlx.go +++ b/database/sqlx.go @@ -19,7 +19,7 @@ methods query, exec, queryrow -- these wrapper functions handle timeouts and oth */ const ( - DB_TIMEOUT = time.Second * 1 + DB_TIMEOUT = time.Second * 10 ) type database struct { From 473bebb04d777f52bfdb4706c09b2d83b1774625 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 00:07:52 -0400 Subject: [PATCH 05/17] sql fixes --- database/sqlx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/sqlx.go b/database/sqlx.go index 3345c55..3d6a392 100644 --- a/database/sqlx.go +++ b/database/sqlx.go @@ -307,7 +307,7 @@ func (d *database) SetActiveServer(name string) error { SET active = 1 WHERE - name = ? + name = ?; ` _, err := d.exec(query, name) From 2045a75d6849b4abaa8a3a07ea74bc8e5b6bc71e Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 15:05:09 -0400 Subject: [PATCH 06/17] [fix] bugfixes from testing - sqlite wrapper func context bug --- cmd/server/active.go | 3 ++ cmd/server/add.go | 1 + cmd/server/view.go | 2 +- database/sqlx.go | 119 ++++++++++++++++++++++++------------------- model/data.go | 2 +- 5 files changed, 73 insertions(+), 54 deletions(-) diff --git a/cmd/server/active.go b/cmd/server/active.go index 58763c0..9d780da 100644 --- a/cmd/server/active.go +++ b/cmd/server/active.go @@ -5,6 +5,7 @@ package server import ( "errors" + "fmt" "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" @@ -21,6 +22,8 @@ var activeCmd = &cobra.Command{ err = db.SetActiveServer(args[0]) cobra.CheckErr(err) + + fmt.Println("Active server updated") }, PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { diff --git a/cmd/server/add.go b/cmd/server/add.go index 27c2d89..a8db7ee 100644 --- a/cmd/server/add.go +++ b/cmd/server/add.go @@ -47,6 +47,7 @@ var addCmd = &cobra.Command{ fmt.Printf("Password: ") ps, err := term.ReadPassword(int(os.Stdin.Fd())) cobra.CheckErr(err) + fmt.Println() db, err := database.New() cobra.CheckErr(err) diff --git a/cmd/server/view.go b/cmd/server/view.go index f54c5ed..630a5d3 100644 --- a/cmd/server/view.go +++ b/cmd/server/view.go @@ -28,7 +28,7 @@ var viewCmd = &cobra.Command{ fmt.Printf("Address: %s\n", s.Server) fmt.Printf("Port: %d\n", s.Port) fmt.Println("Password: [REDACTED]") - fmt.Printf("Default: %t\n", s.Default) + fmt.Printf("Default: %t\n", s.Active) } }, } diff --git a/database/sqlx.go b/database/sqlx.go index 3d6a392..e0b0070 100644 --- a/database/sqlx.go +++ b/database/sqlx.go @@ -2,7 +2,6 @@ package database import ( "context" - "database/sql" "fmt" "os" "time" @@ -32,9 +31,6 @@ type Database interface { Close() error //internals timeout() (context.Context, context.CancelFunc) - query(query string, args ...any) (*sqlx.Rows, error) - queryRow(query string, args ...any) *sqlx.Row - exec(query string, args ...any) (sql.Result, error) //command methods GetCmd(name string) (string, error) GetAllCmds() ([]model.Command, error) @@ -84,8 +80,10 @@ func (d *database) Init() error { active INTEGER NOT NULL DEFAULT 0 ); ` - - _, err := d.exec(query) + // _, err := d.exec(query) + ctx, cl := d.timeout() + defer cl() + _, err := d.ExecContext(ctx, query) return err } @@ -96,7 +94,9 @@ func (d *database) Destroy() error { DROP TABLE servers; ` - _, err := d.exec(query) + ctx, cl := d.timeout() + defer cl() + _, err := d.ExecContext(ctx, query) return err } @@ -108,27 +108,6 @@ func (d *database) timeout() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), DB_TIMEOUT) } -func (d *database) query(query string, args ...any) (*sqlx.Rows, error) { - ctx, cl := d.timeout() - defer cl() - - return d.QueryxContext(ctx, query, args...) -} - -func (d *database) queryRow(query string, args ...any) *sqlx.Row { - ctx, cl := d.timeout() - defer cl() - - return d.QueryRowxContext(ctx, query, args...) -} - -func (d *database) exec(query string, args ...any) (sql.Result, error) { - ctx, cl := d.timeout() - defer cl() - - return d.ExecContext(ctx, query, args...) -} - // gets command using name func (d *database) GetCmd(name string) (string, error) { query := ` @@ -141,7 +120,9 @@ func (d *database) GetCmd(name string) (string, error) { ` var cmd string - err := d.queryRow(query, name).Scan(&cmd) + ctx, cl := d.timeout() + defer cl() + err := d.QueryRowxContext(ctx, query, name).Scan(&cmd) if err != nil { return "", err } @@ -159,7 +140,9 @@ func (d *database) GetAllCmds() ([]model.Command, error) { commands ` - rows, err := d.query(query) + ctx, cancel := d.timeout() + defer cancel() + rows, err := d.QueryxContext(ctx, query) if err != nil { return nil, err } @@ -186,7 +169,9 @@ func (d *database) SaveCmd(name, cmd string) error { VALUES(?, ?) ` - _, err := d.exec(query, name, cmd) + ctx, cancel := d.timeout() + defer cancel() + _, err := d.ExecContext(ctx, query, name, cmd) return err } @@ -201,7 +186,9 @@ func (d *database) UpdateCmd(name, cmd string) error { name = ? ` - _, err := d.exec(query, cmd, name) + ctx, cancel := d.timeout() + defer cancel() + _, err := d.ExecContext(ctx, query, cmd, name) return err } @@ -211,7 +198,9 @@ func (d *database) DeleteCmd(name string) error { WHERE name = ? ` - _, err := d.exec(query, name) + ctx, cancel := d.timeout() + defer cancel() + _, err := d.ExecContext(ctx, query, name) return err } @@ -221,15 +210,18 @@ func (d *database) GetServer(name string) (model.Server, error) { name, server, password, - port + port, + active FROM servers WHERE name = ? ` + ctx, cancel := d.timeout() + defer cancel() var s model.Server - err := d.queryRow(query, name).StructScan(&s) + err := d.QueryRowxContext(ctx, query, name).StructScan(&s) if err != nil { return model.Server{}, err } @@ -242,15 +234,18 @@ func (d *database) GetActiveServer() (model.Server, error) { name, server, password, - port + port, + active FROM servers WHERE active = 1 ` + ctx, cancel := d.timeout() + defer cancel() var s model.Server - err := d.queryRow(query).StructScan(&s) + err := d.QueryRowxContext(ctx, query).StructScan(&s) return s, err } @@ -260,12 +255,15 @@ func (d *database) GetAllServers() ([]model.Server, error) { name, server, password, - port + port, + active FROM servers ` - rows, err := d.query(query) + ctx, cancel := d.timeout() + defer cancel() + rows, err := d.QueryxContext(ctx, query) if err != nil { return nil, err } @@ -291,26 +289,39 @@ func (d *database) SaveServer(srv model.Server) error { VALUES(?, ?, ?, ?) ` - _, err := d.exec(query, srv.Name, srv.Server, srv.Password, srv.Port) + ctx, cancel := d.timeout() + defer cancel() + _, err := d.ExecContext(ctx, query, srv.Name, srv.Server, srv.Password, srv.Port) return err } func (d *database) SetActiveServer(name string) error { - query := ` + clear := ` UPDATE servers SET - active = 0; - - UPDATE - servers - SET - active = 1 - WHERE - name = ?; + active = 0 ` - _, err := d.exec(query, name) + clrctx, clrcancel := d.timeout() + _, err := d.ExecContext(clrctx, clear) + if err != nil { + return err + } + defer clrcancel() + + update := ` + UPDATE + servers + SET + active = 1 + WHERE + name = ? + ` + + ctx, cancel := d.timeout() + defer cancel() + _, err = d.ExecContext(ctx, update) return err } @@ -324,7 +335,9 @@ func (d *database) UpdateServer(name, password string) error { name = ? ` - _, err := d.exec(query, password, name) + ctx, cancel := d.timeout() + defer cancel() + _, err := d.ExecContext(ctx, query, password, name) return err } @@ -335,6 +348,8 @@ func (d *database) DeleteServer(name string) error { name = ? ` - _, err := d.exec(query, name) + ctx, cancel := d.timeout() + defer cancel() + _, err := d.ExecContext(ctx, query, name) return err } diff --git a/model/data.go b/model/data.go index ae87d52..fb02169 100644 --- a/model/data.go +++ b/model/data.go @@ -10,5 +10,5 @@ type Server struct { Server string `db:"server"` Password string `db:"password"` Port int `db:"port"` - Default bool `db:"active"` + Active bool `db:"active"` } From 433e4302c2789b27ce26143f24f8b78e0b4d6289 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 15:13:16 -0400 Subject: [PATCH 07/17] active server bugfix - missing name sql param --- cmd/server/add.go | 2 ++ database/sqlx.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/server/add.go b/cmd/server/add.go index a8db7ee..7d63f83 100644 --- a/cmd/server/add.go +++ b/cmd/server/add.go @@ -63,6 +63,8 @@ var addCmd = &cobra.Command{ Password: base64.StdEncoding.EncodeToString(ps), }) cobra.CheckErr(err) + + fmt.Println("Server saved") }, } diff --git a/database/sqlx.go b/database/sqlx.go index e0b0070..3d481ef 100644 --- a/database/sqlx.go +++ b/database/sqlx.go @@ -321,7 +321,7 @@ func (d *database) SetActiveServer(name string) error { ctx, cancel := d.timeout() defer cancel() - _, err = d.ExecContext(ctx, update) + _, err = d.ExecContext(ctx, update, name) return err } From fc109338a6ae5d8f2275ca60a097e7b4c79e45ff Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 15:17:38 -0400 Subject: [PATCH 08/17] bugfixes - comment out extra sql pull - addressing ignored error --- client/mcr.go | 3 +++ cmd/login.go | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/client/mcr.go b/client/mcr.go index afc93df..dc4e8af 100644 --- a/client/mcr.go +++ b/client/mcr.go @@ -46,6 +46,9 @@ func New(name string) (*Client, error) { var p []byte _, err = base64.StdEncoding.Decode(p, []byte(srv.Password)) + if err != nil { + return nil, err + } //connect to game server cli := mcr.NewClient(srv.Server, mcr.WithPort(srv.Port)) diff --git a/cmd/login.go b/cmd/login.go index a9da7ab..78e1130 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -31,12 +31,12 @@ var loginCmd = &cobra.Command{ defer db.Close() //get default server - if server == "" { - ds, err := db.GetActiveServer() - cobra.CheckErr(err) + // if server == "" { + // ds, err := db.GetActiveServer() + // cobra.CheckErr(err) - server = ds.Name - } + // server = ds + // } cli, err := client.New(server) cobra.CheckErr(err) From 2791ca83f96861606ddcc71c34118a5f66f050c7 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 15:20:24 -0400 Subject: [PATCH 09/17] logging server for tests --- client/mcr.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client/mcr.go b/client/mcr.go index dc4e8af..da396f5 100644 --- a/client/mcr.go +++ b/client/mcr.go @@ -43,6 +43,7 @@ func New(name string) (*Client, error) { } } fmt.Printf("Logging into %s on port %d\n", srv.Server, srv.Port) + fmt.Println(srv) var p []byte _, err = base64.StdEncoding.Decode(p, []byte(srv.Password)) From d0bc383d71b63e38d50921953fbb3b71ec2ba372 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 15:23:56 -0400 Subject: [PATCH 10/17] [fix] base64 updates - decoding fix - removing extra logging --- client/mcr.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/mcr.go b/client/mcr.go index da396f5..671e755 100644 --- a/client/mcr.go +++ b/client/mcr.go @@ -43,10 +43,8 @@ func New(name string) (*Client, error) { } } fmt.Printf("Logging into %s on port %d\n", srv.Server, srv.Port) - fmt.Println(srv) - var p []byte - _, err = base64.StdEncoding.Decode(p, []byte(srv.Password)) + p, err := base64.StdEncoding.DecodeString(srv.Password) if err != nil { return nil, err } From 5e950f5e847aa2bf9db3ad7401853e9f6d48ca5c Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 15:28:21 -0400 Subject: [PATCH 11/17] command run return value bugfix --- cmd/login.go | 12 ++---------- database/sqlx.go | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index 78e1130..57105b1 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -30,14 +30,6 @@ var loginCmd = &cobra.Command{ cobra.CheckErr(err) defer db.Close() - //get default server - // if server == "" { - // ds, err := db.GetActiveServer() - // cobra.CheckErr(err) - - // server = ds - // } - cli, err := client.New(server) cobra.CheckErr(err) defer cli.Close() @@ -48,7 +40,7 @@ var loginCmd = &cobra.Command{ var runningCmd string for runningCmd != "mctl" { - fmt.Printf("RCON@%s /> ", server) + fmt.Printf("RCON /> ") if scanner.Scan() { runningCmd = scanner.Text() @@ -68,7 +60,7 @@ var loginCmd = &cobra.Command{ } } - fmt.Printf("Disconnected from %s\n", server) + fmt.Println("Disconnected") }, } diff --git a/database/sqlx.go b/database/sqlx.go index 3d481ef..c5f0700 100644 --- a/database/sqlx.go +++ b/database/sqlx.go @@ -127,7 +127,7 @@ func (d *database) GetCmd(name string) (string, error) { return "", err } - return name, nil + return cmd, nil } // gets all saved commands From 0d7fbffaf68e6b80293f1214572a381648cea8e5 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 15:29:40 -0400 Subject: [PATCH 12/17] readme update --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 95573cf..6284324 100644 --- a/README.md +++ b/README.md @@ -118,4 +118,5 @@ this repo is currently in development and may encounter breaking changes, use a # TODO 7. update readme, the commands are all broken -8. Maybe move off of fmt \ No newline at end of file +8. Maybe move off of fmt +10. Testing results, list commands that have been tested here. Run command one errors if it cant find command, lets wrap that \ No newline at end of file From 7ff43c82c20cb2476ad68e79bfff42b7ae4902e8 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 17:23:17 -0400 Subject: [PATCH 13/17] [fix] Testing fixes - sqlite error handling and wrapping - more responsive commands - database updates --- cmd/command/add.go | 13 ++++++++++--- cmd/command/command.go | 5 +++++ cmd/command/delete.go | 13 +++++++++---- cmd/command/run.go | 13 +++++++++---- cmd/command/view.go | 11 ++++++++--- cmd/destroy.go | 6 ++++++ cmd/init.go | 7 ++++--- cmd/login.go | 5 ----- cmd/server/active.go | 11 ++++++++--- cmd/server/add.go | 13 +++++++++---- cmd/server/delete.go | 14 +++++++++++--- cmd/server/server.go | 5 +++++ cmd/server/update.go | 11 ++++++++--- cmd/server/view.go | 11 ++++++++--- database/sqlx.go | 14 ++++++++++++-- 15 files changed, 112 insertions(+), 40 deletions(-) diff --git a/cmd/command/add.go b/cmd/command/add.go index 63975ad..fe85d7d 100644 --- a/cmd/command/add.go +++ b/cmd/command/add.go @@ -13,9 +13,10 @@ import ( ) var addCmd = &cobra.Command{ - Use: "add", - Example: "mctl command add", - Short: "Saves a new command to the database", + Use: "add", + Example: "mctl command add", + Short: "Saves a new command to the database", + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { scanner := bufio.NewScanner(os.Stdin) @@ -40,7 +41,13 @@ var addCmd = &cobra.Command{ cobra.CheckErr(err) err = db.SaveCmd(cfgname, cfgcmd) + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } cobra.CheckErr(err) + + fmt.Println("Command saved") }, } diff --git a/cmd/command/command.go b/cmd/command/command.go index 600466b..f9f58dc 100644 --- a/cmd/command/command.go +++ b/cmd/command/command.go @@ -7,6 +7,11 @@ import ( "github.com/spf13/cobra" ) +const ( + ErrInit = "sqlite3: SQL logic error: no such table: commands" + ErrInitRsp = "The 'init' command must be run before mctl can be used" +) + // CommandCmd is such a cool name lol var CommandCmd = &cobra.Command{ Use: "command", diff --git a/cmd/command/delete.go b/cmd/command/delete.go index 499c8f0..ddd4577 100644 --- a/cmd/command/delete.go +++ b/cmd/command/delete.go @@ -12,18 +12,23 @@ import ( ) var deleteCmd = &cobra.Command{ - Use: "delete", - Example: "mctl command delete ", - Short: "Deletes a command from the database", + Use: "delete", + Example: "mctl command delete ", + Short: "Deletes a command from the database", + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) defer db.Close() err = db.DeleteCmd(args[0]) + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } cobra.CheckErr(err) - fmt.Println("Command deleted!") + fmt.Println("Command deleted") }, PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { diff --git a/cmd/command/run.go b/cmd/command/run.go index 8ff9a2b..c739180 100644 --- a/cmd/command/run.go +++ b/cmd/command/run.go @@ -18,10 +18,11 @@ var ( ) var runCmd = &cobra.Command{ - Use: "run", - Example: "mctl command run -s args...", - Short: "Runs a saved command on a server", - Long: `Runs the named command with the provided args on the default/active server unless -s is specified`, + Use: "run", + Example: "mctl command run -s args...", + Short: "Runs a saved command on a server", + Long: `Runs the named command with the provided args on the default/active server unless -s is specified`, + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { cname := args[0] @@ -32,6 +33,10 @@ var runCmd = &cobra.Command{ defer db.Close() crun, err := db.GetCmd(cname) + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return nil + } if err != nil { return err } diff --git a/cmd/command/view.go b/cmd/command/view.go index ec9006c..a615eb3 100644 --- a/cmd/command/view.go +++ b/cmd/command/view.go @@ -11,15 +11,20 @@ import ( ) var viewCmd = &cobra.Command{ - Use: "view", - Example: "mctl command view", - Short: "view all saved commands", + Use: "view", + Example: "mctl command view", + Short: "view all saved commands", + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) defer db.Close() ts, err := db.GetAllCmds() + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } cobra.CheckErr(err) for _, s := range ts { diff --git a/cmd/destroy.go b/cmd/destroy.go index c793b9e..4e892c3 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -9,6 +9,7 @@ import ( "os" "strings" + "code.jakeyoungdev.com/jake/mctl/cmd/command" "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" ) @@ -19,6 +20,7 @@ var destroyCmd = &cobra.Command{ Short: "clears all configuration", Long: `clear all data and drop database tables, this will delete all previously saved data and the 'inti' command must be run again before use`, + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { scanner := bufio.NewScanner(os.Stdin) fmt.Printf("Are you sure you want to destroy your config? (yes|no): ") @@ -29,6 +31,10 @@ var destroyCmd = &cobra.Command{ defer db.Close() err = db.Destroy() + if err.Error() == command.ErrInit { + fmt.Println(command.ErrInitRsp) + return + } cobra.CheckErr(err) fmt.Println("Configuration is cleared, the 'init' command must be run again.") diff --git a/cmd/init.go b/cmd/init.go index b9b144d..49046d5 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -9,9 +9,10 @@ import ( ) var initCmd = &cobra.Command{ - Use: "init", - Example: "mctl init", - Short: "initializes the database tables, must be run before mctl can be used", + Use: "init", + Example: "mctl init", + Short: "initializes the database tables, must be run before mctl can be used", + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) diff --git a/cmd/login.go b/cmd/login.go index 57105b1..298712a 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -9,7 +9,6 @@ import ( "os" "code.jakeyoungdev.com/jake/mctl/client" - "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" ) @@ -26,10 +25,6 @@ var loginCmd = &cobra.Command{ Long: `Login to default server and enter command loop. The default server is used unless the server flag is set`, Run: func(cmd *cobra.Command, args []string) { - db, err := database.New() - cobra.CheckErr(err) - defer db.Close() - cli, err := client.New(server) cobra.CheckErr(err) defer cli.Close() diff --git a/cmd/server/active.go b/cmd/server/active.go index 9d780da..65922f1 100644 --- a/cmd/server/active.go +++ b/cmd/server/active.go @@ -12,15 +12,20 @@ import ( ) var activeCmd = &cobra.Command{ - Use: "active", - Example: "mctl server active ", - Short: "sets the active server to run commands on", + Use: "active", + Example: "mctl server active ", + Short: "sets the active server to run commands on", + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) defer db.Close() err = db.SetActiveServer(args[0]) + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } cobra.CheckErr(err) fmt.Println("Active server updated") diff --git a/cmd/server/add.go b/cmd/server/add.go index 7d63f83..9d67339 100644 --- a/cmd/server/add.go +++ b/cmd/server/add.go @@ -17,10 +17,11 @@ import ( ) var addCmd = &cobra.Command{ - Use: "add", - Example: "mctl server add", - Short: "Saves a new server configuration", - Long: `Saves server address, alias, port, and password to the database.`, + Use: "add", + Example: "mctl server add", + Short: "Saves a new server configuration", + Long: `Saves server address, alias, port, and password to the database.`, + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { scanner := bufio.NewScanner(os.Stdin) @@ -62,6 +63,10 @@ var addCmd = &cobra.Command{ Port: fp, Password: base64.StdEncoding.EncodeToString(ps), }) + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } cobra.CheckErr(err) fmt.Println("Server saved") diff --git a/cmd/server/delete.go b/cmd/server/delete.go index 0fac751..13bab0e 100644 --- a/cmd/server/delete.go +++ b/cmd/server/delete.go @@ -5,22 +5,30 @@ package server import ( "errors" + "fmt" "code.jakeyoungdev.com/jake/mctl/database" "github.com/spf13/cobra" ) var deleteCmd = &cobra.Command{ - Use: "delete", - Example: "mctl server delete ", - Short: "deletes a server from the database", + Use: "delete", + Example: "mctl server delete ", + Short: "deletes a server from the database", + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) defer db.Close() err = db.DeleteServer(args[0]) + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } cobra.CheckErr(err) + + fmt.Println("Server deleted") }, PreRunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { diff --git a/cmd/server/server.go b/cmd/server/server.go index d2bc87e..64352da 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -7,6 +7,11 @@ import ( "github.com/spf13/cobra" ) +const ( + ErrInit = "sqlite3: SQL logic error: no such table: servers" + ErrInitRsp = "The 'init' command must be run before mctl can be used" +) + var ServerCmd = &cobra.Command{ Use: "server", Example: "mctl server ", diff --git a/cmd/server/update.go b/cmd/server/update.go index f2782c8..f5368c1 100644 --- a/cmd/server/update.go +++ b/cmd/server/update.go @@ -15,9 +15,10 @@ import ( ) var updateCmd = &cobra.Command{ - Use: "update", - Example: "mctl server update ", - Short: "updates a saved servers password in the database", + Use: "update", + Example: "mctl server update ", + Short: "updates a saved servers password in the database", + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { //read in password using term to keep it secure/hidden from bash history fmt.Printf("Password: ") @@ -29,6 +30,10 @@ var updateCmd = &cobra.Command{ defer db.Close() err = db.UpdateServer(args[0], base64.StdEncoding.EncodeToString(ps)) + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } cobra.CheckErr(err) fmt.Printf("%s password updated!", args[0]) diff --git a/cmd/server/view.go b/cmd/server/view.go index 630a5d3..e52fb46 100644 --- a/cmd/server/view.go +++ b/cmd/server/view.go @@ -11,15 +11,20 @@ import ( ) var viewCmd = &cobra.Command{ - Use: "view", - Example: "mctl server view", - Short: "view all saved servers", + Use: "view", + Example: "mctl server view", + Short: "view all saved servers", + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { db, err := database.New() cobra.CheckErr(err) defer db.Close() ts, err := db.GetAllServers() + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } cobra.CheckErr(err) for _, s := range ts { diff --git a/database/sqlx.go b/database/sqlx.go index c5f0700..499175a 100644 --- a/database/sqlx.go +++ b/database/sqlx.go @@ -2,6 +2,8 @@ package database import ( "context" + "database/sql" + "errors" "fmt" "os" "time" @@ -13,8 +15,7 @@ import ( ) /* -all sqlx methods for CRUD functionalities of commands and servers. All database methods should use the internal -methods query, exec, queryrow -- these wrapper functions handle timeouts and other configuration +all sqlx methods for CRUD functionalities of commands and servers. */ const ( @@ -123,6 +124,9 @@ func (d *database) GetCmd(name string) (string, error) { ctx, cl := d.timeout() defer cl() err := d.QueryRowxContext(ctx, query, name).Scan(&cmd) + if errors.Is(err, sql.ErrNoRows) { + return "", errors.New("Command not found") + } if err != nil { return "", err } @@ -222,6 +226,9 @@ func (d *database) GetServer(name string) (model.Server, error) { defer cancel() var s model.Server err := d.QueryRowxContext(ctx, query, name).StructScan(&s) + if errors.Is(err, sql.ErrNoRows) { + return model.Server{}, errors.New("Server not found") + } if err != nil { return model.Server{}, err } @@ -246,6 +253,9 @@ func (d *database) GetActiveServer() (model.Server, error) { defer cancel() var s model.Server err := d.QueryRowxContext(ctx, query).StructScan(&s) + if errors.Is(err, sql.ErrNoRows) { + return s, errors.New("No active server set") + } return s, err } From 71ace969d38a89a72c15553ab2cb6ba328dd5ece Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 17:28:56 -0400 Subject: [PATCH 14/17] null pointer fix - need to nil check before pulling value --- cmd/command/add.go | 8 +++++--- cmd/command/command.go | 2 ++ cmd/command/delete.go | 8 +++++--- cmd/command/run.go | 8 ++++---- cmd/command/view.go | 8 +++++--- cmd/destroy.go | 8 +++++--- cmd/server/active.go | 8 +++++--- cmd/server/add.go | 8 +++++--- cmd/server/delete.go | 8 +++++--- cmd/server/server.go | 2 ++ cmd/server/update.go | 8 +++++--- cmd/server/view.go | 8 +++++--- 12 files changed, 53 insertions(+), 31 deletions(-) diff --git a/cmd/command/add.go b/cmd/command/add.go index fe85d7d..7a212fe 100644 --- a/cmd/command/add.go +++ b/cmd/command/add.go @@ -41,9 +41,11 @@ var addCmd = &cobra.Command{ cobra.CheckErr(err) err = db.SaveCmd(cfgname, cfgcmd) - if err.Error() == ErrInit { - fmt.Println(ErrInitRsp) - return + if err != nil { + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } } cobra.CheckErr(err) diff --git a/cmd/command/command.go b/cmd/command/command.go index f9f58dc..ac0e7f9 100644 --- a/cmd/command/command.go +++ b/cmd/command/command.go @@ -8,6 +8,8 @@ import ( ) const ( + //sqlite doesn't have an error type for this error, but we want to catch when this error is thrown + //and provide the proper response. It should not be treated as an error if the db isn't setup. ErrInit = "sqlite3: SQL logic error: no such table: commands" ErrInitRsp = "The 'init' command must be run before mctl can be used" ) diff --git a/cmd/command/delete.go b/cmd/command/delete.go index ddd4577..95fcfa8 100644 --- a/cmd/command/delete.go +++ b/cmd/command/delete.go @@ -22,9 +22,11 @@ var deleteCmd = &cobra.Command{ defer db.Close() err = db.DeleteCmd(args[0]) - if err.Error() == ErrInit { - fmt.Println(ErrInitRsp) - return + if err != nil { + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } } cobra.CheckErr(err) diff --git a/cmd/command/run.go b/cmd/command/run.go index c739180..de4c67d 100644 --- a/cmd/command/run.go +++ b/cmd/command/run.go @@ -33,11 +33,11 @@ var runCmd = &cobra.Command{ defer db.Close() crun, err := db.GetCmd(cname) - if err.Error() == ErrInit { - fmt.Println(ErrInitRsp) - return nil - } if err != nil { + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return nil + } return err } diff --git a/cmd/command/view.go b/cmd/command/view.go index a615eb3..66c1190 100644 --- a/cmd/command/view.go +++ b/cmd/command/view.go @@ -21,9 +21,11 @@ var viewCmd = &cobra.Command{ defer db.Close() ts, err := db.GetAllCmds() - if err.Error() == ErrInit { - fmt.Println(ErrInitRsp) - return + if err != nil { + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } } cobra.CheckErr(err) diff --git a/cmd/destroy.go b/cmd/destroy.go index 4e892c3..dc51872 100644 --- a/cmd/destroy.go +++ b/cmd/destroy.go @@ -31,9 +31,11 @@ var destroyCmd = &cobra.Command{ defer db.Close() err = db.Destroy() - if err.Error() == command.ErrInit { - fmt.Println(command.ErrInitRsp) - return + if err != nil { + if err.Error() == command.ErrInit { + fmt.Println(command.ErrInitRsp) + return + } } cobra.CheckErr(err) diff --git a/cmd/server/active.go b/cmd/server/active.go index 65922f1..fbb63b0 100644 --- a/cmd/server/active.go +++ b/cmd/server/active.go @@ -22,9 +22,11 @@ var activeCmd = &cobra.Command{ defer db.Close() err = db.SetActiveServer(args[0]) - if err.Error() == ErrInit { - fmt.Println(ErrInitRsp) - return + if err != nil { + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } } cobra.CheckErr(err) diff --git a/cmd/server/add.go b/cmd/server/add.go index 9d67339..51074bd 100644 --- a/cmd/server/add.go +++ b/cmd/server/add.go @@ -63,9 +63,11 @@ var addCmd = &cobra.Command{ Port: fp, Password: base64.StdEncoding.EncodeToString(ps), }) - if err.Error() == ErrInit { - fmt.Println(ErrInitRsp) - return + if err != nil { + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } } cobra.CheckErr(err) diff --git a/cmd/server/delete.go b/cmd/server/delete.go index 13bab0e..e18d61d 100644 --- a/cmd/server/delete.go +++ b/cmd/server/delete.go @@ -22,9 +22,11 @@ var deleteCmd = &cobra.Command{ defer db.Close() err = db.DeleteServer(args[0]) - if err.Error() == ErrInit { - fmt.Println(ErrInitRsp) - return + if err != nil { + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } } cobra.CheckErr(err) diff --git a/cmd/server/server.go b/cmd/server/server.go index 64352da..cfc9f73 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -8,6 +8,8 @@ import ( ) const ( + //sqlite doesn't have an error type for this error, but we want to catch when this error is thrown + //and provide the proper response. It should not be treated as an error if the db isn't setup. ErrInit = "sqlite3: SQL logic error: no such table: servers" ErrInitRsp = "The 'init' command must be run before mctl can be used" ) diff --git a/cmd/server/update.go b/cmd/server/update.go index f5368c1..c344edd 100644 --- a/cmd/server/update.go +++ b/cmd/server/update.go @@ -30,9 +30,11 @@ var updateCmd = &cobra.Command{ defer db.Close() err = db.UpdateServer(args[0], base64.StdEncoding.EncodeToString(ps)) - if err.Error() == ErrInit { - fmt.Println(ErrInitRsp) - return + if err != nil { + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } } cobra.CheckErr(err) diff --git a/cmd/server/view.go b/cmd/server/view.go index e52fb46..f13cd7a 100644 --- a/cmd/server/view.go +++ b/cmd/server/view.go @@ -21,9 +21,11 @@ var viewCmd = &cobra.Command{ defer db.Close() ts, err := db.GetAllServers() - if err.Error() == ErrInit { - fmt.Println(ErrInitRsp) - return + if err != nil { + if err.Error() == ErrInit { + fmt.Println(ErrInitRsp) + return + } } cobra.CheckErr(err) From f421bd906a92c890bfb654a65713b58365d5b819 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 17:34:53 -0400 Subject: [PATCH 15/17] small fixes before merge --- cmd/command/run.go | 2 +- cmd/server/active.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/command/run.go b/cmd/command/run.go index de4c67d..610eaca 100644 --- a/cmd/command/run.go +++ b/cmd/command/run.go @@ -44,7 +44,7 @@ var runCmd = &cobra.Command{ count := strings.Count(crun, "%s") l := len(args) if l-1 != count { - return fmt.Errorf("not enough arguments to fill all command placeholders, required: %d - have: %d", count, l) + return fmt.Errorf("not enough arguments to fill all command placeholders, required: %d - have: %d", count, l-1) } cli, err := client.New(cfgserver) diff --git a/cmd/server/active.go b/cmd/server/active.go index fbb63b0..544be6f 100644 --- a/cmd/server/active.go +++ b/cmd/server/active.go @@ -12,7 +12,7 @@ import ( ) var activeCmd = &cobra.Command{ - Use: "active", + Use: "activate", Example: "mctl server active ", Short: "sets the active server to run commands on", SilenceUsage: true, From 5d580e776674eda4ac83d81d42ecba6fe8239e62 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 17:53:12 -0400 Subject: [PATCH 16/17] readme update --- README.md | 114 ++++++++++++++++++++++++------------------------------ 1 file changed, 51 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 6284324..286a4f5 100644 --- a/README.md +++ b/README.md @@ -24,85 +24,78 @@ mctl requires a one-time setup via the 'init' command before using any other com mctl init ``` +
+ ### Add a server -A - - - - - - - - - - - -### Connecting to server -Once the client has been configured commands can be sent to the server using the 'login' command. This will authenticate you with the game server and enter the command loop, the session is logged out when the command loop is broken - +To communicate with servers, they must be first added to the server list ```bash -mctl login #makes auth request to server with saved password +mctl server add +Server alias: home +Server address: x.x.x.x +Server port: 61695 +Password: ``` -### Sending commands -If login is successful the app will enter the command loop, which allows commands to be sent directly to the server. Commands are sent as-is to the server, there is no validation of command syntax within mctl -``` -Logging into X.X.X.X on port 61695 -Connected! Type 'mctl' to close -RCON@X.X.X.X /> list +
-There are 0 of a max of 20 players online: -``` - -### Saving commands -Commands can be saved under an alias for quick execution later, saved commands can contain placeholders '%s' that can be populated at runtime to allow for commands with unique runtime args to still be saved, see [example](#saving-and-running-example) for more: +### Activating a server +Servers can be activated to be used as the default server for mctl operations, the server alias must be +provided to activate it ```bash -mctl save +mctl server activate ``` -### Viewing commands -Saved commands can be viewed with: +
+ +### Login to server +To login to the activated server and start sending commands ```bash -mctl view +mctl login ``` -All saved commands can be viewed with: +The -s flag can be set to login to a non-activated server ```bash -mctl view all +mctl login -s ``` -### Running saved commands -Commands that have been saved can be run with: +
+ +### Savind a command +Commands can be saved to be run later ```bash -mctl run -``` -If the saved command contains placeholders, the necessary arguments must be supplied: -```bash -mctl run args... +mctl command add +Command alias: seed +Command: seed ``` -### Saving and running example +The command saved can then be run with the following command, this will run the 'seed' command on the activated +server, or a different server using the -s flag ```bash -#saving command named "test" to run "tp %s 0 0 0" -mctl save test -Command: tp %s 0 0 0 - -#run command on user "jake" -mctl run test jake -#will run: tp jake 0 0 0 on remote server +mctl command run seed ``` -### Delete saved command -Commands can be deleted with: +Commands can contain placeholders to be filled in at runtime ```bash -mctl delete +#created the 'placeholder' command which runs the 'kill' command +mctl command add +Command alias: placeholder +Command: kill %s +``` +```bash +#runs the 'kill' command on 'player' +mctl command run placeholder kill player +``` +Running the placeholder command with the arg 'player' will run 'kill player' on the server + +
+ +### Clearing database +The mctl database can be cleared with the destoy command. WARNING, this command will delete any saved servers or commands completely resetting mctl +```bash +mctl destroy ``` -### Clear configuration file -To clear all fields from the configuration file use: -```bash -#CAUTION: If the config file is cleared all data previously saved will be lost forever -mctl clear -``` +### CRUD Commands +Other create, read, update, and delete versions of subcommands are available. Use the -h flag for more information
@@ -114,9 +107,4 @@ mctl utilizes [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
# Development -this repo is currently in development and may encounter breaking changes, use a tag to prevent any surprises - -# TODO -7. update readme, the commands are all broken -8. Maybe move off of fmt -10. Testing results, list commands that have been tested here. Run command one errors if it cant find command, lets wrap that \ No newline at end of file +this repo is currently in development and may encounter breaking changes, use a tag to prevent any surprises \ No newline at end of file From b480efc6ec725b1e0739e473df9d9afc4d9c7200 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 19 Jun 2025 17:55:11 -0400 Subject: [PATCH 17/17] vuln patch --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 9a37cd3..90df8b4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module code.jakeyoungdev.com/jake/mctl -go 1.24.2 +go 1.24.4 require ( github.com/jake-young-dev/mcr v1.4.0