300 lines
5.5 KiB
Go
300 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
type Client struct {
|
|
http_client *http.Client
|
|
base_url string
|
|
}
|
|
|
|
type Device struct {
|
|
// Fields directly from UniFi
|
|
Name string `json:"name"`
|
|
MAC string `json:"mac"`
|
|
|
|
// Constructed fields
|
|
Neighbors map[string]*Device // all device peers
|
|
Beyond map[string]*Device // device peers that we reach through this device
|
|
Path []*Device
|
|
|
|
// Intermediate data; ignore
|
|
Ports []*Port `json:"port_table"`
|
|
}
|
|
|
|
type Port struct {
|
|
LLDP []*LLDP `json:"lldp_table"`
|
|
}
|
|
|
|
type LLDP struct {
|
|
MAC string `json:"lldp_chassis_id"`
|
|
Port string `json:"lldp_port_id"`
|
|
Name string `json:"lldp_system_name"`
|
|
}
|
|
|
|
type DeviceResp struct {
|
|
Data []*Device `json:"data"`
|
|
}
|
|
|
|
func NewClient(base_url string) (*Client, error) {
|
|
jar, err := cookiejar.New(nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Client{
|
|
http_client: &http.Client{
|
|
Timeout: 5 * time.Second,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
},
|
|
},
|
|
Jar: jar,
|
|
},
|
|
base_url: base_url,
|
|
}, nil
|
|
}
|
|
|
|
type Creds struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
type ErrorResp struct {
|
|
Errors []string
|
|
}
|
|
|
|
func (c *Client) Login() error {
|
|
needSave := false
|
|
|
|
creds, err := loadCreds()
|
|
if err != nil {
|
|
creds, err = promptCreds()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
needSave = true
|
|
}
|
|
|
|
body, err := json.Marshal(creds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/auth/login", c.base_url), bytes.NewBuffer(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.http_client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
dec := json.NewDecoder(resp.Body)
|
|
error_resp := ErrorResp{}
|
|
err = dec.Decode(&error_resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fmt.Errorf("Failed to log into UniFi with saved credentials (\"%s\"). Delete %s to prompt again.", error_resp.Errors[0], credPath())
|
|
}
|
|
|
|
log.Printf("Logged into UniFi as %s", creds.Username)
|
|
|
|
if needSave {
|
|
err = creds.Save()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) ListDevices() (map[string]*Device, error) {
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("%s/proxy/network/api/s/default/stat/device", c.base_url), nil)
|
|
|
|
resp, err := c.http_client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
dec := json.NewDecoder(resp.Body)
|
|
error_resp := ErrorResp{}
|
|
err = dec.Decode(&error_resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, fmt.Errorf("%s", error_resp.Errors[0])
|
|
}
|
|
|
|
dec := json.NewDecoder(resp.Body)
|
|
device_resp := DeviceResp{}
|
|
err = dec.Decode(&device_resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ret := map[string]*Device{}
|
|
|
|
for _, dev := range device_resp.Data {
|
|
dev.Neighbors = map[string]*Device{}
|
|
dev.Beyond = map[string]*Device{}
|
|
dev.Path = []*Device{}
|
|
dev.MAC = strings.ToUpper(dev.MAC)
|
|
|
|
if ret[dev.Name] != nil {
|
|
return nil, fmt.Errorf("Duplicate UniFi device names: %s", dev.Name)
|
|
}
|
|
|
|
ret[dev.Name] = dev
|
|
}
|
|
|
|
// Second loop after the table is populated
|
|
for _, dev := range ret {
|
|
for _, port := range dev.Ports {
|
|
for _, lldp := range port.LLDP {
|
|
lldp.MAC = strings.ToUpper(lldp.MAC)
|
|
|
|
neighbor := ret[lldp.Name]
|
|
if neighbor == nil {
|
|
continue
|
|
}
|
|
|
|
dev.Neighbors[neighbor.Name] = neighbor
|
|
}
|
|
}
|
|
dev.Ports = nil
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (d *Device) PathHop(path []*Device, visited map[string]*Device) {
|
|
newPath := append(path, d)
|
|
|
|
d.Path = newPath
|
|
visited[d.Name] = d
|
|
|
|
for _, neighbor := range d.Neighbors {
|
|
if visited[neighbor.Name] != nil {
|
|
// Cycle
|
|
continue
|
|
}
|
|
|
|
d.Beyond[neighbor.Name] = neighbor
|
|
neighbor.PathHop(newPath, visited)
|
|
}
|
|
}
|
|
|
|
func (d *Device) LogHop(prefix string, last bool) {
|
|
if last {
|
|
log.Printf("%s└─ %s", prefix, d.Name)
|
|
} else {
|
|
log.Printf("%s├─ %s", prefix, d.Name)
|
|
}
|
|
|
|
names := []string{}
|
|
for _, next := range d.Beyond {
|
|
names = append(names, next.Name)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
i := 0
|
|
for _, name := range names {
|
|
next := d.Beyond[name]
|
|
if last {
|
|
next.LogHop(fmt.Sprintf("%s ", prefix), i == len(names) - 1)
|
|
} else {
|
|
next.LogHop(fmt.Sprintf("%s│ ", prefix), i == len(names) - 1)
|
|
}
|
|
i++
|
|
}
|
|
}
|
|
|
|
func loadCreds() (*Creds, error) {
|
|
fh, err := os.OpenFile(credPath(), os.O_RDONLY, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer fh.Close()
|
|
|
|
dec := yaml.NewDecoder(fh)
|
|
|
|
ret := &Creds{}
|
|
|
|
err = dec.Decode(ret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func promptCreds() (*Creds, error) {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
fmt.Print("UniFi username: ")
|
|
username, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fmt.Print("UniFi password: ")
|
|
password, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Creds{
|
|
Username: strings.TrimSuffix(username, "\n"),
|
|
Password: strings.TrimSuffix(password, "\n"),
|
|
}, nil
|
|
}
|
|
|
|
func (c *Creds) Save() error {
|
|
fh, err := os.OpenFile(credPath(), os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fh.Close()
|
|
|
|
enc := yaml.NewEncoder(fh)
|
|
defer enc.Close()
|
|
|
|
err = enc.Encode(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("Saved credentials to %s", credPath())
|
|
|
|
return nil
|
|
}
|
|
|
|
func credPath() string {
|
|
return path.Join(os.Getenv("HOME"), ".netperfect.creds")
|
|
}
|