From 1bc0d08a84ce557bf1168973ae4e984a702d747f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Erik=20Hedenstr=C3=B6m?= <erik@hedenstroem.com>
Date: Fri, 4 Feb 2022 00:44:47 +0100
Subject: [PATCH] Added more commands

---
 cmd/clients.go           | 35 +++++++++++++++
 cmd/devices.go           | 27 ++++++++++++
 cmd/dhcp_reservations.go | 92 ----------------------------------------
 cmd/dump.go              | 34 +++++++++++++++
 cmd/list.go              | 48 +++++++++++++++++++++
 cmd/ping.go              | 25 +++++++++++
 cmd/root.go              |  9 ++--
 cmd/version.go           | 21 +++++++++
 ssh/tunnel.go            |  4 +-
 utils/document.go        | 80 ++++++++++++++++++++++++++++++++++
 10 files changed, 279 insertions(+), 96 deletions(-)
 create mode 100644 cmd/clients.go
 create mode 100644 cmd/devices.go
 delete mode 100644 cmd/dhcp_reservations.go
 create mode 100644 cmd/dump.go
 create mode 100644 cmd/list.go
 create mode 100644 cmd/ping.go
 create mode 100644 cmd/version.go
 create mode 100644 utils/document.go

diff --git a/cmd/clients.go b/cmd/clients.go
new file mode 100644
index 0000000..49439cd
--- /dev/null
+++ b/cmd/clients.go
@@ -0,0 +1,35 @@
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+	"gitlab.hedenstroem.com/go/udm-query/utils"
+	"go.mongodb.org/mongo-driver/bson"
+)
+
+var fixedIp bool
+
+var clientsCmd = &cobra.Command{
+	Use:   "clients",
+	Short: "clients",
+	Long:  `clients`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		ace := client.Database("ace")
+		clients := ace.Collection("user")
+		filter := bson.D{}
+		if fixedIp {
+			filter = bson.D{{"use_fixedip", true}}
+		}
+		docs, err := utils.FindDocuments(clients, filter)
+		if err != nil {
+			return err
+		}
+		utils.IpSort(docs, "fixed_ip")
+		utils.DocumentsToTable(docs, []string{"fixed_ip", "name", "hostname", "mac"})
+		return nil
+	},
+}
+
+func init() {
+	clientsCmd.Flags().BoolVarP(&fixedIp, "fixed", "f", false, "Show debug output")
+	RootCmd.AddCommand(clientsCmd)
+}
diff --git a/cmd/devices.go b/cmd/devices.go
new file mode 100644
index 0000000..f901c30
--- /dev/null
+++ b/cmd/devices.go
@@ -0,0 +1,27 @@
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+	"gitlab.hedenstroem.com/go/udm-query/utils"
+)
+
+var devicesCmd = &cobra.Command{
+	Use:   "devices",
+	Short: "devices",
+	Long:  `devices`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		ace := client.Database("ace")
+		devices := ace.Collection("device")
+		docs, err := utils.AllDocuments(devices)
+		if err != nil {
+			return err
+		}
+		utils.IpSort(docs, "ip")
+		utils.DocumentsToTable(docs, []string{"ip", "model", "version", "serial", "mac"})
+		return nil
+	},
+}
+
+func init() {
+	RootCmd.AddCommand(devicesCmd)
+}
diff --git a/cmd/dhcp_reservations.go b/cmd/dhcp_reservations.go
deleted file mode 100644
index ab7638f..0000000
--- a/cmd/dhcp_reservations.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package cmd
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"log"
-	"os"
-
-	"github.com/davecgh/go-spew/spew"
-	"github.com/jedib0t/go-pretty/v6/table"
-	"github.com/spf13/cobra"
-	"go.mongodb.org/mongo-driver/bson"
-	"go.mongodb.org/mongo-driver/mongo/options"
-	"go.mongodb.org/mongo-driver/mongo/readpref"
-)
-
-var reservationsCmd = &cobra.Command{
-	Use:   "reservations",
-	Short: "DHCP static reservations",
-	Long:  `Show all DHCP static reservations assignments`,
-	RunE: func(cmd *cobra.Command, args []string) error {
-
-		// Ping the primary
-		if err := client.Ping(context.TODO(), readpref.Primary()); err != nil {
-			panic(err)
-		}
-
-		ace := client.Database("ace")
-		devices := ace.Collection("device")
-		user := ace.Collection("user")
-
-		/*
-			filter := bson.D{
-				{"$and",
-					bson.A{
-						bson.D{{"rating", bson.D{{"$gt", 5}}}},
-						bson.D{{"rating", bson.D{{"$lt", 10}}}},
-					}},
-			}
-		*/
-
-		// _filter := bson.D{{"use_fixedip", true}}
-		opts := options.Find().SetSort(bson.D{{"ip", 1}})
-		filter := bson.D{}
-		cursor, _ := devices.Find(context.TODO(), filter)
-		for cursor.Next(context.TODO()) {
-			var result bson.D
-			if err := cursor.Decode(&result); err != nil {
-				log.Fatal(err)
-			}
-			b, _ := bson.MarshalExtJSONIndent(result, false, true, "", "  ")
-			fmt.Println("D----------------------------------")
-			fmt.Println(string(b))
-		}
-
-		t := table.NewWriter()
-		t.SetOutputMirror(os.Stdout)
-		t.AppendHeader(table.Row{"name", "hostname", "ip"})
-
-		opts = options.Find().SetSort(bson.D{{"fixed_ip", 1}})
-		filter = bson.D{{"use_fixedip", true}}
-		cursor, _ = user.Find(context.TODO(), filter, opts)
-		for cursor.Next(context.TODO()) {
-			var result bson.D
-			if err := cursor.Decode(&result); err != nil {
-				log.Fatal(err)
-			}
-			b, _ := bson.MarshalExtJSON(result, false, true)
-			// b, _ := bson.MarshalExtJSONIndent(result, false, true, "", "  ")
-			var v map[string]interface{}
-			json.Unmarshal(b, &v)
-			t.AppendRows([]table.Row{{v["name"], v["hostname"], v["fixed_ip"]}})
-			fmt.Println("U----------------------------------")
-			fmt.Println(string(b))
-			spew.Dump(v)
-		}
-		t.Render()
-
-		//		spew.Dump(docs)
-		/*
-			dbs, _ := client.ListDatabaseNames(context.TODO(), bson.D{}, nil)
-			fmt.Println("Successfully connected and pinged.")
-			spew.Dump(dbs)
-		*/
-		return nil
-	},
-}
-
-func init() {
-	RootCmd.AddCommand(reservationsCmd)
-}
diff --git a/cmd/dump.go b/cmd/dump.go
new file mode 100644
index 0000000..54c2e71
--- /dev/null
+++ b/cmd/dump.go
@@ -0,0 +1,34 @@
+package cmd
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"github.com/spf13/cobra"
+	"gitlab.hedenstroem.com/go/udm-query/utils"
+)
+
+var dumpCmd = &cobra.Command{
+	Use:   "dump [flags] db collection",
+	Short: "dump",
+	Long:  `dump`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if len(args) != 2 {
+			return errors.New("expected 2 arguments")
+		}
+		db := client.Database(args[0])
+		collection := db.Collection(args[1])
+		docs, err := utils.AllDocuments(collection)
+		if err != nil {
+			return err
+		}
+		b, _ := json.MarshalIndent(docs, "", "  ")
+		fmt.Println(string(b))
+		return nil
+	},
+}
+
+func init() {
+	RootCmd.AddCommand(dumpCmd)
+}
diff --git a/cmd/list.go b/cmd/list.go
new file mode 100644
index 0000000..c9e67fd
--- /dev/null
+++ b/cmd/list.go
@@ -0,0 +1,48 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/jedib0t/go-pretty/v6/list"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+	"go.mongodb.org/mongo-driver/bson"
+)
+
+var listCmd = &cobra.Command{
+	Use:   "list",
+	Short: "list",
+	Long:  `list`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		l := list.NewWriter()
+		l.AppendItem(viper.GetString("ADDRESS"))
+		l.Indent()
+		result, err := client.ListDatabases(context.TODO(), bson.D{})
+		if err != nil {
+			return err
+		}
+		for _, dbSpec := range result.Databases {
+			l.AppendItem(fmt.Sprintf("%s:%dMB", dbSpec.Name, dbSpec.SizeOnDisk/1000000))
+			l.Indent()
+			db := client.Database(dbSpec.Name)
+			collectionNames, err := db.ListCollectionNames(context.TODO(), bson.D{})
+			if err != nil {
+				return err
+			}
+			for _, collectionName := range collectionNames {
+				l.AppendItem(collectionName)
+			}
+			l.UnIndent()
+		}
+		l.SetStyle(list.StyleConnectedRounded)
+		l.SetOutputMirror(os.Stdout)
+		l.Render()
+		return nil
+	},
+}
+
+func init() {
+	RootCmd.AddCommand(listCmd)
+}
diff --git a/cmd/ping.go b/cmd/ping.go
new file mode 100644
index 0000000..8038136
--- /dev/null
+++ b/cmd/ping.go
@@ -0,0 +1,25 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/spf13/cobra"
+	"go.mongodb.org/mongo-driver/mongo/readpref"
+)
+
+var pingCmd = &cobra.Command{
+	Use:   "ping",
+	Short: "Ping the database",
+	Long:  `Sends no-op command to check if the database is responding to commands`,
+	RunE: func(cmd *cobra.Command, args []string) (err error) {
+		if err = client.Ping(context.TODO(), readpref.Primary()); err == nil {
+			fmt.Println("success")
+		}
+		return
+	},
+}
+
+func init() {
+	RootCmd.AddCommand(pingCmd)
+}
diff --git a/cmd/root.go b/cmd/root.go
index 4deffc2..be0cdbd 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -10,24 +10,26 @@ import (
 	"github.com/joho/godotenv"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
-	"gitlab.hedenstroem.com/go/udm-query/constant"
 	"gitlab.hedenstroem.com/go/udm-query/ssh"
 	"go.mongodb.org/mongo-driver/mongo"
 	"go.mongodb.org/mongo-driver/mongo/options"
 )
 
+var debug bool
 var client *mongo.Client
 
 var RootCmd = &cobra.Command{
 	Use:  "udm-query",
-	Long: `UDM query tool ` + constant.Version,
+	Long: `Tool to query Ubiquiti UniFi Dream Machines`,
 	PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
 		tunnel := ssh.NewSSHTunnel(
 			viper.GetString("ADDRESS"),
 			ssh.Password(viper.GetString("PASSWORD")),
 			"127.0.0.1:27117",
 		)
-		tunnel.Log = log.New(os.Stdout, "", log.Ldate|log.Lmicroseconds)
+		if debug {
+			tunnel.Log = log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)
+		}
 		go tunnel.Start()
 		time.Sleep(100 * time.Millisecond)
 		uri := fmt.Sprintf("mongodb://127.0.0.1:%d/", tunnel.Local.Port)
@@ -50,6 +52,7 @@ func init() {
 	cobra.OnInitialize(initEnv)
 	RootCmd.PersistentFlags().StringP("address", "a", "192.168.1.1", "Address to the USM SSH server")
 	RootCmd.PersistentFlags().StringP("password", "p", "", "SSH password")
+	RootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Show debug output")
 	viper.SetEnvPrefix("SSH")
 	viper.BindPFlag("ADDRESS", RootCmd.PersistentFlags().Lookup("address"))
 	viper.BindPFlag("PASSWORD", RootCmd.PersistentFlags().Lookup("password"))
diff --git a/cmd/version.go b/cmd/version.go
new file mode 100644
index 0000000..841a3f3
--- /dev/null
+++ b/cmd/version.go
@@ -0,0 +1,21 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+	"gitlab.hedenstroem.com/go/udm-query/constant"
+)
+
+var versionCmd = &cobra.Command{
+	Use:   "version",
+	Short: "Display version",
+	Long:  `Displays the version of the udm-query tool`,
+	Run: func(cmd *cobra.Command, args []string) {
+		fmt.Println(constant.Version)
+	},
+}
+
+func init() {
+	RootCmd.AddCommand(versionCmd)
+}
diff --git a/ssh/tunnel.go b/ssh/tunnel.go
index 6be4768..f5f363f 100644
--- a/ssh/tunnel.go
+++ b/ssh/tunnel.go
@@ -30,12 +30,13 @@ func (tunnel *SSHTunnel) Start() error {
 	}
 	defer listener.Close()
 	tunnel.Local.Port = listener.Addr().(*net.TCPAddr).Port
+	tunnel.logf("accepting connections at %s", tunnel.Local.String())
 	for {
 		conn, err := listener.Accept()
 		if err != nil {
 			return err
 		}
-		tunnel.logf("accepted connection")
+		tunnel.logf("accepted connection from %s", conn.RemoteAddr())
 		go tunnel.forward(conn)
 	}
 }
@@ -59,6 +60,7 @@ func (tunnel *SSHTunnel) forward(localConn net.Conn) {
 			tunnel.logf("io.Copy error: %s", err)
 		}
 	}
+	tunnel.logf("%s -> %s -> %s -> %s\n", localConn.RemoteAddr(), localConn.LocalAddr(), tunnel.Server, tunnel.Remote)
 	go copyConn(localConn, remoteConn)
 	go copyConn(remoteConn, localConn)
 }
diff --git a/utils/document.go b/utils/document.go
new file mode 100644
index 0000000..5e3ddb2
--- /dev/null
+++ b/utils/document.go
@@ -0,0 +1,80 @@
+package utils
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"net"
+	"os"
+	"sort"
+
+	"github.com/jedib0t/go-pretty/v6/table"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func AllDocuments(collection *mongo.Collection) ([]map[string]interface{}, error) {
+	return FindDocuments(collection, bson.D{})
+}
+
+func FindDocuments(collection *mongo.Collection, filter interface{}, opts ...*options.FindOptions) ([]map[string]interface{}, error) {
+	var docs []map[string]interface{}
+	cursor, err := collection.Find(context.TODO(), filter)
+	if err != nil {
+		return nil, err
+	}
+	defer cursor.Close(context.TODO())
+	for cursor.Next(context.TODO()) {
+		var result bson.D
+		if err := cursor.Decode(&result); err != nil {
+			return nil, err
+		}
+		b, err := bson.MarshalExtJSON(result, false, true)
+		if err != nil {
+			return nil, err
+		}
+		var v map[string]interface{}
+		json.Unmarshal(b, &v)
+		docs = append(docs, v)
+	}
+	return docs, nil
+}
+
+func DocumentsToTable(docs []map[string]interface{}, fields []string) {
+	t := table.NewWriter()
+	header := table.Row{}
+	for _, field := range fields {
+		header = append(header, field)
+	}
+	t.AppendHeader(header)
+	for _, doc := range docs {
+		row := table.Row{}
+		for _, field := range fields {
+			value, exists := doc[field]
+			if exists {
+				row = append(row, value)
+			} else {
+				row = append(row, "-")
+			}
+		}
+		t.AppendRows([]table.Row{row})
+	}
+	t.SetOutputMirror(os.Stdout)
+	t.SetStyle(table.StyleColoredBright)
+	t.Render()
+}
+
+func IpSort(docs []map[string]interface{}, field string) {
+	sort.SliceStable(docs, func(i, j int) bool {
+		ip_i := net.IPv4bcast
+		if str, exists := docs[i][field]; exists {
+			ip_i = net.ParseIP(str.(string))
+		}
+		ip_j := net.IPv4bcast
+		if str, exists := docs[j][field]; exists {
+			ip_j = net.ParseIP(str.(string))
+		}
+		return bytes.Compare(ip_i, ip_j) < 0
+	})
+}
-- 
GitLab