From 13215c74c248e5d3a44b9f482cdc96cf63f57145 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sun, 30 Aug 2026 10:41:18 +0200 Subject: [PATCH] Remove sponsor token gating Unlock all locally gated features without a sponsor token. The change sits in util/sponsor/auth.go instead of the ~68 device constructors: - Subject defaults to a non-empty value, so RedactedStatus reports an active sponsorship and the frontend unlocks isSponsor - IsAuthorized always returns true, opening every caller including the modbus proxy and the optimizer gate - ConfigureSponsorship still validates a configured token but never fails, so an expired token no longer aborts startup Drops TestAlpitronicSponsorGate and TestSigenergyEVDCSponsorGate, which asserted exactly the gate that is removed here. Cloud-backed services (optimizer API, remote access, cloud vehicles, telemetry) still require a real token - those are checked server side. See FORK.md. --- FORK.md | 54 ++++++++++++++++++++++++++++++++++ charger/alpitronic_test.go | 12 -------- charger/sigenergy-evdc_test.go | 12 -------- util/sponsor/auth.go | 39 +++++++++++++++++++----- 4 files changed, 86 insertions(+), 31 deletions(-) create mode 100644 FORK.md diff --git a/FORK.md b/FORK.md new file mode 100644 index 000000000..4508b30fd --- /dev/null +++ b/FORK.md @@ -0,0 +1,54 @@ +# Fork-Hinweise + +Fork von [evcc-io/evcc](https://github.com/evcc-io/evcc). + +## Abweichung vom Upstream + +Das Sponsor-Gating ist entfernt: alle lokal geprüften Features stehen ohne +Sponsor-Token zur Verfügung. Der Eingriff sitzt an einer einzigen Stelle in +`util/sponsor/auth.go`, statt in den ~68 Geräte-Konstruktoren: + +- `Subject` ist per Default nicht leer (`"unlocked"`). Damit meldet + `RedactedStatus()` einen aktiven Sponsor-Status, und das Frontend schaltet + `isSponsor` (`assets/js/views/Config.vue`) frei — Geräte-Dialoge zeigen kein + „Sponsor-Token erforderlich" mehr. +- `IsAuthorized()` liefert immer `true`. Das öffnet alle Aufrufer, u. a. die + Charger-/Vehicle-Konstruktoren, `server/modbus/proxy.go` und den + Optimizer-Gate in `core/site_optimizer.go`. +- `ConfigureSponsorship()` validiert ein hinterlegtes Token weiterhin, gibt aber + nie einen Fehler zurück. Ein abgelaufenes oder ungültiges Token bricht den + Start damit nicht mehr ab; es wird nur verworfen, damit es keinen + Cloud-Diensten angeboten wird. + +Zusätzlich entfernt: `TestAlpitronicSponsorGate` und +`TestSigenergyEVDCSponsorGate` — beide prüften genau das Gate, das hier +absichtlich nicht mehr existiert. + +## Was das *nicht* freischaltet + +Serverseitig geprüfte Dienste brauchen weiterhin ein echtes Token, weil die +Prüfung bei evcc.io stattfindet und nicht im Client: + +- Optimizer-API (`core/site_optimizer.go` sendet `Bearer `) +- Remote Access / Tunnel (`server/remote/`) +- Cloud-Fahrzeuge und Tronity (`vehicle/cloud.go`, `vehicle/tronity.go`) +- Telemetrie (`util/telemetry/`, hängt an `IsAuthorizedForApi()`) + +## Lizenz + +Der Hauptteil von evcc steht unter MIT. `util/sponsor/` trägt einen eigenen +Vermerk („This module is NOT covered by the MIT license. All rights reserved."). +Diese Änderung betrifft genau dieses Modul. + +evcc finanziert sich über Sponsoring: + +## Upstream-Sync + +``` +git remote add upstream https://github.com/evcc-io/evcc.git +git fetch upstream +git rebase upstream/master +``` + +Konflikte sind auf `util/sponsor/auth.go` beschränkt; `FORK.md` liegt bewusst +außerhalb der Upstream-Dateien. diff --git a/charger/alpitronic_test.go b/charger/alpitronic_test.go index 911b660c8..d6b01dd3e 100644 --- a/charger/alpitronic_test.go +++ b/charger/alpitronic_test.go @@ -10,7 +10,6 @@ import ( "github.com/andig/mbserver" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util/modbus" - "github.com/evcc-io/evcc/util/sponsor" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -362,14 +361,3 @@ func TestAlpitronicReadFailure(t *testing.T) { _, err = wb.CurrentPower() assert.Error(t, err) } - -func TestAlpitronicSponsorGate(t *testing.T) { - // go-e tests set the global sponsor.Subject and never reset it - old := sponsor.Subject - sponsor.Subject = "" - t.Cleanup(func() { sponsor.Subject = old }) - - // tests run without sponsorship: the public constructor must refuse - _, err := NewAlpitronicHYC(t.Context(), modbus.TcpSettings{URI: "localhost:0", ID: 1}, 1) - assert.ErrorIs(t, err, api.ErrSponsorRequired) -} diff --git a/charger/sigenergy-evdc_test.go b/charger/sigenergy-evdc_test.go index 36f78b18d..32a197d87 100644 --- a/charger/sigenergy-evdc_test.go +++ b/charger/sigenergy-evdc_test.go @@ -10,7 +10,6 @@ import ( "github.com/andig/mbserver" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util/modbus" - "github.com/evcc-io/evcc/util/sponsor" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -290,17 +289,6 @@ func TestSigenergyEVDCMinMaxCurrent(t *testing.T) { assert.InDelta(t, 36.23, maxA, 0.01) // 25000 W / 690 } -func TestSigenergyEVDCSponsorGate(t *testing.T) { - // go-e tests set the global sponsor.Subject and never reset it - old := sponsor.Subject - sponsor.Subject = "" - t.Cleanup(func() { sponsor.Subject = old }) - - // tests run without sponsorship: the public constructor must refuse - _, err := NewSigenergyEVDC(t.Context(), "localhost:0", 1) - assert.ErrorIs(t, err, api.ErrSponsorRequired) -} - func TestSigenergyEVDCReadFailure(t *testing.T) { // missing register in the bulk-read block -> IllegalDataAddress propagates regs := evdcRegs(evdcStateCharging) diff --git a/util/sponsor/auth.go b/util/sponsor/auth.go index ed225fccf..9b707a889 100644 --- a/util/sponsor/auth.go +++ b/util/sponsor/auth.go @@ -35,10 +35,15 @@ import ( "google.golang.org/grpc/status" ) +// unlocked is the sponsorship subject used by this fork. Sponsor gating is +// removed here, so Subject is never empty and IsAuthorized always holds. +const unlocked = "unlocked" + var ( - mu sync.RWMutex - Subject, Token string - ExpiresAt time.Time + mu sync.RWMutex + Subject = unlocked + Token string + ExpiresAt time.Time ) func machineID() string { @@ -51,9 +56,8 @@ const unavailable = "sponsorship unavailable" const startupTimeout = 30 * time.Second func IsAuthorized() bool { - mu.RLock() - defer mu.RUnlock() - return len(Subject) > 0 + // sponsorship gating removed in this fork + return true } func IsAuthorizedForApi() bool { @@ -62,8 +66,29 @@ func IsAuthorizedForApi() bool { return IsAuthorized() && Subject != unavailable && Token != "" } -// check and set sponsorship token +// ConfigureSponsorship validates a sponsor token when one is configured, but +// never fails: sponsorship is not required in this fork. A valid token is +// still picked up so cloud-backed services keep working for real sponsors. func ConfigureSponsorship(token string) error { + err := configureSponsorship(token) + + mu.Lock() + defer mu.Unlock() + + if err != nil { + // stay unlocked, but do not offer a rejected token to cloud services + Token = "" + } + + if Subject == "" { + Subject = unlocked + } + + return nil +} + +// check and set sponsorship token +func configureSponsorship(token string) error { mu.Lock() defer mu.Unlock()