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.9 KiB
Go
74 lines
2.9 KiB
Go
package api_test
|
|
|
|
// client_update_epoch_test.go — the client-update endpoint never advertises a
|
|
// release whose signed manifest declares a protocol epoch newer than this
|
|
// server's (B2-2): a client that auto-updated onto it would be refused at the
|
|
// next handshake. A manifest that does not verify is treated the same way —
|
|
// fail closed, 204 — since its epoch cannot be trusted. A release with no
|
|
// manifest at all predates the epoch and is advertised as before
|
|
// (client_update_test.go covers that path throughout).
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/J3vb/OwnCord/Server/updater"
|
|
)
|
|
|
|
// fakeGitHubReleaseWithManifest is fakeGitHubRelease plus a server-update
|
|
// manifest and signature, served with the given bytes.
|
|
func fakeGitHubReleaseWithManifest(t *testing.T, tag string, manifest, sig []byte) *httptest.Server {
|
|
t.Helper()
|
|
var srv *httptest.Server
|
|
mux := http.NewServeMux()
|
|
assetNames := []string{
|
|
"OwnCord_1.0.0_x64-setup.nsis.zip",
|
|
"OwnCord_1.0.0_x64-setup.nsis.zip.sig",
|
|
"server-update-manifest.json",
|
|
"server-update-manifest.json.sig",
|
|
}
|
|
mux.HandleFunc("/repos/test/repo/releases/latest", func(w http.ResponseWriter, _ *http.Request) {
|
|
assets := make([]map[string]any, 0, len(assetNames))
|
|
for _, name := range assetNames {
|
|
assets = append(assets, map[string]any{"name": name, "browser_download_url": srv.URL + "/download/" + name})
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"tag_name": tag, "body": "notes", "html_url": "x", "assets": assets})
|
|
})
|
|
mux.HandleFunc("/download/", func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "server-update-manifest.json"):
|
|
_, _ = w.Write(manifest)
|
|
case strings.HasSuffix(r.URL.Path, "server-update-manifest.json.sig"):
|
|
_, _ = w.Write(sig)
|
|
case strings.HasSuffix(r.URL.Path, ".sig"):
|
|
_, _ = w.Write([]byte("dW50cnVzdGVkIGNvbW1lbnQ="))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
srv = httptest.NewServer(mux)
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
func TestClientUpdate_UnverifiableManifestIsNotAdvertised(t *testing.T) {
|
|
// The manifest claims epoch 1 (compatible) but its signature is garbage:
|
|
// the claim is untrusted, so the release is withheld.
|
|
manifest := []byte(`{"version":"v2.0.0","asset":"chatserver.exe","sha256":"00","protocol_epoch":1}`)
|
|
srv := fakeGitHubReleaseWithManifest(t, "v2.0.0", manifest, []byte("not a signature"))
|
|
u := updater.NewUpdater("1.0.0", "", "test", "repo")
|
|
u.SetBaseURL(srv.URL)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/1.0.0", nil)
|
|
rr := httptest.NewRecorder()
|
|
buildClientUpdateRouter(u).ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusNoContent {
|
|
t.Fatalf("status = %d, want 204 (unverifiable manifest must not be advertised); body: %s", rr.Code, rr.Body.String())
|
|
}
|
|
}
|