vodafone-station-exporter/collector/api.go
2025-10-07 21:29:15 +02:00

454 lines
14 KiB
Go

package collector
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/prometheus/common/promslog"
"golang.org/x/exp/slog"
"golang.org/x/crypto/pbkdf2"
"io/ioutil"
"net/http"
"net/http/cookiejar"
"net/url"
"strconv"
"strings"
"time"
)
type VodafoneStation struct {
URL string
Password string
client *http.Client
}
type LoginResponseSalts struct {
Error string `json:"error"`
Salt string `json:"salt"`
SaltWebUI string `json:"saltwebui"`
}
type LoginResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Data *LoginResponseData `json:"data"`
}
type LoginResponseData struct {
Interface string `json:"intf"`
User string `json:"user"`
Uid string `json:"uid"`
DefaultPassword string `json:"Dpd"`
RemoteAddress string `json:"remoteAddr"`
UserAgent string `json:"userAgent"`
HttpReferer string `json:"httpReferer"`
}
type LogoutResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}
type DocsisStatusResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Data *DocsisStatusData `json:"data"`
}
type DocsisStatusData struct {
OfdmDownstreamData []*OfdmDownstreamData `json:"ofdm_downstream"`
Downstream []*DocsisDownstreamChannel `json:"downstream"`
OfdmaUpstreamData []*OfdmaUpstreamData `json:"ofdma_upstream"`
Upstream []*DocsisUpstreamChannel `json:"upstream"`
}
type OfdmDownstreamData 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 DocsisDownstreamChannel 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 OfdmaUpstreamData 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 DocsisUpstreamChannel struct {
Id string `json:"__id"`
ChannelIdUp string `json:"channelidup"`
CentralFrequency string `json:"CentralFrequency"`
Power string `json:"power"`
Snr string `json:"SNR"`
ChannelType string `json:"ChannelType"`
Fft string `json:"FFT"`
RangingStatus string `json:"RangingStatus"`
}
type StationStatusReponse struct {
Error string `json:"error"`
Message string `json:"message"`
Data *StationStatusData `json:"data"`
}
type DNSTable struct {
Id string `json:"__id"`
Type string `json:"Type"`
Dns string `json:"dns"`
}
type StationStatusData struct {
DateAndTime string `json:"dateandtime"`
FirewallStatus string `json:"firewallstatus"`
LanIpv4 string `json:"lanipv4"`
LanMode string `json:"LanMode"`
LanGateway string `json:"langateway"`
LanDHCPstatus string `json:"lanDHCPstatus"`
LanMAC string `json:"lanMAC"`
LanPortStatus4 string `json:"lanportstatus_4"`
LanPortSpeed4 string `json:"lanportspeed_4"`
LanPortStatus1 string `json:"lanportstatus_1"`
LanPortSpeed1 string `json:"lanportspeed_1"`
LanPortStatus2 string `json:"lanportstatus_2"`
LanPortSpeed2 string `json:"lanportspeed_2"`
LanPortStatus3 string `json:"lanportstatus_3"`
LanPortSpeed3 string `json:"lanportspeed_3"`
WifiStatus string `json:"wifistatus"`
Channel string `json:"channel"`
Bandwidth string `json:"bandwidth"`
MaxSpeed string `json:"maxspeed"`
Ssid string `json:"ssid"`
MacAddress string `json:"macaddress"`
Security string `json:"security"`
WifiStatus5 string `json:"wifistatus_5"`
Channel5 string `json:"channel_5"`
Bandwidth5 string `json:"bandwidth_5"`
MaxSpeed5 string `json:"maxspeed_5"`
Ssid5 string `json:"ssid_5"`
MacAddress5 string `json:"macaddress_5"`
Security5 string `json:"security_5"`
DnsEntries string `json:"DnsEntries"`
AFTR string `json:"AFTR"`
Serialnumber string `json:"serialnumber"`
FirmwareVersion string `json:"firmwareversion"`
HardwareType string `json:"hardwaretype"`
Uptime string `json:"uptime"`
InternetIpv4 string `json:"internetipv4"`
DnsTbl []DNSTable `json:"Dns_Tbl"`
DelegatedPrefix string `json:"DelegatedPrefix"`
DNSTblRT []string `json:"DNSTblRT"`
IPAddressRT []string `json:"IPAddressRT"`
IpPrefixClass string `json:"IpPrefixClass"`
}
type WanStatusData struct {
Ipv4 string `json:"ipv4"`
Mac string `json:"mac_address"`
Duration string `json:"duration"`
DurationIpv6 string `json:"durationIpv6"`
Expires string `json:"expires"`
Ipv4Dns string `json:"ipv4_dns"`
IPAddressV6 []string `json:"IPAddress_v6"`
DNSTblRT []string `json:"DNSTblRT"`
}
type CallLog struct {
Lines map[string]*PhoneNumberCallLog
Line0 *PhoneNumberCallLog `json:"0"`
Line1 *PhoneNumberCallLog `json:"1"`
Token string `json:"token"`
}
type PhoneNumberCallLog struct {
Error string `json:"error"`
Message string `json:"message"`
Data *CallLogData `json:"data"`
}
type CallLogData struct {
Entries []*CallLogEntry `json:"CallTbl"`
}
type CallLogEntry struct {
Id string `json:"__id"`
EndTime string `json:"endTime"`
StartTime string `json:"startTime"`
ExternalNumber string `json:"externalNumber"`
Direction string `json:"Direction"`
Type string `json:"type"`
}
type LedSettingResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Data *LedData `json:"data"`
Token string `json:"token"`
}
type LedData struct {
Led string `json:"led"`
}
type StationAboutResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Data *StationAboutData `json:"data"`
}
type WanStatusResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Data *WanStatusData `json:"data"`
}
type StationAboutData struct {
Software []*SoftwareInfo `json:"cosp"`
}
type SoftwareInfo struct {
Name string `json:"name"`
Version string `json:"version"`
License string `json:"license"`
}
type PhonenumbersResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Data *PhonenumbersData `json:"data"`
}
type PhonenumbersData struct {
LineNumber1 string `json:"LineNumber1"`
Callnumber1 string `json:"callnumber1"`
LineStatus1 string `json:"LineStatus1"`
AdditionalInfos1 string `json:"AdditionalInfos1"`
LineNumber2 string `json:"LineNumber2"`
Callnumber2 string `json:"callnumber2"`
LineStatus2 string `json:"LineStatus2"`
AdditionalInfos2 string `json:"AdditionalInfos2"`
}
func NewVodafoneStation(stationUrl, password string) *VodafoneStation {
cookieJar, err := cookiejar.New(nil)
parsedUrl, err := url.Parse(stationUrl)
cookieJar.SetCookies(parsedUrl, []*http.Cookie{
&http.Cookie{
Name: "Cwd",
Value: "No",
},
})
if err != nil {
panic(err)
}
return &VodafoneStation{
URL: stationUrl,
Password: password,
client: &http.Client{
Jar: cookieJar,
Timeout: time.Second * 20, // getting DOCSIS status can be slow!
},
}
}
func (v *VodafoneStation) Login() (*LoginResponse, error) {
_, err := v.doRequest("GET", v.URL, "")
if err != nil {
return nil, err
}
loginResponseSalts, err := v.getLoginSalts()
if err != nil {
return nil, err
}
derivedPassword := GetLoginPassword(v.Password, loginResponseSalts.Salt, loginResponseSalts.SaltWebUI)
responseBody, err := v.doRequest("POST", v.URL+"/api/v1/session/login", "username=admin&password="+derivedPassword)
if err != nil {
return nil, err
}
loginResponse := &LoginResponse{}
err = json.Unmarshal(responseBody, loginResponse)
if loginResponse.Error != "ok" {
return nil, fmt.Errorf("Got non error=ok message from vodafone station")
}
_, err = v.doRequest("GET", v.URL+"/api/v1/session/menu", "")
if err != nil {
return nil, err
}
return loginResponse, nil
}
func (v *VodafoneStation) Logout() (*LogoutResponse, error) {
responseBody, err := v.doRequest("POST", v.URL+"/api/v1/session/logout", "")
if err != nil {
return nil, err
}
logoutResponse := &LogoutResponse{}
err = json.Unmarshal(responseBody, logoutResponse)
if err != nil {
return nil, err
}
if logoutResponse.Error != "ok" {
return nil, fmt.Errorf("Got non error=ok message from vodafone station")
}
return logoutResponse, nil
}
func (v *VodafoneStation) GetDocsisStatus() (*DocsisStatusResponse, error) {
responseBody, err := v.doRequest("GET", v.URL+"/api/v1/sta_docsis_status?_="+strconv.FormatInt(makeTimestamp(), 10), "")
if err != nil {
return nil, err
}
docsisStatusResponse := &DocsisStatusResponse{}
return docsisStatusResponse, json.Unmarshal(responseBody, docsisStatusResponse)
}
func (v *VodafoneStation) GetStationStatus() (*StationStatusReponse, error) {
responseBody, err := v.doRequest("GET", v.URL+"/api/v1/sta_status?_="+strconv.FormatInt(makeTimestamp(), 10), "")
if err != nil {
return nil, err
}
stationStatusReponse := &StationStatusReponse{}
return stationStatusReponse, json.Unmarshal(responseBody, stationStatusReponse)
}
func (v *VodafoneStation) GetCallLog() (*CallLog, error) {
responseBody, err := v.doRequest("GET", v.URL+"/api/v1/phone_calllog/1,2/CallTbl?_="+strconv.FormatInt(makeTimestamp(), 10), "")
if err != nil {
return nil, err
}
callLog := &CallLog{}
err = json.Unmarshal(responseBody, callLog)
if err != nil {
return nil, err
}
callLog.Lines = map[string]*PhoneNumberCallLog{"0": callLog.Line0, "1": callLog.Line1}
return callLog, nil
}
func (v *VodafoneStation) GetLedSetting() (*LedSettingResponse, error) {
responseBody, err := v.doRequest("GET", v.URL+"/api/v1/set_device?_="+strconv.FormatInt(makeTimestamp(), 10), "")
if err != nil {
return nil, err
}
ledSettingResponse := &LedSettingResponse{}
return ledSettingResponse, json.Unmarshal(responseBody, ledSettingResponse)
}
func (v *VodafoneStation) GetWanStatus() (*WanStatusResponse, error) {
responseBody, err := v.doRequest("GET", v.URL+"/api/v1/wan?_="+strconv.FormatInt(makeTimestamp(), 10), "")
if err != nil {
return nil, err
}
WanStatusResponse := &WanStatusResponse{}
return WanStatusResponse, json.Unmarshal(responseBody, WanStatusResponse)
}
func (v *VodafoneStation) GetStationAbout() (*StationAboutResponse, error) {
responseBody, err := v.doRequest("GET", v.URL+"/api/v1/sta_about?_="+strconv.FormatInt(makeTimestamp(), 10), "")
if err != nil {
return nil, err
}
stationAboutResponse := &StationAboutResponse{}
return stationAboutResponse, json.Unmarshal(responseBody, stationAboutResponse)
}
func (v *VodafoneStation) GetPhonenumbers() (*PhonenumbersResponse, error) {
responseBody, err := v.doRequest("GET", v.URL+"/api/v1/pho_phone_numbers?_="+strconv.FormatInt(makeTimestamp(), 10), "")
if err != nil {
return nil, err
}
phonenumbersResponse := &PhonenumbersResponse{}
return phonenumbersResponse, json.Unmarshal(responseBody, phonenumbersResponse)
}
func makeTimestamp() int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}
func (v *VodafoneStation) getLoginSalts() (*LoginResponseSalts, error) {
responseBody, err := v.doRequest("POST", v.URL+"/api/v1/session/login", "username=admin&password=seeksalthash&logout=true")
if err != nil {
return nil, err
}
loginResponseSalts := &LoginResponseSalts{}
err = json.Unmarshal(responseBody, loginResponseSalts)
if err != nil {
return nil, err
}
if loginResponseSalts.Error != "ok" {
return nil, fmt.Errorf("Got non error=ok message from vodafone station")
}
return loginResponseSalts, nil
}
func (v *VodafoneStation) doRequest(method, url, body string) ([]byte, error) {
logger := promslog.New(&promslog.Config{})
logger.Debug(
"Performing request",
slog.String("method", method),
slog.String("url", url),
)
requestBody := strings.NewReader(body)
request, err := http.NewRequest(method, url, requestBody)
if err != nil {
logger.Error(
"Creating request failed",
slog.String("err", err.Error()),
)
return nil, err
}
if method == "POST" {
request.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
}
request.Header.Set("Referer", "http://192.168.100.1")
request.Header.Set("X-Requested-With", "XMLHttpRequest")
response, err := v.client.Do(request)
if err != nil {
logger.Error(
"Performing request failed",
slog.String("err", err.Error()),
)
return nil, err
}
if response.Body != nil {
defer response.Body.Close()
}
return ioutil.ReadAll(response.Body)
}
// GetLoginPassword derives the password using the given salts
func GetLoginPassword(password, salt, saltWebUI string) string {
return DoPbkdf2NotCoded(DoPbkdf2NotCoded(password, salt), saltWebUI)
}
// Equivalent to the JS doPbkdf2NotCoded (see README.md)
func DoPbkdf2NotCoded(key, salt string) string {
temp := pbkdf2.Key([]byte(key), []byte(salt), 0x3e8, 0x80, sha256.New)
return hex.EncodeToString(temp[:16])
}