Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
0e50a4908b | |||
5e91816682 | |||
5de46fecc1 | |||
b480efc6ec | |||
5d580e7766 | |||
f421bd906a | |||
71ace969d3 | |||
7ff43c82c2 | |||
0d7fbffaf6 | |||
5e950f5e84 | |||
d0bc383d71 | |||
2791ca83f9 | |||
fc109338a6 | |||
433e4302c2 | |||
2045a75d68 | |||
473bebb04d | |||
e4984c8941 | |||
c15c16be8d | |||
fe37cac2da | |||
77bb3166c4 | |||
58ece42142 | |||
49c508aae7 | |||
dcfad2dfee | |||
5b6ddc71cf | |||
f8282c3676 | |||
386a766185 | |||
26c50085d6 | |||
13d3b2cef3 | |||
4100762986 | |||
a3527d3388 |
17
.gitea/workflows/security.yaml
Normal file
17
.gitea/workflows/security.yaml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
name: "code scans"
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
scans:
|
||||||
|
runs-on: fire
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: "dependency scan and static code analysis"
|
||||||
|
uses: https://code.jakeyoungdev.com/actions/donotpassgo@v1.1.0
|
143
README.md
143
README.md
@ -1,99 +1,110 @@
|
|||||||
# mctl
|
# mctl
|
||||||
mctl is a terminal-friendly remote connection client
|
mctl is a terminal-friendly remote console client
|
||||||
|
|
||||||
|
# Index
|
||||||
|
1. [Installation](#installation)
|
||||||
|
2. [Use](#use)
|
||||||
|
4. [Security](#security)
|
||||||
|
5. [Development](#development)
|
||||||
|
|
||||||
## Installation
|
|
||||||
Install mctl using golang
|
|
||||||
```bash
|
|
||||||
go install code.jakeyoungdev.com/jake/mctl@main #it is recommended to use a tagged version
|
|
||||||
```
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Setup
|
# Installation
|
||||||
|
Install mctl using golang
|
||||||
|
```bash
|
||||||
|
go install code.jakeyoungdev.com/jake/mctl@main
|
||||||
|
```
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
# Use
|
||||||
### Configuring mctl
|
### Configuring mctl
|
||||||
mctl requires a one-time setup via the 'config' command before interacting with any servers, password is entered securely from the terminal
|
mctl requires a one-time setup via the 'init' command before using any other commands
|
||||||
```bash
|
```bash
|
||||||
mctl config -s <serveraddress> -p <rconport>
|
mctl init
|
||||||
```
|
```
|
||||||
|
|
||||||
### Connecting to server
|
<br />
|
||||||
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
|
|
||||||
|
|
||||||
|
### Add a server
|
||||||
|
To communicate with servers, they must be first added to the server list
|
||||||
```bash
|
```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
|
<br />
|
||||||
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
|
|
||||||
```
|
|
||||||
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:
|
### 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
|
||||||
### 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:
|
|
||||||
```bash
|
```bash
|
||||||
mctl save <name>
|
mctl server activate <name>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Viewing commands
|
<br />
|
||||||
Saved commands can be viewed with:
|
|
||||||
|
### Login to server
|
||||||
|
To login to the activated server and start sending commands
|
||||||
```bash
|
```bash
|
||||||
mctl view <name>
|
mctl login
|
||||||
```
|
```
|
||||||
|
The -s flag can be set to login to a non-activated server
|
||||||
### Running saved commands
|
|
||||||
Commands that have been saved can be run with:
|
|
||||||
```bash
|
```bash
|
||||||
mctl run <name>
|
mctl login -s <name>
|
||||||
```
|
```
|
||||||
If the saved command contains placeholders, the necessary arguments must be supplied:
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
### Savind a command
|
||||||
|
Commands can be saved to be run later
|
||||||
```bash
|
```bash
|
||||||
mctl run <name> 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
|
```bash
|
||||||
#saving command named "test" to run "tp %s 0 0 0"
|
mctl command run seed
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Delete saved command
|
Commands can contain placeholders to be filled in at runtime
|
||||||
Commands can be deleted with:
|
|
||||||
```bash
|
```bash
|
||||||
mctl delete <name>
|
#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 player
|
||||||
|
```
|
||||||
|
Running the placeholder command with the arg 'player' will run 'kill player' on the server
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
### 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
### CRUD Commands
|
||||||
### Commands
|
Other create, read, update, and delete versions of subcommands are available. Use the -h flag for more information
|
||||||
|Command|Description|
|
|
||||||
|---|---|
|
|
||||||
|config|used to update the config file|
|
|
||||||
|login|makes connection request to the server using saved configuration and enters command loop|
|
|
||||||
|save \<name>|saves specific command for reuse|
|
|
||||||
|view \<name>|displays saved command|
|
|
||||||
|delete \<name>|deletes saved command|
|
|
||||||
|run \<name> args...|runs saved command filling placeholders with supplied args|
|
|
||||||
|
|
||||||
### Flags
|
<br />
|
||||||
#### config
|
|
||||||
|Flag|Shorthand|Required|Description|
|
|
||||||
|---|---|---|---|
|
|
||||||
|port|p|yes|RCon port|
|
|
||||||
|server|s|yes|RCon address|
|
|
||||||
|
|
||||||
### Configuration file
|
# Security
|
||||||
All configuration data will be kept in /home/.mctl.yaml or C:\\Users\\username\\.mctl.yaml, passwords are encrypted for an added layer of 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.
|
||||||
|
|
||||||
## Security
|
mctl utilizes [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) and [gosec](https://github.com/securego/gosec) in workflows to ensure quality, secure code is being pushed. These workflow steps must pass before a PR will be accepted
|
||||||
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
|
|
||||||
|
|
||||||
## Development
|
<br />
|
||||||
this repo is currently in heavy development and may encounter breaking changes, use a tag to prevent any surprises
|
|
||||||
|
# Development
|
||||||
|
this repo is currently in development and may encounter breaking changes, use a tag to prevent any surprises
|
@ -1,20 +1,20 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.jakeyoungdev.com/jake/mctl/cryptography"
|
"code.jakeyoungdev.com/jake/mctl/database"
|
||||||
|
"code.jakeyoungdev.com/jake/mctl/model"
|
||||||
"github.com/jake-young-dev/mcr"
|
"github.com/jake-young-dev/mcr"
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This is a simple wrapper for the MCR client to provide easy use of mcr without having to manually
|
This is a simple wrapper for the MCR client to provide easy use of rcon
|
||||||
decrypt the password/hit viper each time.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
cli *mcr.Client
|
cli mcr.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type IClient interface {
|
type IClient interface {
|
||||||
@ -23,22 +23,35 @@ type IClient interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// creates a new mcr client using saved credentials and decrypted password
|
// creates a new mcr client using saved credentials and decrypted password
|
||||||
func New() (*Client, error) {
|
func New(name string) (*Client, error) {
|
||||||
//grab saved credentials
|
db, err := database.New()
|
||||||
server := viper.Get("server").(string)
|
if err != nil {
|
||||||
password := viper.Get("password").(string)
|
return nil, err
|
||||||
port := viper.Get("port").(int)
|
}
|
||||||
fmt.Printf("Logging into %s on port %d\n", server, port)
|
defer db.Close()
|
||||||
|
|
||||||
//decrypt password
|
var srv model.Server
|
||||||
pt, err := cryptography.DecryptPassword([]byte(password))
|
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)
|
||||||
|
|
||||||
|
p, err := base64.StdEncoding.DecodeString(srv.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//connect to game server
|
//connect to game server
|
||||||
cli := mcr.NewClient(server, mcr.WithPort(port))
|
cli := mcr.NewClient(srv.Server, mcr.WithPort(srv.Port))
|
||||||
err = cli.Connect(string(pt))
|
err = cli.Connect(string(p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
58
cmd/command/add.go
Normal file
58
cmd/command/add.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
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 command to the database",
|
||||||
|
SilenceUsage: true,
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == ErrInit {
|
||||||
|
fmt.Println(ErrInitRsp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
|
||||||
|
fmt.Println("Command saved")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
CommandCmd.AddCommand(addCmd)
|
||||||
|
}
|
23
cmd/command/command.go
Normal file
23
cmd/command/command.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2025 Jake jake.young.dev@gmail.com
|
||||||
|
*/
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandCmd is such a cool name lol
|
||||||
|
var CommandCmd = &cobra.Command{
|
||||||
|
Use: "command",
|
||||||
|
Example: "mctl command <subcommand>",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {}
|
45
cmd/command/delete.go
Normal file
45
cmd/command/delete.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2025 Jake jake.young.dev@gmail.com
|
||||||
|
*/
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.jakeyoungdev.com/jake/mctl/database"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var deleteCmd = &cobra.Command{
|
||||||
|
Use: "delete",
|
||||||
|
Example: "mctl command delete <name>",
|
||||||
|
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 != nil {
|
||||||
|
if err.Error() == ErrInit {
|
||||||
|
fmt.Println(ErrInitRsp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
|
||||||
|
fmt.Println("Command deleted")
|
||||||
|
},
|
||||||
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("name parameter missing")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
CommandCmd.AddCommand(deleteCmd)
|
||||||
|
}
|
80
cmd/command/run.go
Normal file
80
cmd/command/run.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
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 <server> <command> 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]
|
||||||
|
|
||||||
|
db, err := database.New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
crun, err := db.GetCmd(cname)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == ErrInit {
|
||||||
|
fmt.Println(ErrInitRsp)
|
||||||
|
return 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-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
42
cmd/command/view.go
Normal file
42
cmd/command/view.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
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 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 != nil {
|
||||||
|
if err.Error() == ErrInit {
|
||||||
|
fmt.Println(ErrInitRsp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
@ -1,97 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2025 Jake jake.young.dev@gmail.com
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"code.jakeyoungdev.com/jake/mctl/cryptography"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cfgserver string
|
|
||||||
cfgport int
|
|
||||||
)
|
|
||||||
|
|
||||||
// configCmd represents the config command
|
|
||||||
var configCmd = &cobra.Command{
|
|
||||||
Use: "config",
|
|
||||||
Example: "mctl config -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)
|
|
||||||
|
|
||||||
//generate and apply random nonce
|
|
||||||
nonce := make([]byte, 12)
|
|
||||||
_, err = io.ReadFull(rand.Reader, nonce)
|
|
||||||
cobra.CheckErr(err)
|
|
||||||
viper.Set("nonce", string(nonce))
|
|
||||||
|
|
||||||
//encrypt password
|
|
||||||
ciphert, err := cryptography.EncryptPassword(ps)
|
|
||||||
cobra.CheckErr(err)
|
|
||||||
|
|
||||||
//update config file with new values
|
|
||||||
viper.Set("server", cfgserver)
|
|
||||||
viper.Set("password", string(ciphert))
|
|
||||||
viper.Set("port", cfgport)
|
|
||||||
viper.WriteConfig()
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("Config file updated!")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initConfig()
|
|
||||||
configCmd.Flags().StringVarP(&cfgserver, "server", "s", "", "server address")
|
|
||||||
configCmd.MarkFlagRequired("server")
|
|
||||||
configCmd.Flags().IntVarP(&cfgport, "port", "p", 0, "server rcon port")
|
|
||||||
configCmd.MarkFlagRequired("port")
|
|
||||||
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()
|
|
||||||
viper.ReadInConfig()
|
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); 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))
|
|
||||||
viper.SafeWriteConfig()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2025 Jake jake.young.dev@gmail.com
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
// deleteCmd represents the delete command
|
|
||||||
var deleteCmd = &cobra.Command{
|
|
||||||
Use: "delete <name>",
|
|
||||||
Example: "mctl delete newcmd",
|
|
||||||
Short: "Delete a saved command",
|
|
||||||
Long: `Deletes a command stored using the save command`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
var cm map[string]any
|
|
||||||
cmdMap := viper.Get("customcmd")
|
|
||||||
if cmdMap == nil {
|
|
||||||
cm = make(map[string]any, 0)
|
|
||||||
} else {
|
|
||||||
cm = cmdMap.(map[string]any)
|
|
||||||
}
|
|
||||||
delete(cm, args[0])
|
|
||||||
viper.Set("customcmd", cmdMap)
|
|
||||||
viper.WriteConfig()
|
|
||||||
},
|
|
||||||
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(deleteCmd)
|
|
||||||
}
|
|
52
cmd/destroy.go
Normal file
52
cmd/destroy.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2025 Jake jake.young.dev@gmail.com
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.jakeyoungdev.com/jake/mctl/cmd/command"
|
||||||
|
"code.jakeyoungdev.com/jake/mctl/database"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// destroyCmd represents the destroy command
|
||||||
|
var destroyCmd = &cobra.Command{
|
||||||
|
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`,
|
||||||
|
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): ")
|
||||||
|
if scanner.Scan() {
|
||||||
|
if strings.EqualFold(scanner.Text(), "yes") {
|
||||||
|
db, err := database.New()
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
err = db.Destroy()
|
||||||
|
if err != nil {
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(destroyCmd)
|
||||||
|
}
|
28
cmd/init.go
Normal file
28
cmd/init.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
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",
|
||||||
|
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)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
err = db.Init()
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(initCmd)
|
||||||
|
}
|
29
cmd/login.go
29
cmd/login.go
@ -5,36 +5,37 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"code.jakeyoungdev.com/jake/mctl/client"
|
"code.jakeyoungdev.com/jake/mctl/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
server string
|
||||||
)
|
)
|
||||||
|
|
||||||
// loginCmd represents the login command
|
// loginCmd represents the login command
|
||||||
var loginCmd = &cobra.Command{
|
var loginCmd = &cobra.Command{
|
||||||
Use: "login",
|
Use: "login",
|
||||||
Example: "mctl login",
|
Example: "mctl login -s <server>",
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
Short: "Login to server and send commands",
|
Short: "Login to server and send commands",
|
||||||
Long: `Login to server using saved config and enter command loop
|
Long: `Login to default server and enter command loop. The default server
|
||||||
sending commands to server and printing the response.`,
|
is used unless the server flag is set`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
//grab saved credentials
|
cli, err := client.New(server)
|
||||||
server := viper.Get("server").(string)
|
|
||||||
cli, err := client.New()
|
|
||||||
cobra.CheckErr(err)
|
cobra.CheckErr(err)
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
//start command loop
|
//start command loop
|
||||||
fmt.Println("Connected! Type 'mctl' to close")
|
fmt.Println("Connected! Type 'mctl' to close")
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
|
||||||
var runningCmd string
|
var runningCmd string
|
||||||
for runningCmd != "mctl" {
|
for runningCmd != "mctl" {
|
||||||
fmt.Printf("RCON@%s /> ", server)
|
fmt.Printf("RCON /> ")
|
||||||
|
|
||||||
if scanner.Scan() {
|
if scanner.Scan() {
|
||||||
runningCmd = scanner.Text()
|
runningCmd = scanner.Text()
|
||||||
@ -54,17 +55,11 @@ var loginCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Disconnected from %s\n", server)
|
fmt.Println("Disconnected")
|
||||||
},
|
|
||||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
//ensure config command has been run
|
|
||||||
if viper.Get("server") == "" || viper.Get("password") == "" || viper.Get("port") == 0 {
|
|
||||||
return errors.New("the 'config' command must be run before you can interact with servers")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
loginCmd.Flags().StringVarP(&server, "server", "s", "", "server alias")
|
||||||
rootCmd.AddCommand(loginCmd)
|
rootCmd.AddCommand(loginCmd)
|
||||||
}
|
}
|
||||||
|
11
cmd/root.go
11
cmd/root.go
@ -6,6 +6,8 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"code.jakeyoungdev.com/jake/mctl/cmd/command"
|
||||||
|
srv "code.jakeyoungdev.com/jake/mctl/cmd/server"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,8 +16,7 @@ var rootCmd = &cobra.Command{
|
|||||||
Use: "mctl",
|
Use: "mctl",
|
||||||
Short: "A remote console client",
|
Short: "A remote console client",
|
||||||
Long: `mctl is a terminal-friendly remote console client made to manage game servers.`,
|
Long: `mctl is a terminal-friendly remote console client made to manage game servers.`,
|
||||||
Version: "v0.3.2",
|
Version: "v0.5.1",
|
||||||
// Run: func(cmd *cobra.Command, args []string) { },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
@ -28,5 +29,9 @@ func Execute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
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.
|
||||||
}
|
}
|
||||||
|
85
cmd/run.go
85
cmd/run.go
@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2025 Jake jake.young.dev@gmail.com
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.jakeyoungdev.com/jake/mctl/client"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
// runCmd represents the run command
|
|
||||||
var runCmd = &cobra.Command{
|
|
||||||
Use: "run <name> 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) {
|
|
||||||
//check for command map
|
|
||||||
var cm map[string]any
|
|
||||||
cmdMap := viper.Get("customcmd")
|
|
||||||
if cmdMap == nil {
|
|
||||||
cm = make(map[string]any, 0)
|
|
||||||
} else {
|
|
||||||
cm = cmdMap.(map[string]any)
|
|
||||||
}
|
|
||||||
//is this an existing command
|
|
||||||
cmdRun, ok := cm[args[0]]
|
|
||||||
if !ok {
|
|
||||||
fmt.Printf("command %s not found", args[0])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//convert arguments to interface
|
|
||||||
var nargs []any
|
|
||||||
for _, a := range args[1:] {
|
|
||||||
nargs = append(nargs, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
//inject arguments
|
|
||||||
fixed := fmt.Sprintf(cmdRun.(string), 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 configuration has been setup
|
|
||||||
if viper.Get("server") == "" || viper.Get("password") == "" || viper.Get("port") == 0 {
|
|
||||||
return errors.New("the 'config' command must be run before you can interact with servers")
|
|
||||||
}
|
|
||||||
|
|
||||||
//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)
|
|
||||||
}
|
|
55
cmd/save.go
55
cmd/save.go
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2025 Jake jake.young.dev@gmail.com
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
// saveCmd represents the save command
|
|
||||||
var saveCmd = &cobra.Command{
|
|
||||||
Use: "save <name>",
|
|
||||||
Example: "mctl save newcmd",
|
|
||||||
Short: "Saves a server command under an alias for quick execution",
|
|
||||||
Long: `Saves supplied command using alias <name> 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("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)
|
|
||||||
viper.WriteConfig()
|
|
||||||
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)
|
|
||||||
}
|
|
45
cmd/server/active.go
Normal file
45
cmd/server/active.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2025 Jake jake.young.dev@gmail.com
|
||||||
|
*/
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.jakeyoungdev.com/jake/mctl/database"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var activeCmd = &cobra.Command{
|
||||||
|
Use: "activate",
|
||||||
|
Example: "mctl server active <server>",
|
||||||
|
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 != nil {
|
||||||
|
if err.Error() == ErrInit {
|
||||||
|
fmt.Println(ErrInitRsp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
|
||||||
|
fmt.Println("Active server updated")
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
80
cmd/server/add.go
Normal file
80
cmd/server/add.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
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 to the database.`,
|
||||||
|
SilenceUsage: true,
|
||||||
|
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)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == ErrInit {
|
||||||
|
fmt.Println(ErrInitRsp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cobra.CheckErr(err)
|
||||||
|
|
||||||
|
fmt.Println("Server saved")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ServerCmd.AddCommand(addCmd)
|
||||||
|
}
|
45
cmd/server/delete.go
Normal file
45
cmd/server/delete.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2025 Jake jake.young.dev@gmail.com
|
||||||
|
*/
|
||||||
|
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 <server>",
|
||||||
|
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 != nil {
|
||||||
|
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 {
|
||||||
|
return errors.New("server parameter missing")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ServerCmd.AddCommand(deleteCmd)
|
||||||
|
}
|
22
cmd/server/server.go
Normal file
22
cmd/server/server.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2025 Jake jake.young.dev@gmail.com
|
||||||
|
*/
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ServerCmd = &cobra.Command{
|
||||||
|
Use: "server",
|
||||||
|
Example: "mctl server <subcommand>",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {}
|
54
cmd/server/update.go
Normal file
54
cmd/server/update.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
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 <name>",
|
||||||
|
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: ")
|
||||||
|
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))
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == ErrInit {
|
||||||
|
fmt.Println(ErrInitRsp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
45
cmd/server/view.go
Normal file
45
cmd/server/view.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
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 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 != nil {
|
||||||
|
if err.Error() == ErrInit {
|
||||||
|
fmt.Println(ErrInitRsp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.Println("Password: [REDACTED]")
|
||||||
|
fmt.Printf("Default: %t\n", s.Active)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ServerCmd.AddCommand(viewCmd)
|
||||||
|
}
|
48
cmd/view.go
48
cmd/view.go
@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright © 2025 Jake jake.young.dev@gmail.com
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
// viewCmd represents the view command
|
|
||||||
var viewCmd = &cobra.Command{
|
|
||||||
Use: "view <name>",
|
|
||||||
Example: "mctl view test",
|
|
||||||
Short: "View saved commands",
|
|
||||||
Long: `Load command using the supplied name and displays it in the terminal`,
|
|
||||||
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)
|
|
||||||
custom, ok := cm[args[0]]
|
|
||||||
if !ok {
|
|
||||||
fmt.Println("command not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Command: %s\n", custom.(string))
|
|
||||||
},
|
|
||||||
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)
|
|
||||||
}
|
|
@ -1,49 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
ct := aesg.Seal(nil, []byte(nonce), []byte(b), nil)
|
|
||||||
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
|
|
||||||
}
|
|
365
database/sqlx.go
Normal file
365
database/sqlx.go
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.jakeyoungdev.com/jake/mctl/model"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/ncruces/go-sqlite3/driver"
|
||||||
|
_ "github.com/ncruces/go-sqlite3/embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
all sqlx methods for CRUD functionalities of commands and servers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const (
|
||||||
|
DB_TIMEOUT = time.Second * 10
|
||||||
|
)
|
||||||
|
|
||||||
|
type database struct {
|
||||||
|
*sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type Database interface {
|
||||||
|
Init() error
|
||||||
|
Destroy() error
|
||||||
|
Close() error
|
||||||
|
//internals
|
||||||
|
timeout() (context.Context, context.CancelFunc)
|
||||||
|
//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)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db, err := sqlx.Open("sqlite3", fmt.Sprintf("file:%s/.mctl", home))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &database{
|
||||||
|
db,
|
||||||
|
}, 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 INTEGER,
|
||||||
|
active INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
`
|
||||||
|
// _, err := d.exec(query)
|
||||||
|
ctx, cl := d.timeout()
|
||||||
|
defer cl()
|
||||||
|
_, err := d.ExecContext(ctx, query)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// drops commands and servers tables
|
||||||
|
func (d *database) Destroy() error {
|
||||||
|
query := `
|
||||||
|
DROP TABLE commands;
|
||||||
|
DROP TABLE servers;
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx, cl := d.timeout()
|
||||||
|
defer cl()
|
||||||
|
_, err := d.ExecContext(ctx, query)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) Close() error {
|
||||||
|
return d.DB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) timeout() (context.Context, context.CancelFunc) {
|
||||||
|
return context.WithTimeout(context.Background(), DB_TIMEOUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets command using name
|
||||||
|
func (d *database) GetCmd(name string) (string, error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
command
|
||||||
|
FROM
|
||||||
|
commands
|
||||||
|
WHERE
|
||||||
|
name = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
var cmd string
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets all saved commands
|
||||||
|
func (d *database) GetAllCmds() ([]model.Command, error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
name,
|
||||||
|
command
|
||||||
|
FROM
|
||||||
|
commands
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx, cancel := d.timeout()
|
||||||
|
defer cancel()
|
||||||
|
rows, err := d.QueryxContext(ctx, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// save a new command
|
||||||
|
func (d *database) SaveCmd(name, cmd string) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO commands(name, command)
|
||||||
|
VALUES(?, ?)
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx, cancel := d.timeout()
|
||||||
|
defer cancel()
|
||||||
|
_, err := d.ExecContext(ctx, query, name, cmd)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO WE NEED THIS?
|
||||||
|
func (d *database) UpdateCmd(name, cmd string) error {
|
||||||
|
query := `
|
||||||
|
UPDATE
|
||||||
|
commands
|
||||||
|
SET
|
||||||
|
cmd = ?
|
||||||
|
WHERE
|
||||||
|
name = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx, cancel := d.timeout()
|
||||||
|
defer cancel()
|
||||||
|
_, err := d.ExecContext(ctx, query, cmd, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) DeleteCmd(name string) error {
|
||||||
|
query := `
|
||||||
|
DELETE FROM commands
|
||||||
|
WHERE name = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx, cancel := d.timeout()
|
||||||
|
defer cancel()
|
||||||
|
_, err := d.ExecContext(ctx, query, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) GetServer(name string) (model.Server, error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
name,
|
||||||
|
server,
|
||||||
|
password,
|
||||||
|
port,
|
||||||
|
active
|
||||||
|
FROM
|
||||||
|
servers
|
||||||
|
WHERE
|
||||||
|
name = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx, cancel := d.timeout()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) GetActiveServer() (model.Server, error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
name,
|
||||||
|
server,
|
||||||
|
password,
|
||||||
|
port,
|
||||||
|
active
|
||||||
|
FROM
|
||||||
|
servers
|
||||||
|
WHERE
|
||||||
|
active = 1
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx, cancel := d.timeout()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) GetAllServers() ([]model.Server, error) {
|
||||||
|
query := `
|
||||||
|
SELECT
|
||||||
|
name,
|
||||||
|
server,
|
||||||
|
password,
|
||||||
|
port,
|
||||||
|
active
|
||||||
|
FROM
|
||||||
|
servers
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx, cancel := d.timeout()
|
||||||
|
defer cancel()
|
||||||
|
rows, err := d.QueryxContext(ctx, 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(?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
|
||||||
|
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 {
|
||||||
|
clear := `
|
||||||
|
UPDATE
|
||||||
|
servers
|
||||||
|
SET
|
||||||
|
active = 0
|
||||||
|
`
|
||||||
|
|
||||||
|
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, name)
|
||||||
|
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 = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx, cancel := d.timeout()
|
||||||
|
defer cancel()
|
||||||
|
_, err := d.ExecContext(ctx, query, password, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *database) DeleteServer(name string) error {
|
||||||
|
query := `
|
||||||
|
DELETE FROM servers
|
||||||
|
WHERE
|
||||||
|
name = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
ctx, cancel := d.timeout()
|
||||||
|
defer cancel()
|
||||||
|
_, err := d.ExecContext(ctx, query, name)
|
||||||
|
return err
|
||||||
|
}
|
23
go.mod
23
go.mod
@ -1,28 +1,19 @@
|
|||||||
module code.jakeyoungdev.com/jake/mctl
|
module code.jakeyoungdev.com/jake/mctl
|
||||||
|
|
||||||
go 1.24.0
|
go 1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jake-young-dev/mcr v1.3.1
|
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/cobra v1.9.1
|
||||||
github.com/spf13/viper v1.20.1
|
|
||||||
golang.org/x/term v0.31.0
|
golang.org/x/term v0.31.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/ncruces/julianday v1.0.0 // 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/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
|
golang.org/x/sys v0.33.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
|
|
||||||
)
|
)
|
||||||
|
72
go.sum
72
go.sum
@ -1,62 +1,34 @@
|
|||||||
|
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/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/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
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.4.0 h1:cXZImkfI8aNIiVPrONE6qP+nfblTGsD2iXpPKTcA25U=
|
||||||
github.com/jake-young-dev/mcr v1.3.1/go.mod h1:74yZHGf9h3tLUDUpInA17grKLrNp9lVesWvisCFCXKY=
|
github.com/jake-young-dev/mcr v1.4.0/go.mod h1:74yZHGf9h3tLUDUpInA17grKLrNp9lVesWvisCFCXKY=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/ncruces/go-sqlite3 v0.26.1 h1:lBXmbmucH1Bsj57NUQR6T84UoMN7jnNImhF+ibEITJU=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/ncruces/go-sqlite3 v0.26.1/go.mod h1:XFTPtFIo1DmGCh+XVP8KGn9b/o2f+z0WZuT09x2N6eo=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
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 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
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 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
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 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
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 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=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
14
model/data.go
Normal file
14
model/data.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
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"`
|
||||||
|
Active bool `db:"active"`
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user