package main import ( "bytes" "encoding/json" "errors" "flag" "fmt" "io/ioutil" "log" "net/http" "net/http/cookiejar" "strconv" "strings" "time" _ "git.nils.zone/nils/prettify" ) // types const ( baseURL = "https://unifi.freifunk-troisdorf.de/v2.1" ucBaseURL = "https://unifi.freifunk-troisdorf.de:8443" iso8601 = "2006-01-02T15:04:05-0700" ) // flags var token = flag.String("token", "", "Defines the x-auth-token") var ucUser = flag.String("ucUser", "", "Defines the Unifi API User") var ucPass = flag.String("ucPass", "", "Defines the Unifi API Password") var version = "development" var delay time.Duration = 5 * time.Second func main() { log.Printf("starting version %s...\n", version) // parse all flags flag.Parse() // check if flags are set if *token == "" { log.Fatalln("Please specify an API token via the flag '-token'") } if *ucPass == "" { log.Fatalln("Please specify an API Password via the flag '-ucPass'") } if *ucUser == "" { log.Fatalln("Please specify an API User via the flag '-ucUser'") } // start API processing (runs in a loop) go processAPIs() // start webserver on Port 3000 serveJSON() } //switch Unifi AP Mod IDs to Names func lookupModels(model string) string { switch model { case "BZ2", "U2S48", "U2Sv2": return "Unifi AP" case "BZ2LR", "U2L48", "U2Lv2": return "UniFi AP-LR" case "U7E", "U7Ev2": return "UniFi AP-AC" case "U7HD", "U7SHD": return "UniFi AP-HD" case "UXSDM": return "UniFi AP-BaseStationXG" case "UCMSH": return "AP-MeshXG" case "U7MP": return "AP-AC-Mesh-Pro" case "U7LR": return "UniFi AP-AC-LR" case "U7LT": return "UniFi AP-AC-Lite" case "U7P": return "UniFi AP-Pro" case "U7MSH": return "UniFi AP-AC-Mesh" case "U7PG2": return "UniFi AP-AC-Pro" default: return "Unifi Gerät" } } //int to bool converter func itob(i int) bool { if i == 1 { return true } return false } //Unifi Controller API processing func processUcAPIs() ([]node, []link) { //get list of Unifi devices to display var nodes []node var links []link d, err := getDevices("ucDevices.json") if err != nil { log.Fatalln(err) } //call Unifi Controller ucAPI := newAPI(*ucUser, *ucPass, ucBaseURL) //login ucAPI.ucLogin() //get all Sites from Controller sites, err := ucAPI.ucGetSites() if err != nil { panic(err) } //get all devices in all sites devices, err := ucAPI.ucGetDevices(sites) if err != nil { panic(err) } //build nodes struct for _, jsonDevice := range d.Devices { var currentDevice ucDevice var currentJSONDevice device for _, device := range devices { if strings.ToUpper(device.Mac) == strings.ToUpper(jsonDevice.MAC) { currentDevice = device currentJSONDevice = jsonDevice } } if isRemoteMACpublished(jsonDevice.MAC, d.Devices) == true { links = ucAddLink(jsonDevice, links) } load, err := strconv.ParseFloat(currentDevice.Sysstats.CPU, 64) if err != nil { panic(err) } mem, err := strconv.ParseFloat(currentDevice.Sysstats.Memory, 64) if err != nil { panic(err) } nodes = append(nodes, node{ Firstseen: "0", Lastseen: time.Unix(int64(currentDevice.LastSeen), 0).Format(iso8601), IsOnline: itob(currentDevice.State), IsGateway: false, Clients: 0, ClientsWifi24: 0, ClientsWifi5: 0, ClientsOther: 0, RootFSUsage: 0, LoadAVG: load / 100, MemoryUsage: mem / 100, Uptime: time.Now().Add(-1 * time.Second * time.Duration(currentDevice.Uptime)).Format(iso8601), GatewayNexthop: currentJSONDevice.GatewayNexthop, Gateway: currentJSONDevice.Gateway, Location: currentJSONDevice.Location, NodeID: strings.ReplaceAll(currentDevice.Mac, ":", ""), MAC: currentDevice.Mac, Adresses: []string{currentDevice.IP}, Domain: currentJSONDevice.Domain, Hostname: "[Unifi] " + currentDevice.Name, Owner: "Freifunk Rhein-Sieg", Firmware: firmware{ Base: "Ubiquiti - Stock", Release: currentDevice.Version, }, Autoupdater: autoupdater{ Enabled: false, Branch: "stable", }, NProc: 1, Model: lookupModels(currentDevice.Model), }) } return nodes, links } //UNMS API processing (Richtfunk) func processUNMSAPI() ([]node, []link) { // Variables for runtime var links []link var nodes []node d, err := getDevices("devices.json") if err != nil { log.Fatalln(err) } // API CALL 1 log.Println("calling API 1") var u []unifiAPIResponse err = callUnifiAPI("/devices", &u) if err != nil { log.Fatalln(err) } for i := range d.Devices { var dev unifiAPIResponse var currentDevice device for j := range u { if strings.ToUpper(u[j].Identification.MAC) == strings.ToUpper(d.Devices[i].MAC) { dev = u[j] currentDevice = d.Devices[i] } } var isOnline bool = false if dev.Overview.Status == "active" { isOnline = true } // END OF API CALL 1 // API CALL 2 log.Println("calling API 2 for device", d.Devices[i].Name) var details unifiAPIDetails callUnifiAPI("/devices/erouters/"+dev.Identification.ID, &details) // END OF API CALL 2 // API CALL 3 log.Println("calling API 3 for device", d.Devices[i].Name) var airmaxes []unifiAPIAirmax callUnifiAPI("/devices/airmaxes/"+dev.Identification.ID+"/stations", &airmaxes) // check if remote mac address is part of our published network for i := range airmaxes { if isRemoteMACpublished(airmaxes[i].DeviceIdentification.MAC, d.Devices) == true { links = addLink(dev, airmaxes[i], links) } } // END OF API CALL 3 // Get info from json file (static) nodes = append(nodes, node{ Firstseen: dev.Overview.CreatedAt.Format(iso8601), Lastseen: dev.Overview.LastSeen.Format(iso8601), IsOnline: isOnline, IsGateway: false, Clients: 0, ClientsWifi24: 0, ClientsWifi5: 0, ClientsOther: 0, RootFSUsage: 0, LoadAVG: details.Overview.CPU / 100, MemoryUsage: details.Overview.RAM / 100, Uptime: dev.Identification.Started.Format(iso8601), GatewayNexthop: currentDevice.GatewayNexthop, Gateway: currentDevice.Gateway, Location: currentDevice.Location, NodeID: strings.ReplaceAll(dev.Identification.MAC, ":", ""), MAC: dev.Identification.MAC, Adresses: getAddresses(details.IPAddress), Domain: currentDevice.Domain, Hostname: "[RiFu] " + details.Identification.Name, Owner: "Freifunk Rhein-Sieg", Firmware: firmware{ Base: "Ubiquiti - Stock", Release: details.Firmware.Current, }, Autoupdater: autoupdater{ Enabled: false, Branch: "stable", }, NProc: 1, Model: details.Identification.Model, }) } return nodes, links } func processAPIs() { tick := time.Tick(delay) for range tick { var nodes []node var links []link unmsNodes, unmsLinks := processUNMSAPI() ucNodes, ucLinks := processUcAPIs() nodes = append(nodes, unmsNodes...) nodes = append(nodes, ucNodes...) links = append(links, unmsLinks...) links = append(links, ucLinks...) // assemble final struct o := output{ Timestamp: time.Now().Format(iso8601), Nodes: nodes, Links: links, } // create file output log.Println("writing json file") if err := o.writeToFile(); err != nil { log.Fatalln(err) } // we're done here log.Println("...done") } } func getFile(url string) []byte { resp, err := http.Get(url) if err != nil { fmt.Printf("error") } data := resp.Body byteValue, _ := ioutil.ReadAll(data) return byteValue } func getDevices(file string) (devices, error) { // get devices from JSON file jsonFile := getFile("https://git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-freifunk-map-api/raw/branch/master/" + file) // read file to bytes // variable for d var d devices // unmarshal to struct json.Unmarshal(jsonFile, &d) return d, nil } func callUnifiAPI(url string, i interface{}) error { request, err := http.NewRequest(http.MethodGet, baseURL+url, nil) if err != nil { return errors.New(fmt.Sprint("can't set request", baseURL+url)) } request.Header.Set("x-auth-token", *token) client := &http.Client{} response, err := client.Do(request) if err != nil { return fmt.Errorf("can't get request %s with x-auth-token %s", baseURL+url, *token) } data, err := ioutil.ReadAll(response.Body) defer response.Body.Close() if err != nil { return fmt.Errorf("can't read response body: %+v", response.Body) } // try to fetch errors var a apiResponse json.Unmarshal(data, &a) if a.StatusCode != 0 { return fmt.Errorf("got following errorcode from API: %d %s %s", a.StatusCode, a.Error, a.Message) } // no error occurred, unmarshal to struct json.Unmarshal(data, &i) return nil } func isRemoteMACpublished(mac string, devices []device) bool { for i := range devices { if devices[i].MAC == mac { return true } } return false } func addLink(dev unifiAPIResponse, airmaxes unifiAPIAirmax, links []link) []link { for i := range links { if links[i].SourceAddr == airmaxes.DeviceIdentification.MAC { // link already exists return links } } links = append(links, link{ Type: "wifi", Source: strings.ReplaceAll(dev.Identification.MAC, ":", ""), Target: strings.ReplaceAll(airmaxes.DeviceIdentification.MAC, ":", ""), SourceTQ: airmaxes.Statistics.LinkScore, TargetTQ: airmaxes.Statistics.LinkScore, SourceAddr: dev.Identification.MAC, TargetAddr: airmaxes.DeviceIdentification.MAC, }) return links } func getAddresses(ip string) []string { var adresses []string adresses = append(adresses, strings.Split(ip, "/")[0]) return adresses } func serveJSON() { fs := http.FileServer(http.Dir("./output")) http.Handle("/", fs) log.Println("Listening on :3000...") err := http.ListenAndServe(":3000", nil) if err != nil { log.Fatalln(err) } } func httpClient() *http.Client { jar, err := cookiejar.New(nil) if err != nil { log.Fatal(err) } client := &http.Client{Jar: jar} return client } func newAPI(user string, pass string, baseURL string) ucAPIData { return ucAPIData{ user: user, pass: pass, baseURL: baseURL, client: httpClient(), } } func (u *ucAPIData) ucCallAPI(url string, method string, body *bytes.Buffer, output interface{}) error { req, err := http.NewRequest(method, u.baseURL+url, body) if err != nil { return fmt.Errorf("can't set request %s", u.baseURL+url) } req.Header.Set("Content-Type", "application/json") response, err := u.client.Do(req) if err != nil { return fmt.Errorf("can't login %s", u.baseURL+url) } defer response.Body.Close() if response.StatusCode != 200 { return fmt.Errorf("Login failed %s", u.baseURL+url) } data, err := ioutil.ReadAll(response.Body) if err != nil { return err } err = json.Unmarshal(data, &output) if err != nil { return err } return nil } func (u *ucAPIData) ucLogin() error { var loginData = []byte(`{"username":"` + u.user + `","password":"` + u.pass + `"}`) url := "/api/login" err := u.ucCallAPI(url, http.MethodPost, bytes.NewBuffer(loginData), nil) if err != nil { return err } return nil } func (u *ucAPIData) ucGetSites() ([]ucSite, error) { var d struct { Data []ucSite `json:"data"` } url := "/api/self/sites" err := u.ucCallAPI(url, http.MethodGet, bytes.NewBuffer([]byte{}), &d) if err != nil { return []ucSite{}, err } return d.Data, nil } func (u *ucAPIData) ucGetDevices(sites []ucSite) ([]ucDevice, error) { var d struct { Data []ucDevice `json:"data"` } var s []ucDevice for _, site := range sites { url := "/api/s/" + site.ID + "/stat/device" u.ucCallAPI(url, http.MethodGet, bytes.NewBuffer([]byte{}), &d) s = append(s, d.Data...) } return s, nil } func ucAddLink(dev device, links []link) []link { for i := range links { if links[i].SourceAddr == dev.MAC { // link already exists return links } } if dev.LinkedTo == "" { //no LinkedTo in ucDevices.json return links } links = append(links, link{ Type: "cable", Source: strings.ReplaceAll(dev.MAC, ":", ""), Target: strings.ReplaceAll(dev.GatewayNexthop, ":", ""), SourceTQ: 100, TargetTQ: 100, SourceAddr: dev.MAC, TargetAddr: dev.LinkedTo, }) return links }