492 lines
12 KiB
Go
492 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"github.com/influxdata/line-protocol/v2/lineprotocol"
|
|
log "github.com/sirupsen/logrus"
|
|
"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
|
|
RangingOK *bool
|
|
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)
|
|
|
|
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.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_ok", lineprotocol.BoolValue(*channel.RangingOK))
|
|
} 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)
|
|
}
|
|
|
|
os.Exit(0)
|
|
}
|
|
|
|
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 {
|
|
rangingOk := channel.RangingStatus == "Completed"
|
|
nChannels = append(nChannels, &DOCSISChannelInfo{
|
|
Type: ChannelType(channel.ChannelType),
|
|
Power: powerStr2Power(channel.Power),
|
|
RangingOK: &rangingOk,
|
|
Direction: UP,
|
|
Frequency: freqStr2Hz(channel.CentralFrequency),
|
|
ID: channel.ChannelIDUp,
|
|
})
|
|
}
|
|
|
|
for _, channel := range rawDOCSIS.Upstream {
|
|
rangingOk := channel.RangingStatus == "Completed"
|
|
nChannels = append(nChannels, &DOCSISChannelInfo{
|
|
Type: ChannelType(channel.ChannelType),
|
|
Power: powerStr2Power(channel.Power),
|
|
RangingOK: &rangingOk,
|
|
Direction: UP,
|
|
Frequency: freqStr2Hz(channel.CentralFrequency),
|
|
ID: channel.ChannelIDUp,
|
|
})
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func snrStr2SNR(snrStr string) *float64 {
|
|
rSNR, unit, err := strUnit2Elements(snrStr)
|
|
if err != nil {
|
|
log.Fatalf("error parsing SNR: %v", err)
|
|
}
|
|
|
|
if strings.ToLower(unit) != "db" {
|
|
log.Fatalf("malformatted power unit: %q", unit)
|
|
}
|
|
|
|
return &rSNR
|
|
}
|
|
|
|
func powerStr2Power(powerStr string) float64 {
|
|
rPower, unit, err := strUnit2Elements(powerStr)
|
|
if err != nil {
|
|
log.Fatalf("error parsing power unit: %v", err)
|
|
}
|
|
|
|
if strings.ToLower(unit) != "dbmv" {
|
|
log.Fatalf("malformed power unit: %q", unit)
|
|
}
|
|
|
|
return rPower
|
|
}
|
|
|
|
func freqStr2Hz(freqStr string) uint64 {
|
|
rFreq, unit, err := strUnit2Elements(freqStr)
|
|
if err != nil {
|
|
log.Fatalf("error parsing frequency: %v", err)
|
|
}
|
|
|
|
if strings.ToLower(unit) != "mhz" {
|
|
log.Fatalf("malformed frequency unit: %q", unit)
|
|
}
|
|
|
|
return uint64(rFreq * 1000000)
|
|
}
|
|
|
|
func strUnit2Elements(rawStr string) (float64, string, error) {
|
|
splitStr := strings.Split(rawStr, " ")
|
|
if len(splitStr) < 2 {
|
|
return -1, "", fmt.Errorf("malformed elements: %q", rawStr)
|
|
}
|
|
|
|
rFloat, err := strconv.ParseFloat(splitStr[0], 64)
|
|
if err != nil {
|
|
return 0, "", fmt.Errorf("error parsing float: %w", err)
|
|
}
|
|
|
|
return rFloat, splitStr[1], nil
|
|
}
|