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