Initial Commit
This commit is contained in:
222
cmd/lasso/cmd/client.go
Normal file
222
cmd/lasso/cmd/client.go
Normal file
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"go.arsenm.dev/lasso/internal/types"
|
||||
)
|
||||
|
||||
// clientCmd represents the client command
|
||||
var clientCmd = &cobra.Command{
|
||||
Use: "client",
|
||||
Short: "Start the lasso client",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Disable certificate verification as server uses self-signed key
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
|
||||
// Perform server status check
|
||||
res, err := http.Get(url("status"))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Server status check failed")
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
// Get node list from server
|
||||
res, err = http.Get(url("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 response body")
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
var nodes map[string]types.Node
|
||||
// Decode response data as node list
|
||||
err = mapstructure.Decode(resp.Data, &nodes)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error decoding response")
|
||||
}
|
||||
|
||||
// Get local IP
|
||||
ip, err := localIP()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting local IP")
|
||||
}
|
||||
|
||||
// Attempt to get node from list
|
||||
node, ok := nodes[viper.GetString("node.name")]
|
||||
// If node does not exist
|
||||
if !ok {
|
||||
// Encode node as msgpack
|
||||
data, err := msgpack.Marshal(types.Node{IP: ip})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error encoding new node")
|
||||
}
|
||||
// Send node to server
|
||||
res, err = http.Post(url("node", viper.GetString("node.name")), "text/plain", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
log.Fatal().Msg("Error adding new node to server")
|
||||
}
|
||||
var resp types.Response
|
||||
// Decode server response
|
||||
err = msgpack.NewDecoder(res.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
log.Fatal().Msg("Error decoding server response")
|
||||
}
|
||||
// If server returned error
|
||||
if resp.Error {
|
||||
log.Fatal().Str("error", resp.Message).Msg("Error returned by server")
|
||||
}
|
||||
// Set new node IP
|
||||
node.IP = ip
|
||||
// Set node ID in viper
|
||||
viper.Set("node.id", resp.Data)
|
||||
// Attempt to write new ID to config
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error writing new ID to config")
|
||||
}
|
||||
}
|
||||
|
||||
// If IP does not match current
|
||||
if node.IP != ip {
|
||||
// Encode new node as msgpack
|
||||
data, err := msgpack.Marshal(types.Node{
|
||||
IP: ip,
|
||||
ID: viper.GetString("node.id"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error encoding updated node")
|
||||
}
|
||||
// Create new PATCH request with new node as data
|
||||
req, err := http.NewRequest(
|
||||
http.MethodPatch,
|
||||
url("node", viper.GetString("node.name")),
|
||||
bytes.NewReader(data),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error creating PATCH request")
|
||||
}
|
||||
// Perform request
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error sending update request to server")
|
||||
}
|
||||
var resp types.Response
|
||||
// Decode server response
|
||||
err = msgpack.NewDecoder(res.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
log.Fatal().Msg("Error decoding server response")
|
||||
}
|
||||
// If server returned error
|
||||
if resp.Error {
|
||||
log.Fatal().Str("error", resp.Message).Msg("Error returned by server")
|
||||
}
|
||||
}
|
||||
|
||||
// Every minute
|
||||
for range time.Tick(time.Minute) {
|
||||
// Get local IP
|
||||
newIP, err := localIP()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error getting new local IP")
|
||||
continue
|
||||
}
|
||||
// If IP has changed since last check
|
||||
if newIP != ip {
|
||||
// Set IP to new IP
|
||||
ip = newIP
|
||||
// Encode new node as msgpack
|
||||
data, err := msgpack.Marshal(types.Node{
|
||||
IP: newIP,
|
||||
ID: viper.GetString("node.id"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error encoding updated node")
|
||||
continue
|
||||
}
|
||||
// Create new PATCH request with new node as data
|
||||
req, err := http.NewRequest(
|
||||
http.MethodPatch,
|
||||
url("node", viper.GetString("node.name")),
|
||||
bytes.NewReader(data),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error creating PATCH request")
|
||||
}
|
||||
// Perform request
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error sending update request to server")
|
||||
}
|
||||
var resp types.Response
|
||||
// Decode server response
|
||||
err = msgpack.NewDecoder(res.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
log.Fatal().Msg("Error decoding server response")
|
||||
}
|
||||
// If server returned error
|
||||
if resp.Error {
|
||||
log.Fatal().Str("error", resp.Message).Msg("Error returned by server")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(clientCmd)
|
||||
}
|
||||
|
||||
// localIP returns the IP address of the default network interface
|
||||
func localIP() (string, error) {
|
||||
// Make UDP "connection" to nonexistant address
|
||||
conn, err := net.Dial("udp", "255.255.255.255:65535")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
// Get local address
|
||||
addr := conn.LocalAddr().String()
|
||||
// Get host from address
|
||||
host, _, _ := net.SplitHostPort(addr)
|
||||
return host, nil
|
||||
}
|
||||
|
||||
// url generates a URL for the given path on the server
|
||||
func url(path ...string) string {
|
||||
// Get server address with port
|
||||
serverAddr := net.JoinHostPort(viper.GetString("server.addr"), viper.GetString("server.port"))
|
||||
// Return HTTPS address
|
||||
return "https://" + serverAddr + "/" + strings.Join(path, "/")
|
||||
}
|
||||
124
cmd/lasso/cmd/gencert.go
Normal file
124
cmd/lasso/cmd/gencert.go
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
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 (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// gencertCmd represents the gencert command
|
||||
var gencertCmd = &cobra.Command{
|
||||
Use: "gencert <cert file> <key file>",
|
||||
Short: "Generate self-signed TLS certificate for master server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 2 {
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Generate an ed25519 key from rand reader
|
||||
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
|
||||
|
||||
// Get current time to use for cert creation time
|
||||
notBefore := time.Now()
|
||||
// Get expiration time 10 years from now
|
||||
notAfter := notBefore.Add(time.Hour * 24 * 365 * 10)
|
||||
|
||||
// Set limit for serial number
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
// Create random serial number
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to generate serial number")
|
||||
}
|
||||
|
||||
// Get local IP
|
||||
ip, err := localIP()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting local IP")
|
||||
}
|
||||
|
||||
// Create x509 certificate template
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Lasso"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
|
||||
IPAddresses: []net.IP{net.ParseIP(ip)},
|
||||
}
|
||||
|
||||
// Create certificate
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to create certificate")
|
||||
}
|
||||
|
||||
// Create certificate output file
|
||||
certOut, err := os.Create(args[0])
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to open cert file for writing")
|
||||
}
|
||||
// Enode certificate as PEM file
|
||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to write data to cert file")
|
||||
}
|
||||
// Close certificate file
|
||||
certOut.Close()
|
||||
log.Info().Str("path", args[0]).Msg("Wrote cert file")
|
||||
|
||||
// Create key output file
|
||||
keyOut, err := os.OpenFile(args[1], os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to open key file for writing")
|
||||
return
|
||||
}
|
||||
// Encode private key as PKCS8
|
||||
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Unable to marshal private key file")
|
||||
}
|
||||
// Encode private key as PEM
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
|
||||
log.Fatal().Err(err).Msg("Failed to write data to key file")
|
||||
}
|
||||
// Close key file
|
||||
keyOut.Close()
|
||||
log.Info().Str("path", args[1]).Msg("Wrote key file")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(gencertCmd)
|
||||
}
|
||||
89
cmd/lasso/cmd/root.go
Normal file
89
cmd/lasso/cmd/root.go
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
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"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"go.arsenm.dev/lasso/internal/types"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "lasso",
|
||||
Short: "A brief description of your application",
|
||||
}
|
||||
|
||||
// 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().StringVar(&cfgFile, "config", "", "Config file path")
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
// Search for config in config directory with name "lasso" (without extension).
|
||||
viper.AddConfigPath("/etc/lasso")
|
||||
viper.SetConfigType("toml")
|
||||
viper.SetConfigName("lasso")
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix("lasso")
|
||||
viper.AutomaticEnv() // Read in environment variables that match
|
||||
|
||||
// If a config file is found, read it in.
|
||||
viper.ReadInConfig()
|
||||
}
|
||||
|
||||
// httpError logs an error as well as writing it to the HTTP response
|
||||
func httpError(res http.ResponseWriter, statusCode int, err error, msg string) {
|
||||
// Write status code
|
||||
res.WriteHeader(statusCode)
|
||||
var message string
|
||||
if err != nil {
|
||||
// Get message string containing error string
|
||||
message = fmt.Sprintf("%s: %s", msg, err)
|
||||
// Log error
|
||||
log.Error().Err(err).Msg(msg)
|
||||
} else {
|
||||
// Set message string to given msg
|
||||
message = msg
|
||||
// Log message
|
||||
log.Error().Msg(msg)
|
||||
}
|
||||
// Encode error response to HTTP response
|
||||
msgpack.NewEncoder(res).Encode(types.Response{
|
||||
Error: true,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
227
cmd/lasso/cmd/server.go
Normal file
227
cmd/lasso/cmd/server.go
Normal file
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
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 (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"go.arsenm.dev/lasso/internal/logging"
|
||||
"go.arsenm.dev/lasso/internal/types"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAlreadyExists = errors.New("node already exists in database")
|
||||
ErrNoExists = errors.New("node does not exist in database")
|
||||
)
|
||||
|
||||
// serverCmd represents the server command
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Start the lasso master server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
// Open nodes database
|
||||
nodes, err := badger.Open(
|
||||
badger.DefaultOptions("/etc/lasso/nodes").
|
||||
WithLogger(logging.BadgerLogger{}),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error opening badger database")
|
||||
}
|
||||
// Close database at end of function
|
||||
defer nodes.Close()
|
||||
|
||||
// Create new router
|
||||
router := chi.NewMux()
|
||||
|
||||
router.Use(logging.ChiLogger)
|
||||
|
||||
// GET /status (Status check)
|
||||
router.Get("/status", func(res http.ResponseWriter, req *http.Request) {
|
||||
// Encode empty response to connection
|
||||
msgpack.NewEncoder(res).Encode(types.Response{})
|
||||
})
|
||||
|
||||
router.Route("/node", func(node chi.Router) {
|
||||
// POST /node/:name (Create new node and return id)
|
||||
node.Post("/{name}", func(res http.ResponseWriter, req *http.Request) {
|
||||
// Get name parameter from URL
|
||||
name := chi.URLParam(req, "name")
|
||||
|
||||
var nodeReq types.Node
|
||||
// Decode request body as a node
|
||||
err = msgpack.NewDecoder(req.Body).Decode(&nodeReq)
|
||||
if err != nil {
|
||||
httpError(res, http.StatusBadRequest, err, "Unable to decode request body")
|
||||
return
|
||||
}
|
||||
|
||||
// Create new UUID
|
||||
id := uuid.New().String()
|
||||
// Set node ID
|
||||
nodeReq.ID = id
|
||||
|
||||
// Update nodes database
|
||||
err = nodes.Update(func(txn *badger.Txn) error {
|
||||
// Attempt to get node from database
|
||||
item, err := txn.Get([]byte(name))
|
||||
// If error exists but item found
|
||||
if err != nil && err != badger.ErrKeyNotFound {
|
||||
return err
|
||||
} else if item != nil {
|
||||
return ErrAlreadyExists
|
||||
}
|
||||
|
||||
// Encode request node to msgpack
|
||||
data, _ := msgpack.Marshal(nodeReq)
|
||||
// Set new node in database and return error
|
||||
return txn.Set([]byte(name), data)
|
||||
})
|
||||
if err != nil {
|
||||
httpError(res, http.StatusInternalServerError, err, "Error adding node to database")
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response with id to connection
|
||||
msgpack.NewEncoder(res).Encode(types.Response{Data: id})
|
||||
})
|
||||
|
||||
// PATCH /node/:name (Update IP address of a node)
|
||||
node.Patch("/{name}", func(res http.ResponseWriter, req *http.Request) {
|
||||
// Get name parameter from URL
|
||||
name := chi.URLParam(req, "name")
|
||||
|
||||
var nodeReq types.Node
|
||||
// Decode request body as node
|
||||
err = msgpack.NewDecoder(req.Body).Decode(&nodeReq)
|
||||
if err != nil {
|
||||
httpError(res, http.StatusBadRequest, err, "Unable to decode request body")
|
||||
return
|
||||
}
|
||||
|
||||
var dbNode types.Node
|
||||
// View nodes database
|
||||
err = nodes.View(func(txn *badger.Txn) error {
|
||||
// Attempt to get node from database
|
||||
item, err := txn.Get([]byte(name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Decode node and return error
|
||||
return item.Value(func(val []byte) error {
|
||||
return msgpack.Unmarshal(val, &dbNode)
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
httpError(res, http.StatusBadRequest, err, "Unable to get node from database")
|
||||
return
|
||||
}
|
||||
|
||||
// If request and database IDs are not the same
|
||||
if nodeReq.ID != dbNode.ID {
|
||||
httpError(res, http.StatusForbidden, nil, "Incorrect UUID for specified node")
|
||||
return
|
||||
}
|
||||
|
||||
// Update nodes database
|
||||
err = nodes.Update(func(txn *badger.Txn) error {
|
||||
// Encode node sent in request
|
||||
data, err := msgpack.Marshal(nodeReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Set new node in database and return error
|
||||
return txn.Set([]byte(name), data)
|
||||
})
|
||||
if err != nil {
|
||||
httpError(res, http.StatusInternalServerError, err, "Error updating node in database")
|
||||
return
|
||||
}
|
||||
|
||||
// Encode response to connection
|
||||
msgpack.NewEncoder(res).Encode(types.Response{})
|
||||
})
|
||||
|
||||
// GET /node/list (Get list of all nodes)
|
||||
node.Get("/list", func(res http.ResponseWriter, req *http.Request) {
|
||||
// Create map of nodes for output
|
||||
out := map[string]types.Node{}
|
||||
|
||||
// View nodes database
|
||||
nodes.View(func(txn *badger.Txn) error {
|
||||
// Create new database iterator
|
||||
it := txn.NewIterator(badger.DefaultIteratorOptions)
|
||||
// Close iterator at end of function
|
||||
defer it.Close()
|
||||
for it.Rewind(); it.Valid(); it.Next() {
|
||||
// Get item from iterator
|
||||
item := it.Item()
|
||||
// Get key from item
|
||||
key := item.Key()
|
||||
var node types.Node
|
||||
// Get value from iterator
|
||||
err = item.Value(func(val []byte) error {
|
||||
// Decode value as node and return error
|
||||
return msgpack.Unmarshal(val, &node)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove node ID
|
||||
node.ID = ""
|
||||
// Set node in map
|
||||
out[string(key)] = node
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Encode map in response on connection
|
||||
msgpack.NewEncoder(res).Encode(types.Response{
|
||||
Data: out,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Get listen address for server from config
|
||||
addr := net.JoinHostPort(viper.GetString("server.addr"), viper.GetString("server.port"))
|
||||
// Log HTTPS server starting
|
||||
log.Info().Str("addr", addr).Msg("Starting HTTPS server")
|
||||
// Start HTTPS server using certificate paths from config
|
||||
err = http.ListenAndServeTLS(
|
||||
addr,
|
||||
viper.GetString("server.tls.cert"),
|
||||
viper.GetString("server.tls.key"),
|
||||
router,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error while running server")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serverCmd)
|
||||
}
|
||||
33
cmd/lasso/main.go
Normal file
33
cmd/lasso/main.go
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
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 (
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.arsenm.dev/lasso/cmd/lasso/cmd"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
}
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
31
cmd/lassoctl/cmd/file.go
Normal file
31
cmd/lassoctl/cmd/file.go
Normal 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
41
cmd/lassoctl/cmd/list.go
Normal 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
361
cmd/lassoctl/cmd/mssh.go
Normal 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
74
cmd/lassoctl/cmd/proxy.go
Normal 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)
|
||||
}
|
||||
95
cmd/lassoctl/cmd/retrieve.go
Normal file
95
cmd/lassoctl/cmd/retrieve.go
Normal 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
126
cmd/lassoctl/cmd/root.go
Normal 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
81
cmd/lassoctl/cmd/send.go
Normal 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
72
cmd/lassoctl/cmd/ssh.go
Normal 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
37
cmd/lassoctl/main.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user