MWSE/internal/services/auth.go

223 lines
6.2 KiB
Go

package services
import (
"git.saqut.com/saqut/mwse/internal/protocol"
"git.saqut.com/saqut/mwse/internal/ws"
)
// ---- relationship helpers ------------------------------------------------
//
// "Secure" reachability comes in two forms: a mutual pairing, or co-membership of
// a room. These helpers centralise the checks the original Client.isSecure /
// getSucureClients spread across files (and fix their bugs).
// isPaired reports a *mutual* pairing between a and b.
func isPaired(a, b *ws.Client) bool {
return a.HasPair(b.ID) && b.HasPair(a.ID)
}
// shareRoom reports whether a and b are members of at least one common room.
func shareRoom(hub *ws.Hub, a, b *ws.Client) bool {
for _, rid := range a.Rooms() {
if r, ok := hub.Room(rid); ok && r.Has(b.ID) {
return true
}
}
return false
}
// isSecure reports whether a may address the client with id peerID.
func isSecure(hub *ws.Hub, a *ws.Client, peerID string) bool {
peer, ok := hub.Client(peerID)
if !ok {
return false
}
return isPaired(a, peer) || shareRoom(hub, a, peer)
}
// secureClients returns the connected peers c may exchange info with: those c has
// paired toward, and those sharing a room with c.
func secureClients(hub *ws.Hub, c *ws.Client) (pairs, roompairs map[string]*ws.Client) {
pairs = make(map[string]*ws.Client)
for _, id := range c.Pairs() {
if pc, ok := hub.Client(id); ok {
pairs[id] = pc
}
}
roompairs = make(map[string]*ws.Client)
for _, rid := range c.Rooms() {
r, ok := hub.Room(rid)
if !ok {
continue
}
for _, m := range r.Members() {
if m.ID == c.ID {
continue
}
roompairs[m.ID] = m
}
}
return pairs, roompairs
}
func registerAuth(hub *ws.Hub) {
// On disconnect: tell secure peers we are gone, then remove EVERY pairing edge
// that references this client (both directions) so no stale id is ever left on
// a surviving peer. Using the outgoing+incoming indexes keeps this O(degree),
// not O(all clients), and prevents unbounded growth under churn.
hub.OnDisconnect(func(c *ws.Client) {
pairs, roompairs := secureClients(hub, c)
notified := make(map[string]bool)
notify := func(set map[string]*ws.Client) {
for id, peer := range set {
if notified[id] {
continue
}
notified[id] = true
peer.Signal("peer/disconnect", map[string]any{"id": c.ID})
}
}
notify(pairs)
notify(roompairs)
cleaned := make(map[string]bool)
for _, peerID := range append(c.Pairs(), c.PairedBy()...) {
if cleaned[peerID] {
continue
}
cleaned[peerID] = true
if peer, ok := hub.Client(peerID); ok {
peer.ForgetPeer(c.ID)
}
}
})
hub.Register("auth/pair-system", func(c *ws.Client, m protocol.Message) any {
switch m.Str("value") {
case "everybody":
c.SetRequiredPair(true)
return success()
case "disable":
c.SetRequiredPair(false)
return success()
}
return fail("INVALID_VALUE")
})
hub.Register("my/socketid", func(c *ws.Client, m protocol.Message) any {
return c.ID
})
hub.Register("auth/public", func(c *ws.Client, m protocol.Message) any {
c.SetRequiredPair(false)
return map[string]any{"value": "success", "mode": "public"}
})
hub.Register("auth/private", func(c *ws.Client, m protocol.Message) any {
c.SetRequiredPair(true)
return map[string]any{"value": "success", "mode": "private"}
})
// request/pair: ask `to` to pair with us. We record our side of the edge and
// notify the target; they complete it with accept/pair.
hub.Register("request/pair", func(c *ws.Client, m protocol.Message) any {
to := m.Str("to")
target, ok := hub.Client(to)
if !ok {
return fail("CLIENT_NOT_FOUND")
}
if isPaired(c, target) {
return map[string]any{"status": "success", "message": "ALREADY-PAIRED"}
}
if c.HasPair(to) {
return fail("ALREADY-REQUESTED")
}
c.AddPair(target)
target.Signal("request/pair", map[string]any{"from": c.ID, "info": c.Info()})
return map[string]any{"status": "success", "message": "REQUESTED"}
})
// accept/pair: complete a pairing the peer `to` requested from us.
hub.Register("accept/pair", func(c *ws.Client, m protocol.Message) any {
to := m.Str("to")
requester, ok := hub.Client(to)
if !ok {
return fail("CLIENT_NOT_FOUND")
}
if isPaired(c, requester) {
return map[string]any{"status": "success", "message": "ALREADY-PAIRED"}
}
if !requester.HasPair(c.ID) {
return fail("NOT_REQUESTED_PAIR")
}
c.AddPair(requester)
requester.Signal("accepted/pair", map[string]any{"from": c.ID, "info": c.Info()})
return success()
})
// reject/pair and end/pair both tear the edge down in both directions and
// notify the other side.
teardown := func(c *ws.Client, m protocol.Message) any {
to := m.Str("to")
other, ok := hub.Client(to)
if !ok {
return fail("CLIENT_NOT_FOUND")
}
c.RemovePair(other)
other.RemovePair(c)
other.Signal("end/pair", map[string]any{"from": c.ID})
return success()
}
hub.Register("reject/pair", teardown)
hub.Register("end/pair", teardown)
hub.Register("pair/list", func(c *ws.Client, m protocol.Message) any {
var list []string
for _, id := range c.Pairs() {
if other, ok := hub.Client(id); ok && other.HasPair(c.ID) {
list = append(list, id)
}
}
return map[string]any{"type": "pair/list", "value": list}
})
hub.Register("is/reachable", func(c *ws.Client, m protocol.Message) any {
to := m.Str("to")
other, ok := hub.Client(to)
if !ok {
return false
}
if other.RequiredPair() && !other.HasPair(c.ID) {
return false
}
return true
})
// auth/info: set our metadata and push the change to every secure peer.
hub.Register("auth/info", func(c *ws.Client, m protocol.Message) any {
name := m.Str("name")
value := m.Get("value")
c.SetInfo(name, value)
pairs, roompairs := secureClients(hub, c)
payload := map[string]any{"from": c.ID, "name": name, "value": value}
for _, peer := range pairs {
peer.Signal("pair/info", payload)
}
for _, peer := range roompairs {
peer.Signal("pair/info", payload)
}
return success()
})
hub.Register("peer/info", func(c *ws.Client, m protocol.Message) any {
peerID := m.Str("peer")
if !isSecure(hub, c, peerID) {
return map[string]any{"status": "fail", "message": "unaccessible user"}
}
peer, _ := hub.Client(peerID)
return map[string]any{"status": "success", "info": peer.Info()}
})
}