Pass remote address in WebSocketMessageHasBeenPosted plugin hook (#27332)

This commit is contained in:
Claudio Costa 2024-06-13 09:01:49 +02:00 committed by GitHub
parent d2c3710265
commit 4b0ae20ef7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 110 additions and 15 deletions

View File

@ -2033,3 +2033,49 @@ func TestPluginWebSocketSession(t *testing.T) {
_, sessionID := hooks.MessageWillBePosted(nil, nil) _, sessionID := hooks.MessageWillBePosted(nil, nil)
require.Equal(t, sessions[0].Id, sessionID) require.Equal(t, sessions[0].Id, sessionID)
} }
func TestPluginWebSocketRemoteAddress(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
pluginID := "com.mattermost.websocket_remote_address_test"
// Compile plugin
fullPath := filepath.Join(server.GetPackagePath(), "channels", "app", "plugin_api_tests", "manual.test_websocket_remote_address", "main.go")
pluginCode, err := os.ReadFile(fullPath)
require.NoError(t, err)
require.NotEmpty(t, pluginCode)
pluginDir, err := filepath.Abs(*th.App.Config().PluginSettings.Directory)
require.NoError(t, err)
backend := filepath.Join(pluginDir, pluginID, "backend.exe")
utils.CompileGo(t, string(pluginCode), backend)
os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(`{"id": "`+pluginID+`", "server": {"executable": "backend.exe"}}`), 0600)
// Activate the plugin
manifest, activated, reterr := th.App.GetPluginsEnvironment().Activate(pluginID)
require.NoError(t, reterr)
require.NotNil(t, manifest)
require.True(t, activated)
// Connect through WebSocket and send a message
reqURL := fmt.Sprintf("ws://localhost:%d", th.Server.ListenAddr.Port)
wsc, err := model.NewWebSocketClient4(reqURL, th.Client.AuthToken)
require.NoError(t, err)
require.NotNil(t, wsc)
wsc.Listen()
defer wsc.Close()
resp := <-wsc.ResponseChannel
require.Equal(t, resp.Status, model.StatusOk)
wsc.SendMessage("custom_action", map[string]any{"value": "test"})
// Verify the remote address has been set correctly. Check plugin code in
// channels/app/plugin_api_tests/manual.test_websocket_remote_address
//
// Here the MessageWillBePosted hook is used purely as a way to
// communicate with the plugin side.
hooks, err := th.App.GetPluginsEnvironment().HooksForPlugin(pluginID)
require.NoError(t, err)
require.NotNil(t, hooks)
_, remoteAddr := hooks.MessageWillBePosted(nil, nil)
require.NotEmpty(t, remoteAddr)
}

View File

@ -44,12 +44,14 @@ func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) {
// We initialize webconn with all the necessary data. // We initialize webconn with all the necessary data.
// If the queues are empty, they are initialized in the constructor. // If the queues are empty, they are initialized in the constructor.
cfg := &platform.WebConnConfig{ cfg := &platform.WebConnConfig{
WebSocket: ws, WebSocket: ws,
Session: *c.AppContext.Session(), Session: *c.AppContext.Session(),
TFunc: c.AppContext.T, TFunc: c.AppContext.T,
Locale: "", Locale: "",
Active: true, Active: true,
PostedAck: r.URL.Query().Get(postedAckParam) == "true", PostedAck: r.URL.Query().Get(postedAckParam) == "true",
RemoteAddress: c.AppContext.IPAddress(),
XForwardedFor: c.AppContext.XForwardedFor(),
} }
// The WebSocket upgrade request coming from mobile is missing the // The WebSocket upgrade request coming from mobile is missing the
// user agent so we need to fallback on the session's metadata. // user agent so we need to fallback on the session's metadata.

View File

@ -61,15 +61,17 @@ type pluginWSPostedHook struct {
} }
type WebConnConfig struct { type WebConnConfig struct {
WebSocket *websocket.Conn WebSocket *websocket.Conn
Session model.Session Session model.Session
TFunc i18n.TranslateFunc TFunc i18n.TranslateFunc
Locale string Locale string
ConnectionID string ConnectionID string
Active bool Active bool
ReuseCount int ReuseCount int
OriginClient string OriginClient string
PostedAck bool PostedAck bool
RemoteAddress string
XForwardedFor string
// These aren't necessary to be exported to api layer. // These aren't necessary to be exported to api layer.
sequence int sequence int
@ -121,6 +123,10 @@ type WebConn struct {
// The client type behind the connection (i.e. web, desktop or mobile) // The client type behind the connection (i.e. web, desktop or mobile)
originClient string originClient string
// The remote address from the original HTTP Upgrade request
remoteAddress string
// The X-Forwarded-For HTTP header value from the origina HTTP Upgrade request
xForwardedFor string
activeChannelID atomic.Value activeChannelID atomic.Value
activeTeamID atomic.Value activeTeamID atomic.Value
@ -245,6 +251,8 @@ func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, runn
lastLogTimeSlow: time.Now(), lastLogTimeSlow: time.Now(),
lastLogTimeFull: time.Now(), lastLogTimeFull: time.Now(),
originClient: cfg.OriginClient, originClient: cfg.OriginClient,
remoteAddress: cfg.RemoteAddress,
xForwardedFor: cfg.XForwardedFor,
} }
wc.Active.Store(cfg.Active) wc.Active.Store(cfg.Active)
@ -479,6 +487,12 @@ func (wc *WebConn) readPump() {
clonedReq.Session.Id = session.Id clonedReq.Session.Id = session.Id
} }
if clonedReq.Data == nil {
clonedReq.Data = map[string]any{}
}
clonedReq.Data[model.WebSocketRemoteAddr] = wc.remoteAddress
clonedReq.Data[model.WebSocketXForwardedFor] = wc.xForwardedFor
wc.pluginPosted <- pluginWSPostedHook{wc.GetConnectionID(), wc.UserId, clonedReq} wc.pluginPosted <- pluginWSPostedHook{wc.GetConnectionID(), wc.UserId, clonedReq}
} }
} }

View File

@ -0,0 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package main
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
)
type Plugin struct {
plugin.MattermostPlugin
addrCh chan string
}
func (p *Plugin) MessageWillBePosted(_ *plugin.Context, _ *model.Post) (*model.Post, string) {
return nil, <-p.addrCh
}
func (p *Plugin) WebSocketMessageHasBeenPosted(connID, userID string, req *model.WebSocketRequest) {
p.addrCh <- req.Data[model.WebSocketRemoteAddr].(string)
}
func main() {
plugin.ClientMain(&Plugin{
addrCh: make(chan string, 1),
})
}

View File

@ -9,6 +9,11 @@ import (
"github.com/vmihailenco/msgpack/v5" "github.com/vmihailenco/msgpack/v5"
) )
const (
WebSocketRemoteAddr = "remote_addr"
WebSocketXForwardedFor = "x_forwarded_for"
)
// WebSocketRequest represents a request made to the server through a websocket. // WebSocketRequest represents a request made to the server through a websocket.
type WebSocketRequest struct { type WebSocketRequest struct {
// Client-provided fields // Client-provided fields