github.com/haalcala/mattermost-server-change-repo/v5@v5.33.2/app/webhub_fuzz.go (about)

     1  // +build gofuzz
     2  
     3  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     4  // See LICENSE.txt for license information.
     5  package app
     6  
     7  import (
     8  	"io/ioutil"
     9  	"math/rand"
    10  	"net"
    11  	"net/http"
    12  	"net/http/httptest"
    13  	"os"
    14  	"strconv"
    15  	"sync"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/gorilla/websocket"
    20  	goi18n "github.com/mattermost/go-i18n/i18n"
    21  
    22  	"github.com/mattermost/mattermost-server/v5/model"
    23  	"github.com/mattermost/mattermost-server/v5/testlib"
    24  )
    25  
    26  // This is a file used to fuzz test the web_hub code.
    27  // It performs a high-level fuzzing of the web_hub by spawning a hub
    28  // and creating connections to it with a fixed concurrency.
    29  //
    30  // During the fuzz test, we create the server just once, and we send
    31  // the random byte slice through a channel and perform some actions depending
    32  // on the random data.
    33  // The actions are decided in the getActionData function which decides
    34  // which user, team, channel should the message go to and some other stuff too.
    35  //
    36  // Since this requires help of the testing library, we have to duplicate some code
    37  // over here because go-fuzz cannot take code from _test.go files. It won't affect
    38  // the main build because it's behind a build tag.
    39  //
    40  // To run this:
    41  // 1. go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
    42  // 2. mv app/helper_test.go app/helper.go
    43  // (Also reduce the number of push notification workers to 1 to debug stack traces easily.)
    44  // 3. go-fuzz-build github.com/mattermost/mattermost-server/v5/app
    45  // 4. Generate a corpus dir. It's just a directory with files containing random data
    46  // for go-fuzz to use as an initial seed. Use the generateInitialCorpus function for that.
    47  // 5. go-fuzz -bin=app-fuzz.zip -workdir=./workdir
    48  var mainHelper *testlib.MainHelper
    49  
    50  func init() {
    51  	testing.Init()
    52  	var options = testlib.HelperOptions{
    53  		EnableStore:     true,
    54  		EnableResources: true,
    55  	}
    56  
    57  	mainHelper = testlib.NewMainHelperWithOptions(&options)
    58  }
    59  
    60  func dummyWebsocketHandler() http.HandlerFunc {
    61  	return func(w http.ResponseWriter, req *http.Request) {
    62  		upgrader := &websocket.Upgrader{
    63  			ReadBufferSize:  1024,
    64  			WriteBufferSize: 1024,
    65  		}
    66  		conn, err := upgrader.Upgrade(w, req, nil)
    67  		for err == nil {
    68  			_, _, err = conn.ReadMessage()
    69  		}
    70  		if _, ok := err.(*websocket.CloseError); !ok {
    71  			panic(err)
    72  		}
    73  	}
    74  }
    75  
    76  func registerDummyWebConn(a *App, addr net.Addr, userID string) *WebConn {
    77  	session, appErr := a.CreateSession(&model.Session{
    78  		UserId: userID,
    79  	})
    80  	if appErr != nil {
    81  		panic(appErr)
    82  	}
    83  
    84  	d := websocket.Dialer{}
    85  	c, _, err := d.Dial("ws://"+addr.String()+"/ws", nil)
    86  	if err != nil {
    87  		panic(err)
    88  	}
    89  
    90  	wc := a.NewWebConn(c, *session, goi18n.IdentityTfunc(), "en")
    91  	a.HubRegister(wc)
    92  	go wc.Pump()
    93  	return wc
    94  }
    95  
    96  type actionData struct {
    97  	event                string
    98  	createUserID         string
    99  	selectChannelID      string
   100  	selectTeamID         string
   101  	invalidateConnUserID string
   102  	updateConnUserID     string
   103  	attachment           map[string]interface{}
   104  }
   105  
   106  func getActionData(data []byte, userIDs, teamIDs, channelIDs []string) *actionData {
   107  	// Some sample events
   108  	events := []string{
   109  		model.WEBSOCKET_EVENT_CHANNEL_CREATED,
   110  		model.WEBSOCKET_EVENT_CHANNEL_DELETED,
   111  		model.WEBSOCKET_EVENT_USER_ADDED,
   112  		model.WEBSOCKET_EVENT_USER_UPDATED,
   113  		model.WEBSOCKET_EVENT_STATUS_CHANGE,
   114  		model.WEBSOCKET_EVENT_HELLO,
   115  		model.WEBSOCKET_AUTHENTICATION_CHALLENGE,
   116  		model.WEBSOCKET_EVENT_REACTION_ADDED,
   117  		model.WEBSOCKET_EVENT_REACTION_REMOVED,
   118  		model.WEBSOCKET_EVENT_RESPONSE,
   119  	}
   120  	// We need atleast 10 bytes to get all the data we need
   121  	if len(data) < 10 {
   122  		return nil
   123  	}
   124  	input := &actionData{}
   125  	//	Assign userID, channelID, teamID randomly from respective byte indices
   126  	input.createUserID = userIDs[int(data[0])%len(userIDs)]
   127  	input.selectChannelID = channelIDs[int(data[1])%len(channelIDs)]
   128  	input.selectTeamID = teamIDs[int(data[2])%len(teamIDs)]
   129  	input.invalidateConnUserID = userIDs[int(data[3])%len(userIDs)]
   130  	input.updateConnUserID = userIDs[int(data[4])%len(userIDs)]
   131  	input.event = events[int(data[5])%len(events)]
   132  	data = data[6:]
   133  	input.attachment = make(map[string]interface{})
   134  	for len(data) >= 4 { // 2 bytes key, 2 bytes value
   135  		k := data[:2]
   136  		v := data[2:4]
   137  		input.attachment[string(k)] = v
   138  		data = data[4:]
   139  	}
   140  
   141  	return input
   142  }
   143  
   144  var startServerOnce sync.Once
   145  var dataChan chan []byte
   146  var resChan = make(chan int, 4) // buffer of 4 to keep reading results.
   147  
   148  func Fuzz(data []byte) int {
   149  	// We don't want to close anything down as the fuzzer will keep on running forever.
   150  	startServerOnce.Do(func() {
   151  		t := &testing.T{}
   152  		th := Setup(t).InitBasic()
   153  
   154  		s := httptest.NewServer(dummyWebsocketHandler())
   155  
   156  		th.App.HubStart()
   157  
   158  		u1 := th.CreateUser()
   159  		u2 := th.CreateUser()
   160  		u3 := th.CreateUser()
   161  
   162  		t1 := th.CreateTeam()
   163  		t2 := th.CreateTeam()
   164  
   165  		ch1 := th.CreateDmChannel(u1)
   166  		ch2 := th.CreateChannel(t1)
   167  		ch3 := th.CreateChannel(t2)
   168  
   169  		th.LinkUserToTeam(u1, t1)
   170  		th.LinkUserToTeam(u1, t2)
   171  		th.LinkUserToTeam(u2, t1)
   172  		th.LinkUserToTeam(u2, t2)
   173  		th.LinkUserToTeam(u3, t1)
   174  		th.LinkUserToTeam(u3, t2)
   175  
   176  		th.AddUserToChannel(u1, ch2)
   177  		th.AddUserToChannel(u2, ch2)
   178  		th.AddUserToChannel(u3, ch2)
   179  		th.AddUserToChannel(u1, ch3)
   180  		th.AddUserToChannel(u2, ch3)
   181  		th.AddUserToChannel(u3, ch3)
   182  
   183  		sema := make(chan struct{}, 4) // A counting semaphore with concurrency of 4.
   184  		dataChan = make(chan []byte)
   185  
   186  		go func() {
   187  			for {
   188  				// get data
   189  				data, ok := <-dataChan
   190  				if !ok {
   191  					return
   192  				}
   193  				// acquire semaphore
   194  				sema <- struct{}{}
   195  				go func(data []byte) {
   196  					defer func() {
   197  						// release semaphore
   198  						<-sema
   199  					}()
   200  					var returnCode int
   201  					defer func() {
   202  						resChan <- returnCode
   203  					}()
   204  					// assign data randomly
   205  					// 3 users, 2 teams, 3 channels
   206  					input := getActionData(data,
   207  						[]string{u1.Id, u2.Id, u3.Id, ""},
   208  						[]string{t1.Id, t2.Id, ""},
   209  						[]string{ch1.Id, ch2.Id, ""})
   210  					if input == nil {
   211  						returnCode = 0
   212  						return
   213  					}
   214  					// We get the input from the random data.
   215  					// Now we perform some actions based on that.
   216  
   217  					conn := registerDummyWebConn(th.App, s.Listener.Addr(), input.createUserID)
   218  					defer func() {
   219  						conn.Close()
   220  						// A sleep of 2 seconds to allow other connections
   221  						// from the same user to be created, before unregistering them.
   222  						// This hits some additional code paths.
   223  						go func() {
   224  							time.Sleep(2 * time.Second)
   225  							th.App.HubUnregister(conn)
   226  						}()
   227  					}()
   228  
   229  					msg := model.NewWebSocketEvent(input.event,
   230  						input.selectTeamID,
   231  						input.selectChannelID,
   232  						input.createUserID, nil)
   233  					for k, v := range input.attachment {
   234  						msg.Add(k, v)
   235  					}
   236  					th.App.Publish(msg)
   237  
   238  					th.App.InvalidateWebConnSessionCacheForUser(input.invalidateConnUserID)
   239  
   240  					sessions, err := th.App.GetSessions(input.updateConnUserID)
   241  					if err != nil {
   242  						panic(err)
   243  					}
   244  					if len(sessions) > 0 {
   245  						th.App.UpdateWebConnUserActivity(*sessions[0], model.GetMillis())
   246  					}
   247  					returnCode = 1
   248  				}(data)
   249  			}
   250  		}()
   251  	})
   252  
   253  	// send data to dataChan
   254  	dataChan <- data
   255  
   256  	// get data from res chan
   257  	result := <-resChan
   258  	return result
   259  }
   260  
   261  // generateInitialCorpus generates the corpus for go-fuzz.
   262  // Place this function in any main.go file and run it.
   263  // Use the generated directory as the corpus.
   264  func generateInitialCorpus() error {
   265  	err := os.MkdirAll("workdir/corpus", 0755)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	for i := 0; i < 100; i++ {
   270  		data := make([]byte, 25)
   271  		_, err = rand.Read(data)
   272  		if err != nil {
   273  			return err
   274  		}
   275  		err = ioutil.WriteFile("./workdir/corpus"+strconv.Itoa(i), data, 0644)
   276  		if err != nil {
   277  			return err
   278  		}
   279  	}
   280  	return nil
   281  }