mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* feat(b2-2): declare protocol_epoch in the schema and generate both constants protocol/schema.json gains protocol_epoch (1). genprotocol emits ws.ProtocolEpoch and PROTOCOL_EPOCH from it; the contract test pins the Go constant to the schema so a stale regeneration fails the required check. * feat(b2-2): check the client's protocol epoch in the auth handshake The auth payload gains epoch (absent = 0). Outside [minClientEpoch, ProtocolEpoch] the server answers one auth_error with code protocol_epoch_unsupported, the client/server/min epochs, and a message naming which side to update, then closes 1008 like every other handshake failure. minClientEpoch is 0 for epoch 1 only so alpha.4 clients keep connecting; the epoch-1 fixtures are unchanged. * feat(b2-2): send the protocol epoch and offer the update on a refused connect ws.ts sends epoch: PROTOCOL_EPOCH in the auth frame (contract test extended on purpose). On auth_error code protocol_epoch_unsupported with a newer server the dispatcher records the host in ui.store.updateRequiredHost and main.ts mounts the UpdateNotifier on the connect page, so a refused client gets the same Update Now banner it would have had on the main page. * feat(b2-2): withhold client releases newer than the server's protocol epoch The signed server-update manifest gains protocol_epoch (release.yml reads it from protocol/schema.json). Updater.ReleaseProtocolEpoch verifies the manifest and reads it; the client-update endpoint answers 204 when the release's epoch is newer than ws.ProtocolEpoch or the manifest does not verify. Releases without a manifest are epoch 0 and advertised as before. Docs: protocol.md Compatibility section, api.md, deployment.md, protocol README, CHANGELOG Unreleased. * docs(b2-2): record the slim B2-2 decision and evidence; fold B2-3/B2-4 into it * ci: prove the protocol_epoch manifest read on every PR, not only at tag time * fix(b2-2): offer the update on an already-mounted connect page and keep the credential on a protocol refusal Codex P1: on a first login or startup auto-login no overlay exists before auth_ok, so a refusal never re-rendered the connect page and the one-time read of updateRequiredHost missed it. The connect page now subscribes to it, and a later refusal replaces the banner. Codex P2: a refusal on reconnect went through the generic logout and deleted the stored credential although the token is still valid. clearAuth gets a protocol_epoch reason; main.ts keeps the credential on it (the skip-auto-login flag is still set and, being sessionStorage, does not survive the relaunch the update triggers).
74 lines
2.2 KiB
Go
74 lines
2.2 KiB
Go
package ws_test
|
|
|
|
// protocol_epoch_test.go — the auth handshake's epoch check (B2-2).
|
|
//
|
|
// The server accepts an auth frame whose `epoch` lies in
|
|
// [minClientEpoch, ProtocolEpoch]; absent means 0, which this first epoch
|
|
// still accepts so v1.2.0-alpha.4 clients (no epoch at all) keep connecting.
|
|
// Anything else gets one auth_error carrying code
|
|
// "protocol_epoch_unsupported" plus the numbers a client needs to say which
|
|
// side is out of date, and then the same 1008 close every other handshake
|
|
// failure gets. Reuses the epoch-1 rig so the table drives a real socket.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/J3vb/OwnCord/Server/ws"
|
|
)
|
|
|
|
func TestAuth_ProtocolEpoch(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
epoch any // nil = field absent
|
|
accept bool
|
|
older bool // on reject: is the client the older side?
|
|
}{
|
|
{name: "absent", epoch: nil, accept: true},
|
|
{name: "zero", epoch: 0, accept: true},
|
|
{name: "current", epoch: ws.ProtocolEpoch, accept: true},
|
|
{name: "newer", epoch: ws.ProtocolEpoch + 1, accept: false, older: false},
|
|
{name: "negative", epoch: -1, accept: false, older: true},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
r := newEpochRig(t, "protocol-epoch-"+tc.name)
|
|
_, tok := r.seedUser(t, "alice")
|
|
c := r.dial(t, "alice")
|
|
|
|
payload := map[string]any{"token": tok, "last_seq": 0}
|
|
if tc.epoch != nil {
|
|
payload["epoch"] = tc.epoch
|
|
}
|
|
c.send(map[string]any{"type": "auth", "payload": payload})
|
|
|
|
if tc.accept {
|
|
c.expect("auth_ok")
|
|
return
|
|
}
|
|
|
|
frame := c.expect("auth_error")
|
|
p, _ := frame["payload"].(map[string]any)
|
|
if got := p["code"]; got != "protocol_epoch_unsupported" {
|
|
t.Fatalf("code = %v, want protocol_epoch_unsupported: %v", got, p)
|
|
}
|
|
if got := p["server_epoch"]; got != json.Number("1") {
|
|
t.Fatalf("server_epoch = %v (%T), want 1", got, got)
|
|
}
|
|
if got := p["min_epoch"]; got != json.Number("0") {
|
|
t.Fatalf("min_epoch = %v, want 0", got)
|
|
}
|
|
msg, _ := p["message"].(string)
|
|
want := "update the server"
|
|
if tc.older {
|
|
want = "update the client"
|
|
}
|
|
if !strings.Contains(msg, want) {
|
|
t.Fatalf("message = %q, want it to say %q", msg, want)
|
|
}
|
|
c.expectClosed()
|
|
})
|
|
}
|
|
}
|