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
Этот коммит содержится в:
scott lee davis
2019-11-06 02:39:59 -08:00
коммит произвёл Jesse Hallam
родитель 5f91c14576
Коммит 516017d3c9
2 изменённых файлов: 302 добавлений и 8 удалений

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

@@ -21,6 +21,8 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"io/ioutil"
"net/http"
"net/url"
"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.
// 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) {
inURL, err := url.Parse(rawURL)
if err != nil {
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)
if siteURL != nil && (strings.HasPrefix(rawURLPath, "/plugins/") || strings.HasPrefix(rawURLPath, "plugins/")) {
inURL.Scheme = siteURL.Scheme
inURL.Host = siteURL.Host
inURL.Path = path.Join("/", siteURL.Path, rawURLPath)
rawURL = inURL.String()
if strings.HasPrefix(rawURLPath, "/plugins/") || strings.HasPrefix(rawURLPath, "plugins/") {
return a.DoLocalRequest(rawURLPath, 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
var httpClient *http.Client
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")) {
req.Header.Set(model.HEADER_AUTH, "Bearer "+a.Session.Token)
httpClient = a.HTTPService.MakeClient(true)
@@ -286,6 +286,74 @@ func (a *App) DoActionRequest(rawURL string, body []byte) (*http.Response, *mode
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 {
clientTriggerId, userId, err := request.DecodeAndVerifyTriggerId(a.AsymmetricSigningKey())
if err != nil {

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

@@ -443,6 +443,37 @@ func TestSubmitInteractiveDialog(t *testing.T) {
}))
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
resp, err := th.App.SubmitInteractiveDialog(submit)
@@ -601,7 +632,8 @@ func TestPostActionRelativeURL(t *testing.T) {
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)
require.NotNil(t, err)
})
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)
_, 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) {
@@ -680,6 +712,200 @@ func TestPostActionRelativeURL(t *testing.T) {
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)
})
}
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, "")
require.Nil(t, err)
})