server läuft
This commit is contained in:
1 parent
c8df9d7651
commit
42f0c0accb
21 files changed
+4185
-10
No files matched your search
@@ -0,0 +1,192 @@
|
||||
// Package license stellt Mandanten-Lizenzen (Capabilities, Spec Abschnitt
|
||||
// 4.1) aus und prüft sie.
|
||||
//
|
||||
// Eine Lizenz legt fest, was ein Mandant darf: Tier, maximale Routenzahl,
|
||||
// erlaubte Domains, Features und Laufzeit. Sie ist mit dem Ed25519-
|
||||
// Master-Key des Servers signiert; jede Änderung an einem Feld macht die
|
||||
// Signatur ungültig.
|
||||
package license
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalid: Die Lizenz verletzt eine Formatregel.
|
||||
ErrInvalid = errors.New("license: ungültige Lizenz")
|
||||
// ErrInvalidSignature: Signatur fehlt, ist beschädigt oder passt nicht.
|
||||
ErrInvalidSignature = errors.New("license: Signatur ungültig")
|
||||
)
|
||||
|
||||
// Tier ist die Lizenzstufe.
|
||||
type Tier string
|
||||
|
||||
const (
|
||||
TierCommunity Tier = "community"
|
||||
TierPro Tier = "pro"
|
||||
TierEnterprise Tier = "enterprise"
|
||||
)
|
||||
|
||||
// Feature ist eine optional freigeschaltete Fähigkeit.
|
||||
type Feature string
|
||||
|
||||
const (
|
||||
FeatureWebsockets Feature = "websockets"
|
||||
FeatureTCPStream Feature = "tcp_stream"
|
||||
FeatureCustomMiddleware Feature = "custom_middleware"
|
||||
)
|
||||
|
||||
// License entspricht license.json (Spec 4.1).
|
||||
type License struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
Tier Tier `json:"tier"`
|
||||
MaxRoutes int `json:"max_routes"` // -1 = unbegrenzt
|
||||
AllowedDomains []string `json:"allowed_domains"`
|
||||
Features []Feature `json:"features"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// signatureVersion trennt Lizenz-Signaturen von anderen Nutzungen desselben
|
||||
// Master-Keys (z. B. künftig signierten Server-Events).
|
||||
const signatureVersion = "SULTAN-LICENSE-V1\n"
|
||||
|
||||
// signedPayload legt Reihenfolge und Format der signierten Felder fest.
|
||||
type signedPayload struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
Tier Tier `json:"tier"`
|
||||
MaxRoutes int `json:"max_routes"`
|
||||
AllowedDomains []string `json:"allowed_domains"`
|
||||
Features []Feature `json:"features"`
|
||||
ValidUntil string `json:"valid_until"`
|
||||
}
|
||||
|
||||
func (l License) message() []byte {
|
||||
b, _ := json.Marshal(signedPayload{
|
||||
TenantID: l.TenantID,
|
||||
Tier: l.Tier,
|
||||
MaxRoutes: l.MaxRoutes,
|
||||
AllowedDomains: l.AllowedDomains,
|
||||
Features: l.Features,
|
||||
ValidUntil: l.ValidUntil.UTC().Format(time.RFC3339),
|
||||
})
|
||||
return append([]byte(signatureVersion), b...)
|
||||
}
|
||||
|
||||
// Issuer stellt Lizenzen mit dem Master-Key des Servers aus.
|
||||
type Issuer struct {
|
||||
Key ed25519.PrivateKey
|
||||
}
|
||||
|
||||
// Issue prüft und normalisiert l (Domains klein und ohne Punkt am Ende,
|
||||
// ValidUntil in UTC auf ganze Sekunden) und signiert sie. Die Eingabe wird
|
||||
// nicht verändert.
|
||||
func (i *Issuer) Issue(l License) (License, error) {
|
||||
if len(i.Key) != ed25519.PrivateKeySize {
|
||||
return License{}, errors.New("license: Issuer ohne gültigen Master-Key")
|
||||
}
|
||||
out := l
|
||||
out.Signature = ""
|
||||
out.ValidUntil = l.ValidUntil.UTC().Truncate(time.Second)
|
||||
out.AllowedDomains = make([]string, len(l.AllowedDomains))
|
||||
for n, d := range l.AllowedDomains {
|
||||
p, err := normalizePattern(d)
|
||||
if err != nil {
|
||||
return License{}, err
|
||||
}
|
||||
out.AllowedDomains[n] = p
|
||||
}
|
||||
out.Features = append([]Feature{}, l.Features...)
|
||||
if err := out.validate(); err != nil {
|
||||
return License{}, err
|
||||
}
|
||||
out.Signature = base64.StdEncoding.EncodeToString(ed25519.Sign(i.Key, out.message()))
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (l License) validate() error {
|
||||
switch {
|
||||
case strings.TrimSpace(l.TenantID) == "":
|
||||
return fmt.Errorf("%w: tenant_id fehlt", ErrInvalid)
|
||||
case l.Tier != TierCommunity && l.Tier != TierPro && l.Tier != TierEnterprise:
|
||||
return fmt.Errorf("%w: unbekannter Tier %q", ErrInvalid, l.Tier)
|
||||
case l.MaxRoutes < -1:
|
||||
return fmt.Errorf("%w: max_routes %d (erlaubt: -1 oder ≥ 0)", ErrInvalid, l.MaxRoutes)
|
||||
case len(l.AllowedDomains) == 0:
|
||||
return fmt.Errorf("%w: allowed_domains leer", ErrInvalid)
|
||||
case l.ValidUntil.IsZero():
|
||||
return fmt.Errorf("%w: valid_until fehlt", ErrInvalid)
|
||||
}
|
||||
for _, d := range l.AllowedDomains {
|
||||
if p, err := normalizePattern(d); err != nil || p != d {
|
||||
return fmt.Errorf("%w: Domain %q nicht normalisiert", ErrInvalid, d)
|
||||
}
|
||||
}
|
||||
for _, f := range l.Features {
|
||||
if f != FeatureWebsockets && f != FeatureTCPStream && f != FeatureCustomMiddleware {
|
||||
return fmt.Errorf("%w: unbekanntes Feature %q", ErrInvalid, f)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify prüft die Signatur mit dem Public Key des Master-Keys.
|
||||
func (l License) Verify(pub ed25519.PublicKey) error {
|
||||
if len(pub) != ed25519.PublicKeySize {
|
||||
return fmt.Errorf("%w: kein gültiger Public Key", ErrInvalidSignature)
|
||||
}
|
||||
sig, err := base64.StdEncoding.DecodeString(l.Signature)
|
||||
if err != nil || len(sig) != ed25519.SignatureSize {
|
||||
return fmt.Errorf("%w: Signatur fehlt oder ist beschädigt", ErrInvalidSignature)
|
||||
}
|
||||
if !ed25519.Verify(pub, l.message(), sig) {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse liest license.json, lehnt unbekannte Felder ab und prüft Format und
|
||||
// Signatur.
|
||||
func Parse(data []byte, pub ed25519.PublicKey) (License, error) {
|
||||
dec := json.NewDecoder(strings.NewReader(string(data)))
|
||||
dec.DisallowUnknownFields()
|
||||
var l License
|
||||
if err := dec.Decode(&l); err != nil {
|
||||
return License{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||
}
|
||||
l.ValidUntil = l.ValidUntil.UTC()
|
||||
if l.Features == nil {
|
||||
l.Features = []Feature{}
|
||||
}
|
||||
if err := l.validate(); err != nil {
|
||||
return License{}, err
|
||||
}
|
||||
if err := l.Verify(pub); err != nil {
|
||||
return License{}, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// Expired meldet, ob die Lizenz zum Zeitpunkt now abgelaufen ist (der
|
||||
// Ablaufzeitpunkt selbst zählt als abgelaufen).
|
||||
func (l License) Expired(now time.Time) bool { return !now.Before(l.ValidUntil) }
|
||||
|
||||
// DaysLeft liefert die verbleibenden vollen Tage (abgerundet); nach Ablauf
|
||||
// negativ. Grundlage für die Checkmk-Schwellen (30/14 Tage).
|
||||
func (l License) DaysLeft(now time.Time) int {
|
||||
return int(math.Floor(l.ValidUntil.Sub(now).Hours() / 24))
|
||||
}
|
||||
|
||||
// AllowsRoutes meldet, ob n Routen im Limit liegen.
|
||||
func (l License) AllowsRoutes(n int) bool { return l.MaxRoutes == -1 || n <= l.MaxRoutes }
|
||||
|
||||
// HasFeature meldet, ob f freigeschaltet ist.
|
||||
func (l License) HasFeature(f Feature) bool { return slices.Contains(l.Features, f) }
|
||||
Reference in new issue
Block a user