inital working commit
This commit is contained in:
478
main.go
Normal file
478
main.go
Normal file
@@ -0,0 +1,478 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
httpClient = &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
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]
|
||||
}
|
Reference in New Issue
Block a user