-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Websocket replacement for Eventstream #20543
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
// Copyright 2022 The Gitea Authors. All rights reserved. | ||
// Use of this source code is governed by a MIT-style | ||
// license that can be found in the LICENSE file. | ||
|
||
package events | ||
|
||
import ( | ||
"net/http" | ||
"net/url" | ||
"time" | ||
|
||
"code.gitea.io/gitea/modules/context" | ||
"code.gitea.io/gitea/modules/eventsource" | ||
"code.gitea.io/gitea/modules/graceful" | ||
"code.gitea.io/gitea/modules/json" | ||
"code.gitea.io/gitea/modules/log" | ||
"code.gitea.io/gitea/modules/setting" | ||
"code.gitea.io/gitea/routers/web/auth" | ||
"github.com/gorilla/websocket" | ||
) | ||
|
||
const ( | ||
writeWait = 10 * time.Second | ||
pongWait = 60 * time.Second | ||
pingPeriod = (pongWait * 9) / 10 | ||
|
||
maximumMessageSize = 2048 | ||
readMessageChanSize = 20 // <- I've put 20 here because it seems like a reasonable buffer but it may to increase | ||
) | ||
|
||
type readMessage struct { | ||
messageType int | ||
message []byte | ||
err error | ||
} | ||
|
||
// Events listens for events | ||
func Websocket(ctx *context.Context) { | ||
upgrader := websocket.Upgrader{ | ||
ReadBufferSize: 1024, | ||
WriteBufferSize: 1024, | ||
CheckOrigin: func(r *http.Request) bool { | ||
origin := r.Header["Origin"] | ||
if len(origin) == 0 { | ||
return true | ||
} | ||
u, err := url.Parse(origin[0]) | ||
if err != nil { | ||
return false | ||
} | ||
appURLURL, err := url.Parse(setting.AppURL) | ||
if err != nil { | ||
return true | ||
} | ||
|
||
return u.Host == appURLURL.Host | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure this is safe, it should probably be stricter and include protocol and port as well. Thought we could also lean more on the cookie header. When the cookie has SameSite=strict, it could technically serve as the only authentification mechanism on the websocket. Thought, origin check is always good to have. |
||
}, | ||
} | ||
|
||
// Because http proxies will tend not to pass these headers | ||
ctx.Req.Header.Add("Upgrade", "websocket") | ||
ctx.Req.Header.Add("Connection", "Upgrade") | ||
|
||
conn, err := upgrader.Upgrade(ctx.Resp, ctx.Req, nil) | ||
if err != nil { | ||
log.Error("Unable to upgrade due to error: %v", err) | ||
return | ||
} | ||
defer conn.Close() | ||
|
||
notify := ctx.Done() | ||
shutdownCtx := graceful.GetManager().ShutdownContext() | ||
|
||
eventChan := make(<-chan *eventsource.Event) | ||
uid := int64(0) | ||
unregister := func() {} | ||
if ctx.IsSigned { | ||
uid = ctx.Doer.ID | ||
eventChan = eventsource.GetManager().Register(uid) | ||
unregister = func() { | ||
go func() { | ||
eventsource.GetManager().Unregister(uid, eventChan) | ||
// ensure the messageChan is closed | ||
for { | ||
_, ok := <-eventChan | ||
if !ok { | ||
break | ||
} | ||
} | ||
}() | ||
} | ||
} | ||
defer unregister() | ||
|
||
readMessageChan := make(chan readMessage, readMessageChanSize) | ||
go readMessagesFromConnToChan(conn, readMessageChan) | ||
|
||
pingTicker := time.NewTicker(pingPeriod) | ||
|
||
for { | ||
select { | ||
case <-notify: | ||
return | ||
case <-shutdownCtx.Done(): | ||
return | ||
case <-pingTicker.C: | ||
// ensure that we're not already cancelled | ||
select { | ||
case <-notify: | ||
return | ||
case <-shutdownCtx.Done(): | ||
return | ||
default: | ||
} | ||
|
||
if err := conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil { | ||
log.Error("unable to SetWriteDeadline: %v", err) | ||
return | ||
} | ||
if err := conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil { | ||
log.Error("unable to send PingMessage: %v", err) | ||
return | ||
} | ||
case message, ok := <-readMessageChan: | ||
if !ok { | ||
break | ||
} | ||
|
||
// ensure that we're not already cancelled | ||
select { | ||
case <-notify: | ||
return | ||
case <-shutdownCtx.Done(): | ||
return | ||
default: | ||
} | ||
|
||
// FIXME: HANDLE MESSAGES | ||
log.Info("Got Message: %d:%s:%v", message.messageType, message.message, message.err) | ||
case event, ok := <-eventChan: | ||
if !ok { | ||
break | ||
} | ||
|
||
// ensure that we're not already cancelled | ||
select { | ||
case <-notify: | ||
return | ||
case <-shutdownCtx.Done(): | ||
return | ||
default: | ||
} | ||
|
||
// Handle events | ||
if event.Name == "logout" { | ||
if ctx.Session.ID() == event.Data { | ||
event = &eventsource.Event{ | ||
Name: "logout", | ||
Data: "here", | ||
} | ||
_ = writeEvent(conn, event) | ||
go unregister() | ||
auth.HandleSignOut(ctx) | ||
break | ||
} | ||
// Replace the event - we don't want to expose the session ID to the user | ||
event = &eventsource.Event{ | ||
Name: "logout", | ||
Data: "elsewhere", | ||
} | ||
} | ||
if err := writeEvent(conn, event); err != nil { | ||
return | ||
} | ||
} | ||
} | ||
} | ||
|
||
func readMessagesFromConnToChan(conn *websocket.Conn, messageChan chan readMessage) { | ||
defer func() { | ||
close(messageChan) // Please note: this has to be within a wrapping anonymous func otherwise it will be evaluated when creating the defer | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting, I didn't know this. Is this evaluation behaviour something specific to the |
||
_ = conn.Close() | ||
}() | ||
conn.SetReadLimit(maximumMessageSize) | ||
if err := conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil { | ||
log.Error("unable to SetReadDeadline: %v", err) | ||
return | ||
} | ||
conn.SetPongHandler(func(string) error { | ||
if err := conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil { | ||
log.Error("unable to SetReadDeadline: %v", err) | ||
} | ||
return nil | ||
}) | ||
|
||
for { | ||
messageType, message, err := conn.ReadMessage() | ||
messageChan <- readMessage{ | ||
messageType: messageType, | ||
message: message, | ||
err: err, | ||
} | ||
if err != nil { | ||
// don't need to handle the error here as it is passed down the channel | ||
return | ||
} | ||
if err := conn.SetReadDeadline(time.Now().Add(pongWait)); err != nil { | ||
log.Error("unable to SetReadDeadline: %v", err) | ||
return | ||
} | ||
} | ||
} | ||
|
||
func writeEvent(conn *websocket.Conn, event *eventsource.Event) error { | ||
if err := conn.SetWriteDeadline(time.Now().Add(writeWait)); err != nil { | ||
log.Error("unable to SetWriteDeadline: %v", err) | ||
return err | ||
} | ||
|
||
w, err := conn.NextWriter(websocket.TextMessage) | ||
if err != nil { | ||
log.Error("Unable to get writer for websocket %v", err) | ||
return err | ||
} | ||
|
||
if err := json.NewEncoder(w).Encode(event); err != nil { | ||
log.Error("Unable to create encoder for %v %v", event, err) | ||
return err | ||
} | ||
if err := w.Close(); err != nil { | ||
log.Warn("Unable to close writer for websocket %v", err) | ||
return err | ||
} | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -365,6 +365,7 @@ func RegisterRoutes(m *web.Route) { | |
}, reqSignOut) | ||
|
||
m.Any("/user/events", routing.MarkLongPolling, events.Events) | ||
m.Any("/user/websocket", routing.MarkLongPolling, events.Websocket) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this cookie-authenticated? If not, it should be. |
||
|
||
m.Group("/login/oauth", func() { | ||
m.Get("/authorize", bindIgnErr(forms.AuthorizationForm{}), auth.AuthorizeOAuth) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Origin header will be present even on first-party requests, so we can require it.