diff --git a/README.md b/README.md index 49387eb..cfb39f4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ # mctl +mctl is a terminal-friendly remote connection client +## Installation +Install mctl using golang +```bash +go install code.jakeyoungdev.com/jake/mctl@master #it is recommended to use a tagged version to prevent pulling unwanted changes +``` + +## Setup +### Configuring mctl +mctl requires a one-time setup via the 'config' command, password is entered securely from the terminal +```bash +mctl config -s -p +``` + +### 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 + +```bash +mctl login #makes auth request to server with saved password +``` + +### Sending commands +If login is successful the app will enter the command loop, which allows commands to be sent directly to the server until 'mctl' is sent. Commands are sent as-is to the server, there is no validation of command syntax within mctl + +## 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| + +### Flags +#### config +|Flag|Shorthand|Required|Description| +|---|---|---|---| +|port|p|yes|RCon port| +|server|s|yes|RCon address| + +#### login +no flags + +### Configuration file +All configuration data will be kept in /home/.mctl.yaml, passwords are encrypted for an added layer of security + +## Development +this repo is currently in heavy development and may encounter breaking changes \ No newline at end of file diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..4c3d7e9 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,95 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package cmd + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "fmt" + "io" + "os" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "golang.org/x/term" +) + +var ( + server string + port int +) + +// configCmd represents the config command +var configCmd = &cobra.Command{ + Use: "config", + 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) + + //setup aes encrypter + block, err := aes.NewCipher([]byte(viper.Get("device").(string))) + cobra.CheckErr(err) + //generate and apply random nonce + nonce := make([]byte, 12) + _, err = io.ReadFull(rand.Reader, nonce) + cobra.CheckErr(err) + aesg, err := cipher.NewGCM(block) + cobra.CheckErr(err) + + //encrypt rcon password + ciphert := aesg.Seal(nil, nonce, ps, nil) + + //update config file with new values + viper.Set("server", server) + viper.Set("password", string(ciphert)) + viper.Set("port", port) + viper.Set("nonce", string(nonce)) + viper.WriteConfig() + fmt.Println() + fmt.Println("Config file updated!") + }, +} + +func init() { + initConfig() + configCmd.Flags().StringVarP(&server, "server", "s", "", "server address") + configCmd.MarkFlagRequired("server") + configCmd.Flags().IntVarP(&port, "port", "p", 0, "server rcon port") + configCmd.MarkFlagRequired("port") + rootCmd.AddCommand(configCmd) +} + +func initConfig() { + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".mctl") + + viper.AutomaticEnv() + viper.ReadInConfig() + + if err := viper.ReadInConfig(); err != nil { + //file does not exist, create it + viper.Set("server", server) + viper.Set("password", "") + viper.Set("port", port) + viper.Set("nonce", "") + + //generate a uuid to be used as encryption key + uu := uuid.New() + viper.Set("device", uu.String()) + + viper.SafeWriteConfig() + } +} diff --git a/cmd/login.go b/cmd/login.go new file mode 100644 index 0000000..d0a87e7 --- /dev/null +++ b/cmd/login.go @@ -0,0 +1,78 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package cmd + +import ( + "bufio" + "crypto/aes" + "crypto/cipher" + "fmt" + "os" + + "github.com/jake-young-dev/mcr" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// loginCmd represents the login command +var loginCmd = &cobra.Command{ + Use: "login", + 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") + password := viper.Get("password") + port := viper.Get("port") + fmt.Printf("Logging into %s on port %d\n", server, port) + + //setup decrypter + nonce := viper.Get("nonce") + block, err := aes.NewCipher([]byte(viper.Get("device").(string))) + cobra.CheckErr(err) + aesg, err := cipher.NewGCM(block) + cobra.CheckErr(err) + + //decrypt password + pwd := []byte(password.(string)) + nn := []byte(nonce.(string)) + pt, err := aesg.Open(nil, nn, pwd, nil) + cobra.CheckErr(err) + + //connect to game server + cli := mcr.NewClient(server.(string), mcr.WithPort(port.(int))) + err = cli.Connect(string(pt)) + cobra.CheckErr(err) + defer cli.Close() + + //start command loop + fmt.Println("Connected! Type 'mctl' to close") + scanner := bufio.NewScanner(os.Stdin) + var runningCmd string + for runningCmd != "mctl" { + fmt.Printf("RCON@%s /> ", server) + + if scanner.Scan() { + runningCmd = scanner.Text() + + if runningCmd == "" { + continue + } + + if runningCmd == "mctl" { + break + } + + res, err := cli.Command(runningCmd) + cobra.CheckErr(err) + fmt.Printf("\n%s\n", res) + } + } + }, +} + +func init() { + rootCmd.AddCommand(loginCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..c15f32e --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,32 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands +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.`, + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + //dont show completion subcommand in help message, makes the syntax confusing + rootCmd.Root().CompletionOptions.DisableDefaultCmd = true +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c2802db --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module code.jakeyoungdev.com/jake/mctl + +go 1.24.0 + +require ( + github.com/google/uuid v1.6.0 + github.com/jake-young-dev/mcr v1.3.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/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 + 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 + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b17ece1 --- /dev/null +++ b/go.sum @@ -0,0 +1,64 @@ +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-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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/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/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= +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/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= +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/main.go b/main.go new file mode 100644 index 0000000..290687b --- /dev/null +++ b/main.go @@ -0,0 +1,10 @@ +/* +Copyright © 2025 Jake jake.young.dev@gmail.com +*/ +package main + +import "code.jakeyoungdev.com/jake/mctl/cmd" + +func main() { + cmd.Execute() +}