Use digest package instead of own implementation

Signed-off-by: Marcel Goerentz <m.goerentz@t-online.de>
This commit is contained in:
Marcel Goerentz 2026-02-24 10:45:02 +01:00
parent 27a512ff45
commit c3cd6e41cd
No known key found for this signature in database
4 changed files with 22 additions and 143 deletions

View file

@ -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()},
},
}

View file

@ -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
View file

@ -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
View file

@ -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=