package main import ( "crypto/sha256" "encoding/hex" "encoding/json" "flag" "fmt" "github.com/influxdata/line-protocol/v2/lineprotocol" log "github.com/sirupsen/logrus" "github.com/wercker/journalhook" "golang.org/x/crypto/pbkdf2" "gopkg.in/yaml.v3" "io" "net/http" "net/http/cookiejar" "net/url" "os" "os/signal" "path" "strconv" "strings" "syscall" "time" ) type Conf struct { Logging struct { Level string } StationIP string `yaml:"station_ip"` Password string `yaml:"password"` Timeout string `yaml:"timeout"` } var ( configFlag = flag.String("config", "config.yaml", "path of config file to use") journalLogFlag = flag.Bool("journal", false, "log to systemd journal instead of stdout/stderr") conf = Conf{} httpClient *http.Client ) const ( ITERATIONS = 1000 KEYSIZEBITS = 128 USERNAME = "admin" ) type ChannelType string //goland:noinspection ALL const ( OFDM ChannelType = "OFDM" SCQAM ChannelType = "SC-QAM" OFDMA ChannelType = "OFDMA" ) func (c ChannelType) String() string { return string(c) } type ChannelDirection string const ( UP ChannelDirection = "UP" DOWN ChannelDirection = "DOWN" ) func (c ChannelDirection) String() string { return strings.ToLower(string(c)) } type BaseResponse struct { Error string `json:"error"` Message string `json:"message"` Data json.RawMessage `json:"data"` } type LoginResponse struct { Intf string `json:"intf"` User string `json:"user"` UID string `json:"uid"` Dpd string `json:"Dpd"` RemoteAddr string `json:"remoteAddr"` UserAgent string `json:"userAgent"` HTTPReferer string `json:"httpReferer"` } type DOCSISResponse struct { OfdmDownstream []*DownstreamOFDMChannelInfo `json:"ofdm_downstream"` Downstream []*DownstreamChannelInfo `json:"downstream"` OfdmaUpstream []*UpstreamOFDMChannelInfo `json:"ofdma_upstream"` Upstream []*UpstreamChannelInfo `json:"upstream"` } type DownstreamOFDMChannelInfo struct { ID string `json:"__id"` ChannelIDOfdm string `json:"channelid_ofdm"` StartFrequency string `json:"start_frequency"` EndFrequency string `json:"end_frequency"` CentralFrequencyOfdm string `json:"CentralFrequency_ofdm"` Bandwidth string `json:"bandwidth"` PowerOfdm string `json:"power_ofdm"` SNROfdm string `json:"SNR_ofdm"` FFTOfdm string `json:"FFT_ofdm"` LockedOfdm string `json:"locked_ofdm"` ChannelType string `json:"ChannelType"` } type UpstreamOFDMChannelInfo struct { ID string `json:"__id"` ChannelIDUp string `json:"channelidup"` StartFrequency string `json:"start_frequency"` EndFrequency string `json:"end_frequency"` Power string `json:"power"` CentralFrequency string `json:"CentralFrequency"` Bandwidth string `json:"bandwidth"` FFT string `json:"FFT"` ChannelType string `json:"ChannelType"` RangingStatus string `json:"RangingStatus"` } type DownstreamChannelInfo struct { ID string `json:"__id"` ChannelID string `json:"channelid"` CentralFrequency string `json:"CentralFrequency"` Power string `json:"power"` SNR string `json:"SNR"` FFT string `json:"FFT"` Locked string `json:"locked"` ChannelType string `json:"ChannelType"` } type UpstreamChannelInfo struct { ID string `json:"__id"` ChannelIDUp string `json:"channelidup"` CentralFrequency string `json:"CentralFrequency"` Power string `json:"power"` ChannelType string `json:"ChannelType"` FFT string `json:"FFT"` RangingStatus string `json:"RangingStatus"` } type SaltResponse struct { Error string `json:"error"` Salt string `json:"salt"` SaltWebUI string `json:"saltwebui"` } type DOCSISChannelInfo struct { Type ChannelType Power float64 RangingStatus *string SNR *float64 Direction ChannelDirection Frequency uint64 ID string } func main() { killSignals := make(chan os.Signal, 1) signal.Notify(killSignals, syscall.SIGINT, syscall.SIGTERM) reloadSignals := make(chan os.Signal, 1) signal.Notify(reloadSignals, syscall.SIGUSR1) flag.Parse() confStr, err := os.ReadFile(*configFlag) if err != nil { log.Fatalf("Unable to open config: %v", err) } err = yaml.Unmarshal(confStr, &conf) if err != nil { log.Fatalf("Unable to parse config: %v", err) } lvl, err := log.ParseLevel(conf.Logging.Level) if err != nil { log.Fatalf("Failure setting logging level: %v", err) } log.SetLevel(lvl) if *journalLogFlag { journalhook.Enable() } cj, err := cookiejar.New(nil) if err != nil { log.Fatalf("error creating jar") } timeout, err := time.ParseDuration(conf.Timeout) if err != nil { log.Fatalf("unable to parse duration %s: %v", conf.Timeout, err) } httpClient = &http.Client{ Timeout: timeout, Jar: cj, } if err := login(); err != nil { log.Fatalf("error logging in: %v", err) } docsisStart := time.Now() var dData *DOCSISResponse if dData, err = DOCSISStatus(); err != nil { log.Errorf("error getting docsis status: %v", err) err = logout() if err != nil { log.Fatalf("error logging out: %v", err) } return } docsisTime := time.Now() var enc lineprotocol.Encoder enc.SetPrecision(lineprotocol.Microsecond) enc.StartLine("docsis_diagnostic") enc.AddField("response_time_ms", lineprotocol.MustNewValue(time.Since(docsisStart).Milliseconds())) enc.EndLine(docsisTime) for _, channel := range transformDOCSIS(dData) { enc.StartLine("docsis") enc.AddTag("channel_id", channel.ID) enc.AddTag("direction", channel.Direction.String()) enc.AddTag("type", channel.Type.String()) enc.AddField("frequency", lineprotocol.MustNewValue(channel.Frequency)) enc.AddField("power", lineprotocol.MustNewValue(channel.Power)) if channel.Direction == UP { enc.AddField("ranging_status", lineprotocol.MustNewValue(*channel.RangingStatus)) } else { enc.AddField("snr", lineprotocol.MustNewValue(*channel.SNR)) } enc.EndLine(docsisTime) } if err = enc.Err(); err != nil { log.Fatalf("influx line protocol encoding error: %v", err) } fmt.Printf("%s", enc.Bytes()) if err = logout(); err != nil { log.Fatalf("error logging out: %v", err) } } func login() error { postData := url.Values{} postData.Add("username", USERNAME) postData.Add("password", "seeksalthash") postData.Add("logout", "true") req, err := http.NewRequest(http.MethodPost, "http://"+path.Join(conf.StationIP, "/api/v1/session/login"), strings.NewReader(postData.Encode())) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") req.Header.Set("Referer", "http://"+conf.StationIP+"/") req.Header.Set("X-Requested-With", "XMLHttpRequest") sResp, err := httpClient.Do(req) if err != nil { return err } if sResp.StatusCode != 200 { return fmt.Errorf("login failed") } rData, err := io.ReadAll(sResp.Body) if err != nil { return err } err = sResp.Body.Close() if err != nil { return err } saltResp := new(SaltResponse) if err = json.Unmarshal(rData, saltResp); err != nil { return err } if saltResp.Error != "ok" { return fmt.Errorf("login failed: %+v", saltResp.Error) } finalKey := Key(Key(conf.Password, saltResp.Salt), saltResp.SaltWebUI) postData = url.Values{} postData.Add("username", USERNAME) postData.Add("password", finalKey) nResp, err := APIRequest(http.MethodPost, "/api/v1/session/login", postData) if err != nil { return err } loginData := new(LoginResponse) if err = json.Unmarshal(nResp.Data, loginData); err != nil { return err } _, err = APIRequest(http.MethodGet, "/api/v1/session/menu", nil) if err != nil { return err } return nil } func DOCSISStatus() (*DOCSISResponse, error) { resp, err := APIRequest(http.MethodGet, "/api/v1/sta_docsis_status", nil) if err != nil { return nil, err } docsisData := new(DOCSISResponse) if err = json.Unmarshal(resp.Data, docsisData); err != nil { return nil, err } log.Debugf("docsis response: %+v", docsisData) return docsisData, nil } func logout() error { _, err := APIRequest(http.MethodPost, "/api/v1/session/logout", nil) if err != nil { return err } return nil } func Key(pw string, salt string) string { return hex.EncodeToString(pbkdf2.Key([]byte(pw), []byte(salt), ITERATIONS, KEYSIZEBITS/8, sha256.New)) } func APIRequest(method string, endpoint string, postData url.Values) (*BaseResponse, error) { log.Debugf("[API] %s: %s", method, endpoint) var bodyReader io.Reader if postData != nil { bodyReader = strings.NewReader(postData.Encode()) } req, err := http.NewRequest(method, "http://"+path.Join(conf.StationIP, endpoint), bodyReader) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") req.Header.Set("Referer", "http://"+conf.StationIP+"/") req.Header.Set("X-Requested-With", "XMLHttpRequest") resp, err := httpClient.Do(req) if err != nil { return nil, err } rData, err := io.ReadAll(resp.Body) if err != nil { return nil, err } err = resp.Body.Close() if err != nil { return nil, err } if resp.StatusCode != 200 { return nil, fmt.Errorf("[API] calling %s failed with %d: %s", endpoint, resp.StatusCode, string(rData)) } baseResp := new(BaseResponse) if err = json.Unmarshal(rData, baseResp); err != nil { return nil, err } if baseResp.Error != "ok" { var data any _ = json.Unmarshal(baseResp.Data, &data) return nil, fmt.Errorf("%s failed: %s (data: %+v)", endpoint, baseResp.Message, data) } return baseResp, nil } func transformDOCSIS(rawDOCSIS *DOCSISResponse) (nChannels []*DOCSISChannelInfo) { for _, channel := range rawDOCSIS.OfdmDownstream { nChannels = append(nChannels, &DOCSISChannelInfo{ Type: ChannelType(channel.ChannelType), Power: powerStr2Power(channel.PowerOfdm), SNR: snrStr2SNR(channel.SNROfdm), Direction: DOWN, Frequency: freqStr2Hz(channel.CentralFrequencyOfdm), ID: channel.ChannelIDOfdm, }) } for _, channel := range rawDOCSIS.Downstream { nChannels = append(nChannels, &DOCSISChannelInfo{ Type: ChannelType(channel.ChannelType), Power: powerStr2Power(channel.Power), SNR: snrStr2SNR(channel.SNR), Direction: DOWN, Frequency: freqStr2Hz(channel.CentralFrequency), ID: channel.ChannelID, }) } for _, channel := range rawDOCSIS.OfdmaUpstream { nChannels = append(nChannels, &DOCSISChannelInfo{ Type: ChannelType(channel.ChannelType), Power: powerStr2Power(channel.Power), RangingStatus: &channel.RangingStatus, Direction: UP, Frequency: freqStr2Hz(channel.CentralFrequency), ID: channel.ChannelIDUp, }) } for _, channel := range rawDOCSIS.Upstream { nChannels = append(nChannels, &DOCSISChannelInfo{ Type: ChannelType(channel.ChannelType), Power: powerStr2Power(channel.Power), RangingStatus: &channel.RangingStatus, Direction: UP, Frequency: freqStr2Hz(channel.CentralFrequency), ID: channel.ChannelIDUp, }) } return } func snrStr2SNR(snrStr string) *float64 { rSNR, unit := strUnit2Elements(snrStr) if strings.ToLower(unit) != "db" { panic(fmt.Sprintf("error parsing power unit %s", snrStr)) } return &rSNR } func powerStr2Power(powerStr string) float64 { rPower, unit := strUnit2Elements(powerStr) if strings.ToLower(unit) != "dbmv" { panic(fmt.Sprintf("error parsing power unit %s", powerStr)) } return rPower } func freqStr2Hz(freqStr string) uint64 { rFreq, unit := strUnit2Elements(freqStr) if strings.ToLower(unit) != "mhz" { panic(fmt.Sprintf("error parsing frequency unit %s", freqStr)) } return uint64(rFreq * 1000000) } func strUnit2Elements(rawStr string) (float64, string) { splitStr := strings.Split(rawStr, " ") if len(splitStr) < 2 { panic(fmt.Sprintf("error parsing floatUnit %s", rawStr)) } rFloat, err := strconv.ParseFloat(splitStr[0], 64) if err != nil { panic(fmt.Sprintf("error parsing floatUnit %s", splitStr[0])) } return rFloat, splitStr[1] }