Refactor context out of API packages (#8755)

* Refactor context out of API packages

* Update function names per feedback

* Move webhook handlers to web and fix web tests

* Move more webhook tests out of api package

* Fix static handler
Этот коммит содержится в:
Joram Wilander
2018-05-14 10:24:58 -04:00
коммит произвёл GitHub
родитель 7e7c551987
Коммит 47250c6629
16 изменённых файлов: 708 добавлений и 611 удалений

Просмотреть файл

@@ -4,14 +4,12 @@
package api4
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
"github.com/mattermost/mattermost-server/web"
_ "github.com/nicksnyder/go-i18n/i18n"
)
@@ -231,7 +229,7 @@ func Init(a *app.App, root *mux.Router, full bool) *API {
api.InitRole()
api.InitImage()
root.Handle("/api/v4/{anything:.*}", http.HandlerFunc(Handle404))
root.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
// REMOVE CONDITION WHEN APIv3 REMOVED
if full {
@@ -241,14 +239,8 @@ func Init(a *app.App, root *mux.Router, full bool) *API {
return api
}
func Handle404(w http.ResponseWriter, r *http.Request) {
err := model.NewAppError("Handle404", "api.context.404.app_error", nil, "", http.StatusNotFound)
mlog.Debug(fmt.Sprintf("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r)))
w.WriteHeader(err.StatusCode)
err.DetailedError = "There doesn't appear to be an api call for the url='" + r.URL.Path + "'."
w.Write([]byte(err.ToJson()))
func (api *API) Handle404(w http.ResponseWriter, r *http.Request) {
web.Handle404(api.App, w, r)
}
func ReturnStatusOK(w http.ResponseWriter) {

Просмотреть файл

@@ -1,663 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api4
import (
"fmt"
"net/http"
"regexp"
"strings"
"time"
goi18n "github.com/nicksnyder/go-i18n/i18n"
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
)
type Context struct {
App *app.App
Session model.Session
Params *ApiParams
Err *model.AppError
T goi18n.TranslateFunc
RequestId string
IpAddress string
Path string
siteURLHeader string
}
func (api *API) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &handler{
app: api.App,
handleFunc: h,
requireSession: false,
trustRequester: false,
requireMfa: false,
}
}
func (api *API) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &handler{
app: api.App,
handleFunc: h,
requireSession: true,
trustRequester: false,
requireMfa: true,
}
}
func (api *API) ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &handler{
app: api.App,
handleFunc: h,
requireSession: true,
trustRequester: false,
requireMfa: false,
}
}
func (api *API) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &handler{
app: api.App,
handleFunc: h,
requireSession: false,
trustRequester: true,
requireMfa: false,
}
}
func (api *API) ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &handler{
app: api.App,
handleFunc: h,
requireSession: true,
trustRequester: true,
requireMfa: true,
}
}
type handler struct {
app *app.App
handleFunc func(*Context, http.ResponseWriter, *http.Request)
requireSession bool
trustRequester bool
requireMfa bool
}
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{}
c.App = h.app
c.T, _ = utils.GetTranslationsAndLocale(w, r)
c.RequestId = model.NewId()
c.IpAddress = utils.GetIpAddress(r)
c.Params = ApiParamsFromRequest(r)
token, tokenLocation := app.ParseAuthTokenFromRequest(r)
// CSRF Check
if tokenLocation == app.TokenLocationCookie && h.requireSession && !h.trustRequester {
if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
c.Err = model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
token = ""
}
}
c.SetSiteURLHeader(app.GetProtocol(r) + "://" + r.Host)
w.Header().Set(model.HEADER_REQUEST_ID, c.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))
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
w.Header().Set("Expires", "0")
}
if len(token) != 0 {
session, err := c.App.GetSession(token)
if err != nil {
mlog.Info(fmt.Sprintf("Invalid session err=%v", err.Error()))
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.Session = *session
}
// Rate limit by UserID
if c.App.Srv.RateLimiter != nil && c.App.Srv.RateLimiter.UserIdRateLimit(c.Session.UserId, w) {
return
}
}
c.Path = r.URL.Path
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 {
c.Err.Translate(c.T)
c.Err.RequestId = c.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 = ""
}
w.WriteHeader(c.Err.StatusCode)
w.Write([]byte(c.Err.ToJson()))
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)
}
}
}
func (c *Context) LogAudit(extraInfo string) {
audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
if r := <-c.App.Srv.Store.Audit().Save(audit); r.Err != nil {
c.LogError(r.Err)
}
}
func (c *Context) LogAuditWithUserId(userId, extraInfo string) {
if len(c.Session.UserId) > 0 {
extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.Session.UserId)
}
audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
if r := <-c.App.Srv.Store.Audit().Save(audit); r.Err != nil {
c.LogError(r.Err)
}
}
func (c *Context) LogError(err *model.AppError) {
// Filter out 404s, endless reconnects and browser compatibility errors
if err.StatusCode == http.StatusNotFound ||
(c.Path == "/api/v3/users/websocket" && err.StatusCode == 401) ||
err.Id == "web.check_browser_compatibility.app_error" {
c.LogDebug(err)
} else {
mlog.Error(fmt.Sprintf("%v:%v code=%v rid=%v uid=%v ip=%v %v [details: %v]", c.Path, err.Where, err.StatusCode,
c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.TDefault), err.DetailedError), mlog.String("user_id", c.Session.UserId))
}
}
func (c *Context) LogInfo(err *model.AppError) {
mlog.Info(fmt.Sprintf("%v:%v code=%v rid=%v uid=%v ip=%v %v [details: %v]", c.Path, err.Where, err.StatusCode,
c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.TDefault), err.DetailedError), mlog.String("user_id", c.Session.UserId))
}
func (c *Context) LogDebug(err *model.AppError) {
mlog.Debug(fmt.Sprintf("%v:%v code=%v rid=%v uid=%v ip=%v %v [details: %v]", c.Path, err.Where, err.StatusCode,
c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.TDefault), err.DetailedError), mlog.String("user_id", c.Session.UserId))
}
func (c *Context) IsSystemAdmin() bool {
return c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM)
}
func (c *Context) SessionRequired() {
if !*c.App.Config().ServiceSettings.EnableUserAccessTokens && c.Session.Props[model.SESSION_PROP_TYPE] == model.SESSION_TYPE_USER_ACCESS_TOKEN {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserAccessToken", http.StatusUnauthorized)
return
}
if len(c.Session.UserId) == 0 {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized)
return
}
}
func (c *Context) MfaRequired() {
// Must be licensed for MFA and have it configured for enforcement
if license := c.App.License(); license == nil || !*license.Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication || !*c.App.Config().ServiceSettings.EnforceMultifactorAuthentication {
return
}
// OAuth integrations are excepted
if c.Session.IsOAuth {
return
}
if user, err := c.App.GetUser(c.Session.UserId); err != nil {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "MfaRequired", http.StatusUnauthorized)
return
} else {
// Only required for email and ldap accounts
if user.AuthService != "" &&
user.AuthService != model.USER_AUTH_SERVICE_EMAIL &&
user.AuthService != model.USER_AUTH_SERVICE_LDAP {
return
}
// Special case to let user get themself
if c.Path == "/api/v4/users/me" {
return
}
if !user.MfaActive {
c.Err = model.NewAppError("", "api.context.mfa_required.app_error", nil, "MfaRequired", http.StatusForbidden)
return
}
}
}
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, cookie)
}
func (c *Context) SetInvalidParam(parameter string) {
c.Err = NewInvalidParamError(parameter)
}
func (c *Context) SetInvalidUrlParam(parameter string) {
c.Err = NewInvalidUrlParamError(parameter)
}
func (c *Context) HandleEtag(etag string, routeName string, w http.ResponseWriter, r *http.Request) bool {
metrics := c.App.Metrics
if et := r.Header.Get(model.HEADER_ETAG_CLIENT); len(etag) > 0 {
if et == etag {
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.WriteHeader(http.StatusNotModified)
if metrics != nil {
metrics.IncrementEtagHitCounter(routeName)
}
return true
}
}
if metrics != nil {
metrics.IncrementEtagMissCounter(routeName)
}
return false
}
func NewInvalidParamError(parameter string) *model.AppError {
err := model.NewAppError("Context", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": parameter}, "", http.StatusBadRequest)
return err
}
func NewInvalidUrlParamError(parameter string) *model.AppError {
err := model.NewAppError("Context", "api.context.invalid_url_param.app_error", map[string]interface{}{"Name": parameter}, "", http.StatusBadRequest)
return err
}
func (c *Context) SetPermissionError(permission *model.Permission) {
c.Err = model.NewAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", "+"permission="+permission.Id, http.StatusForbidden)
}
func (c *Context) SetSiteURLHeader(url string) {
c.siteURLHeader = strings.TrimRight(url, "/")
}
func (c *Context) GetSiteURLHeader() string {
return c.siteURLHeader
}
func (c *Context) RequireUserId() *Context {
if c.Err != nil {
return c
}
if c.Params.UserId == model.ME {
c.Params.UserId = c.Session.UserId
}
if len(c.Params.UserId) != 26 {
c.SetInvalidUrlParam("user_id")
}
return c
}
func (c *Context) RequireTeamId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.TeamId) != 26 {
c.SetInvalidUrlParam("team_id")
}
return c
}
func (c *Context) RequireInviteId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.InviteId) == 0 {
c.SetInvalidUrlParam("invite_id")
}
return c
}
func (c *Context) RequireTokenId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.TokenId) != 26 {
c.SetInvalidUrlParam("token_id")
}
return c
}
func (c *Context) RequireChannelId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.ChannelId) != 26 {
c.SetInvalidUrlParam("channel_id")
}
return c
}
func (c *Context) RequireUsername() *Context {
if c.Err != nil {
return c
}
if !model.IsValidUsername(c.Params.Username) {
c.SetInvalidParam("username")
}
return c
}
func (c *Context) RequirePostId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.PostId) != 26 {
c.SetInvalidUrlParam("post_id")
}
return c
}
func (c *Context) RequireAppId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.AppId) != 26 {
c.SetInvalidUrlParam("app_id")
}
return c
}
func (c *Context) RequireFileId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.FileId) != 26 {
c.SetInvalidUrlParam("file_id")
}
return c
}
func (c *Context) RequireFilename() *Context {
if c.Err != nil {
return c
}
if len(c.Params.Filename) == 0 {
c.SetInvalidUrlParam("filename")
}
return c
}
func (c *Context) RequirePluginId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.PluginId) == 0 {
c.SetInvalidUrlParam("plugin_id")
}
return c
}
func (c *Context) RequireReportId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.ReportId) != 26 {
c.SetInvalidUrlParam("report_id")
}
return c
}
func (c *Context) RequireEmojiId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.EmojiId) != 26 {
c.SetInvalidUrlParam("emoji_id")
}
return c
}
func (c *Context) RequireTeamName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidTeamName(c.Params.TeamName) {
c.SetInvalidUrlParam("team_name")
}
return c
}
func (c *Context) RequireChannelName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidChannelIdentifier(c.Params.ChannelName) {
c.SetInvalidUrlParam("channel_name")
}
return c
}
func (c *Context) RequireEmail() *Context {
if c.Err != nil {
return c
}
if !model.IsValidEmail(c.Params.Email) {
c.SetInvalidUrlParam("email")
}
return c
}
func (c *Context) RequireCategory() *Context {
if c.Err != nil {
return c
}
if !model.IsValidAlphaNumHyphenUnderscore(c.Params.Category, true) {
c.SetInvalidUrlParam("category")
}
return c
}
func (c *Context) RequireService() *Context {
if c.Err != nil {
return c
}
if len(c.Params.Service) == 0 {
c.SetInvalidUrlParam("service")
}
return c
}
func (c *Context) RequirePreferenceName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidAlphaNumHyphenUnderscore(c.Params.PreferenceName, true) {
c.SetInvalidUrlParam("preference_name")
}
return c
}
func (c *Context) RequireEmojiName() *Context {
if c.Err != nil {
return c
}
validName := regexp.MustCompile(`^[a-zA-Z0-9\-\+_]+$`)
if len(c.Params.EmojiName) == 0 || len(c.Params.EmojiName) > model.EMOJI_NAME_MAX_LENGTH || !validName.MatchString(c.Params.EmojiName) {
c.SetInvalidUrlParam("emoji_name")
}
return c
}
func (c *Context) RequireHookId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.HookId) != 26 {
c.SetInvalidUrlParam("hook_id")
}
return c
}
func (c *Context) RequireCommandId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.CommandId) != 26 {
c.SetInvalidUrlParam("command_id")
}
return c
}
func (c *Context) RequireJobId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.JobId) != 26 {
c.SetInvalidUrlParam("job_id")
}
return c
}
func (c *Context) RequireJobType() *Context {
if c.Err != nil {
return c
}
if len(c.Params.JobType) == 0 || len(c.Params.JobType) > 32 {
c.SetInvalidUrlParam("job_type")
}
return c
}
func (c *Context) RequireActionId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.ActionId) != 26 {
c.SetInvalidUrlParam("action_id")
}
return c
}
func (c *Context) RequireRoleId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.RoleId) != 26 {
c.SetInvalidUrlParam("role_id")
}
return c
}
func (c *Context) RequireRoleName() *Context {
if c.Err != nil {
return c
}
if !model.IsValidRoleName(c.Params.RoleName) {
c.SetInvalidUrlParam("role_name")
}
return c
}

Просмотреть файл

@@ -1,31 +0,0 @@
package api4
import (
"net/http"
"testing"
)
func TestRequireHookId(t *testing.T) {
c := &Context{}
t.Run("WhenHookIdIsValid", func(t *testing.T) {
c.Params = &ApiParams{HookId: "abcdefghijklmnopqrstuvwxyz"}
c.RequireHookId()
if c.Err != nil {
t.Fatal("Hook Id is Valid. Should not have set error in context")
}
})
t.Run("WhenHookIdIsInvalid", func(t *testing.T) {
c.Params = &ApiParams{HookId: "abc"}
c.RequireHookId()
if c.Err == nil {
t.Fatal("Should have set Error in context")
}
if c.Err.StatusCode != http.StatusBadRequest {
t.Fatal("Should have set status as 400")
}
})
}

Просмотреть файл

@@ -9,6 +9,7 @@ import (
"github.com/mattermost/mattermost-server/app"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/web"
)
const (
@@ -197,7 +198,7 @@ func searchEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
emojis, err := c.App.SearchEmoji(emojiSearch.Term, emojiSearch.PrefixOnly, PER_PAGE_MAXIMUM)
emojis, err := c.App.SearchEmoji(emojiSearch.Term, emojiSearch.PrefixOnly, web.PER_PAGE_MAXIMUM)
if err != nil {
c.Err = err
return

67
api4/handlers.go Обычный файл
Просмотреть файл

@@ -0,0 +1,67 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api4
import (
"net/http"
"github.com/mattermost/mattermost-server/web"
)
type Context = web.Context
func (api *API) ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &web.Handler{
App: api.App,
HandleFunc: h,
RequireSession: false,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
}
}
func (api *API) ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &web.Handler{
App: api.App,
HandleFunc: h,
RequireSession: true,
TrustRequester: false,
RequireMfa: true,
IsStatic: false,
}
}
func (api *API) ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &web.Handler{
App: api.App,
HandleFunc: h,
RequireSession: true,
TrustRequester: false,
RequireMfa: false,
IsStatic: false,
}
}
func (api *API) ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &web.Handler{
App: api.App,
HandleFunc: h,
RequireSession: false,
TrustRequester: true,
RequireMfa: false,
IsStatic: false,
}
}
func (api *API) ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &web.Handler{
App: api.App,
HandleFunc: h,
RequireSession: true,
TrustRequester: true,
RequireMfa: true,
IsStatic: false,
}
}

Просмотреть файл

@@ -1,197 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api4
import (
"net/http"
"strconv"
"strings"
"github.com/gorilla/mux"
)
const (
PAGE_DEFAULT = 0
PER_PAGE_DEFAULT = 60
PER_PAGE_MAXIMUM = 200
LOGS_PER_PAGE_DEFAULT = 10000
LOGS_PER_PAGE_MAXIMUM = 10000
)
type ApiParams struct {
UserId string
TeamId string
InviteId string
TokenId string
ChannelId string
PostId string
FileId string
Filename string
PluginId string
CommandId string
HookId string
ReportId string
EmojiId string
AppId string
Email string
Username string
TeamName string
ChannelName string
PreferenceName string
EmojiName string
Category string
Service string
JobId string
JobType string
ActionId string
RoleId string
RoleName string
Page int
PerPage int
LogsPerPage int
Permanent bool
}
func ApiParamsFromRequest(r *http.Request) *ApiParams {
params := &ApiParams{}
props := mux.Vars(r)
query := r.URL.Query()
if val, ok := props["user_id"]; ok {
params.UserId = val
}
if val, ok := props["team_id"]; ok {
params.TeamId = val
}
if val, ok := props["invite_id"]; ok {
params.InviteId = val
}
if val, ok := props["token_id"]; ok {
params.TokenId = val
}
if val, ok := props["channel_id"]; ok {
params.ChannelId = val
} else {
params.ChannelId = query.Get("channel_id")
}
if val, ok := props["post_id"]; ok {
params.PostId = val
}
if val, ok := props["file_id"]; ok {
params.FileId = val
}
params.Filename = query.Get("filename")
if val, ok := props["plugin_id"]; ok {
params.PluginId = val
}
if val, ok := props["command_id"]; ok {
params.CommandId = val
}
if val, ok := props["hook_id"]; ok {
params.HookId = val
}
if val, ok := props["report_id"]; ok {
params.ReportId = val
}
if val, ok := props["emoji_id"]; ok {
params.EmojiId = val
}
if val, ok := props["app_id"]; ok {
params.AppId = val
}
if val, ok := props["email"]; ok {
params.Email = val
}
if val, ok := props["username"]; ok {
params.Username = val
}
if val, ok := props["team_name"]; ok {
params.TeamName = strings.ToLower(val)
}
if val, ok := props["channel_name"]; ok {
params.ChannelName = strings.ToLower(val)
}
if val, ok := props["category"]; ok {
params.Category = val
}
if val, ok := props["service"]; ok {
params.Service = val
}
if val, ok := props["preference_name"]; ok {
params.PreferenceName = val
}
if val, ok := props["emoji_name"]; ok {
params.EmojiName = val
}
if val, ok := props["job_id"]; ok {
params.JobId = val
}
if val, ok := props["job_type"]; ok {
params.JobType = val
}
if val, ok := props["action_id"]; ok {
params.ActionId = val
}
if val, ok := props["role_id"]; ok {
params.RoleId = val
}
if val, ok := props["role_name"]; ok {
params.RoleName = val
}
if val, err := strconv.Atoi(query.Get("page")); err != nil || val < 0 {
params.Page = PAGE_DEFAULT
} else {
params.Page = val
}
if val, err := strconv.ParseBool(query.Get("permanent")); err != nil {
params.Permanent = val
}
if val, err := strconv.Atoi(query.Get("per_page")); err != nil || val < 0 {
params.PerPage = PER_PAGE_DEFAULT
} else if val > PER_PAGE_MAXIMUM {
params.PerPage = PER_PAGE_MAXIMUM
} else {
params.PerPage = val
}
if val, err := strconv.Atoi(query.Get("logs_per_page")); err != nil || val < 0 {
params.LogsPerPage = LOGS_PER_PAGE_DEFAULT
} else if val > LOGS_PER_PAGE_MAXIMUM {
params.LogsPerPage = LOGS_PER_PAGE_MAXIMUM
} else {
params.LogsPerPage = val
}
return params
}

Просмотреть файл

@@ -4,14 +4,8 @@
package api4
import (
"fmt"
"io"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
)
@@ -28,12 +22,6 @@ func (api *API) InitWebhook() {
api.BaseRoutes.OutgoingHook.Handle("", api.ApiSessionRequired(updateOutgoingHook)).Methods("PUT")
api.BaseRoutes.OutgoingHook.Handle("", api.ApiSessionRequired(deleteOutgoingHook)).Methods("DELETE")
api.BaseRoutes.OutgoingHook.Handle("/regen_token", api.ApiSessionRequired(regenOutgoingHookToken)).Methods("POST")
api.BaseRoutes.Root.Handle("/hooks/commands/{id:[A-Za-z0-9]+}", api.ApiHandler(commandWebhook)).Methods("POST")
api.BaseRoutes.Root.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiHandler(incomingWebhook)).Methods("POST")
// Old endpoint for backwards compatibility
api.BaseRoutes.Root.Handle("/api/v3/teams/{team_id:[A-Za-z0-9]+}/hooks/{id:[A-Za-z0-9]+}", api.ApiHandler(incomingWebhook)).Methods("POST")
}
func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -454,82 +442,3 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("success")
ReturnStatusOK(w)
}
func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
r.ParseForm()
var err *model.AppError
incomingWebhookPayload := &model.IncomingWebhookRequest{}
contentType := r.Header.Get("Content-Type")
if strings.Split(contentType, "; ")[0] == "application/x-www-form-urlencoded" {
payload := strings.NewReader(r.FormValue("payload"))
incomingWebhookPayload, err = decodePayload(payload)
if err != nil {
c.Err = err
return
}
} else if strings.HasPrefix(contentType, "multipart/form-data") {
r.ParseMultipartForm(0)
decoder := schema.NewDecoder()
err := decoder.Decode(incomingWebhookPayload, r.PostForm)
if err != nil {
c.Err = model.NewAppError("incomingWebhook", "api.webhook.incoming.error", nil, err.Error(), http.StatusBadRequest)
return
}
} else {
incomingWebhookPayload, err = decodePayload(r.Body)
if err != nil {
c.Err = err
return
}
}
if c.App.Config().LogSettings.EnableWebhookDebugging {
mlog.Debug(fmt.Sprint("Incoming webhook received. Content=", incomingWebhookPayload.ToJson()))
}
err = c.App.HandleIncomingWebhook(id, incomingWebhookPayload)
if err != nil {
c.Err = err
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("ok"))
}
func commandWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
response, err := model.CommandResponseFromHTTPBody(r.Header.Get("Content-Type"), r.Body)
if err != nil {
c.Err = model.NewAppError("commandWebhook", "web.command_webhook.parse.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
appErr := c.App.HandleCommandWebhook(id, response)
if appErr != nil {
c.Err = appErr
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("ok"))
}
func decodePayload(payload io.Reader) (*model.IncomingWebhookRequest, *model.AppError) {
incomingWebhookPayload, decodeError := model.IncomingWebhookRequestFromJson(payload)
if decodeError != nil {
return nil, decodeError
}
return incomingWebhookPayload, nil
}

Просмотреть файл

@@ -4,8 +4,6 @@
package api4
import (
"bytes"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -892,46 +890,3 @@ func TestDeleteOutgoingHook(t *testing.T) {
CheckForbiddenStatus(t, resp)
})
}
func TestCommandWebhooks(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown()
Client := th.SystemAdminClient
cmd := &model.Command{
CreatorId: th.BasicUser.Id,
TeamId: th.BasicTeam.Id,
URL: "http://nowhere.com",
Method: model.COMMAND_METHOD_POST,
Trigger: "delayed"}
cmd, _ = Client.CreateCommand(cmd)
args := &model.CommandArgs{
TeamId: th.BasicTeam.Id,
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
}
hook, err := th.App.CreateCommandWebhook(cmd.Id, args)
if err != nil {
t.Fatal(err)
}
if resp, _ := http.Post(Client.Url+"/hooks/commands/123123123123", "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusNotFound {
t.Fatal("expected not-found for non-existent hook")
}
if resp, err := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"invalid`)); err != nil || resp.StatusCode != http.StatusBadRequest {
t.Fatal(err)
}
for i := 0; i < 5; i++ {
if resp, err := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); err != nil || resp.StatusCode != http.StatusOK {
t.Fatal(err)
}
}
if resp, _ := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString(`{"text":"this is a test"}`)); resp.StatusCode != http.StatusBadRequest {
t.Fatal("expected error for sixth usage")
}
}