Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
281 lines
7.4 KiB
Go
281 lines
7.4 KiB
Go
//go: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/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/i18n"
|
|
"github.com/mattermost/mattermost/server/v8/channels/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/v8/channels/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
|
|
}
|