handlers.go 6.78 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.

package web

import (
	"fmt"
	"net/http"
	"time"

	"github.com/mattermost/mattermost-server/app"
	"github.com/mattermost/mattermost-server/mlog"
	"github.com/mattermost/mattermost-server/model"
	"github.com/mattermost/mattermost-server/utils"
)

func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
	return &Handler{
19 20 21 22 23 24
		GetGlobalAppOptions: w.GetGlobalAppOptions,
		HandleFunc:          h,
		RequireSession:      false,
		TrustRequester:      false,
		RequireMfa:          false,
		IsStatic:            false,
25 26 27 28
	}
}

func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
29 30 31 32
	// Determine the CSP SHA directive needed for subpath support, if any. This value is fixed
	// on server start and intentionally requires a restart to take effect.
	subpath, _ := utils.GetSubpathFromConfig(w.ConfigService.Config())

33
	return &Handler{
34 35 36 37 38 39
		GetGlobalAppOptions: w.GetGlobalAppOptions,
		HandleFunc:          h,
		RequireSession:      false,
		TrustRequester:      false,
		RequireMfa:          false,
		IsStatic:            true,
40 41

		cspShaDirective: utils.GetSubpathScriptHash(subpath),
42 43 44 45
	}
}

type Handler struct {
46 47 48 49 50 51
	GetGlobalAppOptions app.AppOptionCreator
	HandleFunc          func(*Context, http.ResponseWriter, *http.Request)
	RequireSession      bool
	TrustRequester      bool
	RequireMfa          bool
	IsStatic            bool
52 53

	cspShaDirective string
54 55 56 57 58 59 60
}

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	now := time.Now()
	mlog.Debug(fmt.Sprintf("%v - %v", r.Method, r.URL.Path))

	c := &Context{}
61 62 63 64 65 66
	c.App = app.New(
		h.GetGlobalAppOptions()...,
	)
	c.App.T, _ = utils.GetTranslationsAndLocale(w, r)
	c.App.RequestId = model.NewId()
	c.App.IpAddress = utils.GetIpAddress(r)
67 68
	c.App.UserAgent = r.UserAgent()
	c.App.AcceptLanguage = r.Header.Get("Accept-Language")
69
	c.Params = ParamsFromRequest(r)
70
	c.App.Path = r.URL.Path
71
	c.Log = c.App.Log
72

73 74 75
	subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
	siteURLHeader := app.GetProtocol(r) + "://" + r.Host + subpath
	c.SetSiteURLHeader(siteURLHeader)
76

77
	w.Header().Set(model.HEADER_REQUEST_ID, c.App.RequestId)
78 79
	w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.License() != nil))

80 81 82 83
	if *c.App.Config().ServiceSettings.TLSStrictTransport {
		w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", *c.App.Config().ServiceSettings.TLSStrictTransportMaxAge))
	}

84 85 86
	if h.IsStatic {
		// Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking
		w.Header().Set("X-Frame-Options", "SAMEORIGIN")
87
		// Set content security policy. This is also specified in the root.html of the webapp in a meta tag.
88 89
		w.Header().Set("Content-Security-Policy", fmt.Sprintf(
			"frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/%s",
90
			h.cspShaDirective,
91
		))
92 93 94 95 96 97 98 99 100
	} else {
		// All api response bodies will be JSON formatted by default
		w.Header().Set("Content-Type", "application/json")

		if r.Method == "GET" {
			w.Header().Set("Expires", "0")
		}
	}

101 102
	token, tokenLocation := app.ParseAuthTokenFromRequest(r)

103 104
	if len(token) != 0 {
		session, err := c.App.GetSession(token)
105 106 107 108 109 110 111
		csrfCheckPassed := false

		// CSRF Check
		if tokenLocation == app.TokenLocationCookie && h.RequireSession && !h.TrustRequester && r.Method != "GET" {
			csrfHeader := r.Header.Get(model.HEADER_CSRF_TOKEN)
			if csrfHeader == session.GetCSRF() {
				csrfCheckPassed = true
112
			} else if r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML {
113
				// ToDo(DSchalla) 2019/01/04: Remove after deprecation period and only allow CSRF Header (MM-13657)
114 115 116 117 118 119 120
				csrfErrorMessage := "CSRF Header check failed for request - Please upgrade your web application or custom app to set a CSRF Header"
				if *c.App.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement {
					c.Log.Warn(csrfErrorMessage)
				} else {
					c.Log.Debug(csrfErrorMessage)
					csrfCheckPassed = true
				}
121
			}
122

123 124 125 126
			if !csrfCheckPassed {
				token = ""
				session = nil
				c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
127 128
			}
		} else {
129
			csrfCheckPassed = true
130 131
		}

132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
		if csrfCheckPassed {
			if err != nil {
				c.Log.Info("Invalid session", mlog.Err(err))
				if err.StatusCode == http.StatusInternalServerError {
					c.Err = err
				} else if h.RequireSession {
					c.RemoveSessionCookie(w, r)
					c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
				}
			} else if !session.IsOAuth && tokenLocation == app.TokenLocationQueryString {
				c.Err = model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
			} else {
				c.App.Session = *session
			}

			// Rate limit by UserID
			if c.App.Srv.RateLimiter != nil && c.App.Srv.RateLimiter.UserIdRateLimit(c.App.Session.UserId, w) {
				return
			}
151 152 153
		}
	}

154
	c.Log = c.App.Log.With(
155 156 157 158
		mlog.String("path", c.App.Path),
		mlog.String("request_id", c.App.RequestId),
		mlog.String("ip_addr", c.App.IpAddress),
		mlog.String("user_id", c.App.Session.UserId),
159 160
		mlog.String("method", r.Method),
	)
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175

	if c.Err == nil && h.RequireSession {
		c.SessionRequired()
	}

	if c.Err == nil && h.RequireMfa {
		c.MfaRequired()
	}

	if c.Err == nil {
		h.HandleFunc(c, w, r)
	}

	// Handle errors that have occurred
	if c.Err != nil {
176 177
		c.Err.Translate(c.App.T)
		c.Err.RequestId = c.App.RequestId
178 179 180 181 182 183 184 185 186 187 188 189 190 191

		if c.Err.Id == "api.context.session_expired.app_error" {
			c.LogInfo(c.Err)
		} else {
			c.LogError(c.Err)
		}

		c.Err.Where = r.URL.Path

		// Block out detailed error when not in developer mode
		if !*c.App.Config().ServiceSettings.EnableDeveloper {
			c.Err.DetailedError = ""
		}

192 193 194 195 196 197 198 199 200 201
		// Sanitize all 5xx error messages in hardened mode
		if *c.App.Config().ServiceSettings.ExperimentalEnableHardenedMode && c.Err.StatusCode >= 500 {
			c.Err.Id = ""
			c.Err.Message = "Internal Server Error"
			c.Err.DetailedError = ""
			c.Err.StatusCode = 500
			c.Err.Where = ""
			c.Err.IsOAuth = false
		}

202
		if IsApiCall(c.App, r) || IsWebhookCall(c.App, r) || len(r.Header.Get("X-Mobile-App")) > 0 {
203 204 205
			w.WriteHeader(c.Err.StatusCode)
			w.Write([]byte(c.Err.ToJson()))
		} else {
206
			utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
207
		}
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222

		if c.App.Metrics != nil {
			c.App.Metrics.IncrementHttpError()
		}
	}

	if c.App.Metrics != nil {
		c.App.Metrics.IncrementHttpRequest()

		if r.URL.Path != model.API_URL_SUFFIX+"/websocket" {
			elapsed := float64(time.Since(now)) / float64(time.Second)
			c.App.Metrics.ObserveHttpRequestDuration(elapsed)
		}
	}
}