2018-06-25 15:33:13 -04:00
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
2021-01-07 12:12:43 -05:00
"bytes"
2022-08-09 07:25:46 -04:00
"io"
2018-06-25 15:33:13 -04:00
"net/http"
2018-07-26 08:33:56 -04:00
"path"
2019-04-05 10:35:51 -04:00
"path/filepath"
2018-06-25 15:33:13 -04:00
"strings"
"github.com/gorilla/mux"
2021-01-07 12:12:43 -05:00
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/mlog"
2025-02-17 01:50:21 -05:00
"github.com/mattermost/mattermost/server/public/shared/request"
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/v8/channels/utils"
2018-06-25 15:33:13 -04:00
)
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) ServePluginRequest ( w http . ResponseWriter , r * http . Request ) {
2022-06-07 02:12:15 -04:00
params := mux . Vars ( r )
2024-02-15 07:01:44 -05:00
pluginID := params [ "plugin_id" ]
2022-06-07 02:12:15 -04:00
2022-12-21 14:10:26 -05:00
pluginsEnvironment := ch . GetPluginsEnvironment ( )
2018-11-20 08:52:51 -05:00
if pluginsEnvironment == nil {
2024-11-07 03:09:11 -05:00
appErr := model . NewAppError ( "ServePluginRequest" , "app.plugin.disabled.app_error" , nil , "Enable plugins to serve plugin requests" , http . StatusNotImplemented )
mlog . Error ( appErr . Error ( ) )
w . WriteHeader ( appErr . StatusCode )
2018-06-25 15:33:13 -04:00
w . Header ( ) . Set ( "Content-Type" , "application/json" )
2024-11-07 03:09:11 -05:00
if _ , err := w . Write ( [ ] byte ( appErr . ToJSON ( ) ) ) ; err != nil {
mlog . Warn ( "Error while writing response" , mlog . Err ( err ) )
}
2018-06-25 15:33:13 -04:00
return
}
2024-02-15 07:01:44 -05:00
hooks , err := pluginsEnvironment . HooksForPlugin ( pluginID )
2018-06-25 15:33:13 -04:00
if err != nil {
2022-02-16 11:59:27 -05:00
mlog . Debug ( "Access to route for non-existent plugin" ,
2024-02-15 07:01:44 -05:00
mlog . String ( "missing_plugin_id" , pluginID ) ,
2020-08-12 11:00:53 -04:00
mlog . String ( "url" , r . URL . String ( ) ) ,
mlog . Err ( err ) )
2018-06-25 15:33:13 -04:00
http . NotFound ( w , r )
return
}
2022-12-21 14:10:26 -05:00
ch . servePluginRequest ( w , r , hooks . ServeHTTP )
2018-06-25 15:33:13 -04:00
}
2025-10-30 15:09:27 -04:00
// ServeInternalPluginRequest handles internal requests to plugins from either core server or other plugins.
// This is used by the Plugin Bridge to route requests with proper authentication headers.
//
// Parameters:
// - userID: User ID to set in the authentication header (empty string if no user context)
// - w: HTTP response writer
// - r: HTTP request (should have URL path set to the endpoint, NOT including plugin ID)
// - sourcePluginID: ID of calling plugin (empty string if from core)
// - targetPluginID: ID of target plugin to call
func ( a * App ) ServeInternalPluginRequest ( userID string , w http . ResponseWriter , r * http . Request , sourcePluginID , targetPluginID string ) {
2022-12-21 14:10:26 -05:00
pluginsEnvironment := a . ch . GetPluginsEnvironment ( )
2019-11-04 20:35:58 -05:00
if pluginsEnvironment == nil {
2025-10-30 15:09:27 -04:00
appErr := model . NewAppError ( "ServeInternalPluginRequest" , "app.plugin.disabled.app_error" , nil , "Plugin environment not found." , http . StatusNotImplemented )
2024-11-07 03:09:11 -05:00
a . Log ( ) . Error ( appErr . Error ( ) )
w . WriteHeader ( appErr . StatusCode )
2019-11-04 20:35:58 -05:00
w . Header ( ) . Set ( "Content-Type" , "application/json" )
2024-11-07 03:09:11 -05:00
if _ , err := w . Write ( [ ] byte ( appErr . ToJSON ( ) ) ) ; err != nil {
mlog . Warn ( "Error while writing response" , mlog . Err ( err ) )
}
2019-11-04 20:35:58 -05:00
return
}
2025-10-30 15:09:27 -04:00
hooks , err := pluginsEnvironment . HooksForPlugin ( targetPluginID )
2019-11-04 20:35:58 -05:00
if err != nil {
2025-10-30 15:09:27 -04:00
a . Log ( ) . Error ( "Access to route for non-existent plugin in internal plugin request" ,
mlog . String ( "source_plugin_id" , sourcePluginID ) ,
mlog . String ( "target_plugin_id" , targetPluginID ) ,
2020-08-12 11:00:53 -04:00
mlog . String ( "url" , r . URL . String ( ) ) ,
2019-11-04 20:35:58 -05:00
mlog . Err ( err ) ,
)
http . NotFound ( w , r )
return
}
context := & plugin . Context {
2021-08-03 12:52:14 -04:00
RequestId : model . NewId ( ) ,
UserAgent : r . UserAgent ( ) ,
2019-11-04 20:35:58 -05:00
}
2025-10-30 15:09:27 -04:00
// Set authentication headers - these are trusted because this function is internal
// and not exposed to external HTTP routes
r . Header . Set ( "Mattermost-User-Id" , userID )
// Set plugin ID header to identify the caller
// Use a special ID for core server calls to distinguish them from plugin-to-plugin calls
if sourcePluginID != "" {
r . Header . Set ( "Mattermost-Plugin-ID" , sourcePluginID )
} else {
// Core server call - use special identifier
r . Header . Set ( "Mattermost-Plugin-ID" , "com.mattermost.server" )
}
2021-04-19 08:02:00 -04:00
2019-11-04 20:35:58 -05:00
hooks . ServeHTTP ( context , w , r )
}
2025-10-30 15:09:27 -04:00
// ServeInterPluginRequest handles inter-plugin HTTP requests.
// This function does not set user authentication headers, unlike ServeInternalPluginRequest.
func ( a * App ) ServeInterPluginRequest ( w http . ResponseWriter , r * http . Request , sourcePluginId , destinationPluginId string ) {
// Call ServeInternalPluginRequest with empty userID since this function doesn't handle user authentication
a . ServeInternalPluginRequest ( "" , w , r , sourcePluginId , destinationPluginId )
}
2019-04-05 10:35:51 -04:00
// ServePluginPublicRequest serves public plugin files
// at the URL http(s)://$SITE_URL/plugins/$PLUGIN_ID/public/{anything}
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) ServePluginPublicRequest ( w http . ResponseWriter , r * http . Request ) {
2019-04-05 10:35:51 -04:00
if strings . HasSuffix ( r . URL . Path , "/" ) {
http . NotFound ( w , r )
return
}
2023-01-10 12:45:17 -05:00
// Should be in the form of /(subpath/)?/plugins/{plugin_id}/public/* by the time we get here
2019-04-05 10:35:51 -04:00
vars := mux . Vars ( r )
pluginID := vars [ "plugin_id" ]
2022-12-21 14:10:26 -05:00
pluginsEnv := ch . GetPluginsEnvironment ( )
2021-02-17 05:23:30 -05:00
// Check if someone has nullified the pluginsEnv in the meantime
if pluginsEnv == nil {
http . NotFound ( w , r )
return
}
publicFilesPath , err := pluginsEnv . PublicFilesPath ( pluginID )
2019-04-05 10:35:51 -04:00
if err != nil {
http . NotFound ( w , r )
return
}
2023-01-10 12:45:17 -05:00
subpath , err := utils . GetSubpathFromConfig ( ch . cfgSvc . Config ( ) )
if err != nil {
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
2025-08-18 05:17:33 -04:00
return
2023-01-10 12:45:17 -05:00
}
2019-04-05 10:35:51 -04:00
publicFilePath := path . Clean ( r . URL . Path )
2023-01-10 12:45:17 -05:00
prefix := path . Join ( subpath , "plugins" , pluginID , "public" )
2019-04-05 10:35:51 -04:00
if ! strings . HasPrefix ( publicFilePath , prefix ) {
http . NotFound ( w , r )
return
}
publicFile := filepath . Join ( publicFilesPath , strings . TrimPrefix ( publicFilePath , prefix ) )
http . ServeFile ( w , r , publicFile )
}
2022-12-21 14:10:26 -05:00
func ( ch * Channels ) servePluginRequest ( w http . ResponseWriter , r * http . Request , handler func ( * plugin . Context , http . ResponseWriter , * http . Request ) ) {
2025-08-18 05:17:33 -04:00
handleInternalServerError := func ( rctx request . CTX , logMsg string , err error ) {
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
rctx . Logger ( ) . Error ( logMsg , mlog . Err ( err ) )
}
2018-12-05 13:46:08 -05:00
context := & plugin . Context {
RequestId : model . NewId ( ) ,
2022-12-21 14:10:26 -05:00
IPAddress : utils . GetIPAddress ( r , ch . cfgSvc . Config ( ) . ServiceSettings . TrustedProxyIPHeader ) ,
2018-12-05 13:46:08 -05:00
AcceptLanguage : r . Header . Get ( "Accept-Language" ) ,
UserAgent : r . UserAgent ( ) ,
}
2018-06-25 15:33:13 -04:00
2025-08-18 05:17:33 -04:00
pluginID := mux . Vars ( r ) [ "plugin_id" ]
const headerBearerPrefix = model . HeaderBearer + " "
const headerTokenPrefix = model . HeaderToken + " "
var cookieAuth bool
var token string
2021-07-12 14:05:36 -04:00
authHeader := r . Header . Get ( model . HeaderAuth )
2025-08-18 05:17:33 -04:00
if strings . HasPrefix ( strings . ToUpper ( authHeader ) , headerBearerPrefix ) {
token = authHeader [ len ( headerBearerPrefix ) : ]
} else if strings . HasPrefix ( strings . ToLower ( authHeader ) , headerTokenPrefix ) {
token = authHeader [ len ( headerTokenPrefix ) : ]
2021-07-12 14:05:36 -04:00
} else if cookie , _ := r . Cookie ( model . SessionCookieToken ) ; cookie != nil {
2018-06-25 15:33:13 -04:00
token = cookie . Value
2018-08-01 18:16:04 -04:00
cookieAuth = true
2018-06-25 15:33:13 -04:00
} else {
token = r . URL . Query ( ) . Get ( "access_token" )
}
2021-04-19 08:02:00 -04:00
// Mattermost-Plugin-ID can only be set by inter-plugin requests
r . Header . Del ( "Mattermost-Plugin-ID" )
2025-08-18 05:17:33 -04:00
// Clean Mattermost-User-Id header. The server sets this header for authenticated requests
r . Header . Del ( "Mattermost-User-Id" )
2018-06-25 15:33:13 -04:00
cookies := r . Cookies ( )
r . Header . Del ( "Cookie" )
for _ , c := range cookies {
2021-07-12 14:05:36 -04:00
if c . Name != model . SessionCookieToken {
2018-06-25 15:33:13 -04:00
r . AddCookie ( c )
}
}
r . Header . Del ( "Referer" )
newQuery := r . URL . Query ( )
newQuery . Del ( "access_token" )
r . URL . RawQuery = newQuery . Encode ( )
2025-08-18 05:17:33 -04:00
app := New ( ServerConnector ( ch ) )
rctx := request . EmptyContext (
ch . srv . Log ( ) . With (
mlog . String ( "plugin_id" , pluginID ) ,
mlog . String ( "path" , r . URL . Path ) ,
mlog . String ( "method" , r . Method ) ,
mlog . String ( "request_id" , context . RequestId ) ,
mlog . String ( "ip_addr" , utils . GetIPAddress ( r , app . Config ( ) . ServiceSettings . TrustedProxyIPHeader ) ) ,
) ,
) . WithPath ( r . URL . Path )
subpath , err := utils . GetSubpathFromConfig ( ch . cfgSvc . Config ( ) )
if err != nil {
handleInternalServerError ( rctx , "Failed to get subpath for plugin request" , err )
return
}
r . URL . Path = strings . TrimPrefix ( r . URL . Path , path . Join ( subpath , "plugins" , pluginID ) )
// Short path for un-authenticated requests
if token == "" {
handler ( context , w , r )
return
}
session , appErr := app . GetSession ( token )
if appErr != nil {
if appErr . StatusCode == http . StatusInternalServerError {
handleInternalServerError ( rctx , "Internal server error while loading session" , err )
return
}
rctx . Logger ( ) . Debug ( "Token in plugin request is invalid. Treating request as unauthenticated" ,
mlog . Err ( appErr ) ,
)
handler ( context , w , r )
return
}
2025-10-30 15:27:22 -04:00
// If we get to this point, the token resolved to a valid session, and we don't need to remit
// the authorization header to the plugin at all. This also prevents the plugin from incorrectly
// using the token if MFA or CSRF fail below.
r . Header . Del ( model . HeaderAuth )
2025-08-18 05:17:33 -04:00
rctx = rctx .
WithLogger ( rctx . Logger ( ) . With (
mlog . String ( "user_id" , session . UserId ) ,
) ) .
WithSession ( session )
// If MFA is required and user has not activated it, treat it as unauthenticated
if appErr := app . MFARequired ( rctx ) ; appErr != nil {
if appErr . StatusCode == http . StatusInternalServerError {
handleInternalServerError ( rctx , "Internal server error during MFA validation" , err )
return
}
rctx . Logger ( ) . Warn ( "Treating session as unauthenticated since MFA required" ,
mlog . Err ( appErr ) ,
)
handler ( context , w , r )
return
}
2025-10-10 13:15:45 -04:00
if validateCSRFForPluginRequest ( rctx , r , session , cookieAuth , * ch . cfgSvc . Config ( ) . ServiceSettings . ExperimentalStrictCSRFEnforcement ) {
2025-08-18 05:17:33 -04:00
r . Header . Set ( "Mattermost-User-Id" , session . UserId )
context . SessionId = session . Id
} else {
rctx . Logger ( ) . Debug ( "CSRF request failed. Treating the request as unauthenticated." )
}
2018-06-25 15:33:13 -04:00
2018-08-01 18:16:04 -04:00
handler ( context , w , r )
2018-06-25 15:33:13 -04:00
}
2025-08-18 05:17:33 -04:00
// validateCSRFForPluginRequest validates CSRF token for plugin requests
func validateCSRFForPluginRequest ( rctx request . CTX , r * http . Request , session * model . Session , cookieAuth bool , strictCSRFEnforcement bool ) bool {
// Skip CSRF check for non-cookie auth or GET requests
if ! cookieAuth || r . Method == http . MethodGet {
return true
}
csrfTokenFromClient := r . Header . Get ( model . HeaderCsrfToken )
if csrfTokenFromClient == "" {
bodyBytes , err := io . ReadAll ( r . Body )
if err != nil {
rctx . Logger ( ) . Warn ( "Failed to read request body for plugin request" , mlog . Err ( err ) )
}
r . Body = io . NopCloser ( bytes . NewBuffer ( bodyBytes ) )
if err := r . ParseForm ( ) ; err != nil {
rctx . Logger ( ) . Warn ( "Failed to parse form data for plugin request" , mlog . Err ( err ) )
}
csrfTokenFromClient = r . FormValue ( "csrf" )
r . Body = io . NopCloser ( bytes . NewBuffer ( bodyBytes ) )
}
expectedToken := session . GetCSRF ( )
if csrfTokenFromClient == expectedToken {
return true
}
// ToDo(DSchalla) 2019/01/04: Remove after deprecation period and only allow CSRF Header (MM-13657)
if r . Header . Get ( model . HeaderRequestedWith ) == model . HeaderRequestedWithXML {
csrfErrorMessage := "CSRF Check failed for request - Please migrate your plugin to either send a CSRF Header or Form Field, XMLHttpRequest is deprecated"
if strictCSRFEnforcement {
rctx . Logger ( ) . Warn ( csrfErrorMessage , mlog . String ( "session_id" , session . Id ) )
return false
}
// Allow XMLHttpRequest for backward compatibility when not strict
rctx . Logger ( ) . Debug ( csrfErrorMessage , mlog . String ( "session_id" , session . Id ) )
return true
}
return false
}