feat(app): CORS for appen, en billet til live-synk, og /ping siger hvad den er

Andet trin mod iOS-appen. Appen henter sin flade fra capacitor://localhost og
taler med den server, ejeren har skrevet ind, så hver anmodning er cross-origin.

- CORS på /api/v1 for appens origin og ingen andres. Ingen
  Allow-Credentials: appen bærer sin session som bearer, aldrig som cookie.
  Alle andre origins får ingen headere, præcis som før.
- POST /ws-ticket giver en engangsbillet, der lever 30 sekunder, og
  /ws?ticket=… åbner socket'en med den. En browser kan ikke sætte en header
  på et WebSocket-håndtryk, og tokenet selv i URL'en ville ligge i hver
  proxy-log i månedsvis. En socket med billet springer origin-tjekket over,
  fordi den ikke bærer nogen cookie, som en fremmed side kunne låne.
- Et håndtryk fra appens origin uden billet afvises. Prøven fandt hvorfor:
  biblioteket sammenligner kun værtsnavne, og capacitor://localhost har samme
  værtsnavn som en instans på http://localhost.
- /ping svarer med product, edition og api, så appen kan kende en Verdande
  fra en routers login-side, før nogen er logget ind. Ingen version: /version
  er bag et login med vilje.

Fem plantede fejl, hver fanget af sin egen prøve: CORS til enhver origin,
genbrugelig billet, billet uden udløb, app-origin på en cookie, og
Allow-Credentials.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-10-23 14:03:01 +02:00
co-authored by Claude Opus 5.5
parent a8db761a4b
commit a71af41d9e
6 changed files with 430 additions and 4 deletions
+4
View File
@@ -45,11 +45,15 @@ func (s *Server) publishToOwner(r *http.Request, event string, payload any) {
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
user := userFrom(r.Context())
ticketed, _ := r.Context().Value(ctxTicket).(bool)
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
// The socket is same-origin only. Without this, any page on the internet
// could open a socket carrying the user's cookie and watch their tasks —
// the same-origin policy does not apply to WebSocket handshakes.
OriginPatterns: s.allowedOrigins(),
// Except one opened with a ticket, which carries no cookie for a foreign
// page to borrow: see handleWebSocketEntry.
InsecureSkipVerify: ticketed,
})
if err != nil {
s.log.Debug("websocket handshake", "err", err)