snapshot of https://github.com/go-gitea/gitea.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
cache-seeder / prune (push) Has been skipped
cache-seeder / frontend (push) Failing after 3s
cache-seeder / python (push) Failing after 3s
cache-seeder / gobuild (push) Failing after 4s
cache-seeder / lint (bindata, lint-backend) (push) Failing after 3s
giteabot backport / giteabot (push) Has been skipped
giteabot / giteabot (push) Has been skipped
cache-prune / prune (push) Has been skipped
release-nightly-snapcraft / build-and-publish (ubuntu-24.04) (push) Has been cancelled
release-nightly-snapcraft / build-and-publish (ubuntu-24.04-arm) (push) Has been cancelled
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
cron-renovate / cron-renovate (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:41:59 +05:00
commit 52ae0c5802
6280 changed files with 624193 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package websocket
import (
gocontext "context"
"net/http"
"time"
"gitea.dev/modules/graceful"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
"gitea.dev/services/context"
"gitea.dev/services/pubsub"
websocket_service "gitea.dev/services/websocket"
gitea_ws "github.com/coder/websocket"
)
const (
pingInterval = 30 * time.Second
pingTimeout = 10 * time.Second
writeTimeout = 10 * time.Second
// First code in the IANA library/framework reserved range (3000–3999).
// Sentinel for an unauthenticated session so the SharedWorker can tell
// "your cookie is gone" apart from a transient network failure and stop
// reconnecting in a tight loop.
closeCodeUnauthenticated gitea_ws.StatusCode = 3000
)
// filterLogout forwards a session-free logout only to the targeted connection
// (its own session, or every session when SessionID is empty) and drops it for
// the rest. Non-logout messages pass through untouched.
func filterLogout(eventType string, eventDataBytes []byte, connSessionID string) []byte {
if eventType != websocket_service.EventLogout {
return eventDataBytes
}
var lm websocket_service.UserEventMessage[websocket_service.LogoutEventData]
if err := json.Unmarshal(eventDataBytes, &lm); err != nil {
return eventDataBytes
}
if lm.EventData.SessionID == "" || lm.EventData.SessionID == connSessionID {
return []byte(`{"eventType":"logout"}`)
}
return nil
}
func Serve(ctx *context.Context) {
// Answer plain GETs (health checks, crawlers) here; letting Accept reject them
// would log an error per request. Same reply it would have sent.
if ctx.Req.Header.Get("Upgrade") == "" {
ctx.Resp.Header().Set("Connection", "Upgrade")
ctx.Resp.Header().Set("Upgrade", "websocket")
ctx.Resp.WriteHeader(http.StatusUpgradeRequired)
return
}
conn, err := gitea_ws.Accept(ctx.Resp, ctx.Req, nil)
if err != nil {
log.Error("websocket: accept failed: %v", err)
return
}
defer conn.CloseNow() //nolint:errcheck // best-effort close
if !ctx.IsSigned {
_ = conn.Close(closeCodeUnauthenticated, "unauthenticated")
return
}
sessionID := ctx.Session.ID()
ch, cancel := pubsub.DefaultBroker.Subscribe(pubsub.UserTopic(ctx.Doer.ID))
defer cancel()
// Ping requires a concurrent reader to observe the pong frame; CloseRead
// spawns one and cancels its context when the peer goes away.
wsCtx := conn.CloseRead(ctx.Req.Context())
shutdownCtx := graceful.GetManager().ShutdownContext()
pingTicker := time.NewTicker(pingInterval)
defer pingTicker.Stop()
for {
select {
case <-wsCtx.Done():
return
case <-shutdownCtx.Done():
return
case <-pingTicker.C:
pingCtx, cancelPing := gocontext.WithTimeout(wsCtx, pingTimeout)
err := conn.Ping(pingCtx)
cancelPing()
if err != nil {
log.Trace("websocket: ping failed: %v", err)
return
}
case brokerPayload, ok := <-ch:
if !ok {
return
}
eventType, eventDataBytes := websocket_service.ExtractUserEventMessage(brokerPayload)
eventDataBytes = filterLogout(eventType, eventDataBytes, sessionID)
if eventDataBytes == nil {
continue
}
// Bound the write so a stalled/slow peer can't block this goroutine
// indefinitely and starve the ping ticker.
writeCtx, cancelWrite := gocontext.WithTimeout(wsCtx, writeTimeout)
err := conn.Write(writeCtx, gitea_ws.MessageText, eventDataBytes)
cancelWrite()
if err != nil {
log.Trace("websocket: write failed: %v", err)
return
}
}
}
}

View File

@@ -0,0 +1,60 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package websocket
import (
"testing"
"gitea.dev/services/websocket"
"github.com/stretchr/testify/assert"
)
func TestFilterLogout(t *testing.T) {
cases := []struct {
name string
brokerMsg []byte
connSessID string
want []byte // expected payload forwarded to the client
}{
{
name: "originating session gets a session-free logout",
brokerMsg: websocket.MakeUserEventMessage("logout", websocket.LogoutEventData{SessionID: "sess-A"}),
connSessID: "sess-A",
want: []byte(`{"eventType":"logout"}`),
},
{
name: "other session is dropped",
brokerMsg: websocket.MakeUserEventMessage("logout", websocket.LogoutEventData{SessionID: "sess-A"}),
connSessID: "sess-B",
want: nil,
},
{
name: "empty sessionID reaches every session",
brokerMsg: websocket.MakeUserEventMessage("logout", nil),
connSessID: "sess-A",
want: []byte(`{"eventType":"logout"}`),
},
{
name: "non-logout message passes through unchanged",
brokerMsg: websocket.MakeUserEventMessage("other", map[string]any{"k": "v"}),
connSessID: "sess-A",
want: []byte(`{"eventType":"other","eventData":{"k":"v"}}`),
},
{
name: "malformed JSON with logout marker passes through unchanged",
brokerMsg: []byte("any type\nany data"),
connSessID: "sess-A",
want: []byte("any data"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
eventType, eventData := websocket.ExtractUserEventMessage(tc.brokerMsg)
out := filterLogout(eventType, eventData, tc.connSessID)
assert.Equal(t, tc.want, out)
})
}
}