Files
VodaDOCSIS/main.go
2024-10-26 23:19:57 +02:00

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
}