unpoller_unpoller/core/unifi/unifi.go

133 lines
3.7 KiB
Go

// Package unifi provides a set of types to unload (unmarshal) Ubiquiti UniFi
// controller data. Also provided are methods to easily get data for devices -
// things like access points and switches, and for clients - the things
// connected to those access points and switches. As a bonus, each device and
// client type provided has an attached method to create InfluxDB datapoints.
package unifi
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/cookiejar"
"strings"
)
// NewUnifi creates a http.Client with authenticated cookies.
// Used to make additional, authenticated requests to the APIs.
// Start here.
func NewUnifi(config *Config) (*Unifi, error) {
jar, err := cookiejar.New(nil)
if err != nil {
return nil, err
}
if config.ErrorLog == nil {
config.ErrorLog = DiscardLogs
}
if config.DebugLog == nil {
config.DebugLog = DiscardLogs
}
config.URL = strings.TrimRight(config.URL, "/")
u := &Unifi{Config: config,
Client: &http.Client{
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.VerifySSL}},
Jar: jar,
},
}
if err := u.Login(); err != nil {
return u, err
}
if err := u.GetServerData(); err != nil {
return u, fmt.Errorf("unable to get server version: %v", err)
}
return u, nil
}
// Login is a helper method. It can be called to grab a new authentication cookie.
func (u *Unifi) Login() error {
// magic login.
req, err := u.UniReq(LoginPath, fmt.Sprintf(`{"username":"%s","password":"%s"}`, u.User, u.Pass))
if err != nil {
return err
}
resp, err := u.Do(req)
if err != nil {
return err
}
defer func() {
_, _ = io.Copy(ioutil.Discard, resp.Body) // avoid leaking.
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("authentication failed (user: %s): %s (status: %s)",
u.User, u.URL+LoginPath, resp.Status)
}
return nil
}
// GetServerData sets the controller's version and UUID. Only call this if you
// previously called Login and suspect the controller version has changed.
func (u *Unifi) GetServerData() error {
var response struct {
Data server `json:"meta"`
}
u.server = &response.Data
return u.GetData(StatusPath, &response)
}
// GetData makes a unifi request and unmarshal the response into a provided pointer.
func (u *Unifi) GetData(methodPath string, v interface{}) error {
body, err := u.GetJSON(methodPath)
if err != nil {
return err
}
u.DebugLog("Unmarshaling %s (bytes: %d)", methodPath, len(body))
return json.Unmarshal(body, v)
}
// UniReq is a small helper function that adds an Accept header.
// Use this if you're unmarshalling UniFi data into custom types.
// And if you're doing that... sumbut a pull request with your new struct. :)
// This is a helper method that is exposed for convenience.
func (u *Unifi) UniReq(apiPath string, params string) (req *http.Request, err error) {
path := u.URL + apiPath
switch {
case params == "":
req, err = http.NewRequest("GET", path, nil)
default:
req, err = http.NewRequest("POST", path, bytes.NewBufferString(params))
}
if err == nil {
req.Header.Add("Accept", "application/json")
}
u.DebugLog("Downloading %s (params: %v, error: %v)", path, params != "", err != nil)
return
}
// GetJSON returns the raw JSON from a path. This is useful for debugging.
func (u *Unifi) GetJSON(apiPath string) ([]byte, error) {
req, err := u.UniReq(apiPath, "")
if err != nil {
return []byte{}, err
}
resp, err := u.Do(req)
if err != nil {
return []byte{}, err
}
defer func() {
_ = resp.Body.Close()
}()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return body, err
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf("invalid status code from server %s", resp.Status)
}
return body, err
}