1// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2// See LICENSE.txt for license information.
3
4package web
5
6import (
7	"bytes"
8	"context"
9	"fmt"
10	"net/http"
11	"reflect"
12	"runtime"
13	"strconv"
14	"strings"
15	"time"
16
17	"github.com/mattermost/gziphandler"
18	"github.com/opentracing/opentracing-go"
19	"github.com/opentracing/opentracing-go/ext"
20	spanlog "github.com/opentracing/opentracing-go/log"
21
22	"github.com/mattermost/mattermost-server/v6/app"
23	app_opentracing "github.com/mattermost/mattermost-server/v6/app/opentracing"
24	"github.com/mattermost/mattermost-server/v6/app/request"
25	"github.com/mattermost/mattermost-server/v6/model"
26	"github.com/mattermost/mattermost-server/v6/services/tracing"
27	"github.com/mattermost/mattermost-server/v6/shared/i18n"
28	"github.com/mattermost/mattermost-server/v6/shared/mlog"
29	"github.com/mattermost/mattermost-server/v6/store/opentracinglayer"
30	"github.com/mattermost/mattermost-server/v6/utils"
31)
32
33func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
34	handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
35	pos := strings.LastIndex(handlerName, ".")
36	if pos != -1 && len(handlerName) > pos {
37		handlerName = handlerName[pos+1:]
38	}
39	return handlerName
40}
41
42func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
43	return &Handler{
44		App:            w.app,
45		HandleFunc:     h,
46		HandlerName:    GetHandlerName(h),
47		RequireSession: false,
48		TrustRequester: false,
49		RequireMfa:     false,
50		IsStatic:       false,
51		IsLocal:        false,
52	}
53}
54
55func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
56	// Determine the CSP SHA directive needed for subpath support, if any. This value is fixed
57	// on server start and intentionally requires a restart to take effect.
58	subpath, _ := utils.GetSubpathFromConfig(w.app.Config())
59
60	return &Handler{
61		App:            w.app,
62		HandleFunc:     h,
63		HandlerName:    GetHandlerName(h),
64		RequireSession: false,
65		TrustRequester: false,
66		RequireMfa:     false,
67		IsStatic:       true,
68
69		cspShaDirective: utils.GetSubpathScriptHash(subpath),
70	}
71}
72
73type Handler struct {
74	App                       app.AppIface
75	HandleFunc                func(*Context, http.ResponseWriter, *http.Request)
76	HandlerName               string
77	RequireSession            bool
78	RequireCloudKey           bool
79	RequireRemoteClusterToken bool
80	TrustRequester            bool
81	RequireMfa                bool
82	IsStatic                  bool
83	IsLocal                   bool
84	DisableWhenBusy           bool
85
86	cspShaDirective string
87}
88
89func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
90	w = newWrappedWriter(w)
91	now := time.Now()
92
93	requestID := model.NewId()
94	var statusCode string
95	defer func() {
96		responseLogFields := []mlog.Field{
97			mlog.String("method", r.Method),
98			mlog.String("url", r.URL.Path),
99			mlog.String("request_id", requestID),
100			mlog.String("host", r.Host),
101			mlog.String("scheme", r.Header.Get(model.HeaderForwardedProto)),
102		}
103		// Websockets are returning status code 0 to requests after closing the socket
104		if statusCode != "0" {
105			responseLogFields = append(responseLogFields, mlog.String("status_code", statusCode))
106		}
107		mlog.Debug("Received HTTP request", responseLogFields...)
108	}()
109
110	c := &Context{
111		AppContext: &request.Context{},
112		App:        h.App,
113	}
114
115	t, _ := i18n.GetTranslationsAndLocaleFromRequest(r)
116	c.AppContext.SetT(t)
117	c.AppContext.SetRequestId(requestID)
118	c.AppContext.SetIPAddress(utils.GetIPAddress(r, c.App.Config().ServiceSettings.TrustedProxyIPHeader))
119	c.AppContext.SetUserAgent(r.UserAgent())
120	c.AppContext.SetAcceptLanguage(r.Header.Get("Accept-Language"))
121	c.AppContext.SetPath(r.URL.Path)
122	c.Params = ParamsFromRequest(r)
123	c.Logger = c.App.Log()
124
125	if *c.App.Config().ServiceSettings.EnableOpenTracing {
126		span, ctx := tracing.StartRootSpanByContext(context.Background(), "web:ServeHTTP")
127		carrier := opentracing.HTTPHeadersCarrier(r.Header)
128		_ = opentracing.GlobalTracer().Inject(span.Context(), opentracing.HTTPHeaders, carrier)
129		ext.HTTPMethod.Set(span, r.Method)
130		ext.HTTPUrl.Set(span, c.AppContext.Path())
131		ext.PeerAddress.Set(span, c.AppContext.IPAddress())
132		span.SetTag("request_id", c.AppContext.RequestId())
133		span.SetTag("user_agent", c.AppContext.UserAgent())
134
135		defer func() {
136			if c.Err != nil {
137				span.LogFields(spanlog.Error(c.Err))
138				ext.HTTPStatusCode.Set(span, uint16(c.Err.StatusCode))
139				ext.Error.Set(span, true)
140			}
141			span.Finish()
142		}()
143		c.AppContext.SetContext(ctx)
144
145		tmpSrv := *c.App.Srv()
146		tmpSrv.Store = opentracinglayer.New(c.App.Srv().Store, ctx)
147		c.App.SetServer(&tmpSrv)
148		c.App = app_opentracing.NewOpenTracingAppLayer(c.App, ctx)
149	}
150
151	// Set the max request body size to be equal to MaxFileSize.
152	// Ideally, non-file request bodies should be smaller than file request bodies,
153	// but we don't have a clean way to identify all file upload handlers.
154	// So to keep it simple, we clamp it to the max file size.
155	// We add a buffer of bytes.MinRead so that file sizes close to max file size
156	// do not get cut off.
157	r.Body = http.MaxBytesReader(w, r.Body, *c.App.Config().FileSettings.MaxFileSize+bytes.MinRead)
158
159	siteURLHeader := *c.App.Config().ServiceSettings.SiteURL
160	c.SetSiteURLHeader(siteURLHeader)
161
162	w.Header().Set(model.HeaderRequestId, c.AppContext.RequestId())
163	w.Header().Set(model.HeaderVersionId, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.Srv().License() != nil))
164
165	if *c.App.Config().ServiceSettings.TLSStrictTransport {
166		w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", *c.App.Config().ServiceSettings.TLSStrictTransportMaxAge))
167	}
168
169	cloudCSP := ""
170	if c.App.Srv().License() != nil && *c.App.Srv().License().Features.Cloud {
171		cloudCSP = " js.stripe.com/v3"
172	}
173
174	if h.IsStatic {
175		// Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking
176		w.Header().Set("X-Frame-Options", "SAMEORIGIN")
177
178		// Add unsafe-eval to the content security policy for faster source maps in development mode
179		devCSP := ""
180		if model.BuildNumber == "dev" {
181			devCSP += " 'unsafe-eval'"
182		}
183
184		// Add unsafe-inline to unlock extensions like React & Redux DevTools in Firefox
185		// see https://github.com/reduxjs/redux-devtools/issues/380
186		if model.BuildNumber == "dev" {
187			devCSP += " 'unsafe-inline'"
188		}
189
190		// Set content security policy. This is also specified in the root.html of the webapp in a meta tag.
191		w.Header().Set("Content-Security-Policy", fmt.Sprintf(
192			"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com%s%s%s",
193			cloudCSP,
194			h.cspShaDirective,
195			devCSP,
196		))
197	} else {
198		// All api response bodies will be JSON formatted by default
199		w.Header().Set("Content-Type", "application/json")
200
201		if r.Method == "GET" {
202			w.Header().Set("Expires", "0")
203		}
204	}
205
206	token, tokenLocation := app.ParseAuthTokenFromRequest(r)
207
208	if token != "" && tokenLocation != app.TokenLocationCloudHeader && tokenLocation != app.TokenLocationRemoteClusterHeader {
209		session, err := c.App.GetSession(token)
210		defer c.App.ReturnSessionToPool(session)
211
212		if err != nil {
213			c.Logger.Info("Invalid session", mlog.Err(err))
214			if err.StatusCode == http.StatusInternalServerError {
215				c.Err = err
216			} else if h.RequireSession {
217				c.RemoveSessionCookie(w, r)
218				c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
219			}
220		} else if !session.IsOAuth && tokenLocation == app.TokenLocationQueryString {
221			c.Err = model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
222		} else {
223			c.AppContext.SetSession(session)
224		}
225
226		// Rate limit by UserID
227		if c.App.Srv().RateLimiter != nil && c.App.Srv().RateLimiter.UserIdRateLimit(c.AppContext.Session().UserId, w) {
228			return
229		}
230
231		h.checkCSRFToken(c, r, token, tokenLocation, session)
232	} else if token != "" && c.App.Srv().License() != nil && *c.App.Srv().License().Features.Cloud && tokenLocation == app.TokenLocationCloudHeader {
233		// Check to see if this provided token matches our CWS Token
234		session, err := c.App.GetCloudSession(token)
235		if err != nil {
236			c.Logger.Warn("Invalid CWS token", mlog.Err(err))
237			c.Err = err
238		} else {
239			c.AppContext.SetSession(session)
240		}
241	} else if token != "" && c.App.Srv().License() != nil && *c.App.Srv().License().Features.RemoteClusterService && tokenLocation == app.TokenLocationRemoteClusterHeader {
242		// Get the remote cluster
243		if remoteId := c.GetRemoteID(r); remoteId == "" {
244			c.Logger.Warn("Missing remote cluster id") //
245			c.Err = model.NewAppError("ServeHTTP", "api.context.remote_id_missing.app_error", nil, "", http.StatusUnauthorized)
246		} else {
247			// Check the token is correct for the remote cluster id.
248			session, err := c.App.GetRemoteClusterSession(token, remoteId)
249			if err != nil {
250				c.Logger.Warn("Invalid remote cluster token", mlog.Err(err))
251				c.Err = err
252			} else {
253				c.AppContext.SetSession(session)
254			}
255		}
256	}
257
258	c.Logger = c.App.Log().With(
259		mlog.String("path", c.AppContext.Path()),
260		mlog.String("request_id", c.AppContext.RequestId()),
261		mlog.String("ip_addr", c.AppContext.IPAddress()),
262		mlog.String("user_id", c.AppContext.Session().UserId),
263		mlog.String("method", r.Method),
264	)
265
266	if c.Err == nil && h.RequireSession {
267		c.SessionRequired()
268	}
269
270	if c.Err == nil && h.RequireMfa {
271		c.MfaRequired()
272	}
273
274	if c.Err == nil && h.DisableWhenBusy && c.App.Srv().Busy.IsBusy() {
275		c.SetServerBusyError()
276	}
277
278	if c.Err == nil && h.RequireCloudKey {
279		c.CloudKeyRequired()
280	}
281
282	if c.Err == nil && h.RequireRemoteClusterToken {
283		c.RemoteClusterTokenRequired()
284	}
285
286	if c.Err == nil && h.IsLocal {
287		// if the connection is local, RemoteAddr shouldn't have the
288		// shape IP:PORT (it will be "@" in Linux, for example)
289		isLocalOrigin := !strings.Contains(r.RemoteAddr, ":")
290		if *c.App.Config().ServiceSettings.EnableLocalMode && isLocalOrigin {
291			c.AppContext.SetSession(&model.Session{Local: true})
292		} else if !isLocalOrigin {
293			c.Err = model.NewAppError("", "api.context.local_origin_required.app_error", nil, "LocalOriginRequired", http.StatusUnauthorized)
294		}
295	}
296
297	if c.Err == nil {
298		h.HandleFunc(c, w, r)
299	}
300
301	// Handle errors that have occurred
302	if c.Err != nil {
303		c.Err.Translate(c.AppContext.T)
304		c.Err.RequestId = c.AppContext.RequestId()
305		c.LogErrorByCode(c.Err)
306
307		c.Err.Where = r.URL.Path
308
309		// Block out detailed error when not in developer mode
310		if !*c.App.Config().ServiceSettings.EnableDeveloper {
311			c.Err.DetailedError = ""
312		}
313
314		// Sanitize all 5xx error messages in hardened mode
315		if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode && c.Err.StatusCode >= 500 {
316			c.Err.Id = ""
317			c.Err.Message = "Internal Server Error"
318			c.Err.DetailedError = ""
319			c.Err.StatusCode = 500
320			c.Err.Where = ""
321			c.Err.IsOAuth = false
322		}
323
324		if IsAPICall(c.App, r) || IsWebhookCall(c.App, r) || IsOAuthAPICall(c.App, r) || r.Header.Get("X-Mobile-App") != "" {
325			w.WriteHeader(c.Err.StatusCode)
326			w.Write([]byte(c.Err.ToJSON()))
327		} else {
328			utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
329		}
330
331		if c.App.Metrics() != nil {
332			c.App.Metrics().IncrementHTTPError()
333		}
334	}
335
336	statusCode = strconv.Itoa(w.(*responseWriterWrapper).StatusCode())
337	if c.App.Metrics() != nil {
338		c.App.Metrics().IncrementHTTPRequest()
339
340		if r.URL.Path != model.APIURLSuffix+"/websocket" {
341			elapsed := float64(time.Since(now)) / float64(time.Second)
342			c.App.Metrics().ObserveAPIEndpointDuration(h.HandlerName, r.Method, statusCode, elapsed)
343		}
344	}
345}
346
347// checkCSRFToken performs a CSRF check on the provided request with the given CSRF token. Returns whether or not
348// a CSRF check occurred and whether or not it succeeded.
349func (h *Handler) checkCSRFToken(c *Context, r *http.Request, token string, tokenLocation app.TokenLocation, session *model.Session) (checked bool, passed bool) {
350	csrfCheckNeeded := session != nil && c.Err == nil && tokenLocation == app.TokenLocationCookie && !h.TrustRequester && r.Method != "GET"
351	csrfCheckPassed := false
352
353	if csrfCheckNeeded {
354		csrfHeader := r.Header.Get(model.HeaderCsrfToken)
355
356		if csrfHeader == session.GetCSRF() {
357			csrfCheckPassed = true
358		} else if r.Header.Get(model.HeaderRequestedWith) == model.HeaderRequestedWithXML {
359			// ToDo(DSchalla) 2019/01/04: Remove after deprecation period and only allow CSRF Header (MM-13657)
360			csrfErrorMessage := "CSRF Header check failed for request - Please upgrade your web application or custom app to set a CSRF Header"
361
362			sid := ""
363			userId := ""
364
365			if session != nil {
366				sid = session.Id
367				userId = session.UserId
368			}
369
370			fields := []mlog.Field{
371				mlog.String("path", r.URL.Path),
372				mlog.String("ip", r.RemoteAddr),
373				mlog.String("session_id", sid),
374				mlog.String("user_id", userId),
375			}
376
377			if *c.App.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement {
378				c.Logger.Warn(csrfErrorMessage, fields...)
379			} else {
380				c.Logger.Debug(csrfErrorMessage, fields...)
381				csrfCheckPassed = true
382			}
383		}
384
385		if !csrfCheckPassed {
386			c.AppContext.SetSession(&model.Session{})
387			c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
388		}
389	}
390
391	return csrfCheckNeeded, csrfCheckPassed
392}
393
394// APIHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
395// granted.
396func (w *Web) APIHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
397	handler := &Handler{
398		App:            w.app,
399		HandleFunc:     h,
400		HandlerName:    GetHandlerName(h),
401		RequireSession: false,
402		TrustRequester: false,
403		RequireMfa:     false,
404		IsStatic:       false,
405		IsLocal:        false,
406	}
407	if *w.app.Config().ServiceSettings.WebserverMode == "gzip" {
408		return gziphandler.GzipHandler(handler)
409	}
410	return handler
411}
412
413// APIHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
414// allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
415// websocket.
416func (w *Web) APIHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
417	handler := &Handler{
418		App:            w.app,
419		HandleFunc:     h,
420		HandlerName:    GetHandlerName(h),
421		RequireSession: false,
422		TrustRequester: true,
423		RequireMfa:     false,
424		IsStatic:       false,
425		IsLocal:        false,
426	}
427	if *w.app.Config().ServiceSettings.WebserverMode == "gzip" {
428		return gziphandler.GzipHandler(handler)
429	}
430	return handler
431}
432
433// APISessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
434// be granted.
435func (w *Web) APISessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
436	handler := &Handler{
437		App:            w.app,
438		HandleFunc:     h,
439		HandlerName:    GetHandlerName(h),
440		RequireSession: true,
441		TrustRequester: false,
442		RequireMfa:     true,
443		IsStatic:       false,
444		IsLocal:        false,
445	}
446	if *w.app.Config().ServiceSettings.WebserverMode == "gzip" {
447		return gziphandler.GzipHandler(handler)
448	}
449	return handler
450}
451