Remove sponsor token gating
Some checks failed
CodeQL / Analyze (go) (push) Failing after 1m16s
CodeQL / Analyze (actions) (push) Failing after 4s
CodeQL / Analyze (javascript-typescript) (push) Failing after 7s
Default / Clean (push) Has been cancelled
Default / Build (push) Has been cancelled
Default / Test (push) Has been cancelled
Default / Lint (push) Has been cancelled
Default / UI (push) Has been cancelled
Default / Integration (push) Has been cancelled
Default / Warm fork cache (Go) (push) Has been cancelled
Default / Warm fork cache (UI) (push) Has been cancelled

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.
This commit is contained in:
Stefan 2026-08-30 10:41:18 +02:00
parent d379b7802e
commit 13215c74c2
4 changed files with 86 additions and 31 deletions

54
FORK.md Normal file
View file

@ -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 <token>`)
- 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: <https://sponsor.evcc.io>
## 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.

View file

@ -10,7 +10,6 @@ import (
"github.com/andig/mbserver" "github.com/andig/mbserver"
"github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util/modbus" "github.com/evcc-io/evcc/util/modbus"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -362,14 +361,3 @@ func TestAlpitronicReadFailure(t *testing.T) {
_, err = wb.CurrentPower() _, err = wb.CurrentPower()
assert.Error(t, err) 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)
}

View file

@ -10,7 +10,6 @@ import (
"github.com/andig/mbserver" "github.com/andig/mbserver"
"github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util/modbus" "github.com/evcc-io/evcc/util/modbus"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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 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) { func TestSigenergyEVDCReadFailure(t *testing.T) {
// missing register in the bulk-read block -> IllegalDataAddress propagates // missing register in the bulk-read block -> IllegalDataAddress propagates
regs := evdcRegs(evdcStateCharging) regs := evdcRegs(evdcStateCharging)

View file

@ -35,10 +35,15 @@ import (
"google.golang.org/grpc/status" "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 ( var (
mu sync.RWMutex mu sync.RWMutex
Subject, Token string Subject = unlocked
ExpiresAt time.Time Token string
ExpiresAt time.Time
) )
func machineID() string { func machineID() string {
@ -51,9 +56,8 @@ const unavailable = "sponsorship unavailable"
const startupTimeout = 30 * time.Second const startupTimeout = 30 * time.Second
func IsAuthorized() bool { func IsAuthorized() bool {
mu.RLock() // sponsorship gating removed in this fork
defer mu.RUnlock() return true
return len(Subject) > 0
} }
func IsAuthorizedForApi() bool { func IsAuthorizedForApi() bool {
@ -62,8 +66,29 @@ func IsAuthorizedForApi() bool {
return IsAuthorized() && Subject != unavailable && Token != "" 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 { 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() mu.Lock()
defer mu.Unlock() defer mu.Unlock()