MM-18068 Route integration actions to plugins without hitting the network (#12156)
* wip * plugin Action requests doesn’t make external request * import formatting fix * changes requested in review by @lieut-data * wip * wip pointer * added review changes requested by @lieut-data * condense relative plugin url tests * changes based on review by @crspeller * gofmt
Этот коммит содержится в:
коммит произвёл
Jesse Hallam
родитель
5f91c14576
Коммит
516017d3c9
@@ -21,6 +21,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
@@ -242,19 +244,16 @@ func (a *App) DoPostActionWithCookie(postId, actionId, userId, selectedOption st
|
|||||||
|
|
||||||
// Perform an HTTP POST request to an integration's action endpoint.
|
// Perform an HTTP POST request to an integration's action endpoint.
|
||||||
// Caller must consume and close returned http.Response as necessary.
|
// Caller must consume and close returned http.Response as necessary.
|
||||||
|
// For internal requests, requests are routed directly to a plugin ServerHTTP hook
|
||||||
func (a *App) DoActionRequest(rawURL string, body []byte) (*http.Response, *model.AppError) {
|
func (a *App) DoActionRequest(rawURL string, body []byte) (*http.Response, *model.AppError) {
|
||||||
inURL, err := url.Parse(rawURL)
|
inURL, err := url.Parse(rawURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, err.Error(), http.StatusBadRequest)
|
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
siteURL, _ := url.Parse(*a.Config().ServiceSettings.SiteURL)
|
|
||||||
rawURLPath := path.Clean(rawURL)
|
rawURLPath := path.Clean(rawURL)
|
||||||
if siteURL != nil && (strings.HasPrefix(rawURLPath, "/plugins/") || strings.HasPrefix(rawURLPath, "plugins/")) {
|
if strings.HasPrefix(rawURLPath, "/plugins/") || strings.HasPrefix(rawURLPath, "plugins/") {
|
||||||
inURL.Scheme = siteURL.Scheme
|
return a.DoLocalRequest(rawURLPath, body)
|
||||||
inURL.Host = siteURL.Host
|
|
||||||
inURL.Path = path.Join("/", siteURL.Path, rawURLPath)
|
|
||||||
rawURL = inURL.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", rawURL, bytes.NewReader(body))
|
req, err := http.NewRequest("POST", rawURL, bytes.NewReader(body))
|
||||||
@@ -267,6 +266,7 @@ func (a *App) DoActionRequest(rawURL string, body []byte) (*http.Response, *mode
|
|||||||
// Allow access to plugin routes for action buttons
|
// Allow access to plugin routes for action buttons
|
||||||
var httpClient *http.Client
|
var httpClient *http.Client
|
||||||
subpath, _ := utils.GetSubpathFromConfig(a.Config())
|
subpath, _ := utils.GetSubpathFromConfig(a.Config())
|
||||||
|
siteURL, _ := url.Parse(*a.Config().ServiceSettings.SiteURL)
|
||||||
if (inURL.Hostname() == "localhost" || inURL.Hostname() == "127.0.0.1" || inURL.Hostname() == siteURL.Hostname()) && strings.HasPrefix(inURL.Path, path.Join(subpath, "plugins")) {
|
if (inURL.Hostname() == "localhost" || inURL.Hostname() == "127.0.0.1" || inURL.Hostname() == siteURL.Hostname()) && strings.HasPrefix(inURL.Path, path.Join(subpath, "plugins")) {
|
||||||
req.Header.Set(model.HEADER_AUTH, "Bearer "+a.Session.Token)
|
req.Header.Set(model.HEADER_AUTH, "Bearer "+a.Session.Token)
|
||||||
httpClient = a.HTTPService.MakeClient(true)
|
httpClient = a.HTTPService.MakeClient(true)
|
||||||
@@ -286,6 +286,74 @@ func (a *App) DoActionRequest(rawURL string, body []byte) (*http.Response, *mode
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LocalResponseWriter struct {
|
||||||
|
data []byte
|
||||||
|
headers http.Header
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LocalResponseWriter) Header() http.Header {
|
||||||
|
if w.headers == nil {
|
||||||
|
w.headers = make(http.Header)
|
||||||
|
}
|
||||||
|
return w.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LocalResponseWriter) Write(bytes []byte) (int, error) {
|
||||||
|
w.data = make([]byte, len(bytes))
|
||||||
|
copy(w.data, bytes)
|
||||||
|
return len(w.data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LocalResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
w.status = statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) DoLocalRequest(rawURL string, body []byte) (*http.Response, *model.AppError) {
|
||||||
|
rawURL = strings.TrimPrefix(rawURL, "/")
|
||||||
|
inURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
result := strings.Split(inURL.Path, "/")
|
||||||
|
if len(result) < 2 {
|
||||||
|
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "err=Unable to find pluginId", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
if result[0] != "plugins" {
|
||||||
|
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "err=plugins not in path", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
pluginId := result[1]
|
||||||
|
|
||||||
|
path := strings.TrimPrefix(inURL.Path, "plugins/"+pluginId)
|
||||||
|
|
||||||
|
w := &LocalResponseWriter{}
|
||||||
|
r, err := http.NewRequest("POST", path, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "err="+err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
r.Header.Set("Mattermost-User-Id", a.Session.UserId)
|
||||||
|
r.Header.Set(model.HEADER_AUTH, "Bearer "+a.Session.Token)
|
||||||
|
params := make(map[string]string)
|
||||||
|
params["plugin_id"] = pluginId
|
||||||
|
r = mux.SetURLVars(r, params)
|
||||||
|
|
||||||
|
a.ServePluginRequest(w, r)
|
||||||
|
|
||||||
|
resp := &http.Response{
|
||||||
|
StatusCode: w.status,
|
||||||
|
Proto: "HTTP/1.1",
|
||||||
|
ProtoMajor: 1,
|
||||||
|
ProtoMinor: 1,
|
||||||
|
Header: w.headers,
|
||||||
|
Body: ioutil.NopCloser(bytes.NewReader(w.data)),
|
||||||
|
}
|
||||||
|
if resp.StatusCode == 0 {
|
||||||
|
resp.StatusCode = http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
|
func (a *App) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
|
||||||
clientTriggerId, userId, err := request.DecodeAndVerifyTriggerId(a.AsymmetricSigningKey())
|
clientTriggerId, userId, err := request.DecodeAndVerifyTriggerId(a.AsymmetricSigningKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -443,6 +443,37 @@ func TestSubmitInteractiveDialog(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
|
setupPluginApiTest(t,
|
||||||
|
`
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"github.com/mattermost/mattermost-server/plugin"
|
||||||
|
"github.com/mattermost/mattermost-server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MyPlugin struct {
|
||||||
|
plugin.MattermostPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
response := &model.SubmitDialogResponse{
|
||||||
|
Errors: map[string]string{"name1": "some error"},
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(response.ToJson())
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugin.ClientMain(&MyPlugin{})
|
||||||
|
}
|
||||||
|
`, `{"id": "myplugin", "backend": {"executable": "backend.exe"}}`, "myplugin", th.App)
|
||||||
|
|
||||||
|
hooks, err2 := th.App.GetPluginsEnvironment().HooksForPlugin("myplugin")
|
||||||
|
require.Nil(t, err2)
|
||||||
|
require.NotNil(t, hooks)
|
||||||
|
|
||||||
submit.URL = ts.URL
|
submit.URL = ts.URL
|
||||||
|
|
||||||
resp, err := th.App.SubmitInteractiveDialog(submit)
|
resp, err := th.App.SubmitInteractiveDialog(submit)
|
||||||
@@ -601,7 +632,8 @@ func TestPostActionRelativeURL(t *testing.T) {
|
|||||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||||
|
|
||||||
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||||
require.Nil(t, err)
|
require.NotNil(t, err)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("valid (but dirty) relative URL with SiteURL set", func(t *testing.T) {
|
t.Run("valid (but dirty) relative URL with SiteURL set", func(t *testing.T) {
|
||||||
@@ -641,7 +673,7 @@ func TestPostActionRelativeURL(t *testing.T) {
|
|||||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||||
|
|
||||||
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||||
require.Nil(t, err)
|
require.NotNil(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("valid relative URL with SiteURL set and no leading slash", func(t *testing.T) {
|
t.Run("valid relative URL with SiteURL set and no leading slash", func(t *testing.T) {
|
||||||
@@ -680,6 +712,200 @@ func TestPostActionRelativeURL(t *testing.T) {
|
|||||||
require.NotEmpty(t, attachments[0].Actions)
|
require.NotEmpty(t, attachments[0].Actions)
|
||||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||||
|
|
||||||
|
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||||
|
require.NotNil(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostActionRelativePluginURL(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
setupPluginApiTest(t,
|
||||||
|
`
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"github.com/mattermost/mattermost-server/plugin"
|
||||||
|
"github.com/mattermost/mattermost-server/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MyPlugin struct {
|
||||||
|
plugin.MattermostPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MyPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
response := &model.PostActionIntegrationResponse{}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(response.ToJson())
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugin.ClientMain(&MyPlugin{})
|
||||||
|
}
|
||||||
|
`, `{"id": "myplugin", "backend": {"executable": "backend.exe"}}`, "myplugin", th.App)
|
||||||
|
|
||||||
|
hooks, err2 := th.App.GetPluginsEnvironment().HooksForPlugin("myplugin")
|
||||||
|
require.Nil(t, err2)
|
||||||
|
require.NotNil(t, hooks)
|
||||||
|
|
||||||
|
t.Run("invalid relative URL", func(t *testing.T) {
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = ""
|
||||||
|
*cfg.ServiceSettings.SiteURL = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
interactivePost := model.Post{
|
||||||
|
Message: "Interactive post",
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
Props: model.StringInterface{
|
||||||
|
"attachments": []*model.SlackAttachment{
|
||||||
|
{
|
||||||
|
Text: "hello",
|
||||||
|
Actions: []*model.PostAction{
|
||||||
|
{
|
||||||
|
Integration: &model.PostActionIntegration{
|
||||||
|
URL: "/notaplugin/some/path",
|
||||||
|
},
|
||||||
|
Name: "action",
|
||||||
|
Type: "some_type",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := th.App.CreatePostAsUser(&interactivePost, "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
attachments, ok := post.Props["attachments"].([]*model.SlackAttachment)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.NotEmpty(t, attachments[0].Actions)
|
||||||
|
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||||
|
|
||||||
|
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||||
|
require.NotNil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid relative URL", func(t *testing.T) {
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = ""
|
||||||
|
*cfg.ServiceSettings.SiteURL = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
interactivePost := model.Post{
|
||||||
|
Message: "Interactive post",
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
Props: model.StringInterface{
|
||||||
|
"attachments": []*model.SlackAttachment{
|
||||||
|
{
|
||||||
|
Text: "hello",
|
||||||
|
Actions: []*model.PostAction{
|
||||||
|
{
|
||||||
|
Integration: &model.PostActionIntegration{
|
||||||
|
URL: "/plugins/myplugin/myaction",
|
||||||
|
},
|
||||||
|
Name: "action",
|
||||||
|
Type: "some_type",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := th.App.CreatePostAsUser(&interactivePost, "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
attachments, ok := post.Props["attachments"].([]*model.SlackAttachment)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.NotEmpty(t, attachments[0].Actions)
|
||||||
|
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||||
|
|
||||||
|
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid (but dirty) relative URL", func(t *testing.T) {
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = ""
|
||||||
|
*cfg.ServiceSettings.SiteURL = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
interactivePost := model.Post{
|
||||||
|
Message: "Interactive post",
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
Props: model.StringInterface{
|
||||||
|
"attachments": []*model.SlackAttachment{
|
||||||
|
{
|
||||||
|
Text: "hello",
|
||||||
|
Actions: []*model.PostAction{
|
||||||
|
{
|
||||||
|
Integration: &model.PostActionIntegration{
|
||||||
|
URL: "//plugins/myplugin///myaction",
|
||||||
|
},
|
||||||
|
Name: "action",
|
||||||
|
Type: "some_type",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := th.App.CreatePostAsUser(&interactivePost, "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
attachments, ok := post.Props["attachments"].([]*model.SlackAttachment)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.NotEmpty(t, attachments[0].Actions)
|
||||||
|
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||||
|
|
||||||
|
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid relative URL and no leading slash", func(t *testing.T) {
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
|
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = ""
|
||||||
|
*cfg.ServiceSettings.SiteURL = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
interactivePost := model.Post{
|
||||||
|
Message: "Interactive post",
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
||||||
|
UserId: th.BasicUser.Id,
|
||||||
|
Props: model.StringInterface{
|
||||||
|
"attachments": []*model.SlackAttachment{
|
||||||
|
{
|
||||||
|
Text: "hello",
|
||||||
|
Actions: []*model.PostAction{
|
||||||
|
{
|
||||||
|
Integration: &model.PostActionIntegration{
|
||||||
|
URL: "plugins/myplugin/myaction",
|
||||||
|
},
|
||||||
|
Name: "action",
|
||||||
|
Type: "some_type",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
post, err := th.App.CreatePostAsUser(&interactivePost, "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
attachments, ok := post.Props["attachments"].([]*model.SlackAttachment)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.NotEmpty(t, attachments[0].Actions)
|
||||||
|
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||||
|
|
||||||
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
_, err = th.App.DoPostAction(post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
})
|
})
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user