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

217
server/go/hub/hub.go Normal file
View File

@@ -0,0 +1,217 @@
// Package hub maintains the registry of currently online devices and acts as
// the bridge between the TCP server (which sees raw client connections) and
// the web server (which serves browser clients).
//
// The TCP side calls RegisterDevice / UnregisterDevice as clients come and go.
// The web side calls ListDevices / GetDevice / (Phase 4) SendToDevice.
// Neither side imports the other — both depend only on this package.
//
// Phase 3 scope: device list only. Frame/cursor pub-sub and SendToDevice are
// added in later phases as features need them.
package hub
import (
"sync"
"time"
"github.com/yuanyuanxiang/SimpleRemoter/server/go/connection"
)
// Device is the internal record for one logical end-device (keyed by MasterID).
// A single device may use multiple TCP sub-connections (screen, terminal …);
// only the main login connection is stored here.
//
// PCName from LOGIN_INFOR is interpreted as "ComputerName/Group" and
// ModuleVersion as "Version-Capability"; the split halves live in separate
// fields so the front-end can render them independently.
type Device struct {
ID string // MasterID — stable identifier the client reports at login
Name string // PCName before '/' (real computer name)
Group string // PCName after '/' (group label; may be empty)
Version string // ModuleVersion before '-' (semantic version)
Capability string // ModuleVersion after '-' (capability tags; may be empty)
OS string // OS version string
CPU string // from LOGIN_INFOR reserved field 2
FilePath string // from LOGIN_INFOR reserved field 4
InstallTime string // from LOGIN_INFOR reserved field 6 (or StartTime)
Location string // client-reported geo string (reserved field 10)
PeerIP string // network-level remote address as seen by the server
PublicIP string // client-reported public IP (reserved field 11)
ConnectedAt time.Time
// Live fields refreshed on every heartbeat. Protected by hub.mu.
RTT int // network latency in ms (Heartbeat.Ping)
ActiveWindow string // foreground window title (Heartbeat.ActiveWnd, decoded)
// conn is the main connection's context. Web side will use it in Phase 4
// to push COMMAND_SCREEN_SPY and similar commands via the hub.
conn *connection.Context
}
// DeviceInfo is the JSON-safe projection of Device for the /api/devices
// endpoint and the WS device_list message. Field names match what the
// existing browser front-end expects.
type DeviceInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Group string `json:"group,omitempty"`
Version string `json:"version"`
Capability string `json:"capability,omitempty"`
OS string `json:"os"`
CPU string `json:"cpu,omitempty"`
FilePath string `json:"file_path,omitempty"`
InstallTime string `json:"install_time,omitempty"`
Location string `json:"location,omitempty"`
IP string `json:"ip"` // client-reported public IP (matches C++ key)
PeerIP string `json:"peer_ip,omitempty"`
RTT int `json:"rtt"`
ActiveWindow string `json:"activeWindow,omitempty"`
ConnectedAt int64 `json:"connected_at"`
Online bool `json:"online"`
}
// EventHandler receives notifications about device lifecycle and per-tick
// live updates. Methods are invoked synchronously from Register / Unregister /
// UpdateLive — implementations must be non-blocking (typically just write to
// a channel or queue).
type EventHandler interface {
OnDeviceOnline(d DeviceInfo)
OnDeviceOffline(id string)
OnDeviceUpdate(id string, rtt int, activeWindow string)
}
// Hub is a thread-safe registry of online devices.
type Hub struct {
mu sync.RWMutex
devices map[string]*Device
subMu sync.RWMutex
subscribers []EventHandler
}
// New returns an empty Hub.
func New() *Hub {
return &Hub{devices: make(map[string]*Device)}
}
// Subscribe registers an EventHandler. The returned func removes it.
// Multiple handlers are supported; each receives every event.
func (h *Hub) Subscribe(eh EventHandler) (unsubscribe func()) {
h.subMu.Lock()
h.subscribers = append(h.subscribers, eh)
h.subMu.Unlock()
return func() {
h.subMu.Lock()
defer h.subMu.Unlock()
for i, x := range h.subscribers {
if x == eh {
h.subscribers = append(h.subscribers[:i], h.subscribers[i+1:]...)
return
}
}
}
}
func (h *Hub) snapshotSubscribers() []EventHandler {
h.subMu.RLock()
defer h.subMu.RUnlock()
out := make([]EventHandler, len(h.subscribers))
copy(out, h.subscribers)
return out
}
// Register records a device as online. Re-registering an existing ID overwrites
// the previous entry (e.g. a client reconnect with the same MasterID).
// A nil device or empty ID is silently ignored.
// Subscribers are notified after the device is added.
func (h *Hub) Register(d *Device) {
if d == nil || d.ID == "" {
return
}
h.mu.Lock()
h.devices[d.ID] = d
info := deviceToInfo(d)
h.mu.Unlock()
for _, s := range h.snapshotSubscribers() {
s.OnDeviceOnline(info)
}
}
// Unregister removes a device by ID. No-op if not present.
// Subscribers are notified after the device is removed (only if it existed).
func (h *Hub) Unregister(id string) {
if id == "" {
return
}
h.mu.Lock()
_, existed := h.devices[id]
delete(h.devices, id)
h.mu.Unlock()
if !existed {
return
}
for _, s := range h.snapshotSubscribers() {
s.OnDeviceOffline(id)
}
}
// ListDevices returns a fresh snapshot slice. The caller may mutate it freely;
// it shares no state with the hub.
func (h *Hub) ListDevices() []DeviceInfo {
h.mu.RLock()
defer h.mu.RUnlock()
out := make([]DeviceInfo, 0, len(h.devices))
for _, d := range h.devices {
out = append(out, deviceToInfo(d))
}
return out
}
func deviceToInfo(d *Device) DeviceInfo {
return DeviceInfo{
ID: d.ID,
Name: d.Name,
Group: d.Group,
Version: d.Version,
Capability: d.Capability,
OS: d.OS,
CPU: d.CPU,
FilePath: d.FilePath,
InstallTime: d.InstallTime,
Location: d.Location,
IP: d.PublicIP,
PeerIP: d.PeerIP,
RTT: d.RTT,
ActiveWindow: d.ActiveWindow,
ConnectedAt: d.ConnectedAt.Unix(),
Online: true, // a device that's in the map is by definition online
}
}
// UpdateLive applies a heartbeat-derived RTT and active-window title to the
// device's live fields, then notifies subscribers. No-op if the device is
// not registered (e.g. heartbeat arriving for a connection that never sent
// TOKEN_LOGIN or has already disconnected).
func (h *Hub) UpdateLive(id string, rtt int, activeWindow string) {
if id == "" {
return
}
h.mu.Lock()
d, ok := h.devices[id]
if !ok {
h.mu.Unlock()
return
}
d.RTT = rtt
d.ActiveWindow = activeWindow
h.mu.Unlock()
for _, s := range h.snapshotSubscribers() {
s.OnDeviceUpdate(id, rtt, activeWindow)
}
}
// Count returns the current number of online devices.
func (h *Hub) Count() int {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.devices)
}