From 8d94166d93556ee2c095afef60f7bb390365b4af Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 23 Nov 2025 18:03:56 +0100 Subject: [PATCH] Config UI: add redirect uri suggestion service (#25478) --- server/network/registry.go | 33 ------------- server/network/service.go | 34 +++++++++++++ templates/definition/charger/viessmann.yaml | 1 + .../definition/vehicle/volvo-connected.yaml | 1 + util/homeassistant/instance.go | 47 ++++++++++++++---- util/homeassistant/oauth2.go | 12 ++--- util/homeassistant/proxy.go | 44 ----------------- util/homeassistant/service.go | 2 +- util/homeassistant/zeroconf.go | 48 ++++++++----------- 9 files changed, 101 insertions(+), 121 deletions(-) delete mode 100644 server/network/registry.go create mode 100644 server/network/service.go delete mode 100644 util/homeassistant/proxy.go diff --git a/server/network/registry.go b/server/network/registry.go deleted file mode 100644 index b3bd0a23d..000000000 --- a/server/network/registry.go +++ /dev/null @@ -1,33 +0,0 @@ -package network - -import ( - "sync" - - "github.com/evcc-io/evcc/api/globalconfig" -) - -var ( - mu sync.Mutex - registry []func() -) - -var config globalconfig.Network - -func Config() globalconfig.Network { - return config -} - -func Register(fun func()) { - mu.Lock() - defer mu.Unlock() - - registry = append(registry, fun) -} - -func Start(conf globalconfig.Network) { - config = conf - - for _, fun := range registry { - go fun() - } -} diff --git a/server/network/service.go b/server/network/service.go new file mode 100644 index 000000000..fa5441407 --- /dev/null +++ b/server/network/service.go @@ -0,0 +1,34 @@ +package network + +import ( + "encoding/json" + "net/http" + + "github.com/evcc-io/evcc/api/globalconfig" + "github.com/evcc-io/evcc/server/service" +) + +var config globalconfig.Network + +const CallbackPath = "/providerauth/callback" + +func init() { + // auth service is registered here to avoid import cycle + mux := http.NewServeMux() + mux.HandleFunc("GET /redirecturi", getRedirectUri) + + service.Register("auth", mux) +} + +func Start(conf globalconfig.Network) { + config = conf +} + +func Config() globalconfig.Network { + return config +} + +func getRedirectUri(w http.ResponseWriter, req *http.Request) { + uri := config.ExternalURL() + CallbackPath + json.NewEncoder(w).Encode([]string{uri}) +} diff --git a/templates/definition/charger/viessmann.yaml b/templates/definition/charger/viessmann.yaml index 7845b1de8..b05966a6a 100644 --- a/templates/definition/charger/viessmann.yaml +++ b/templates/definition/charger/viessmann.yaml @@ -29,6 +29,7 @@ params: help: en: "Redirect URI of the evcc instance. Must match the redirect URI set in the Viessmann developer portal." de: "Redirect-URI der evcc-Instanz. Muss mit der Redirect URI übereinstimmen, die Viessmann Developer Portal konfiguriert ist." + service: auth/redirecturi example: "https://evcc.example.org/providerauth/callback" - name: gateway_serial required: true diff --git a/templates/definition/vehicle/volvo-connected.yaml b/templates/definition/vehicle/volvo-connected.yaml index 200580485..54ade382d 100644 --- a/templates/definition/vehicle/volvo-connected.yaml +++ b/templates/definition/vehicle/volvo-connected.yaml @@ -50,6 +50,7 @@ params: help: en: "Redirect URI of your evcc instance. Must match the redirect URI set in your Volvo Developer App." de: "Redirect-URI deiner evcc-Instanz. Muss mit der Redirect-URI übereinstimmen, die in deiner Volvo Developer App festgelegt ist." + service: auth/redirecturi example: "https://evcc.example.org/providerauth/callback" - name: vin example: WF0FXX... diff --git a/util/homeassistant/instance.go b/util/homeassistant/instance.go index d741566b5..936bc78e3 100644 --- a/util/homeassistant/instance.go +++ b/util/homeassistant/instance.go @@ -1,23 +1,50 @@ package homeassistant import ( + "fmt" "sync" "golang.org/x/oauth2" ) -type instance struct { - URI string +type proxyInstance struct { + mu sync.Mutex + home, uri string oauth2.TokenSource } -var ( - mu sync.Mutex - instances = make(map[string]*instance) -) +func (inst *proxyInstance) URI() string { + inst.mu.Lock() + defer inst.mu.Unlock() -func instanceByName(name string) *instance { - mu.Lock() - defer mu.Unlock() - return instances[name] + if inst.uri == "" { + inst.uri = instanceUriByName(inst.home) + + if inst.uri == "" { + return "" + } + } + + return inst.uri +} + +func (inst *proxyInstance) Token() (*oauth2.Token, error) { + uri := inst.URI() + if uri == "" { + return nil, fmt.Errorf("unknown instance: %s", inst.home) + } + + inst.mu.Lock() + defer inst.mu.Unlock() + + if inst.TokenSource == nil { + ts, err := NewHomeAssistant(inst.home, uri) + if err != nil { + return nil, err + } + + inst.TokenSource = ts + } + + return inst.TokenSource.Token() } diff --git a/util/homeassistant/oauth2.go b/util/homeassistant/oauth2.go index 42a3cba4c..25eb01322 100644 --- a/util/homeassistant/oauth2.go +++ b/util/homeassistant/oauth2.go @@ -26,17 +26,17 @@ func NewHomeAssistantFromConfig(other map[string]any) (oauth2.TokenSource, error return nil, err } - inst := instanceByName(cc.Home) - if inst == nil { + uri := instanceUriByName(cc.Home) + if uri == "" { return nil, fmt.Errorf("unknown instance: %s", cc.Home) } - return NewHomeAssistant(cc.Home, inst.URI) + return NewHomeAssistant(cc.Home, uri) } -func NewHomeAssistant(name, uri string) (oauth2.TokenSource, error) { +func NewHomeAssistant(home, uri string) (oauth2.TokenSource, error) { extUrl := network.Config().ExternalURL() - redirectUri := extUrl + "/providerauth/callback" + redirectUri := extUrl + network.CallbackPath log := util.NewLogger("homeassistant") ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log)) @@ -50,5 +50,5 @@ func NewHomeAssistant(name, uri string) (oauth2.TokenSource, error) { }, } - return auth.NewOAuth(ctx, "HomeAssistant", name, &oc) + return auth.NewOAuth(ctx, "HomeAssistant", home, &oc) } diff --git a/util/homeassistant/proxy.go b/util/homeassistant/proxy.go deleted file mode 100644 index c2408b182..000000000 --- a/util/homeassistant/proxy.go +++ /dev/null @@ -1,44 +0,0 @@ -package homeassistant - -import ( - "fmt" - "sync" - - "golang.org/x/oauth2" -) - -type proxyInstance struct { - mu sync.Mutex - home string - instance *instance -} - -func (inst *proxyInstance) URI() string { - inst.mu.Lock() - defer inst.mu.Unlock() - - if inst.instance == nil { - inst.instance = instanceByName(inst.home) - - if inst.instance == nil { - return "" - } - } - - return inst.instance.URI -} - -func (inst *proxyInstance) Token() (*oauth2.Token, error) { - inst.mu.Lock() - defer inst.mu.Unlock() - - if inst.instance == nil { - inst.instance = instanceByName(inst.home) - - if inst.instance == nil { - return nil, fmt.Errorf("unknown instance: %s", inst.home) - } - } - - return inst.instance.Token() -} diff --git a/util/homeassistant/service.go b/util/homeassistant/service.go index 302662d2c..faba40256 100644 --- a/util/homeassistant/service.go +++ b/util/homeassistant/service.go @@ -32,7 +32,7 @@ func getHomes(w http.ResponseWriter, req *http.Request) { func getEntities(w http.ResponseWriter, req *http.Request) { home := req.PathValue("home") - if instanceByName(home) == nil { + if instanceUriByName(home) == "" { w.WriteHeader(http.StatusBadRequest) return } diff --git a/util/homeassistant/zeroconf.go b/util/homeassistant/zeroconf.go index 1f6ad2ee1..8fd16d38c 100644 --- a/util/homeassistant/zeroconf.go +++ b/util/homeassistant/zeroconf.go @@ -4,20 +4,36 @@ import ( "context" "fmt" "strings" + "sync" - "github.com/evcc-io/evcc/server/network" "github.com/evcc-io/evcc/util" "github.com/libp2p/zeroconf/v2" ) +var ( + mu sync.Mutex + instances = make(map[string]string) +) + func init() { - network.Register(scan) + go scan() +} + +func instanceUriByName(name string) string { + mu.Lock() + defer mu.Unlock() + return instances[name] +} + +func addInstance(name, uri string) { + mu.Lock() + defer mu.Unlock() + instances[name] = uri } func scan() { ctx := context.Background() - log := util.NewLogger("homeassistant") entries := make(chan *zeroconf.ServiceEntry, 1) go func() { @@ -36,9 +52,7 @@ func scan() { } } - if err := authorize(se.Instance, uri); err != nil { - log.ERROR.Println(err) - } + addInstance(se.Instance, uri) case <-ctx.Done(): return @@ -47,27 +61,7 @@ func scan() { }() if err := zeroconf.Browse(ctx, "_home-assistant._tcp.", "local.", entries); err != nil { + log := util.NewLogger("homeassistant") log.ERROR.Println("zeroconf: failed to browse:", err.Error()) } } - -func authorize(name, uri string) error { - mu.Lock() - defer mu.Unlock() - - if _, ok := instances[name]; ok { - return nil - } - - ts, err := NewHomeAssistant(name, uri) - if err != nil { - return err - } - - instances[name] = &instance{ - URI: uri, - TokenSource: ts, - } - - return nil -}