Skip to content

Commit

Permalink
MM-53023: Add origin client to ObserveAPIEndpointDuration (mattermost…
Browse files Browse the repository at this point in the history
…#23631)

* Add origin device to ObserveAPIEndpointDuration

* Fix generation of einterfaces mocks

* make einterfaces-mocks

* Use request's query and headers to get origin

* Add desktop to the origin device identification

* Test originDevice function

* Rename origin device to origin client
  • Loading branch information
agarciamontoro authored Oct 12, 2023
1 parent 1fe2295 commit 2bc9939
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 5 deletions.
45 changes: 44 additions & 1 deletion server/channels/web/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,11 +407,54 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

if r.URL.Path != model.APIURLSuffix+"/websocket" {
elapsed := float64(time.Since(now)) / float64(time.Second)
c.App.Metrics().ObserveAPIEndpointDuration(h.HandlerName, r.Method, statusCode, elapsed)
originClient := string(originClient(r))
c.App.Metrics().ObserveAPIEndpointDuration(h.HandlerName, r.Method, statusCode, originClient, elapsed)
}
}
}

type OriginClient string

const (
OriginClientUnknown OriginClient = "unknown"
OriginClientWeb OriginClient = "web"
OriginClientMobile OriginClient = "mobile"
OriginClientDesktop OriginClient = "desktop"
)

// originClient returns the device from which the provided request was issued. The algorithm roughly looks like:
// - If the URL contains the query mobilev2=true, then it's mobile
// - If the first field of the user agent starts with either "rnbeta" or "Mattermost", then it's mobile
// - If the last field of the user agent starts with "Mattermost", then it's desktop
// - Otherwise, it's web
func originClient(r *http.Request) OriginClient {
userAgent := r.Header.Get("User-Agent")
fields := strings.Fields(userAgent)
if len(fields) < 1 {
return OriginClientUnknown
}

// Is mobile post v2?
queryParam := r.URL.Query().Get("mobilev2")
if queryParam == "true" {
return OriginClientMobile
}

// Is mobile pre v2?
clientAgent := fields[0]
if strings.HasPrefix(clientAgent, "rnbeta") || strings.HasPrefix(clientAgent, "Mattermost") {
return OriginClientMobile
}

// Is desktop?
if strings.HasPrefix(fields[len(fields)-1], "Mattermost") {
return OriginClientDesktop
}

// Default to web
return OriginClientWeb
}

// checkCSRFToken performs a CSRF check on the provided request with the given CSRF token. Returns whether or not
// a CSRF check occurred and whether or not it succeeded.
func (h *Handler) checkCSRFToken(c *Context, r *http.Request, token string, tokenLocation app.TokenLocation, session *model.Session) (checked bool, passed bool) {
Expand Down
68 changes: 68 additions & 0 deletions server/channels/web/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -882,3 +882,71 @@ func TestCheckCSRFToken(t *testing.T) {
assert.Nil(t, c.Err)
})
}

func TestOriginClient(t *testing.T) {
testCases := []struct {
name string
userAgent string
mobilev2 bool
expectedClient OriginClient
}{
{
name: "No user agent - unknown client",
userAgent: "",
expectedClient: OriginClientUnknown,
},
{
name: "Mozilla user agent",
userAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0",
expectedClient: OriginClientWeb,
},
{
name: "Chrome user agent",
userAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
expectedClient: OriginClientWeb,
},
{
name: "Mobile post v2",
userAgent: "someother-agent/3.2.4",
mobilev2: true,
expectedClient: OriginClientMobile,
},
{
name: "Mobile Android",
userAgent: "rnbeta/2.0.0.441 someother-agent/3.2.4",
expectedClient: OriginClientMobile,
},
{
name: "Mobile iOS",
userAgent: "Mattermost/2.0.0.441 someother-agent/3.2.4",
expectedClient: OriginClientMobile,
},
{
name: "Desktop user agent",
userAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5481.177 Electron/23.1.2 Safari/537.36 Mattermost/5.3.1",
expectedClient: OriginClientDesktop,
},
}

for _, tc := range testCases {
req, err := http.NewRequest(http.MethodGet, "example.com", nil)
require.NoError(t, err)

// Set User-Agent header, if any
if tc.userAgent != "" {
req.Header.Set("User-Agent", tc.userAgent)
}

// Set mobilev2 query if needed
if tc.mobilev2 {
q := req.URL.Query()
q.Add("mobilev2", "true")
req.URL.RawQuery = q.Encode()
}

// Compute origin client
actualClient := originClient(req)

require.Equal(t, tc.expectedClient, actualClient)
}
}
2 changes: 1 addition & 1 deletion server/einterfaces/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ type MetricsInterface interface {
IncrementFilesSearchCounter()
ObserveFilesSearchDuration(elapsed float64)
ObserveStoreMethodDuration(method, success string, elapsed float64)
ObserveAPIEndpointDuration(endpoint, method, statusCode string, elapsed float64)
ObserveAPIEndpointDuration(endpoint, method, statusCode, originClient string, elapsed float64)
IncrementPostIndexCounter()
IncrementFileIndexCounter()
IncrementUserIndexCounter()
Expand Down
6 changes: 3 additions & 3 deletions server/einterfaces/mocks/MetricsInterface.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2bc9939

Please sign in to comment.