Use digest package instead of own implementation
Signed-off-by: Marcel Goerentz <m.goerentz@t-online.de>
This commit is contained in:
parent
27a512ff45
commit
c3cd6e41cd
4 changed files with 22 additions and 143 deletions
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/evcc-io/evcc/charger/warp"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
"github.com/icholy/digest"
|
||||
)
|
||||
|
||||
type WarpWS struct {
|
||||
|
|
@ -116,10 +117,9 @@ func NewWarpWSFromConfig(ctx context.Context, other map[string]any) (api.Charger
|
|||
}
|
||||
wb.pmHelper = request.NewHelper(wb.log)
|
||||
if cc.EnergyManagerUser != "" {
|
||||
wb.pmHelper.Client.Transport = &warp.DigestTransport{
|
||||
wb.pmHelper.Client.Transport = &digest.Transport{
|
||||
Username: cc.EnergyManagerUser,
|
||||
Password: cc.EnergyManagerPassword,
|
||||
Base: wb.pmHelper.Client.Transport,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -159,10 +159,9 @@ func NewWarpWS(ctx context.Context, uri, user, password string, meterIndex uint)
|
|||
|
||||
client := request.NewHelper(log)
|
||||
if user != "" {
|
||||
client.Client.Transport = &warp.DigestTransport{
|
||||
client.Client.Transport = &digest.Transport{
|
||||
Username: user,
|
||||
Password: password,
|
||||
Base: client.Client.Transport,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +202,7 @@ func (w *WarpWS) run(uri, user, pass string, ctx context.Context) {
|
|||
if conn != nil {
|
||||
conn.Close(websocket.StatusInternalError, "dial failed")
|
||||
}
|
||||
w.log.ERROR.Printf("ws dial failed: %v", err)
|
||||
w.log.ERROR.Printf("ws dial to %s failed: %v", uri, err)
|
||||
} else {
|
||||
w.log.DEBUG.Printf("ws connected to %s", uri)
|
||||
bo.Reset()
|
||||
|
|
@ -234,22 +233,29 @@ func dialWebsocket(ctx context.Context, wsURL, user, pass string) (*websocket.Co
|
|||
}
|
||||
|
||||
// Extract challeng from response
|
||||
if resp == nil {
|
||||
return nil, nil, fmt.Errorf("no response on websocket dial")
|
||||
}
|
||||
www := resp.Header.Get("WWW-Authenticate")
|
||||
resp.Body.Close()
|
||||
|
||||
ch, err := warp.ParseDigestChallenge(www)
|
||||
chal, err := digest.ParseChallenge(www)
|
||||
if err != nil {
|
||||
return nil, resp, fmt.Errorf("digest parse error: %w", err)
|
||||
}
|
||||
|
||||
// Build authorization header
|
||||
u, _ := url.Parse(wsURL)
|
||||
auth := warp.BuildDigestAuthHeader(ch, "GET", u.Path, user, pass)
|
||||
cred, _ := digest.Digest(chal, digest.Options{
|
||||
Username: user,
|
||||
Password: pass,
|
||||
Method: "GET",
|
||||
URI: wsURL,
|
||||
Count: 1,
|
||||
})
|
||||
resp.Body.Close()
|
||||
|
||||
// Dial with Digest Auth
|
||||
dialer := websocket.DialOptions{
|
||||
HTTPHeader: http.Header{
|
||||
"Authorization": []string{auth},
|
||||
"Authorization": []string{cred.String()},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,132 +0,0 @@
|
|||
package warp
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type DigestTransport struct {
|
||||
Username string
|
||||
Password string
|
||||
Base http.RoundTripper
|
||||
mu sync.Mutex
|
||||
challenge *DigestChallenge
|
||||
}
|
||||
|
||||
type DigestChallenge struct {
|
||||
Realm string
|
||||
Nonce string
|
||||
Qop string
|
||||
Opaque string
|
||||
Algorithm string
|
||||
}
|
||||
|
||||
func ParseDigestChallenge(h string) (*DigestChallenge, error) {
|
||||
dc := &DigestChallenge{}
|
||||
parts := strings.Split(h, ",")
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if strings.HasPrefix(p, "Digest ") {
|
||||
p = strings.TrimPrefix(p, "Digest ")
|
||||
}
|
||||
kv := strings.SplitN(p, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(kv[0])
|
||||
val := strings.Trim(kv[1], `"`)
|
||||
switch key {
|
||||
case "realm":
|
||||
dc.Realm = val
|
||||
case "nonce":
|
||||
dc.Nonce = val
|
||||
case "opaque":
|
||||
dc.Opaque = val
|
||||
case "qop":
|
||||
dc.Qop = val
|
||||
case "algorithm":
|
||||
dc.Algorithm = val
|
||||
}
|
||||
}
|
||||
return dc, nil
|
||||
}
|
||||
|
||||
func BuildDigestAuthHeader(ch *DigestChallenge, method, uri, user, pass string) string {
|
||||
ha1 := md5Hex(fmt.Sprintf("%s:%s:%s", user, ch.Realm, pass))
|
||||
ha2 := md5Hex(fmt.Sprintf("%s:%s", method, uri))
|
||||
|
||||
cnonce := randomHex(16)
|
||||
nc := "00000001"
|
||||
|
||||
response := md5Hex(fmt.Sprintf("%s:%s:%s:%s:%s:%s",
|
||||
ha1, ch.Nonce, nc, cnonce, ch.Qop, ha2))
|
||||
|
||||
return fmt.Sprintf(
|
||||
`Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm="MD5", response="%s", qop=%s, nc=%s, cnonce="%s"`,
|
||||
user, ch.Realm, ch.Nonce, uri, response, ch.Qop, nc, cnonce,
|
||||
)
|
||||
}
|
||||
|
||||
func md5Hex(s string) string {
|
||||
h := md5.Sum([]byte(s))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func randomHex(n int) string {
|
||||
b := make([]byte, n)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func (t *DigestTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if t.Base == nil {
|
||||
t.Base = http.DefaultTransport
|
||||
}
|
||||
// If we already have a challenge → send digest directly
|
||||
t.mu.Lock()
|
||||
ch := t.challenge
|
||||
t.mu.Unlock()
|
||||
if ch != nil {
|
||||
return t.roundTripWithDigest(req, ch)
|
||||
}
|
||||
// 1. First try without Auth
|
||||
resp, err := t.Base.RoundTrip(req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
return resp, nil
|
||||
}
|
||||
// 2. Parse challenge
|
||||
hdr := resp.Header.Get("WWW-Authenticate")
|
||||
ch, err = ParseDigestChallenge(hdr)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
// Save challenge
|
||||
t.mu.Lock()
|
||||
t.challenge = ch
|
||||
t.mu.Unlock()
|
||||
// Reread body (if necessery)
|
||||
if req.Body != nil {
|
||||
bodyBytes, _ := io.ReadAll(req.Body)
|
||||
req.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))
|
||||
}
|
||||
// 3. Second try with Digest Auth
|
||||
return t.roundTripWithDigest(req, ch)
|
||||
}
|
||||
|
||||
func (t *DigestTransport) roundTripWithDigest(req *http.Request, ch *DigestChallenge) (*http.Response, error) {
|
||||
// Copy request
|
||||
r2 := req.Clone(req.Context())
|
||||
uri := r2.URL.RequestURI()
|
||||
auth := BuildDigestAuthHeader(ch, r2.Method, uri, t.Username, t.Password)
|
||||
r2.Header.Set("Authorization", auth)
|
||||
return t.Base.RoundTrip(r2)
|
||||
}
|
||||
1
go.mod
1
go.mod
|
|
@ -55,6 +55,7 @@ require (
|
|||
github.com/hashicorp/go-version v1.8.0
|
||||
github.com/hasura/go-graphql-client v0.15.1
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
||||
github.com/icholy/digest v1.1.0
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.14.0
|
||||
github.com/insomniacslk/tapo v1.0.2
|
||||
github.com/itchyny/gojq v0.12.18
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -399,6 +399,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
|
|||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
||||
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
|
|
@ -1066,6 +1068,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue