From 986772d525fe89f14040a4bd2cab3fe9dcdbf4d1 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 27 Mar 2025 12:35:13 +0100 Subject: [PATCH] Http: add pluggable authentication and token (#20066) --- plugin/auth/api.go | 7 ++++ plugin/auth/config.go | 26 ++++++++++++ plugin/auth/nop.go | 27 +++++++++++++ plugin/http.go | 54 ++++++++----------------- plugin/http_auth.go | 52 ++++++++++++++++++++++++ templates/definition/meter/enphase.yaml | 22 +++++----- 6 files changed, 140 insertions(+), 48 deletions(-) create mode 100644 plugin/auth/api.go create mode 100644 plugin/auth/config.go create mode 100644 plugin/auth/nop.go create mode 100644 plugin/http_auth.go diff --git a/plugin/auth/api.go b/plugin/auth/api.go new file mode 100644 index 000000000..ec2e235e3 --- /dev/null +++ b/plugin/auth/api.go @@ -0,0 +1,7 @@ +package auth + +import "net/http" + +type Authorizer interface { + Transport(base http.RoundTripper) (http.RoundTripper, error) +} diff --git a/plugin/auth/config.go b/plugin/auth/config.go new file mode 100644 index 000000000..69e7a8d27 --- /dev/null +++ b/plugin/auth/config.go @@ -0,0 +1,26 @@ +package auth + +import ( + "context" + "fmt" + "strings" + + reg "github.com/evcc-io/evcc/util/registry" +) + +var registry = reg.New[Authorizer]("auth") + +// NewFromConfig creates auth from configuration +func NewFromConfig(ctx context.Context, typ string, other map[string]any) (Authorizer, error) { + factory, err := registry.Get(strings.ToLower(typ)) + if err != nil { + return nil, err + } + + v, err := factory(ctx, other) + if err != nil { + err = fmt.Errorf("cannot create auth type '%s': %w", typ, err) + } + + return v, err +} diff --git a/plugin/auth/nop.go b/plugin/auth/nop.go new file mode 100644 index 000000000..e9cf17f30 --- /dev/null +++ b/plugin/auth/nop.go @@ -0,0 +1,27 @@ +package auth + +import ( + "context" + "net/http" + + "github.com/evcc-io/evcc/util" +) + +type nop struct{} + +func init() { + registry.AddCtx("nop", NewNopFromConfig) +} + +func NewNopFromConfig(ctx context.Context, other map[string]any) (Authorizer, error) { + var cc struct{} + if err := util.DecodeOther(other, &cc); err != nil { + return nil, err + } + + return new(nop), nil +} + +func (p *nop) Transport(base http.RoundTripper) (http.RoundTripper, error) { + return base, nil +} diff --git a/plugin/http.go b/plugin/http.go index 0a0b89fe6..eb2532fce 100644 --- a/plugin/http.go +++ b/plugin/http.go @@ -14,7 +14,6 @@ import ( "github.com/evcc-io/evcc/util/request" "github.com/evcc-io/evcc/util/transport" "github.com/gregjones/httpcache" - "github.com/jpfielding/go-http-digest/pkg/digest" ) // HTTP implements HTTP request provider @@ -33,11 +32,6 @@ func init() { var mc = httpcache.NewMemoryCache() -// Auth is the authorization config -type Auth struct { - Type, User, Password string -} - // NewHTTPPluginFromConfig creates a HTTP provider func NewHTTPPluginFromConfig(ctx context.Context, other map[string]interface{}) (Plugin, error) { cc := struct { @@ -80,18 +74,21 @@ func NewHTTPPluginFromConfig(ctx context.Context, other map[string]interface{}) p.getter = defaultGetters(p, cc.Scale) - var err error - if cc.Auth.Type != "" { - _, err = p.WithAuth(cc.Auth.Type, cc.Auth.User, cc.Auth.Password) + if cc.Auth.Type != "" || cc.Auth.Source != "" { + transport, err := cc.Auth.Transport(ctx, p.Client.Transport) + if err != nil { + return nil, err + } + p.Client.Transport = transport } - if err == nil { - var pipe *pipeline.Pipeline - pipe, err = pipeline.New(log, cc.Settings) - p = p.WithPipeline(pipe) + pipe, err := pipeline.New(log, cc.Settings) + if err != nil { + return nil, err } + p.pipeline = pipe - return p, err + return p, nil } // NewHTTP create HTTP provider @@ -128,7 +125,12 @@ func NewHTTP(log *util.Logger, method, uri string, insecure bool, cache time.Dur // WithBody adds request body func (p *HTTP) WithBody(body string) *HTTP { - p.body = body + if body != "" { + p.body = body + if p.method == http.MethodGet { + p.method = http.MethodPost + } + } return p } @@ -138,28 +140,6 @@ func (p *HTTP) WithHeaders(headers map[string]string) *HTTP { return p } -// WithPipeline adds a processing pipeline -func (p *HTTP) WithPipeline(pipeline *pipeline.Pipeline) *HTTP { - p.pipeline = pipeline - return p -} - -// WithAuth adds authorized transport -func (p *HTTP) WithAuth(typ, user, password string) (*HTTP, error) { - switch strings.ToLower(typ) { - case "basic": - p.Client.Transport = transport.BasicAuth(user, password, p.Client.Transport) - case "bearer": - p.Client.Transport = transport.BearerAuth(password, p.Client.Transport) - case "digest": - p.Client.Transport = digest.NewTransport(user, password, p.Client.Transport) - default: - return nil, fmt.Errorf("unknown auth type '%s'", typ) - } - - return p, nil -} - // request executes the configured request or returns the cached value func (p *HTTP) request(url string, body string) ([]byte, error) { var b io.Reader diff --git a/plugin/http_auth.go b/plugin/http_auth.go new file mode 100644 index 000000000..e5b78c02b --- /dev/null +++ b/plugin/http_auth.go @@ -0,0 +1,52 @@ +package plugin + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/evcc-io/evcc/plugin/auth" + "github.com/evcc-io/evcc/util/transport" + "github.com/jpfielding/go-http-digest/pkg/digest" +) + +// Auth is the authorization config +type Auth struct { + Type, User, Password, Token string + + Source string + Other map[string]any `mapstructure:",remain"` +} + +func (p *Auth) Transport(ctx context.Context, base http.RoundTripper) (http.RoundTripper, error) { + switch strings.ToLower(p.Type) { + case "digest": + return digest.NewTransport(p.User, p.Password, base), nil + + case "basic": + return transport.BasicAuth(p.User, p.Password, base), nil + + case "bearer": + return transport.BearerAuth(p.Token, base), nil + + default: + if p.Source == "" { + return nil, fmt.Errorf("unknown auth type '%s'", p.Type) + } + + if p.User != "" { + p.Other["user"] = p.User + } + if p.Password != "" { + p.Other["password"] = p.Password + } + + authorizer, err := auth.NewFromConfig(ctx, p.Source, p.Other) + if err != nil { + return nil, err + } + + return authorizer.Transport(base) + } +} diff --git a/templates/definition/meter/enphase.yaml b/templates/definition/meter/enphase.yaml index 440f8db9e..bd586327d 100644 --- a/templates/definition/meter/enphase.yaml +++ b/templates/definition/meter/enphase.yaml @@ -29,7 +29,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }} @@ -40,7 +40,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }} @@ -50,7 +50,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }} @@ -60,7 +60,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }} @@ -73,7 +73,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }} @@ -84,7 +84,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }} @@ -96,7 +96,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }} @@ -106,7 +106,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }} @@ -116,7 +116,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }} @@ -129,7 +129,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }} @@ -140,7 +140,7 @@ render: | {{- if .token }} auth: type: bearer - password: {{ .token }} + token: {{ .token }} insecure: true {{- end }} cache: {{ .cache }}