349 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			349 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
| // Package inputunifi implements the poller.Input interface and bridges the gap between
 | |
| // metrics from the unifi library, and the augments required to pump them into unifi-poller.
 | |
| package inputunifi
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/unpoller/unifi"
 | |
| 	"github.com/unpoller/unpoller/pkg/poller"
 | |
| )
 | |
| 
 | |
| // PluginName is the name of this input plugin.
 | |
| const PluginName = "unifi"
 | |
| 
 | |
| const (
 | |
| 	defaultURL  = "https://127.0.0.1:8443"
 | |
| 	defaultUser = "unifipoller"
 | |
| 	defaultPass = "unifipoller"
 | |
| 	defaultSite = "all"
 | |
| )
 | |
| 
 | |
| // InputUnifi contains the running data.
 | |
| type InputUnifi struct {
 | |
| 	*Config    `json:"unifi" toml:"unifi" xml:"unifi" yaml:"unifi"`
 | |
| 	dynamic    map[string]*Controller
 | |
| 	sync.Mutex // to lock the map above.
 | |
| 	Logger     poller.Logger
 | |
| }
 | |
| 
 | |
| // Controller represents the configuration for a UniFi Controller.
 | |
| // Each polled controller may have its own configuration.
 | |
| type Controller struct {
 | |
| 	VerifySSL  *bool        `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"`
 | |
| 	SaveAnomal *bool        `json:"save_anomalies" toml:"save_anomalies" xml:"save_anomalies" yaml:"save_anomalies"`
 | |
| 	SaveAlarms *bool        `json:"save_alarms" toml:"save_alarms" xml:"save_alarms" yaml:"save_alarms"`
 | |
| 	SaveEvents *bool        `json:"save_events" toml:"save_events" xml:"save_events" yaml:"save_events"`
 | |
| 	SaveIDS    *bool        `json:"save_ids" toml:"save_ids" xml:"save_ids" yaml:"save_ids"`
 | |
| 	SaveDPI    *bool        `json:"save_dpi" toml:"save_dpi" xml:"save_dpi" yaml:"save_dpi"`
 | |
| 	SaveRogue  *bool        `json:"save_rogue" toml:"save_rogue" xml:"save_rogue" yaml:"save_rogue"`
 | |
| 	HashPII    *bool        `json:"hash_pii" toml:"hash_pii" xml:"hash_pii" yaml:"hash_pii"`
 | |
| 	DropPII    *bool        `json:"drop_pii" toml:"drop_pii" xml:"drop_pii" yaml:"drop_pii"`
 | |
| 	SaveSites  *bool        `json:"save_sites" toml:"save_sites" xml:"save_sites" yaml:"save_sites"`
 | |
| 	CertPaths  []string     `json:"ssl_cert_paths" toml:"ssl_cert_paths" xml:"ssl_cert_path" yaml:"ssl_cert_paths"`
 | |
| 	User       string       `json:"user" toml:"user" xml:"user" yaml:"user"`
 | |
| 	Pass       string       `json:"pass" toml:"pass" xml:"pass" yaml:"pass"`
 | |
| 	URL        string       `json:"url" toml:"url" xml:"url" yaml:"url"`
 | |
| 	Sites      []string     `json:"sites" toml:"sites" xml:"site" yaml:"sites"`
 | |
| 	Unifi      *unifi.Unifi `json:"-" toml:"-" xml:"-" yaml:"-"`
 | |
| 	ID         string       `json:"id,omitempty"` // this is an output, not an input.
 | |
| }
 | |
| 
 | |
| // Config contains our configuration data.
 | |
| type Config struct {
 | |
| 	sync.RWMutex               // locks the Unifi struct member when re-authing to unifi.
 | |
| 	Default      Controller    `json:"defaults" toml:"defaults" xml:"default" yaml:"defaults"`
 | |
| 	Disable      bool          `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"`
 | |
| 	Dynamic      bool          `json:"dynamic" toml:"dynamic" xml:"dynamic,attr" yaml:"dynamic"`
 | |
| 	Controllers  []*Controller `json:"controllers" toml:"controller" xml:"controller" yaml:"controllers"`
 | |
| }
 | |
| 
 | |
| // Metrics is simply a useful container for everything.
 | |
| type Metrics struct {
 | |
| 	TS         time.Time
 | |
| 	Sites      []*unifi.Site
 | |
| 	Clients    []*unifi.Client
 | |
| 	SitesDPI   []*unifi.DPITable
 | |
| 	ClientsDPI []*unifi.DPITable
 | |
| 	RogueAPs   []*unifi.RogueAP
 | |
| 	Devices    *unifi.Devices
 | |
| }
 | |
| 
 | |
| func init() { // nolint: gochecknoinits
 | |
| 	u := &InputUnifi{
 | |
| 		dynamic: make(map[string]*Controller),
 | |
| 	}
 | |
| 
 | |
| 	poller.NewInput(&poller.InputPlugin{
 | |
| 		Name:   PluginName,
 | |
| 		Input:  u, // this library implements poller.Input interface for Metrics().
 | |
| 		Config: u, // Defines our config data interface.
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // getCerts reads in cert files from disk and stores them as a slice of of byte slices.
 | |
| func (c *Controller) getCerts() ([][]byte, error) {
 | |
| 	if len(c.CertPaths) == 0 {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 
 | |
| 	b := make([][]byte, len(c.CertPaths))
 | |
| 
 | |
| 	for i, f := range c.CertPaths {
 | |
| 		d, err := os.ReadFile(f)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("reading SSL cert file: %w", err)
 | |
| 		}
 | |
| 
 | |
| 		b[i] = d
 | |
| 	}
 | |
| 
 | |
| 	return b, nil
 | |
| }
 | |
| 
 | |
| // getUnifi (re-)authenticates to a unifi controller.
 | |
| // If certificate files are provided, they are re-read.
 | |
| func (u *InputUnifi) getUnifi(c *Controller) error {
 | |
| 	u.Lock()
 | |
| 	defer u.Unlock()
 | |
| 
 | |
| 	if c.Unifi != nil {
 | |
| 		c.Unifi.CloseIdleConnections()
 | |
| 	}
 | |
| 
 | |
| 	certs, err := c.getCerts()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Create an authenticated session to the Unifi Controller.
 | |
| 	c.Unifi, err = unifi.NewUnifi(&unifi.Config{
 | |
| 		User:      c.User,
 | |
| 		Pass:      c.Pass,
 | |
| 		URL:       c.URL,
 | |
| 		SSLCert:   certs,
 | |
| 		VerifySSL: *c.VerifySSL,
 | |
| 		ErrorLog:  u.LogErrorf, // Log all errors.
 | |
| 		DebugLog:  u.LogDebugf, // Log debug messages.
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		c.Unifi = nil
 | |
| 		return fmt.Errorf("unifi controller: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	u.LogDebugf("Authenticated with controller successfully, %s", c.URL)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // checkSites makes sure the list of provided sites exists on the controller.
 | |
| // This only runs once during initialization.
 | |
| func (u *InputUnifi) checkSites(c *Controller) error {
 | |
| 	u.RLock()
 | |
| 	defer u.RUnlock()
 | |
| 
 | |
| 	if len(c.Sites) == 0 || c.Sites[0] == "" {
 | |
| 		c.Sites = []string{"all"}
 | |
| 	}
 | |
| 
 | |
| 	u.LogDebugf("Checking Controller Sites List")
 | |
| 
 | |
| 	sites, err := c.Unifi.GetSites()
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("controller: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	msg := []string{}
 | |
| 	for _, site := range sites {
 | |
| 		msg = append(msg, site.Name+" ("+site.Desc+")")
 | |
| 	}
 | |
| 
 | |
| 	u.Logf("Found %d site(s) on controller %s: %v", len(msg), c.URL, strings.Join(msg, ", "))
 | |
| 
 | |
| 	if StringInSlice("all", c.Sites) {
 | |
| 		c.Sites = []string{"all"}
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	keep := []string{}
 | |
| 
 | |
| FIRST:
 | |
| 	for _, s := range c.Sites {
 | |
| 		for _, site := range sites {
 | |
| 			if s == site.Name {
 | |
| 				keep = append(keep, s)
 | |
| 				continue FIRST
 | |
| 			}
 | |
| 		}
 | |
| 		u.LogErrorf("Configured site not found on controller %s: %v", c.URL, s)
 | |
| 	}
 | |
| 
 | |
| 	if c.Sites = keep; len(keep) == 0 {
 | |
| 		c.Sites = []string{"all"}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (u *InputUnifi) getPassFromFile(filename string) string {
 | |
| 	b, err := os.ReadFile(filename)
 | |
| 	if err != nil {
 | |
| 		u.LogErrorf("Reading UniFi Password File: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return strings.TrimSpace(string(b))
 | |
| }
 | |
| 
 | |
| // setDefaults sets the default defaults.
 | |
| func (u *InputUnifi) setDefaults(c *Controller) { //nolint:cyclop
 | |
| 	t := true
 | |
| 	f := false
 | |
| 
 | |
| 	// Default defaults.
 | |
| 	if c.SaveSites == nil {
 | |
| 		c.SaveSites = &t
 | |
| 	}
 | |
| 
 | |
| 	if c.VerifySSL == nil {
 | |
| 		c.VerifySSL = &f
 | |
| 	}
 | |
| 
 | |
| 	if c.HashPII == nil {
 | |
| 		c.HashPII = &f
 | |
| 	}
 | |
| 
 | |
| 	if c.DropPII == nil {
 | |
| 		c.DropPII = &f
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveDPI == nil {
 | |
| 		c.SaveDPI = &f
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveRogue == nil {
 | |
| 		c.SaveRogue = &f
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveIDS == nil {
 | |
| 		c.SaveIDS = &f
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveEvents == nil {
 | |
| 		c.SaveEvents = &f
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveAlarms == nil {
 | |
| 		c.SaveAlarms = &f
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveAnomal == nil {
 | |
| 		c.SaveAnomal = &f
 | |
| 	}
 | |
| 
 | |
| 	if c.URL == "" {
 | |
| 		c.URL = defaultURL
 | |
| 	}
 | |
| 
 | |
| 	if strings.HasPrefix(c.Pass, "file://") {
 | |
| 		c.Pass = u.getPassFromFile(strings.TrimPrefix(c.Pass, "file://"))
 | |
| 	}
 | |
| 
 | |
| 	if c.Pass == "" {
 | |
| 		c.Pass = defaultPass
 | |
| 	}
 | |
| 
 | |
| 	if c.User == "" {
 | |
| 		c.User = defaultUser
 | |
| 	}
 | |
| 
 | |
| 	if len(c.Sites) == 0 {
 | |
| 		c.Sites = []string{defaultSite}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // setControllerDefaults sets defaults for the for controllers.
 | |
| // Any missing values come from defaults (above).
 | |
| func (u *InputUnifi) setControllerDefaults(c *Controller) *Controller { //nolint:cyclop,funlen
 | |
| 	// Configured controller defaults.
 | |
| 	if c.SaveSites == nil {
 | |
| 		c.SaveSites = u.Default.SaveSites
 | |
| 	}
 | |
| 
 | |
| 	if c.VerifySSL == nil {
 | |
| 		c.VerifySSL = u.Default.VerifySSL
 | |
| 	}
 | |
| 
 | |
| 	if c.CertPaths == nil {
 | |
| 		c.CertPaths = u.Default.CertPaths
 | |
| 	}
 | |
| 
 | |
| 	if c.HashPII == nil {
 | |
| 		c.HashPII = u.Default.HashPII
 | |
| 	}
 | |
| 
 | |
| 	if c.DropPII == nil {
 | |
| 		c.DropPII = u.Default.DropPII
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveDPI == nil {
 | |
| 		c.SaveDPI = u.Default.SaveDPI
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveIDS == nil {
 | |
| 		c.SaveIDS = u.Default.SaveIDS
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveRogue == nil {
 | |
| 		c.SaveRogue = u.Default.SaveRogue
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveEvents == nil {
 | |
| 		c.SaveEvents = u.Default.SaveEvents
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveAlarms == nil {
 | |
| 		c.SaveAlarms = u.Default.SaveAlarms
 | |
| 	}
 | |
| 
 | |
| 	if c.SaveAnomal == nil {
 | |
| 		c.SaveAnomal = u.Default.SaveAnomal
 | |
| 	}
 | |
| 
 | |
| 	if c.URL == "" {
 | |
| 		c.URL = u.Default.URL
 | |
| 	}
 | |
| 
 | |
| 	if strings.HasPrefix(c.Pass, "file://") {
 | |
| 		c.Pass = u.getPassFromFile(strings.TrimPrefix(c.Pass, "file://"))
 | |
| 	}
 | |
| 
 | |
| 	if c.Pass == "" {
 | |
| 		c.Pass = u.Default.Pass
 | |
| 	}
 | |
| 
 | |
| 	if c.User == "" {
 | |
| 		c.User = u.Default.User
 | |
| 	}
 | |
| 
 | |
| 	if len(c.Sites) == 0 {
 | |
| 		c.Sites = u.Default.Sites
 | |
| 	}
 | |
| 
 | |
| 	return c
 | |
| }
 | |
| 
 | |
| // StringInSlice returns true if a string is in a slice.
 | |
| func StringInSlice(str string, slice []string) bool {
 | |
| 	for _, s := range slice {
 | |
| 		if strings.EqualFold(s, str) {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 |