github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/web/handlers.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package web 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "net/http" 11 "reflect" 12 "runtime" 13 "strings" 14 "time" 15 16 "github.com/NYTimes/gziphandler" 17 "github.com/opentracing/opentracing-go" 18 "github.com/opentracing/opentracing-go/ext" 19 spanlog "github.com/opentracing/opentracing-go/log" 20 21 "github.com/vnforks/kid/v5/app" 22 "github.com/vnforks/kid/v5/mlog" 23 "github.com/vnforks/kid/v5/model" 24 "github.com/vnforks/kid/v5/services/tracing" 25 "github.com/vnforks/kid/v5/store" 26 "github.com/vnforks/kid/v5/utils" 27 ) 28 29 func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string { 30 handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name() 31 pos := strings.LastIndex(handlerName, ".") 32 if pos != -1 && len(handlerName) > pos { 33 handlerName = handlerName[pos+1:] 34 } 35 return handlerName 36 } 37 38 func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { 39 return &Handler{ 40 GetGlobalAppOptions: w.GetGlobalAppOptions, 41 HandleFunc: h, 42 HandlerName: GetHandlerName(h), 43 RequireSession: false, 44 TrustRequester: false, 45 RequireMfa: false, 46 IsStatic: false, 47 } 48 } 49 50 func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { 51 // Determine the CSP SHA directive needed for subpath support, if any. This value is fixed 52 // on server start and intentionally requires a restart to take effect. 53 subpath, _ := utils.GetSubpathFromConfig(w.ConfigService.Config()) 54 55 return &Handler{ 56 GetGlobalAppOptions: w.GetGlobalAppOptions, 57 HandleFunc: h, 58 HandlerName: GetHandlerName(h), 59 RequireSession: false, 60 TrustRequester: false, 61 RequireMfa: false, 62 IsStatic: true, 63 64 cspShaDirective: utils.GetSubpathScriptHash(subpath), 65 } 66 } 67 68 type Handler struct { 69 GetGlobalAppOptions app.AppOptionCreator 70 HandleFunc func(*Context, http.ResponseWriter, *http.Request) 71 HandlerName string 72 RequireSession bool 73 TrustRequester bool 74 RequireMfa bool 75 IsStatic bool 76 DisableWhenBusy bool 77 78 cspShaDirective string 79 } 80 81 func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 82 now := time.Now() 83 84 requestID := model.NewId() 85 mlog.Debug("Received HTTP request", mlog.String("method", r.Method), mlog.String("url", r.URL.Path), mlog.String("request_id", requestID)) 86 87 c := &Context{} 88 c.App = app.New( 89 h.GetGlobalAppOptions()..., 90 ) 91 92 t, _ := utils.GetTranslationsAndLocale(w, r) 93 c.App.SetT(t) 94 c.App.SetRequestId(requestID) 95 c.App.SetIpAddress(utils.GetIpAddress(r, c.App.Config().ServiceSettings.TrustedProxyIPHeader)) 96 c.App.SetUserAgent(r.UserAgent()) 97 c.App.SetAcceptLanguage(r.Header.Get("Accept-Language")) 98 c.App.SetPath(r.URL.Path) 99 c.Params = ParamsFromRequest(r) 100 c.Log = c.App.Log() 101 102 if *c.App.Config().ServiceSettings.EnableOpenTracing { 103 span, ctx := tracing.StartRootSpanByContext(context.Background(), "web:ServeHTTP") 104 carrier := opentracing.HTTPHeadersCarrier(r.Header) 105 _ = opentracing.GlobalTracer().Inject(span.Context(), opentracing.HTTPHeaders, carrier) 106 ext.HTTPMethod.Set(span, r.Method) 107 ext.HTTPUrl.Set(span, c.App.Path()) 108 ext.PeerAddress.Set(span, c.App.IpAddress()) 109 span.SetTag("request_id", c.App.RequestId()) 110 span.SetTag("user_agent", c.App.UserAgent()) 111 112 defer func() { 113 if c.Err != nil { 114 span.LogFields(spanlog.Error(c.Err)) 115 ext.HTTPStatusCode.Set(span, uint16(c.Err.StatusCode)) 116 ext.Error.Set(span, true) 117 } 118 span.Finish() 119 }() 120 c.App.SetContext(ctx) 121 122 tmpSrv := app.Server{} 123 tmpSrv = *c.App.Srv() 124 tmpSrv.Store = store.NewOpenTracingLayer(c.App.Srv().Store, ctx) 125 c.App.SetServer(&tmpSrv) 126 c.App = app.NewOpenTracingAppLayer(c.App, ctx) 127 } 128 129 // Set the max request body size to be equal to MaxFileSize. 130 // Ideally, non-file request bodies should be smaller than file request bodies, 131 // but we don't have a clean way to identify all file upload handlers. 132 // So to keep it simple, we clamp it to the max file size. 133 // We add a buffer of bytes.MinRead so that file sizes close to max file size 134 // do not get cut off. 135 r.Body = http.MaxBytesReader(w, r.Body, *c.App.Config().FileSettings.MaxFileSize+bytes.MinRead) 136 137 subpath, _ := utils.GetSubpathFromConfig(c.App.Config()) 138 siteURLHeader := app.GetProtocol(r) + "://" + r.Host + subpath 139 c.SetSiteURLHeader(siteURLHeader) 140 141 w.Header().Set(model.HEADER_REQUEST_ID, c.App.RequestId()) 142 w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.License() != nil)) 143 144 if *c.App.Config().ServiceSettings.TLSStrictTransport { 145 w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", *c.App.Config().ServiceSettings.TLSStrictTransportMaxAge)) 146 } 147 148 if h.IsStatic { 149 // Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking 150 w.Header().Set("X-Frame-Options", "SAMEORIGIN") 151 // Set content security policy. This is also specified in the root.html of the webapp in a meta tag. 152 w.Header().Set("Content-Security-Policy", fmt.Sprintf( 153 "frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/%s", 154 h.cspShaDirective, 155 )) 156 } else { 157 // All api response bodies will be JSON formatted by default 158 w.Header().Set("Content-Type", "application/json") 159 160 if r.Method == "GET" { 161 w.Header().Set("Expires", "0") 162 } 163 } 164 165 token, tokenLocation := app.ParseAuthTokenFromRequest(r) 166 167 if len(token) != 0 { 168 session, err := c.App.GetSession(token) 169 if err != nil { 170 c.Log.Info("Invalid session", mlog.Err(err)) 171 if err.StatusCode == http.StatusInternalServerError { 172 c.Err = err 173 } else if h.RequireSession { 174 c.RemoveSessionCookie(w, r) 175 c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized) 176 } 177 } else if !session.IsOAuth && tokenLocation == app.TokenLocationQueryString { 178 c.Err = model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized) 179 } else { 180 c.App.SetSession(session) 181 } 182 183 // Rate limit by UserID 184 if c.App.Srv().RateLimiter != nil && c.App.Srv().RateLimiter.UserIdRateLimit(c.App.Session().UserId, w) { 185 return 186 } 187 188 h.checkCSRFToken(c, r, token, tokenLocation, session) 189 } 190 191 c.Log = c.App.Log().With( 192 mlog.String("path", c.App.Path()), 193 mlog.String("request_id", c.App.RequestId()), 194 mlog.String("ip_addr", c.App.IpAddress()), 195 mlog.String("user_id", c.App.Session().UserId), 196 mlog.String("method", r.Method), 197 ) 198 199 if c.Err == nil && h.RequireSession { 200 c.SessionRequired() 201 } 202 203 if c.Err == nil && h.RequireMfa { 204 c.MfaRequired() 205 } 206 207 if c.Err == nil && h.DisableWhenBusy && c.App.Srv().Busy.IsBusy() { 208 c.SetServerBusyError() 209 } 210 211 if c.Err == nil { 212 h.HandleFunc(c, w, r) 213 } 214 215 // Handle errors that have occurred 216 if c.Err != nil { 217 c.Err.Translate(c.App.T) 218 c.Err.RequestId = c.App.RequestId() 219 220 if c.Err.Id == "api.context.session_expired.app_error" { 221 c.LogInfo(c.Err) 222 } else { 223 c.LogError(c.Err) 224 } 225 226 c.Err.Where = r.URL.Path 227 228 // Block out detailed error when not in developer mode 229 if !*c.App.Config().ServiceSettings.EnableDeveloper { 230 c.Err.DetailedError = "" 231 } 232 233 // Sanitize all 5xx error messages in hardened mode 234 if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode && c.Err.StatusCode >= 500 { 235 c.Err.Id = "" 236 c.Err.Message = "Internal Server Error" 237 c.Err.DetailedError = "" 238 c.Err.StatusCode = 500 239 c.Err.Where = "" 240 c.Err.IsOAuth = false 241 } 242 243 if IsApiCall(c.App, r) || IsWebhookCall(c.App, r) || IsOAuthApiCall(c.App, r) || len(r.Header.Get("X-Mobile-App")) > 0 { 244 w.WriteHeader(c.Err.StatusCode) 245 w.Write([]byte(c.Err.ToJson())) 246 } else { 247 utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey()) 248 } 249 250 if c.App.Metrics() != nil { 251 c.App.Metrics().IncrementHttpError() 252 } 253 } 254 255 if c.App.Metrics() != nil { 256 c.App.Metrics().IncrementHttpRequest() 257 258 if r.URL.Path != model.API_URL_SUFFIX+"/websocket" { 259 elapsed := float64(time.Since(now)) / float64(time.Second) 260 c.App.Metrics().ObserveHttpRequestDuration(elapsed) 261 c.App.Metrics().ObserveApiEndpointDuration(h.HandlerName, r.Method, elapsed) 262 } 263 } 264 } 265 266 // checkCSRFToken performs a CSRF check on the provided request with the given CSRF token. Returns whether or not 267 // a CSRF check occurred and whether or not it succeeded. 268 func (h *Handler) checkCSRFToken(c *Context, r *http.Request, token string, tokenLocation app.TokenLocation, session *model.Session) (checked bool, passed bool) { 269 csrfCheckNeeded := session != nil && c.Err == nil && tokenLocation == app.TokenLocationCookie && !h.TrustRequester && r.Method != "GET" 270 csrfCheckPassed := false 271 272 if csrfCheckNeeded { 273 csrfHeader := r.Header.Get(model.HEADER_CSRF_TOKEN) 274 275 if csrfHeader == session.GetCSRF() { 276 csrfCheckPassed = true 277 } else if r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML { 278 // ToDo(DSchalla) 2019/01/04: Remove after deprecation period and only allow CSRF Header (MM-13657) 279 csrfErrorMessage := "CSRF Header check failed for request - Please upgrade your web application or custom app to set a CSRF Header" 280 281 sid := "" 282 userId := "" 283 284 if session != nil { 285 sid = session.Id 286 userId = session.UserId 287 } 288 289 fields := []mlog.Field{ 290 mlog.String("path", r.URL.Path), 291 mlog.String("ip", r.RemoteAddr), 292 mlog.String("session_id", sid), 293 mlog.String("user_id", userId), 294 } 295 296 if *c.App.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement { 297 c.Log.Warn(csrfErrorMessage, fields...) 298 } else { 299 c.Log.Debug(csrfErrorMessage, fields...) 300 csrfCheckPassed = true 301 } 302 } 303 304 if !csrfCheckPassed { 305 c.App.SetSession(&model.Session{}) 306 c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized) 307 } 308 } 309 310 return csrfCheckNeeded, csrfCheckPassed 311 } 312 313 // ApiHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be 314 // granted. 315 func (w *Web) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { 316 handler := &Handler{ 317 GetGlobalAppOptions: w.GetGlobalAppOptions, 318 HandleFunc: h, 319 HandlerName: GetHandlerName(h), 320 RequireSession: false, 321 TrustRequester: false, 322 RequireMfa: false, 323 IsStatic: false, 324 } 325 if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { 326 return gziphandler.GzipHandler(handler) 327 } 328 return handler 329 } 330 331 // ApiHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are 332 // allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the 333 // websocket. 334 func (w *Web) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { 335 handler := &Handler{ 336 GetGlobalAppOptions: w.GetGlobalAppOptions, 337 HandleFunc: h, 338 HandlerName: GetHandlerName(h), 339 RequireSession: false, 340 TrustRequester: true, 341 RequireMfa: false, 342 IsStatic: false, 343 } 344 if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { 345 return gziphandler.GzipHandler(handler) 346 } 347 return handler 348 } 349 350 // ApiSessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to 351 // be granted. 352 func (w *Web) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { 353 handler := &Handler{ 354 GetGlobalAppOptions: w.GetGlobalAppOptions, 355 HandleFunc: h, 356 HandlerName: GetHandlerName(h), 357 RequireSession: true, 358 TrustRequester: false, 359 RequireMfa: true, 360 IsStatic: false, 361 } 362 if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" { 363 return gziphandler.GzipHandler(handler) 364 } 365 return handler 366 }