Feature(licensing): anonymous trial mode + server-side quota enforcement
This commit is contained in:
@@ -415,9 +415,29 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) {
|
||||
resolution = info.GetReservedField(protocol.ResFieldResolution)
|
||||
}
|
||||
|
||||
// Register with hub so the web side can list this device. Sub-connections
|
||||
// (screen / terminal etc.) reuse the MasterID and will overwrite this entry
|
||||
// harmlessly, but only the main login carries enough info to be useful here.
|
||||
// Sign BEFORE registering in the hub so a quota-rejected device never
|
||||
// appears in the web device list, even briefly. If signing fails we still
|
||||
// send CMD_MASTERSETTING (zeroed signature, wire protocol stays clean) and
|
||||
// then close the connection — no hub registration happens at all.
|
||||
sigErr := h.sendMasterSetting(ctx, info.StartTime, clientID)
|
||||
if sigErr != nil {
|
||||
// Any sign error means no valid signature was issued — close without
|
||||
// registering. Covers: quota exceeded (403), anonymous IP rate limit
|
||||
// (429), and transient errors on a brand-new device with no cache.
|
||||
// Existing devices reconnecting hit the fresh-cache path in Sign() and
|
||||
// return ("sig", nil), so they are never rejected here.
|
||||
h.log.Warn("sign failed for clientID=%s (%v) — closing connection", clientID, sigErr)
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond) // let CMD_MASTERSETTING flush
|
||||
ctx.Close()
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
// Signing succeeded: register with hub so the web side can list this
|
||||
// device. Sub-connections (screen / terminal etc.) reuse the MasterID and
|
||||
// will overwrite this entry harmlessly, but only the main login carries
|
||||
// enough info to be useful here.
|
||||
h.hub.Register(&hub.Device{
|
||||
ID: clientID,
|
||||
Name: name,
|
||||
@@ -434,11 +454,6 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) {
|
||||
PublicIP: clientInfo.IP,
|
||||
ConnectedAt: time.Now(),
|
||||
}, ctx)
|
||||
|
||||
// Push CMD_MASTERSETTING with a signature over "StartTime|ClientID".
|
||||
// The client's private FileUpload init verifies this before allowing
|
||||
// screen / file operations — without it the binary aborts itself.
|
||||
h.sendMasterSetting(ctx, info.StartTime, clientID)
|
||||
}
|
||||
|
||||
// sendMasterSetting builds the 1001-byte CMD_MASTERSETTING reply and ships it
|
||||
@@ -448,10 +463,10 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) {
|
||||
// - RemoteSigner: HTTPS POST to operator's License Server (customer deployment)
|
||||
// - NoOpSigner: returns empty signature (free tier; client refuses screen/file ops)
|
||||
//
|
||||
// On signer error (License Server unreachable + no cache hit), we still ship
|
||||
// a zeroed signature so the packet is well-formed; the client will retry on
|
||||
// next reconnect.
|
||||
func (h *MyHandler) sendMasterSetting(ctx *connection.Context, startTime, clientID string) {
|
||||
// Returns the signer error (not the send error) so callers can distinguish
|
||||
// quota-exceeded rejections from transient failures and act accordingly.
|
||||
// The packet is always sent — even on error — so the wire protocol stays clean.
|
||||
func (h *MyHandler) sendMasterSetting(ctx *connection.Context, startTime, clientID string) error {
|
||||
buf := make([]byte, 1+protocol.MasterSettingsSize)
|
||||
buf[0] = protocol.CmdMasterSetting
|
||||
|
||||
@@ -462,10 +477,10 @@ func (h *MyHandler) sendMasterSetting(ctx *connection.Context, startTime, client
|
||||
buf[1:5],
|
||||
uint32(protocol.DefaultReportIntervalSec))
|
||||
|
||||
sig, err := h.signer.Sign(startTime, clientID)
|
||||
if err != nil {
|
||||
sig, sigErr := h.signer.Sign(startTime, clientID)
|
||||
if sigErr != nil {
|
||||
h.log.Error("signer (%s) failed for clientID=%s: %v — sending zeroed signature",
|
||||
h.signer.Mode(), clientID, err)
|
||||
h.signer.Mode(), clientID, sigErr)
|
||||
} else if sig == "" {
|
||||
// NoOpSigner path, or LocalSigner with empty master key — same effect.
|
||||
// Log only once per process via the startup banner; don't spam here.
|
||||
@@ -477,6 +492,7 @@ func (h *MyHandler) sendMasterSetting(ctx *connection.Context, startTime, client
|
||||
if err := h.srv.Send(ctx, buf); err != nil {
|
||||
h.log.Error("CMD_MASTERSETTING send failed for conn=%d: %v", ctx.ID, err)
|
||||
}
|
||||
return sigErr
|
||||
}
|
||||
|
||||
// handleAuth handles authorization request (TOKEN_AUTH = 100)
|
||||
@@ -700,13 +716,13 @@ func main() {
|
||||
deviceHub := hub.New()
|
||||
|
||||
// Build the CMD_MASTERSETTING signer based on env vars:
|
||||
// - YAMA_SIGN_PASSWORD set → LocalSigner (operator's own deployment;
|
||||
// HMAC master key lives here)
|
||||
// - YAMA_LICENSE_SERVER + YAMA_LICENSE_TOKEN set → RemoteSigner
|
||||
// (customer deployment; never sees the master key, fetches signatures
|
||||
// from operator's License Server with 24h cache)
|
||||
// - neither → NoOpSigner (free tier; client refuses screen/file ops
|
||||
// but device list still works)
|
||||
// - YAMA_SIGN_PASSWORD set → LocalSigner (operator's own deployment;
|
||||
// HMAC master key lives here)
|
||||
// - YAMA_LICENSE_DISABLED=1 → NoOpSigner (explicit opt-out; dev / offline)
|
||||
// - YAMA_LICENSE_TOKEN set → RemoteSigner (paid customer; talks to
|
||||
// operator's License Server with JWT)
|
||||
// - neither of the above → RemoteSigner (anonymous trial; default
|
||||
// URL, no JWT, cap FreeMaxDevices)
|
||||
signer, mode, err := licensing.NewFromEnv(log)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to initialize signer: %v", err)
|
||||
@@ -718,10 +734,21 @@ func main() {
|
||||
case licensing.ModeLocal:
|
||||
log.Info("Signer mode: LOCAL (operator deployment, master key held in-process)")
|
||||
case licensing.ModeRemote:
|
||||
log.Info("Signer mode: REMOTE (customer deployment, %s=%s)",
|
||||
licensing.EnvLicenseServer, os.Getenv(licensing.EnvLicenseServer))
|
||||
licServer := os.Getenv(licensing.EnvLicenseServer)
|
||||
if licServer == "" {
|
||||
licServer = licensing.DefaultLicenseServerURL
|
||||
}
|
||||
log.Info("Signer mode: REMOTE (paid customer, license server=%s)", licServer)
|
||||
case licensing.ModeTrial:
|
||||
licServer := os.Getenv(licensing.EnvLicenseServer)
|
||||
if licServer == "" {
|
||||
licServer = licensing.DefaultLicenseServerURL
|
||||
}
|
||||
log.Info("Signer mode: TRIAL (anonymous试用模式, license server=%s, 最多 %d 台受管设备; 设置 %s 解锁付费配额)",
|
||||
licServer, licensing.FreeMaxDevices, licensing.EnvLicenseToken)
|
||||
case licensing.ModeNoOp:
|
||||
log.Warn("Signer mode: NOOP (no licensing configured; screen/file features disabled on clients)")
|
||||
log.Warn("Signer mode: NOOP (licensing disabled via %s=1; client refuses screen/file features)",
|
||||
licensing.EnvLicenseDisabled)
|
||||
}
|
||||
|
||||
// If the operator also wants this LocalSigner deployment to serve as
|
||||
@@ -749,6 +776,10 @@ func main() {
|
||||
adminPass = defaultWebAdminPass
|
||||
rememberDefault("YAMA_WEB_ADMIN_PASS", defaultWebAdminPass)
|
||||
usingDefaultWebPass = true
|
||||
// Loud warn (in addition to the startup banner): the binary is now
|
||||
// accepting admin/admin on the public web UI. Anyone running this
|
||||
// without overriding the env var in prod needs to see it in red.
|
||||
log.Warn("⚠ YAMA_WEB_ADMIN_PASS / YAMA_PWD 均未设置,Web 管理使用默认密码 admin/admin — 生产环境务必覆盖")
|
||||
}
|
||||
webAuth.AddAdminFromPlainPassword("admin", adminPass)
|
||||
log.Info("Web admin user configured")
|
||||
|
||||
Reference in New Issue
Block a user