mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
* [MM-24146] Add unix socket listener for mmctl local mode (#14296) * add unix socket listener for mmctl local mode * add a constant for local-mode socket path * reflect review comments * [MM-24401] Base approach for Local Mode (#14333) * add unix socket listener for mmctl local mode * First working PoC * Adds the channel list endpoint * Add team list endpoint * Add a LocalClient to the api test helper and start local mode * Add helper to test with both SystemAdmin and Local clients * Add some docs * Adds TestForAllClients test helper * Incorporating @ashishbhate's proposal for adding test names to the helpers * Fix init errors after merge * Adds create channel tests * Always init local mode to allow for enabling-disabling it via config * Check the RemoteAddr of the request before marking session as local * Mark the request as errored if it's local and the origin is remote * Set the socket permissions to read/write when initialising * Fix linter * Replace RemoteAddr check to ditch connections with the IP:PORT shape Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> * Fix translations order * [MM-24832] Migrate plugin endpoints to local mode (#14543) * [MM-24832] Migrate plugin endpoints to local mode * Fix client reference in helper * [MM-24776] Migrate config endpoints to local mode (#14544) * [MM-24776] Migrate get config endpoint to local mode * [MM-24777] Migrate update config endpoint to local mode * Fix update config to bypass RestrictSystemAdmin flag * Add patchConfig endpoint * MM-24774/MM-24755: local mode for addLicense and removeLicense (#14491) Automatic Merge Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> Co-authored-by: Ashish Bhate <bhate.ashish@gmail.com>
385 lines
13 KiB
Go
385 lines
13 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mkraft/gziphandler"
|
|
"github.com/opentracing/opentracing-go"
|
|
"github.com/opentracing/opentracing-go/ext"
|
|
spanlog "github.com/opentracing/opentracing-go/log"
|
|
|
|
"github.com/mattermost/mattermost-server/v5/app"
|
|
"github.com/mattermost/mattermost-server/v5/mlog"
|
|
"github.com/mattermost/mattermost-server/v5/model"
|
|
"github.com/mattermost/mattermost-server/v5/services/tracing"
|
|
"github.com/mattermost/mattermost-server/v5/store"
|
|
"github.com/mattermost/mattermost-server/v5/utils"
|
|
)
|
|
|
|
func GetHandlerName(h func(*Context, http.ResponseWriter, *http.Request)) string {
|
|
handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()
|
|
pos := strings.LastIndex(handlerName, ".")
|
|
if pos != -1 && len(handlerName) > pos {
|
|
handlerName = handlerName[pos+1:]
|
|
}
|
|
return handlerName
|
|
}
|
|
|
|
func (w *Web) NewHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
|
return &Handler{
|
|
GetGlobalAppOptions: w.GetGlobalAppOptions,
|
|
HandleFunc: h,
|
|
HandlerName: GetHandlerName(h),
|
|
RequireSession: false,
|
|
TrustRequester: false,
|
|
RequireMfa: false,
|
|
IsStatic: false,
|
|
IsLocal: false,
|
|
}
|
|
}
|
|
|
|
func (w *Web) NewStaticHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
|
// 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())
|
|
|
|
return &Handler{
|
|
GetGlobalAppOptions: w.GetGlobalAppOptions,
|
|
HandleFunc: h,
|
|
HandlerName: GetHandlerName(h),
|
|
RequireSession: false,
|
|
TrustRequester: false,
|
|
RequireMfa: false,
|
|
IsStatic: true,
|
|
|
|
cspShaDirective: utils.GetSubpathScriptHash(subpath),
|
|
}
|
|
}
|
|
|
|
type Handler struct {
|
|
GetGlobalAppOptions app.AppOptionCreator
|
|
HandleFunc func(*Context, http.ResponseWriter, *http.Request)
|
|
HandlerName string
|
|
RequireSession bool
|
|
TrustRequester bool
|
|
RequireMfa bool
|
|
IsStatic bool
|
|
IsLocal bool
|
|
DisableWhenBusy bool
|
|
|
|
cspShaDirective string
|
|
}
|
|
|
|
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
w = newWrappedWriter(w)
|
|
now := time.Now()
|
|
|
|
requestID := model.NewId()
|
|
mlog.Debug("Received HTTP request", mlog.String("method", r.Method), mlog.String("url", r.URL.Path), mlog.String("request_id", requestID))
|
|
|
|
c := &Context{}
|
|
c.App = app.New(
|
|
h.GetGlobalAppOptions()...,
|
|
)
|
|
|
|
t, _ := utils.GetTranslationsAndLocale(w, r)
|
|
c.App.SetT(t)
|
|
c.App.SetRequestId(requestID)
|
|
c.App.SetIpAddress(utils.GetIpAddress(r, c.App.Config().ServiceSettings.TrustedProxyIPHeader))
|
|
c.App.SetUserAgent(r.UserAgent())
|
|
c.App.SetAcceptLanguage(r.Header.Get("Accept-Language"))
|
|
c.App.SetPath(r.URL.Path)
|
|
c.Params = ParamsFromRequest(r)
|
|
c.Log = c.App.Log()
|
|
|
|
if *c.App.Config().ServiceSettings.EnableOpenTracing {
|
|
span, ctx := tracing.StartRootSpanByContext(context.Background(), "web:ServeHTTP")
|
|
carrier := opentracing.HTTPHeadersCarrier(r.Header)
|
|
_ = opentracing.GlobalTracer().Inject(span.Context(), opentracing.HTTPHeaders, carrier)
|
|
ext.HTTPMethod.Set(span, r.Method)
|
|
ext.HTTPUrl.Set(span, c.App.Path())
|
|
ext.PeerAddress.Set(span, c.App.IpAddress())
|
|
span.SetTag("request_id", c.App.RequestId())
|
|
span.SetTag("user_agent", c.App.UserAgent())
|
|
|
|
defer func() {
|
|
if c.Err != nil {
|
|
span.LogFields(spanlog.Error(c.Err))
|
|
ext.HTTPStatusCode.Set(span, uint16(c.Err.StatusCode))
|
|
ext.Error.Set(span, true)
|
|
}
|
|
span.Finish()
|
|
}()
|
|
c.App.SetContext(ctx)
|
|
|
|
tmpSrv := app.Server{}
|
|
tmpSrv = *c.App.Srv()
|
|
tmpSrv.Store = store.NewOpenTracingLayer(c.App.Srv().Store, ctx)
|
|
c.App.SetServer(&tmpSrv)
|
|
c.App = app.NewOpenTracingAppLayer(c.App, ctx)
|
|
}
|
|
|
|
// Set the max request body size to be equal to MaxFileSize.
|
|
// Ideally, non-file request bodies should be smaller than file request bodies,
|
|
// but we don't have a clean way to identify all file upload handlers.
|
|
// So to keep it simple, we clamp it to the max file size.
|
|
// We add a buffer of bytes.MinRead so that file sizes close to max file size
|
|
// do not get cut off.
|
|
r.Body = http.MaxBytesReader(w, r.Body, *c.App.Config().FileSettings.MaxFileSize+bytes.MinRead)
|
|
|
|
subpath, _ := utils.GetSubpathFromConfig(c.App.Config())
|
|
siteURLHeader := app.GetProtocol(r) + "://" + r.Host + subpath
|
|
c.SetSiteURLHeader(siteURLHeader)
|
|
|
|
w.Header().Set(model.HEADER_REQUEST_ID, c.App.RequestId())
|
|
w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.License() != nil))
|
|
|
|
if *c.App.Config().ServiceSettings.TLSStrictTransport {
|
|
w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", *c.App.Config().ServiceSettings.TLSStrictTransportMaxAge))
|
|
}
|
|
|
|
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")
|
|
// Set content security policy. This is also specified in the root.html of the webapp in a meta tag.
|
|
w.Header().Set("Content-Security-Policy", fmt.Sprintf(
|
|
"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com cdn.segment.com/analytics.js/%s",
|
|
h.cspShaDirective,
|
|
))
|
|
} 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")
|
|
}
|
|
}
|
|
|
|
token, tokenLocation := app.ParseAuthTokenFromRequest(r)
|
|
|
|
if len(token) != 0 {
|
|
session, err := c.App.GetSession(token)
|
|
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.SetSession(session)
|
|
}
|
|
|
|
// Rate limit by UserID
|
|
if c.App.Srv().RateLimiter != nil && c.App.Srv().RateLimiter.UserIdRateLimit(c.App.Session().UserId, w) {
|
|
return
|
|
}
|
|
|
|
h.checkCSRFToken(c, r, token, tokenLocation, session)
|
|
}
|
|
|
|
c.Log = c.App.Log().With(
|
|
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),
|
|
mlog.String("method", r.Method),
|
|
)
|
|
|
|
if c.Err == nil && h.RequireSession {
|
|
c.SessionRequired()
|
|
}
|
|
|
|
if c.Err == nil && h.RequireMfa {
|
|
c.MfaRequired()
|
|
}
|
|
|
|
if c.Err == nil && h.DisableWhenBusy && c.App.Srv().Busy.IsBusy() {
|
|
c.SetServerBusyError()
|
|
}
|
|
|
|
if c.Err == nil && h.IsLocal {
|
|
// if the connection is local, RemoteAddr shouldn't have the
|
|
// shape IP:PORT (it will be "@" in Linux, for example)
|
|
isLocalOrigin := !strings.Contains(r.RemoteAddr, ":")
|
|
if *c.App.Config().ServiceSettings.EnableLocalMode && isLocalOrigin {
|
|
c.App.SetSession(&model.Session{Local: true})
|
|
} else if !isLocalOrigin {
|
|
c.Err = model.NewAppError("", "api.context.local_origin_required.app_error", nil, "LocalOriginRequired", http.StatusUnauthorized)
|
|
}
|
|
}
|
|
|
|
if c.Err == nil {
|
|
h.HandleFunc(c, w, r)
|
|
}
|
|
|
|
// Handle errors that have occurred
|
|
if c.Err != nil {
|
|
c.Err.Translate(c.App.T)
|
|
c.Err.RequestId = c.App.RequestId()
|
|
|
|
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 = ""
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
if IsApiCall(c.App, r) || IsWebhookCall(c.App, r) || IsOAuthApiCall(c.App, r) || len(r.Header.Get("X-Mobile-App")) > 0 {
|
|
w.WriteHeader(c.Err.StatusCode)
|
|
w.Write([]byte(c.Err.ToJson()))
|
|
} else {
|
|
utils.RenderWebAppError(c.App.Config(), w, r, c.Err, c.App.AsymmetricSigningKey())
|
|
}
|
|
|
|
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)
|
|
statusCode := strconv.Itoa(w.(*responseWriterWrapper).StatusCode())
|
|
c.App.Metrics().ObserveApiEndpointDuration(h.HandlerName, r.Method, statusCode, elapsed)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
csrfCheckNeeded := session != nil && c.Err == nil && tokenLocation == app.TokenLocationCookie && !h.TrustRequester && r.Method != "GET"
|
|
csrfCheckPassed := false
|
|
|
|
if csrfCheckNeeded {
|
|
csrfHeader := r.Header.Get(model.HEADER_CSRF_TOKEN)
|
|
|
|
if csrfHeader == session.GetCSRF() {
|
|
csrfCheckPassed = true
|
|
} else if r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML {
|
|
// ToDo(DSchalla) 2019/01/04: Remove after deprecation period and only allow CSRF Header (MM-13657)
|
|
csrfErrorMessage := "CSRF Header check failed for request - Please upgrade your web application or custom app to set a CSRF Header"
|
|
|
|
sid := ""
|
|
userId := ""
|
|
|
|
if session != nil {
|
|
sid = session.Id
|
|
userId = session.UserId
|
|
}
|
|
|
|
fields := []mlog.Field{
|
|
mlog.String("path", r.URL.Path),
|
|
mlog.String("ip", r.RemoteAddr),
|
|
mlog.String("session_id", sid),
|
|
mlog.String("user_id", userId),
|
|
}
|
|
|
|
if *c.App.Config().ServiceSettings.ExperimentalStrictCSRFEnforcement {
|
|
c.Log.Warn(csrfErrorMessage, fields...)
|
|
} else {
|
|
c.Log.Debug(csrfErrorMessage, fields...)
|
|
csrfCheckPassed = true
|
|
}
|
|
}
|
|
|
|
if !csrfCheckPassed {
|
|
c.App.SetSession(&model.Session{})
|
|
c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
|
|
}
|
|
}
|
|
|
|
return csrfCheckNeeded, csrfCheckPassed
|
|
}
|
|
|
|
// ApiHandler provides a handler for API endpoints which do not require the user to be logged in order for access to be
|
|
// granted.
|
|
func (w *Web) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
|
handler := &Handler{
|
|
GetGlobalAppOptions: w.GetGlobalAppOptions,
|
|
HandleFunc: h,
|
|
HandlerName: GetHandlerName(h),
|
|
RequireSession: false,
|
|
TrustRequester: false,
|
|
RequireMfa: false,
|
|
IsStatic: false,
|
|
IsLocal: false,
|
|
}
|
|
if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" {
|
|
return gziphandler.GzipHandler(handler)
|
|
}
|
|
return handler
|
|
}
|
|
|
|
// ApiHandlerTrustRequester provides a handler for API endpoints which do not require the user to be logged in and are
|
|
// allowed to be requested directly rather than via javascript/XMLHttpRequest, such as site branding images or the
|
|
// websocket.
|
|
func (w *Web) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
|
handler := &Handler{
|
|
GetGlobalAppOptions: w.GetGlobalAppOptions,
|
|
HandleFunc: h,
|
|
HandlerName: GetHandlerName(h),
|
|
RequireSession: false,
|
|
TrustRequester: true,
|
|
RequireMfa: false,
|
|
IsStatic: false,
|
|
IsLocal: false,
|
|
}
|
|
if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" {
|
|
return gziphandler.GzipHandler(handler)
|
|
}
|
|
return handler
|
|
}
|
|
|
|
// ApiSessionRequired provides a handler for API endpoints which require the user to be logged in in order for access to
|
|
// be granted.
|
|
func (w *Web) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
|
handler := &Handler{
|
|
GetGlobalAppOptions: w.GetGlobalAppOptions,
|
|
HandleFunc: h,
|
|
HandlerName: GetHandlerName(h),
|
|
RequireSession: true,
|
|
TrustRequester: false,
|
|
RequireMfa: true,
|
|
IsStatic: false,
|
|
IsLocal: false,
|
|
}
|
|
if *w.ConfigService.Config().ServiceSettings.WebserverMode == "gzip" {
|
|
return gziphandler.GzipHandler(handler)
|
|
}
|
|
return handler
|
|
}
|