Merge branch 'master' of ../inputunifi into merge-them-all
This commit is contained in:
		
						commit
						deb94ac251
					
				|  | @ -0,0 +1,9 @@ | |||
| language: go | ||||
| go: | ||||
| - 1.16.x | ||||
| before_install: | ||||
|   # download super-linter: golangci-lint | ||||
| - curl -sL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin latest | ||||
| script: | ||||
| - golangci-lint run --enable-all -D nlreturn,exhaustivestruct | ||||
| - go test ./... | ||||
|  | @ -0,0 +1,21 @@ | |||
| MIT LICENSE. | ||||
| Copyright (c) 2018-2021 David Newhall II | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining | ||||
| a copy of this software and associated documentation files (the | ||||
| "Software"), to deal in the Software without restriction, including | ||||
| without limitation the rights to use, copy, modify, merge, publish, | ||||
| distribute, sublicense, and/or sell copies of the Software, and to | ||||
| permit persons to whom the Software is furnished to do so, subject to | ||||
| the following conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be | ||||
| included in all copies or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||||
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||||
| LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||||
| OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||||
| WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||||
|  | @ -0,0 +1,3 @@ | |||
| # inputunifi | ||||
| 
 | ||||
| ## UnPoller Input Plugin | ||||
|  | @ -0,0 +1,158 @@ | |||
| package inputunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/unpoller/unifi" | ||||
| 	"github.com/unpoller/webserver" | ||||
| ) | ||||
| 
 | ||||
| /* Event collection. Events are also sent to the webserver for display. */ | ||||
| 
 | ||||
| func (u *InputUnifi) collectControllerEvents(c *Controller) ([]interface{}, error) { | ||||
| 	if u.isNill(c) { | ||||
| 		u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) | ||||
| 
 | ||||
| 		if err := u.getUnifi(c); err != nil { | ||||
| 			return nil, fmt.Errorf("re-authenticating to %s: %w", c.URL, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var ( | ||||
| 		logs    = []interface{}{} | ||||
| 		newLogs []interface{} | ||||
| 	) | ||||
| 
 | ||||
| 	// Get the sites we care about.
 | ||||
| 	sites, err := u.getFilteredSites(c) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("unifi.GetSites(): %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	type caller func([]interface{}, []*unifi.Site, *Controller) ([]interface{}, error) | ||||
| 
 | ||||
| 	for _, call := range []caller{u.collectIDS, u.collectAnomalies, u.collectAlarms, u.collectEvents} { | ||||
| 		if newLogs, err = call(logs, sites, c); err != nil { | ||||
| 			return logs, err | ||||
| 		} | ||||
| 
 | ||||
| 		logs = append(logs, newLogs...) | ||||
| 	} | ||||
| 
 | ||||
| 	return logs, nil | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) collectAlarms(logs []interface{}, sites []*unifi.Site, c *Controller) ([]interface{}, error) { | ||||
| 	if *c.SaveAlarms { | ||||
| 		for _, s := range sites { | ||||
| 			events, err := c.Unifi.GetAlarmsSite(s) | ||||
| 			if err != nil { | ||||
| 				return logs, fmt.Errorf("unifi.GetAlarms(): %w", err) | ||||
| 			} | ||||
| 
 | ||||
| 			for _, e := range events { | ||||
| 				logs = append(logs, e) | ||||
| 
 | ||||
| 				webserver.NewInputEvent(PluginName, s.ID+"_alarms", &webserver.Event{ | ||||
| 					Ts: e.Datetime, Msg: e.Msg, Tags: map[string]string{ | ||||
| 						"type": "alarm", "key": e.Key, "site_id": e.SiteID, | ||||
| 						"site_name": e.SiteName, "source": e.SourceName, | ||||
| 					}, | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return logs, nil | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) collectAnomalies(logs []interface{}, sites []*unifi.Site, c *Controller) ([]interface{}, error) { | ||||
| 	if *c.SaveAnomal { | ||||
| 		for _, s := range sites { | ||||
| 			events, err := c.Unifi.GetAnomaliesSite(s) | ||||
| 			if err != nil { | ||||
| 				return logs, fmt.Errorf("unifi.GetAnomalies(): %w", err) | ||||
| 			} | ||||
| 
 | ||||
| 			for _, e := range events { | ||||
| 				logs = append(logs, e) | ||||
| 
 | ||||
| 				webserver.NewInputEvent(PluginName, s.ID+"_anomalies", &webserver.Event{ | ||||
| 					Ts: e.Datetime, Msg: e.Anomaly, Tags: map[string]string{ | ||||
| 						"type": "anomaly", "site_name": e.SiteName, "source": e.SourceName, | ||||
| 					}, | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return logs, nil | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) collectEvents(logs []interface{}, sites []*unifi.Site, c *Controller) ([]interface{}, error) { | ||||
| 	if *c.SaveEvents { | ||||
| 		for _, s := range sites { | ||||
| 			events, err := c.Unifi.GetSiteEvents(s, time.Hour) | ||||
| 			if err != nil { | ||||
| 				return logs, fmt.Errorf("unifi.GetEvents(): %w", err) | ||||
| 			} | ||||
| 
 | ||||
| 			for _, e := range events { | ||||
| 				e := redactEvent(e, c.HashPII) | ||||
| 				logs = append(logs, e) | ||||
| 
 | ||||
| 				webserver.NewInputEvent(PluginName, s.ID+"_events", &webserver.Event{ | ||||
| 					Msg: e.Msg, Ts: e.Datetime, Tags: map[string]string{ | ||||
| 						"type": "event", "key": e.Key, "site_id": e.SiteID, | ||||
| 						"site_name": e.SiteName, "source": e.SourceName, | ||||
| 					}, | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return logs, nil | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) collectIDS(logs []interface{}, sites []*unifi.Site, c *Controller) ([]interface{}, error) { | ||||
| 	if *c.SaveIDS { | ||||
| 		for _, s := range sites { | ||||
| 			events, err := c.Unifi.GetIDSSite(s) | ||||
| 			if err != nil { | ||||
| 				return logs, fmt.Errorf("unifi.GetIDS(): %w", err) | ||||
| 			} | ||||
| 
 | ||||
| 			for _, e := range events { | ||||
| 				logs = append(logs, e) | ||||
| 
 | ||||
| 				webserver.NewInputEvent(PluginName, s.ID+"_ids", &webserver.Event{ | ||||
| 					Ts: e.Datetime, Msg: e.Msg, Tags: map[string]string{ | ||||
| 						"type": "ids", "key": e.Key, "site_id": e.SiteID, | ||||
| 						"site_name": e.SiteName, "source": e.SourceName, | ||||
| 					}, | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return logs, nil | ||||
| } | ||||
| 
 | ||||
| // redactEvent attempts to mask personally identying information from log messages.
 | ||||
| // This currently misses the "msg" value entirely and leaks PII information.
 | ||||
| func redactEvent(e *unifi.Event, hash *bool) *unifi.Event { | ||||
| 	if !*hash { | ||||
| 		return e | ||||
| 	} | ||||
| 
 | ||||
| 	// metrics.Events[i].Msg <-- not sure what to do here.
 | ||||
| 	e.DestIPGeo = unifi.IPGeo{} | ||||
| 	e.SourceIPGeo = unifi.IPGeo{} | ||||
| 	e.Host = RedactNamePII(e.Host, hash) | ||||
| 	e.Hostname = RedactNamePII(e.Hostname, hash) | ||||
| 	e.DstMAC = RedactMacPII(e.DstMAC, hash) | ||||
| 	e.SrcMAC = RedactMacPII(e.SrcMAC, hash) | ||||
| 
 | ||||
| 	return e | ||||
| } | ||||
|  | @ -0,0 +1,269 @@ | |||
| package inputunifi | ||||
| 
 | ||||
| // nolint: gosec
 | ||||
| import ( | ||||
| 	"crypto/md5" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/unpoller/poller" | ||||
| 	"github.com/unpoller/unifi" | ||||
| ) | ||||
| 
 | ||||
| var ErrScrapeFilterMatchFailed = fmt.Errorf("scrape filter match failed, and filter is not http URL") | ||||
| 
 | ||||
| func (u *InputUnifi) isNill(c *Controller) bool { | ||||
| 	u.RLock() | ||||
| 	defer u.RUnlock() | ||||
| 
 | ||||
| 	return c.Unifi == nil | ||||
| } | ||||
| 
 | ||||
| // newDynamicCntrlr creates and saves a controller definition for further use.
 | ||||
| // This is called when an unconfigured controller is requested.
 | ||||
| func (u *InputUnifi) newDynamicCntrlr(url string) (bool, *Controller) { | ||||
| 	u.Lock() | ||||
| 	defer u.Unlock() | ||||
| 
 | ||||
| 	if c := u.dynamic[url]; c != nil { | ||||
| 		// it already exists.
 | ||||
| 		return false, c | ||||
| 	} | ||||
| 
 | ||||
| 	ccopy := u.Default // copy defaults into new controller
 | ||||
| 	u.dynamic[url] = &ccopy | ||||
| 	u.dynamic[url].URL = url | ||||
| 
 | ||||
| 	return true, u.dynamic[url] | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) dynamicController(filter *poller.Filter) (*poller.Metrics, error) { | ||||
| 	if !strings.HasPrefix(filter.Path, "http") { | ||||
| 		return nil, ErrScrapeFilterMatchFailed | ||||
| 	} | ||||
| 
 | ||||
| 	newCntrlr, c := u.newDynamicCntrlr(filter.Path) | ||||
| 
 | ||||
| 	if newCntrlr { | ||||
| 		u.Logf("Authenticating to Dynamic UniFi Controller: %s", filter.Path) | ||||
| 
 | ||||
| 		if err := u.getUnifi(c); err != nil { | ||||
| 			u.logController(c) | ||||
| 			return nil, fmt.Errorf("authenticating to %s: %w", filter.Path, err) | ||||
| 		} | ||||
| 
 | ||||
| 		u.logController(c) | ||||
| 	} | ||||
| 
 | ||||
| 	return u.collectController(c) | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) collectController(c *Controller) (*poller.Metrics, error) { | ||||
| 	if u.isNill(c) { | ||||
| 		u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) | ||||
| 
 | ||||
| 		if err := u.getUnifi(c); err != nil { | ||||
| 			return nil, fmt.Errorf("re-authenticating to %s: %w", c.URL, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	metrics, err := u.pollController(c) | ||||
| 	if err != nil { | ||||
| 		u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) | ||||
| 
 | ||||
| 		if err := u.getUnifi(c); err != nil { | ||||
| 			return metrics, fmt.Errorf("re-authenticating to %s: %w", c.URL, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return metrics, err | ||||
| } | ||||
| 
 | ||||
| //nolint:cyclop
 | ||||
| func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) { | ||||
| 	u.RLock() | ||||
| 	defer u.RUnlock() | ||||
| 
 | ||||
| 	// Get the sites we care about.
 | ||||
| 	sites, err := u.getFilteredSites(c) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("unifi.GetSites(): %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	m := &Metrics{TS: time.Now(), Sites: sites} | ||||
| 	defer updateWeb(c, m) | ||||
| 
 | ||||
| 	if c.SaveRogue != nil && *c.SaveRogue { | ||||
| 		if m.RogueAPs, err = c.Unifi.GetRogueAPs(sites); err != nil { | ||||
| 			return nil, fmt.Errorf("unifi.GetRogueAPs(%s): %w", c.URL, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if c.SaveDPI != nil && *c.SaveDPI { | ||||
| 		if m.SitesDPI, err = c.Unifi.GetSiteDPI(sites); err != nil { | ||||
| 			return nil, fmt.Errorf("unifi.GetSiteDPI(%s): %w", c.URL, err) | ||||
| 		} | ||||
| 
 | ||||
| 		if m.ClientsDPI, err = c.Unifi.GetClientsDPI(sites); err != nil { | ||||
| 			return nil, fmt.Errorf("unifi.GetClientsDPI(%s): %w", c.URL, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Get all the points.
 | ||||
| 	if m.Clients, err = c.Unifi.GetClients(sites); err != nil { | ||||
| 		return nil, fmt.Errorf("unifi.GetClients(%s): %w", c.URL, err) | ||||
| 	} | ||||
| 
 | ||||
| 	if m.Devices, err = c.Unifi.GetDevices(sites); err != nil { | ||||
| 		return nil, fmt.Errorf("unifi.GetDevices(%s): %w", c.URL, err) | ||||
| 	} | ||||
| 
 | ||||
| 	return u.augmentMetrics(c, m), nil | ||||
| } | ||||
| 
 | ||||
| // augmentMetrics is our middleware layer between collecting metrics and writing them.
 | ||||
| // This is where we can manipuate the returned data or make arbitrary decisions.
 | ||||
| // This method currently adds parent device names to client metrics and hashes PII.
 | ||||
| // This method also converts our local *Metrics type into a slice of interfaces for poller.
 | ||||
| func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Metrics { | ||||
| 	if metrics == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	m, devices, bssdIDs := extractDevices(metrics) | ||||
| 
 | ||||
| 	// These come blank, so set them here.
 | ||||
| 	for _, client := range metrics.Clients { | ||||
| 		if devices[client.Mac] = client.Name; client.Name == "" { | ||||
| 			devices[client.Mac] = client.Hostname | ||||
| 		} | ||||
| 
 | ||||
| 		client.Mac = RedactMacPII(client.Mac, c.HashPII) | ||||
| 		client.Name = RedactNamePII(client.Name, c.HashPII) | ||||
| 		client.Hostname = RedactNamePII(client.Hostname, c.HashPII) | ||||
| 		client.SwName = devices[client.SwMac] | ||||
| 		client.ApName = devices[client.ApMac] | ||||
| 		client.GwName = devices[client.GwMac] | ||||
| 		client.RadioDescription = bssdIDs[client.Bssid] + client.RadioProto | ||||
| 		m.Clients = append(m.Clients, client) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, client := range metrics.ClientsDPI { | ||||
| 		// Name on Client DPI data also comes blank, find it based on MAC address.
 | ||||
| 		client.Name = devices[client.MAC] | ||||
| 		if client.Name == "" { | ||||
| 			client.Name = client.MAC | ||||
| 		} | ||||
| 
 | ||||
| 		client.Name = RedactNamePII(client.Name, c.HashPII) | ||||
| 		client.MAC = RedactMacPII(client.MAC, c.HashPII) | ||||
| 		m.ClientsDPI = append(m.ClientsDPI, client) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, ap := range metrics.RogueAPs { | ||||
| 		// XXX: do we need augment this data?
 | ||||
| 		m.RogueAPs = append(m.RogueAPs, ap) | ||||
| 	} | ||||
| 
 | ||||
| 	if *c.SaveSites { | ||||
| 		for _, site := range metrics.Sites { | ||||
| 			m.Sites = append(m.Sites, site) | ||||
| 		} | ||||
| 
 | ||||
| 		for _, site := range metrics.SitesDPI { | ||||
| 			m.SitesDPI = append(m.SitesDPI, site) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return m | ||||
| } | ||||
| 
 | ||||
| // this is a helper function for augmentMetrics.
 | ||||
| func extractDevices(metrics *Metrics) (*poller.Metrics, map[string]string, map[string]string) { | ||||
| 	m := &poller.Metrics{TS: metrics.TS} | ||||
| 	devices := make(map[string]string) | ||||
| 	bssdIDs := make(map[string]string) | ||||
| 
 | ||||
| 	for _, r := range metrics.Devices.UAPs { | ||||
| 		devices[r.Mac] = r.Name | ||||
| 		m.Devices = append(m.Devices, r) | ||||
| 
 | ||||
| 		for _, v := range r.VapTable { | ||||
| 			bssdIDs[v.Bssid] = fmt.Sprintf("%s %s %s:", r.Name, v.Radio, v.RadioName) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, r := range metrics.Devices.USGs { | ||||
| 		devices[r.Mac] = r.Name | ||||
| 		m.Devices = append(m.Devices, r) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, r := range metrics.Devices.USWs { | ||||
| 		devices[r.Mac] = r.Name | ||||
| 		m.Devices = append(m.Devices, r) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, r := range metrics.Devices.UDMs { | ||||
| 		devices[r.Mac] = r.Name | ||||
| 		m.Devices = append(m.Devices, r) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, r := range metrics.Devices.UXGs { | ||||
| 		devices[r.Mac] = r.Name | ||||
| 		m.Devices = append(m.Devices, r) | ||||
| 	} | ||||
| 
 | ||||
| 	return m, devices, bssdIDs | ||||
| } | ||||
| 
 | ||||
| // RedactNamePII converts a name string to an md5 hash (first 24 chars only).
 | ||||
| // Useful for maskiing out personally identifying information.
 | ||||
| func RedactNamePII(pii string, hash *bool) string { | ||||
| 	if hash == nil || !*hash || pii == "" { | ||||
| 		return pii | ||||
| 	} | ||||
| 
 | ||||
| 	s := fmt.Sprintf("%x", md5.Sum([]byte(pii))) // nolint: gosec
 | ||||
| 	// instead of 32 characters, only use 24.
 | ||||
| 	return s[:24] | ||||
| } | ||||
| 
 | ||||
| // RedactMacPII converts a MAC address to an md5 hashed version (first 14 chars only).
 | ||||
| // Useful for maskiing out personally identifying information.
 | ||||
| func RedactMacPII(pii string, hash *bool) (output string) { | ||||
| 	if hash == nil || !*hash || pii == "" { | ||||
| 		return pii | ||||
| 	} | ||||
| 
 | ||||
| 	s := fmt.Sprintf("%x", md5.Sum([]byte(pii))) // nolint: gosec
 | ||||
| 	// This formats a "fake" mac address looking string.
 | ||||
| 	return fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", s[:2], s[2:4], s[4:6], s[6:8], s[8:10], s[10:12], s[12:14]) | ||||
| } | ||||
| 
 | ||||
| // getFilteredSites returns a list of sites to fetch data for.
 | ||||
| // Omits requested but unconfigured sites. Grabs the full list from the
 | ||||
| // controller and returns the sites provided in the config file.
 | ||||
| func (u *InputUnifi) getFilteredSites(c *Controller) ([]*unifi.Site, error) { | ||||
| 	u.RLock() | ||||
| 	defer u.RUnlock() | ||||
| 
 | ||||
| 	sites, err := c.Unifi.GetSites() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("controller: %w", err) | ||||
| 	} else if len(c.Sites) == 0 || StringInSlice("all", c.Sites) { | ||||
| 		return sites, nil | ||||
| 	} | ||||
| 
 | ||||
| 	i := 0 | ||||
| 
 | ||||
| 	for _, s := range sites { | ||||
| 		// Only include valid sites in the request filter.
 | ||||
| 		if StringInSlice(s.Name, c.Sites) { | ||||
| 			sites[i] = s | ||||
| 			i++ | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return sites[:i], nil | ||||
| } | ||||
|  | @ -0,0 +1,10 @@ | |||
| module github.com/unpoller/inputunifi | ||||
| 
 | ||||
| go 1.16 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28 | ||||
| 	github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80 | ||||
| 	github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf | ||||
| 	golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect | ||||
| ) | ||||
|  | @ -0,0 +1,58 @@ | |||
| github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= | ||||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= | ||||
| github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= | ||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||
| github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c h1:zqmyTlQyufRC65JnImJ6H1Sf7BDj8bG31EV919NVEQc= | ||||
| github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28 h1:YAv5naMdpOFahnxteFFRidZlrSEwLv8V2nBKJKmLmHg= | ||||
| github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28/go.mod h1:AbDp60t5WlLSRELAliMJ0RFQpm/0yXpyolVSZqNtero= | ||||
| github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80 h1:XjHGfJhMwnB63DYHgtWGJgDxLhxVcAOtf+cfuvpGoyo= | ||||
| github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80/go.mod h1:K9QFFGfZws4gzB+Popix19S/rBKqrtqI+tyPORyg3F0= | ||||
| github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf h1:HhXi3qca3kyFEFPh0mqdr0bpQs94hJvMbUJztwPtf2A= | ||||
| github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf/go.mod h1:77PywuUvspdtoRuH1htFhR3Tp0pLyWj6kJlYR4tBYho= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= | ||||
| golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= | ||||
| golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= | ||||
| golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= | ||||
| golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= | ||||
| golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= | ||||
| golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golift.io/cnfg v0.0.7 h1:qkNpP5Bq+5Gtoc6HcI8kapMD5zFOVan6qguxqBQF3OY= | ||||
| golift.io/cnfg v0.0.7/go.mod h1:AsB0DJe7nv0bizKaoy3e3MjjOF7upTpMOMvsfv4CNNk= | ||||
| golift.io/version v0.0.2 h1:i0gXRuSDHKs4O0sVDUg4+vNIuOxYoXhaxspftu2FRTE= | ||||
| golift.io/version v0.0.2/go.mod h1:76aHNz8/Pm7CbuxIsDi97jABL5Zui3f2uZxDm4vB6hU= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= | ||||
| gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
|  | @ -0,0 +1,339 @@ | |||
| // 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" | ||||
| 	"io/ioutil" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/unpoller/poller" | ||||
| 	"github.com/unpoller/unifi" | ||||
| ) | ||||
| 
 | ||||
| // 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"` | ||||
| 	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 := ioutil.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 := ioutil.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.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.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 | ||||
| } | ||||
|  | @ -0,0 +1,202 @@ | |||
| package inputunifi | ||||
| 
 | ||||
| /* This file contains the three poller.Input interface methods. */ | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/unpoller/poller" | ||||
| 	"github.com/unpoller/unifi" | ||||
| 	"github.com/unpoller/webserver" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrDynamicLookupsDisabled = fmt.Errorf("filter path requested but dynamic lookups disabled") | ||||
| 	ErrControllerNumNotFound  = fmt.Errorf("controller number not found") | ||||
| 	ErrNoFilterKindProvided   = fmt.Errorf("must provide filter: devices, clients, other") | ||||
| ) | ||||
| 
 | ||||
| // Initialize gets called one time when starting up.
 | ||||
| // Satisfies poller.Input interface.
 | ||||
| func (u *InputUnifi) Initialize(l poller.Logger) error { | ||||
| 	if u.Config == nil { | ||||
| 		u.Config = &Config{Disable: true} | ||||
| 	} | ||||
| 
 | ||||
| 	if u.Logger = l; u.Disable { | ||||
| 		u.Logf("UniFi input plugin disabled or missing configuration!") | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	if u.setDefaults(&u.Default); len(u.Controllers) == 0 && !u.Dynamic { | ||||
| 		u.Controllers = []*Controller{&u.Default} | ||||
| 	} | ||||
| 
 | ||||
| 	if len(u.Controllers) == 0 { | ||||
| 		u.Logf("No controllers configured. Polling dynamic controllers only! Defaults:") | ||||
| 		u.logController(&u.Default) | ||||
| 	} | ||||
| 
 | ||||
| 	for i, c := range u.Controllers { | ||||
| 		if err := u.getUnifi(u.setControllerDefaults(c)); err != nil { | ||||
| 			u.LogErrorf("Controller %d of %d Auth or Connection Error, retrying: %v", i+1, len(u.Controllers), err) | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		if err := u.checkSites(c); err != nil { | ||||
| 			u.LogErrorf("checking sites on %s: %v", c.URL, err) | ||||
| 		} | ||||
| 
 | ||||
| 		u.Logf("Configured UniFi Controller %d of %d:", i+1, len(u.Controllers)) | ||||
| 		u.logController(c) | ||||
| 	} | ||||
| 
 | ||||
| 	webserver.UpdateInput(&webserver.Input{Name: PluginName, Config: formatConfig(u.Config)}) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) logController(c *Controller) { | ||||
| 	u.Logf("   => URL: %s (verify SSL: %v)", c.URL, *c.VerifySSL) | ||||
| 
 | ||||
| 	if len(c.CertPaths) > 0 { | ||||
| 		u.Logf("   => Cert Files: %s", strings.Join(c.CertPaths, ", ")) | ||||
| 	} | ||||
| 
 | ||||
| 	if c.Unifi != nil { | ||||
| 		u.Logf("   => Version: %s (%s)", c.Unifi.ServerVersion, c.Unifi.UUID) | ||||
| 	} | ||||
| 
 | ||||
| 	u.Logf("   => Username: %s (has password: %v)", c.User, c.Pass != "") | ||||
| 	u.Logf("   => Hash PII / Poll Sites: %v / %s", *c.HashPII, strings.Join(c.Sites, ", ")) | ||||
| 	u.Logf("   => Save Sites / Save DPI: %v / %v (metrics)", *c.SaveSites, *c.SaveDPI) | ||||
| 	u.Logf("   => Save Events / Save IDS: %v / %v (logs)", *c.SaveEvents, *c.SaveIDS) | ||||
| 	u.Logf("   => Save Alarms / Anomalies: %v / %v (logs)", *c.SaveAlarms, *c.SaveAnomal) | ||||
| 	u.Logf("   => Save Rogue APs: %v", *c.SaveRogue) | ||||
| } | ||||
| 
 | ||||
| // Events allows you to pull only events (and IDS) from the UniFi Controller.
 | ||||
| // This does not fully respect HashPII, but it may in the future!
 | ||||
| // Use Filter.Path to pick a specific controller, otherwise poll them all!
 | ||||
| func (u *InputUnifi) Events(filter *poller.Filter) (*poller.Events, error) { | ||||
| 	if u.Disable { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	logs := []interface{}{} | ||||
| 
 | ||||
| 	if filter == nil { | ||||
| 		filter = &poller.Filter{} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, c := range u.Controllers { | ||||
| 		if filter.Path != "" && !strings.EqualFold(c.URL, filter.Path) { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		events, err := u.collectControllerEvents(c) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 
 | ||||
| 		logs = append(logs, events...) | ||||
| 	} | ||||
| 
 | ||||
| 	return &poller.Events{Logs: logs}, nil | ||||
| } | ||||
| 
 | ||||
| // Metrics grabs all the measurements from a UniFi controller and returns them.
 | ||||
| // Set Filter.Path to a controller URL for a specific controller (or get them all).
 | ||||
| func (u *InputUnifi) Metrics(filter *poller.Filter) (*poller.Metrics, error) { | ||||
| 	if u.Disable { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	metrics := &poller.Metrics{} | ||||
| 
 | ||||
| 	if filter == nil { | ||||
| 		filter = &poller.Filter{} | ||||
| 	} | ||||
| 
 | ||||
| 	// Check if the request is for an existing, configured controller (or all controllers)
 | ||||
| 	for _, c := range u.Controllers { | ||||
| 		if filter.Path != "" && !strings.EqualFold(c.URL, filter.Path) { | ||||
| 			// continue only if we have a filter path and it doesn't match.
 | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		m, err := u.collectController(c) | ||||
| 		if err != nil { | ||||
| 			return metrics, err | ||||
| 		} | ||||
| 
 | ||||
| 		metrics = poller.AppendMetrics(metrics, m) | ||||
| 	} | ||||
| 
 | ||||
| 	if filter.Path == "" || len(metrics.Clients) != 0 { | ||||
| 		return metrics, nil | ||||
| 	} | ||||
| 
 | ||||
| 	if !u.Dynamic { | ||||
| 		return nil, ErrDynamicLookupsDisabled | ||||
| 	} | ||||
| 
 | ||||
| 	// Attempt a dynamic metrics fetch from an unconfigured controller.
 | ||||
| 	return u.dynamicController(filter) | ||||
| } | ||||
| 
 | ||||
| // RawMetrics returns API output from the first configured UniFi controller.
 | ||||
| // Adjust filter.Unit to pull from a controller other than the first.
 | ||||
| func (u *InputUnifi) RawMetrics(filter *poller.Filter) ([]byte, error) { | ||||
| 	if l := len(u.Controllers); filter.Unit >= l { | ||||
| 		return nil, fmt.Errorf("%d controller(s) configured, '%d': %w", l, filter.Unit, ErrControllerNumNotFound) | ||||
| 	} | ||||
| 
 | ||||
| 	c := u.Controllers[filter.Unit] | ||||
| 	if u.isNill(c) { | ||||
| 		u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) | ||||
| 
 | ||||
| 		if err := u.getUnifi(c); err != nil { | ||||
| 			return nil, fmt.Errorf("re-authenticating to %s: %w", c.URL, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if err := u.checkSites(c); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	sites, err := u.getFilteredSites(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	switch filter.Kind { | ||||
| 	case "d", "device", "devices": | ||||
| 		return u.getSitesJSON(c, unifi.APIDevicePath, sites) | ||||
| 	case "client", "clients", "c": | ||||
| 		return u.getSitesJSON(c, unifi.APIClientPath, sites) | ||||
| 	case "other", "o": | ||||
| 		return c.Unifi.GetJSON(filter.Path) | ||||
| 	default: | ||||
| 		return []byte{}, ErrNoFilterKindProvided | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) getSitesJSON(c *Controller, path string, sites []*unifi.Site) ([]byte, error) { | ||||
| 	allJSON := []byte{} | ||||
| 
 | ||||
| 	for _, s := range sites { | ||||
| 		apiPath := fmt.Sprintf(path, s.Name) | ||||
| 		u.LogDebugf("Returning Path '%s' for site: %s (%s):\n", apiPath, s.Desc, s.Name) | ||||
| 
 | ||||
| 		body, err := c.Unifi.GetJSON(apiPath) | ||||
| 		if err != nil { | ||||
| 			return allJSON, fmt.Errorf("controller: %w", err) | ||||
| 		} | ||||
| 
 | ||||
| 		allJSON = append(allJSON, body...) | ||||
| 	} | ||||
| 
 | ||||
| 	return allJSON, nil | ||||
| } | ||||
|  | @ -0,0 +1,214 @@ | |||
| package inputunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/unpoller/unifi" | ||||
| 	"github.com/unpoller/webserver" | ||||
| ) | ||||
| 
 | ||||
| /* This code reformats our data to be displayed on the built-in web interface. */ | ||||
| 
 | ||||
| func updateWeb(c *Controller, metrics *Metrics) { | ||||
| 	webserver.UpdateInput(&webserver.Input{ | ||||
| 		Name:    PluginName, // Forgetting this leads to 3 hours of head scratching.
 | ||||
| 		Sites:   formatSites(c, metrics.Sites), | ||||
| 		Clients: formatClients(c, metrics.Clients), | ||||
| 		Devices: formatDevices(c, metrics.Devices), | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func formatConfig(config *Config) *Config { | ||||
| 	return &Config{ | ||||
| 		Default:     *formatControllers([]*Controller{&config.Default})[0], | ||||
| 		Disable:     config.Disable, | ||||
| 		Dynamic:     config.Dynamic, | ||||
| 		Controllers: formatControllers(config.Controllers), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func formatControllers(controllers []*Controller) []*Controller { | ||||
| 	fixed := []*Controller{} | ||||
| 
 | ||||
| 	for _, c := range controllers { | ||||
| 		id := "" | ||||
| 		if c.Unifi != nil { | ||||
| 			id = c.Unifi.UUID | ||||
| 		} | ||||
| 
 | ||||
| 		fixed = append(fixed, &Controller{ | ||||
| 			VerifySSL:  c.VerifySSL, | ||||
| 			SaveAnomal: c.SaveAnomal, | ||||
| 			SaveAlarms: c.SaveAlarms, | ||||
| 			SaveRogue:  c.SaveRogue, | ||||
| 			SaveEvents: c.SaveEvents, | ||||
| 			SaveIDS:    c.SaveIDS, | ||||
| 			SaveDPI:    c.SaveDPI, | ||||
| 			HashPII:    c.HashPII, | ||||
| 			SaveSites:  c.SaveSites, | ||||
| 			User:       c.User, | ||||
| 			Pass:       strconv.FormatBool(c.Pass != ""), | ||||
| 			URL:        c.URL, | ||||
| 			Sites:      c.Sites, | ||||
| 			ID:         id, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	return fixed | ||||
| } | ||||
| 
 | ||||
| func formatSites(c *Controller, sites []*unifi.Site) (s webserver.Sites) { | ||||
| 	for _, site := range sites { | ||||
| 		s = append(s, &webserver.Site{ | ||||
| 			ID:         site.ID, | ||||
| 			Name:       site.Name, | ||||
| 			Desc:       site.Desc, | ||||
| 			Source:     site.SourceName, | ||||
| 			Controller: c.Unifi.UUID, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	return s | ||||
| } | ||||
| 
 | ||||
| func formatClients(c *Controller, clients []*unifi.Client) (d webserver.Clients) { | ||||
| 	for _, client := range clients { | ||||
| 		clientType, deviceMAC := "unknown", "unknown" | ||||
| 		if client.ApMac != "" { | ||||
| 			clientType = "wireless" | ||||
| 			deviceMAC = client.ApMac | ||||
| 		} else if client.SwMac != "" { | ||||
| 			clientType = "wired" | ||||
| 			deviceMAC = client.SwMac | ||||
| 		} | ||||
| 
 | ||||
| 		if deviceMAC == "" { | ||||
| 			deviceMAC = client.GwMac | ||||
| 		} | ||||
| 
 | ||||
| 		d = append(d, &webserver.Client{ | ||||
| 			Name:       client.Name, | ||||
| 			SiteID:     client.SiteID, | ||||
| 			Source:     client.SourceName, | ||||
| 			Controller: c.Unifi.UUID, | ||||
| 			MAC:        client.Mac, | ||||
| 			IP:         client.IP, | ||||
| 			Type:       clientType, | ||||
| 			DeviceMAC:  deviceMAC, | ||||
| 			Rx:         client.RxBytes, | ||||
| 			Tx:         client.TxBytes, | ||||
| 			Since:      time.Unix(client.FirstSeen, 0), | ||||
| 			Last:       time.Unix(client.LastSeen, 0), | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	return d | ||||
| } | ||||
| 
 | ||||
| func formatDevices(c *Controller, devices *unifi.Devices) (d webserver.Devices) { // nolint: funlen
 | ||||
| 	if devices == nil { | ||||
| 		return d | ||||
| 	} | ||||
| 
 | ||||
| 	for _, device := range devices.UAPs { | ||||
| 		d = append(d, &webserver.Device{ | ||||
| 			Name:       device.Name, | ||||
| 			SiteID:     device.SiteID, | ||||
| 			Source:     device.SourceName, | ||||
| 			Controller: c.Unifi.UUID, | ||||
| 			MAC:        device.Mac, | ||||
| 			IP:         device.IP, | ||||
| 			Type:       device.Type, | ||||
| 			Model:      device.Model, | ||||
| 			Version:    device.Version, | ||||
| 			Uptime:     int(device.Uptime.Val), | ||||
| 			Clients:    int(device.NumSta.Val), | ||||
| 			Config:     nil, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, device := range devices.UDMs { | ||||
| 		d = append(d, &webserver.Device{ | ||||
| 			Name:       device.Name, | ||||
| 			SiteID:     device.SiteID, | ||||
| 			Source:     device.SourceName, | ||||
| 			Controller: c.Unifi.UUID, | ||||
| 			MAC:        device.Mac, | ||||
| 			IP:         device.IP, | ||||
| 			Type:       device.Type, | ||||
| 			Model:      device.Model, | ||||
| 			Version:    device.Version, | ||||
| 			Uptime:     int(device.Uptime.Val), | ||||
| 			Clients:    int(device.NumSta.Val), | ||||
| 			Config:     nil, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, device := range devices.USWs { | ||||
| 		d = append(d, &webserver.Device{ | ||||
| 			Name:       device.Name, | ||||
| 			SiteID:     device.SiteID, | ||||
| 			Source:     device.SourceName, | ||||
| 			Controller: c.Unifi.UUID, | ||||
| 			MAC:        device.Mac, | ||||
| 			IP:         device.IP, | ||||
| 			Type:       device.Type, | ||||
| 			Model:      device.Model, | ||||
| 			Version:    device.Version, | ||||
| 			Uptime:     int(device.Uptime.Val), | ||||
| 			Clients:    int(device.NumSta.Val), | ||||
| 			Config:     nil, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, device := range devices.USGs { | ||||
| 		d = append(d, &webserver.Device{ | ||||
| 			Name:       device.Name, | ||||
| 			SiteID:     device.SiteID, | ||||
| 			Source:     device.SourceName, | ||||
| 			Controller: c.Unifi.UUID, | ||||
| 			MAC:        device.Mac, | ||||
| 			IP:         device.IP, | ||||
| 			Type:       device.Type, | ||||
| 			Model:      device.Model, | ||||
| 			Version:    device.Version, | ||||
| 			Uptime:     int(device.Uptime.Val), | ||||
| 			Clients:    int(device.NumSta.Val), | ||||
| 			Config:     nil, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	return d | ||||
| } | ||||
| 
 | ||||
| // Logf logs a message.
 | ||||
| func (u *InputUnifi) Logf(msg string, v ...interface{}) { | ||||
| 	webserver.NewInputEvent(PluginName, PluginName, &webserver.Event{ | ||||
| 		Ts:   time.Now(), | ||||
| 		Msg:  fmt.Sprintf(msg, v...), | ||||
| 		Tags: map[string]string{"type": "info"}, | ||||
| 	}) | ||||
| 	u.Logger.Logf(msg, v...) | ||||
| } | ||||
| 
 | ||||
| // LogErrorf logs an error message.
 | ||||
| func (u *InputUnifi) LogErrorf(msg string, v ...interface{}) { | ||||
| 	webserver.NewInputEvent(PluginName, PluginName, &webserver.Event{ | ||||
| 		Ts:   time.Now(), | ||||
| 		Msg:  fmt.Sprintf(msg, v...), | ||||
| 		Tags: map[string]string{"type": "error"}, | ||||
| 	}) | ||||
| 	u.Logger.LogErrorf(msg, v...) | ||||
| } | ||||
| 
 | ||||
| // LogDebugf logs a debug message.
 | ||||
| func (u *InputUnifi) LogDebugf(msg string, v ...interface{}) { | ||||
| 	webserver.NewInputEvent(PluginName, PluginName, &webserver.Event{ | ||||
| 		Ts:   time.Now(), | ||||
| 		Msg:  fmt.Sprintf(msg, v...), | ||||
| 		Tags: map[string]string{"type": "debug"}, | ||||
| 	}) | ||||
| 	u.Logger.LogDebugf(msg, v...) | ||||
| } | ||||
		Loading…
	
		Reference in New Issue