sberk42
4 years ago
2 changed files with 502 additions and 114 deletions
@ -0,0 +1,318 @@ |
|||
// client for fritzbox lua API
|
|||
package lua_client |
|||
|
|||
// Copyright 2020 Andreas Krebs
|
|||
//
|
|||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
// you may not use this file except in compliance with the License.
|
|||
// You may obtain a copy of the License at
|
|||
//
|
|||
// http://www.apache.org/licenses/LICENSE-2.0
|
|||
//
|
|||
// Unless required by applicable law or agreed to in writing, software
|
|||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
// See the License for the specific language governing permissions and
|
|||
// limitations under the License.
|
|||
|
|||
import ( |
|||
"bytes" |
|||
"crypto/md5" |
|||
"encoding/json" |
|||
"encoding/xml" |
|||
"errors" |
|||
"fmt" |
|||
"io/ioutil" |
|||
"net/http" |
|||
"regexp" |
|||
"strconv" |
|||
"strings" |
|||
|
|||
"golang.org/x/text/encoding/unicode" |
|||
"golang.org/x/text/transform" |
|||
) |
|||
|
|||
// session XML from login_sid.lua
|
|||
type SessionInfo struct { |
|||
SID string `xml:"SID"` |
|||
Challenge string `xml:"Challenge"` |
|||
BlockTime int `xml:"BlockTime"` |
|||
Rights string `xml:"Rights"` |
|||
} |
|||
|
|||
type LuaSession struct { |
|||
BaseUrl string |
|||
Username string |
|||
Password string |
|||
SID string |
|||
SessionInfo SessionInfo |
|||
} |
|||
|
|||
type LuaPage struct { |
|||
Path string |
|||
Params string |
|||
} |
|||
|
|||
type LuaMetricValueDefinition struct { |
|||
Path string |
|||
Key string |
|||
Labels []string |
|||
} |
|||
|
|||
type LuaMetricValue struct { |
|||
Name string |
|||
Value string |
|||
Labels map[string]string |
|||
} |
|||
|
|||
type LabelRename struct { |
|||
Pattern regexp.Regexp |
|||
Name string |
|||
} |
|||
|
|||
func (lua *LuaSession) do_Login(response string) error { |
|||
url_params := "" |
|||
if response != "" { |
|||
url_params = fmt.Sprintf("?response=%s&user=%s", response, lua.Username) |
|||
} |
|||
|
|||
resp, err := http.Get(fmt.Sprintf("%s/login_sid.lua%s", lua.BaseUrl, url_params)) |
|||
if err != nil { |
|||
return errors.New(fmt.Sprintf("Error calling login_sid.lua: %s", err.Error())) |
|||
} |
|||
|
|||
defer resp.Body.Close() |
|||
dec := xml.NewDecoder(resp.Body) |
|||
|
|||
err = dec.Decode(&lua.SessionInfo) |
|||
if err != nil { |
|||
return errors.New(fmt.Sprintf("Error decoding SessionInfo: %s", err.Error())) |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (lua *LuaSession) Login() error { |
|||
|
|||
err := lua.do_Login("") |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
challenge := lua.SessionInfo.Challenge |
|||
if lua.SessionInfo.SID == "0000000000000000" && challenge != "" { |
|||
// no SID, but challenge so calc response
|
|||
hash := utf16leMd5(fmt.Sprintf("%s-%s", challenge, lua.Password)) |
|||
response := fmt.Sprintf("%s-%x", challenge, hash) |
|||
err := lua.do_Login(response) |
|||
|
|||
if err != nil { |
|||
return err |
|||
} |
|||
} |
|||
|
|||
sid := lua.SessionInfo.SID |
|||
if sid == "0000000000000000" || sid == "" { |
|||
return errors.New("LUA login failed - no SID received - check username and password") |
|||
} |
|||
|
|||
lua.SID = sid |
|||
|
|||
return nil |
|||
} |
|||
|
|||
func (lua *LuaSession) LoadData(page LuaPage) ([]byte, error) { |
|||
data_url := fmt.Sprintf("%s/%s", lua.BaseUrl, page.Path) |
|||
|
|||
callDone := false |
|||
var resp *http.Response |
|||
var err error |
|||
for !callDone { |
|||
// perform login if no SID or previous call failed with (403)
|
|||
if lua.SID == "" || resp != nil { |
|||
err = lua.Login() |
|||
callDone = true // consider call done, since we tried login
|
|||
|
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
} |
|||
|
|||
// send by UI for data.lua: xhr=1&sid=xxxxxxx&lang=de&page=energy&xhrId=all&no_sidrenew=
|
|||
// but SID and page seem to be enough
|
|||
params := "sid=" + lua.SID |
|||
if page.Params != "" { |
|||
params += "&" + page.Params |
|||
} |
|||
|
|||
resp, err = http.Post(data_url, "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(params))) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
defer resp.Body.Close() |
|||
|
|||
if resp.StatusCode == http.StatusOK { |
|||
callDone = true |
|||
} else if resp.StatusCode == http.StatusForbidden && !callDone { |
|||
// we assume SID is expired, so retry login
|
|||
} else { |
|||
return nil, errors.New(fmt.Sprintf("data.lua failed: %s", resp.Status)) |
|||
} |
|||
} |
|||
|
|||
body, err := ioutil.ReadAll(resp.Body) |
|||
|
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return body, nil |
|||
} |
|||
|
|||
func getRenamedLabel(labelRenames *[]LabelRename, label string) string { |
|||
if labelRenames != nil { |
|||
for _, lblRen := range *labelRenames { |
|||
if lblRen.Pattern.MatchString(label) { |
|||
return lblRen.Name |
|||
} |
|||
} |
|||
} |
|||
|
|||
return label |
|||
} |
|||
|
|||
func GetMetrics(labelRenames *[]LabelRename, data map[string]interface{}, metricDef LuaMetricValueDefinition) ([]LuaMetricValue, error) { |
|||
|
|||
var values []map[string]interface{} |
|||
var err error |
|||
if metricDef.Path != "" { |
|||
pathItems := strings.Split(metricDef.Path, ".") |
|||
values, err = _getValues(data, pathItems, "") |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
} else { |
|||
values = make([]map[string]interface{}, 1) |
|||
values[0] = data |
|||
} |
|||
|
|||
name := metricDef.Path |
|||
if name != "" { |
|||
name += "." |
|||
} |
|||
name += metricDef.Key |
|||
|
|||
metrics := make([]LuaMetricValue, 0) |
|||
for _, valMap := range values { |
|||
value, exists := valMap[metricDef.Key] |
|||
if exists { |
|||
lmv := LuaMetricValue{ |
|||
Name: name, |
|||
Value: toString(value), |
|||
Labels: make(map[string]string), |
|||
} |
|||
|
|||
for _, l := range metricDef.Labels { |
|||
lv, exists := valMap[l] |
|||
if exists { |
|||
lmv.Labels[l] = getRenamedLabel(labelRenames, toString(lv)) |
|||
} |
|||
} |
|||
|
|||
metrics = append(metrics, lmv) |
|||
} |
|||
} |
|||
|
|||
return metrics, nil |
|||
} |
|||
|
|||
// from https://stackoverflow.com/questions/33710672/golang-encode-string-utf16-little-endian-and-hash-with-md5
|
|||
func utf16leMd5(s string) []byte { |
|||
enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() |
|||
hasher := md5.New() |
|||
t := transform.NewWriter(hasher, enc) |
|||
t.Write([]byte(s)) |
|||
return hasher.Sum(nil) |
|||
} |
|||
|
|||
func ParseJSON(jsonData []byte) (map[string]interface{}, error) { |
|||
var data map[string]interface{} |
|||
|
|||
// Unmarshal or Decode the JSON to the interface.
|
|||
json.Unmarshal(jsonData, &data) |
|||
|
|||
return data, nil |
|||
} |
|||
|
|||
// helper for retrieving values from parsed JSON
|
|||
func _getValues(data interface{}, pathItems []string, parentPath string) ([]map[string]interface{}, error) { |
|||
|
|||
value := data |
|||
curPath := parentPath |
|||
|
|||
for i, p := range pathItems { |
|||
switch vv := value.(type) { |
|||
case []interface{}: |
|||
if p == "*" { |
|||
|
|||
values := make([]map[string]interface{}, 0, len(vv)) |
|||
for index, u := range vv { |
|||
subvals, err := _getValues(u, pathItems[i+1:], fmt.Sprintf("%s.%d", curPath, index)) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
values = append(values, subvals...) |
|||
} |
|||
|
|||
return values, nil |
|||
} else { |
|||
index, err := strconv.Atoi(p) |
|||
if err != nil { |
|||
return nil, errors.New(fmt.Sprintf("item '%s' is an array, but path item '%s' is neither '*' nor a number", curPath, p)) |
|||
} |
|||
|
|||
if index < 0 { |
|||
// this is an index from the end of the values
|
|||
index += len(vv) |
|||
} |
|||
|
|||
if index >= 0 && index < len(vv) { |
|||
value = vv[index] |
|||
} else { |
|||
return nil, errors.New(fmt.Sprintf("index %d is invalid for array '%s' with length %d", index, curPath, len(vv))) |
|||
} |
|||
} |
|||
|
|||
case map[string]interface{}: |
|||
var exits bool |
|||
value, exits = vv[p] |
|||
if !exits { |
|||
return nil, errors.New(fmt.Sprintf("key '%s' not existing in hash '%s'", p, curPath)) |
|||
} |
|||
|
|||
default: |
|||
return nil, errors.New(fmt.Sprintf("item '%s' is neither a hash or array", curPath)) |
|||
} |
|||
|
|||
if curPath == "" { |
|||
curPath = p |
|||
} else { |
|||
curPath += "." + p |
|||
} |
|||
} |
|||
|
|||
vm, isType := value.(map[string]interface{}) |
|||
if !isType { |
|||
return nil, errors.New(fmt.Sprintf("item '%s' is not a hash", curPath)) |
|||
} |
|||
|
|||
values := make([]map[string]interface{}, 1) |
|||
values[0] = vm |
|||
return values, nil |
|||
} |
|||
|
|||
func toString(value interface{}) string { |
|||
// should we better check or simple convert everything ????
|
|||
return fmt.Sprintf("%v", value) |
|||
} |
Loading…
Reference in new issue