diff --git a/charger/mcc.go b/charger/mcc.go index cbf9f5924..dbfe1a2d6 100644 --- a/charger/mcc.go +++ b/charger/mcc.go @@ -12,6 +12,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" ) const ( @@ -87,7 +88,7 @@ func NewMobileConnect(uri string, password string) (*MobileConnect, error) { } // ignore the self signed certificate - mcc.Client.Transport = request.NewTripper(log, request.InsecureTransport()) + mcc.Client.Transport = request.NewTripper(log, transport.Insecure()) return mcc, nil } diff --git a/charger/shelly.go b/charger/shelly.go index 35472793a..42715c96e 100644 --- a/charger/shelly.go +++ b/charger/shelly.go @@ -9,8 +9,8 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/charger/shelly" "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/basicauth" "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" "github.com/jpfielding/go-http-digest/pkg/digest" ) @@ -72,7 +72,7 @@ func NewShelly(uri, user, password string, channel int, standbypower float64) (* gen: resp.Gen, } - c.Client.Transport = request.NewTripper(log, request.InsecureTransport()) + c.Client.Transport = request.NewTripper(log, transport.Insecure()) if (resp.Auth || resp.AuthEn) && (user == "" || password == "") { return c, fmt.Errorf("%s (%s) missing user/password", resp.Model, resp.Mac) @@ -84,7 +84,7 @@ func NewShelly(uri, user, password string, channel int, standbypower float64) (* // https://shelly-api-docs.shelly.cloud/gen1/#shelly-family-overview c.uri = util.DefaultScheme(uri, "http") if user != "" { - c.Client.Transport = basicauth.NewTransport(user, password, c.Client.Transport) + c.Client.Transport = transport.BasicAuth(user, password, c.Client.Transport) } if resp.NumMeters == 0 { diff --git a/charger/tasmota.go b/charger/tasmota.go index c4e24b638..6d4101154 100644 --- a/charger/tasmota.go +++ b/charger/tasmota.go @@ -10,6 +10,7 @@ import ( "github.com/evcc-io/evcc/charger/tasmota" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" ) // Tasmota project homepage @@ -57,7 +58,7 @@ func NewTasmota(uri, user, password string, standbypower float64) (*Tasmota, err password: password, standbypower: standbypower, } - c.Client.Transport = request.NewTripper(log, request.InsecureTransport()) + c.Client.Transport = request.NewTripper(log, transport.Insecure()) return c, nil } diff --git a/meter/discovergy.go b/meter/discovergy.go index daa428c66..893459700 100644 --- a/meter/discovergy.go +++ b/meter/discovergy.go @@ -7,8 +7,8 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/provider" "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/basicauth" "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" "github.com/thoas/go-funk" ) @@ -42,7 +42,7 @@ func NewDiscovergyFromConfig(other map[string]interface{}) (api.Meter, error) { log := util.NewLogger("discgy").Redact(cc.User, cc.Password, cc.Meter) client := request.NewHelper(log) - client.Transport = basicauth.NewTransport(cc.User, cc.Password, client.Transport) + client.Transport = transport.BasicAuth(cc.User, cc.Password, client.Transport) var meters []discovergyMeter if err := client.GetJSON(fmt.Sprintf("%s/meters", discovergyAPI), &meters); err != nil { diff --git a/meter/lgpcs/lgpcs.go b/meter/lgpcs/lgpcs.go index 3e625a2fb..871acee5c 100644 --- a/meter/lgpcs/lgpcs.go +++ b/meter/lgpcs/lgpcs.go @@ -13,6 +13,7 @@ import ( "github.com/evcc-io/evcc/provider" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" ) // URIs @@ -63,7 +64,7 @@ func GetInstance(uri, password string, cache time.Duration) (*Com, error) { } // ignore the self signed certificate - instance.Client.Transport = request.NewTripper(log, request.InsecureTransport()) + instance.Client.Transport = request.NewTripper(log, transport.Insecure()) // caches the data access for the "cache" time duration // sends a new request to the pcs if the cache is expired and Data() requested diff --git a/meter/tesla.go b/meter/tesla.go index 2f93c5bfa..c43c35271 100644 --- a/meter/tesla.go +++ b/meter/tesla.go @@ -12,6 +12,7 @@ import ( "github.com/evcc-io/evcc/meter/powerwall" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" ) // credits to https://github.com/vloschiavo/powerwall2 @@ -74,7 +75,7 @@ func NewTesla(uri, usage, password string) (api.Meter, error) { } // ignore the self signed certificate - m.Client.Transport = request.NewTripper(log, request.InsecureTransport()) + m.Client.Transport = request.NewTripper(log, transport.Insecure()) // create cookie jar to save login tokens m.Client.Jar, _ = cookiejar.New(nil) diff --git a/provider/http.go b/provider/http.go index 6446eb303..b880ba85e 100644 --- a/provider/http.go +++ b/provider/http.go @@ -10,9 +10,9 @@ import ( "time" "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/basicauth" "github.com/evcc-io/evcc/util/jq" "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" "github.com/itchyny/gojq" "github.com/jpfielding/go-http-digest/pkg/digest" ) @@ -107,7 +107,7 @@ func NewHTTP(log *util.Logger, method, uri string, insecure bool, scale float64, // ignore the self signed certificate if insecure { - p.Client.Transport = request.NewTripper(log, request.InsecureTransport()) + p.Client.Transport = request.NewTripper(log, transport.Insecure()) } return p @@ -153,7 +153,7 @@ func (p *HTTP) WithJq(jq string) (*HTTP, error) { func (p *HTTP) WithAuth(typ, user, password string) (*HTTP, error) { switch strings.ToLower(typ) { case "basic": - p.Client.Transport = basicauth.NewTransport(user, password, p.Client.Transport) + p.Client.Transport = transport.BasicAuth(user, password, p.Client.Transport) case "digest": p.Client.Transport = digest.NewTransport(user, password, p.Client.Transport) default: diff --git a/provider/socket.go b/provider/socket.go index 1201da505..433d72f49 100644 --- a/provider/socket.go +++ b/provider/socket.go @@ -8,9 +8,9 @@ import ( "time" "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/basicauth" "github.com/evcc-io/evcc/util/jq" "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" "github.com/gorilla/websocket" "github.com/itchyny/gojq" ) @@ -70,12 +70,12 @@ func NewSocketProviderFromConfig(other map[string]interface{}) (IntProvider, err // handle basic auth if cc.Auth.Type != "" { - p.headers["Authorization"] = basicauth.Header(cc.Auth.User, cc.Auth.Password) + p.headers["Authorization"] = transport.BasicAuthHeader(cc.Auth.User, cc.Auth.Password) } // ignore the self signed certificate if cc.Insecure { - p.Client.Transport = request.NewTripper(log, request.InsecureTransport()) + p.Client.Transport = request.NewTripper(log, transport.Insecure()) } if cc.Jq != "" { diff --git a/util/basicauth/transport.go b/util/basicauth/transport.go deleted file mode 100644 index de79405fc..000000000 --- a/util/basicauth/transport.go +++ /dev/null @@ -1,30 +0,0 @@ -package basicauth - -import ( - "encoding/base64" - "net/http" -) - -type transport struct { - header string - base http.RoundTripper -} - -// Header returns the basic auth header -func Header(user, password string) string { - return "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) -} - -// NewTransport creates an http transport performing basic auth -func NewTransport(user, password string, base http.RoundTripper) http.RoundTripper { - return &transport{ - header: Header(user, password), - base: base, - } -} - -// RoundTrip implements the http.RoundTripper interface -func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Set("Authorization", t.header) - return t.base.RoundTrip(req) -} diff --git a/util/fritzdect/fritzdect.go b/util/fritzdect/fritzdect.go index 12401432c..450c6eae7 100644 --- a/util/fritzdect/fritzdect.go +++ b/util/fritzdect/fritzdect.go @@ -14,6 +14,7 @@ import ( "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" "golang.org/x/text/encoding/unicode" ) @@ -70,7 +71,7 @@ func NewConnection(uri, ain, user, password string) (*Connection, error) { Settings: settings, } - fritzdect.Client.Transport = request.NewTripper(log, request.InsecureTransport()) + fritzdect.Client.Transport = request.NewTripper(log, transport.Insecure()) return fritzdect, nil } diff --git a/util/transport/basicauth.go b/util/transport/basicauth.go new file mode 100644 index 000000000..3d9af39e0 --- /dev/null +++ b/util/transport/basicauth.go @@ -0,0 +1,21 @@ +package transport + +import ( + "encoding/base64" + "net/http" +) + +// BasicAuthHeader returns the basic auth header +func BasicAuthHeader(user, password string) string { + return "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password)) +} + +// BasicAuth creates an http transport performing basic auth +func BasicAuth(user, password string, base http.RoundTripper) http.RoundTripper { + return &Decorator{ + Decorator: DecorateHeaders(map[string]string{ + "Authorization": BasicAuthHeader(user, password), + }), + Base: base, + } +} diff --git a/util/transport/decorator.go b/util/transport/decorator.go new file mode 100644 index 000000000..64f1f5f92 --- /dev/null +++ b/util/transport/decorator.go @@ -0,0 +1,59 @@ +package transport + +import ( + "errors" + "net/http" +) + +// Decorator is an http.RoundTripper that makes HTTP requests, +// wrapping a base RoundTripper and modifying given base requests. +type Decorator struct { + // Decorator modifies the outgoing request + Decorator func(*http.Request) error + + // Base is the base RoundTripper used to make HTTP requests. + // If nil, http.DefaultTransport is used. + Base http.RoundTripper +} + +// RoundTrip decorates the request using the Decorator. +func (t *Decorator) RoundTrip(req *http.Request) (*http.Response, error) { + reqBodyClosed := false + if req.Body != nil { + defer func() { + if !reqBodyClosed { + req.Body.Close() + } + }() + } + + if t.Decorator == nil { + return nil, errors.New("transport: Transport's Decorator is nil") + } + + req2 := cloneRequest(req) // per RoundTripper contract + if err := t.Decorator(req2); err != nil { + return nil, err + } + + // req.Body is assumed to be closed by the base RoundTripper. + reqBodyClosed = true + return t.base().RoundTrip(req2) +} + +func (t *Decorator) base() http.RoundTripper { + if t.Base != nil { + return t.Base + } + return Default() +} + +// cloneRequest returns a clone of the provided *http.Request. +// The clone is a shallow copy of the struct and its Header map. +func cloneRequest(r *http.Request) *http.Request { + // shallow copy of the struct + r2 := new(http.Request) + *r2 = *r + r2.Header = r.Header.Clone() + return r2 +} diff --git a/util/request/transport.go b/util/transport/default.go similarity index 59% rename from util/request/transport.go rename to util/transport/default.go index 17cc5ba8f..4e9ed1867 100644 --- a/util/request/transport.go +++ b/util/transport/default.go @@ -1,12 +1,12 @@ -package request +package transport import ( "crypto/tls" "net/http" ) -// DefaultTransport returns http.DefaultTransport as http.Transport instead of http.RoundTripper -func DefaultTransport() *http.Transport { +// Default returns http.DefaultTransport as http.Transport instead of http.RoundTripper +func Default() *http.Transport { t, ok := http.DefaultTransport.(*http.Transport) if !ok { panic("http.DefaultTransport is not an http.Transport") @@ -15,8 +15,8 @@ func DefaultTransport() *http.Transport { } // InsecureTransport is an http.Transport with TLSClientConfig.InsecureSkipVerify enabled -func InsecureTransport() *http.Transport { - t := DefaultTransport() +func Insecure() *http.Transport { + t := Default() t.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} return t } diff --git a/util/transport/headers.go b/util/transport/headers.go new file mode 100644 index 000000000..8320fc67f --- /dev/null +++ b/util/transport/headers.go @@ -0,0 +1,12 @@ +package transport + +import "net/http" + +func DecorateHeaders(headers map[string]string) func(req *http.Request) error { + return func(req *http.Request) error { + for k, v := range headers { + req.Header.Set(k, v) + } + return nil + } +} diff --git a/vehicle/volvo.go b/vehicle/volvo.go index c920bd6ca..677a075fd 100644 --- a/vehicle/volvo.go +++ b/vehicle/volvo.go @@ -1,15 +1,14 @@ package vehicle import ( - "encoding/base64" "fmt" - "net/http" "time" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/provider" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/request" + "github.com/evcc-io/evcc/util/transport" "github.com/evcc-io/evcc/vehicle/volvo" ) @@ -17,8 +16,8 @@ import ( type Volvo struct { *embed *request.Helper - user, password, vin string - statusG func() (interface{}, error) + vin string + statusG func() (interface{}, error) } func init() { @@ -42,14 +41,26 @@ func NewVolvoFromConfig(other map[string]interface{}) (api.Vehicle, error) { log := util.NewLogger("volvo").Redact(cc.User, cc.Password, cc.VIN) v := &Volvo{ - embed: &cc.embed, - Helper: request.NewHelper(log), - user: cc.User, - password: cc.Password, - vin: cc.VIN, + embed: &cc.embed, + Helper: request.NewHelper(log), + vin: cc.VIN, } - v.statusG = provider.NewCached(v.status, cc.Cache).InterfaceGetter() + v.Client.Transport = &transport.Decorator{ + Base: v.Client.Transport, + Decorator: transport.DecorateHeaders(map[string]string{ + "Authorization": transport.BasicAuthHeader(cc.User, cc.Password), + "Content-Type": "application/json", + "X-Device-Id": "Device", + "X-OS-Type": "Android", + "X-Originator-Type": "App", + "X-OS-Version": "22", + }), + } + + v.statusG = provider.NewCached(func() (interface{}, error) { + return v.status() + }, cc.Cache).InterfaceGetter() var err error if cc.VIN == "" { @@ -62,50 +73,33 @@ func NewVolvoFromConfig(other map[string]interface{}) (api.Vehicle, error) { return v, err } -func (v *Volvo) request(uri string) (*http.Request, error) { - basicAuth := base64.StdEncoding.EncodeToString([]byte(v.user + ":" + v.password)) - - return request.New(http.MethodGet, uri, nil, map[string]string{ - "Authorization": fmt.Sprintf("Basic %s", basicAuth), - "Content-Type": "application/json", - "X-Device-Id": "Device", - "X-OS-Type": "Android", - "X-Originator-Type": "App", - "X-OS-Version": "22", - }) -} - // vehicles implements returns the list of user vehicles func (v *Volvo) vehicles() ([]string, error) { var vehicles []string - req, err := v.request(fmt.Sprintf("%s/customeraccounts", volvo.ApiURI)) - if err == nil { - var res volvo.AccountResponse - err = v.DoJSON(req, &res) + uri := fmt.Sprintf("%s/customeraccounts", volvo.ApiURI) + var res volvo.AccountResponse + err := v.GetJSON(uri, &res) + if err == nil { for _, rel := range res.VehicleRelations { var vehicle volvo.VehicleRelation - if req, err := v.request(rel); err == nil { - if err = v.DoJSON(req, &vehicle); err != nil { - return vehicles, err - } - - vehicles = append(vehicles, vehicle.VehicleID) + if err := v.GetJSON(rel, &vehicle); err != nil { + return vehicles, err } + + vehicles = append(vehicles, vehicle.VehicleID) } } return vehicles, err } -func (v *Volvo) status() (interface{}, error) { +func (v *Volvo) status() (volvo.Status, error) { var res volvo.Status - req, err := v.request(fmt.Sprintf("%s/vehicles/%s/status", volvo.ApiURI, v.vin)) - if err == nil { - err = v.DoJSON(req, &res) - } + uri := fmt.Sprintf("%s/vehicles/%s/status", volvo.ApiURI, v.vin) + err := v.GetJSON(uri, &res) return res, err } @@ -129,7 +123,7 @@ func (v *Volvo) Status() (api.ChargeStatus, error) { switch res.HvBattery.HvBatteryChargeStatusDerived { case "CableNotPluggedInCar": return api.StatusA, nil - case "CablePluggedInCar": + case "CablePluggedInCar", "CablePluggedInCar_FullyCharged": return api.StatusB, nil case "Charging": return api.StatusC, nil