Files
deathwatch/auth_test.go
T
emmatherock e1f7e6f529 Initial commit: DeathWatch, an Elden Ring death counter for OBS
Reads the game's process memory read-only (AOB signature scanning,
re-resolved every tick — never a cached pointer) and serves a live
death counter as a browser-source overlay plus a status panel.

Supports co-op: each player runs the program locally, one acts as
hub. The peer link is authenticated (HMAC challenge/response, no
token on the wire, constant-time compare) but not yet encrypted —
see CLAUDE.md's TODO section for the planned TLS+pinning split.

Zero external dependencies — including a hand-rolled WebSocket
implementation — so the whole thing ships as one .exe.
2026-09-17 21:10:22 -03:00

193 lines
5.2 KiB
Go

package main
import (
"encoding/json"
"net"
"net/http"
"testing"
"time"
)
// arrancaHub levanta un hub con un token dado y devuelve su direccion.
func arrancaHub(t *testing.T, token string) (*peerRegistry, string) {
t.Helper()
reg := newPeerRegistry()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
mux.HandleFunc("/ws", reg.wsHandler(token))
srv := &http.Server{Handler: mux}
go srv.Serve(ln)
t.Cleanup(func() { srv.Close() })
return reg, ln.Addr().String()
}
func peers(reg *peerRegistry) int { return len(reg.views()) }
// El token correcto entra y sus datos llegan.
func TestTokenCorrectoEntra(t *testing.T) {
reg, addr := arrancaHub(t, "el-bueno")
c, err := wsDial(addr, "/ws", 3*time.Second)
if err != nil {
t.Fatal(err)
}
defer c.Close()
if err := authenticate(c, config{Token: "el-bueno", clientID: "abc"}); err != nil {
t.Fatalf("deberia haber autenticado: %v", err)
}
msg, _ := json.Marshal(peerMessage{Type: "state", ID: "abc", Name: "Fulano", Deaths: 7})
if err := c.WriteText(msg); err != nil {
t.Fatal(err)
}
esperarHasta(t, func() bool { return peers(reg) == 1 }, "el peer nunca aparecio")
if got := reg.views()[0].Deaths; got != 7 {
t.Fatalf("esperaba 7 muertes, dio %d", got)
}
}
// El token equivocado NO entra y no deja rastro.
func TestTokenEquivocadoNoEntra(t *testing.T) {
reg, addr := arrancaHub(t, "el-bueno")
c, err := wsDial(addr, "/ws", 3*time.Second)
if err != nil {
t.Fatal(err)
}
defer c.Close()
_ = authenticate(c, config{Token: "el-malo", clientID: "x"})
msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Intruso", Deaths: 999})
c.WriteText(msg)
time.Sleep(700 * time.Millisecond)
if peers(reg) != 0 {
t.Fatalf("el hub acepto datos con el token equivocado: %+v", reg.views())
}
}
// Mandar datos SIN autenticarse tampoco funciona.
func TestSinAutenticarNoEntra(t *testing.T) {
reg, addr := arrancaHub(t, "el-bueno")
c, err := wsDial(addr, "/ws", 3*time.Second)
if err != nil {
t.Fatal(err)
}
defer c.Close()
msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Intruso", Deaths: 999})
c.WriteText(msg)
time.Sleep(700 * time.Millisecond)
if peers(reg) != 0 {
t.Fatalf("el hub acepto datos sin autenticacion: %+v", reg.views())
}
}
// El token no viaja: lo unico que sale del peer es un HMAC del desafio.
func TestElTokenNoViajaPorLaRed(t *testing.T) {
_, addr := arrancaHub(t, "secreto-muy-secreto")
c, err := wsDial(addr, "/ws", 3*time.Second)
if err != nil {
t.Fatal(err)
}
defer c.Close()
raw, err := c.ReadMessage() // el desafio
if err != nil {
t.Fatal(err)
}
var ch authChallenge
json.Unmarshal(raw, &ch)
if ch.Nonce == "" {
t.Fatal("el hub no mando desafio")
}
reply, _ := json.Marshal(authReply{Type: "auth", ID: "x", Proof: proofFor("secreto-muy-secreto", ch.Nonce)})
if string(reply) == "" {
t.Fatal("respuesta vacia")
}
for _, b := range [][]byte{raw, reply} {
if contiene(string(b), "secreto-muy-secreto") {
t.Fatalf("el token aparece en el trafico: %s", b)
}
}
}
// Una respuesta capturada no sirve en otra conexion: el desafio cambia.
func TestNoSePuedeRepetirUnaRespuestaVieja(t *testing.T) {
reg, addr := arrancaHub(t, "el-bueno")
c1, err := wsDial(addr, "/ws", 3*time.Second)
if err != nil {
t.Fatal(err)
}
raw, _ := c1.ReadMessage()
var ch1 authChallenge
json.Unmarshal(raw, &ch1)
proofViejo := proofFor("el-bueno", ch1.Nonce)
c1.Close()
c2, err := wsDial(addr, "/ws", 3*time.Second)
if err != nil {
t.Fatal(err)
}
defer c2.Close()
raw2, _ := c2.ReadMessage()
var ch2 authChallenge
json.Unmarshal(raw2, &ch2)
if ch2.Nonce == ch1.Nonce {
t.Fatal("el desafio se repitio entre conexiones: el nonce tiene que ser distinto")
}
reply, _ := json.Marshal(authReply{Type: "auth", ID: "x", Proof: proofViejo})
c2.WriteText(reply)
msg, _ := json.Marshal(peerMessage{Type: "state", ID: "x", Name: "Repetidor", Deaths: 1})
c2.WriteText(msg)
time.Sleep(700 * time.Millisecond)
if peers(reg) != 0 {
t.Fatalf("el hub acepto una respuesta repetida: %+v", reg.views())
}
}
// El token generado es aleatorio y de largo razonable.
func TestTokenGeneradoEsFuerte(t *testing.T) {
testExeDir = t.TempDir()
a, gen, err := resolveToken(config{})
if err != nil || !gen {
t.Fatalf("deberia haber generado uno: gen=%v err=%v", gen, err)
}
if len(a) != tokenBytes*2 {
t.Fatalf("largo inesperado: %d", len(a))
}
// la segunda vez lo lee del archivo, no genera otro
b, gen2, _ := resolveToken(config{})
if gen2 || a != b {
t.Fatalf("deberia reusar el guardado: %q vs %q (gen=%v)", a, b, gen2)
}
// y en otra instalacion sale distinto
testExeDir = t.TempDir()
c, _, _ := resolveToken(config{})
if c == a {
t.Fatal("dos instalaciones generaron el mismo token")
}
testExeDir = ""
}
func contiene(s, sub string) bool {
return len(sub) > 0 && len(s) >= len(sub) && (func() bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
})()
}
func esperarHasta(t *testing.T, cond func() bool, msg string) {
t.Helper()
deadline := time.Now().Add(4 * time.Second)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(50 * time.Millisecond)
}
t.Fatal(msg)
}