mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
* Adds OmitConnection parameter to broadcast Currently we have no means to omit sending a websocket event to a specific connection id. This is needed mainly so that the initiator won't receive an event for the action it just initiated. Will be used for the global drafts feature, so that we won't update drafts through ws when a user is typing. This commit adds OmitConnection to the Broadcast struct and to the NewWebSocketEvent function signature. shouldSendEvent should return false for that specific connection. * Return early only if connection id matches the omitted Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
282 lines
7.4 KiB
Go
282 lines
7.4 KiB
Go
//go:build gofuzz
|
|
// +build gofuzz
|
|
|
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
package app
|
|
|
|
import (
|
|
"math/rand"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"github.com/mattermost/mattermost-server/v6/model"
|
|
"github.com/mattermost/mattermost-server/v6/shared/i18n"
|
|
"github.com/mattermost/mattermost-server/v6/testlib"
|
|
)
|
|
|
|
// This is a file used to fuzz test the web_hub code.
|
|
// It performs a high-level fuzzing of the web_hub by spawning a hub
|
|
// and creating connections to it with a fixed concurrency.
|
|
//
|
|
// During the fuzz test, we create the server just once, and we send
|
|
// the random byte slice through a channel and perform some actions depending
|
|
// on the random data.
|
|
// The actions are decided in the getActionData function which decides
|
|
// which user, team, channel should the message go to and some other stuff too.
|
|
//
|
|
// Since this requires help of the testing library, we have to duplicate some code
|
|
// over here because go-fuzz cannot take code from _test.go files. It won't affect
|
|
// the main build because it's behind a build tag.
|
|
//
|
|
// To run this:
|
|
// 1. go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
|
|
// 2. mv app/helper_test.go app/helper.go
|
|
// (Also reduce the number of push notification workers to 1 to debug stack traces easily.)
|
|
// 3. go-fuzz-build github.com/mattermost/mattermost-server/v6/app
|
|
// 4. Generate a corpus dir. It's just a directory with files containing random data
|
|
// for go-fuzz to use as an initial seed. Use the generateInitialCorpus function for that.
|
|
// 5. go-fuzz -bin=app-fuzz.zip -workdir=./workdir
|
|
var mainHelper *testlib.MainHelper
|
|
|
|
func init() {
|
|
testing.Init()
|
|
var options = testlib.HelperOptions{
|
|
EnableStore: true,
|
|
EnableResources: true,
|
|
}
|
|
|
|
mainHelper = testlib.NewMainHelperWithOptions(&options)
|
|
}
|
|
|
|
func dummyWebsocketHandler() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, req *http.Request) {
|
|
upgrader := &websocket.Upgrader{
|
|
ReadBufferSize: 1024,
|
|
WriteBufferSize: 1024,
|
|
}
|
|
conn, err := upgrader.Upgrade(w, req, nil)
|
|
for err == nil {
|
|
_, _, err = conn.ReadMessage()
|
|
}
|
|
if _, ok := err.(*websocket.CloseError); !ok {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func registerDummyWebConn(a *App, addr net.Addr, userID string) *WebConn {
|
|
session, appErr := a.CreateSession(&model.Session{
|
|
UserId: userID,
|
|
})
|
|
if appErr != nil {
|
|
panic(appErr)
|
|
}
|
|
|
|
d := websocket.Dialer{}
|
|
c, _, err := d.Dial("ws://"+addr.String()+"/ws", nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
wc := a.NewWebConn(c, *session, i18n.IdentityTfunc(), "en")
|
|
a.HubRegister(wc)
|
|
go wc.Pump()
|
|
return wc
|
|
}
|
|
|
|
type actionData struct {
|
|
event string
|
|
createUserID string
|
|
selectChannelID string
|
|
selectTeamID string
|
|
invalidateConnUserID string
|
|
updateConnUserID string
|
|
attachment map[string]any
|
|
}
|
|
|
|
func getActionData(data []byte, userIDs, teamIDs, channelIDs []string) *actionData {
|
|
// Some sample events
|
|
events := []string{
|
|
model.WebsocketEventChannelCreated,
|
|
model.WebsocketEventChannelDeleted,
|
|
model.WebsocketEventUserAdded,
|
|
model.WebsocketEventUserUpdated,
|
|
model.WebsocketEventStatusChange,
|
|
model.WebsocketEventHello,
|
|
model.WebsocketAuthenticationChallenge,
|
|
model.WebsocketEventReactionAdded,
|
|
model.WebsocketEventReactionRemoved,
|
|
model.WebsocketEventResponse,
|
|
}
|
|
// We need atleast 10 bytes to get all the data we need
|
|
if len(data) < 10 {
|
|
return nil
|
|
}
|
|
input := &actionData{}
|
|
// Assign userID, channelID, teamID randomly from respective byte indices
|
|
input.createUserID = userIDs[int(data[0])%len(userIDs)]
|
|
input.selectChannelID = channelIDs[int(data[1])%len(channelIDs)]
|
|
input.selectTeamID = teamIDs[int(data[2])%len(teamIDs)]
|
|
input.invalidateConnUserID = userIDs[int(data[3])%len(userIDs)]
|
|
input.updateConnUserID = userIDs[int(data[4])%len(userIDs)]
|
|
input.event = events[int(data[5])%len(events)]
|
|
data = data[6:]
|
|
input.attachment = make(map[string]any)
|
|
for len(data) >= 4 { // 2 bytes key, 2 bytes value
|
|
k := data[:2]
|
|
v := data[2:4]
|
|
input.attachment[string(k)] = v
|
|
data = data[4:]
|
|
}
|
|
|
|
return input
|
|
}
|
|
|
|
var startServerOnce sync.Once
|
|
var dataChan chan []byte
|
|
var resChan = make(chan int, 4) // buffer of 4 to keep reading results.
|
|
|
|
func Fuzz(data []byte) int {
|
|
// We don't want to close anything down as the fuzzer will keep on running forever.
|
|
startServerOnce.Do(func() {
|
|
t := &testing.T{}
|
|
th := Setup(t).InitBasic()
|
|
|
|
s := httptest.NewServer(dummyWebsocketHandler())
|
|
|
|
th.Server.HubStart()
|
|
|
|
u1 := th.CreateUser()
|
|
u2 := th.CreateUser()
|
|
u3 := th.CreateUser()
|
|
|
|
t1 := th.CreateTeam()
|
|
t2 := th.CreateTeam()
|
|
|
|
ch1 := th.CreateDmChannel(u1)
|
|
ch2 := th.CreateChannel(t1)
|
|
ch3 := th.CreateChannel(t2)
|
|
|
|
th.LinkUserToTeam(u1, t1)
|
|
th.LinkUserToTeam(u1, t2)
|
|
th.LinkUserToTeam(u2, t1)
|
|
th.LinkUserToTeam(u2, t2)
|
|
th.LinkUserToTeam(u3, t1)
|
|
th.LinkUserToTeam(u3, t2)
|
|
|
|
th.AddUserToChannel(u1, ch2)
|
|
th.AddUserToChannel(u2, ch2)
|
|
th.AddUserToChannel(u3, ch2)
|
|
th.AddUserToChannel(u1, ch3)
|
|
th.AddUserToChannel(u2, ch3)
|
|
th.AddUserToChannel(u3, ch3)
|
|
|
|
sema := make(chan struct{}, 4) // A counting semaphore with concurrency of 4.
|
|
dataChan = make(chan []byte)
|
|
|
|
go func() {
|
|
for {
|
|
// get data
|
|
data, ok := <-dataChan
|
|
if !ok {
|
|
return
|
|
}
|
|
// acquire semaphore
|
|
sema <- struct{}{}
|
|
go func(data []byte) {
|
|
defer func() {
|
|
// release semaphore
|
|
<-sema
|
|
}()
|
|
var returnCode int
|
|
defer func() {
|
|
resChan <- returnCode
|
|
}()
|
|
// assign data randomly
|
|
// 3 users, 2 teams, 3 channels
|
|
input := getActionData(data,
|
|
[]string{u1.Id, u2.Id, u3.Id, ""},
|
|
[]string{t1.Id, t2.Id, ""},
|
|
[]string{ch1.Id, ch2.Id, ""})
|
|
if input == nil {
|
|
returnCode = 0
|
|
return
|
|
}
|
|
// We get the input from the random data.
|
|
// Now we perform some actions based on that.
|
|
|
|
conn := registerDummyWebConn(th.App, s.Listener.Addr(), input.createUserID)
|
|
defer func() {
|
|
conn.Close()
|
|
// A sleep of 2 seconds to allow other connections
|
|
// from the same user to be created, before unregistering them.
|
|
// This hits some additional code paths.
|
|
go func() {
|
|
time.Sleep(2 * time.Second)
|
|
th.App.HubUnregister(conn)
|
|
}()
|
|
}()
|
|
|
|
msg := model.NewWebSocketEvent(input.event,
|
|
input.selectTeamID,
|
|
input.selectChannelID,
|
|
input.createUserID, nil, "")
|
|
for k, v := range input.attachment {
|
|
msg.Add(k, v)
|
|
}
|
|
th.App.Publish(msg)
|
|
|
|
th.App.InvalidateWebConnSessionCacheForUser(input.invalidateConnUserID)
|
|
|
|
sessions, err := th.App.GetSessions(input.updateConnUserID)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if len(sessions) > 0 {
|
|
th.App.UpdateWebConnUserActivity(*sessions[0], model.GetMillis())
|
|
}
|
|
returnCode = 1
|
|
}(data)
|
|
}
|
|
}()
|
|
})
|
|
|
|
// send data to dataChan
|
|
dataChan <- data
|
|
|
|
// get data from res chan
|
|
result := <-resChan
|
|
return result
|
|
}
|
|
|
|
// generateInitialCorpus generates the corpus for go-fuzz.
|
|
// Place this function in any main.go file and run it.
|
|
// Use the generated directory as the corpus.
|
|
func generateInitialCorpus() error {
|
|
err := os.MkdirAll("workdir/corpus", 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := 0; i < 100; i++ {
|
|
data := make([]byte, 25)
|
|
_, err = rand.Read(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile("./workdir/corpus"+strconv.Itoa(i), data, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|