Files
mattermost/app/webhub_fuzz.go
Kyriakos Z c11ad8995f Adds OmitConnection parameter to broadcast (#20723)
* 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>
2022-09-02 13:17:22 +03:00

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
}