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  }