Feat(go): add Signer interface + License Server for multi-customer deployments
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/auth"
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/connection"
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/hub"
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/licensing"
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/logger"
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol"
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/server"
|
||||
@@ -23,11 +26,11 @@ import (
|
||||
|
||||
// MyHandler implements the server.Handler interface
|
||||
type MyHandler struct {
|
||||
log *logger.Logger
|
||||
auth *auth.Authenticator
|
||||
srv *server.Server
|
||||
hub *hub.Hub
|
||||
signPwd string // HMAC key for CMD_MASTERSETTING signatures (YAMA_SIGN_PASSWORD)
|
||||
log *logger.Logger
|
||||
auth *auth.Authenticator
|
||||
srv *server.Server
|
||||
hub *hub.Hub
|
||||
signer licensing.Signer // CMD_MASTERSETTING signer (local / remote / noop)
|
||||
}
|
||||
|
||||
// OnConnect is called when a client connects
|
||||
@@ -436,10 +439,14 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) {
|
||||
|
||||
// sendMasterSetting builds the 1001-byte CMD_MASTERSETTING reply and ships it
|
||||
// down the main TCP connection. Most fields stay zeroed — only Signature
|
||||
// matters today. If no signing password is configured, a zeroed signature is
|
||||
// still sent (and logged once) so the client at least sees a well-formed
|
||||
// packet; in that case the client's private library will refuse to start
|
||||
// screen / file features and abort.
|
||||
// matters today. The signer is one of:
|
||||
// - LocalSigner: HMAC directly with master key (operator's own deployment)
|
||||
// - 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) {
|
||||
buf := make([]byte, 1+protocol.MasterSettingsSize)
|
||||
buf[0] = protocol.CmdMasterSetting
|
||||
@@ -451,12 +458,14 @@ func (h *MyHandler) sendMasterSetting(ctx *connection.Context, startTime, client
|
||||
buf[1:5],
|
||||
uint32(protocol.DefaultReportIntervalSec))
|
||||
|
||||
if h.signPwd == "" {
|
||||
h.log.Warn("YAMA_SIGN_PASSWORD not set — client may abort on screen/file ops")
|
||||
sig, err := h.signer.Sign(startTime, clientID)
|
||||
if err != nil {
|
||||
h.log.Error("signer (%s) failed for clientID=%s: %v — sending zeroed signature",
|
||||
h.signer.Mode(), clientID, err)
|
||||
} 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.
|
||||
} else {
|
||||
msg := startTime + "|" + clientID
|
||||
sig := protocol.SignMessage(h.signPwd, []byte(msg))
|
||||
// Signature[64] lives at offset 508 of the struct, +1 for the cmd byte.
|
||||
const sigOffset = 1 + protocol.MasterSettingsOffSignature
|
||||
copy(buf[sigOffset:sigOffset+protocol.MasterSettingsSignatureLen], []byte(sig))
|
||||
}
|
||||
@@ -676,14 +685,37 @@ func main() {
|
||||
// the HTTP server reads from it.
|
||||
deviceHub := hub.New()
|
||||
|
||||
// HMAC key used to sign the per-login CMD_MASTERSETTING reply. The
|
||||
// client verifies this signature before enabling its screen / file
|
||||
// features and aborts the process on mismatch. Kept in an env var so
|
||||
// the literal stays out of the binary; provision out-of-band and
|
||||
// never commit it.
|
||||
signPwd := os.Getenv("YAMA_SIGN_PASSWORD")
|
||||
if signPwd == "" {
|
||||
log.Warn("YAMA_SIGN_PASSWORD not set; clients will refuse screen/file ops")
|
||||
// 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)
|
||||
signer, mode, err := licensing.NewFromEnv(log)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to initialize signer: %v", err)
|
||||
}
|
||||
// signer.Close() is called explicitly after the License Server HTTP is
|
||||
// drained at shutdown — sequencing matters because an in-flight
|
||||
// handleSign on the HTTP path needs a live signer to complete.
|
||||
switch mode {
|
||||
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))
|
||||
case licensing.ModeNoOp:
|
||||
log.Warn("Signer mode: NOOP (no licensing configured; screen/file features disabled on clients)")
|
||||
}
|
||||
|
||||
// If the operator also wants this LocalSigner deployment to serve as
|
||||
// License Server for RemoteSigner customers, YAMA_LICENSE_PUBLIC_KEY +
|
||||
// YAMA_LICENSE_HTTP_ADDR enable it. Off by default.
|
||||
licSrv, licAddr, err := licensing.LicenseServerFromEnv(signer, log)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to initialize License Server: %v", err)
|
||||
}
|
||||
|
||||
// Web user authenticator. Bootstrap admin from env var YAMA_WEB_ADMIN_PASS;
|
||||
@@ -726,11 +758,11 @@ func main() {
|
||||
|
||||
// Create handler for this server
|
||||
handler := &MyHandler{
|
||||
log: log.WithPrefix(fmt.Sprintf("Handler:%d", port)),
|
||||
auth: authenticator,
|
||||
srv: srv,
|
||||
hub: deviceHub,
|
||||
signPwd: signPwd,
|
||||
log: log.WithPrefix(fmt.Sprintf("Handler:%d", port)),
|
||||
auth: authenticator,
|
||||
srv: srv,
|
||||
hub: deviceHub,
|
||||
signer: signer,
|
||||
}
|
||||
srv.SetHandler(handler)
|
||||
|
||||
@@ -785,10 +817,42 @@ func main() {
|
||||
log.Fatal("Failed to start HTTP server: %v", err)
|
||||
}
|
||||
|
||||
// Optionally serve the License Server HTTP endpoints in this process.
|
||||
// Only active when the operator set YAMA_LICENSE_PUBLIC_KEY +
|
||||
// YAMA_LICENSE_HTTP_ADDR. We serve plain HTTP — operator should put
|
||||
// nginx/Caddy in front for TLS. (RemoteSigner customers connect to
|
||||
// the public URL configured via YAMA_LICENSE_SERVER on their side.)
|
||||
//
|
||||
// Timeouts cap Slowloris / FD-exhaustion attacks. Values are generous
|
||||
// enough for a slow public link (TLS-terminating proxy in front, real
|
||||
// customer round-trips at ~hundreds of ms) but tight enough that a
|
||||
// trickle-byte attacker cannot pin a connection indefinitely.
|
||||
var licenseHTTP *http.Server
|
||||
if licSrv != nil {
|
||||
licenseHTTP = &http.Server{
|
||||
Addr: licAddr,
|
||||
Handler: licSrv.Handler(),
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
MaxHeaderBytes: 16 << 10,
|
||||
}
|
||||
go func() {
|
||||
log.Info("License Server listening on %s (POST /license/sign, /license/heartbeat)", licAddr)
|
||||
if err := licenseHTTP.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Error("License Server stopped: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
fmt.Printf("Server started on port(s): %v\n", ports)
|
||||
if *httpPort != 0 {
|
||||
fmt.Printf("Web UI on http://localhost:%d/\n", *httpPort)
|
||||
}
|
||||
if licenseHTTP != nil {
|
||||
fmt.Printf("License Server on http://%s/license/{sign,heartbeat}\n", licAddr)
|
||||
}
|
||||
fmt.Println("Logs are written to: logs/server.log")
|
||||
fmt.Println("Press Ctrl+C to stop...")
|
||||
|
||||
@@ -798,6 +862,17 @@ func main() {
|
||||
<-sigChan
|
||||
|
||||
fmt.Println("\nShutting down...")
|
||||
// Order matters: drain License Server HTTP first so no handleSign is
|
||||
// mid-flight; THEN close the signer (which may release HTTP keepalives
|
||||
// in RemoteSigner mode, or be a no-op for LocalSigner/NoOp).
|
||||
if licenseHTTP != nil {
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_ = licenseHTTP.Shutdown(shutdownCtx)
|
||||
cancel()
|
||||
}
|
||||
if err := signer.Close(); err != nil {
|
||||
log.Warn("signer Close: %v", err)
|
||||
}
|
||||
httpSrv.Stop()
|
||||
for _, srv := range servers {
|
||||
srv.Stop()
|
||||
|
||||
Reference in New Issue
Block a user