Initial Commit

This commit is contained in:
2021-11-11 16:13:40 -08:00
commit 0645bc876c
28 changed files with 3363 additions and 0 deletions

31
cmd/lassoctl/cmd/file.go Normal file
View File

@@ -0,0 +1,31 @@
/*
Copyright © 2021 Arsen Musayelyan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"github.com/spf13/cobra"
)
// fileCmd represents the file command
var fileCmd = &cobra.Command{
Use: "file",
Short: "Send and retrieve files from a lasso node",
}
func init() {
rootCmd.AddCommand(fileCmd)
}

41
cmd/lassoctl/cmd/list.go Normal file
View File

@@ -0,0 +1,41 @@
/*
Copyright © 2021 Arsen Musayelyan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// listCmd represents the list command
var listCmd = &cobra.Command{
Use: "list",
Short: "Display a list of available nodes on the server",
Aliases: []string{"ls"},
Run: func(cmd *cobra.Command, args []string) {
nodes := getNodeList()
for name, node := range nodes {
fmt.Println(name+":", node.IP)
}
},
}
func init() {
rootCmd.AddCommand(listCmd)
}

361
cmd/lassoctl/cmd/mssh.go Normal file
View File

@@ -0,0 +1,361 @@
/*
Copyright © 2021 NAME HERE <EMAIL ADDRESS>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"bufio"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/abiosoft/ishell"
"github.com/melbahja/goph"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// msshCmd represents the mssh command
var msshCmd = &cobra.Command{
Use: "mssh <user@node...>",
Short: "Make an SSH connection to multiple nodes simultaneously",
Run: func(cmd *cobra.Command, args []string) {
// Create new shell
shell := ishell.New()
var password string
// If password prompt requested
if viper.GetBool("password") {
// Print prompt
shell.Print("Password: ")
// Read password into variable
password = shell.ReadPassword()
}
// Get node list from server
nodes := getNodeList()
// Create new goph auth
var auth goph.Auth
// If password prompt requested
if viper.GetBool("password") {
// Add password method to auth
auth = append(auth, goph.Password(password)...)
}
// If identity given
if viper.GetString("identity") != "" {
// Get identity as ssh key
keyAuth, err := goph.Key(viper.GetString("identity"), "")
if err != nil {
log.Fatal().Err(err).Msg("Error getting SSH key:")
}
// Add key to auth
auth = append(auth, keyAuth...)
}
// Get all identities in default location
keys, err := getAllIdentities()
if err != nil {
log.Fatal().Msg("Error getting SSH identities")
}
// Add keys to auth
auth = append(auth, keys...)
// If ssh agent exists
if goph.HasAgent() {
// Get ssh agent
agent, err := goph.UseAgent()
if err != nil {
log.Fatal().Msg("Error getting SSH agent")
}
// Add ssh agent to auth
auth = append(auth, agent...)
}
// Create map to store goph clients
clients := map[string]*goph.Client{}
// For every device
for _, device := range args {
// Split device by "@"
splitArg := strings.Split(device, "@")
// If split device has less than two elements, it is invalid
if len(splitArg) != 2 {
log.Fatal().Msg("Invalid username/node argument")
}
// Get variables from split device
username, nodeName := splitArg[0], splitArg[1]
// Get node from list if it exists
node, ok := nodes[nodeName]
if !ok {
log.Fatal().Str("node", nodeName).Msg("Node does not exist on the server")
}
// Connect to node without verifying known hosts
client, err := goph.NewUnknown(username, node.IP, auth)
if err != nil {
log.Fatal().Err(err).Msg("Error connecting to node")
}
// Add device to client list
clients[device] = client
}
// Clode all clients at the end of the function
defer closeClients(clients)
// Add run command to shell
shell.AddCmd(&ishell.Cmd{
Name: "run",
Help: "Run a shell command on all nodes simultaneously.",
Func: func(c *ishell.Context) {
// Create new wait group
wg := &sync.WaitGroup{}
// For every client
for name, client := range clients {
// Add goroutine to wait group
wg.Add(1)
go func(client *goph.Client, name string) {
// Remove from wait group at the end of the function
defer wg.Done()
// Create command from given arguments
cmd, err := client.Command(c.Args[0], c.Args[1:]...)
if err != nil {
cmdError(shell, name, err)
return
}
// Get command Stdout pipe
stdout, err := cmd.StdoutPipe()
if err != nil {
cmdError(shell, name, err)
return
}
// Get command Stderr pipe
stderr, err := cmd.StderrPipe()
if err != nil {
cmdError(shell, name, err)
return
}
// Combine Stdout and Stderr
combined := io.MultiReader(stdout, stderr)
// Start command without waiting for it to finish
if err := cmd.Start(); err != nil {
cmdError(shell, name, err)
return
}
// Create new scanner for combined output
scanner := bufio.NewScanner(combined)
for scanner.Scan() {
// Print command output
cmdOut(shell, name, scanner.Text())
}
}(client, name)
}
// Wait for all goroutines to complete
wg.Wait()
},
})
// Create file command
file := &ishell.Cmd{
Name: "file",
Help: "Transfer files to/from nodes",
}
// Add send command to file command
file.AddCmd(&ishell.Cmd{
Name: "send",
Help: "Send a file to all nodes. '~' will expand to device's home directory.",
Func: func(c *ishell.Context) {
// Get local and remote paths
localPath := c.Args[0]
remotePath := c.Args[1]
// Create new wait group
wg := &sync.WaitGroup{}
// For every client
for name, client := range clients {
// Set new remote to remote path
newRemote := remotePath
// If new remote starts with "~"
if strings.HasPrefix(newRemote, "~") {
// Replace ~ with "/home/user"
newRemote = filepath.Join(
"/home/"+client.User(),
strings.TrimPrefix(newRemote, "~"),
)
}
// Add one goroutine to wait group
wg.Add(1)
go func(client *goph.Client, name string) {
// Remove from wait group at the end of the function
defer wg.Done()
// Upload file to remote
err := client.Upload(localPath, newRemote)
if err != nil {
shell.Printf("%s [error]: %v\n", name, err)
return
}
// Print success message
cmdSuccess(shell, name, "upload complete")
}(client, name)
// Wait for all added goroutines to complete
wg.Wait()
}
},
})
// Add retrieve command to shell
file.AddCmd(&ishell.Cmd{
Name: "retrieve",
Aliases: []string{"retr", "recv", "get"},
Help: "Retrieve file from all nodes. File will be saved as 'file-user@node.ext'",
Func: func(c *ishell.Context) {
// Get remote and local paths
remotePath := c.Args[0]
localPath := c.Args[1]
// Create new wait group
wg := &sync.WaitGroup{}
// For every client
for name, client := range clients {
// Get extension, base, and directory of local path
localExt := filepath.Ext(localPath)
localBase := filepath.Base(localPath)
localDir := filepath.Dir(localPath)
// Attempt to stat local path
info, err := os.Stat(localPath)
if err != nil {
log.Fatal().Err(err).Msg("Error getting file info")
}
// If local path is a directory
if info.IsDir() {
// Set local directory to local path
localDir = localPath
// Set local extention to remote extension
localExt = filepath.Ext(remotePath)
// Set local base to remote base
localBase = filepath.Base(remotePath)
}
// Remove extension from local base
localBaseNoExt := strings.TrimSuffix(localBase, localExt)
// Create new local, formatted as "filename-user@node.extension"
newLocal := filepath.Join(localDir, localBaseNoExt+"-"+name+localExt)
// Set new remote to remote path
newRemote := remotePath
// If new remote starts with "~"
if strings.HasPrefix(newRemote, "~") {
// Replace ~ with "/home/user"
newRemote = filepath.Join(
"/home/"+client.User(),
strings.TrimPrefix(newRemote, "~"),
)
}
// Add goroutine to wait group
wg.Add(1)
go func(client *goph.Client, name string) {
// Remove from wait group at end of function
defer wg.Done()
// Download file from remote
err := client.Download(newRemote, newLocal)
if err != nil {
cmdError(shell, name, err)
return
}
// Print success message
cmdSuccess(shell, name, "download complete")
}(client, name)
// Wait for all added goroutines to complete
wg.Wait()
}
},
})
// Add file command to shell
shell.AddCmd(file)
// Run shell
shell.Run()
},
}
func init() {
rootCmd.AddCommand(msshCmd)
msshCmd.Flags().BoolP("password", "p", false, "Prompt for SSH password on start")
msshCmd.Flags().StringP("identity", "i", "", "SSH identity file to use")
viper.BindPFlags(msshCmd.Flags())
}
func getAllIdentities() (goph.Auth, error) {
// Get user's home directory
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
// Get all ssh keys in "~/.ssh"
matches, err := filepath.Glob(filepath.Join(home, ".ssh", "id_*.pub"))
if err != nil {
return nil, err
}
// Create goph auth for keys
var out goph.Auth
// For every glob match
for _, match := range matches {
// Get path of private key
privKeyPath := strings.TrimSuffix(match, ".pub")
// Get SSH key as goph ket
auth, err := goph.Key(privKeyPath, "")
if err != nil {
return out, err
}
// Add goph key to auth
out = append(out, auth...)
}
return out, nil
}
func closeClients(clients map[string]*goph.Client) {
// For every client
for _, client := range clients {
// Close client
client.Close()
}
}

74
cmd/lassoctl/cmd/proxy.go Normal file
View File

@@ -0,0 +1,74 @@
/*
Copyright © 2021 Arsen Musayelyan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"os"
"os/exec"
"strings"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
// proxyCmd represents the proxy command
var proxyCmd = &cobra.Command{
Use: "proxy [flags] <user@node> <local-port:host:remote-port>",
Short: "Proxy a particular port from a node to a local port",
Example: "proxy pi@raspberrypi 8080:localhost:80",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 {
cmd.Usage()
os.Exit(1)
}
// Get node list from server
nodes := getNodeList()
// Split first argument by "@"
splitArg := strings.Split(args[0], "@")
// If less than two elements, argumen is invalid
if len(splitArg) != 2 {
log.Fatal().Msg("Invalid username/node argument")
}
// Get variables from split argument
username, nodeName := splitArg[0], splitArg[1]
// Get node from list if it exists
node, ok := nodes[nodeName]
if !ok {
log.Fatal().Str("node", nodeName).Msg("Node does not exist on the server")
}
// Create ssh command
ssh := exec.Command("ssh", username+"@"+node.IP, "-L", args[1], "-N")
ssh.Stdin = os.Stdin
ssh.Stdout = os.Stdout
ssh.Stderr = os.Stderr
// Run ssh command
if err := ssh.Run(); err != nil {
log.Fatal().Err(err).Msg("Error received from ssh command")
}
},
}
func init() {
rootCmd.AddCommand(proxyCmd)
}

View File

@@ -0,0 +1,95 @@
/*
Copyright © 2021 Arsen Musayelyan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"os"
"os/exec"
"regexp"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
// retrieveCmd represents the retrieve command
var retrieveCmd = &cobra.Command{
Use: "retrieve",
Short: "A brief description of your command",
Aliases: []string{"retr", "recv", "get"},
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 {
cmd.Usage()
os.Exit(1)
}
// Get list of nodes from server
nodes := getNodeList()
// Compile regular expression for target argument
regex := regexp.MustCompile(`(.+)@(.+):(.+)`)
var sources []string
// For every argument
for index, arg := range args {
// Stop if last argument
if index == len(args)-1 {
break
}
// Find submatches in current argument
submatches := regex.FindStringSubmatch(arg)
// If no submatches found
if submatches == nil {
log.Fatal().Msg("Invalid target argument")
}
// Get variables from submatches
username, nodeName, path := submatches[1], submatches[2], submatches[3]
// Get node from list if it exists
node, ok := nodes[nodeName]
if !ok {
log.Fatal().Str("node", nodeName).Msg("Node does not exist on the server")
}
// Add new source to sources slice
sources = append(sources, username+"@"+node.IP+":"+path)
}
// Get last argument
file := args[len(args)-1]
// Create arguments for scp command
var scpArgs []string
scpArgs = append(scpArgs, sources...)
scpArgs = append(scpArgs, file)
// Create scp command
scp := exec.Command("scp", scpArgs...)
scp.Stdin = os.Stdin
scp.Stdout = os.Stdout
scp.Stderr = os.Stderr
// Run scp command
if err := scp.Run(); err != nil {
log.Fatal().Err(err).Msg("Error received from scp command")
}
},
}
func init() {
fileCmd.AddCommand(retrieveCmd)
}

126
cmd/lassoctl/cmd/root.go Normal file
View File

@@ -0,0 +1,126 @@
/*
Copyright © 2021 Arsen Musayelyan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"net"
"net/http"
"os"
"github.com/abiosoft/ishell"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/vmihailenco/msgpack/v5"
"go.arsenm.dev/lasso/internal/types"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "lassoctl",
Short: "Manage devices using IPs retrieved from a lasso server",
}
// 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() {
cobra.CheckErr(rootCmd.Execute())
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "Config file to use")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)
// Search config in home directory with name ".lassoctl" (without extension).
viper.AddConfigPath(".")
viper.AddConfigPath(home)
viper.SetConfigType("toml")
viper.SetConfigName("lassoctl")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
viper.ReadInConfig()
}
// getNodeList gets a list of nodes from the lasso server
func getNodeList() map[string]types.Node {
// Get server address
addr := net.JoinHostPort(viper.GetString("server.addr"), viper.GetString("server.port"))
// Create base url for HTTPS server
baseURL := "https://" + addr
// Get node list
res, err := http.Get(baseURL + "/node/list")
if err != nil {
log.Fatal().Err(err).Msg("Error getting node list from server")
}
var resp types.Response
// Decode server response
err = msgpack.NewDecoder(res.Body).Decode(&resp)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding server response")
}
// If server response is an error
if resp.Error {
log.Fatal().Str("error", resp.Message).Msg("Server returned error")
}
var nodes map[string]types.Node
// Decode response data as node map
err = mapstructure.Decode(resp.Data, &nodes)
if err != nil {
log.Fatal().Err(err).Msg("Error decoding nodes map")
}
// Return node map
return nodes
}
// cmdError prints an error while running mssh
func cmdError(shell *ishell.Shell, node string, err error) {
shell.Printf("%s [error]: %v\n", node, err)
}
// cmdOut prints output from a command while running mssh
func cmdOut(shell *ishell.Shell, node, out string) {
shell.Printf("%s [out]: %s\n", node, out)
}
// cmdSucces prints a success message while running mssh
func cmdSuccess(shell *ishell.Shell, node, msg string) {
shell.Printf("%s [success]: %s\n", node, msg)
}

81
cmd/lassoctl/cmd/send.go Normal file
View File

@@ -0,0 +1,81 @@
/*
Copyright © 2021 Arsen Musayelyan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"os"
"os/exec"
"regexp"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
// sendCmd represents the send command
var sendCmd = &cobra.Command{
Use: "send <file...> <username@node:path>",
Short: "Send a file to the node via ssh",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 {
cmd.Usage()
os.Exit(1)
}
// Get list of nodes from server
nodes := getNodeList()
// Compile regular expression for target argument
regex := regexp.MustCompile(`(.+)@(.+):(.+)`)
// Find submatches within last argument
submatches := regex.FindStringSubmatch(args[len(args)-1])
// If no submatches found
if submatches == nil {
log.Fatal().Msg("Invalid target argument")
}
// Files are all arguments except last one
files := args[0 : len(args)-1]
// Get variables from submatches
username, nodeName, path := submatches[1], submatches[2], submatches[3]
// Get node from list if it exists
node, ok := nodes[nodeName]
if !ok {
log.Fatal().Str("node", nodeName).Msg("Node does not exist on the server")
}
// Create arguments for scp command
var scpArgs []string
scpArgs = append(scpArgs, files...)
scpArgs = append(scpArgs, username+"@"+node.IP+":"+path)
// Create scp command
scp := exec.Command("scp", scpArgs...)
scp.Stdin = os.Stdin
scp.Stdout = os.Stdout
scp.Stderr = os.Stderr
// Run scp command
if err := scp.Run(); err != nil {
log.Fatal().Err(err).Msg("Error received from scp command")
}
},
}
func init() {
fileCmd.AddCommand(sendCmd)
}

72
cmd/lassoctl/cmd/ssh.go Normal file
View File

@@ -0,0 +1,72 @@
/*
Copyright © 2021 Arsen Musayelyan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"os"
"os/exec"
"strings"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
// sshCmd represents the ssh command
var sshCmd = &cobra.Command{
Use: "ssh <user@node>",
Short: "Make an ssh connection to the specified node",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
cmd.Usage()
os.Exit(1)
}
// Get list of nodes from server
nodes := getNodeList()
// Split first argument by "@"
splitArg := strings.Split(args[0], "@")
// If split string has less than two elements, argument is invalid
if len(splitArg) != 2 {
log.Fatal().Msg("Invalid username/node argument")
}
// Get variables from split string
username, nodeName := splitArg[0], splitArg[1]
// Get node from list if it exists
node, ok := nodes[nodeName]
if !ok {
log.Fatal().Str("node", nodeName).Msg("Node does not exist on the server")
}
// Create SSH command using node IP
ssh := exec.Command("ssh", username+"@"+node.IP)
ssh.Stdin = os.Stdin
ssh.Stdout = os.Stdout
ssh.Stderr = os.Stderr
// Attempt to run command
if err := ssh.Run(); err != nil {
log.Fatal().Err(err).Msg("Error received from ssh command")
}
},
}
func init() {
rootCmd.AddCommand(sshCmd)
}

37
cmd/lassoctl/main.go Normal file
View File

@@ -0,0 +1,37 @@
/*
Copyright © 2021 Arsen Musayelyan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"crypto/tls"
"net/http"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.arsenm.dev/lasso/cmd/lassoctl/cmd"
)
func init() {
// Disable certificate verification as server uses self-signed key
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
func main() {
cmd.Execute()
}