Feature(Go): Web auth, WebSocket signaling and live device list (Phase 3)
This commit is contained in:
166
server/go/web/ws_handlers.go
Normal file
166
server/go/web/ws_handlers.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// dispatch routes one inbound message to its handler. The `raw` payload is
|
||||
// passed through so handlers can re-parse to their own shape.
|
||||
//
|
||||
// Phase 3 implements: get_salt, login, get_devices, ping, disconnect.
|
||||
// Phase 4/5/6 commands (connect, mouse, key, term_*, etc.) get a friendly
|
||||
// "not yet implemented" reply so the browser UI doesn't hang silently.
|
||||
func (h *wsHub) dispatch(c *wsClient, cmd string, raw []byte) {
|
||||
switch cmd {
|
||||
case "get_salt":
|
||||
h.handleGetSalt(c, raw)
|
||||
case "login":
|
||||
h.handleLogin(c, raw)
|
||||
case "get_devices":
|
||||
h.handleGetDevices(c, raw)
|
||||
case "ping":
|
||||
// no-op heartbeat; the read itself was the keep-alive signal
|
||||
case "disconnect":
|
||||
c.queue([]byte(`{"cmd":"disconnect_result","ok":true}`))
|
||||
|
||||
// Reserved for later phases. Reply with a benign failure so the UI can
|
||||
// surface a clear error instead of spinning indefinitely.
|
||||
case "connect":
|
||||
h.replyNotImplemented(c, "connect_result", "Screen sharing not yet implemented on Go server")
|
||||
case "rdp_reset":
|
||||
// silently ignored — UI uses this as a fire-and-forget
|
||||
case "mouse", "key":
|
||||
// silently ignored — no remote screen yet
|
||||
case "term_open":
|
||||
h.replyNotImplemented(c, "term_closed", "Web terminal not yet implemented on Go server")
|
||||
case "term_input", "term_resize", "term_close":
|
||||
// silently ignored — no terminal session
|
||||
|
||||
// Admin operations (Phase 7).
|
||||
case "create_user":
|
||||
h.replyNotImplemented(c, "create_user_result", "User management not yet implemented")
|
||||
case "delete_user":
|
||||
h.replyNotImplemented(c, "delete_user_result", "User management not yet implemented")
|
||||
case "list_users":
|
||||
h.replyNotImplemented(c, "list_users_result", "User management not yet implemented")
|
||||
case "get_groups":
|
||||
c.queue([]byte(`{"cmd":"groups","ok":true,"groups":[]}`))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *wsHub) replyNotImplemented(c *wsClient, replyCmd, msg string) {
|
||||
c.queue(mustJSON(map[string]any{
|
||||
"cmd": replyCmd,
|
||||
"ok": false,
|
||||
"msg": msg,
|
||||
}))
|
||||
}
|
||||
|
||||
// ----- handlers ------------------------------------------------------------
|
||||
|
||||
func (h *wsHub) handleGetSalt(c *wsClient, raw []byte) {
|
||||
var in struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
|
||||
salt, ok := h.auth.GetSalt(in.Username)
|
||||
// Do not leak which usernames exist: always return ok=true with a salt.
|
||||
// For unknown users hand back the empty salt (matches admin convention)
|
||||
// so the timing/shape of the response is uniform.
|
||||
if !ok {
|
||||
salt = ""
|
||||
}
|
||||
c.queue(mustJSON(map[string]any{
|
||||
"cmd": "salt",
|
||||
"ok": true,
|
||||
"salt": salt,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *wsHub) handleLogin(c *wsClient, raw []byte) {
|
||||
var in struct {
|
||||
Username string `json:"username"`
|
||||
Response string `json:"response"`
|
||||
Nonce string `json:"nonce"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &in); err != nil {
|
||||
c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid request"}))
|
||||
return
|
||||
}
|
||||
|
||||
// Bind the response to the challenge we issued at connect time so that
|
||||
// replays from a different connection can't reuse a captured response.
|
||||
if in.Nonce == "" || in.Nonce != c.nonce {
|
||||
c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid challenge"}))
|
||||
return
|
||||
}
|
||||
|
||||
token, role, err := h.auth.VerifyLogin(in.Username, in.Response, in.Nonce)
|
||||
if err != nil {
|
||||
// Burn the challenge on failure too — forces a new round on retry.
|
||||
c.nonce = ""
|
||||
c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid credentials"}))
|
||||
return
|
||||
}
|
||||
c.nonce = ""
|
||||
c.token = token
|
||||
c.role = role
|
||||
h.log.Info("ws login: user=%s role=%s addr=%s", in.Username, role, c.addr)
|
||||
|
||||
c.queue(mustJSON(map[string]any{
|
||||
"cmd": "login_result",
|
||||
"ok": true,
|
||||
"token": token,
|
||||
"role": role,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *wsHub) handleGetDevices(c *wsClient, raw []byte) {
|
||||
if !h.requireAuth(c, raw, "device_list") {
|
||||
return
|
||||
}
|
||||
devices := h.devices.ListDevices()
|
||||
c.queue(mustJSON(map[string]any{
|
||||
"cmd": "device_list",
|
||||
"ok": true,
|
||||
"devices": devices,
|
||||
}))
|
||||
}
|
||||
|
||||
// requireAuth validates the token embedded in raw against the authenticator's
|
||||
// session store (not against c.token). Tokens live independently of WS
|
||||
// connections — the browser may reconnect after a visibility/network blip and
|
||||
// resume with the same token, so we must not tie validity to one WS lifetime.
|
||||
// On the first authenticated message we cache the token/role on the wsClient
|
||||
// so broadcasts know to deliver to this connection.
|
||||
func (h *wsHub) requireAuth(c *wsClient, raw []byte, replyCmd string) bool {
|
||||
var in struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
if in.Token == "" {
|
||||
c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": false}))
|
||||
return false
|
||||
}
|
||||
sess, err := h.auth.ValidateToken(in.Token)
|
||||
if err != nil {
|
||||
c.queue(mustJSON(map[string]any{"cmd": replyCmd, "ok": false}))
|
||||
return false
|
||||
}
|
||||
if c.token == "" {
|
||||
c.token = in.Token
|
||||
c.role = sess.Role
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func mustJSON(v any) []byte {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
// All callers pass simple map[string]any with primitive values;
|
||||
// marshal can't realistically fail. If it does, return a safe fallback.
|
||||
return []byte(`{"cmd":"error","msg":"internal encode error"}`)
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user