Feature(Go): Web auth, WebSocket signaling and live device list (Phase 3)

This commit is contained in:
yuanyuanxiang
2026-05-17 22:18:29 +02:00
parent af2aa4893f
commit b1f229706c
13 changed files with 1211 additions and 21 deletions

View File

@@ -8,13 +8,16 @@ import (
"strconv"
"strings"
"syscall"
"time"
"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/logger"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/server"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/web"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/wsauth"
)
// MyHandler implements the server.Handler interface
@@ -22,6 +25,7 @@ type MyHandler struct {
log *logger.Logger
auth *auth.Authenticator
srv *server.Server
hub *hub.Hub
}
// OnConnect is called when a client connects
@@ -37,6 +41,7 @@ func (h *MyHandler) OnDisconnect(ctx *connection.Context) {
"clientID", info.ClientID,
"computer", info.ComputerName,
)
h.hub.Unregister(info.ClientID)
}
}
@@ -110,8 +115,40 @@ func (h *MyHandler) handleLogin(ctx *connection.Context, data []byte) {
"version", info.ModuleVersion,
"path", clientInfo.FilePath,
)
// PCName carries "ComputerName/Group"; ModuleVersion carries "Version-Capability".
// strings.Cut returns the full string as the head when the separator is
// absent, which gives us the natural "no group / no capability" fallback.
name, group, _ := strings.Cut(info.PCName, "/")
version, capability, _ := strings.Cut(info.ModuleVersion, "-")
// Reserved field 10 (ClientLoc) is the client-reported geo string.
location := ""
if len(reserved) > 10 {
location = info.GetReservedField(10)
}
// 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,
Group: group,
Version: version,
Capability: capability,
OS: info.OsVerInfo,
CPU: clientInfo.CPU,
FilePath: clientInfo.FilePath,
InstallTime: info.StartTime,
Location: location,
PeerIP: ctx.GetPeerIP(),
PublicIP: clientInfo.IP,
ConnectedAt: time.Now(),
})
}
// handleAuth handles authorization request (TOKEN_AUTH = 100)
func (h *MyHandler) handleAuth(ctx *connection.Context, data []byte) {
result := h.auth.Authenticate(data)
@@ -160,6 +197,25 @@ func (h *MyHandler) handleHeartbeat(ctx *connection.Context, data []byte) {
uint64(data[5])<<32 | uint64(data[6])<<40 | uint64(data[7])<<48 | uint64(data[8])<<56
}
// Forward live fields (ActiveWnd + Ping) to the hub so the web UI can
// display current latency and foreground window per device. Skip until
// login has happened — the hub is keyed by MasterID, which only exists
// post-login.
if info := ctx.GetInfo(); info.ClientID != "" {
var rtt int32
var activeWindow string
// ActiveWnd at data[9..521] is a 512-byte GBK-encoded string.
if len(data) >= 9+512 {
activeWindow = protocol.GbkToUTF8(data[9 : 9+512])
}
// Ping at data[521..525] is a little-endian int32.
if len(data) >= 525 {
rtt = int32(uint32(data[521]) | uint32(data[522])<<8 |
uint32(data[523])<<16 | uint32(data[524])<<24)
}
h.hub.UpdateLive(info.ClientID, int(rtt), activeWindow)
}
// Authenticate heartbeat if it contains authorization info
// data[1:] skips the command byte to get the raw Heartbeat structure
var authorized byte = 0
@@ -269,6 +325,27 @@ func main() {
// Create authenticator (shared by all servers)
authenticator := auth.New(authCfg)
// Shared device registry — every TCP handler reports devices into it,
// the HTTP server reads from it.
deviceHub := hub.New()
// Web user authenticator. Bootstrap admin from env var YAMA_WEB_ADMIN_PASS;
// if unset, fall back to YAMA_PWD (same secret the TCP authorization uses)
// so a single password env var is enough to bring up the whole stack.
// If neither is set, no admin is registered and login will always fail —
// the user must define a password before browsers can log in.
webAuth := wsauth.New()
adminPass := os.Getenv("YAMA_WEB_ADMIN_PASS")
if adminPass == "" {
adminPass = os.Getenv("YAMA_PWD")
}
if adminPass != "" {
webAuth.AddAdminFromPlainPassword("admin", adminPass)
log.Info("Web admin user configured")
} else {
log.Warn("Neither YAMA_WEB_ADMIN_PASS nor YAMA_PWD is set; web login will be unavailable")
}
// Create servers for each port
var servers []*server.Server
for _, port := range ports {
@@ -284,6 +361,7 @@ func main() {
log: log.WithPrefix(fmt.Sprintf("Handler:%d", port)),
auth: authenticator,
srv: srv,
hub: deviceHub,
}
srv.SetHandler(handler)
@@ -297,8 +375,9 @@ func main() {
}
}
// Start HTTP server for web UI (Phase 1: serves index.html only)
httpSrv := web.New(*httpPort, log.WithPrefix("Web"))
// Start HTTP server for web UI. Hub gives it read-only access to the
// device registry; the authenticator owns user accounts and session tokens.
httpSrv := web.New(*httpPort, log.WithPrefix("Web"), deviceHub, webAuth)
if err := httpSrv.Start(); err != nil {
log.Fatal("Failed to start HTTP server: %v", err)
}