Portiert mail-gateway/internal/license (Verify, Cache, Trial, Signature) + DB-Mirror (internal/services/license) + REST-Handler (status/verify/key/clear) + UI-Page /license (Activate, Status, Limits, Features, Re-verify) + <LicenseBanner /> neben UpdateBanner (trial-expiring, expired, verify-failed) + Scheduler: täglich Re-verify (24h-Tick) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
79 lines
2.1 KiB
Go
79 lines
2.1 KiB
Go
package license
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// KeyStore holds the per-node license key on disk. Every node in a
|
|
// cluster runs its own verification against license.netcell-it.com
|
|
// with its own fingerprint + key, so the file lives under
|
|
// /var/lib/edgeguard/ (node-local, never replicated via the config
|
|
// fan-out).
|
|
//
|
|
// Shape on disk: a single-line text file, leading/trailing whitespace
|
|
// stripped. Mode 0600, owner nmg so a compromised read of a
|
|
// checked-out working copy never leaks the key.
|
|
type KeyStore struct {
|
|
Path string
|
|
|
|
// cached holds the last-loaded key so Get() is cheap; Reload()
|
|
// flushes it.
|
|
cached atomic.Value // string
|
|
}
|
|
|
|
const DefaultKeyStorePath = "/var/lib/edgeguard/license_key"
|
|
|
|
// NewKeyStore returns a store writing to /var/lib/edgeguard/license_key.
|
|
func NewKeyStore() *KeyStore { return &KeyStore{Path: DefaultKeyStorePath} }
|
|
|
|
// Get returns the currently persisted key, or "" when the file is
|
|
// absent / empty. Never returns an error so callers can happily fall
|
|
// through to the trial-mode path.
|
|
func (s *KeyStore) Get() string {
|
|
if v, ok := s.cached.Load().(string); ok && v != "" {
|
|
return v
|
|
}
|
|
data, err := os.ReadFile(s.Path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
k := strings.TrimSpace(string(data))
|
|
s.cached.Store(k)
|
|
return k
|
|
}
|
|
|
|
// Save writes the key atomically (tmp + rename) with mode 0600 and
|
|
// primes the in-memory cache. Empty key removes the file — that's
|
|
// how an admin returns the node to trial mode.
|
|
func (s *KeyStore) Save(key string) error {
|
|
key = strings.TrimSpace(key)
|
|
dir := filepath.Dir(s.Path)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
if key == "" {
|
|
if err := os.Remove(s.Path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
s.cached.Store("")
|
|
return nil
|
|
}
|
|
tmp := s.Path + ".tmp"
|
|
if err := os.WriteFile(tmp, []byte(key+"\n"), 0o600); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Chmod(tmp, 0o600); err != nil {
|
|
_ = os.Remove(tmp)
|
|
return err
|
|
}
|
|
if err := os.Rename(tmp, s.Path); err != nil {
|
|
return err
|
|
}
|
|
s.cached.Store(key)
|
|
return nil
|
|
}
|