diff --git a/api/webhook.go b/api/webhook.go index 676fd2cbc1..6297133da2 100644 --- a/api/webhook.go +++ b/api/webhook.go @@ -4,6 +4,7 @@ package api import ( + "io" "net/http" "strings" @@ -373,14 +374,33 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) { r.ParseForm() - var parsedRequest *model.IncomingWebhookRequest + var payload io.Reader contentType := r.Header.Get("Content-Type") if strings.Split(contentType, "; ")[0] == "application/x-www-form-urlencoded" { - parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload"))) + payload = strings.NewReader(r.FormValue("payload")) } else { - parsedRequest = model.IncomingWebhookRequestFromJson(r.Body) + payload = r.Body } + if utils.Cfg.LogSettings.EnableWebhookDebugging { + var err error + payload, err = utils.DebugReader( + payload, + utils.T("api.webhook.incoming.debug"), + ) + if err != nil { + c.Err = model.NewLocAppError( + "incomingWebhook", + "api.webhook.incoming.debug.error", + nil, + err.Error(), + ) + return + } + } + + parsedRequest := model.IncomingWebhookRequestFromJson(payload) + if parsedRequest == nil { c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.parse.app_error", nil, "") return diff --git a/config/config.json b/config/config.json index 726cb0d8cb..db62b0bac2 100644 --- a/config/config.json +++ b/config/config.json @@ -52,7 +52,8 @@ "EnableFile": true, "FileLevel": "INFO", "FileFormat": "", - "FileLocation": "" + "FileLocation": "", + "EnableWebhookDebugging": true }, "FileSettings": { "MaxFileSize": 52428800, diff --git a/i18n/en.json b/i18n/en.json index f88e00d291..1589dd4acb 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1955,6 +1955,14 @@ "id": "api.webhook.get_outgoing.disabled.app_error", "translation": "Outgoing webhooks have been disabled by the system admin." }, + { + "id": "api.webhook.incoming.debug", + "translation": "Incoming webhook received. Content=" + }, + { + "id": "api.webhook.incoming.debug.error", + "translation": "Could not read payload of incoming webhook." + }, { "id": "api.webhook.init.debug", "translation": "Initializing webhook api routes" diff --git a/model/config.go b/model/config.go index 7e810be02f..b9177204bd 100644 --- a/model/config.go +++ b/model/config.go @@ -83,12 +83,13 @@ type SqlSettings struct { } type LogSettings struct { - EnableConsole bool - ConsoleLevel string - EnableFile bool - FileLevel string - FileFormat string - FileLocation string + EnableConsole bool + ConsoleLevel string + EnableFile bool + FileLevel string + FileFormat string + FileLocation string + EnableWebhookDebugging bool } type FileSettings struct { diff --git a/utils/log.go b/utils/log.go new file mode 100644 index 0000000000..360c785d08 --- /dev/null +++ b/utils/log.go @@ -0,0 +1,33 @@ +// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package utils + +import ( + "bytes" + "io" + "io/ioutil" + + l4g "github.com/alecthomas/log4go" +) + +// DebugReader logs the content of the io.Reader and returns a new io.Reader +// with the same content as the received io.Reader. +// If you pass reader by reference, it won't be re-created unless the loglevel +// includes Debug. +// If an error is returned, the reader is consumed an cannot be read again. +func DebugReader(reader io.Reader, message string) (io.Reader, error) { + var err error + l4g.Debug(func() string { + content, err := ioutil.ReadAll(reader) + if err != nil { + return "" + } + + reader = bytes.NewReader(content) + + return message + string(content) + }) + + return reader, err +} diff --git a/webapp/components/admin_console/log_settings.jsx b/webapp/components/admin_console/log_settings.jsx index 0a69cbc165..238040b710 100644 --- a/webapp/components/admin_console/log_settings.jsx +++ b/webapp/components/admin_console/log_settings.jsx @@ -26,7 +26,8 @@ export default class LogSettings extends AdminSettings { enableFile: props.config.LogSettings.EnableFile, fileLevel: props.config.LogSettings.FileLevel, fileLocation: props.config.LogSettings.FileLocation, - fileFormat: props.config.LogSettings.FileFormat + fileFormat: props.config.LogSettings.FileFormat, + enableWebhookDebugging: props.config.LogSettings.EnableWebhookDebugging }); } @@ -37,6 +38,7 @@ export default class LogSettings extends AdminSettings { config.LogSettings.FileLevel = this.state.fileLevel; config.LogSettings.FileLocation = this.state.fileLocation; config.LogSettings.FileFormat = this.state.fileFormat; + config.LogSettings.EnableWebhookDebugging = this.state.enableWebhookDebugging; return config; } @@ -166,6 +168,23 @@ export default class LogSettings extends AdminSettings { onChange={this.handleChange} disabled={!this.state.enableFile} /> + + } + helpText={ + + } + value={this.state.enableWebhookDebugging} + onChange={this.handleChange} + /> ); } diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index 29269adeb7..1e0788ebe7 100644 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -326,6 +326,8 @@ "admin.log.logSettings": "Log Settings", "admin.logs.reload": "Reload", "admin.logs.title": "Server Logs", + "admin.log.enableWebhookDebugging": "Enable Webhook Debugging:", + "admin.log.enableWebhookDebuggingDescription": "You can set this to false to disable the debug logging of all incoming webhook request bodies.", "admin.nav.help": "Help", "admin.nav.logout": "Logout", "admin.nav.report": "Report a Problem",