Merge branch 'plugins-2'
Этот коммит содержится в:
95
Gopkg.lock
сгенерированный
95
Gopkg.lock
сгенерированный
@@ -7,6 +7,15 @@
|
||||
revision = "2600fb119af974220d3916a5916d6e31176aac1b"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/alecthomas/template"
|
||||
packages = [
|
||||
".",
|
||||
"parse"
|
||||
]
|
||||
revision = "a0175ee3bccc567396460bf5acd36800cb10c49c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/armon/go-metrics"
|
||||
@@ -112,7 +121,13 @@
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
packages = [
|
||||
"proto",
|
||||
"ptypes",
|
||||
"ptypes/any",
|
||||
"ptypes/duration",
|
||||
"ptypes/timestamp"
|
||||
]
|
||||
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
|
||||
version = "v1.1.0"
|
||||
|
||||
@@ -158,6 +173,12 @@
|
||||
packages = ["."]
|
||||
revision = "7554cd9344cec97297fa6649b055a8c98c2a1e55"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/go-hclog"
|
||||
packages = ["."]
|
||||
revision = "69ff559dc25f3b435631604f573a5fa1efdb6433"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/go-immutable-radix"
|
||||
@@ -176,6 +197,12 @@
|
||||
packages = ["."]
|
||||
revision = "b7773ae218740a7be65057fc60b366a49b538a44"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/go-plugin"
|
||||
packages = ["."]
|
||||
revision = "e8d22c780116115ae5624720c9af0c97afe4f551"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/go-sockaddr"
|
||||
@@ -214,6 +241,12 @@
|
||||
packages = ["."]
|
||||
revision = "2288bf30e9c8d7b5f6549bf62e07120d72fd4b6c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/hashicorp/yamux"
|
||||
packages = ["."]
|
||||
revision = "2658be15c5f05e76244154714161f17e3e77de2e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/icrowley/fake"
|
||||
@@ -330,6 +363,12 @@
|
||||
packages = ["."]
|
||||
revision = "3864e76763d94a6df2f9960b16a20a33da9f9a66"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/go-testing-interface"
|
||||
packages = ["."]
|
||||
revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
@@ -347,6 +386,12 @@
|
||||
revision = "0dc1626d56435e9d605a29875701721c54bc9bbd"
|
||||
version = "v1.10.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/oklog/run"
|
||||
packages = ["."]
|
||||
revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/olekukonko/tablewriter"
|
||||
@@ -581,15 +626,20 @@
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"bpf",
|
||||
"context",
|
||||
"html",
|
||||
"html/atom",
|
||||
"html/charset",
|
||||
"http/httpguts",
|
||||
"http2",
|
||||
"http2/hpack",
|
||||
"idna",
|
||||
"internal/iana",
|
||||
"internal/socket",
|
||||
"internal/timeseries",
|
||||
"ipv4",
|
||||
"ipv6"
|
||||
"ipv6",
|
||||
"trace"
|
||||
]
|
||||
revision = "afe8f62b1d6bbd81f31868121a50b06d8188e1f9"
|
||||
|
||||
@@ -642,6 +692,45 @@
|
||||
revision = "b1f26356af11148e710935ed1ac8a7f5702c7612"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "google.golang.org/genproto"
|
||||
packages = ["googleapis/rpc/status"]
|
||||
revision = "4065a77fc542a455295382a23a996a08ed18813a"
|
||||
|
||||
[[projects]]
|
||||
name = "google.golang.org/grpc"
|
||||
packages = [
|
||||
".",
|
||||
"balancer",
|
||||
"balancer/base",
|
||||
"balancer/roundrobin",
|
||||
"channelz",
|
||||
"codes",
|
||||
"connectivity",
|
||||
"credentials",
|
||||
"encoding",
|
||||
"encoding/proto",
|
||||
"grpclb/grpc_lb_v1/messages",
|
||||
"grpclog",
|
||||
"health",
|
||||
"health/grpc_health_v1",
|
||||
"internal",
|
||||
"keepalive",
|
||||
"metadata",
|
||||
"naming",
|
||||
"peer",
|
||||
"resolver",
|
||||
"resolver/dns",
|
||||
"resolver/passthrough",
|
||||
"stats",
|
||||
"status",
|
||||
"tap",
|
||||
"transport"
|
||||
]
|
||||
revision = "41344da2231b913fa3d983840a57a6b1b7b631a1"
|
||||
version = "v1.12.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "v3"
|
||||
name = "gopkg.in/alexcesaro/quotedprintable.v3"
|
||||
@@ -685,6 +774,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "76d5a63bc94bc0a75fe311cbc1df579fdd77b5d039a006236c00ee122ab69c1b"
|
||||
inputs-digest = "2147cb986b5e1ba46b0c4479601fd9829cc46ccbc1bf4e8b9e59dfe0cccad77e"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
26
Makefile
26
Makefile
@@ -282,31 +282,11 @@ ldap-mocks: ## Creates mock files for ldap.
|
||||
plugin-mocks: ## Creates mock files for plugins.
|
||||
go get github.com/vektra/mockery/...
|
||||
$(GOPATH)/bin/mockery -dir plugin -name API -output plugin/plugintest -outpkg plugintest -case underscore -note 'Regenerate this file using `make plugin-mocks`.'
|
||||
$(GOPATH)/bin/mockery -dir plugin -name KeyValueStore -output plugin/plugintest -outpkg plugintest -case underscore -note 'Regenerate this file using `make plugin-mocks`.'
|
||||
$(GOPATH)/bin/mockery -dir plugin -name API -inpkg -output plugin -testonly -outpkg plugin -case underscore -note 'Regenerate this file using `make plugin-mocks`.'
|
||||
$(GOPATH)/bin/mockery -dir plugin -name Hooks -output plugin/plugintest -outpkg plugintest -case underscore -note 'Regenerate this file using `make plugin-mocks`.'
|
||||
@sed -i'' -e 's|API|APIMOCKINTERNAL|g' plugin/plugintest/api.go
|
||||
|
||||
update-jira-plugin: ## Updates Jira plugin.
|
||||
go get github.com/mattermost/go-bindata/...
|
||||
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep darwin-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
|
||||
$(shell go env GOPATH)/bin/go-bindata -pkg jira -o app/plugin/jira/plugin_darwin_amd64.go plugin.tar.gz
|
||||
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep linux-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
|
||||
$(shell go env GOPATH)/bin/go-bindata -pkg jira -o app/plugin/jira/plugin_linux_amd64.go plugin.tar.gz
|
||||
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-jira/releases/latest | grep browser_download_url | grep windows-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
|
||||
$(shell go env GOPATH)/bin/go-bindata -pkg jira -o app/plugin/jira/plugin_windows_amd64.go plugin.tar.gz
|
||||
rm plugin.tar.gz
|
||||
gofmt -s -w ./app/plugin/jira
|
||||
|
||||
update-zoom-plugin: ## Updates Zoom plugin.
|
||||
go get github.com/mattermost/go-bindata/...
|
||||
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-zoom/releases/latest | grep browser_download_url | grep darwin-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
|
||||
$(shell go env GOPATH)/bin/go-bindata -pkg zoom -o app/plugin/zoom/plugin_darwin_amd64.go plugin.tar.gz
|
||||
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-zoom/releases/latest | grep browser_download_url | grep linux-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
|
||||
$(shell go env GOPATH)/bin/go-bindata -pkg zoom -o app/plugin/zoom/plugin_linux_amd64.go plugin.tar.gz
|
||||
curl -s https://api.github.com/repos/mattermost/mattermost-plugin-zoom/releases/latest | grep browser_download_url | grep windows-amd64 | cut -d '"' -f 4 | wget -qi - -O plugin.tar.gz
|
||||
$(shell go env GOPATH)/bin/go-bindata -pkg zoom -o app/plugin/zoom/plugin_windows_amd64.go plugin.tar.gz
|
||||
rm plugin.tar.gz
|
||||
gofmt -s -w ./app/plugin/zoom
|
||||
pluginapi: ## Generates api and hooks glue code for plugins
|
||||
go generate ./plugin
|
||||
|
||||
check-licenses: ## Checks license status.
|
||||
./scripts/license-check.sh $(TE_PACKAGES) $(EE_PACKAGES)
|
||||
|
||||
@@ -49,6 +49,7 @@ type TestHelper struct {
|
||||
|
||||
SystemAdminClient *model.Client4
|
||||
SystemAdminUser *model.User
|
||||
tempWorkspace string
|
||||
}
|
||||
|
||||
type persistentTestStore struct {
|
||||
@@ -137,6 +138,25 @@ func setupTestHelper(enterprise bool) *TestHelper {
|
||||
|
||||
th.Client = th.CreateClient()
|
||||
th.SystemAdminClient = th.CreateClient()
|
||||
|
||||
if th.tempWorkspace == "" {
|
||||
dir, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
th.tempWorkspace = dir
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(th.tempWorkspace, "plugins")
|
||||
webappDir := filepath.Join(th.tempWorkspace, "webapp")
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Directory = pluginDir
|
||||
*cfg.PluginSettings.ClientDirectory = webappDir
|
||||
})
|
||||
|
||||
th.App.InitPlugins(pluginDir, webappDir)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
|
||||
@@ -1931,9 +1931,6 @@ func TestRemoveChannelMember(t *testing.T) {
|
||||
t.Fatal("should have passed")
|
||||
}
|
||||
|
||||
_, resp = Client.RemoveUserFromChannel(th.BasicChannel.Id, th.BasicUser2.Id)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
_, resp = Client.RemoveUserFromChannel(th.BasicChannel.Id, "junk")
|
||||
CheckBadRequestStatus(t, resp)
|
||||
|
||||
|
||||
@@ -24,11 +24,10 @@ func (api *API) InitPlugin() {
|
||||
api.BaseRoutes.Plugin.Handle("", api.ApiSessionRequired(removePlugin)).Methods("DELETE")
|
||||
|
||||
api.BaseRoutes.Plugins.Handle("/statuses", api.ApiSessionRequired(getPluginStatuses)).Methods("GET")
|
||||
api.BaseRoutes.Plugin.Handle("/activate", api.ApiSessionRequired(activatePlugin)).Methods("POST")
|
||||
api.BaseRoutes.Plugin.Handle("/deactivate", api.ApiSessionRequired(deactivatePlugin)).Methods("POST")
|
||||
api.BaseRoutes.Plugin.Handle("/enable", api.ApiSessionRequired(enablePlugin)).Methods("POST")
|
||||
api.BaseRoutes.Plugin.Handle("/disable", api.ApiSessionRequired(disablePlugin)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.Plugins.Handle("/webapp", api.ApiHandler(getWebappPlugins)).Methods("GET")
|
||||
|
||||
}
|
||||
|
||||
func uploadPlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -165,7 +164,7 @@ func getWebappPlugins(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(model.ManifestListToJson(clientManifests)))
|
||||
}
|
||||
|
||||
func activatePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
func enablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePluginId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
@@ -189,7 +188,7 @@ func activatePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func deactivatePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
func disablePlugin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequirePluginId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
|
||||
@@ -6,7 +6,6 @@ package api4
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -14,18 +13,9 @@ import (
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
pluginDir, err := ioutil.TempDir("", "mm-plugin-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(pluginDir)
|
||||
|
||||
webappDir, err := ioutil.TempDir("", "mm-webapp-test")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(webappDir)
|
||||
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -47,12 +37,6 @@ func TestPlugin(t *testing.T) {
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
th.App.InitPlugins(pluginDir, webappDir, nil)
|
||||
defer func() {
|
||||
th.App.ShutDownPlugins()
|
||||
th.App.PluginEnv = nil
|
||||
}()
|
||||
|
||||
path, _ := utils.FindDir("tests")
|
||||
file, err := os.Open(filepath.Join(path, "testplugin.tar.gz"))
|
||||
if err != nil {
|
||||
@@ -109,7 +93,7 @@ func TestPlugin(t *testing.T) {
|
||||
assert.False(t, found)
|
||||
|
||||
// Successful activate
|
||||
ok, resp := th.SystemAdminClient.ActivatePlugin(manifest.Id)
|
||||
ok, resp := th.SystemAdminClient.EnablePlugin(manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
assert.True(t, ok)
|
||||
|
||||
@@ -126,12 +110,12 @@ func TestPlugin(t *testing.T) {
|
||||
assert.True(t, found)
|
||||
|
||||
// Activate error case
|
||||
ok, resp = th.SystemAdminClient.ActivatePlugin("junk")
|
||||
ok, resp = th.SystemAdminClient.EnablePlugin("junk")
|
||||
CheckBadRequestStatus(t, resp)
|
||||
assert.False(t, ok)
|
||||
|
||||
// Successful deactivate
|
||||
ok, resp = th.SystemAdminClient.DeactivatePlugin(manifest.Id)
|
||||
ok, resp = th.SystemAdminClient.DisablePlugin(manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
assert.True(t, ok)
|
||||
|
||||
@@ -148,7 +132,7 @@ func TestPlugin(t *testing.T) {
|
||||
assert.True(t, found)
|
||||
|
||||
// Deactivate error case
|
||||
ok, resp = th.SystemAdminClient.DeactivatePlugin("junk")
|
||||
ok, resp = th.SystemAdminClient.DisablePlugin("junk")
|
||||
CheckBadRequestStatus(t, resp)
|
||||
assert.False(t, ok)
|
||||
|
||||
@@ -162,7 +146,7 @@ func TestPlugin(t *testing.T) {
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
// Successful webapp get
|
||||
_, resp = th.SystemAdminClient.ActivatePlugin(manifest.Id)
|
||||
_, resp = th.SystemAdminClient.EnablePlugin(manifest.Id)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
manifests, resp := th.Client.GetWebappPlugins()
|
||||
|
||||
10
app/app.go
10
app/app.go
@@ -25,7 +25,7 @@ import (
|
||||
tjobs "github.com/mattermost/mattermost-server/jobs/interfaces"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
@@ -42,10 +42,8 @@ type App struct {
|
||||
|
||||
Log *mlog.Logger
|
||||
|
||||
PluginEnv *pluginenv.Environment
|
||||
PluginConfigListenerId string
|
||||
IsPluginSandboxSupported bool
|
||||
pluginStatuses map[string]*model.PluginStatus
|
||||
Plugins *plugin.Environment
|
||||
PluginConfigListenerId string
|
||||
|
||||
EmailBatching *EmailBatchingJob
|
||||
EmailRateLimiter *throttled.GCRARateLimiter
|
||||
@@ -242,8 +240,6 @@ func New(options ...Option) (outApp *App, outErr error) {
|
||||
handlers: make(map[string]webSocketHandler),
|
||||
}
|
||||
|
||||
app.initBuiltInPlugins()
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,21 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/store/storetest"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
@@ -35,7 +33,6 @@ type TestHelper struct {
|
||||
|
||||
tempConfigPath string
|
||||
tempWorkspace string
|
||||
pluginHooks map[string]plugin.Hooks
|
||||
}
|
||||
|
||||
type persistentTestStore struct {
|
||||
@@ -93,7 +90,6 @@ func setupTestHelper(enterprise bool) *TestHelper {
|
||||
|
||||
th := &TestHelper{
|
||||
App: a,
|
||||
pluginHooks: make(map[string]plugin.Hooks),
|
||||
tempConfigPath: tempConfig.Name(),
|
||||
}
|
||||
|
||||
@@ -123,6 +119,19 @@ func setupTestHelper(enterprise bool) *TestHelper {
|
||||
th.App.SetLicense(nil)
|
||||
}
|
||||
|
||||
if th.tempWorkspace == "" {
|
||||
dir, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
th.tempWorkspace = dir
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(th.tempWorkspace, "plugins")
|
||||
webappDir := filepath.Join(th.tempWorkspace, "webapp")
|
||||
|
||||
th.App.InitPlugins(pluginDir, webappDir)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
@@ -382,65 +391,6 @@ func (me *TestHelper) TearDown() {
|
||||
}
|
||||
}
|
||||
|
||||
type mockPluginSupervisor struct {
|
||||
hooks plugin.Hooks
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Start(api plugin.API) error {
|
||||
return s.hooks.OnActivate(api)
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Wait() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Hooks() plugin.Hooks {
|
||||
return s.hooks
|
||||
}
|
||||
|
||||
func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks) {
|
||||
if me.tempWorkspace == "" {
|
||||
dir, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
me.tempWorkspace = dir
|
||||
}
|
||||
|
||||
manifestCopy := *manifest
|
||||
if manifestCopy.Backend == nil {
|
||||
manifestCopy.Backend = &model.ManifestBackend{}
|
||||
}
|
||||
manifestBytes, err := json.Marshal(&manifestCopy)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(me.tempWorkspace, "plugins")
|
||||
webappDir := filepath.Join(me.tempWorkspace, "webapp")
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(pluginDir, manifest.Id), 0700); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(pluginDir, manifest.Id, "plugin.json"), manifestBytes, 0600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
me.App.InitPlugins(pluginDir, webappDir, func(bundle *model.BundleInfo) (plugin.Supervisor, error) {
|
||||
if hooks, ok := me.pluginHooks[bundle.Manifest.Id]; ok {
|
||||
return &mockPluginSupervisor{hooks}, nil
|
||||
}
|
||||
return pluginenv.DefaultSupervisorProvider(bundle)
|
||||
})
|
||||
|
||||
me.pluginHooks[manifest.Id] = hooks
|
||||
}
|
||||
|
||||
func (me *TestHelper) ResetRoleMigration() {
|
||||
if _, err := testStoreSqlSupplier.GetMaster().Exec("DELETE from Roles"); err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
@@ -183,6 +184,16 @@ func (a *App) CreateChannel(channel *model.Channel, addMember bool) (*model.Chan
|
||||
a.InvalidateCacheForUser(channel.CreatorId)
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ChannelHasBeenCreated(pluginContext, sc)
|
||||
return true
|
||||
}, plugin.ChannelHasBeenCreatedId)
|
||||
})
|
||||
}
|
||||
|
||||
return sc, nil
|
||||
}
|
||||
}
|
||||
@@ -200,6 +211,16 @@ func (a *App) CreateDirectChannel(userId string, otherUserId string) (*model.Cha
|
||||
a.InvalidateCacheForUser(userId)
|
||||
a.InvalidateCacheForUser(otherUserId)
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.ChannelHasBeenCreated(pluginContext, channel)
|
||||
return true
|
||||
}, plugin.ChannelHasBeenCreatedId)
|
||||
})
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil)
|
||||
message.Add("teammate_id", otherUserId)
|
||||
a.Publish(message)
|
||||
@@ -798,6 +819,16 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedChannel(pluginContext, cm, userRequestor)
|
||||
return true
|
||||
}, plugin.UserHasJoinedChannelId)
|
||||
})
|
||||
}
|
||||
|
||||
if userRequestorId == "" || userId == userRequestorId {
|
||||
a.postJoinChannelMessage(user, channel)
|
||||
} else {
|
||||
@@ -1128,10 +1159,21 @@ func (a *App) JoinChannel(channel *model.Channel, userId string) *model.AppError
|
||||
user := uresult.Data.(*model.User)
|
||||
|
||||
if channel.Type == model.CHANNEL_OPEN {
|
||||
if _, err := a.AddUserToChannel(user, channel); err != nil {
|
||||
cm, err := a.AddUserToChannel(user, channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedChannel(pluginContext, cm, nil)
|
||||
return true
|
||||
}, plugin.UserHasJoinedChannelId)
|
||||
})
|
||||
}
|
||||
|
||||
if err := a.postJoinChannelMessage(user, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1312,6 +1354,11 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string,
|
||||
return model.NewAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
cm, err := a.GetChannelMember(channel.Id, userIdToRemove)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmresult := <-a.Srv.Store.Channel().RemoveMember(channel.Id, userIdToRemove); cmresult.Err != nil {
|
||||
return cmresult.Err
|
||||
}
|
||||
@@ -1322,6 +1369,22 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string,
|
||||
a.InvalidateCacheForUser(userIdToRemove)
|
||||
a.InvalidateCacheForChannelMembers(channel.Id)
|
||||
|
||||
if a.PluginsReady() {
|
||||
|
||||
var actorUser *model.User
|
||||
if removerUserId != "" {
|
||||
actorUser, err = a.GetUser(removerUserId)
|
||||
}
|
||||
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasLeftChannel(pluginContext, cm, actorUser)
|
||||
return true
|
||||
}, plugin.UserHasLeftChannelId)
|
||||
})
|
||||
}
|
||||
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil)
|
||||
message.Add("user_id", userIdToRemove)
|
||||
message.Add("remover_id", removerUserId)
|
||||
@@ -1338,6 +1401,7 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string,
|
||||
|
||||
func (a *App) RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
|
||||
var err *model.AppError
|
||||
|
||||
if err = a.removeUserFromChannel(userIdToRemove, removerUserId, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1350,6 +1414,11 @@ func (a *App) RemoveUserFromChannel(userIdToRemove string, removerUserId string,
|
||||
if userIdToRemove == removerUserId {
|
||||
a.postLeaveChannelMessage(user, channel)
|
||||
} else {
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.Go(func() {
|
||||
a.postRemoveFromChannelMessage(removerUserId, user, channel)
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"runtime"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
@@ -98,13 +97,7 @@ func pluginSetting(pluginSettings *model.PluginSettings, plugin, key string, def
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if b, err := json.Marshal(settings); err != nil {
|
||||
return defaultValue
|
||||
} else {
|
||||
json.Unmarshal(b, &m)
|
||||
}
|
||||
if value, ok := m[key]; ok {
|
||||
if value, ok := settings[key]; ok {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
@@ -570,59 +563,60 @@ func (a *App) trackLicense() {
|
||||
}
|
||||
|
||||
func (a *App) trackPlugins() {
|
||||
if *a.Config().PluginSettings.Enable {
|
||||
totalActiveCount := -1 // -1 to indicate disabled or error
|
||||
webappActiveCount := 0
|
||||
backendActiveCount := 0
|
||||
totalInactiveCount := -1 // -1 to indicate disabled or error
|
||||
webappInactiveCount := 0
|
||||
backendInactiveCount := 0
|
||||
if a.PluginsReady() {
|
||||
totalEnabledCount := 0
|
||||
webappEnabledCount := 0
|
||||
backendEnabledCount := 0
|
||||
totalDisabledCount := 0
|
||||
webappDisabledCount := 0
|
||||
backendDisabledCount := 0
|
||||
brokenManifestCount := 0
|
||||
settingsCount := 0
|
||||
|
||||
plugins, _ := a.GetPlugins()
|
||||
pluginStates := a.Config().PluginSettings.PluginStates
|
||||
plugins, _ := a.Plugins.Available()
|
||||
|
||||
if plugins != nil {
|
||||
totalActiveCount = len(plugins.Active)
|
||||
|
||||
for _, plugin := range plugins.Active {
|
||||
if plugin.Webapp != nil {
|
||||
webappActiveCount += 1
|
||||
if pluginStates != nil && plugins != nil {
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil {
|
||||
brokenManifestCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if plugin.Backend != nil {
|
||||
backendActiveCount += 1
|
||||
if state, ok := pluginStates[plugin.Manifest.Id]; ok && state.Enable {
|
||||
totalEnabledCount += 1
|
||||
if plugin.Manifest.Backend != nil {
|
||||
backendEnabledCount += 1
|
||||
}
|
||||
if plugin.Manifest.Webapp != nil {
|
||||
webappEnabledCount += 1
|
||||
}
|
||||
} else {
|
||||
totalDisabledCount += 1
|
||||
if plugin.Manifest.Backend != nil {
|
||||
backendDisabledCount += 1
|
||||
}
|
||||
if plugin.Manifest.Webapp != nil {
|
||||
webappDisabledCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
if plugin.SettingsSchema != nil {
|
||||
settingsCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
totalInactiveCount = len(plugins.Inactive)
|
||||
|
||||
for _, plugin := range plugins.Inactive {
|
||||
if plugin.Webapp != nil {
|
||||
webappInactiveCount += 1
|
||||
}
|
||||
|
||||
if plugin.Backend != nil {
|
||||
backendInactiveCount += 1
|
||||
}
|
||||
|
||||
if plugin.SettingsSchema != nil {
|
||||
if plugin.Manifest.SettingsSchema != nil {
|
||||
settingsCount += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
totalEnabledCount = -1 // -1 to indicate disabled or error
|
||||
totalDisabledCount = -1 // -1 to indicate disabled or error
|
||||
}
|
||||
|
||||
a.SendDiagnostic(TRACK_PLUGINS, map[string]interface{}{
|
||||
"active_plugins": totalActiveCount,
|
||||
"active_webapp_plugins": webappActiveCount,
|
||||
"active_backend_plugins": backendActiveCount,
|
||||
"inactive_plugins": totalInactiveCount,
|
||||
"inactive_webapp_plugins": webappInactiveCount,
|
||||
"inactive_backend_plugins": backendInactiveCount,
|
||||
"plugins_with_settings": settingsCount,
|
||||
"enabled_plugins": totalEnabledCount,
|
||||
"enabled_webapp_plugins": webappEnabledCount,
|
||||
"enabled_backend_plugins": backendEnabledCount,
|
||||
"disabled_plugins": totalDisabledCount,
|
||||
"disabled_webapp_plugins": webappDisabledCount,
|
||||
"disabled_backend_plugins": backendDisabledCount,
|
||||
"plugins_with_settings": settingsCount,
|
||||
"plugins_with_broken_manifests": brokenManifestCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ func newTestServer() (chan string, *httptest.Server) {
|
||||
|
||||
func TestPluginSetting(t *testing.T) {
|
||||
settings := &model.PluginSettings{
|
||||
Plugins: map[string]interface{}{
|
||||
"test": map[string]string{
|
||||
Plugins: map[string]map[string]interface{}{
|
||||
"test": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
|
||||
895
app/plugin.go
895
app/plugin.go
@@ -4,356 +4,135 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
|
||||
builtinplugin "github.com/mattermost/mattermost-server/app/plugin"
|
||||
"github.com/mattermost/mattermost-server/app/plugin/jira"
|
||||
"github.com/mattermost/mattermost-server/app/plugin/ldapextras"
|
||||
"github.com/mattermost/mattermost-server/app/plugin/zoom"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin/sandbox"
|
||||
)
|
||||
|
||||
var prepackagedPlugins map[string]func(string) ([]byte, error) = map[string]func(string) ([]byte, error){
|
||||
"jira": jira.Asset,
|
||||
"zoom": zoom.Asset,
|
||||
}
|
||||
|
||||
func (a *App) notifyPluginStatusesChanged() error {
|
||||
pluginStatuses, err := a.GetClusterPluginStatuses()
|
||||
if err != nil {
|
||||
return err
|
||||
func (a *App) SyncPluginsActiveState() {
|
||||
if a.Plugins == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Notify any system admins.
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED, "", "", "", nil)
|
||||
message.Add("plugin_statuses", pluginStatuses)
|
||||
message.Broadcast.ContainsSensitiveData = true
|
||||
a.Publish(message)
|
||||
config := a.Config().PluginSettings
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) setPluginStatusState(id string, state int) error {
|
||||
if _, ok := a.pluginStatuses[id]; !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.pluginStatuses[id].State = state
|
||||
|
||||
return a.notifyPluginStatusesChanged()
|
||||
}
|
||||
|
||||
func (a *App) initBuiltInPlugins() {
|
||||
plugins := map[string]builtinplugin.Plugin{
|
||||
"ldapextras": &ldapextras.Plugin{},
|
||||
}
|
||||
for id, p := range plugins {
|
||||
mlog.Debug("Initializing built-in plugin", mlog.String("plugin_id", id))
|
||||
api := &BuiltInPluginAPI{
|
||||
id: id,
|
||||
router: a.Srv.Router.PathPrefix("/plugins/" + id).Subrouter(),
|
||||
app: a,
|
||||
if *config.Enable {
|
||||
availablePlugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
a.Log.Error("Unable to get available plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
p.Initialize(api)
|
||||
}
|
||||
a.AddConfigListener(func(before, after *model.Config) {
|
||||
for _, p := range plugins {
|
||||
p.OnConfigurationChange()
|
||||
|
||||
// Deactivate any plugins that have been disabled.
|
||||
for _, plugin := range a.Plugins.Active() {
|
||||
// Determine if plugin is enabled
|
||||
pluginId := plugin.Manifest.Id
|
||||
pluginEnabled := false
|
||||
if state, ok := config.PluginStates[pluginId]; ok {
|
||||
pluginEnabled = state.Enable
|
||||
}
|
||||
|
||||
// If it's not enabled we need to deactivate it
|
||||
if !pluginEnabled {
|
||||
a.Plugins.Deactivate(pluginId)
|
||||
}
|
||||
}
|
||||
|
||||
// Activate any plugins that have been enabled
|
||||
for _, plugin := range availablePlugins {
|
||||
if plugin.Manifest == nil {
|
||||
plugin.WrapLogger(a.Log).Error("Plugin manifest could not be loaded", mlog.Err(plugin.ManifestError))
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine if plugin is enabled
|
||||
pluginId := plugin.Manifest.Id
|
||||
pluginEnabled := false
|
||||
if state, ok := config.PluginStates[pluginId]; ok {
|
||||
pluginEnabled = state.Enable
|
||||
}
|
||||
|
||||
// Activate plugin if enabled
|
||||
if pluginEnabled {
|
||||
if err := a.Plugins.Activate(pluginId); err != nil {
|
||||
plugin.WrapLogger(a.Log).Error("Unable to activate plugin", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // If plugins are disabled, shutdown plugins.
|
||||
a.Plugins.Shutdown()
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) NewPluginAPI(manifest *model.Manifest) plugin.API {
|
||||
return NewPluginAPI(a, manifest)
|
||||
}
|
||||
|
||||
func (a *App) InitPlugins(pluginDir, webappPluginDir string) {
|
||||
if a.Plugins != nil || !*a.Config().PluginSettings.Enable {
|
||||
a.SyncPluginsActiveState()
|
||||
return
|
||||
}
|
||||
|
||||
a.Log.Info("Starting up plugins")
|
||||
|
||||
if err := os.Mkdir(pluginDir, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Mkdir(webappPluginDir, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
if env, err := plugin.NewEnvironment(a.NewPluginAPI, pluginDir, webappPluginDir, a.Log); err != nil {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
} else {
|
||||
a.Plugins = env
|
||||
}
|
||||
|
||||
// Sync plugin active state when config changes. Also notify plugins.
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = a.AddConfigListener(func(*model.Config, *model.Config) {
|
||||
a.SyncPluginsActiveState()
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.OnConfigurationChange()
|
||||
return true
|
||||
}, plugin.OnConfigurationChangeId)
|
||||
})
|
||||
for _, p := range plugins {
|
||||
p.OnConfigurationChange()
|
||||
}
|
||||
|
||||
a.SyncPluginsActiveState()
|
||||
}
|
||||
|
||||
func (a *App) setPluginsActive(activate bool) {
|
||||
if a.PluginEnv == nil {
|
||||
mlog.Error(fmt.Sprintf("Cannot setPluginsActive(%t): plugin env not initialized", activate))
|
||||
func (a *App) ShutDownPlugins() {
|
||||
if a.Plugins == nil {
|
||||
return
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
mlog.Error(fmt.Sprintf("Cannot setPluginsActive(%t)", activate), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
mlog.Info("Shutting down plugins")
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
a.Plugins.Shutdown()
|
||||
|
||||
enabled := false
|
||||
if state, ok := a.Config().PluginSettings.PluginStates[plugin.Manifest.Id]; ok {
|
||||
enabled = state.Enable
|
||||
}
|
||||
|
||||
a.pluginStatuses[plugin.Manifest.Id] = &model.PluginStatus{
|
||||
ClusterId: a.GetClusterId(),
|
||||
PluginId: plugin.Manifest.Id,
|
||||
PluginPath: filepath.Dir(plugin.ManifestPath),
|
||||
IsSandboxed: a.IsPluginSandboxSupported,
|
||||
Name: plugin.Manifest.Name,
|
||||
Description: plugin.Manifest.Description,
|
||||
Version: plugin.Manifest.Version,
|
||||
}
|
||||
|
||||
if activate && enabled {
|
||||
a.setPluginActive(plugin, activate)
|
||||
} else if !activate {
|
||||
a.setPluginActive(plugin, activate)
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) setPluginActiveById(id string, activate bool) {
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
mlog.Error(fmt.Sprintf("Cannot setPluginActiveById(%t)", activate), mlog.String("plugin_id", id), mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest != nil && plugin.Manifest.Id == id {
|
||||
a.setPluginActive(plugin, activate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) setPluginActive(plugin *model.BundleInfo, activate bool) {
|
||||
if plugin.Manifest == nil {
|
||||
return
|
||||
}
|
||||
|
||||
id := plugin.Manifest.Id
|
||||
|
||||
active := a.PluginEnv.IsPluginActive(id)
|
||||
|
||||
if activate {
|
||||
if !active {
|
||||
if err := a.activatePlugin(plugin.Manifest); err != nil {
|
||||
mlog.Error("Plugin failed to activate", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.DetailedError))
|
||||
}
|
||||
}
|
||||
|
||||
} else if !activate {
|
||||
if active {
|
||||
if err := a.deactivatePlugin(plugin.Manifest); err != nil {
|
||||
mlog.Error("Plugin failed to deactivate", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.DetailedError))
|
||||
}
|
||||
} else {
|
||||
if err := a.setPluginStatusState(plugin.Manifest.Id, model.PluginStateNotRunning); err != nil {
|
||||
mlog.Error("Plugin status state failed to update", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) activatePlugin(manifest *model.Manifest) *model.AppError {
|
||||
mlog.Debug("Activating plugin", mlog.String("plugin_id", manifest.Id))
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStarting); err != nil {
|
||||
return model.NewAppError("activatePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
onError := func(err error) {
|
||||
mlog.Debug("Plugin failed to stay running", mlog.String("plugin_id", manifest.Id), mlog.Err(err))
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateFailedToStayRunning); err != nil {
|
||||
mlog.Error("Failed to record plugin status", mlog.String("plugin_id", manifest.Id), mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.PluginEnv.ActivatePlugin(manifest.Id, onError); err != nil {
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateFailedToStart); err != nil {
|
||||
return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateRunning); err != nil {
|
||||
return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ACTIVATED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
mlog.Info("Activated plugin", mlog.String("plugin_id", manifest.Id))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError {
|
||||
mlog.Debug("Deactivating plugin", mlog.String("plugin_id", manifest.Id))
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStopping); err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if err := a.PluginEnv.DeactivatePlugin(manifest.Id); err != nil {
|
||||
return model.NewAppError("deactivatePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
a.UnregisterPluginCommands(manifest.Id)
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DEACTIVATED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateNotRunning); err != nil {
|
||||
return model.NewAppError("deactivatePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
mlog.Info("Deactivated plugin", mlog.String("plugin_id", manifest.Id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallPlugin unpacks and installs a plugin but does not activate it.
|
||||
func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
|
||||
return a.installPlugin(pluginFile, false)
|
||||
}
|
||||
|
||||
func (a *App) installPlugin(pluginFile io.Reader, allowPrepackaged bool) (*model.Manifest, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "plugintmp")
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
tmpPluginDir := tmpDir
|
||||
dir, err := ioutil.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(dir) == 1 && dir[0].IsDir() {
|
||||
tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name())
|
||||
}
|
||||
|
||||
manifest, _, err := model.FindManifest(tmpPluginDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
_, isPrepackaged := prepackagedPlugins[manifest.Id]
|
||||
if isPrepackaged && !allowPrepackaged {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !plugin.IsValidId(manifest.Id) {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidId.String()}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
bundles, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Manifest != nil && bundle.Manifest.Id == manifest.Id {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(a.PluginEnv.SearchPath(), manifest.Id)
|
||||
err = utils.CopyDir(tmpPluginDir, pluginPath)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.pluginStatuses[manifest.Id] = &model.PluginStatus{
|
||||
ClusterId: a.GetClusterId(),
|
||||
PluginId: manifest.Id,
|
||||
PluginPath: pluginPath,
|
||||
State: model.PluginStateNotRunning,
|
||||
IsSandboxed: a.IsPluginSandboxSupported,
|
||||
IsPrepackaged: isPrepackaged,
|
||||
Name: manifest.Name,
|
||||
Description: manifest.Description,
|
||||
Version: manifest.Version,
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// GetPlugins returned the plugins installed on this server, including the manifests needed to
|
||||
// enable plugins with web functionality.
|
||||
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}}
|
||||
for _, plugin := range plugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info := &model.PluginInfo{
|
||||
Manifest: *plugin.Manifest,
|
||||
}
|
||||
_, info.Prepackaged = prepackagedPlugins[plugin.Manifest.Id]
|
||||
if a.PluginEnv.IsPluginActive(plugin.Manifest.Id) {
|
||||
resp.Active = append(resp.Active, info)
|
||||
} else {
|
||||
resp.Inactive = append(resp.Inactive, info)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = ""
|
||||
a.Plugins = nil
|
||||
}
|
||||
|
||||
func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetActivePluginManifests", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins := a.PluginEnv.ActivePlugins()
|
||||
plugins := a.Plugins.Active()
|
||||
|
||||
manifests := make([]*model.Manifest, len(plugins))
|
||||
for i, plugin := range plugins {
|
||||
@@ -363,99 +142,14 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
// GetPluginStatuses returns the status for plugins installed on this server.
|
||||
func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
if !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
pluginStatuses := make([]*model.PluginStatus, 0, len(a.pluginStatuses))
|
||||
for _, pluginStatus := range a.pluginStatuses {
|
||||
pluginStatuses = append(pluginStatuses, pluginStatus)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
// GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster.
|
||||
func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
pluginStatuses, err := a.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.Cluster != nil && *a.Config().ClusterSettings.Enable {
|
||||
clusterPluginStatuses, err := a.Cluster.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
pluginStatuses = append(pluginStatuses, clusterPluginStatuses...)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
return a.removePlugin(id, false)
|
||||
}
|
||||
|
||||
func (a *App) removePlugin(id string, allowPrepackaged bool) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
if _, ok := prepackagedPlugins[id]; ok && !allowPrepackaged {
|
||||
return model.NewAppError("removePlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
var manifest *model.Manifest
|
||||
var pluginPath string
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
pluginPath = filepath.Dir(p.ManifestPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if a.PluginEnv.IsPluginActive(id) {
|
||||
err := a.deactivatePlugin(manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = os.RemoveAll(pluginPath)
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
delete(a.pluginStatuses, manifest.Id)
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnablePlugin will set the config for an installed plugin to enabled, triggering asynchronous
|
||||
// activation if inactive anywhere in the cluster.
|
||||
func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
plugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -472,14 +166,17 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStarting); err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_ENABLED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
// This call will cause SyncPluginsActiveState to be called and the plugin to be activated
|
||||
if err := a.SaveConfig(a.Config(), true); err != nil {
|
||||
if err.Id == "ent.cluster.save_config.error" {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.cluster.save_config.app_error", nil, "", http.StatusInternalServerError)
|
||||
@@ -492,11 +189,11 @@ func (a *App) EnablePlugin(id string) *model.AppError {
|
||||
|
||||
// DisablePlugin will set the config for an installed plugin to disabled, triggering deactivation if active.
|
||||
func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.PluginEnv.Plugins()
|
||||
plugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -513,14 +210,16 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.setPluginStatusState(manifest.Id, model.PluginStateStopping); err != nil {
|
||||
return model.NewAppError("EnablePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
a.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false}
|
||||
})
|
||||
|
||||
if manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DISABLED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
if err := a.SaveConfig(a.Config(), true); err != nil {
|
||||
return model.NewAppError("DisablePlugin", "app.plugin.config.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -528,331 +227,35 @@ func (a *App) DisablePlugin(id string) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride pluginenv.SupervisorProviderFunc) {
|
||||
if a.PluginEnv != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !*a.Config().PluginSettings.Enable {
|
||||
return
|
||||
}
|
||||
|
||||
mlog.Info("Starting up plugins")
|
||||
|
||||
a.pluginStatuses = make(map[string]*model.PluginStatus)
|
||||
|
||||
if err := os.Mkdir(pluginPath, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Mkdir(webappPath, 0744); err != nil && !os.IsExist(err) {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
|
||||
options := []pluginenv.Option{
|
||||
pluginenv.SearchPath(pluginPath),
|
||||
pluginenv.WebappPath(webappPath),
|
||||
pluginenv.APIProvider(func(m *model.Manifest) (plugin.API, error) {
|
||||
return &PluginAPI{
|
||||
id: m.Id,
|
||||
app: a,
|
||||
keyValueStore: &PluginKeyValueStore{
|
||||
id: m.Id,
|
||||
app: a,
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
a.IsPluginSandboxSupported = sandbox.CheckSupport() == nil
|
||||
|
||||
if supervisorOverride != nil {
|
||||
options = append(options, pluginenv.SupervisorProvider(supervisorOverride))
|
||||
} else if a.IsPluginSandboxSupported {
|
||||
options = append(options, pluginenv.SupervisorProvider(sandbox.SupervisorProvider))
|
||||
} else {
|
||||
options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider))
|
||||
}
|
||||
|
||||
if env, err := pluginenv.New(options...); err != nil {
|
||||
mlog.Error("Failed to start up plugins", mlog.Err(err))
|
||||
return
|
||||
} else {
|
||||
a.PluginEnv = env
|
||||
}
|
||||
|
||||
for id, asset := range prepackagedPlugins {
|
||||
if tarball, err := asset("plugin.tar.gz"); err != nil {
|
||||
mlog.Error("Failed to install prepackaged plugin", mlog.Err(err))
|
||||
} else if tarball != nil {
|
||||
a.removePlugin(id, true)
|
||||
if _, err := a.installPlugin(bytes.NewReader(tarball), true); err != nil {
|
||||
mlog.Error("Failed to install prepackaged plugin", mlog.Err(err))
|
||||
}
|
||||
if _, ok := a.Config().PluginSettings.PluginStates[id]; !ok && id != "zoom" {
|
||||
if err := a.EnablePlugin(id); err != nil {
|
||||
mlog.Error("Failed to enable prepackaged plugin", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = a.AddConfigListener(func(oldCfg *model.Config, cfg *model.Config) {
|
||||
if a.PluginEnv == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if *oldCfg.PluginSettings.Enable != *cfg.PluginSettings.Enable {
|
||||
a.setPluginsActive(*cfg.PluginSettings.Enable)
|
||||
} else {
|
||||
plugins := map[string]bool{}
|
||||
for id := range oldCfg.PluginSettings.PluginStates {
|
||||
plugins[id] = true
|
||||
}
|
||||
for id := range cfg.PluginSettings.PluginStates {
|
||||
plugins[id] = true
|
||||
}
|
||||
|
||||
for id := range plugins {
|
||||
oldPluginState := oldCfg.PluginSettings.PluginStates[id]
|
||||
pluginState := cfg.PluginSettings.PluginStates[id]
|
||||
|
||||
wasEnabled := oldPluginState != nil && oldPluginState.Enable
|
||||
isEnabled := pluginState != nil && pluginState.Enable
|
||||
|
||||
if wasEnabled != isEnabled {
|
||||
a.setPluginActiveById(id, isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, err := range a.PluginEnv.Hooks().OnConfigurationChange() {
|
||||
mlog.Error(err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
a.setPluginsActive(true)
|
||||
}
|
||||
|
||||
func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable {
|
||||
err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented)
|
||||
mlog.Error(err.Error())
|
||||
w.WriteHeader(err.StatusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(err.ToJson()))
|
||||
return
|
||||
}
|
||||
|
||||
a.servePluginRequest(w, r, a.PluginEnv.Hooks().ServeHTTP)
|
||||
}
|
||||
|
||||
func (a *App) servePluginRequest(w http.ResponseWriter, r *http.Request, handler http.HandlerFunc) {
|
||||
token := ""
|
||||
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if strings.HasPrefix(strings.ToUpper(authHeader), model.HEADER_BEARER+" ") {
|
||||
token = authHeader[len(model.HEADER_BEARER)+1:]
|
||||
} else if strings.HasPrefix(strings.ToLower(authHeader), model.HEADER_TOKEN+" ") {
|
||||
token = authHeader[len(model.HEADER_TOKEN)+1:]
|
||||
} else if cookie, _ := r.Cookie(model.SESSION_COOKIE_TOKEN); cookie != nil && (r.Method == "GET" || r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML) {
|
||||
token = cookie.Value
|
||||
} else {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
}
|
||||
|
||||
r.Header.Del("Mattermost-User-Id")
|
||||
if token != "" {
|
||||
if session, err := a.GetSession(token); session != nil && err == nil {
|
||||
r.Header.Set("Mattermost-User-Id", session.UserId)
|
||||
}
|
||||
}
|
||||
|
||||
cookies := r.Cookies()
|
||||
r.Header.Del("Cookie")
|
||||
for _, c := range cookies {
|
||||
if c.Name != model.SESSION_COOKIE_TOKEN {
|
||||
r.AddCookie(c)
|
||||
}
|
||||
}
|
||||
r.Header.Del(model.HEADER_AUTH)
|
||||
r.Header.Del("Referer")
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
newQuery := r.URL.Query()
|
||||
newQuery.Del("access_token")
|
||||
r.URL.RawQuery = newQuery.Encode()
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/plugins/"+params["plugin_id"])
|
||||
|
||||
handler(w, r.WithContext(context.WithValue(r.Context(), "plugin_id", params["plugin_id"])))
|
||||
}
|
||||
|
||||
func (a *App) ShutDownPlugins() {
|
||||
if a.PluginEnv == nil {
|
||||
return
|
||||
}
|
||||
|
||||
mlog.Info("Shutting down plugins")
|
||||
|
||||
for _, err := range a.PluginEnv.Shutdown() {
|
||||
mlog.Error(err.Error())
|
||||
}
|
||||
a.RemoveConfigListener(a.PluginConfigListenerId)
|
||||
a.PluginConfigListenerId = ""
|
||||
a.PluginEnv = nil
|
||||
}
|
||||
|
||||
func getKeyHash(key string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(key))
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (a *App) SetPluginKey(pluginId string, key string, value []byte) *model.AppError {
|
||||
kv := &model.PluginKeyValue{
|
||||
PluginId: pluginId,
|
||||
Key: getKeyHash(key),
|
||||
Value: value,
|
||||
}
|
||||
|
||||
result := <-a.Srv.Store.Plugin().SaveOrUpdate(kv)
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
|
||||
func (a *App) GetPluginKey(pluginId string, key string) ([]byte, *model.AppError) {
|
||||
result := <-a.Srv.Store.Plugin().Get(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
if result.Err.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
mlog.Error(result.Err.Error())
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
kv := result.Data.(*model.PluginKeyValue)
|
||||
|
||||
return kv.Value, nil
|
||||
}
|
||||
|
||||
func (a *App) DeletePluginKey(pluginId string, key string) *model.AppError {
|
||||
result := <-a.Srv.Store.Plugin().Delete(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
|
||||
type PluginCommand struct {
|
||||
Command *model.Command
|
||||
PluginId string
|
||||
}
|
||||
|
||||
func (a *App) RegisterPluginCommand(pluginId string, command *model.Command) error {
|
||||
if command.Trigger == "" {
|
||||
return fmt.Errorf("invalid command")
|
||||
}
|
||||
|
||||
command = &model.Command{
|
||||
Trigger: strings.ToLower(command.Trigger),
|
||||
TeamId: command.TeamId,
|
||||
AutoComplete: command.AutoComplete,
|
||||
AutoCompleteDesc: command.AutoCompleteDesc,
|
||||
AutoCompleteHint: command.AutoCompleteHint,
|
||||
DisplayName: command.DisplayName,
|
||||
}
|
||||
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId {
|
||||
if pc.PluginId == pluginId {
|
||||
pc.Command = command
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.pluginCommands = append(a.pluginCommands, &PluginCommand{
|
||||
Command: command,
|
||||
PluginId: pluginId,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UnregisterPluginCommand(pluginId, teamId, trigger string) {
|
||||
trigger = strings.ToLower(trigger)
|
||||
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
var remaining []*PluginCommand
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.TeamId != teamId || pc.Command.Trigger != trigger {
|
||||
remaining = append(remaining, pc)
|
||||
}
|
||||
}
|
||||
a.pluginCommands = remaining
|
||||
}
|
||||
|
||||
func (a *App) UnregisterPluginCommands(pluginId string) {
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
var remaining []*PluginCommand
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.PluginId != pluginId {
|
||||
remaining = append(remaining, pc)
|
||||
}
|
||||
}
|
||||
a.pluginCommands = remaining
|
||||
}
|
||||
|
||||
func (a *App) PluginCommandsForTeam(teamId string) []*model.Command {
|
||||
a.pluginCommandsLock.RLock()
|
||||
defer a.pluginCommandsLock.RUnlock()
|
||||
|
||||
var commands []*model.Command
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.TeamId == "" || pc.Command.TeamId == teamId {
|
||||
commands = append(commands, pc.Command)
|
||||
}
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func (a *App) ExecutePluginCommand(args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
parts := strings.Split(args.Command, " ")
|
||||
trigger := parts[0][1:]
|
||||
trigger = strings.ToLower(trigger)
|
||||
|
||||
a.pluginCommandsLock.RLock()
|
||||
defer a.pluginCommandsLock.RUnlock()
|
||||
|
||||
for _, pc := range a.pluginCommands {
|
||||
if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger {
|
||||
response, appErr, err := a.PluginEnv.HooksForPlugin(pc.PluginId).ExecuteCommand(args)
|
||||
if err != nil {
|
||||
return pc.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return pc.Command, response, appErr
|
||||
}
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (a *App) PluginsReady() bool {
|
||||
return a.PluginEnv != nil && *a.Config().PluginSettings.Enable
|
||||
return a.Plugins != nil && *a.Config().PluginSettings.Enable
|
||||
}
|
||||
|
||||
func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
if !a.PluginsReady() {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
availablePlugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPlugins", "app.plugin.get_plugins.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
resp := &model.PluginsResponse{Active: []*model.PluginInfo{}, Inactive: []*model.PluginInfo{}}
|
||||
for _, plugin := range availablePlugins {
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info := &model.PluginInfo{
|
||||
Manifest: *plugin.Manifest,
|
||||
}
|
||||
|
||||
if a.Plugins.IsActive(plugin.Manifest.Id) {
|
||||
resp.Active = append(resp.Active, info)
|
||||
} else {
|
||||
resp.Inactive = append(resp.Inactive, info)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
type API interface {
|
||||
// Loads the plugin's configuration
|
||||
LoadPluginConfiguration(dest interface{}) error
|
||||
|
||||
// The plugin's router
|
||||
PluginRouter() *mux.Router
|
||||
|
||||
// Gets a team by its name
|
||||
GetTeamByName(name string) (*model.Team, *model.AppError)
|
||||
|
||||
// Gets a user by its name
|
||||
GetUserByName(name string) (*model.User, *model.AppError)
|
||||
|
||||
// Gets a channel by its name
|
||||
GetChannelByName(teamId, name string) (*model.Channel, *model.AppError)
|
||||
|
||||
// Gets a direct message channel
|
||||
GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError)
|
||||
|
||||
// Creates a post
|
||||
CreatePost(post *model.Post) (*model.Post, *model.AppError)
|
||||
|
||||
// Get LDAP attributes for a user
|
||||
GetLdapUserAttributes(userId string, attributes []string) (map[string]string, *model.AppError)
|
||||
|
||||
// Temporary for built-in plugins, copied from api4/context.go ServeHTTP function.
|
||||
// If a request has a valid token for an active session, the session is returned otherwise
|
||||
// it errors.
|
||||
GetSessionFromRequest(r *http.Request) (*model.Session, *model.AppError)
|
||||
|
||||
// Returns a localized string. If a request is given, its headers will be used to pick a locale.
|
||||
I18n(id string, r *http.Request) string
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
// Base provides default implementations for hooks.
|
||||
type Base struct{}
|
||||
|
||||
func (b *Base) OnConfigurationChange() {}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
// All implementations should be safe for concurrent use.
|
||||
type Hooks interface {
|
||||
// Invoked when configuration changes may have been made
|
||||
OnConfigurationChange()
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// +build !amd64 !darwin,!linux,!windows
|
||||
|
||||
package jira
|
||||
|
||||
func Asset(name string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package ldapextras
|
||||
|
||||
type Configuration struct {
|
||||
Enabled bool
|
||||
Attributes []string
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package ldapextras
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/mattermost/mattermost-server/app/plugin"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
type Plugin struct {
|
||||
plugin.Base
|
||||
api plugin.API
|
||||
configuration atomic.Value
|
||||
}
|
||||
|
||||
func (p *Plugin) Initialize(api plugin.API) {
|
||||
p.api = api
|
||||
p.OnConfigurationChange()
|
||||
api.PluginRouter().HandleFunc("/users/{user_id:[A-Za-z0-9]+}/attributes", p.handleGetAttributes).Methods("GET")
|
||||
}
|
||||
|
||||
func (p *Plugin) config() *Configuration {
|
||||
return p.configuration.Load().(*Configuration)
|
||||
}
|
||||
|
||||
func (p *Plugin) OnConfigurationChange() {
|
||||
var configuration Configuration
|
||||
if err := p.api.LoadPluginConfiguration(&configuration); err != nil {
|
||||
mlog.Error(err.Error())
|
||||
}
|
||||
p.configuration.Store(&configuration)
|
||||
}
|
||||
|
||||
func (p *Plugin) handleGetAttributes(w http.ResponseWriter, r *http.Request) {
|
||||
config := p.config()
|
||||
if !config.Enabled || len(config.Attributes) == 0 {
|
||||
http.Error(w, "This plugin is not configured", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := p.api.GetSessionFromRequest(r)
|
||||
|
||||
if session == nil || err != nil {
|
||||
http.Error(w, "Invalid session", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Only requires a valid session, no other permission checks required
|
||||
|
||||
params := mux.Vars(r)
|
||||
id := params["user_id"]
|
||||
|
||||
if len(id) != 26 {
|
||||
http.Error(w, "Invalid user id", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
attributes, err := p.api.GetLdapUserAttributes(id, config.Attributes)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Errored getting attributes: %v", err.Error()), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.Write([]byte(model.MapToJson(attributes)))
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
type Plugin interface {
|
||||
Initialize(API)
|
||||
Hooks
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// +build !amd64 !darwin,!linux,!windows
|
||||
|
||||
package zoom
|
||||
|
||||
func Asset(name string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -5,32 +5,48 @@ package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
type PluginAPI struct {
|
||||
id string
|
||||
app *App
|
||||
keyValueStore *PluginKeyValueStore
|
||||
id string
|
||||
app *App
|
||||
logger *mlog.SugarLogger
|
||||
manifest *model.Manifest
|
||||
}
|
||||
|
||||
type PluginKeyValueStore struct {
|
||||
id string
|
||||
app *App
|
||||
func NewPluginAPI(a *App, manifest *model.Manifest) *PluginAPI {
|
||||
return &PluginAPI{
|
||||
id: manifest.Id,
|
||||
manifest: manifest,
|
||||
app: a,
|
||||
logger: a.Log.With(mlog.String("plugin_id", manifest.Id)).Sugar(),
|
||||
}
|
||||
}
|
||||
|
||||
func (api *PluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
|
||||
finalConfig := make(map[string]interface{})
|
||||
|
||||
// First set final config to defaults
|
||||
if api.manifest.SettingsSchema != nil {
|
||||
for _, setting := range api.manifest.SettingsSchema.Settings {
|
||||
finalConfig[strings.ToLower(setting.Key)] = setting.Default
|
||||
}
|
||||
}
|
||||
|
||||
// If we have settings given we override the defaults with them
|
||||
for setting, value := range api.app.Config().PluginSettings.Plugins[api.id] {
|
||||
finalConfig[strings.ToLower(setting)] = value
|
||||
}
|
||||
|
||||
if pluginSettingsJsonBytes, err := json.Marshal(finalConfig); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return json.Unmarshal(b, dest)
|
||||
return json.Unmarshal(pluginSettingsJsonBytes, dest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +59,14 @@ func (api *PluginAPI) UnregisterCommand(teamId, trigger string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetConfig() *model.Config {
|
||||
return api.app.GetConfig()
|
||||
}
|
||||
|
||||
func (api *PluginAPI) SaveConfig(config *model.Config) *model.AppError {
|
||||
return api.app.SaveConfig(config, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
return api.app.CreateTeam(team)
|
||||
}
|
||||
@@ -51,6 +75,10 @@ func (api *PluginAPI) DeleteTeam(teamId string) *model.AppError {
|
||||
return api.app.SoftDeleteTeam(teamId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeams() ([]*model.Team, *model.AppError) {
|
||||
return api.app.GetAllTeams()
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeam(teamId string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeam(teamId)
|
||||
}
|
||||
@@ -63,6 +91,30 @@ func (api *PluginAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError
|
||||
return api.app.UpdateTeam(team)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError) {
|
||||
return api.app.AddTeamMember(teamId, userId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateTeamMembers(teamId string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError) {
|
||||
return api.app.AddTeamMembers(teamId, userIds, requestorId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeleteTeamMember(teamId, userId, requestorId string) *model.AppError {
|
||||
return api.app.RemoveUserFromTeam(teamId, userId, requestorId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeamMembers(teamId string, offset, limit int) ([]*model.TeamMember, *model.AppError) {
|
||||
return api.app.GetTeamMembers(teamId, offset, limit)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError) {
|
||||
return api.app.GetTeamMember(teamId, userId)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) UpdateTeamMemberRoles(teamId, userId, newRoles string) (*model.TeamMember, *model.AppError) {
|
||||
return api.app.UpdateTeamMemberRoles(teamId, userId, newRoles)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
return api.app.CreateUser(user)
|
||||
}
|
||||
@@ -104,6 +156,10 @@ func (api *PluginAPI) DeleteChannel(channelId string) *model.AppError {
|
||||
return api.app.DeleteChannel(channel, "")
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetPublicChannelsForTeam(teamId string, offset, limit int) (*model.ChannelList, *model.AppError) {
|
||||
return api.app.GetPublicChannelsForTeam(teamId, offset, limit)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) GetChannel(channelId string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannel(channelId)
|
||||
}
|
||||
@@ -157,6 +213,10 @@ func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError
|
||||
return api.app.CreatePostMissingChannel(post, true)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) SendEphemeralPost(userId string, post *model.Post) *model.Post {
|
||||
return api.app.SendEphemeralPost(userId, post)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) DeletePost(postId string) *model.AppError {
|
||||
_, err := api.app.DeletePost(postId, api.id)
|
||||
return err
|
||||
@@ -170,129 +230,35 @@ func (api *PluginAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError
|
||||
return api.app.UpdatePost(post, false)
|
||||
}
|
||||
|
||||
func (api *PluginAPI) KeyValueStore() plugin.KeyValueStore {
|
||||
return api.keyValueStore
|
||||
func (api *PluginAPI) KVSet(key string, value []byte) *model.AppError {
|
||||
return api.app.SetPluginKey(api.id, key, value)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Set(key string, value []byte) *model.AppError {
|
||||
return s.app.SetPluginKey(s.id, key, value)
|
||||
func (api *PluginAPI) KVGet(key string) ([]byte, *model.AppError) {
|
||||
return api.app.GetPluginKey(api.id, key)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Get(key string) ([]byte, *model.AppError) {
|
||||
return s.app.GetPluginKey(s.id, key)
|
||||
func (api *PluginAPI) KVDelete(key string) *model.AppError {
|
||||
return api.app.DeletePluginKey(api.id, key)
|
||||
}
|
||||
|
||||
func (s *PluginKeyValueStore) Delete(key string) *model.AppError {
|
||||
return s.app.DeletePluginKey(s.id, key)
|
||||
func (api *PluginAPI) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) {
|
||||
api.app.Publish(&model.WebSocketEvent{
|
||||
Event: fmt.Sprintf("custom_%v_%v", api.id, event),
|
||||
Data: payload,
|
||||
Broadcast: broadcast,
|
||||
})
|
||||
}
|
||||
|
||||
type BuiltInPluginAPI struct {
|
||||
id string
|
||||
router *mux.Router
|
||||
app *App
|
||||
func (api *PluginAPI) LogDebug(msg string, keyValuePairs ...interface{}) {
|
||||
api.logger.Debug(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
if b, err := json.Marshal(api.app.Config().PluginSettings.Plugins[api.id]); err != nil {
|
||||
return err
|
||||
} else {
|
||||
return json.Unmarshal(b, dest)
|
||||
}
|
||||
func (api *PluginAPI) LogInfo(msg string, keyValuePairs ...interface{}) {
|
||||
api.logger.Info(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) PluginRouter() *mux.Router {
|
||||
return api.router
|
||||
func (api *PluginAPI) LogError(msg string, keyValuePairs ...interface{}) {
|
||||
api.logger.Error(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
return api.app.GetTeamByName(name)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetUserByName(name string) (*model.User, *model.AppError) {
|
||||
return api.app.GetUserByUsername(name)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetChannelByName(teamId, name string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetChannelByName(name, teamId)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
|
||||
return api.app.GetDirectChannel(userId1, userId2)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
return api.app.CreatePostMissingChannel(post, true)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetLdapUserAttributes(userId string, attributes []string) (map[string]string, *model.AppError) {
|
||||
if api.app.Ldap == nil {
|
||||
return nil, model.NewAppError("GetLdapUserAttributes", "ent.ldap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
user, err := api.app.GetUser(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.AuthData == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
return api.app.Ldap.GetUserAttributes(*user.AuthData, attributes)
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) GetSessionFromRequest(r *http.Request) (*model.Session, *model.AppError) {
|
||||
token := ""
|
||||
isTokenFromQueryString := false
|
||||
|
||||
// Attempt to parse token out of the header
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER {
|
||||
// Default session token
|
||||
token = authHeader[7:]
|
||||
|
||||
} else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN {
|
||||
// OAuth token
|
||||
token = authHeader[6:]
|
||||
}
|
||||
|
||||
// Attempt to parse the token from the cookie
|
||||
if len(token) == 0 {
|
||||
if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
|
||||
token = cookie.Value
|
||||
|
||||
if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt", http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to parse token out of the query string
|
||||
if len(token) == 0 {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
isTokenFromQueryString = true
|
||||
}
|
||||
|
||||
if len(token) == 0 {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
session, err := api.app.GetSession(token)
|
||||
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
} else if !session.IsOAuth && isTokenFromQueryString {
|
||||
return nil, model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (api *BuiltInPluginAPI) I18n(id string, r *http.Request) string {
|
||||
if r != nil {
|
||||
f, _ := utils.GetTranslationsAndLocale(nil, r)
|
||||
return f(id)
|
||||
}
|
||||
f, _ := utils.GetTranslationsBySystemLocale()
|
||||
return f(id)
|
||||
func (api *PluginAPI) LogWarn(msg string, keyValuePairs ...interface{}) {
|
||||
api.logger.Warn(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
113
app/plugin_commands.go
Обычный файл
113
app/plugin_commands.go
Обычный файл
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
type PluginCommand struct {
|
||||
Command *model.Command
|
||||
PluginId string
|
||||
}
|
||||
|
||||
func (a *App) RegisterPluginCommand(pluginId string, command *model.Command) error {
|
||||
if command.Trigger == "" {
|
||||
return fmt.Errorf("invalid command")
|
||||
}
|
||||
|
||||
command = &model.Command{
|
||||
Trigger: strings.ToLower(command.Trigger),
|
||||
TeamId: command.TeamId,
|
||||
AutoComplete: command.AutoComplete,
|
||||
AutoCompleteDesc: command.AutoCompleteDesc,
|
||||
AutoCompleteHint: command.AutoCompleteHint,
|
||||
DisplayName: command.DisplayName,
|
||||
}
|
||||
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.Trigger == command.Trigger && pc.Command.TeamId == command.TeamId {
|
||||
if pc.PluginId == pluginId {
|
||||
pc.Command = command
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.pluginCommands = append(a.pluginCommands, &PluginCommand{
|
||||
Command: command,
|
||||
PluginId: pluginId,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UnregisterPluginCommand(pluginId, teamId, trigger string) {
|
||||
trigger = strings.ToLower(trigger)
|
||||
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
var remaining []*PluginCommand
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.TeamId != teamId || pc.Command.Trigger != trigger {
|
||||
remaining = append(remaining, pc)
|
||||
}
|
||||
}
|
||||
a.pluginCommands = remaining
|
||||
}
|
||||
|
||||
func (a *App) UnregisterPluginCommands(pluginId string) {
|
||||
a.pluginCommandsLock.Lock()
|
||||
defer a.pluginCommandsLock.Unlock()
|
||||
|
||||
var remaining []*PluginCommand
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.PluginId != pluginId {
|
||||
remaining = append(remaining, pc)
|
||||
}
|
||||
}
|
||||
a.pluginCommands = remaining
|
||||
}
|
||||
|
||||
func (a *App) PluginCommandsForTeam(teamId string) []*model.Command {
|
||||
a.pluginCommandsLock.RLock()
|
||||
defer a.pluginCommandsLock.RUnlock()
|
||||
|
||||
var commands []*model.Command
|
||||
for _, pc := range a.pluginCommands {
|
||||
if pc.Command.TeamId == "" || pc.Command.TeamId == teamId {
|
||||
commands = append(commands, pc.Command)
|
||||
}
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func (a *App) ExecutePluginCommand(args *model.CommandArgs) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
parts := strings.Split(args.Command, " ")
|
||||
trigger := parts[0][1:]
|
||||
trigger = strings.ToLower(trigger)
|
||||
|
||||
a.pluginCommandsLock.RLock()
|
||||
defer a.pluginCommandsLock.RUnlock()
|
||||
|
||||
for _, pc := range a.pluginCommands {
|
||||
if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger {
|
||||
pluginHooks, err := a.Plugins.HooksForPlugin(pc.PluginId)
|
||||
if err != nil {
|
||||
return pc.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
response, appErr := pluginHooks.ExecuteCommand(plugin.NewBlankContext(), args)
|
||||
return pc.Command, response, appErr
|
||||
}
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
125
app/plugin_install.go
Обычный файл
125
app/plugin_install.go
Обычный файл
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
|
||||
// InstallPlugin unpacks and installs a plugin but does not enable or activate it.
|
||||
func (a *App) InstallPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
|
||||
return a.installPlugin(pluginFile)
|
||||
}
|
||||
|
||||
func (a *App) installPlugin(pluginFile io.Reader) (*model.Manifest, *model.AppError) {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "plugintmp")
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := utils.ExtractTarGz(pluginFile, tmpDir); err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.extract.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
tmpPluginDir := tmpDir
|
||||
dir, err := ioutil.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.filesystem.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if len(dir) == 1 && dir[0].IsDir() {
|
||||
tmpPluginDir = filepath.Join(tmpPluginDir, dir[0].Name())
|
||||
}
|
||||
|
||||
manifest, _, err := model.FindManifest(tmpPluginDir)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if !plugin.IsValidId(manifest.Id) {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.invalid_id.app_error", map[string]interface{}{"Min": plugin.MinIdLength, "Max": plugin.MaxIdLength, "Regex": plugin.ValidId.String()}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
bundles, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Check that there is no plugin with the same ID
|
||||
for _, bundle := range bundles {
|
||||
if bundle.Manifest != nil && bundle.Manifest.Id == manifest.Id {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.install_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pluginPath := filepath.Join(*a.Config().PluginSettings.Directory, manifest.Id)
|
||||
err = utils.CopyDir(tmpPluginDir, pluginPath)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("installPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if err := a.notifyPluginStatusesChanged(); err != nil {
|
||||
mlog.Error("failed to notify plugin status changed", mlog.Err(err))
|
||||
}
|
||||
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func (a *App) RemovePlugin(id string) *model.AppError {
|
||||
return a.removePlugin(id)
|
||||
}
|
||||
|
||||
func (a *App) removePlugin(id string) *model.AppError {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
plugins, err := a.Plugins.Available()
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
var manifest *model.Manifest
|
||||
var pluginPath string
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
manifest = p.Manifest
|
||||
pluginPath = filepath.Dir(p.ManifestPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if manifest == nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if a.Plugins.IsActive(id) && manifest.HasClient() {
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_DISABLED, "", "", "", nil)
|
||||
message.Add("manifest", manifest.ClientManifest())
|
||||
a.Publish(message)
|
||||
}
|
||||
|
||||
a.Plugins.Deactivate(id)
|
||||
|
||||
err = os.RemoveAll(pluginPath)
|
||||
if err != nil {
|
||||
return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
61
app/plugin_key_value_store.go
Обычный файл
61
app/plugin_key_value_store.go
Обычный файл
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
func getKeyHash(key string) string {
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(key))
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (a *App) SetPluginKey(pluginId string, key string, value []byte) *model.AppError {
|
||||
kv := &model.PluginKeyValue{
|
||||
PluginId: pluginId,
|
||||
Key: getKeyHash(key),
|
||||
Value: value,
|
||||
}
|
||||
|
||||
result := <-a.Srv.Store.Plugin().SaveOrUpdate(kv)
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
|
||||
func (a *App) GetPluginKey(pluginId string, key string) ([]byte, *model.AppError) {
|
||||
result := <-a.Srv.Store.Plugin().Get(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
if result.Err.StatusCode == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
mlog.Error(result.Err.Error())
|
||||
return nil, result.Err
|
||||
}
|
||||
|
||||
kv := result.Data.(*model.PluginKeyValue)
|
||||
|
||||
return kv.Value, nil
|
||||
}
|
||||
|
||||
func (a *App) DeletePluginKey(pluginId string, key string) *model.AppError {
|
||||
result := <-a.Srv.Store.Plugin().Delete(pluginId, getKeyHash(key))
|
||||
|
||||
if result.Err != nil {
|
||||
mlog.Error(result.Err.Error())
|
||||
}
|
||||
|
||||
return result.Err
|
||||
}
|
||||
76
app/plugin_requests.go
Обычный файл
76
app/plugin_requests.go
Обычный файл
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
func (a *App) ServePluginRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
err := model.NewAppError("ServePluginRequest", "app.plugin.disabled.app_error", nil, "Enable plugins to serve plugin requests", http.StatusNotImplemented)
|
||||
a.Log.Error(err.Error())
|
||||
w.WriteHeader(err.StatusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(err.ToJson()))
|
||||
return
|
||||
}
|
||||
|
||||
params := mux.Vars(r)
|
||||
hooks, err := a.Plugins.HooksForPlugin(params["plugin_id"])
|
||||
if err != nil {
|
||||
a.Log.Error("Access to route for non-existant plugin", mlog.String("missing_plugin_id", params["plugin_id"]), mlog.Err(err))
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
a.servePluginRequest(w, r, hooks.ServeHTTP)
|
||||
}
|
||||
|
||||
func (a *App) servePluginRequest(w http.ResponseWriter, r *http.Request, handler func(*plugin.Context, http.ResponseWriter, *http.Request)) {
|
||||
token := ""
|
||||
|
||||
authHeader := r.Header.Get(model.HEADER_AUTH)
|
||||
if strings.HasPrefix(strings.ToUpper(authHeader), model.HEADER_BEARER+" ") {
|
||||
token = authHeader[len(model.HEADER_BEARER)+1:]
|
||||
} else if strings.HasPrefix(strings.ToLower(authHeader), model.HEADER_TOKEN+" ") {
|
||||
token = authHeader[len(model.HEADER_TOKEN)+1:]
|
||||
} else if cookie, _ := r.Cookie(model.SESSION_COOKIE_TOKEN); cookie != nil && (r.Method == "GET" || r.Header.Get(model.HEADER_REQUESTED_WITH) == model.HEADER_REQUESTED_WITH_XML) {
|
||||
token = cookie.Value
|
||||
} else {
|
||||
token = r.URL.Query().Get("access_token")
|
||||
}
|
||||
|
||||
r.Header.Del("Mattermost-User-Id")
|
||||
if token != "" {
|
||||
if session, err := a.GetSession(token); session != nil && err == nil {
|
||||
r.Header.Set("Mattermost-User-Id", session.UserId)
|
||||
}
|
||||
}
|
||||
|
||||
cookies := r.Cookies()
|
||||
r.Header.Del("Cookie")
|
||||
for _, c := range cookies {
|
||||
if c.Name != model.SESSION_COOKIE_TOKEN {
|
||||
r.AddCookie(c)
|
||||
}
|
||||
}
|
||||
r.Header.Del(model.HEADER_AUTH)
|
||||
r.Header.Del("Referer")
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
newQuery := r.URL.Query()
|
||||
newQuery.Del("access_token")
|
||||
r.URL.RawQuery = newQuery.Encode()
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/plugins/"+params["plugin_id"])
|
||||
|
||||
handler(plugin.NewBlankContext(), w, r)
|
||||
}
|
||||
63
app/plugin_statuses.go
Обычный файл
63
app/plugin_statuses.go
Обычный файл
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
// GetPluginStatuses returns the status for plugins installed on this server.
|
||||
func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
if a.Plugins == nil || !*a.Config().PluginSettings.Enable {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
pluginStatuses, err := a.Plugins.Statuses()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetPluginStatuses", "Unable to get plugin statuses", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Add our cluster ID
|
||||
for _, status := range pluginStatuses {
|
||||
status.ClusterId = a.GetClusterId()
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
// GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster.
|
||||
func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) {
|
||||
pluginStatuses, err := a.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a.Cluster != nil && *a.Config().ClusterSettings.Enable {
|
||||
clusterPluginStatuses, err := a.Cluster.GetPluginStatuses()
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
pluginStatuses = append(pluginStatuses, clusterPluginStatuses...)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
func (a *App) notifyPluginStatusesChanged() error {
|
||||
pluginStatuses, err := a.GetClusterPluginStatuses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Notify any system admins.
|
||||
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED, "", "", "", nil)
|
||||
message.Add("plugin_statuses", pluginStatuses)
|
||||
message.Broadcast.ContainsSensitiveData = true
|
||||
a.Publish(message)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,19 +4,15 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPluginKeyValueStore(t *testing.T) {
|
||||
@@ -73,7 +69,7 @@ func TestHandlePluginRequest(t *testing.T) {
|
||||
var assertions func(*http.Request)
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/plugins/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}/{anything:.*}", func(_ http.ResponseWriter, r *http.Request) {
|
||||
th.App.servePluginRequest(nil, r, func(_ http.ResponseWriter, r *http.Request) {
|
||||
th.App.servePluginRequest(nil, r, func(_ *plugin.Context, _ http.ResponseWriter, r *http.Request) {
|
||||
assertions(r)
|
||||
})
|
||||
})
|
||||
@@ -103,152 +99,6 @@ func TestHandlePluginRequest(t *testing.T) {
|
||||
router.ServeHTTP(nil, r)
|
||||
}
|
||||
|
||||
type testPlugin struct {
|
||||
plugintest.Hooks
|
||||
}
|
||||
|
||||
func (p *testPlugin) OnConfigurationChange() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *testPlugin) OnDeactivate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type pluginCommandTestPlugin struct {
|
||||
testPlugin
|
||||
|
||||
TeamId string
|
||||
}
|
||||
|
||||
func (p *pluginCommandTestPlugin) OnActivate(api plugin.API) error {
|
||||
if err := api.RegisterCommand(&model.Command{
|
||||
Trigger: "foo",
|
||||
TeamId: p.TeamId,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := api.RegisterCommand(&model.Command{
|
||||
Trigger: "foo2",
|
||||
TeamId: p.TeamId,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return api.UnregisterCommand(p.TeamId, "foo2")
|
||||
}
|
||||
|
||||
func (p *pluginCommandTestPlugin) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
if args.Command == "/foo" {
|
||||
return &model.CommandResponse{
|
||||
Text: "bar",
|
||||
}, nil
|
||||
}
|
||||
return nil, model.NewAppError("ExecuteCommand", "this is an error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestPluginCommands(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.InstallPlugin(&model.Manifest{
|
||||
Id: "foo",
|
||||
}, &pluginCommandTestPlugin{
|
||||
TeamId: th.BasicTeam.Id,
|
||||
})
|
||||
|
||||
require.Nil(t, th.App.EnablePlugin("foo"))
|
||||
|
||||
// Ideally, we would wait for the websocket activation event instead of just sleeping.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
pluginStatuses, err := th.App.GetPluginStatuses()
|
||||
require.Nil(t, err)
|
||||
found := false
|
||||
for _, pluginStatus := range pluginStatuses {
|
||||
if pluginStatus.PluginId == "foo" {
|
||||
require.Equal(t, model.PluginStateRunning, pluginStatus.State)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "failed to find plugin foo in plugin statuses")
|
||||
|
||||
resp, err := th.App.ExecuteCommand(&model.CommandArgs{
|
||||
Command: "/foo2",
|
||||
TeamId: th.BasicTeam.Id,
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
|
||||
resp, err = th.App.ExecuteCommand(&model.CommandArgs{
|
||||
Command: "/foo",
|
||||
TeamId: th.BasicTeam.Id,
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, "bar", resp.Text)
|
||||
|
||||
resp, err = th.App.ExecuteCommand(&model.CommandArgs{
|
||||
Command: "/foo baz",
|
||||
TeamId: th.BasicTeam.Id,
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "this is an error", err.Message)
|
||||
assert.Nil(t, resp)
|
||||
|
||||
require.Nil(t, th.App.RemovePlugin("foo"))
|
||||
|
||||
resp, err = th.App.ExecuteCommand(&model.CommandArgs{
|
||||
Command: "/foo",
|
||||
TeamId: th.BasicTeam.Id,
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
}
|
||||
|
||||
type pluginBadActivation struct {
|
||||
testPlugin
|
||||
}
|
||||
|
||||
func (p *pluginBadActivation) OnActivate(api plugin.API) error {
|
||||
return errors.New("won't activate for some reason")
|
||||
}
|
||||
|
||||
func TestPluginBadActivation(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.InstallPlugin(&model.Manifest{
|
||||
Id: "foo",
|
||||
}, &pluginBadActivation{})
|
||||
|
||||
t.Run("EnablePlugin bad activation", func(t *testing.T) {
|
||||
err := th.App.EnablePlugin("foo")
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Ideally, we would wait for the websocket activation event instead of just
|
||||
// sleeping.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
pluginStatuses, err := th.App.GetPluginStatuses()
|
||||
require.Nil(t, err)
|
||||
found := false
|
||||
for _, pluginStatus := range pluginStatuses {
|
||||
if pluginStatus.PluginId == "foo" {
|
||||
require.Equal(t, model.PluginStateFailedToStart, pluginStatus.State)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "failed to find plugin foo in plugin statuses")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPluginStatusesDisabled(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
35
app/post.go
35
app/post.go
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/dyatlov/go-opengraph/opengraph"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
"golang.org/x/net/html/charset"
|
||||
@@ -161,10 +162,14 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
if newPost, rejectionReason := a.PluginEnv.Hooks().MessageWillBePosted(post); newPost == nil {
|
||||
var rejectionReason string
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
post, rejectionReason = hooks.MessageWillBePosted(pluginContext, post)
|
||||
return post != nil
|
||||
}, plugin.MessageWillBePostedId)
|
||||
if post == nil {
|
||||
return nil, model.NewAppError("createPost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
} else {
|
||||
post = newPost
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +182,11 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
a.PluginEnv.Hooks().MessageHasBeenPosted(rpost)
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.MessageHasBeenPosted(pluginContext, rpost)
|
||||
return true
|
||||
}, plugin.MessageHasBeenPostedId)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -386,10 +395,14 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
if pluginModifiedPost, rejectionReason := a.PluginEnv.Hooks().MessageWillBeUpdated(newPost, oldPost); pluginModifiedPost == nil {
|
||||
return nil, model.NewAppError("createPost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
} else {
|
||||
newPost = pluginModifiedPost
|
||||
var rejectionReason string
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
newPost, rejectionReason = hooks.MessageWillBeUpdated(pluginContext, newPost, oldPost)
|
||||
return post != nil
|
||||
}, plugin.MessageWillBeUpdatedId)
|
||||
if newPost == nil {
|
||||
return nil, model.NewAppError("UpdatePost", "Post rejected by plugin. "+rejectionReason, nil, "", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,7 +413,11 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
|
||||
|
||||
if a.PluginsReady() {
|
||||
a.Go(func() {
|
||||
a.PluginEnv.Hooks().MessageHasBeenUpdated(newPost, oldPost)
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.MessageHasBeenUpdated(pluginContext, newPost, oldPost)
|
||||
return true
|
||||
}, plugin.MessageHasBeenUpdatedId)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ func TestUpdatePostEditAt(t *testing.T) {
|
||||
} else if saved.EditAt == post.EditAt {
|
||||
t.Fatal("should have updated post.EditAt when updating post message")
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
|
||||
func TestUpdatePostTimeLimit(t *testing.T) {
|
||||
|
||||
45
app/team.go
45
app/team.go
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
|
||||
@@ -429,12 +430,28 @@ func (a *App) joinUserToTeam(team *model.Team, user *model.User) (*model.TeamMem
|
||||
}
|
||||
|
||||
func (a *App) JoinUserToTeam(team *model.Team, user *model.User, userRequestorId string) *model.AppError {
|
||||
if _, alreadyAdded, err := a.joinUserToTeam(team, user); err != nil {
|
||||
tm, alreadyAdded, err := a.joinUserToTeam(team, user)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if alreadyAdded {
|
||||
return nil
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
var actor *model.User
|
||||
if userRequestorId != "" {
|
||||
actor, err = a.GetUser(userRequestorId)
|
||||
}
|
||||
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasJoinedTeam(pluginContext, tm, actor)
|
||||
return true
|
||||
}, plugin.UserHasJoinedTeamId)
|
||||
})
|
||||
}
|
||||
|
||||
if uua := <-a.Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil {
|
||||
return uua.Err
|
||||
}
|
||||
@@ -575,9 +592,8 @@ func (a *App) AddTeamMember(teamId, userId string) (*model.TeamMember, *model.Ap
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var teamMember *model.TeamMember
|
||||
var err *model.AppError
|
||||
if teamMember, err = a.GetTeamMember(teamId, userId); err != nil {
|
||||
teamMember, err := a.GetTeamMember(teamId, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -692,10 +708,8 @@ func (a *App) RemoveUserFromTeam(teamId string, userId string, requestorId strin
|
||||
}
|
||||
|
||||
func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) *model.AppError {
|
||||
var teamMember *model.TeamMember
|
||||
var err *model.AppError
|
||||
|
||||
if teamMember, err = a.GetTeamMember(team.Id, user.Id); err != nil {
|
||||
teamMember, err := a.GetTeamMember(team.Id, user.Id)
|
||||
if err != nil {
|
||||
return model.NewAppError("LeaveTeam", "api.team.remove_user_from_team.missing.app_error", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -753,6 +767,21 @@ func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string)
|
||||
return result.Err
|
||||
}
|
||||
|
||||
if a.PluginsReady() {
|
||||
var actor *model.User
|
||||
if requestorId != "" {
|
||||
actor, err = a.GetUser(requestorId)
|
||||
}
|
||||
|
||||
a.Go(func() {
|
||||
pluginContext := &plugin.Context{}
|
||||
a.Plugins.RunMultiPluginHook(func(hooks plugin.Hooks) bool {
|
||||
hooks.UserHasLeftTeam(pluginContext, teamMember, actor)
|
||||
return true
|
||||
}, plugin.UserHasLeftTeamId)
|
||||
})
|
||||
}
|
||||
|
||||
if uua := <-a.Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil {
|
||||
return uua.Err
|
||||
}
|
||||
|
||||
@@ -98,10 +98,10 @@ func runServer(configFileLocation string, disableConfigWatch bool, usedPlatform
|
||||
a.DoAdvancedPermissionsMigration()
|
||||
a.DoEmojisPermissionsMigration()
|
||||
|
||||
a.InitPlugins(*a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory, nil)
|
||||
a.InitPlugins(*a.Config().PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory)
|
||||
a.AddConfigListener(func(prevCfg, cfg *model.Config) {
|
||||
if *cfg.PluginSettings.Enable {
|
||||
a.InitPlugins(*cfg.PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory, nil)
|
||||
a.InitPlugins(*cfg.PluginSettings.Directory, *a.Config().PluginSettings.ClientDirectory)
|
||||
} else {
|
||||
a.ShutDownPlugins()
|
||||
}
|
||||
|
||||
@@ -4,19 +4,15 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/app"
|
||||
"github.com/mattermost/mattermost-server/einterfaces"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/pluginenv"
|
||||
"github.com/mattermost/mattermost-server/store"
|
||||
"github.com/mattermost/mattermost-server/store/sqlstore"
|
||||
"github.com/mattermost/mattermost-server/store/storetest"
|
||||
@@ -35,7 +31,6 @@ type TestHelper struct {
|
||||
|
||||
tempConfigPath string
|
||||
tempWorkspace string
|
||||
pluginHooks map[string]plugin.Hooks
|
||||
}
|
||||
|
||||
type persistentTestStore struct {
|
||||
@@ -93,7 +88,6 @@ func setupTestHelper(enterprise bool) *TestHelper {
|
||||
|
||||
th := &TestHelper{
|
||||
App: a,
|
||||
pluginHooks: make(map[string]plugin.Hooks),
|
||||
tempConfigPath: tempConfig.Name(),
|
||||
}
|
||||
|
||||
@@ -307,62 +301,6 @@ func (me *TestHelper) TearDown() {
|
||||
}
|
||||
}
|
||||
|
||||
type mockPluginSupervisor struct {
|
||||
hooks plugin.Hooks
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Start(api plugin.API) error {
|
||||
return s.hooks.OnActivate(api)
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Hooks() plugin.Hooks {
|
||||
return s.hooks
|
||||
}
|
||||
|
||||
func (s *mockPluginSupervisor) Wait() error { return nil }
|
||||
|
||||
func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks) {
|
||||
if me.tempWorkspace == "" {
|
||||
dir, err := ioutil.TempDir("", "apptest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
me.tempWorkspace = dir
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(me.tempWorkspace, "plugins")
|
||||
webappDir := filepath.Join(me.tempWorkspace, "webapp")
|
||||
me.App.InitPlugins(pluginDir, webappDir, func(bundle *model.BundleInfo) (plugin.Supervisor, error) {
|
||||
if hooks, ok := me.pluginHooks[bundle.Manifest.Id]; ok {
|
||||
return &mockPluginSupervisor{hooks}, nil
|
||||
}
|
||||
return pluginenv.DefaultSupervisorProvider(bundle)
|
||||
})
|
||||
|
||||
me.pluginHooks[manifest.Id] = hooks
|
||||
|
||||
manifestCopy := *manifest
|
||||
if manifestCopy.Backend == nil {
|
||||
manifestCopy.Backend = &model.ManifestBackend{}
|
||||
}
|
||||
manifestBytes, err := json.Marshal(&manifestCopy)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(pluginDir, manifest.Id), 0700); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(pluginDir, manifest.Id, "plugin.json"), manifestBytes, 0600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (me *TestHelper) ResetRoleMigration() {
|
||||
if _, err := testStoreSqlSupplier.GetMaster().Exec("DELETE from Roles"); err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -11,7 +11,9 @@ import (
|
||||
var globalLogger *Logger
|
||||
|
||||
func InitGlobalLogger(logger *Logger) {
|
||||
globalLogger = logger
|
||||
glob := *logger
|
||||
glob.zap = glob.zap.WithOptions(zap.AddCallerSkip(1))
|
||||
globalLogger = &glob
|
||||
Debug = globalLogger.Debug
|
||||
Info = globalLogger.Info
|
||||
Warn = globalLogger.Warn
|
||||
@@ -20,7 +22,7 @@ func InitGlobalLogger(logger *Logger) {
|
||||
}
|
||||
|
||||
func RedirectStdLog(logger *Logger) {
|
||||
zap.RedirectStdLogAt(logger.zap.With(zap.String("source", "stdlog")), zapcore.ErrorLevel)
|
||||
zap.RedirectStdLogAt(logger.zap.With(zap.String("source", "stdlog")).WithOptions(zap.AddCallerSkip(-2)), zapcore.ErrorLevel)
|
||||
}
|
||||
|
||||
type LogFunc func(string, ...Field)
|
||||
|
||||
29
mlog/log.go
29
mlog/log.go
@@ -4,6 +4,7 @@
|
||||
package mlog
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
@@ -28,9 +29,11 @@ type Field = zapcore.Field
|
||||
|
||||
var Int64 = zap.Int64
|
||||
var Int = zap.Int
|
||||
var Uint32 = zap.Uint32
|
||||
var String = zap.String
|
||||
var Any = zap.Any
|
||||
var Err = zap.Error
|
||||
var Bool = zap.Bool
|
||||
|
||||
type LoggerConfiguration struct {
|
||||
EnableConsole bool
|
||||
@@ -99,7 +102,7 @@ func NewLogger(config *LoggerConfiguration) *Logger {
|
||||
combinedCore := zapcore.NewTee(cores...)
|
||||
|
||||
logger.zap = zap.New(combinedCore,
|
||||
zap.AddCallerSkip(2),
|
||||
zap.AddCallerSkip(1),
|
||||
zap.AddCaller(),
|
||||
)
|
||||
|
||||
@@ -125,6 +128,30 @@ func (l *Logger) StdLog(fields ...Field) *log.Logger {
|
||||
return zap.NewStdLog(l.With(fields...).zap.WithOptions(getStdLogOption()))
|
||||
}
|
||||
|
||||
// StdLogWriter returns a writer that can be hooked up to the output of a golang standard logger
|
||||
// anything written will be interpreted as log entries accordingly
|
||||
func (l *Logger) StdLogWriter() io.Writer {
|
||||
newLogger := *l
|
||||
newLogger.zap = newLogger.zap.WithOptions(zap.AddCallerSkip(4), getStdLogOption())
|
||||
f := newLogger.Info
|
||||
return &loggerWriter{f}
|
||||
}
|
||||
|
||||
func (l *Logger) WithCallerSkip(skip int) *Logger {
|
||||
newlogger := *l
|
||||
newlogger.zap = newlogger.zap.WithOptions(zap.AddCallerSkip(skip))
|
||||
return &newlogger
|
||||
}
|
||||
|
||||
// Made for the plugin interface, wraps mlog in a simpler interface
|
||||
// at the cost of performance
|
||||
func (l *Logger) Sugar() *SugarLogger {
|
||||
return &SugarLogger{
|
||||
wrappedLogger: l,
|
||||
zapSugar: l.zap.Sugar(),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(message string, fields ...Field) {
|
||||
l.zap.Debug(message, fields...)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package mlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
@@ -72,3 +73,15 @@ func getStdLogOption() zap.Option {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
type loggerWriter struct {
|
||||
logFunc func(msg string, fields ...Field)
|
||||
}
|
||||
|
||||
func (l *loggerWriter) Write(p []byte) (int, error) {
|
||||
trimmed := string(bytes.TrimSpace(p))
|
||||
for _, line := range strings.Split(trimmed, "\n") {
|
||||
l.logFunc(string(line))
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
28
mlog/sugar.go
Обычный файл
28
mlog/sugar.go
Обычный файл
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlog
|
||||
|
||||
import "go.uber.org/zap"
|
||||
|
||||
// Made for the plugin interface, use the regular logger for other uses
|
||||
type SugarLogger struct {
|
||||
wrappedLogger *Logger
|
||||
zapSugar *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (l *SugarLogger) Debug(msg string, keyValuePairs ...interface{}) {
|
||||
l.zapSugar.Debugw(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (l *SugarLogger) Info(msg string, keyValuePairs ...interface{}) {
|
||||
l.zapSugar.Infow(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (l *SugarLogger) Error(msg string, keyValuePairs ...interface{}) {
|
||||
l.zapSugar.Errorw(msg, keyValuePairs...)
|
||||
}
|
||||
|
||||
func (l *SugarLogger) Warn(msg string, keyValuePairs ...interface{}) {
|
||||
l.zapSugar.Warnw(msg, keyValuePairs...)
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
package model
|
||||
|
||||
import "github.com/mattermost/mattermost-server/mlog"
|
||||
|
||||
type BundleInfo struct {
|
||||
Path string
|
||||
|
||||
@@ -11,6 +13,13 @@ type BundleInfo struct {
|
||||
ManifestError error
|
||||
}
|
||||
|
||||
func (b *BundleInfo) WrapLogger(logger *mlog.Logger) *mlog.Logger {
|
||||
if b.Manifest != nil {
|
||||
return logger.With(mlog.String("plugin_id", b.Manifest.Id))
|
||||
}
|
||||
return logger.With(mlog.String("plugin_path", b.Path))
|
||||
}
|
||||
|
||||
// Returns bundle info for the given path. The return value is never nil.
|
||||
func BundleInfoForPath(path string) *BundleInfo {
|
||||
m, mpath, err := FindManifest(path)
|
||||
|
||||
@@ -3701,8 +3701,8 @@ func (c *Client4) GetWebappPlugins() ([]*Manifest, *Response) {
|
||||
|
||||
// ActivatePlugin will activate an plugin installed.
|
||||
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
|
||||
func (c *Client4) ActivatePlugin(id string) (bool, *Response) {
|
||||
if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/activate", ""); err != nil {
|
||||
func (c *Client4) EnablePlugin(id string) (bool, *Response) {
|
||||
if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/enable", ""); err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
@@ -3712,8 +3712,8 @@ func (c *Client4) ActivatePlugin(id string) (bool, *Response) {
|
||||
|
||||
// DeactivatePlugin will deactivate an active plugin.
|
||||
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
|
||||
func (c *Client4) DeactivatePlugin(id string) (bool, *Response) {
|
||||
if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/deactivate", ""); err != nil {
|
||||
func (c *Client4) DisablePlugin(id string) (bool, *Response) {
|
||||
if r, err := c.DoApiPost(c.GetPluginRoute(id)+"/disable", ""); err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
|
||||
@@ -1727,7 +1727,7 @@ type PluginSettings struct {
|
||||
EnableUploads *bool
|
||||
Directory *string
|
||||
ClientDirectory *string
|
||||
Plugins map[string]interface{}
|
||||
Plugins map[string]map[string]interface{}
|
||||
PluginStates map[string]*PluginState
|
||||
}
|
||||
|
||||
@@ -1757,7 +1757,7 @@ func (s *PluginSettings) SetDefaults() {
|
||||
}
|
||||
|
||||
if s.Plugins == nil {
|
||||
s.Plugins = make(map[string]interface{})
|
||||
s.Plugins = make(map[string]map[string]interface{})
|
||||
}
|
||||
|
||||
if s.PluginStates == nil {
|
||||
|
||||
@@ -10,24 +10,22 @@ import (
|
||||
|
||||
const (
|
||||
PluginStateNotRunning = 0
|
||||
PluginStateStarting = 1
|
||||
PluginStateStarting = 1 // unused by server
|
||||
PluginStateRunning = 2
|
||||
PluginStateFailedToStart = 3
|
||||
PluginStateFailedToStayRunning = 4
|
||||
PluginStateStopping = 5
|
||||
PluginStateFailedToStayRunning = 4 // unused by server
|
||||
PluginStateStopping = 5 // unused by server
|
||||
)
|
||||
|
||||
// PluginStatus provides a cluster-aware view of installed plugins.
|
||||
type PluginStatus struct {
|
||||
PluginId string `json:"plugin_id"`
|
||||
ClusterId string `json:"cluster_id"`
|
||||
PluginPath string `json:"plugin_path"`
|
||||
State int `json:"state"`
|
||||
IsSandboxed bool `json:"is_sandboxed"`
|
||||
IsPrepackaged bool `json:"is_prepackaged"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
PluginId string `json:"plugin_id"`
|
||||
ClusterId string `json:"cluster_id"`
|
||||
PluginPath string `json:"plugin_path"`
|
||||
State int `json:"state"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type PluginStatuses []*PluginStatus
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
type PluginInfo struct {
|
||||
Manifest
|
||||
Prepackaged bool `json:"prepackaged"`
|
||||
}
|
||||
|
||||
type PluginsResponse struct {
|
||||
|
||||
@@ -44,9 +44,9 @@ const (
|
||||
WEBSOCKET_EVENT_RESPONSE = "response"
|
||||
WEBSOCKET_EVENT_EMOJI_ADDED = "emoji_added"
|
||||
WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed"
|
||||
WEBSOCKET_EVENT_PLUGIN_ACTIVATED = "plugin_activated" // EXPERIMENTAL - SUBJECT TO CHANGE
|
||||
WEBSOCKET_EVENT_PLUGIN_DEACTIVATED = "plugin_deactivated" // EXPERIMENTAL - SUBJECT TO CHANGE
|
||||
WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED = "plugin_statuses_changed" // EXPERIMENTAL - SUBJECT TO CHANGE
|
||||
WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED = "plugin_statuses_changed"
|
||||
WEBSOCKET_EVENT_PLUGIN_ENABLED = "plugin_enabled"
|
||||
WEBSOCKET_EVENT_PLUGIN_DISABLED = "plugin_disabled"
|
||||
WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated"
|
||||
WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed"
|
||||
WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
@@ -23,6 +24,12 @@ type API interface {
|
||||
// UnregisterCommand unregisters a command previously registered via RegisterCommand.
|
||||
UnregisterCommand(teamId, trigger string) error
|
||||
|
||||
// GetConfig fetches the currently persisted config
|
||||
GetConfig() *model.Config
|
||||
|
||||
// SaveConfig sets the given config and persists the changes
|
||||
SaveConfig(config *model.Config) *model.AppError
|
||||
|
||||
// CreateUser creates a user.
|
||||
CreateUser(user *model.User) (*model.User, *model.AppError)
|
||||
|
||||
@@ -47,6 +54,9 @@ type API interface {
|
||||
// DeleteTeam deletes a team.
|
||||
DeleteTeam(teamId string) *model.AppError
|
||||
|
||||
// GetTeam gets all teams.
|
||||
GetTeams() ([]*model.Team, *model.AppError)
|
||||
|
||||
// GetTeam gets a team.
|
||||
GetTeam(teamId string) (*model.Team, *model.AppError)
|
||||
|
||||
@@ -56,12 +66,33 @@ type API interface {
|
||||
// UpdateTeam updates a team.
|
||||
UpdateTeam(team *model.Team) (*model.Team, *model.AppError)
|
||||
|
||||
// CreateTeamMember creates a team membership.
|
||||
CreateTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError)
|
||||
|
||||
// CreateTeamMember creates a team membership for all provided user ids.
|
||||
CreateTeamMembers(teamId string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError)
|
||||
|
||||
// DeleteTeamMember deletes a team membership.
|
||||
DeleteTeamMember(teamId, userId, requestorId string) *model.AppError
|
||||
|
||||
// GetTeamMembers returns the memberships of a specific team.
|
||||
GetTeamMembers(teamId string, offset, limit int) ([]*model.TeamMember, *model.AppError)
|
||||
|
||||
// GetTeamMember returns a specific membership.
|
||||
GetTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError)
|
||||
|
||||
// UpdateTeamMemberRoles updates the role for a team membership.
|
||||
UpdateTeamMemberRoles(teamId, userId, newRoles string) (*model.TeamMember, *model.AppError)
|
||||
|
||||
// CreateChannel creates a channel.
|
||||
CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError)
|
||||
|
||||
// DeleteChannel deletes a channel.
|
||||
DeleteChannel(channelId string) *model.AppError
|
||||
|
||||
// GetChannels gets a list of all channels.
|
||||
GetPublicChannelsForTeam(teamId string, offset, limit int) (*model.ChannelList, *model.AppError)
|
||||
|
||||
// GetChannel gets a channel.
|
||||
GetChannel(channelId string) (*model.Channel, *model.AppError)
|
||||
|
||||
@@ -95,6 +126,9 @@ type API interface {
|
||||
// CreatePost creates a post.
|
||||
CreatePost(post *model.Post) (*model.Post, *model.AppError)
|
||||
|
||||
// SendEphemeralPost creates an ephemeral post.
|
||||
SendEphemeralPost(userId string, post *model.Post) *model.Post
|
||||
|
||||
// DeletePost deletes a post.
|
||||
DeletePost(postId string) *model.AppError
|
||||
|
||||
@@ -104,17 +138,48 @@ type API interface {
|
||||
// UpdatePost updates a post.
|
||||
UpdatePost(post *model.Post) (*model.Post, *model.AppError)
|
||||
|
||||
// KeyValueStore returns an object for accessing the persistent key value storage.
|
||||
KeyValueStore() KeyValueStore
|
||||
}
|
||||
|
||||
type KeyValueStore interface {
|
||||
// Set will store a key-value pair, unique per plugin.
|
||||
Set(key string, value []byte) *model.AppError
|
||||
KVSet(key string, value []byte) *model.AppError
|
||||
|
||||
// Get will retrieve a value based on the key. Returns nil for non-existent keys.
|
||||
Get(key string) ([]byte, *model.AppError)
|
||||
KVGet(key string) ([]byte, *model.AppError)
|
||||
|
||||
// Delete will remove a key-value pair. Returns nil for non-existent keys.
|
||||
Delete(key string) *model.AppError
|
||||
KVDelete(key string) *model.AppError
|
||||
|
||||
// PublishWebSocketEvent sends an event to WebSocket connections.
|
||||
// event is the type and will be prepended with "custom_<pluginid>_"
|
||||
// payload is the data sent with the event. Interface values must be primitive Go types or mattermost-server/model types
|
||||
// broadcast determines to which users to send the event
|
||||
PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast)
|
||||
|
||||
// LogDebug writes a log message to the Mattermost server log file.
|
||||
// Appropriate context such as the plugin name will already be added as fields so plugins
|
||||
// do not need to add that info.
|
||||
// keyValuePairs should be primitive go types or other values that can be encoded by encoding/gob
|
||||
LogDebug(msg string, keyValuePairs ...interface{})
|
||||
|
||||
// LogInfo writes a log message to the Mattermost server log file.
|
||||
// Appropriate context such as the plugin name will already be added as fields so plugins
|
||||
// do not need to add that info.
|
||||
// keyValuePairs should be primitive go types or other values that can be encoded by encoding/gob
|
||||
LogInfo(msg string, keyValuePairs ...interface{})
|
||||
|
||||
// LogError writes a log message to the Mattermost server log file.
|
||||
// Appropriate context such as the plugin name will already be added as fields so plugins
|
||||
// do not need to add that info.
|
||||
// keyValuePairs should be primitive go types or other values that can be encoded by encoding/gob
|
||||
LogError(msg string, keyValuePairs ...interface{})
|
||||
|
||||
// LogWarn writes a log message to the Mattermost server log file.
|
||||
// Appropriate context such as the plugin name will already be added as fields so plugins
|
||||
// do not need to add that info.
|
||||
// keyValuePairs should be primitive go types or other values that can be encoded by encoding/gob
|
||||
LogWarn(msg string, keyValuePairs ...interface{})
|
||||
}
|
||||
|
||||
var Handshake = plugin.HandshakeConfig{
|
||||
ProtocolVersion: 1,
|
||||
MagicCookieKey: "MATTERMOST_PLUGIN",
|
||||
MagicCookieValue: "Securely message teams, anywhere.",
|
||||
}
|
||||
|
||||
51
plugin/client.go
Обычный файл
51
plugin/client.go
Обычный файл
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
// Starts the serving of a Mattermost plugin over rpc or gRPC
|
||||
// Call this when your plugin is ready to start
|
||||
func ClientMain(pluginImplementation interface{}) {
|
||||
if impl, ok := pluginImplementation.(interface {
|
||||
SetAPI(api API)
|
||||
SetSelfRef(ref interface{})
|
||||
}); !ok {
|
||||
panic("Plugin implementation given must embed plugin.MattermostPlugin")
|
||||
} else {
|
||||
impl.SetAPI(nil)
|
||||
impl.SetSelfRef(pluginImplementation)
|
||||
}
|
||||
|
||||
pluginMap := map[string]plugin.Plugin{
|
||||
"hooks": &HooksPlugin{hooks: pluginImplementation},
|
||||
}
|
||||
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: Handshake,
|
||||
Plugins: pluginMap,
|
||||
})
|
||||
}
|
||||
|
||||
type MattermostPlugin struct {
|
||||
API API
|
||||
selfRef interface{} // This is so we can unmarshal into our parent
|
||||
}
|
||||
|
||||
func (p *MattermostPlugin) SetAPI(api API) {
|
||||
p.API = api
|
||||
}
|
||||
|
||||
func (p *MattermostPlugin) SetSelfRef(ref interface{}) {
|
||||
p.selfRef = ref
|
||||
}
|
||||
|
||||
func (p *MattermostPlugin) OnConfigurationChange() error {
|
||||
if p.selfRef != nil {
|
||||
return p.API.LoadPluginConfiguration(p.selfRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
328
plugin/client_rpc.go
Обычный файл
328
plugin/client_rpc.go
Обычный файл
@@ -0,0 +1,328 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:generate go run interface_generator/main.go
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
var HookNameToId map[string]int = make(map[string]int)
|
||||
|
||||
type HooksRPCClient struct {
|
||||
client *rpc.Client
|
||||
log *mlog.Logger
|
||||
muxBroker *plugin.MuxBroker
|
||||
apiImpl API
|
||||
implemented [TotalHooksId]bool
|
||||
}
|
||||
|
||||
type HooksRPCServer struct {
|
||||
impl interface{}
|
||||
muxBroker *plugin.MuxBroker
|
||||
apiRPCClient *APIRPCClient
|
||||
}
|
||||
|
||||
// Implements hashicorp/go-plugin/plugin.Plugin interface to connect the hooks of a plugin
|
||||
type HooksPlugin struct {
|
||||
hooks interface{}
|
||||
apiImpl API
|
||||
log *mlog.Logger
|
||||
}
|
||||
|
||||
func (p *HooksPlugin) Server(b *plugin.MuxBroker) (interface{}, error) {
|
||||
return &HooksRPCServer{impl: p.hooks, muxBroker: b}, nil
|
||||
}
|
||||
|
||||
func (p *HooksPlugin) Client(b *plugin.MuxBroker, client *rpc.Client) (interface{}, error) {
|
||||
return &HooksRPCClient{client: client, log: p.log, muxBroker: b, apiImpl: p.apiImpl}, nil
|
||||
}
|
||||
|
||||
type APIRPCClient struct {
|
||||
client *rpc.Client
|
||||
log *mlog.Logger
|
||||
}
|
||||
|
||||
type APIRPCServer struct {
|
||||
impl API
|
||||
}
|
||||
|
||||
// Registering some types used by MM for encoding/gob used by rpc
|
||||
func init() {
|
||||
gob.Register([]*model.SlackAttachment{})
|
||||
gob.Register([]interface{}{})
|
||||
gob.Register(map[string]interface{}{})
|
||||
}
|
||||
|
||||
// These enforce compile time checks to make sure types implement the interface
|
||||
// If you are getting an error here, you probably need to run `make pluginapi` to
|
||||
// autogenerate RPC glue code
|
||||
var _ plugin.Plugin = &HooksPlugin{}
|
||||
var _ Hooks = &HooksRPCClient{}
|
||||
|
||||
//
|
||||
// Below are specal cases for hooks or APIs that can not be auto generated
|
||||
//
|
||||
|
||||
func (g *HooksRPCClient) Implemented() (impl []string, err error) {
|
||||
err = g.client.Call("Plugin.Implemented", struct{}{}, &impl)
|
||||
for _, hookName := range impl {
|
||||
if hookId, ok := HookNameToId[hookName]; ok {
|
||||
g.implemented[hookId] = true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Implemented replies with the names of the hooks that are implemented.
|
||||
func (s *HooksRPCServer) Implemented(args struct{}, reply *[]string) error {
|
||||
ifaceType := reflect.TypeOf((*Hooks)(nil)).Elem()
|
||||
implType := reflect.TypeOf(s.impl)
|
||||
selfType := reflect.TypeOf(s)
|
||||
var methods []string
|
||||
for i := 0; i < ifaceType.NumMethod(); i++ {
|
||||
method := ifaceType.Method(i)
|
||||
if m, ok := implType.MethodByName(method.Name); !ok {
|
||||
continue
|
||||
} else if m.Type.NumIn() != method.Type.NumIn()+1 {
|
||||
continue
|
||||
} else if m.Type.NumOut() != method.Type.NumOut() {
|
||||
continue
|
||||
} else {
|
||||
match := true
|
||||
for j := 0; j < method.Type.NumIn(); j++ {
|
||||
if m.Type.In(j+1) != method.Type.In(j) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
for j := 0; j < method.Type.NumOut(); j++ {
|
||||
if m.Type.Out(j) != method.Type.Out(j) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, ok := selfType.MethodByName(method.Name); !ok {
|
||||
continue
|
||||
}
|
||||
methods = append(methods, method.Name)
|
||||
}
|
||||
*reply = methods
|
||||
return nil
|
||||
}
|
||||
|
||||
type OnActivateArgs struct {
|
||||
APIMuxId uint32
|
||||
}
|
||||
|
||||
type OnActivateReturns struct {
|
||||
A error
|
||||
}
|
||||
|
||||
func (g *HooksRPCClient) OnActivate() error {
|
||||
muxId := g.muxBroker.NextId()
|
||||
go g.muxBroker.AcceptAndServe(muxId, &APIRPCServer{
|
||||
impl: g.apiImpl,
|
||||
})
|
||||
|
||||
_args := &OnActivateArgs{
|
||||
APIMuxId: muxId,
|
||||
}
|
||||
_returns := &OnActivateReturns{}
|
||||
|
||||
if err := g.client.Call("Plugin.OnActivate", _args, _returns); err != nil {
|
||||
g.log.Error("RPC call to OnActivate plugin failed.", mlog.Err(err))
|
||||
}
|
||||
return _returns.A
|
||||
}
|
||||
|
||||
func (s *HooksRPCServer) OnActivate(args *OnActivateArgs, returns *OnActivateReturns) error {
|
||||
connection, err := s.muxBroker.Dial(args.APIMuxId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.apiRPCClient = &APIRPCClient{
|
||||
client: rpc.NewClient(connection),
|
||||
}
|
||||
|
||||
if mmplugin, ok := s.impl.(interface {
|
||||
SetAPI(api API)
|
||||
OnConfigurationChange() error
|
||||
}); !ok {
|
||||
} else {
|
||||
mmplugin.SetAPI(s.apiRPCClient)
|
||||
mmplugin.OnConfigurationChange()
|
||||
}
|
||||
|
||||
// Capture output of standard logger because go-plugin
|
||||
// redirects it.
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
if hook, ok := s.impl.(interface {
|
||||
OnActivate() error
|
||||
}); ok {
|
||||
returns.A = hook.OnActivate()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LoadPluginConfigurationArgs struct {
|
||||
}
|
||||
|
||||
type LoadPluginConfigurationReturns struct {
|
||||
A []byte
|
||||
}
|
||||
|
||||
func (g *APIRPCClient) LoadPluginConfiguration(dest interface{}) error {
|
||||
_args := &LoadPluginConfigurationArgs{}
|
||||
_returns := &LoadPluginConfigurationReturns{}
|
||||
if err := g.client.Call("Plugin.LoadPluginConfiguration", _args, _returns); err != nil {
|
||||
g.log.Error("RPC call to LoadPluginConfiguration API failed.", mlog.Err(err))
|
||||
}
|
||||
return json.Unmarshal(_returns.A, dest)
|
||||
}
|
||||
|
||||
func (s *APIRPCServer) LoadPluginConfiguration(args *LoadPluginConfigurationArgs, returns *LoadPluginConfigurationReturns) error {
|
||||
var config interface{}
|
||||
if hook, ok := s.impl.(interface {
|
||||
LoadPluginConfiguration(dest interface{}) error
|
||||
}); ok {
|
||||
if err := hook.LoadPluginConfiguration(&config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
returns.A = b
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
HookNameToId["ServeHTTP"] = ServeHTTPId
|
||||
}
|
||||
|
||||
type ServeHTTPArgs struct {
|
||||
ResponseWriterStream uint32
|
||||
Request *http.Request
|
||||
Context *Context
|
||||
RequestBodyStream uint32
|
||||
}
|
||||
|
||||
func (g *HooksRPCClient) ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !g.implemented[ServeHTTPId] {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
serveHTTPStreamId := g.muxBroker.NextId()
|
||||
go func() {
|
||||
connection, err := g.muxBroker.Accept(serveHTTPStreamId)
|
||||
if err != nil {
|
||||
g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't accept connection", mlog.Uint32("serve_http_stream_id", serveHTTPStreamId), mlog.Err(err))
|
||||
http.Error(w, "500 internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
rpcServer := rpc.NewServer()
|
||||
if err := rpcServer.RegisterName("Plugin", &HTTPResponseWriterRPCServer{w: w}); err != nil {
|
||||
g.log.Error("Plugin failed to ServeHTTP, coulden't register RPC name", mlog.Err(err))
|
||||
http.Error(w, "500 internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rpcServer.ServeConn(connection)
|
||||
}()
|
||||
|
||||
requestBodyStreamId := uint32(0)
|
||||
if r.Body != nil {
|
||||
requestBodyStreamId = g.muxBroker.NextId()
|
||||
go func() {
|
||||
bodyConnection, err := g.muxBroker.Accept(requestBodyStreamId)
|
||||
if err != nil {
|
||||
g.log.Error("Plugin failed to ServeHTTP, muxBroker couldn't Accept request body connecion", mlog.Err(err))
|
||||
http.Error(w, "500 internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer bodyConnection.Close()
|
||||
ServeIOReader(r.Body, bodyConnection)
|
||||
}()
|
||||
}
|
||||
|
||||
forwardedRequest := &http.Request{
|
||||
Method: r.Method,
|
||||
URL: r.URL,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
Header: r.Header,
|
||||
Host: r.Host,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
RequestURI: r.RequestURI,
|
||||
}
|
||||
|
||||
if err := g.client.Call("Plugin.ServeHTTP", ServeHTTPArgs{
|
||||
Context: c,
|
||||
ResponseWriterStream: serveHTTPStreamId,
|
||||
Request: forwardedRequest,
|
||||
RequestBodyStream: requestBodyStreamId,
|
||||
}, nil); err != nil {
|
||||
g.log.Error("Plugin failed to ServeHTTP, RPC call failed", mlog.Err(err))
|
||||
http.Error(w, "500 internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *HooksRPCServer) ServeHTTP(args *ServeHTTPArgs, returns *struct{}) error {
|
||||
connection, err := s.muxBroker.Dial(args.ResponseWriterStream)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote response writer stream, error: %v", err.Error())
|
||||
return err
|
||||
}
|
||||
w := ConnectHTTPResponseWriter(connection)
|
||||
defer w.Close()
|
||||
|
||||
r := args.Request
|
||||
if args.RequestBodyStream != 0 {
|
||||
connection, err := s.muxBroker.Dial(args.RequestBodyStream)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] Can't connect to remote request body stream, error: %v", err.Error())
|
||||
return err
|
||||
}
|
||||
r.Body = ConnectIOReader(connection)
|
||||
} else {
|
||||
r.Body = ioutil.NopCloser(&bytes.Buffer{})
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if hook, ok := s.impl.(interface {
|
||||
ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request)
|
||||
}); ok {
|
||||
hook.ServeHTTP(args.Context, w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1829
plugin/client_rpc_generated.go
Обычный файл
1829
plugin/client_rpc_generated.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
260
plugin/environment.go
Обычный файл
260
plugin/environment.go
Обычный файл
@@ -0,0 +1,260 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type APIImplCreatorFunc func(*model.Manifest) API
|
||||
type SupervisorCreatorFunc func(*model.BundleInfo, *mlog.Logger, API) (*Supervisor, error)
|
||||
|
||||
// Hooks will be the hooks API for the plugin
|
||||
// Return value should be true if we should continue calling more plugins
|
||||
type MultliPluginHookRunnerFunc func(hooks Hooks) bool
|
||||
|
||||
type ActivePlugin struct {
|
||||
BundleInfo *model.BundleInfo
|
||||
State int
|
||||
Supervisor *Supervisor
|
||||
}
|
||||
|
||||
type Environment struct {
|
||||
activePlugins map[string]ActivePlugin
|
||||
mutex sync.RWMutex
|
||||
logger *mlog.Logger
|
||||
newAPIImpl APIImplCreatorFunc
|
||||
pluginDir string
|
||||
webappPluginDir string
|
||||
}
|
||||
|
||||
func NewEnvironment(newAPIImpl APIImplCreatorFunc, pluginDir string, webappPluginDir string, logger *mlog.Logger) (*Environment, error) {
|
||||
return &Environment{
|
||||
activePlugins: make(map[string]ActivePlugin),
|
||||
logger: logger,
|
||||
newAPIImpl: newAPIImpl,
|
||||
pluginDir: pluginDir,
|
||||
webappPluginDir: webappPluginDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Performs a full scan of the given path.
|
||||
//
|
||||
// This function will return info for all subdirectories that appear to be plugins (i.e. all
|
||||
// subdirectories containing plugin manifest files, regardless of whether they could actually be
|
||||
// parsed).
|
||||
//
|
||||
// Plugins are found non-recursively and paths beginning with a dot are always ignored.
|
||||
func ScanSearchPath(path string) ([]*model.BundleInfo, error) {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ret []*model.BundleInfo
|
||||
for _, file := range files {
|
||||
if !file.IsDir() || file.Name()[0] == '.' {
|
||||
continue
|
||||
}
|
||||
if info := model.BundleInfoForPath(filepath.Join(path, file.Name())); info.ManifestPath != "" {
|
||||
ret = append(ret, info)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Returns a list of all plugins within the environment.
|
||||
func (env *Environment) Available() ([]*model.BundleInfo, error) {
|
||||
return ScanSearchPath(env.pluginDir)
|
||||
}
|
||||
|
||||
// Returns a list of all currently active plugins within the environment.
|
||||
func (env *Environment) Active() []*model.BundleInfo {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
activePlugins := []*model.BundleInfo{}
|
||||
for _, p := range env.activePlugins {
|
||||
activePlugins = append(activePlugins, p.BundleInfo)
|
||||
}
|
||||
|
||||
return activePlugins
|
||||
}
|
||||
|
||||
func (env *Environment) IsActive(id string) bool {
|
||||
_, ok := env.activePlugins[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Returns a list of plugin statuses reprensenting the state of every plugin
|
||||
func (env *Environment) Statuses() (model.PluginStatuses, error) {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
plugins, err := env.Available()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get plugin statuses")
|
||||
}
|
||||
|
||||
pluginStatuses := make(model.PluginStatuses, 0, len(plugins))
|
||||
for _, plugin := range plugins {
|
||||
// For now we don't handle bad manifests, we should
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pluginState := model.PluginStateNotRunning
|
||||
if plugin, ok := env.activePlugins[plugin.Manifest.Id]; ok {
|
||||
pluginState = plugin.State
|
||||
}
|
||||
|
||||
status := &model.PluginStatus{
|
||||
PluginId: plugin.Manifest.Id,
|
||||
PluginPath: filepath.Dir(plugin.ManifestPath),
|
||||
State: pluginState,
|
||||
Name: plugin.Manifest.Name,
|
||||
Description: plugin.Manifest.Description,
|
||||
Version: plugin.Manifest.Version,
|
||||
}
|
||||
|
||||
pluginStatuses = append(pluginStatuses, status)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
func (env *Environment) Activate(id string) (reterr error) {
|
||||
env.mutex.Lock()
|
||||
defer env.mutex.Unlock()
|
||||
|
||||
// Check if we are already active
|
||||
if _, ok := env.activePlugins[id]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
plugins, err := env.Available()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var pluginInfo *model.BundleInfo
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
if pluginInfo != nil {
|
||||
return fmt.Errorf("multiple plugins found: %v", id)
|
||||
}
|
||||
pluginInfo = p
|
||||
}
|
||||
}
|
||||
if pluginInfo == nil {
|
||||
return fmt.Errorf("plugin not found: %v", id)
|
||||
}
|
||||
|
||||
activePlugin := ActivePlugin{BundleInfo: pluginInfo}
|
||||
defer func() {
|
||||
if reterr == nil {
|
||||
activePlugin.State = model.PluginStateRunning
|
||||
} else {
|
||||
activePlugin.State = model.PluginStateFailedToStart
|
||||
}
|
||||
env.activePlugins[pluginInfo.Manifest.Id] = activePlugin
|
||||
}()
|
||||
|
||||
if pluginInfo.Manifest.Webapp != nil {
|
||||
bundlePath := filepath.Clean(pluginInfo.Manifest.Webapp.BundlePath)
|
||||
if bundlePath == "" || bundlePath[0] == '.' {
|
||||
return fmt.Errorf("invalid webapp bundle path")
|
||||
}
|
||||
bundlePath = filepath.Join(env.pluginDir, id, bundlePath)
|
||||
|
||||
webappBundle, err := ioutil.ReadFile(bundlePath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to read webapp bundle: %v", id)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(fmt.Sprintf("%s/%s_bundle.js", env.webappPluginDir, id), webappBundle, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to write webapp bundle: %v", id)
|
||||
}
|
||||
}
|
||||
|
||||
if pluginInfo.Manifest.Backend != nil {
|
||||
supervisor, err := NewSupervisor(pluginInfo, env.logger, env.newAPIImpl(pluginInfo.Manifest))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to start plugin: %v", id)
|
||||
}
|
||||
activePlugin.Supervisor = supervisor
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivates the plugin with the given id.
|
||||
func (env *Environment) Deactivate(id string) {
|
||||
env.mutex.Lock()
|
||||
defer env.mutex.Unlock()
|
||||
|
||||
if activePlugin, ok := env.activePlugins[id]; !ok {
|
||||
return
|
||||
} else {
|
||||
delete(env.activePlugins, id)
|
||||
if activePlugin.Supervisor != nil {
|
||||
if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil {
|
||||
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", activePlugin.BundleInfo.Manifest.Id), mlog.Err(err))
|
||||
}
|
||||
activePlugin.Supervisor.Shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deactivates all plugins and gracefully shuts down the environment.
|
||||
func (env *Environment) Shutdown() {
|
||||
env.mutex.Lock()
|
||||
defer env.mutex.Unlock()
|
||||
|
||||
for _, activePlugin := range env.activePlugins {
|
||||
if activePlugin.Supervisor != nil {
|
||||
if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil {
|
||||
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", activePlugin.BundleInfo.Manifest.Id), mlog.Err(err))
|
||||
}
|
||||
activePlugin.Supervisor.Shutdown()
|
||||
}
|
||||
}
|
||||
env.activePlugins = make(map[string]ActivePlugin)
|
||||
return
|
||||
}
|
||||
|
||||
// Returns the hooks API for the plugin ID specified
|
||||
// You should probably use RunMultiPluginHook instead.
|
||||
func (env *Environment) HooksForPlugin(id string) (Hooks, error) {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
if plug, ok := env.activePlugins[id]; ok && plug.Supervisor != nil {
|
||||
return plug.Supervisor.Hooks(), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("plugin not found: %v", id)
|
||||
}
|
||||
|
||||
// Calls hookRunnerFunc with the hooks for each active plugin that implments the given HookId
|
||||
// If hookRunnerFunc returns false, then iteration will not continue.
|
||||
func (env *Environment) RunMultiPluginHook(hookRunnerFunc MultliPluginHookRunnerFunc, mustImplement int) {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
for _, activePlugin := range env.activePlugins {
|
||||
if activePlugin.Supervisor == nil || !activePlugin.Supervisor.Implements(mustImplement) {
|
||||
continue
|
||||
}
|
||||
if !hookRunnerFunc(activePlugin.Supervisor.Hooks()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package plugin_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type HelloUserPlugin struct {
|
||||
api plugin.API
|
||||
}
|
||||
|
||||
func (p *HelloUserPlugin) OnActivate(api plugin.API) error {
|
||||
// Just save api for later when we need to look up users.
|
||||
p.api = api
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HelloUserPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if userId := r.Header.Get("Mattermost-User-Id"); userId == "" {
|
||||
// Our visitor is unauthenticated.
|
||||
fmt.Fprintf(w, "Hello, stranger!")
|
||||
} else if user, err := p.api.GetUser(userId); err == nil {
|
||||
// Greet the user by name!
|
||||
fmt.Fprintf(w, "Welcome back, %v!", user.Username)
|
||||
} else {
|
||||
// This won't happen in normal circumstances, but let's just be safe.
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// This example demonstrates a plugin that handles HTTP requests which respond by greeting the user
|
||||
// by name.
|
||||
func Example_helloUser() {
|
||||
rpcplugin.Main(&HelloUserPlugin{})
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package plugin_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type HelloWorldPlugin struct{}
|
||||
|
||||
func (p *HelloWorldPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hello, world!")
|
||||
}
|
||||
|
||||
// This example demonstrates a plugin that handles HTTP requests which respond by greeting the
|
||||
// world.
|
||||
func Example_helloWorld() {
|
||||
rpcplugin.Main(&HelloWorldPlugin{})
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package plugin_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
)
|
||||
|
||||
func TestHelloUserPlugin(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "billybob",
|
||||
}
|
||||
|
||||
api := &plugintest.API{}
|
||||
api.On("GetUser", user.Id).Return(user, nil)
|
||||
defer api.AssertExpectations(t)
|
||||
|
||||
p := &HelloUserPlugin{}
|
||||
p.OnActivate(api)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r.Header.Add("Mattermost-User-Id", user.Id)
|
||||
p.ServeHTTP(w, r)
|
||||
body, err := ioutil.ReadAll(w.Result().Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Welcome back, billybob!", string(body))
|
||||
}
|
||||
99
plugin/hclog_adapter.go
Обычный файл
99
plugin/hclog_adapter.go
Обычный файл
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
)
|
||||
|
||||
type HclogAdapter struct {
|
||||
wrappedLogger *mlog.Logger
|
||||
extrasKey string
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) Trace(msg string, args ...interface{}) {
|
||||
extras := strings.TrimSpace(fmt.Sprint(args...))
|
||||
if extras != "" {
|
||||
h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, extras))
|
||||
} else {
|
||||
h.wrappedLogger.Debug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) Debug(msg string, args ...interface{}) {
|
||||
extras := strings.TrimSpace(fmt.Sprint(args...))
|
||||
if extras != "" {
|
||||
h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, extras))
|
||||
} else {
|
||||
h.wrappedLogger.Debug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) Info(msg string, args ...interface{}) {
|
||||
extras := strings.TrimSpace(fmt.Sprint(args...))
|
||||
if extras != "" {
|
||||
h.wrappedLogger.Info(msg, mlog.String(h.extrasKey, extras))
|
||||
} else {
|
||||
h.wrappedLogger.Info(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) Warn(msg string, args ...interface{}) {
|
||||
extras := strings.TrimSpace(fmt.Sprint(args...))
|
||||
if extras != "" {
|
||||
h.wrappedLogger.Warn(msg, mlog.String(h.extrasKey, extras))
|
||||
} else {
|
||||
h.wrappedLogger.Warn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) Error(msg string, args ...interface{}) {
|
||||
extras := strings.TrimSpace(fmt.Sprint(args...))
|
||||
if extras != "" {
|
||||
h.wrappedLogger.Error(msg, mlog.String(h.extrasKey, extras))
|
||||
} else {
|
||||
h.wrappedLogger.Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) IsTrace() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) IsDebug() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) IsInfo() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) IsWarn() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) IsError() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) With(args ...interface{}) hclog.Logger {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) Named(name string) hclog.Logger {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) ResetNamed(name string) hclog.Logger {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
|
||||
return h.wrappedLogger.StdLog()
|
||||
}
|
||||
@@ -9,15 +9,37 @@ import (
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
// These assignments are part of the wire protocol. You can add more, but should not change existing
|
||||
// assignments. Follow the naming convention of <HookName>Id as the autogenerated glue code depends on that.
|
||||
const (
|
||||
OnActivateId = 0
|
||||
OnDeactivateId = 1
|
||||
ServeHTTPId = 2
|
||||
OnConfigurationChangeId = 3
|
||||
ExecuteCommandId = 4
|
||||
MessageWillBePostedId = 5
|
||||
MessageWillBeUpdatedId = 6
|
||||
MessageHasBeenPostedId = 7
|
||||
MessageHasBeenUpdatedId = 8
|
||||
UserHasJoinedChannelId = 9
|
||||
UserHasLeftChannelId = 10
|
||||
UserHasJoinedTeamId = 11
|
||||
UserHasLeftTeamId = 12
|
||||
ChannelHasBeenCreatedId = 13
|
||||
TotalHooksId = iota
|
||||
)
|
||||
|
||||
// Methods from the Hooks interface can be used by a plugin to respond to events. Methods are likely
|
||||
// to be added over time, and plugins are not expected to implement all of them. Instead, plugins
|
||||
// are expected to implement a subset of them and pass an instance to plugin/rpcplugin.Main, which
|
||||
// will take over execution of the process and add default behaviors for missing hooks.
|
||||
type Hooks interface {
|
||||
// OnActivate is invoked when the plugin is activated. Implementations will usually want to save
|
||||
// the api argument for later use. Loading configuration for the first time is also a commonly
|
||||
// done here.
|
||||
OnActivate(API) error
|
||||
// OnActivate is invoked when the plugin is activated.
|
||||
OnActivate() error
|
||||
|
||||
// Implemented returns a list of hooks that are implmented by the plugin.
|
||||
// Plugins do not need to provide an implementation. Any given will be ignored.
|
||||
Implemented() ([]string, error)
|
||||
|
||||
// OnDeactivate is invoked when the plugin is deactivated. This is the plugin's last chance to
|
||||
// use the API, and the plugin will be terminated shortly after this invocation.
|
||||
@@ -31,13 +53,13 @@ type Hooks interface {
|
||||
//
|
||||
// The Mattermost-User-Id header will be present if (and only if) the request is by an
|
||||
// authenticated user.
|
||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
||||
ServeHTTP(c *Context, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// ExecuteCommand executes a command that has been previously registered via the RegisterCommand
|
||||
// API.
|
||||
ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError)
|
||||
ExecuteCommand(c *Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError)
|
||||
|
||||
// MessageWillBePosted is invoked when a message is posted by a user before it is commited
|
||||
// MessageWillBePosted is invoked when a message is posted by a user before it is committed
|
||||
// to the database. If you also want to act on edited posts, see MessageWillBeUpdated.
|
||||
// Return values should be the modified post or nil if rejected and an explanation for the user.
|
||||
//
|
||||
@@ -45,9 +67,9 @@ type Hooks interface {
|
||||
//
|
||||
// Note that this method will be called for posts created by plugins, including the plugin that
|
||||
// created the post.
|
||||
MessageWillBePosted(post *model.Post) (*model.Post, string)
|
||||
MessageWillBePosted(c *Context, post *model.Post) (*model.Post, string)
|
||||
|
||||
// MessageWillBeUpdated is invoked when a message is updated by a user before it is commited
|
||||
// MessageWillBeUpdated is invoked when a message is updated by a user before it is committed
|
||||
// to the database. If you also want to act on new posts, see MessageWillBePosted.
|
||||
// Return values should be the modified post or nil if rejected and an explanation for the user.
|
||||
// On rejection, the post will be kept in its previous state.
|
||||
@@ -56,17 +78,36 @@ type Hooks interface {
|
||||
//
|
||||
// Note that this method will be called for posts updated by plugins, including the plugin that
|
||||
// updated the post.
|
||||
MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string)
|
||||
MessageWillBeUpdated(c *Context, newPost, oldPost *model.Post) (*model.Post, string)
|
||||
|
||||
// MessageHasBeenPosted is invoked after the message has been commited to the databse.
|
||||
// MessageHasBeenPosted is invoked after the message has been committed to the database.
|
||||
// If you need to modify or reject the post, see MessageWillBePosted
|
||||
// Note that this method will be called for posts created by plugins, including the plugin that
|
||||
// created the post.
|
||||
MessageHasBeenPosted(post *model.Post)
|
||||
MessageHasBeenPosted(c *Context, post *model.Post)
|
||||
|
||||
// MessageHasBeenUpdated is invoked after a message is updated and has been updated in the databse.
|
||||
// MessageHasBeenUpdated is invoked after a message is updated and has been updated in the database.
|
||||
// If you need to modify or reject the post, see MessageWillBeUpdated
|
||||
// Note that this method will be called for posts created by plugins, including the plugin that
|
||||
// created the post.
|
||||
MessageHasBeenUpdated(newPost, oldPost *model.Post)
|
||||
MessageHasBeenUpdated(c *Context, newPost, oldPost *model.Post)
|
||||
|
||||
// ChannelHasBeenCreated is invoked after the channel has been committed to the database.
|
||||
ChannelHasBeenCreated(c *Context, channel *model.Channel)
|
||||
|
||||
// UserHasJoinedChannel is invoked after the membership has been committed to the database.
|
||||
// If actor is not nil, the user was invited to the channel by the actor.
|
||||
UserHasJoinedChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
|
||||
|
||||
// UserHasLeftChannel is invoked after the membership has been removed from the database.
|
||||
// If actor is not nil, the user was removed from the channel by the actor.
|
||||
UserHasLeftChannel(c *Context, channelMember *model.ChannelMember, actor *model.User)
|
||||
|
||||
// UserHasJoinedTeam is invoked after the membership has been committed to the database.
|
||||
// If actor is not nil, the user was added to the team by the actor.
|
||||
UserHasJoinedTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
|
||||
|
||||
// UserHasLeftTeam is invoked after the membership has been removed from the database.
|
||||
// If actor is not nil, the user was removed from the team by the actor.
|
||||
UserHasLeftTeam(c *Context, teamMember *model.TeamMember, actor *model.User)
|
||||
}
|
||||
|
||||
91
plugin/http.go
Обычный файл
91
plugin/http.go
Обычный файл
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
type HTTPResponseWriterRPCServer struct {
|
||||
w http.ResponseWriter
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCServer) Header(args struct{}, reply *http.Header) error {
|
||||
*reply = w.w.Header()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCServer) Write(args []byte, reply *struct{}) error {
|
||||
_, err := w.w.Write(args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCServer) WriteHeader(args int, reply *struct{}) error {
|
||||
w.w.WriteHeader(args)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCServer) SyncHeader(args http.Header, reply *struct{}) error {
|
||||
dest := w.w.Header()
|
||||
for k := range dest {
|
||||
if _, ok := args[k]; !ok {
|
||||
delete(dest, k)
|
||||
}
|
||||
}
|
||||
for k, v := range args {
|
||||
dest[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ServeHTTPResponseWriter(w http.ResponseWriter, conn io.ReadWriteCloser) {
|
||||
server := rpc.NewServer()
|
||||
server.Register(&HTTPResponseWriterRPCServer{
|
||||
w: w,
|
||||
})
|
||||
server.ServeConn(conn)
|
||||
}
|
||||
|
||||
type HTTPResponseWriterRPCClient struct {
|
||||
client *rpc.Client
|
||||
header http.Header
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = (*HTTPResponseWriterRPCClient)(nil)
|
||||
|
||||
func (w *HTTPResponseWriterRPCClient) Header() http.Header {
|
||||
if w.header == nil {
|
||||
w.client.Call("Plugin.Header", struct{}{}, &w.header)
|
||||
}
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCClient) Write(b []byte) (int, error) {
|
||||
if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := w.client.Call("Plugin.Write", b, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCClient) WriteHeader(statusCode int) {
|
||||
if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil {
|
||||
return
|
||||
}
|
||||
w.client.Call("Plugin.WriteHeader", statusCode, nil)
|
||||
}
|
||||
|
||||
func (h *HTTPResponseWriterRPCClient) Close() error {
|
||||
return h.client.Close()
|
||||
}
|
||||
|
||||
func ConnectHTTPResponseWriter(conn io.ReadWriteCloser) *HTTPResponseWriterRPCClient {
|
||||
return &HTTPResponseWriterRPCClient{
|
||||
client: rpc.NewClient(conn),
|
||||
}
|
||||
}
|
||||
394
plugin/interface_generator/main.go
Обычный файл
394
plugin/interface_generator/main.go
Обычный файл
@@ -0,0 +1,394 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/template"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type IHookEntry struct {
|
||||
FuncName string
|
||||
Args *ast.FieldList
|
||||
Results *ast.FieldList
|
||||
}
|
||||
|
||||
type PluginInterfaceInfo struct {
|
||||
Hooks []IHookEntry
|
||||
API []IHookEntry
|
||||
FileSet *token.FileSet
|
||||
}
|
||||
|
||||
func FieldListToFuncList(fieldList *ast.FieldList, fileset *token.FileSet) string {
|
||||
result := []string{}
|
||||
if fieldList == nil || len(fieldList.List) == 0 {
|
||||
return "()"
|
||||
}
|
||||
for _, field := range fieldList.List {
|
||||
typeNameBuffer := &bytes.Buffer{}
|
||||
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
typeName := typeNameBuffer.String()
|
||||
names := []string{}
|
||||
for _, name := range field.Names {
|
||||
names = append(names, name.Name)
|
||||
}
|
||||
result = append(result, strings.Join(names, ", ")+" "+typeName)
|
||||
}
|
||||
|
||||
return "(" + strings.Join(result, ", ") + ")"
|
||||
}
|
||||
|
||||
func FieldListToNames(fieldList *ast.FieldList, fileset *token.FileSet) string {
|
||||
result := []string{}
|
||||
if fieldList == nil || len(fieldList.List) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, field := range fieldList.List {
|
||||
for _, name := range field.Names {
|
||||
result = append(result, name.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
func FieldListDestruct(structPrefix string, fieldList *ast.FieldList, fileset *token.FileSet) string {
|
||||
result := []string{}
|
||||
if fieldList == nil || len(fieldList.List) == 0 {
|
||||
return ""
|
||||
}
|
||||
nextLetter := 'A'
|
||||
for _, field := range fieldList.List {
|
||||
typeNameBuffer := &bytes.Buffer{}
|
||||
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
typeName := typeNameBuffer.String()
|
||||
suffix := ""
|
||||
if strings.HasPrefix(typeName, "...") {
|
||||
suffix = "..."
|
||||
}
|
||||
if len(field.Names) == 0 {
|
||||
result = append(result, structPrefix+string(nextLetter)+suffix)
|
||||
nextLetter += 1
|
||||
} else {
|
||||
for range field.Names {
|
||||
result = append(result, structPrefix+string(nextLetter)+suffix)
|
||||
nextLetter += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
func FieldListToStructList(fieldList *ast.FieldList, fileset *token.FileSet) string {
|
||||
result := []string{}
|
||||
if fieldList == nil || len(fieldList.List) == 0 {
|
||||
return ""
|
||||
}
|
||||
nextLetter := 'A'
|
||||
for _, field := range fieldList.List {
|
||||
typeNameBuffer := &bytes.Buffer{}
|
||||
err := printer.Fprint(typeNameBuffer, fileset, field.Type)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
typeName := typeNameBuffer.String()
|
||||
if strings.HasPrefix(typeName, "...") {
|
||||
typeName = strings.Replace(typeName, "...", "[]", 1)
|
||||
}
|
||||
if len(field.Names) == 0 {
|
||||
result = append(result, string(nextLetter)+" "+typeName)
|
||||
nextLetter += 1
|
||||
} else {
|
||||
for range field.Names {
|
||||
result = append(result, string(nextLetter)+" "+typeName)
|
||||
nextLetter += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n\t")
|
||||
}
|
||||
|
||||
func goList(dir string) ([]string, error) {
|
||||
cmd := exec.Command("go", "list", "-f", "{{.Dir}}", dir)
|
||||
bytes, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Can't list packages")
|
||||
}
|
||||
|
||||
return strings.Fields(string(bytes)), nil
|
||||
}
|
||||
|
||||
func (info *PluginInterfaceInfo) addHookMethod(method *ast.Field) {
|
||||
info.Hooks = append(info.Hooks, IHookEntry{
|
||||
FuncName: method.Names[0].Name,
|
||||
Args: method.Type.(*ast.FuncType).Params,
|
||||
Results: method.Type.(*ast.FuncType).Results,
|
||||
})
|
||||
}
|
||||
|
||||
func (info *PluginInterfaceInfo) addAPIMethod(method *ast.Field) {
|
||||
info.API = append(info.API, IHookEntry{
|
||||
FuncName: method.Names[0].Name,
|
||||
Args: method.Type.(*ast.FuncType).Params,
|
||||
Results: method.Type.(*ast.FuncType).Results,
|
||||
})
|
||||
}
|
||||
|
||||
func (info *PluginInterfaceInfo) makeHookInspector() func(node ast.Node) bool {
|
||||
return func(node ast.Node) bool {
|
||||
if typeSpec, ok := node.(*ast.TypeSpec); ok {
|
||||
if typeSpec.Name.Name == "Hooks" {
|
||||
for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List {
|
||||
info.addHookMethod(method)
|
||||
}
|
||||
return false
|
||||
} else if typeSpec.Name.Name == "API" {
|
||||
for _, method := range typeSpec.Type.(*ast.InterfaceType).Methods.List {
|
||||
info.addAPIMethod(method)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func getPluginInfo(dir string) (*PluginInterfaceInfo, error) {
|
||||
pluginInfo := &PluginInterfaceInfo{
|
||||
Hooks: make([]IHookEntry, 0),
|
||||
FileSet: token.NewFileSet(),
|
||||
}
|
||||
|
||||
packages, err := parser.ParseDir(pluginInfo.FileSet, dir, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
log.Println("Parser error in dir "+dir+": ", err)
|
||||
}
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg.Name != "plugin" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range pkg.Files {
|
||||
ast.Inspect(file, pluginInfo.makeHookInspector())
|
||||
}
|
||||
}
|
||||
|
||||
return pluginInfo, nil
|
||||
}
|
||||
|
||||
var hooksTemplate = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Code generated by "make pluginapi"
|
||||
// DO NOT EDIT
|
||||
|
||||
package plugin
|
||||
|
||||
{{range .HooksMethods}}
|
||||
|
||||
func init() {
|
||||
HookNameToId["{{.Name}}"] = {{.Name}}Id
|
||||
}
|
||||
|
||||
type {{.Name}}Args struct {
|
||||
{{structStyle .Params}}
|
||||
}
|
||||
|
||||
type {{.Name}}Returns struct {
|
||||
{{structStyle .Return}}
|
||||
}
|
||||
|
||||
func (g *HooksRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
||||
_args := &{{.Name}}Args{ {{valuesOnly .Params}} }
|
||||
_returns := &{{.Name}}Returns{}
|
||||
if g.implemented[{{.Name}}Id] {
|
||||
if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
|
||||
g.log.Error("RPC call {{.Name}} to plugin failed.", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
return {{destruct "_returns." .Return}}
|
||||
}
|
||||
|
||||
func (s *HooksRPCServer) {{.Name}}(args *{{.Name}}Args, returns *{{.Name}}Returns) error {
|
||||
if hook, ok := s.impl.(interface {
|
||||
{{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
|
||||
}); ok {
|
||||
{{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}})
|
||||
} else {
|
||||
return fmt.Errorf("Hook {{.Name}} called but not implemented.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{range .APIMethods}}
|
||||
|
||||
type {{.Name}}Args struct {
|
||||
{{structStyle .Params}}
|
||||
}
|
||||
|
||||
type {{.Name}}Returns struct {
|
||||
{{structStyle .Return}}
|
||||
}
|
||||
|
||||
func (g *APIRPCClient) {{.Name}}{{funcStyle .Params}} {{funcStyle .Return}} {
|
||||
_args := &{{.Name}}Args{ {{valuesOnly .Params}} }
|
||||
_returns := &{{.Name}}Returns{}
|
||||
if err := g.client.Call("Plugin.{{.Name}}", _args, _returns); err != nil {
|
||||
g.log.Error("RPC call to {{.Name}} API failed.", mlog.Err(err))
|
||||
}
|
||||
return {{destruct "_returns." .Return}}
|
||||
}
|
||||
|
||||
func (s *APIRPCServer) {{.Name}}(args *{{.Name}}Args, returns *{{.Name}}Returns) error {
|
||||
if hook, ok := s.impl.(interface {
|
||||
{{.Name}}{{funcStyle .Params}} {{funcStyle .Return}}
|
||||
}); ok {
|
||||
{{if .Return}}{{destruct "returns." .Return}} = {{end}}hook.{{.Name}}({{destruct "args." .Params}})
|
||||
} else {
|
||||
return fmt.Errorf("API {{.Name}} called but not implemented.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
type MethodParams struct {
|
||||
Name string
|
||||
Params *ast.FieldList
|
||||
Return *ast.FieldList
|
||||
}
|
||||
|
||||
type HooksTemplateParams struct {
|
||||
HooksMethods []MethodParams
|
||||
APIMethods []MethodParams
|
||||
}
|
||||
|
||||
func generateGlue(info *PluginInterfaceInfo) {
|
||||
templateFunctions := map[string]interface{}{
|
||||
"funcStyle": func(fields *ast.FieldList) string { return FieldListToFuncList(fields, info.FileSet) },
|
||||
"structStyle": func(fields *ast.FieldList) string { return FieldListToStructList(fields, info.FileSet) },
|
||||
"valuesOnly": func(fields *ast.FieldList) string { return FieldListToNames(fields, info.FileSet) },
|
||||
"destruct": func(structPrefix string, fields *ast.FieldList) string {
|
||||
return FieldListDestruct(structPrefix, fields, info.FileSet)
|
||||
},
|
||||
}
|
||||
|
||||
hooksTemplate, err := template.New("hooks").Funcs(templateFunctions).Parse(hooksTemplate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
templateParams := HooksTemplateParams{}
|
||||
for _, hook := range info.Hooks {
|
||||
templateParams.HooksMethods = append(templateParams.HooksMethods, MethodParams{
|
||||
Name: hook.FuncName,
|
||||
Params: hook.Args,
|
||||
Return: hook.Results,
|
||||
})
|
||||
}
|
||||
for _, api := range info.API {
|
||||
templateParams.APIMethods = append(templateParams.APIMethods, MethodParams{
|
||||
Name: api.FuncName,
|
||||
Params: api.Args,
|
||||
Return: api.Results,
|
||||
})
|
||||
}
|
||||
templateResult := &bytes.Buffer{}
|
||||
hooksTemplate.Execute(templateResult, &templateParams)
|
||||
|
||||
importsBuffer := &bytes.Buffer{}
|
||||
cmd := exec.Command("goimports")
|
||||
cmd.Stdin = templateResult
|
||||
cmd.Stdout = importsBuffer
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(getPluginPackageDir(), "client_rpc_generated.go"), importsBuffer.Bytes(), 0664); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func getPluginPackageDir() string {
|
||||
dirs, err := goList("github.com/mattermost/mattermost-server/plugin")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
} else if len(dirs) != 1 {
|
||||
panic("More than one package dir, or no dirs!")
|
||||
}
|
||||
|
||||
return dirs[0]
|
||||
}
|
||||
|
||||
func removeExcluded(info *PluginInterfaceInfo) *PluginInterfaceInfo {
|
||||
toBeExcluded := func(item string) bool {
|
||||
excluded := []string{
|
||||
"OnActivate",
|
||||
"Implemented",
|
||||
"LoadPluginConfiguration",
|
||||
"ServeHTTP",
|
||||
}
|
||||
for _, exclusion := range excluded {
|
||||
if exclusion == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
hooksResult := make([]IHookEntry, 0, len(info.Hooks))
|
||||
for _, hook := range info.Hooks {
|
||||
if !toBeExcluded(hook.FuncName) {
|
||||
hooksResult = append(hooksResult, hook)
|
||||
}
|
||||
}
|
||||
info.Hooks = hooksResult
|
||||
|
||||
apiResult := make([]IHookEntry, 0, len(info.API))
|
||||
for _, api := range info.API {
|
||||
if !toBeExcluded(api.FuncName) {
|
||||
apiResult = append(apiResult, api)
|
||||
}
|
||||
}
|
||||
info.API = apiResult
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func main() {
|
||||
pluginPackageDir := getPluginPackageDir()
|
||||
|
||||
log.Println("Generating plugin glue")
|
||||
info, err := getPluginInfo(pluginPackageDir)
|
||||
if err != nil {
|
||||
fmt.Println("Unable to get plugin info: " + err.Error())
|
||||
}
|
||||
|
||||
info = removeExcluded(info)
|
||||
|
||||
generateGlue(info)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package rpcplugin
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
1018
plugin/mock_api_test.go
Обычный файл
1018
plugin/mock_api_test.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// The plugin package defines the primary interfaces for interacting with a Mattermost server: the
|
||||
// API and the hook interfaces.
|
||||
//
|
||||
// The API interface is used to perform actions. The Hook interface is used to respond to actions.
|
||||
//
|
||||
// Plugins should define a type that implements some of the methods from the Hook interface, then
|
||||
// pass an instance of that object into the rpcplugin package's Main function (See the HelloWorld
|
||||
// example.).
|
||||
//
|
||||
// Testing
|
||||
//
|
||||
// To make testing plugins easier, you can use the plugintest package to create a mock API for your
|
||||
// plugin to interact with. See
|
||||
// https://godoc.org/github.com/mattermost/mattermost-server/plugin/plugintest
|
||||
package plugin
|
||||
@@ -1,396 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// Package pluginenv provides high level functionality for discovering and launching plugins.
|
||||
package pluginenv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
type APIProviderFunc func(*model.Manifest) (plugin.API, error)
|
||||
type SupervisorProviderFunc func(*model.BundleInfo) (plugin.Supervisor, error)
|
||||
|
||||
type ActivePlugin struct {
|
||||
BundleInfo *model.BundleInfo
|
||||
Supervisor plugin.Supervisor
|
||||
}
|
||||
|
||||
// Environment represents an environment that plugins are discovered and launched in.
|
||||
type Environment struct {
|
||||
searchPath string
|
||||
webappPath string
|
||||
apiProvider APIProviderFunc
|
||||
supervisorProvider SupervisorProviderFunc
|
||||
activePlugins map[string]ActivePlugin
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
type Option func(*Environment)
|
||||
|
||||
// Creates a new environment. At a minimum, the APIProvider and SearchPath options are required.
|
||||
func New(options ...Option) (*Environment, error) {
|
||||
env := &Environment{
|
||||
activePlugins: make(map[string]ActivePlugin),
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(env)
|
||||
}
|
||||
if env.supervisorProvider == nil {
|
||||
env.supervisorProvider = DefaultSupervisorProvider
|
||||
}
|
||||
if env.searchPath == "" {
|
||||
return nil, fmt.Errorf("a search path must be provided")
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// Returns the configured webapp path.
|
||||
func (env *Environment) WebappPath() string {
|
||||
return env.webappPath
|
||||
}
|
||||
|
||||
// Returns the configured search path.
|
||||
func (env *Environment) SearchPath() string {
|
||||
return env.searchPath
|
||||
}
|
||||
|
||||
// Returns a list of all plugins found within the environment.
|
||||
func (env *Environment) Plugins() ([]*model.BundleInfo, error) {
|
||||
return ScanSearchPath(env.searchPath)
|
||||
}
|
||||
|
||||
// Returns a list of all currently active plugins within the environment.
|
||||
func (env *Environment) ActivePlugins() []*model.BundleInfo {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
activePlugins := []*model.BundleInfo{}
|
||||
for _, p := range env.activePlugins {
|
||||
activePlugins = append(activePlugins, p.BundleInfo)
|
||||
}
|
||||
|
||||
return activePlugins
|
||||
}
|
||||
|
||||
// Returns the ids of the currently active plugins.
|
||||
func (env *Environment) ActivePluginIds() (ids []string) {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
for id := range env.activePlugins {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Returns true if the plugin is active, false otherwise.
|
||||
func (env *Environment) IsPluginActive(pluginId string) bool {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
for id := range env.activePlugins {
|
||||
if id == pluginId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Activates the plugin with the given id.
|
||||
func (env *Environment) ActivatePlugin(id string, onError func(error)) error {
|
||||
env.mutex.Lock()
|
||||
defer env.mutex.Unlock()
|
||||
|
||||
if !plugin.IsValidId(id) {
|
||||
return fmt.Errorf("invalid plugin id: %s", id)
|
||||
}
|
||||
|
||||
if _, ok := env.activePlugins[id]; ok {
|
||||
return fmt.Errorf("plugin already active: %v", id)
|
||||
}
|
||||
plugins, err := ScanSearchPath(env.searchPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var bundle *model.BundleInfo
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
if bundle != nil {
|
||||
return fmt.Errorf("multiple plugins found: %v", id)
|
||||
}
|
||||
bundle = p
|
||||
}
|
||||
}
|
||||
if bundle == nil {
|
||||
return fmt.Errorf("plugin not found: %v", id)
|
||||
}
|
||||
|
||||
activePlugin := ActivePlugin{BundleInfo: bundle}
|
||||
|
||||
var supervisor plugin.Supervisor
|
||||
|
||||
if bundle.Manifest.Backend != nil {
|
||||
if env.apiProvider == nil {
|
||||
return fmt.Errorf("env missing api provider, cannot activate plugin: %v", id)
|
||||
}
|
||||
|
||||
supervisor, err = env.supervisorProvider(bundle)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to create supervisor for plugin: %v", id)
|
||||
}
|
||||
api, err := env.apiProvider(bundle.Manifest)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to get api for plugin: %v", id)
|
||||
}
|
||||
if err := supervisor.Start(api); err != nil {
|
||||
return errors.Wrapf(err, "unable to start plugin: %v", id)
|
||||
}
|
||||
if onError != nil {
|
||||
go func() {
|
||||
err := supervisor.Wait()
|
||||
if err != nil {
|
||||
onError(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
activePlugin.Supervisor = supervisor
|
||||
}
|
||||
|
||||
if bundle.Manifest.Webapp != nil {
|
||||
if env.webappPath == "" {
|
||||
if supervisor != nil {
|
||||
supervisor.Stop()
|
||||
}
|
||||
return fmt.Errorf("env missing webapp path, cannot activate plugin: %v", id)
|
||||
}
|
||||
|
||||
bundlePath := filepath.Clean(bundle.Manifest.Webapp.BundlePath)
|
||||
if bundlePath == "" || bundlePath[0] == '.' {
|
||||
return fmt.Errorf("invalid webapp bundle path")
|
||||
}
|
||||
bundlePath = filepath.Join(env.searchPath, id, bundlePath)
|
||||
|
||||
webappBundle, err := ioutil.ReadFile(bundlePath)
|
||||
if err != nil {
|
||||
// Backwards compatibility for plugins where webapp.bundle_path was ignored. This should
|
||||
// be removed eventually.
|
||||
if webappBundle2, err2 := ioutil.ReadFile(fmt.Sprintf("%s/%s/webapp/%s_bundle.js", env.searchPath, id, id)); err2 == nil {
|
||||
webappBundle = webappBundle2
|
||||
} else {
|
||||
if supervisor != nil {
|
||||
supervisor.Stop()
|
||||
}
|
||||
return errors.Wrapf(err, "unable to read webapp bundle: %v", id)
|
||||
}
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(fmt.Sprintf("%s/%s_bundle.js", env.webappPath, id), webappBundle, 0644)
|
||||
if err != nil {
|
||||
if supervisor != nil {
|
||||
supervisor.Stop()
|
||||
}
|
||||
return errors.Wrapf(err, "unable to write webapp bundle: %v", id)
|
||||
}
|
||||
}
|
||||
|
||||
env.activePlugins[id] = activePlugin
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivates the plugin with the given id.
|
||||
func (env *Environment) DeactivatePlugin(id string) error {
|
||||
env.mutex.Lock()
|
||||
defer env.mutex.Unlock()
|
||||
|
||||
if activePlugin, ok := env.activePlugins[id]; !ok {
|
||||
return fmt.Errorf("plugin not active: %v", id)
|
||||
} else {
|
||||
delete(env.activePlugins, id)
|
||||
var err error
|
||||
if activePlugin.Supervisor != nil {
|
||||
err = activePlugin.Supervisor.Hooks().OnDeactivate()
|
||||
if serr := activePlugin.Supervisor.Stop(); err == nil {
|
||||
err = serr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Deactivates all plugins and gracefully shuts down the environment.
|
||||
func (env *Environment) Shutdown() (errs []error) {
|
||||
env.mutex.Lock()
|
||||
defer env.mutex.Unlock()
|
||||
|
||||
for _, activePlugin := range env.activePlugins {
|
||||
if activePlugin.Supervisor != nil {
|
||||
if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil {
|
||||
errs = append(errs, errors.Wrapf(err, "OnDeactivate() error for %v", activePlugin.BundleInfo.Manifest.Id))
|
||||
}
|
||||
if err := activePlugin.Supervisor.Stop(); err != nil {
|
||||
errs = append(errs, errors.Wrapf(err, "error stopping supervisor for %v", activePlugin.BundleInfo.Manifest.Id))
|
||||
}
|
||||
}
|
||||
}
|
||||
env.activePlugins = make(map[string]ActivePlugin)
|
||||
return
|
||||
}
|
||||
|
||||
type MultiPluginHooks struct {
|
||||
env *Environment
|
||||
}
|
||||
|
||||
type SinglePluginHooks struct {
|
||||
env *Environment
|
||||
pluginId string
|
||||
}
|
||||
|
||||
func (env *Environment) Hooks() *MultiPluginHooks {
|
||||
return &MultiPluginHooks{
|
||||
env: env,
|
||||
}
|
||||
}
|
||||
|
||||
func (env *Environment) HooksForPlugin(id string) *SinglePluginHooks {
|
||||
return &SinglePluginHooks{
|
||||
env: env,
|
||||
pluginId: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MultiPluginHooks) invoke(f func(plugin.Hooks) error) (errs []error) {
|
||||
h.env.mutex.RLock()
|
||||
defer h.env.mutex.RUnlock()
|
||||
|
||||
for _, activePlugin := range h.env.activePlugins {
|
||||
if activePlugin.Supervisor == nil {
|
||||
continue
|
||||
}
|
||||
if err := f(activePlugin.Supervisor.Hooks()); err != nil {
|
||||
errs = append(errs, errors.Wrapf(err, "hook error for %v", activePlugin.BundleInfo.Manifest.Id))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// OnConfigurationChange invokes the OnConfigurationChange hook for all plugins. Any errors
|
||||
// encountered will be returned.
|
||||
func (h *MultiPluginHooks) OnConfigurationChange() []error {
|
||||
return h.invoke(func(hooks plugin.Hooks) error {
|
||||
if err := hooks.OnConfigurationChange(); err != nil {
|
||||
return errors.Wrapf(err, "error calling OnConfigurationChange hook")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ServeHTTP invokes the ServeHTTP hook for the plugin identified by the request or responds with a
|
||||
// 404 not found.
|
||||
//
|
||||
// It expects the request's context to have a plugin_id set.
|
||||
func (h *MultiPluginHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if id := r.Context().Value("plugin_id"); id != nil {
|
||||
if idstr, ok := id.(string); ok {
|
||||
h.env.mutex.RLock()
|
||||
defer h.env.mutex.RUnlock()
|
||||
if plugin, ok := h.env.activePlugins[idstr]; ok && plugin.Supervisor != nil {
|
||||
plugin.Supervisor.Hooks().ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
// MessageWillBePosted invokes the MessageWillBePosted hook for all plugins. Ordering
|
||||
// is not guaranteed and the next plugin will get the previous one's modifications.
|
||||
// if a plugin rejects a post, the rest of the plugins will not know that an attempt was made.
|
||||
// Returns the final result post, or nil if the post was rejected and a string with a reason
|
||||
// for the user the message was rejected.
|
||||
func (h *MultiPluginHooks) MessageWillBePosted(post *model.Post) (*model.Post, string) {
|
||||
h.env.mutex.RLock()
|
||||
defer h.env.mutex.RUnlock()
|
||||
|
||||
for _, activePlugin := range h.env.activePlugins {
|
||||
if activePlugin.Supervisor == nil {
|
||||
continue
|
||||
}
|
||||
var rejectionReason string
|
||||
post, rejectionReason = activePlugin.Supervisor.Hooks().MessageWillBePosted(post)
|
||||
if post == nil {
|
||||
return nil, rejectionReason
|
||||
}
|
||||
}
|
||||
return post, ""
|
||||
}
|
||||
|
||||
// MessageWillBeUpdated invokes the MessageWillBeUpdated hook for all plugins. Ordering
|
||||
// is not guaranteed and the next plugin will get the previous one's modifications.
|
||||
// if a plugin rejects a post, the rest of the plugins will not know that an attempt was made.
|
||||
// Returns the final result post, or nil if the post was rejected and a string with a reason
|
||||
// for the user the message was rejected.
|
||||
func (h *MultiPluginHooks) MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string) {
|
||||
h.env.mutex.RLock()
|
||||
defer h.env.mutex.RUnlock()
|
||||
|
||||
post := newPost
|
||||
for _, activePlugin := range h.env.activePlugins {
|
||||
if activePlugin.Supervisor == nil {
|
||||
continue
|
||||
}
|
||||
var rejectionReason string
|
||||
post, rejectionReason = activePlugin.Supervisor.Hooks().MessageWillBeUpdated(post, oldPost)
|
||||
if post == nil {
|
||||
return nil, rejectionReason
|
||||
}
|
||||
}
|
||||
return post, ""
|
||||
}
|
||||
|
||||
func (h *MultiPluginHooks) MessageHasBeenPosted(post *model.Post) {
|
||||
h.invoke(func(hooks plugin.Hooks) error {
|
||||
hooks.MessageHasBeenPosted(post)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MultiPluginHooks) MessageHasBeenUpdated(newPost, oldPost *model.Post) {
|
||||
h.invoke(func(hooks plugin.Hooks) error {
|
||||
hooks.MessageHasBeenUpdated(newPost, oldPost)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SinglePluginHooks) invoke(f func(plugin.Hooks) error) error {
|
||||
h.env.mutex.RLock()
|
||||
defer h.env.mutex.RUnlock()
|
||||
|
||||
if activePlugin, ok := h.env.activePlugins[h.pluginId]; ok && activePlugin.Supervisor != nil {
|
||||
if err := f(activePlugin.Supervisor.Hooks()); err != nil {
|
||||
return errors.Wrapf(err, "hook error for plugin: %v", activePlugin.BundleInfo.Manifest.Id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unable to invoke hook for plugin: %v", h.pluginId)
|
||||
}
|
||||
|
||||
// ExecuteCommand invokes the ExecuteCommand hook for the plugin.
|
||||
func (h *SinglePluginHooks) ExecuteCommand(args *model.CommandArgs) (resp *model.CommandResponse, appErr *model.AppError, err error) {
|
||||
err = h.invoke(func(hooks plugin.Hooks) error {
|
||||
resp, appErr = hooks.ExecuteCommand(args)
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
package pluginenv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
)
|
||||
|
||||
type MockProvider struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockProvider) API(manifest *model.Manifest) (plugin.API, error) {
|
||||
ret := m.Called()
|
||||
if ret.Get(0) == nil {
|
||||
return nil, ret.Error(1)
|
||||
}
|
||||
return ret.Get(0).(plugin.API), ret.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockProvider) Supervisor(bundle *model.BundleInfo) (plugin.Supervisor, error) {
|
||||
ret := m.Called()
|
||||
if ret.Get(0) == nil {
|
||||
return nil, ret.Error(1)
|
||||
}
|
||||
return ret.Get(0).(plugin.Supervisor), ret.Error(1)
|
||||
}
|
||||
|
||||
type MockSupervisor struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockSupervisor) Start(api plugin.API) error {
|
||||
return m.Called(api).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockSupervisor) Stop() error {
|
||||
return m.Called().Error(0)
|
||||
}
|
||||
|
||||
func (m *MockSupervisor) Hooks() plugin.Hooks {
|
||||
return m.Called().Get(0).(plugin.Hooks)
|
||||
}
|
||||
|
||||
func (m *MockSupervisor) Wait() error {
|
||||
return m.Called().Get(0).(error)
|
||||
}
|
||||
|
||||
func initTmpDir(t *testing.T, files map[string]string) string {
|
||||
success := false
|
||||
dir, err := ioutil.TempDir("", "mm-plugin-test")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if !success {
|
||||
os.RemoveAll(dir)
|
||||
}
|
||||
}()
|
||||
|
||||
for name, contents := range files {
|
||||
path := filepath.Join(dir, name)
|
||||
parent := filepath.Dir(path)
|
||||
require.NoError(t, os.MkdirAll(parent, 0700))
|
||||
f, err := os.Create(path)
|
||||
require.NoError(t, err)
|
||||
_, err = f.WriteString(contents)
|
||||
f.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
success = true
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestNew_MissingOptions(t *testing.T) {
|
||||
dir := initTmpDir(t, map[string]string{
|
||||
"foo/plugin.json": `{"id": "foo"}`,
|
||||
})
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var provider MockProvider
|
||||
defer provider.AssertExpectations(t)
|
||||
|
||||
env, err := New(
|
||||
APIProvider(provider.API),
|
||||
)
|
||||
assert.Nil(t, env)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEnvironment(t *testing.T) {
|
||||
dir := initTmpDir(t, map[string]string{
|
||||
".foo/plugin.json": `{"id": "foo"}`,
|
||||
"foo/bar": "asdf",
|
||||
"foo/plugin.json": `{"id": "foo", "backend": {}}`,
|
||||
"bar/zxc": "qwer",
|
||||
"baz/plugin.yaml": "id: baz",
|
||||
"bad/plugin.json": "asd",
|
||||
"qwe": "asd",
|
||||
})
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
webappDir := "notarealdirectory"
|
||||
|
||||
var provider MockProvider
|
||||
defer provider.AssertExpectations(t)
|
||||
|
||||
env, err := New(
|
||||
SearchPath(dir),
|
||||
WebappPath(webappDir),
|
||||
APIProvider(provider.API),
|
||||
SupervisorProvider(provider.Supervisor),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer env.Shutdown()
|
||||
|
||||
plugins, err := env.Plugins()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, plugins, 3)
|
||||
|
||||
activePlugins := env.ActivePlugins()
|
||||
assert.Len(t, activePlugins, 0)
|
||||
|
||||
assert.Error(t, env.ActivatePlugin("x", nil))
|
||||
|
||||
var api struct{ plugin.API }
|
||||
var supervisor MockSupervisor
|
||||
defer supervisor.AssertExpectations(t)
|
||||
var hooks plugintest.Hooks
|
||||
defer hooks.AssertExpectations(t)
|
||||
|
||||
provider.On("API").Return(&api, nil)
|
||||
provider.On("Supervisor").Return(&supervisor, nil)
|
||||
|
||||
supervisor.On("Start", &api).Return(nil)
|
||||
supervisor.On("Stop").Return(nil)
|
||||
supervisor.On("Hooks").Return(&hooks)
|
||||
|
||||
assert.NoError(t, env.ActivatePlugin("foo", nil))
|
||||
assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
|
||||
activePlugins = env.ActivePlugins()
|
||||
assert.Len(t, activePlugins, 1)
|
||||
assert.Error(t, env.ActivatePlugin("foo", nil))
|
||||
assert.True(t, env.IsPluginActive("foo"))
|
||||
|
||||
hooks.On("OnDeactivate").Return(nil)
|
||||
assert.NoError(t, env.DeactivatePlugin("foo"))
|
||||
assert.Error(t, env.DeactivatePlugin("foo"))
|
||||
assert.False(t, env.IsPluginActive("foo"))
|
||||
|
||||
assert.NoError(t, env.ActivatePlugin("foo", nil))
|
||||
assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
|
||||
|
||||
assert.Equal(t, env.SearchPath(), dir)
|
||||
assert.Equal(t, env.WebappPath(), webappDir)
|
||||
|
||||
assert.Empty(t, env.Shutdown())
|
||||
}
|
||||
|
||||
func TestEnvironment_DuplicatePluginError(t *testing.T) {
|
||||
dir := initTmpDir(t, map[string]string{
|
||||
"foo/plugin.json": `{"id": "foo"}`,
|
||||
"foo2/plugin.json": `{"id": "foo"}`,
|
||||
})
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var provider MockProvider
|
||||
defer provider.AssertExpectations(t)
|
||||
|
||||
env, err := New(
|
||||
SearchPath(dir),
|
||||
APIProvider(provider.API),
|
||||
SupervisorProvider(provider.Supervisor),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer env.Shutdown()
|
||||
|
||||
assert.Error(t, env.ActivatePlugin("foo", nil))
|
||||
assert.Empty(t, env.ActivePluginIds())
|
||||
}
|
||||
|
||||
func TestEnvironment_BadSearchPathError(t *testing.T) {
|
||||
var provider MockProvider
|
||||
defer provider.AssertExpectations(t)
|
||||
|
||||
env, err := New(
|
||||
SearchPath("thissearchpathshouldnotexist!"),
|
||||
APIProvider(provider.API),
|
||||
SupervisorProvider(provider.Supervisor),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer env.Shutdown()
|
||||
|
||||
assert.Error(t, env.ActivatePlugin("foo", nil))
|
||||
assert.Empty(t, env.ActivePluginIds())
|
||||
}
|
||||
|
||||
func TestEnvironment_ActivatePluginErrors(t *testing.T) {
|
||||
dir := initTmpDir(t, map[string]string{
|
||||
"foo/plugin.json": `{"id": "foo", "backend": {}}`,
|
||||
})
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var provider MockProvider
|
||||
|
||||
env, err := New(
|
||||
SearchPath(dir),
|
||||
APIProvider(provider.API),
|
||||
SupervisorProvider(provider.Supervisor),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer env.Shutdown()
|
||||
|
||||
var api struct{ plugin.API }
|
||||
var supervisor MockSupervisor
|
||||
var hooks plugintest.Hooks
|
||||
|
||||
for name, setup := range map[string]func(){
|
||||
"SupervisorProviderError": func() {
|
||||
provider.On("Supervisor").Return(nil, fmt.Errorf("test error"))
|
||||
},
|
||||
"APIProviderError": func() {
|
||||
provider.On("API").Return(plugin.API(nil), fmt.Errorf("test error"))
|
||||
provider.On("Supervisor").Return(&supervisor, nil)
|
||||
},
|
||||
"SupervisorError": func() {
|
||||
provider.On("API").Return(&api, nil)
|
||||
provider.On("Supervisor").Return(&supervisor, nil)
|
||||
|
||||
supervisor.On("Start", &api).Return(fmt.Errorf("test error"))
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
supervisor.Mock = mock.Mock{}
|
||||
hooks.Mock = mock.Mock{}
|
||||
provider.Mock = mock.Mock{}
|
||||
setup()
|
||||
assert.Error(t, env.ActivatePlugin("foo", nil))
|
||||
assert.Empty(t, env.ActivePluginIds())
|
||||
supervisor.AssertExpectations(t)
|
||||
hooks.AssertExpectations(t)
|
||||
provider.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironment_ShutdownError(t *testing.T) {
|
||||
dir := initTmpDir(t, map[string]string{
|
||||
"foo/plugin.json": `{"id": "foo", "backend": {}}`,
|
||||
})
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var provider MockProvider
|
||||
defer provider.AssertExpectations(t)
|
||||
|
||||
env, err := New(
|
||||
SearchPath(dir),
|
||||
APIProvider(provider.API),
|
||||
SupervisorProvider(provider.Supervisor),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer env.Shutdown()
|
||||
|
||||
var api struct{ plugin.API }
|
||||
var supervisor MockSupervisor
|
||||
defer supervisor.AssertExpectations(t)
|
||||
var hooks plugintest.Hooks
|
||||
defer hooks.AssertExpectations(t)
|
||||
|
||||
provider.On("API").Return(&api, nil)
|
||||
provider.On("Supervisor").Return(&supervisor, nil)
|
||||
|
||||
supervisor.On("Start", &api).Return(nil)
|
||||
supervisor.On("Stop").Return(fmt.Errorf("test error"))
|
||||
supervisor.On("Hooks").Return(&hooks)
|
||||
|
||||
hooks.On("OnDeactivate").Return(fmt.Errorf("test error"))
|
||||
|
||||
assert.NoError(t, env.ActivatePlugin("foo", nil))
|
||||
assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
|
||||
assert.Len(t, env.Shutdown(), 2)
|
||||
}
|
||||
|
||||
func TestEnvironment_ConcurrentHookInvocations(t *testing.T) {
|
||||
dir := initTmpDir(t, map[string]string{
|
||||
"foo/plugin.json": `{"id": "foo", "backend": {}}`,
|
||||
})
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var provider MockProvider
|
||||
defer provider.AssertExpectations(t)
|
||||
|
||||
var api struct{ plugin.API }
|
||||
var supervisor MockSupervisor
|
||||
defer supervisor.AssertExpectations(t)
|
||||
var hooks plugintest.Hooks
|
||||
defer hooks.AssertExpectations(t)
|
||||
|
||||
env, err := New(
|
||||
SearchPath(dir),
|
||||
APIProvider(provider.API),
|
||||
SupervisorProvider(provider.Supervisor),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer env.Shutdown()
|
||||
|
||||
provider.On("API").Return(&api, nil)
|
||||
provider.On("Supervisor").Return(&supervisor, nil)
|
||||
|
||||
supervisor.On("Start", &api).Return(nil)
|
||||
supervisor.On("Stop").Return(nil)
|
||||
supervisor.On("Hooks").Return(&hooks)
|
||||
|
||||
ch := make(chan bool)
|
||||
|
||||
hooks.On("OnDeactivate").Return(nil)
|
||||
hooks.On("ServeHTTP", mock.AnythingOfType("*httptest.ResponseRecorder"), mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) {
|
||||
r := args.Get(1).(*http.Request)
|
||||
if r.URL.Path == "/1" {
|
||||
<-ch
|
||||
} else {
|
||||
ch <- true
|
||||
}
|
||||
})
|
||||
|
||||
assert.NoError(t, env.ActivatePlugin("foo", nil))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
req, err := http.NewRequest("GET", "/1", nil)
|
||||
require.NoError(t, err)
|
||||
env.Hooks().ServeHTTP(rec, req.WithContext(context.WithValue(context.Background(), "plugin_id", "foo")))
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
req, err := http.NewRequest("GET", "/2", nil)
|
||||
require.NoError(t, err)
|
||||
env.Hooks().ServeHTTP(rec, req.WithContext(context.WithValue(context.Background(), "plugin_id", "foo")))
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestEnvironment_HooksForPlugins(t *testing.T) {
|
||||
dir := initTmpDir(t, map[string]string{
|
||||
"foo/plugin.json": `{"id": "foo", "backend": {}}`,
|
||||
})
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
var provider MockProvider
|
||||
defer provider.AssertExpectations(t)
|
||||
|
||||
env, err := New(
|
||||
SearchPath(dir),
|
||||
APIProvider(provider.API),
|
||||
SupervisorProvider(provider.Supervisor),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer env.Shutdown()
|
||||
|
||||
var api struct{ plugin.API }
|
||||
var supervisor MockSupervisor
|
||||
defer supervisor.AssertExpectations(t)
|
||||
var hooks plugintest.Hooks
|
||||
defer hooks.AssertExpectations(t)
|
||||
|
||||
provider.On("API").Return(&api, nil)
|
||||
provider.On("Supervisor").Return(&supervisor, nil)
|
||||
|
||||
supervisor.On("Start", &api).Return(nil)
|
||||
supervisor.On("Stop").Return(nil)
|
||||
supervisor.On("Hooks").Return(&hooks)
|
||||
|
||||
hooks.On("OnDeactivate").Return(nil)
|
||||
hooks.On("ExecuteCommand", mock.AnythingOfType("*model.CommandArgs")).Return(&model.CommandResponse{
|
||||
Text: "bar",
|
||||
}, nil)
|
||||
|
||||
assert.NoError(t, env.ActivatePlugin("foo", nil))
|
||||
assert.Equal(t, env.ActivePluginIds(), []string{"foo"})
|
||||
|
||||
resp, appErr, err := env.HooksForPlugin("foo").ExecuteCommand(&model.CommandArgs{
|
||||
Command: "/foo",
|
||||
})
|
||||
assert.Equal(t, "bar", resp.Text)
|
||||
assert.Nil(t, appErr)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Empty(t, env.Shutdown())
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package pluginenv
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin/sandbox"
|
||||
)
|
||||
|
||||
// APIProvider specifies a function that provides an API implementation to each plugin.
|
||||
func APIProvider(provider APIProviderFunc) Option {
|
||||
return func(env *Environment) {
|
||||
env.apiProvider = provider
|
||||
}
|
||||
}
|
||||
|
||||
// SupervisorProvider specifies a function that provides a Supervisor implementation to each plugin.
|
||||
// If unspecified, DefaultSupervisorProvider is used.
|
||||
func SupervisorProvider(provider SupervisorProviderFunc) Option {
|
||||
return func(env *Environment) {
|
||||
env.supervisorProvider = provider
|
||||
}
|
||||
}
|
||||
|
||||
// SearchPath specifies a directory that contains the plugins to launch.
|
||||
func SearchPath(path string) Option {
|
||||
return func(env *Environment) {
|
||||
env.searchPath = path
|
||||
}
|
||||
}
|
||||
|
||||
// WebappPath specifies the static directory serving the webapp.
|
||||
func WebappPath(path string) Option {
|
||||
return func(env *Environment) {
|
||||
env.webappPath = path
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultSupervisorProvider chooses a supervisor based on the system and the plugin's manifest
|
||||
// contents. E.g. if the manifest specifies a backend executable, it will be given an
|
||||
// rpcplugin.Supervisor.
|
||||
func DefaultSupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) {
|
||||
if err := sandbox.CheckSupport(); err == nil {
|
||||
return sandbox.SupervisorProvider(bundle)
|
||||
}
|
||||
return rpcplugin.SupervisorProvider(bundle)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package pluginenv
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
func TestDefaultSupervisorProvider(t *testing.T) {
|
||||
_, err := DefaultSupervisorProvider(&model.BundleInfo{})
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = DefaultSupervisorProvider(&model.BundleInfo{
|
||||
Manifest: &model.Manifest{},
|
||||
})
|
||||
assert.Error(t, err)
|
||||
|
||||
supervisor, err := DefaultSupervisorProvider(&model.BundleInfo{
|
||||
Manifest: &model.Manifest{
|
||||
Backend: &model.ManifestBackend{
|
||||
Executable: "foo",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, ok := supervisor.(*rpcplugin.Supervisor)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package pluginenv
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
// Performs a full scan of the given path.
|
||||
//
|
||||
// This function will return info for all subdirectories that appear to be plugins (i.e. all
|
||||
// subdirectories containing plugin manifest files, regardless of whether they could actually be
|
||||
// parsed).
|
||||
//
|
||||
// Plugins are found non-recursively and paths beginning with a dot are always ignored.
|
||||
func ScanSearchPath(path string) ([]*model.BundleInfo, error) {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ret []*model.BundleInfo
|
||||
for _, file := range files {
|
||||
if !file.IsDir() || file.Name()[0] == '.' {
|
||||
continue
|
||||
}
|
||||
if info := model.BundleInfoForPath(filepath.Join(path, file.Name())); info.ManifestPath != "" {
|
||||
ret = append(ret, info)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package pluginenv
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
func TestScanSearchPath(t *testing.T) {
|
||||
dir := initTmpDir(t, map[string]string{
|
||||
".foo/plugin.json": `{"id": "foo"}`,
|
||||
"foo/bar": "asdf",
|
||||
"foo/plugin.json": `{"id": "foo"}`,
|
||||
"bar/zxc": "qwer",
|
||||
"baz/plugin.yaml": "id: baz",
|
||||
"bad/plugin.json": "asd",
|
||||
"qwe": "asd",
|
||||
})
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
plugins, err := ScanSearchPath(dir)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, plugins, 3)
|
||||
assert.Contains(t, plugins, &model.BundleInfo{
|
||||
Path: filepath.Join(dir, "foo"),
|
||||
ManifestPath: filepath.Join(dir, "foo", "plugin.json"),
|
||||
Manifest: &model.Manifest{
|
||||
Id: "foo",
|
||||
},
|
||||
})
|
||||
assert.Contains(t, plugins, &model.BundleInfo{
|
||||
Path: filepath.Join(dir, "baz"),
|
||||
ManifestPath: filepath.Join(dir, "baz", "plugin.yaml"),
|
||||
Manifest: &model.Manifest{
|
||||
Id: "baz",
|
||||
},
|
||||
})
|
||||
foundError := false
|
||||
for _, x := range plugins {
|
||||
if x.ManifestError != nil {
|
||||
assert.Equal(t, x.Path, filepath.Join(dir, "bad"))
|
||||
assert.Equal(t, x.ManifestPath, filepath.Join(dir, "bad", "plugin.json"))
|
||||
syntexError, ok := x.ManifestError.(*json.SyntaxError)
|
||||
assert.True(t, ok)
|
||||
assert.EqualValues(t, 1, syntexError.Offset)
|
||||
foundError = true
|
||||
}
|
||||
}
|
||||
assert.True(t, foundError)
|
||||
}
|
||||
|
||||
func TestScanSearchPath_Error(t *testing.T) {
|
||||
plugins, err := ScanSearchPath("not a valid path!")
|
||||
assert.Nil(t, plugins)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -6,15 +6,14 @@ package plugintest
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import model "github.com/mattermost/mattermost-server/model"
|
||||
import plugin "github.com/mattermost/mattermost-server/plugin"
|
||||
|
||||
// APIMOCKINTERNAL is an autogenerated mock type for the APIMOCKINTERNAL type
|
||||
type APIMOCKINTERNAL struct {
|
||||
// API is an autogenerated mock type for the API type
|
||||
type API struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// AddChannelMember provides a mock function with given fields: channelId, userId
|
||||
func (_m *APIMOCKINTERNAL) AddChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
|
||||
func (_m *API) AddChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
|
||||
ret := _m.Called(channelId, userId)
|
||||
|
||||
var r0 *model.ChannelMember
|
||||
@@ -39,7 +38,7 @@ func (_m *APIMOCKINTERNAL) AddChannelMember(channelId string, userId string) (*m
|
||||
}
|
||||
|
||||
// CreateChannel provides a mock function with given fields: channel
|
||||
func (_m *APIMOCKINTERNAL) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||
func (_m *API) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||
ret := _m.Called(channel)
|
||||
|
||||
var r0 *model.Channel
|
||||
@@ -64,7 +63,7 @@ func (_m *APIMOCKINTERNAL) CreateChannel(channel *model.Channel) (*model.Channel
|
||||
}
|
||||
|
||||
// CreatePost provides a mock function with given fields: post
|
||||
func (_m *APIMOCKINTERNAL) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
func (_m *API) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
ret := _m.Called(post)
|
||||
|
||||
var r0 *model.Post
|
||||
@@ -89,7 +88,7 @@ func (_m *APIMOCKINTERNAL) CreatePost(post *model.Post) (*model.Post, *model.App
|
||||
}
|
||||
|
||||
// CreateTeam provides a mock function with given fields: team
|
||||
func (_m *APIMOCKINTERNAL) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
func (_m *API) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
ret := _m.Called(team)
|
||||
|
||||
var r0 *model.Team
|
||||
@@ -113,8 +112,58 @@ func (_m *APIMOCKINTERNAL) CreateTeam(team *model.Team) (*model.Team, *model.App
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreateTeamMember provides a mock function with given fields: teamId, userId
|
||||
func (_m *API) CreateTeamMember(teamId string, userId string) (*model.TeamMember, *model.AppError) {
|
||||
ret := _m.Called(teamId, userId)
|
||||
|
||||
var r0 *model.TeamMember
|
||||
if rf, ok := ret.Get(0).(func(string, string) *model.TeamMember); ok {
|
||||
r0 = rf(teamId, userId)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.TeamMember)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(1).(func(string, string) *model.AppError); ok {
|
||||
r1 = rf(teamId, userId)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreateTeamMembers provides a mock function with given fields: teamId, userIds, requestorId
|
||||
func (_m *API) CreateTeamMembers(teamId string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError) {
|
||||
ret := _m.Called(teamId, userIds, requestorId)
|
||||
|
||||
var r0 []*model.TeamMember
|
||||
if rf, ok := ret.Get(0).(func(string, []string, string) []*model.TeamMember); ok {
|
||||
r0 = rf(teamId, userIds, requestorId)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*model.TeamMember)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(1).(func(string, []string, string) *model.AppError); ok {
|
||||
r1 = rf(teamId, userIds, requestorId)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreateUser provides a mock function with given fields: user
|
||||
func (_m *APIMOCKINTERNAL) CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
func (_m *API) CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
ret := _m.Called(user)
|
||||
|
||||
var r0 *model.User
|
||||
@@ -139,7 +188,7 @@ func (_m *APIMOCKINTERNAL) CreateUser(user *model.User) (*model.User, *model.App
|
||||
}
|
||||
|
||||
// DeleteChannel provides a mock function with given fields: channelId
|
||||
func (_m *APIMOCKINTERNAL) DeleteChannel(channelId string) *model.AppError {
|
||||
func (_m *API) DeleteChannel(channelId string) *model.AppError {
|
||||
ret := _m.Called(channelId)
|
||||
|
||||
var r0 *model.AppError
|
||||
@@ -155,7 +204,7 @@ func (_m *APIMOCKINTERNAL) DeleteChannel(channelId string) *model.AppError {
|
||||
}
|
||||
|
||||
// DeleteChannelMember provides a mock function with given fields: channelId, userId
|
||||
func (_m *APIMOCKINTERNAL) DeleteChannelMember(channelId string, userId string) *model.AppError {
|
||||
func (_m *API) DeleteChannelMember(channelId string, userId string) *model.AppError {
|
||||
ret := _m.Called(channelId, userId)
|
||||
|
||||
var r0 *model.AppError
|
||||
@@ -171,7 +220,7 @@ func (_m *APIMOCKINTERNAL) DeleteChannelMember(channelId string, userId string)
|
||||
}
|
||||
|
||||
// DeletePost provides a mock function with given fields: postId
|
||||
func (_m *APIMOCKINTERNAL) DeletePost(postId string) *model.AppError {
|
||||
func (_m *API) DeletePost(postId string) *model.AppError {
|
||||
ret := _m.Called(postId)
|
||||
|
||||
var r0 *model.AppError
|
||||
@@ -187,7 +236,7 @@ func (_m *APIMOCKINTERNAL) DeletePost(postId string) *model.AppError {
|
||||
}
|
||||
|
||||
// DeleteTeam provides a mock function with given fields: teamId
|
||||
func (_m *APIMOCKINTERNAL) DeleteTeam(teamId string) *model.AppError {
|
||||
func (_m *API) DeleteTeam(teamId string) *model.AppError {
|
||||
ret := _m.Called(teamId)
|
||||
|
||||
var r0 *model.AppError
|
||||
@@ -202,8 +251,24 @@ func (_m *APIMOCKINTERNAL) DeleteTeam(teamId string) *model.AppError {
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteTeamMember provides a mock function with given fields: teamId, userId, requestorId
|
||||
func (_m *API) DeleteTeamMember(teamId string, userId string, requestorId string) *model.AppError {
|
||||
ret := _m.Called(teamId, userId, requestorId)
|
||||
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) *model.AppError); ok {
|
||||
r0 = rf(teamId, userId, requestorId)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteUser provides a mock function with given fields: userId
|
||||
func (_m *APIMOCKINTERNAL) DeleteUser(userId string) *model.AppError {
|
||||
func (_m *API) DeleteUser(userId string) *model.AppError {
|
||||
ret := _m.Called(userId)
|
||||
|
||||
var r0 *model.AppError
|
||||
@@ -219,7 +284,7 @@ func (_m *APIMOCKINTERNAL) DeleteUser(userId string) *model.AppError {
|
||||
}
|
||||
|
||||
// GetChannel provides a mock function with given fields: channelId
|
||||
func (_m *APIMOCKINTERNAL) GetChannel(channelId string) (*model.Channel, *model.AppError) {
|
||||
func (_m *API) GetChannel(channelId string) (*model.Channel, *model.AppError) {
|
||||
ret := _m.Called(channelId)
|
||||
|
||||
var r0 *model.Channel
|
||||
@@ -244,7 +309,7 @@ func (_m *APIMOCKINTERNAL) GetChannel(channelId string) (*model.Channel, *model.
|
||||
}
|
||||
|
||||
// GetChannelByName provides a mock function with given fields: name, teamId
|
||||
func (_m *APIMOCKINTERNAL) GetChannelByName(name string, teamId string) (*model.Channel, *model.AppError) {
|
||||
func (_m *API) GetChannelByName(name string, teamId string) (*model.Channel, *model.AppError) {
|
||||
ret := _m.Called(name, teamId)
|
||||
|
||||
var r0 *model.Channel
|
||||
@@ -269,7 +334,7 @@ func (_m *APIMOCKINTERNAL) GetChannelByName(name string, teamId string) (*model.
|
||||
}
|
||||
|
||||
// GetChannelMember provides a mock function with given fields: channelId, userId
|
||||
func (_m *APIMOCKINTERNAL) GetChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
|
||||
func (_m *API) GetChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
|
||||
ret := _m.Called(channelId, userId)
|
||||
|
||||
var r0 *model.ChannelMember
|
||||
@@ -293,8 +358,24 @@ func (_m *APIMOCKINTERNAL) GetChannelMember(channelId string, userId string) (*m
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetConfig provides a mock function with given fields:
|
||||
func (_m *API) GetConfig() *model.Config {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *model.Config
|
||||
if rf, ok := ret.Get(0).(func() *model.Config); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.Config)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetDirectChannel provides a mock function with given fields: userId1, userId2
|
||||
func (_m *APIMOCKINTERNAL) GetDirectChannel(userId1 string, userId2 string) (*model.Channel, *model.AppError) {
|
||||
func (_m *API) GetDirectChannel(userId1 string, userId2 string) (*model.Channel, *model.AppError) {
|
||||
ret := _m.Called(userId1, userId2)
|
||||
|
||||
var r0 *model.Channel
|
||||
@@ -319,7 +400,7 @@ func (_m *APIMOCKINTERNAL) GetDirectChannel(userId1 string, userId2 string) (*mo
|
||||
}
|
||||
|
||||
// GetGroupChannel provides a mock function with given fields: userIds
|
||||
func (_m *APIMOCKINTERNAL) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
|
||||
func (_m *API) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
|
||||
ret := _m.Called(userIds)
|
||||
|
||||
var r0 *model.Channel
|
||||
@@ -344,7 +425,7 @@ func (_m *APIMOCKINTERNAL) GetGroupChannel(userIds []string) (*model.Channel, *m
|
||||
}
|
||||
|
||||
// GetPost provides a mock function with given fields: postId
|
||||
func (_m *APIMOCKINTERNAL) GetPost(postId string) (*model.Post, *model.AppError) {
|
||||
func (_m *API) GetPost(postId string) (*model.Post, *model.AppError) {
|
||||
ret := _m.Called(postId)
|
||||
|
||||
var r0 *model.Post
|
||||
@@ -368,8 +449,33 @@ func (_m *APIMOCKINTERNAL) GetPost(postId string) (*model.Post, *model.AppError)
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetPublicChannelsForTeam provides a mock function with given fields: teamId, offset, limit
|
||||
func (_m *API) GetPublicChannelsForTeam(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError) {
|
||||
ret := _m.Called(teamId, offset, limit)
|
||||
|
||||
var r0 *model.ChannelList
|
||||
if rf, ok := ret.Get(0).(func(string, int, int) *model.ChannelList); ok {
|
||||
r0 = rf(teamId, offset, limit)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.ChannelList)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
|
||||
r1 = rf(teamId, offset, limit)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTeam provides a mock function with given fields: teamId
|
||||
func (_m *APIMOCKINTERNAL) GetTeam(teamId string) (*model.Team, *model.AppError) {
|
||||
func (_m *API) GetTeam(teamId string) (*model.Team, *model.AppError) {
|
||||
ret := _m.Called(teamId)
|
||||
|
||||
var r0 *model.Team
|
||||
@@ -394,7 +500,7 @@ func (_m *APIMOCKINTERNAL) GetTeam(teamId string) (*model.Team, *model.AppError)
|
||||
}
|
||||
|
||||
// GetTeamByName provides a mock function with given fields: name
|
||||
func (_m *APIMOCKINTERNAL) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
func (_m *API) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *model.Team
|
||||
@@ -418,8 +524,83 @@ func (_m *APIMOCKINTERNAL) GetTeamByName(name string) (*model.Team, *model.AppEr
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTeamMember provides a mock function with given fields: teamId, userId
|
||||
func (_m *API) GetTeamMember(teamId string, userId string) (*model.TeamMember, *model.AppError) {
|
||||
ret := _m.Called(teamId, userId)
|
||||
|
||||
var r0 *model.TeamMember
|
||||
if rf, ok := ret.Get(0).(func(string, string) *model.TeamMember); ok {
|
||||
r0 = rf(teamId, userId)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.TeamMember)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(1).(func(string, string) *model.AppError); ok {
|
||||
r1 = rf(teamId, userId)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTeamMembers provides a mock function with given fields: teamId, offset, limit
|
||||
func (_m *API) GetTeamMembers(teamId string, offset int, limit int) ([]*model.TeamMember, *model.AppError) {
|
||||
ret := _m.Called(teamId, offset, limit)
|
||||
|
||||
var r0 []*model.TeamMember
|
||||
if rf, ok := ret.Get(0).(func(string, int, int) []*model.TeamMember); ok {
|
||||
r0 = rf(teamId, offset, limit)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*model.TeamMember)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(1).(func(string, int, int) *model.AppError); ok {
|
||||
r1 = rf(teamId, offset, limit)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetTeams provides a mock function with given fields:
|
||||
func (_m *API) GetTeams() ([]*model.Team, *model.AppError) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*model.Team
|
||||
if rf, ok := ret.Get(0).(func() []*model.Team); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*model.Team)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(1).(func() *model.AppError); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetUser provides a mock function with given fields: userId
|
||||
func (_m *APIMOCKINTERNAL) GetUser(userId string) (*model.User, *model.AppError) {
|
||||
func (_m *API) GetUser(userId string) (*model.User, *model.AppError) {
|
||||
ret := _m.Called(userId)
|
||||
|
||||
var r0 *model.User
|
||||
@@ -444,7 +625,7 @@ func (_m *APIMOCKINTERNAL) GetUser(userId string) (*model.User, *model.AppError)
|
||||
}
|
||||
|
||||
// GetUserByEmail provides a mock function with given fields: email
|
||||
func (_m *APIMOCKINTERNAL) GetUserByEmail(email string) (*model.User, *model.AppError) {
|
||||
func (_m *API) GetUserByEmail(email string) (*model.User, *model.AppError) {
|
||||
ret := _m.Called(email)
|
||||
|
||||
var r0 *model.User
|
||||
@@ -469,7 +650,7 @@ func (_m *APIMOCKINTERNAL) GetUserByEmail(email string) (*model.User, *model.App
|
||||
}
|
||||
|
||||
// GetUserByUsername provides a mock function with given fields: name
|
||||
func (_m *APIMOCKINTERNAL) GetUserByUsername(name string) (*model.User, *model.AppError) {
|
||||
func (_m *API) GetUserByUsername(name string) (*model.User, *model.AppError) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *model.User
|
||||
@@ -493,16 +674,57 @@ func (_m *APIMOCKINTERNAL) GetUserByUsername(name string) (*model.User, *model.A
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// KeyValueStore provides a mock function with given fields:
|
||||
func (_m *APIMOCKINTERNAL) KeyValueStore() plugin.KeyValueStore {
|
||||
ret := _m.Called()
|
||||
// KVDelete provides a mock function with given fields: key
|
||||
func (_m *API) KVDelete(key string) *model.AppError {
|
||||
ret := _m.Called(key)
|
||||
|
||||
var r0 plugin.KeyValueStore
|
||||
if rf, ok := ret.Get(0).(func() plugin.KeyValueStore); ok {
|
||||
r0 = rf()
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
|
||||
r0 = rf(key)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(plugin.KeyValueStore)
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// KVGet provides a mock function with given fields: key
|
||||
func (_m *API) KVGet(key string) ([]byte, *model.AppError) {
|
||||
ret := _m.Called(key)
|
||||
|
||||
var r0 []byte
|
||||
if rf, ok := ret.Get(0).(func(string) []byte); ok {
|
||||
r0 = rf(key)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
|
||||
r1 = rf(key)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// KVSet provides a mock function with given fields: key, value
|
||||
func (_m *API) KVSet(key string, value []byte) *model.AppError {
|
||||
ret := _m.Called(key, value)
|
||||
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(string, []byte) *model.AppError); ok {
|
||||
r0 = rf(key, value)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +732,7 @@ func (_m *APIMOCKINTERNAL) KeyValueStore() plugin.KeyValueStore {
|
||||
}
|
||||
|
||||
// LoadPluginConfiguration provides a mock function with given fields: dest
|
||||
func (_m *APIMOCKINTERNAL) LoadPluginConfiguration(dest interface{}) error {
|
||||
func (_m *API) LoadPluginConfiguration(dest interface{}) error {
|
||||
ret := _m.Called(dest)
|
||||
|
||||
var r0 error
|
||||
@@ -523,8 +745,45 @@ func (_m *APIMOCKINTERNAL) LoadPluginConfiguration(dest interface{}) error {
|
||||
return r0
|
||||
}
|
||||
|
||||
// LogDebug provides a mock function with given fields: msg, keyValuePairs
|
||||
func (_m *API) LogDebug(msg string, keyValuePairs ...interface{}) {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, msg)
|
||||
_ca = append(_ca, keyValuePairs...)
|
||||
_m.Called(_ca...)
|
||||
}
|
||||
|
||||
// LogError provides a mock function with given fields: msg, keyValuePairs
|
||||
func (_m *API) LogError(msg string, keyValuePairs ...interface{}) {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, msg)
|
||||
_ca = append(_ca, keyValuePairs...)
|
||||
_m.Called(_ca...)
|
||||
}
|
||||
|
||||
// LogInfo provides a mock function with given fields: msg, keyValuePairs
|
||||
func (_m *API) LogInfo(msg string, keyValuePairs ...interface{}) {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, msg)
|
||||
_ca = append(_ca, keyValuePairs...)
|
||||
_m.Called(_ca...)
|
||||
}
|
||||
|
||||
// LogWarn provides a mock function with given fields: msg, keyValuePairs
|
||||
func (_m *API) LogWarn(msg string, keyValuePairs ...interface{}) {
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, msg)
|
||||
_ca = append(_ca, keyValuePairs...)
|
||||
_m.Called(_ca...)
|
||||
}
|
||||
|
||||
// PublishWebSocketEvent provides a mock function with given fields: event, payload, broadcast
|
||||
func (_m *API) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) {
|
||||
_m.Called(event, payload, broadcast)
|
||||
}
|
||||
|
||||
// RegisterCommand provides a mock function with given fields: command
|
||||
func (_m *APIMOCKINTERNAL) RegisterCommand(command *model.Command) error {
|
||||
func (_m *API) RegisterCommand(command *model.Command) error {
|
||||
ret := _m.Called(command)
|
||||
|
||||
var r0 error
|
||||
@@ -537,8 +796,40 @@ func (_m *APIMOCKINTERNAL) RegisterCommand(command *model.Command) error {
|
||||
return r0
|
||||
}
|
||||
|
||||
// SaveConfig provides a mock function with given fields: config
|
||||
func (_m *API) SaveConfig(config *model.Config) *model.AppError {
|
||||
ret := _m.Called(config)
|
||||
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(*model.Config) *model.AppError); ok {
|
||||
r0 = rf(config)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendEphemeralPost provides a mock function with given fields: userId, post
|
||||
func (_m *API) SendEphemeralPost(userId string, post *model.Post) *model.Post {
|
||||
ret := _m.Called(userId, post)
|
||||
|
||||
var r0 *model.Post
|
||||
if rf, ok := ret.Get(0).(func(string, *model.Post) *model.Post); ok {
|
||||
r0 = rf(userId, post)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.Post)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UnregisterCommand provides a mock function with given fields: teamId, trigger
|
||||
func (_m *APIMOCKINTERNAL) UnregisterCommand(teamId string, trigger string) error {
|
||||
func (_m *API) UnregisterCommand(teamId string, trigger string) error {
|
||||
ret := _m.Called(teamId, trigger)
|
||||
|
||||
var r0 error
|
||||
@@ -552,7 +843,7 @@ func (_m *APIMOCKINTERNAL) UnregisterCommand(teamId string, trigger string) erro
|
||||
}
|
||||
|
||||
// UpdateChannel provides a mock function with given fields: channel
|
||||
func (_m *APIMOCKINTERNAL) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||
func (_m *API) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||
ret := _m.Called(channel)
|
||||
|
||||
var r0 *model.Channel
|
||||
@@ -577,7 +868,7 @@ func (_m *APIMOCKINTERNAL) UpdateChannel(channel *model.Channel) (*model.Channel
|
||||
}
|
||||
|
||||
// UpdateChannelMemberNotifications provides a mock function with given fields: channelId, userId, notifications
|
||||
func (_m *APIMOCKINTERNAL) UpdateChannelMemberNotifications(channelId string, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
|
||||
func (_m *API) UpdateChannelMemberNotifications(channelId string, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
|
||||
ret := _m.Called(channelId, userId, notifications)
|
||||
|
||||
var r0 *model.ChannelMember
|
||||
@@ -602,7 +893,7 @@ func (_m *APIMOCKINTERNAL) UpdateChannelMemberNotifications(channelId string, us
|
||||
}
|
||||
|
||||
// UpdateChannelMemberRoles provides a mock function with given fields: channelId, userId, newRoles
|
||||
func (_m *APIMOCKINTERNAL) UpdateChannelMemberRoles(channelId string, userId string, newRoles string) (*model.ChannelMember, *model.AppError) {
|
||||
func (_m *API) UpdateChannelMemberRoles(channelId string, userId string, newRoles string) (*model.ChannelMember, *model.AppError) {
|
||||
ret := _m.Called(channelId, userId, newRoles)
|
||||
|
||||
var r0 *model.ChannelMember
|
||||
@@ -627,7 +918,7 @@ func (_m *APIMOCKINTERNAL) UpdateChannelMemberRoles(channelId string, userId str
|
||||
}
|
||||
|
||||
// UpdatePost provides a mock function with given fields: post
|
||||
func (_m *APIMOCKINTERNAL) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
func (_m *API) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
ret := _m.Called(post)
|
||||
|
||||
var r0 *model.Post
|
||||
@@ -652,7 +943,7 @@ func (_m *APIMOCKINTERNAL) UpdatePost(post *model.Post) (*model.Post, *model.App
|
||||
}
|
||||
|
||||
// UpdateTeam provides a mock function with given fields: team
|
||||
func (_m *APIMOCKINTERNAL) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
func (_m *API) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
ret := _m.Called(team)
|
||||
|
||||
var r0 *model.Team
|
||||
@@ -676,8 +967,33 @@ func (_m *APIMOCKINTERNAL) UpdateTeam(team *model.Team) (*model.Team, *model.App
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateTeamMemberRoles provides a mock function with given fields: teamId, userId, newRoles
|
||||
func (_m *API) UpdateTeamMemberRoles(teamId string, userId string, newRoles string) (*model.TeamMember, *model.AppError) {
|
||||
ret := _m.Called(teamId, userId, newRoles)
|
||||
|
||||
var r0 *model.TeamMember
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) *model.TeamMember); ok {
|
||||
r0 = rf(teamId, userId, newRoles)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.TeamMember)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(1).(func(string, string, string) *model.AppError); ok {
|
||||
r1 = rf(teamId, userId, newRoles)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateUser provides a mock function with given fields: user
|
||||
func (_m *APIMOCKINTERNAL) UpdateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
func (_m *API) UpdateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
ret := _m.Called(user)
|
||||
|
||||
var r0 *model.User
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugintest
|
||||
|
||||
import "github.com/mattermost/mattermost-server/plugin"
|
||||
|
||||
type API struct {
|
||||
APIMOCKINTERNAL
|
||||
Store *KeyValueStore
|
||||
}
|
||||
|
||||
var _ plugin.API = (*API)(nil)
|
||||
var _ plugin.KeyValueStore = (*KeyValueStore)(nil)
|
||||
|
||||
func (m *API) KeyValueStore() plugin.KeyValueStore {
|
||||
return m.Store
|
||||
}
|
||||
@@ -14,13 +14,18 @@ type Hooks struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ExecuteCommand provides a mock function with given fields: args
|
||||
func (_m *Hooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
ret := _m.Called(args)
|
||||
// ChannelHasBeenCreated provides a mock function with given fields: c, channel
|
||||
func (_m *Hooks) ChannelHasBeenCreated(c *plugin.Context, channel *model.Channel) {
|
||||
_m.Called(c, channel)
|
||||
}
|
||||
|
||||
// ExecuteCommand provides a mock function with given fields: c, args
|
||||
func (_m *Hooks) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
ret := _m.Called(c, args)
|
||||
|
||||
var r0 *model.CommandResponse
|
||||
if rf, ok := ret.Get(0).(func(*model.CommandArgs) *model.CommandResponse); ok {
|
||||
r0 = rf(args)
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.CommandArgs) *model.CommandResponse); ok {
|
||||
r0 = rf(c, args)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.CommandResponse)
|
||||
@@ -28,8 +33,8 @@ func (_m *Hooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse
|
||||
}
|
||||
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(1).(func(*model.CommandArgs) *model.AppError); ok {
|
||||
r1 = rf(args)
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, *model.CommandArgs) *model.AppError); ok {
|
||||
r1 = rf(c, args)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
@@ -39,23 +44,46 @@ func (_m *Hooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MessageHasBeenPosted provides a mock function with given fields: post
|
||||
func (_m *Hooks) MessageHasBeenPosted(post *model.Post) {
|
||||
_m.Called(post)
|
||||
// Implemented provides a mock function with given fields:
|
||||
func (_m *Hooks) Implemented() ([]string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func() []string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MessageHasBeenUpdated provides a mock function with given fields: newPost, oldPost
|
||||
func (_m *Hooks) MessageHasBeenUpdated(newPost *model.Post, oldPost *model.Post) {
|
||||
_m.Called(newPost, oldPost)
|
||||
// MessageHasBeenPosted provides a mock function with given fields: c, post
|
||||
func (_m *Hooks) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {
|
||||
_m.Called(c, post)
|
||||
}
|
||||
|
||||
// MessageWillBePosted provides a mock function with given fields: post
|
||||
func (_m *Hooks) MessageWillBePosted(post *model.Post) (*model.Post, string) {
|
||||
ret := _m.Called(post)
|
||||
// MessageHasBeenUpdated provides a mock function with given fields: c, newPost, oldPost
|
||||
func (_m *Hooks) MessageHasBeenUpdated(c *plugin.Context, newPost *model.Post, oldPost *model.Post) {
|
||||
_m.Called(c, newPost, oldPost)
|
||||
}
|
||||
|
||||
// MessageWillBePosted provides a mock function with given fields: c, post
|
||||
func (_m *Hooks) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
|
||||
ret := _m.Called(c, post)
|
||||
|
||||
var r0 *model.Post
|
||||
if rf, ok := ret.Get(0).(func(*model.Post) *model.Post); ok {
|
||||
r0 = rf(post)
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.Post) *model.Post); ok {
|
||||
r0 = rf(c, post)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.Post)
|
||||
@@ -63,8 +91,8 @@ func (_m *Hooks) MessageWillBePosted(post *model.Post) (*model.Post, string) {
|
||||
}
|
||||
|
||||
var r1 string
|
||||
if rf, ok := ret.Get(1).(func(*model.Post) string); ok {
|
||||
r1 = rf(post)
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, *model.Post) string); ok {
|
||||
r1 = rf(c, post)
|
||||
} else {
|
||||
r1 = ret.Get(1).(string)
|
||||
}
|
||||
@@ -72,13 +100,13 @@ func (_m *Hooks) MessageWillBePosted(post *model.Post) (*model.Post, string) {
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MessageWillBeUpdated provides a mock function with given fields: newPost, oldPost
|
||||
func (_m *Hooks) MessageWillBeUpdated(newPost *model.Post, oldPost *model.Post) (*model.Post, string) {
|
||||
ret := _m.Called(newPost, oldPost)
|
||||
// MessageWillBeUpdated provides a mock function with given fields: c, newPost, oldPost
|
||||
func (_m *Hooks) MessageWillBeUpdated(c *plugin.Context, newPost *model.Post, oldPost *model.Post) (*model.Post, string) {
|
||||
ret := _m.Called(c, newPost, oldPost)
|
||||
|
||||
var r0 *model.Post
|
||||
if rf, ok := ret.Get(0).(func(*model.Post, *model.Post) *model.Post); ok {
|
||||
r0 = rf(newPost, oldPost)
|
||||
if rf, ok := ret.Get(0).(func(*plugin.Context, *model.Post, *model.Post) *model.Post); ok {
|
||||
r0 = rf(c, newPost, oldPost)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.Post)
|
||||
@@ -86,8 +114,8 @@ func (_m *Hooks) MessageWillBeUpdated(newPost *model.Post, oldPost *model.Post)
|
||||
}
|
||||
|
||||
var r1 string
|
||||
if rf, ok := ret.Get(1).(func(*model.Post, *model.Post) string); ok {
|
||||
r1 = rf(newPost, oldPost)
|
||||
if rf, ok := ret.Get(1).(func(*plugin.Context, *model.Post, *model.Post) string); ok {
|
||||
r1 = rf(c, newPost, oldPost)
|
||||
} else {
|
||||
r1 = ret.Get(1).(string)
|
||||
}
|
||||
@@ -95,13 +123,13 @@ func (_m *Hooks) MessageWillBeUpdated(newPost *model.Post, oldPost *model.Post)
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// OnActivate provides a mock function with given fields: _a0
|
||||
func (_m *Hooks) OnActivate(_a0 plugin.API) error {
|
||||
ret := _m.Called(_a0)
|
||||
// OnActivate provides a mock function with given fields:
|
||||
func (_m *Hooks) OnActivate() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(plugin.API) error); ok {
|
||||
r0 = rf(_a0)
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
@@ -137,7 +165,27 @@ func (_m *Hooks) OnDeactivate() error {
|
||||
return r0
|
||||
}
|
||||
|
||||
// ServeHTTP provides a mock function with given fields: _a0, _a1
|
||||
func (_m *Hooks) ServeHTTP(_a0 http.ResponseWriter, _a1 *http.Request) {
|
||||
_m.Called(_a0, _a1)
|
||||
// ServeHTTP provides a mock function with given fields: c, w, r
|
||||
func (_m *Hooks) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
|
||||
_m.Called(c, w, r)
|
||||
}
|
||||
|
||||
// UserHasJoinedChannel provides a mock function with given fields: c, channelMember, actor
|
||||
func (_m *Hooks) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) {
|
||||
_m.Called(c, channelMember, actor)
|
||||
}
|
||||
|
||||
// UserHasJoinedTeam provides a mock function with given fields: c, teamMember, actor
|
||||
func (_m *Hooks) UserHasJoinedTeam(c *plugin.Context, teamMember *model.TeamMember, actor *model.User) {
|
||||
_m.Called(c, teamMember, actor)
|
||||
}
|
||||
|
||||
// UserHasLeftChannel provides a mock function with given fields: c, channelMember, actor
|
||||
func (_m *Hooks) UserHasLeftChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) {
|
||||
_m.Called(c, channelMember, actor)
|
||||
}
|
||||
|
||||
// UserHasLeftTeam provides a mock function with given fields: c, teamMember, actor
|
||||
func (_m *Hooks) UserHasLeftTeam(c *plugin.Context, teamMember *model.TeamMember, actor *model.User) {
|
||||
_m.Called(c, teamMember, actor)
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make plugin-mocks`.
|
||||
|
||||
package plugintest
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import model "github.com/mattermost/mattermost-server/model"
|
||||
|
||||
// KeyValueStore is an autogenerated mock type for the KeyValueStore type
|
||||
type KeyValueStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: key
|
||||
func (_m *KeyValueStore) Delete(key string) *model.AppError {
|
||||
ret := _m.Called(key)
|
||||
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(string) *model.AppError); ok {
|
||||
r0 = rf(key)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: key
|
||||
func (_m *KeyValueStore) Get(key string) ([]byte, *model.AppError) {
|
||||
ret := _m.Called(key)
|
||||
|
||||
var r0 []byte
|
||||
if rf, ok := ret.Get(0).(func(string) []byte); ok {
|
||||
r0 = rf(key)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]byte)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 *model.AppError
|
||||
if rf, ok := ret.Get(1).(func(string) *model.AppError); ok {
|
||||
r1 = rf(key)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Set provides a mock function with given fields: key, value
|
||||
func (_m *KeyValueStore) Set(key string, value []byte) *model.AppError {
|
||||
ret := _m.Called(key, value)
|
||||
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(string, []byte) *model.AppError); ok {
|
||||
r0 = rf(key, value)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
11
plugin/request_context.go
Обычный файл
11
plugin/request_context.go
Обычный файл
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
type Context struct {
|
||||
}
|
||||
|
||||
func NewBlankContext() *Context {
|
||||
return &Context{}
|
||||
}
|
||||
@@ -1,718 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
type LocalAPI struct {
|
||||
api plugin.API
|
||||
muxer *Muxer
|
||||
}
|
||||
|
||||
func (api *LocalAPI) LoadPluginConfiguration(args struct{}, reply *[]byte) error {
|
||||
var config interface{}
|
||||
if err := api.api.LoadPluginConfiguration(&config); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*reply = b
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) RegisterCommand(args *model.Command, reply *APITeamReply) error {
|
||||
return api.api.RegisterCommand(args)
|
||||
}
|
||||
|
||||
func (api *LocalAPI) UnregisterCommand(args *APIUnregisterCommandArgs, reply *APITeamReply) error {
|
||||
return api.api.UnregisterCommand(args.TeamId, args.Trigger)
|
||||
}
|
||||
|
||||
type APIErrorReply struct {
|
||||
Error *model.AppError
|
||||
}
|
||||
|
||||
type APITeamReply struct {
|
||||
Team *model.Team
|
||||
Error *model.AppError
|
||||
}
|
||||
|
||||
func (api *LocalAPI) CreateTeam(args *model.Team, reply *APITeamReply) error {
|
||||
team, err := api.api.CreateTeam(args)
|
||||
*reply = APITeamReply{
|
||||
Team: team,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) DeleteTeam(args string, reply *APIErrorReply) error {
|
||||
*reply = APIErrorReply{
|
||||
Error: api.api.DeleteTeam(args),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetTeam(args string, reply *APITeamReply) error {
|
||||
team, err := api.api.GetTeam(args)
|
||||
*reply = APITeamReply{
|
||||
Team: team,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetTeamByName(args string, reply *APITeamReply) error {
|
||||
team, err := api.api.GetTeamByName(args)
|
||||
*reply = APITeamReply{
|
||||
Team: team,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) UpdateTeam(args *model.Team, reply *APITeamReply) error {
|
||||
team, err := api.api.UpdateTeam(args)
|
||||
*reply = APITeamReply{
|
||||
Team: team,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type APIUserReply struct {
|
||||
User *model.User
|
||||
Error *model.AppError
|
||||
}
|
||||
|
||||
func (api *LocalAPI) CreateUser(args *model.User, reply *APIUserReply) error {
|
||||
user, err := api.api.CreateUser(args)
|
||||
*reply = APIUserReply{
|
||||
User: user,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) DeleteUser(args string, reply *APIErrorReply) error {
|
||||
*reply = APIErrorReply{
|
||||
Error: api.api.DeleteUser(args),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetUser(args string, reply *APIUserReply) error {
|
||||
user, err := api.api.GetUser(args)
|
||||
*reply = APIUserReply{
|
||||
User: user,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetUserByEmail(args string, reply *APIUserReply) error {
|
||||
user, err := api.api.GetUserByEmail(args)
|
||||
*reply = APIUserReply{
|
||||
User: user,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetUserByUsername(args string, reply *APIUserReply) error {
|
||||
user, err := api.api.GetUserByUsername(args)
|
||||
*reply = APIUserReply{
|
||||
User: user,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) UpdateUser(args *model.User, reply *APIUserReply) error {
|
||||
user, err := api.api.UpdateUser(args)
|
||||
*reply = APIUserReply{
|
||||
User: user,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type APIGetChannelByNameArgs struct {
|
||||
Name string
|
||||
TeamId string
|
||||
}
|
||||
|
||||
type APIGetDirectChannelArgs struct {
|
||||
UserId1 string
|
||||
UserId2 string
|
||||
}
|
||||
|
||||
type APIGetGroupChannelArgs struct {
|
||||
UserIds []string
|
||||
}
|
||||
|
||||
type APIAddChannelMemberArgs struct {
|
||||
ChannelId string
|
||||
UserId string
|
||||
}
|
||||
|
||||
type APIGetChannelMemberArgs struct {
|
||||
ChannelId string
|
||||
UserId string
|
||||
}
|
||||
|
||||
type APIUpdateChannelMemberRolesArgs struct {
|
||||
ChannelId string
|
||||
UserId string
|
||||
NewRoles string
|
||||
}
|
||||
|
||||
type APIUpdateChannelMemberNotificationsArgs struct {
|
||||
ChannelId string
|
||||
UserId string
|
||||
Notifications map[string]string
|
||||
}
|
||||
|
||||
type APIDeleteChannelMemberArgs struct {
|
||||
ChannelId string
|
||||
UserId string
|
||||
}
|
||||
|
||||
type APIChannelReply struct {
|
||||
Channel *model.Channel
|
||||
Error *model.AppError
|
||||
}
|
||||
|
||||
type APIChannelMemberReply struct {
|
||||
ChannelMember *model.ChannelMember
|
||||
Error *model.AppError
|
||||
}
|
||||
|
||||
func (api *LocalAPI) CreateChannel(args *model.Channel, reply *APIChannelReply) error {
|
||||
channel, err := api.api.CreateChannel(args)
|
||||
*reply = APIChannelReply{
|
||||
Channel: channel,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) DeleteChannel(args string, reply *APIErrorReply) error {
|
||||
*reply = APIErrorReply{
|
||||
Error: api.api.DeleteChannel(args),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetChannel(args string, reply *APIChannelReply) error {
|
||||
channel, err := api.api.GetChannel(args)
|
||||
*reply = APIChannelReply{
|
||||
Channel: channel,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetChannelByName(args *APIGetChannelByNameArgs, reply *APIChannelReply) error {
|
||||
channel, err := api.api.GetChannelByName(args.Name, args.TeamId)
|
||||
*reply = APIChannelReply{
|
||||
Channel: channel,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetDirectChannel(args *APIGetDirectChannelArgs, reply *APIChannelReply) error {
|
||||
channel, err := api.api.GetDirectChannel(args.UserId1, args.UserId2)
|
||||
*reply = APIChannelReply{
|
||||
Channel: channel,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetGroupChannel(args *APIGetGroupChannelArgs, reply *APIChannelReply) error {
|
||||
channel, err := api.api.GetGroupChannel(args.UserIds)
|
||||
*reply = APIChannelReply{
|
||||
Channel: channel,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) UpdateChannel(args *model.Channel, reply *APIChannelReply) error {
|
||||
channel, err := api.api.UpdateChannel(args)
|
||||
*reply = APIChannelReply{
|
||||
Channel: channel,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) AddChannelMember(args *APIAddChannelMemberArgs, reply *APIChannelMemberReply) error {
|
||||
member, err := api.api.AddChannelMember(args.ChannelId, args.UserId)
|
||||
*reply = APIChannelMemberReply{
|
||||
ChannelMember: member,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetChannelMember(args *APIGetChannelMemberArgs, reply *APIChannelMemberReply) error {
|
||||
member, err := api.api.GetChannelMember(args.ChannelId, args.UserId)
|
||||
*reply = APIChannelMemberReply{
|
||||
ChannelMember: member,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) UpdateChannelMemberRoles(args *APIUpdateChannelMemberRolesArgs, reply *APIChannelMemberReply) error {
|
||||
member, err := api.api.UpdateChannelMemberRoles(args.ChannelId, args.UserId, args.NewRoles)
|
||||
*reply = APIChannelMemberReply{
|
||||
ChannelMember: member,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) UpdateChannelMemberNotifications(args *APIUpdateChannelMemberNotificationsArgs, reply *APIChannelMemberReply) error {
|
||||
member, err := api.api.UpdateChannelMemberNotifications(args.ChannelId, args.UserId, args.Notifications)
|
||||
*reply = APIChannelMemberReply{
|
||||
ChannelMember: member,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) DeleteChannelMember(args *APIDeleteChannelMemberArgs, reply *APIErrorReply) error {
|
||||
err := api.api.DeleteChannelMember(args.ChannelId, args.UserId)
|
||||
*reply = APIErrorReply{
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type APIPostReply struct {
|
||||
Post *model.Post
|
||||
Error *model.AppError
|
||||
}
|
||||
|
||||
func (api *LocalAPI) CreatePost(args *model.Post, reply *APIPostReply) error {
|
||||
post, err := api.api.CreatePost(args)
|
||||
*reply = APIPostReply{
|
||||
Post: post,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) DeletePost(args string, reply *APIErrorReply) error {
|
||||
*reply = APIErrorReply{
|
||||
Error: api.api.DeletePost(args),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) GetPost(args string, reply *APIPostReply) error {
|
||||
post, err := api.api.GetPost(args)
|
||||
*reply = APIPostReply{
|
||||
Post: post,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) UpdatePost(args *model.Post, reply *APIPostReply) error {
|
||||
post, err := api.api.UpdatePost(args)
|
||||
*reply = APIPostReply{
|
||||
Post: post,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type APIKeyValueStoreReply struct {
|
||||
Value []byte
|
||||
Error *model.AppError
|
||||
}
|
||||
|
||||
type APIKeyValueStoreSetArgs struct {
|
||||
Key string
|
||||
Value []byte
|
||||
}
|
||||
|
||||
func (api *LocalAPI) KeyValueStoreSet(args *APIKeyValueStoreSetArgs, reply *APIErrorReply) error {
|
||||
err := api.api.KeyValueStore().Set(args.Key, args.Value)
|
||||
*reply = APIErrorReply{
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) KeyValueStoreGet(args string, reply *APIKeyValueStoreReply) error {
|
||||
v, err := api.api.KeyValueStore().Get(args)
|
||||
*reply = APIKeyValueStoreReply{
|
||||
Value: v,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *LocalAPI) KeyValueStoreDelete(args string, reply *APIErrorReply) error {
|
||||
err := api.api.KeyValueStore().Delete(args)
|
||||
*reply = APIErrorReply{
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ServeAPI(api plugin.API, conn io.ReadWriteCloser, muxer *Muxer) {
|
||||
server := rpc.NewServer()
|
||||
server.Register(&LocalAPI{
|
||||
api: api,
|
||||
muxer: muxer,
|
||||
})
|
||||
server.ServeConn(conn)
|
||||
}
|
||||
|
||||
type RemoteAPI struct {
|
||||
client *rpc.Client
|
||||
muxer *Muxer
|
||||
keyValueStore *RemoteKeyValueStore
|
||||
}
|
||||
|
||||
type RemoteKeyValueStore struct {
|
||||
api *RemoteAPI
|
||||
}
|
||||
|
||||
var _ plugin.API = (*RemoteAPI)(nil)
|
||||
var _ plugin.KeyValueStore = (*RemoteKeyValueStore)(nil)
|
||||
|
||||
func (api *RemoteAPI) LoadPluginConfiguration(dest interface{}) error {
|
||||
var config []byte
|
||||
if err := api.client.Call("LocalAPI.LoadPluginConfiguration", struct{}{}, &config); err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(config, dest)
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) RegisterCommand(command *model.Command) error {
|
||||
return api.client.Call("LocalAPI.RegisterCommand", command, nil)
|
||||
}
|
||||
|
||||
type APIUnregisterCommandArgs struct {
|
||||
TeamId string
|
||||
Trigger string
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) UnregisterCommand(teamId, trigger string) error {
|
||||
return api.client.Call("LocalAPI.UnregisterCommand", &APIUnregisterCommandArgs{
|
||||
TeamId: teamId,
|
||||
Trigger: trigger,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) CreateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
var reply APIUserReply
|
||||
if err := api.client.Call("LocalAPI.CreateUser", user, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.CreateUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.User, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) DeleteUser(userId string) *model.AppError {
|
||||
var reply APIErrorReply
|
||||
if err := api.client.Call("LocalAPI.DeleteUser", userId, &reply); err != nil {
|
||||
return model.NewAppError("RemoteAPI.DeleteUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetUser(userId string) (*model.User, *model.AppError) {
|
||||
var reply APIUserReply
|
||||
if err := api.client.Call("LocalAPI.GetUser", userId, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.User, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetUserByEmail(email string) (*model.User, *model.AppError) {
|
||||
var reply APIUserReply
|
||||
if err := api.client.Call("LocalAPI.GetUserByEmail", email, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetUserByEmail", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.User, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetUserByUsername(name string) (*model.User, *model.AppError) {
|
||||
var reply APIUserReply
|
||||
if err := api.client.Call("LocalAPI.GetUserByUsername", name, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetUserByUsername", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.User, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) UpdateUser(user *model.User) (*model.User, *model.AppError) {
|
||||
var reply APIUserReply
|
||||
if err := api.client.Call("LocalAPI.UpdateUser", user, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.UpdateUser", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.User, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
var reply APITeamReply
|
||||
if err := api.client.Call("LocalAPI.CreateTeam", team, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.CreateTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Team, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) DeleteTeam(teamId string) *model.AppError {
|
||||
var reply APIErrorReply
|
||||
if err := api.client.Call("LocalAPI.DeleteTeam", teamId, &reply); err != nil {
|
||||
return model.NewAppError("RemoteAPI.DeleteTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetTeam(teamId string) (*model.Team, *model.AppError) {
|
||||
var reply APITeamReply
|
||||
if err := api.client.Call("LocalAPI.GetTeam", teamId, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Team, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetTeamByName(name string) (*model.Team, *model.AppError) {
|
||||
var reply APITeamReply
|
||||
if err := api.client.Call("LocalAPI.GetTeamByName", name, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetTeamByName", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Team, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
|
||||
var reply APITeamReply
|
||||
if err := api.client.Call("LocalAPI.UpdateTeam", team, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.UpdateTeam", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Team, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) CreateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||
var reply APIChannelReply
|
||||
if err := api.client.Call("LocalAPI.CreateChannel", channel, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.CreateChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Channel, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) DeleteChannel(channelId string) *model.AppError {
|
||||
var reply APIErrorReply
|
||||
if err := api.client.Call("LocalAPI.DeleteChannel", channelId, &reply); err != nil {
|
||||
return model.NewAppError("RemoteAPI.DeleteChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetChannel(channelId string) (*model.Channel, *model.AppError) {
|
||||
var reply APIChannelReply
|
||||
if err := api.client.Call("LocalAPI.GetChannel", channelId, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Channel, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetChannelByName(name, teamId string) (*model.Channel, *model.AppError) {
|
||||
var reply APIChannelReply
|
||||
if err := api.client.Call("LocalAPI.GetChannelByName", &APIGetChannelByNameArgs{
|
||||
Name: name,
|
||||
TeamId: teamId,
|
||||
}, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetChannelByName", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Channel, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetDirectChannel(userId1, userId2 string) (*model.Channel, *model.AppError) {
|
||||
var reply APIChannelReply
|
||||
if err := api.client.Call("LocalAPI.GetDirectChannel", &APIGetDirectChannelArgs{
|
||||
UserId1: userId1,
|
||||
UserId2: userId2,
|
||||
}, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetDirectChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Channel, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) {
|
||||
var reply APIChannelReply
|
||||
if err := api.client.Call("LocalAPI.GetGroupChannel", &APIGetGroupChannelArgs{
|
||||
UserIds: userIds,
|
||||
}, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetGroupChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Channel, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
|
||||
var reply APIChannelReply
|
||||
if err := api.client.Call("LocalAPI.UpdateChannel", channel, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.UpdateChannel", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Channel, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) AddChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
|
||||
var reply APIChannelMemberReply
|
||||
if err := api.client.Call("LocalAPI.AddChannelMember", &APIAddChannelMemberArgs{
|
||||
ChannelId: channelId,
|
||||
UserId: userId,
|
||||
}, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.AddChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.ChannelMember, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetChannelMember(channelId, userId string) (*model.ChannelMember, *model.AppError) {
|
||||
var reply APIChannelMemberReply
|
||||
if err := api.client.Call("LocalAPI.GetChannelMember", &APIGetChannelMemberArgs{
|
||||
ChannelId: channelId,
|
||||
UserId: userId,
|
||||
}, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.ChannelMember, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) UpdateChannelMemberRoles(channelId, userId, newRoles string) (*model.ChannelMember, *model.AppError) {
|
||||
var reply APIChannelMemberReply
|
||||
if err := api.client.Call("LocalAPI.UpdateChannelMemberRoles", &APIUpdateChannelMemberRolesArgs{
|
||||
ChannelId: channelId,
|
||||
UserId: userId,
|
||||
NewRoles: newRoles,
|
||||
}, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.UpdateChannelMemberRoles", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.ChannelMember, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) UpdateChannelMemberNotifications(channelId, userId string, notifications map[string]string) (*model.ChannelMember, *model.AppError) {
|
||||
var reply APIChannelMemberReply
|
||||
if err := api.client.Call("LocalAPI.UpdateChannelMemberNotifications", &APIUpdateChannelMemberNotificationsArgs{
|
||||
ChannelId: channelId,
|
||||
UserId: userId,
|
||||
Notifications: notifications,
|
||||
}, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.UpdateChannelMemberNotifications", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.ChannelMember, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) DeleteChannelMember(channelId, userId string) *model.AppError {
|
||||
var reply APIErrorReply
|
||||
if err := api.client.Call("LocalAPI.DeleteChannelMember", &APIDeleteChannelMemberArgs{
|
||||
ChannelId: channelId,
|
||||
UserId: userId,
|
||||
}, &reply); err != nil {
|
||||
return model.NewAppError("RemoteAPI.DeleteChannelMember", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
var reply APIPostReply
|
||||
if err := api.client.Call("LocalAPI.CreatePost", post, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.CreatePost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Post, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) DeletePost(postId string) *model.AppError {
|
||||
var reply APIErrorReply
|
||||
if err := api.client.Call("LocalAPI.DeletePost", postId, &reply); err != nil {
|
||||
return model.NewAppError("RemoteAPI.DeletePost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) GetPost(postId string) (*model.Post, *model.AppError) {
|
||||
var reply APIPostReply
|
||||
if err := api.client.Call("LocalAPI.GetPost", postId, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.GetPost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Post, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
|
||||
var reply APIPostReply
|
||||
if err := api.client.Call("LocalAPI.UpdatePost", post, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.UpdatePost", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Post, reply.Error
|
||||
}
|
||||
|
||||
func (api *RemoteAPI) KeyValueStore() plugin.KeyValueStore {
|
||||
return api.keyValueStore
|
||||
}
|
||||
|
||||
func (s *RemoteKeyValueStore) Set(key string, value []byte) *model.AppError {
|
||||
var reply APIErrorReply
|
||||
if err := s.api.client.Call("LocalAPI.KeyValueStoreSet", &APIKeyValueStoreSetArgs{Key: key, Value: value}, &reply); err != nil {
|
||||
return model.NewAppError("RemoteAPI.KeyValueStoreSet", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Error
|
||||
}
|
||||
|
||||
func (s *RemoteKeyValueStore) Get(key string) ([]byte, *model.AppError) {
|
||||
var reply APIKeyValueStoreReply
|
||||
if err := s.api.client.Call("LocalAPI.KeyValueStoreGet", key, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteAPI.KeyValueStoreGet", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Value, reply.Error
|
||||
}
|
||||
|
||||
func (s *RemoteKeyValueStore) Delete(key string) *model.AppError {
|
||||
var reply APIErrorReply
|
||||
if err := s.api.client.Call("LocalAPI.KeyValueStoreDelete", key, &reply); err != nil {
|
||||
return model.NewAppError("RemoteAPI.KeyValueStoreDelete", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Error
|
||||
}
|
||||
|
||||
func (h *RemoteAPI) Close() error {
|
||||
return h.client.Close()
|
||||
}
|
||||
|
||||
func ConnectAPI(conn io.ReadWriteCloser, muxer *Muxer) *RemoteAPI {
|
||||
remoteKeyValueStore := &RemoteKeyValueStore{}
|
||||
remoteApi := &RemoteAPI{
|
||||
client: rpc.NewClient(conn),
|
||||
muxer: muxer,
|
||||
keyValueStore: remoteKeyValueStore,
|
||||
}
|
||||
|
||||
remoteKeyValueStore.api = remoteApi
|
||||
|
||||
return remoteApi
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]*model.SlackAttachment{})
|
||||
gob.Register([]interface{}{})
|
||||
gob.Register(map[string]interface{}{})
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
)
|
||||
|
||||
func testAPIRPC(api plugin.API, f func(plugin.API)) {
|
||||
r1, w1 := io.Pipe()
|
||||
r2, w2 := io.Pipe()
|
||||
|
||||
c1 := NewMuxer(NewReadWriteCloser(r1, w2), false)
|
||||
defer c1.Close()
|
||||
|
||||
c2 := NewMuxer(NewReadWriteCloser(r2, w1), true)
|
||||
defer c2.Close()
|
||||
|
||||
id, server := c1.Serve()
|
||||
go ServeAPI(api, server, c1)
|
||||
|
||||
remote := ConnectAPI(c2.Connect(id), c2)
|
||||
defer remote.Close()
|
||||
|
||||
f(remote)
|
||||
}
|
||||
|
||||
func TestAPI(t *testing.T) {
|
||||
keyValueStore := &plugintest.KeyValueStore{}
|
||||
api := plugintest.API{Store: keyValueStore}
|
||||
defer api.AssertExpectations(t)
|
||||
|
||||
type Config struct {
|
||||
Foo string
|
||||
Bar struct {
|
||||
Baz string
|
||||
}
|
||||
}
|
||||
|
||||
api.On("LoadPluginConfiguration", mock.MatchedBy(func(x interface{}) bool { return true })).Run(func(args mock.Arguments) {
|
||||
dest := args.Get(0).(interface{})
|
||||
json.Unmarshal([]byte(`{"Foo": "foo", "Bar": {"Baz": "baz"}}`), dest)
|
||||
}).Return(nil)
|
||||
|
||||
testChannel := &model.Channel{
|
||||
Id: "thechannelid",
|
||||
}
|
||||
|
||||
testChannelMember := &model.ChannelMember{
|
||||
ChannelId: "thechannelid",
|
||||
UserId: "theuserid",
|
||||
}
|
||||
|
||||
testTeam := &model.Team{
|
||||
Id: "theteamid",
|
||||
}
|
||||
teamNotFoundError := model.NewAppError("SqlTeamStore.GetByName", "store.sql_team.get_by_name.app_error", nil, "name=notateam", http.StatusNotFound)
|
||||
|
||||
testUser := &model.User{
|
||||
Id: "theuserid",
|
||||
}
|
||||
|
||||
testPost := &model.Post{
|
||||
Message: "hello",
|
||||
}
|
||||
|
||||
testAPIRPC(&api, func(remote plugin.API) {
|
||||
var config Config
|
||||
assert.NoError(t, remote.LoadPluginConfiguration(&config))
|
||||
assert.Equal(t, "foo", config.Foo)
|
||||
assert.Equal(t, "baz", config.Bar.Baz)
|
||||
|
||||
api.On("RegisterCommand", mock.AnythingOfType("*model.Command")).Return(fmt.Errorf("foo")).Once()
|
||||
assert.Error(t, remote.RegisterCommand(&model.Command{}))
|
||||
api.On("RegisterCommand", mock.AnythingOfType("*model.Command")).Return(nil).Once()
|
||||
assert.NoError(t, remote.RegisterCommand(&model.Command{}))
|
||||
|
||||
api.On("UnregisterCommand", "team", "trigger").Return(fmt.Errorf("foo")).Once()
|
||||
assert.Error(t, remote.UnregisterCommand("team", "trigger"))
|
||||
api.On("UnregisterCommand", "team", "trigger").Return(nil).Once()
|
||||
assert.NoError(t, remote.UnregisterCommand("team", "trigger"))
|
||||
|
||||
api.On("CreateChannel", mock.AnythingOfType("*model.Channel")).Return(func(c *model.Channel) *model.Channel {
|
||||
c.Id = "thechannelid"
|
||||
return c
|
||||
}, nil).Once()
|
||||
channel, err := remote.CreateChannel(testChannel)
|
||||
assert.Equal(t, "thechannelid", channel.Id)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("DeleteChannel", "thechannelid").Return(nil).Once()
|
||||
assert.Nil(t, remote.DeleteChannel("thechannelid"))
|
||||
|
||||
api.On("GetChannel", "thechannelid").Return(testChannel, nil).Once()
|
||||
channel, err = remote.GetChannel("thechannelid")
|
||||
assert.Equal(t, testChannel, channel)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("GetChannelByName", "foo", "theteamid").Return(testChannel, nil).Once()
|
||||
channel, err = remote.GetChannelByName("foo", "theteamid")
|
||||
assert.Equal(t, testChannel, channel)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("GetDirectChannel", "user1", "user2").Return(testChannel, nil).Once()
|
||||
channel, err = remote.GetDirectChannel("user1", "user2")
|
||||
assert.Equal(t, testChannel, channel)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("GetGroupChannel", []string{"user1", "user2", "user3"}).Return(testChannel, nil).Once()
|
||||
channel, err = remote.GetGroupChannel([]string{"user1", "user2", "user3"})
|
||||
assert.Equal(t, testChannel, channel)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("UpdateChannel", mock.AnythingOfType("*model.Channel")).Return(func(c *model.Channel) *model.Channel {
|
||||
return c
|
||||
}, nil).Once()
|
||||
channel, err = remote.UpdateChannel(testChannel)
|
||||
assert.Equal(t, testChannel, channel)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("AddChannelMember", testChannel.Id, "theuserid").Return(testChannelMember, nil).Once()
|
||||
member, err := remote.AddChannelMember(testChannel.Id, "theuserid")
|
||||
assert.Equal(t, testChannelMember, member)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("GetChannelMember", "thechannelid", "theuserid").Return(testChannelMember, nil).Once()
|
||||
member, err = remote.GetChannelMember("thechannelid", "theuserid")
|
||||
assert.Equal(t, testChannelMember, member)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("UpdateChannelMemberRoles", testChannel.Id, "theuserid", model.CHANNEL_ADMIN_ROLE_ID).Return(testChannelMember, nil).Once()
|
||||
member, err = remote.UpdateChannelMemberRoles(testChannel.Id, "theuserid", model.CHANNEL_ADMIN_ROLE_ID)
|
||||
assert.Equal(t, testChannelMember, member)
|
||||
assert.Nil(t, err)
|
||||
|
||||
notifications := map[string]string{}
|
||||
notifications[model.MARK_UNREAD_NOTIFY_PROP] = model.CHANNEL_MARK_UNREAD_MENTION
|
||||
api.On("UpdateChannelMemberNotifications", testChannel.Id, "theuserid", notifications).Return(testChannelMember, nil).Once()
|
||||
member, err = remote.UpdateChannelMemberNotifications(testChannel.Id, "theuserid", notifications)
|
||||
assert.Equal(t, testChannelMember, member)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("DeleteChannelMember", "thechannelid", "theuserid").Return(nil).Once()
|
||||
err = remote.DeleteChannelMember("thechannelid", "theuserid")
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("CreateUser", mock.AnythingOfType("*model.User")).Return(func(u *model.User) *model.User {
|
||||
u.Id = "theuserid"
|
||||
return u
|
||||
}, nil).Once()
|
||||
user, err := remote.CreateUser(testUser)
|
||||
assert.Equal(t, "theuserid", user.Id)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("DeleteUser", "theuserid").Return(nil).Once()
|
||||
assert.Nil(t, remote.DeleteUser("theuserid"))
|
||||
|
||||
api.On("GetUser", "theuserid").Return(testUser, nil).Once()
|
||||
user, err = remote.GetUser("theuserid")
|
||||
assert.Equal(t, testUser, user)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("GetUserByEmail", "foo@foo").Return(testUser, nil).Once()
|
||||
user, err = remote.GetUserByEmail("foo@foo")
|
||||
assert.Equal(t, testUser, user)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("GetUserByUsername", "foo").Return(testUser, nil).Once()
|
||||
user, err = remote.GetUserByUsername("foo")
|
||||
assert.Equal(t, testUser, user)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("UpdateUser", mock.AnythingOfType("*model.User")).Return(func(u *model.User) *model.User {
|
||||
return u
|
||||
}, nil).Once()
|
||||
user, err = remote.UpdateUser(testUser)
|
||||
assert.Equal(t, testUser, user)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("CreateTeam", mock.AnythingOfType("*model.Team")).Return(func(t *model.Team) *model.Team {
|
||||
t.Id = "theteamid"
|
||||
return t
|
||||
}, nil).Once()
|
||||
team, err := remote.CreateTeam(testTeam)
|
||||
assert.Equal(t, "theteamid", team.Id)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("DeleteTeam", "theteamid").Return(nil).Once()
|
||||
assert.Nil(t, remote.DeleteTeam("theteamid"))
|
||||
|
||||
api.On("GetTeam", "theteamid").Return(testTeam, nil).Once()
|
||||
team, err = remote.GetTeam("theteamid")
|
||||
assert.Equal(t, testTeam, team)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("GetTeamByName", "foo").Return(testTeam, nil).Once()
|
||||
team, err = remote.GetTeamByName("foo")
|
||||
assert.Equal(t, testTeam, team)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("GetTeamByName", "notateam").Return(nil, teamNotFoundError).Once()
|
||||
team, err = remote.GetTeamByName("notateam")
|
||||
assert.Nil(t, team)
|
||||
assert.Equal(t, teamNotFoundError, err)
|
||||
|
||||
api.On("UpdateTeam", mock.AnythingOfType("*model.Team")).Return(func(t *model.Team) *model.Team {
|
||||
return t
|
||||
}, nil).Once()
|
||||
team, err = remote.UpdateTeam(testTeam)
|
||||
assert.Equal(t, testTeam, team)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(func(p *model.Post) *model.Post {
|
||||
p.Id = "thepostid"
|
||||
return p
|
||||
}, nil).Once()
|
||||
post, err := remote.CreatePost(testPost)
|
||||
require.Nil(t, err)
|
||||
assert.NotEmpty(t, post.Id)
|
||||
assert.Equal(t, testPost.Message, post.Message)
|
||||
|
||||
api.On("DeletePost", "thepostid").Return(nil).Once()
|
||||
assert.Nil(t, remote.DeletePost("thepostid"))
|
||||
|
||||
api.On("GetPost", "thepostid").Return(testPost, nil).Once()
|
||||
post, err = remote.GetPost("thepostid")
|
||||
assert.Equal(t, testPost, post)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.On("UpdatePost", mock.AnythingOfType("*model.Post")).Return(func(p *model.Post) *model.Post {
|
||||
return p
|
||||
}, nil).Once()
|
||||
post, err = remote.UpdatePost(testPost)
|
||||
assert.Equal(t, testPost, post)
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.KeyValueStore().(*plugintest.KeyValueStore).On("Set", "thekey", []byte("thevalue")).Return(nil).Once()
|
||||
err = remote.KeyValueStore().Set("thekey", []byte("thevalue"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
api.KeyValueStore().(*plugintest.KeyValueStore).On("Get", "thekey").Return(func(key string) []byte {
|
||||
return []byte("thevalue")
|
||||
}, nil).Once()
|
||||
ret, err := remote.KeyValueStore().Get("thekey")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("thevalue"), ret)
|
||||
|
||||
api.KeyValueStore().(*plugintest.KeyValueStore).On("Delete", "thekey").Return(nil).Once()
|
||||
err = remote.KeyValueStore().Delete("thekey")
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPI_GobRegistration(t *testing.T) {
|
||||
keyValueStore := &plugintest.KeyValueStore{}
|
||||
api := plugintest.API{Store: keyValueStore}
|
||||
defer api.AssertExpectations(t)
|
||||
|
||||
testAPIRPC(&api, func(remote plugin.API) {
|
||||
api.On("CreatePost", mock.AnythingOfType("*model.Post")).Return(func(p *model.Post) *model.Post {
|
||||
p.Id = "thepostid"
|
||||
return p
|
||||
}, nil).Once()
|
||||
_, err := remote.CreatePost(&model.Post{
|
||||
Message: "hello",
|
||||
Props: map[string]interface{}{
|
||||
"attachments": []*model.SlackAttachment{
|
||||
&model.SlackAttachment{
|
||||
Actions: []*model.PostAction{
|
||||
&model.PostAction{
|
||||
Integration: &model.PostActionIntegration{
|
||||
Context: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
"foos": []interface{}{"bar", "baz", 1, 2},
|
||||
"foo_map": map[string]interface{}{
|
||||
"1": "bar",
|
||||
"2": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Timestamp: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Nil(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
"reflect"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
type LocalHooks struct {
|
||||
hooks interface{}
|
||||
muxer *Muxer
|
||||
remoteAPI *RemoteAPI
|
||||
}
|
||||
|
||||
// Implemented replies with the names of the hooks that are implemented.
|
||||
func (h *LocalHooks) Implemented(args struct{}, reply *[]string) error {
|
||||
ifaceType := reflect.TypeOf((*plugin.Hooks)(nil)).Elem()
|
||||
implType := reflect.TypeOf(h.hooks)
|
||||
selfType := reflect.TypeOf(h)
|
||||
var methods []string
|
||||
for i := 0; i < ifaceType.NumMethod(); i++ {
|
||||
method := ifaceType.Method(i)
|
||||
if m, ok := implType.MethodByName(method.Name); !ok {
|
||||
continue
|
||||
} else if m.Type.NumIn() != method.Type.NumIn()+1 {
|
||||
continue
|
||||
} else if m.Type.NumOut() != method.Type.NumOut() {
|
||||
continue
|
||||
} else {
|
||||
match := true
|
||||
for j := 0; j < method.Type.NumIn(); j++ {
|
||||
if m.Type.In(j+1) != method.Type.In(j) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
for j := 0; j < method.Type.NumOut(); j++ {
|
||||
if m.Type.Out(j) != method.Type.Out(j) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, ok := selfType.MethodByName(method.Name); !ok {
|
||||
continue
|
||||
}
|
||||
methods = append(methods, method.Name)
|
||||
}
|
||||
*reply = methods
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *LocalHooks) OnActivate(args int64, reply *struct{}) error {
|
||||
if h.remoteAPI != nil {
|
||||
h.remoteAPI.Close()
|
||||
h.remoteAPI = nil
|
||||
}
|
||||
if hook, ok := h.hooks.(interface {
|
||||
OnActivate(plugin.API) error
|
||||
}); ok {
|
||||
stream := h.muxer.Connect(args)
|
||||
h.remoteAPI = ConnectAPI(stream, h.muxer)
|
||||
return hook.OnActivate(h.remoteAPI)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *LocalHooks) OnDeactivate(args, reply *struct{}) (err error) {
|
||||
if hook, ok := h.hooks.(interface {
|
||||
OnDeactivate() error
|
||||
}); ok {
|
||||
err = hook.OnDeactivate()
|
||||
}
|
||||
if h.remoteAPI != nil {
|
||||
h.remoteAPI.Close()
|
||||
h.remoteAPI = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *LocalHooks) OnConfigurationChange(args, reply *struct{}) error {
|
||||
if hook, ok := h.hooks.(interface {
|
||||
OnConfigurationChange() error
|
||||
}); ok {
|
||||
return hook.OnConfigurationChange()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ServeHTTPArgs struct {
|
||||
ResponseWriterStream int64
|
||||
Request *http.Request
|
||||
RequestBodyStream int64
|
||||
}
|
||||
|
||||
func (h *LocalHooks) ServeHTTP(args ServeHTTPArgs, reply *struct{}) error {
|
||||
w := ConnectHTTPResponseWriter(h.muxer.Connect(args.ResponseWriterStream))
|
||||
defer w.Close()
|
||||
|
||||
r := args.Request
|
||||
if args.RequestBodyStream != 0 {
|
||||
r.Body = ConnectIOReader(h.muxer.Connect(args.RequestBodyStream))
|
||||
} else {
|
||||
r.Body = ioutil.NopCloser(&bytes.Buffer{})
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if hook, ok := h.hooks.(http.Handler); ok {
|
||||
hook.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type HooksExecuteCommandReply struct {
|
||||
Response *model.CommandResponse
|
||||
Error *model.AppError
|
||||
}
|
||||
|
||||
func (h *LocalHooks) ExecuteCommand(args *model.CommandArgs, reply *HooksExecuteCommandReply) error {
|
||||
if hook, ok := h.hooks.(interface {
|
||||
ExecuteCommand(*model.CommandArgs) (*model.CommandResponse, *model.AppError)
|
||||
}); ok {
|
||||
reply.Response, reply.Error = hook.ExecuteCommand(args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MessageWillBeReply struct {
|
||||
Post *model.Post
|
||||
RejectionReason string
|
||||
}
|
||||
|
||||
type MessageUpdatedArgs struct {
|
||||
NewPost *model.Post
|
||||
OldPost *model.Post
|
||||
}
|
||||
|
||||
func (h *LocalHooks) MessageWillBePosted(args *model.Post, reply *MessageWillBeReply) error {
|
||||
if hook, ok := h.hooks.(interface {
|
||||
MessageWillBePosted(*model.Post) (*model.Post, string)
|
||||
}); ok {
|
||||
reply.Post, reply.RejectionReason = hook.MessageWillBePosted(args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *LocalHooks) MessageWillBeUpdated(args *MessageUpdatedArgs, reply *MessageWillBeReply) error {
|
||||
if hook, ok := h.hooks.(interface {
|
||||
MessageWillBeUpdated(*model.Post, *model.Post) (*model.Post, string)
|
||||
}); ok {
|
||||
reply.Post, reply.RejectionReason = hook.MessageWillBeUpdated(args.NewPost, args.OldPost)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *LocalHooks) MessageHasBeenPosted(args *model.Post, reply *struct{}) error {
|
||||
if hook, ok := h.hooks.(interface {
|
||||
MessageHasBeenPosted(*model.Post)
|
||||
}); ok {
|
||||
hook.MessageHasBeenPosted(args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *LocalHooks) MessageHasBeenUpdated(args *MessageUpdatedArgs, reply *struct{}) error {
|
||||
if hook, ok := h.hooks.(interface {
|
||||
MessageHasBeenUpdated(*model.Post, *model.Post)
|
||||
}); ok {
|
||||
hook.MessageHasBeenUpdated(args.NewPost, args.OldPost)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ServeHooks(hooks interface{}, conn io.ReadWriteCloser, muxer *Muxer) {
|
||||
server := rpc.NewServer()
|
||||
server.Register(&LocalHooks{
|
||||
hooks: hooks,
|
||||
muxer: muxer,
|
||||
})
|
||||
server.ServeConn(conn)
|
||||
}
|
||||
|
||||
// These assignments are part of the wire protocol. You can add more, but should not change existing
|
||||
// assignments.
|
||||
const (
|
||||
remoteOnActivate = 0
|
||||
remoteOnDeactivate = 1
|
||||
remoteServeHTTP = 2
|
||||
remoteOnConfigurationChange = 3
|
||||
remoteExecuteCommand = 4
|
||||
remoteMessageWillBePosted = 5
|
||||
remoteMessageWillBeUpdated = 6
|
||||
remoteMessageHasBeenPosted = 7
|
||||
remoteMessageHasBeenUpdated = 8
|
||||
maxRemoteHookCount = iota
|
||||
)
|
||||
|
||||
type RemoteHooks struct {
|
||||
client *rpc.Client
|
||||
muxer *Muxer
|
||||
apiCloser io.Closer
|
||||
implemented [maxRemoteHookCount]bool
|
||||
pluginId string
|
||||
}
|
||||
|
||||
var _ plugin.Hooks = (*RemoteHooks)(nil)
|
||||
|
||||
func (h *RemoteHooks) Implemented() (impl []string, err error) {
|
||||
err = h.client.Call("LocalHooks.Implemented", struct{}{}, &impl)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) OnActivate(api plugin.API) error {
|
||||
if h.apiCloser != nil {
|
||||
h.apiCloser.Close()
|
||||
h.apiCloser = nil
|
||||
}
|
||||
if !h.implemented[remoteOnActivate] {
|
||||
return nil
|
||||
}
|
||||
id, stream := h.muxer.Serve()
|
||||
h.apiCloser = stream
|
||||
go ServeAPI(api, stream, h.muxer)
|
||||
return h.client.Call("LocalHooks.OnActivate", id, nil)
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) OnDeactivate() error {
|
||||
if !h.implemented[remoteOnDeactivate] {
|
||||
return nil
|
||||
}
|
||||
return h.client.Call("LocalHooks.OnDeactivate", struct{}{}, nil)
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) OnConfigurationChange() error {
|
||||
if !h.implemented[remoteOnConfigurationChange] {
|
||||
return nil
|
||||
}
|
||||
return h.client.Call("LocalHooks.OnConfigurationChange", struct{}{}, nil)
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.implemented[remoteServeHTTP] {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
responseWriterStream, stream := h.muxer.Serve()
|
||||
defer stream.Close()
|
||||
go ServeHTTPResponseWriter(w, stream)
|
||||
|
||||
requestBodyStream := int64(0)
|
||||
if r.Body != nil {
|
||||
rid, rstream := h.muxer.Serve()
|
||||
defer rstream.Close()
|
||||
go ServeIOReader(r.Body, rstream)
|
||||
requestBodyStream = rid
|
||||
}
|
||||
|
||||
forwardedRequest := &http.Request{
|
||||
Method: r.Method,
|
||||
URL: r.URL,
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
Header: r.Header,
|
||||
Host: r.Host,
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
RequestURI: r.RequestURI,
|
||||
}
|
||||
|
||||
if err := h.client.Call("LocalHooks.ServeHTTP", ServeHTTPArgs{
|
||||
ResponseWriterStream: responseWriterStream,
|
||||
Request: forwardedRequest,
|
||||
RequestBodyStream: requestBodyStream,
|
||||
}, nil); err != nil {
|
||||
mlog.Error("Plugin failed to ServeHTTP", mlog.String("plugin_id", h.pluginId), mlog.Err(err))
|
||||
http.Error(w, "500 internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
if !h.implemented[remoteExecuteCommand] {
|
||||
return nil, model.NewAppError("RemoteHooks.ExecuteCommand", "plugin.rpcplugin.invocation.error", nil, "err=ExecuteCommand hook not implemented", http.StatusInternalServerError)
|
||||
}
|
||||
var reply HooksExecuteCommandReply
|
||||
if err := h.client.Call("LocalHooks.ExecuteCommand", args, &reply); err != nil {
|
||||
return nil, model.NewAppError("RemoteHooks.ExecuteCommand", "plugin.rpcplugin.invocation.error", nil, "err="+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return reply.Response, reply.Error
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) MessageWillBePosted(args *model.Post) (*model.Post, string) {
|
||||
if !h.implemented[remoteMessageWillBePosted] {
|
||||
return args, ""
|
||||
}
|
||||
var reply MessageWillBeReply
|
||||
if err := h.client.Call("LocalHooks.MessageWillBePosted", args, &reply); err != nil {
|
||||
return nil, ""
|
||||
}
|
||||
return reply.Post, reply.RejectionReason
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) MessageWillBeUpdated(newPost, oldPost *model.Post) (*model.Post, string) {
|
||||
if !h.implemented[remoteMessageWillBeUpdated] {
|
||||
return newPost, ""
|
||||
}
|
||||
var reply MessageWillBeReply
|
||||
args := &MessageUpdatedArgs{
|
||||
NewPost: newPost,
|
||||
OldPost: oldPost,
|
||||
}
|
||||
if err := h.client.Call("LocalHooks.MessageWillBeUpdated", args, &reply); err != nil {
|
||||
return nil, ""
|
||||
}
|
||||
return reply.Post, reply.RejectionReason
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) MessageHasBeenPosted(args *model.Post) {
|
||||
if !h.implemented[remoteMessageHasBeenPosted] {
|
||||
return
|
||||
}
|
||||
if err := h.client.Call("LocalHooks.MessageHasBeenPosted", args, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) MessageHasBeenUpdated(newPost, oldPost *model.Post) {
|
||||
if !h.implemented[remoteMessageHasBeenUpdated] {
|
||||
return
|
||||
}
|
||||
args := &MessageUpdatedArgs{
|
||||
NewPost: newPost,
|
||||
OldPost: oldPost,
|
||||
}
|
||||
if err := h.client.Call("LocalHooks.MessageHasBeenUpdated", args, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RemoteHooks) Close() error {
|
||||
if h.apiCloser != nil {
|
||||
h.apiCloser.Close()
|
||||
h.apiCloser = nil
|
||||
}
|
||||
return h.client.Close()
|
||||
}
|
||||
|
||||
func ConnectHooks(conn io.ReadWriteCloser, muxer *Muxer, pluginId string) (*RemoteHooks, error) {
|
||||
remote := &RemoteHooks{
|
||||
client: rpc.NewClient(conn),
|
||||
muxer: muxer,
|
||||
pluginId: pluginId,
|
||||
}
|
||||
implemented, err := remote.Implemented()
|
||||
if err != nil {
|
||||
remote.Close()
|
||||
return nil, err
|
||||
}
|
||||
for _, method := range implemented {
|
||||
switch method {
|
||||
case "OnActivate":
|
||||
remote.implemented[remoteOnActivate] = true
|
||||
case "OnDeactivate":
|
||||
remote.implemented[remoteOnDeactivate] = true
|
||||
case "OnConfigurationChange":
|
||||
remote.implemented[remoteOnConfigurationChange] = true
|
||||
case "ServeHTTP":
|
||||
remote.implemented[remoteServeHTTP] = true
|
||||
case "ExecuteCommand":
|
||||
remote.implemented[remoteExecuteCommand] = true
|
||||
case "MessageWillBePosted":
|
||||
remote.implemented[remoteMessageWillBePosted] = true
|
||||
case "MessageWillBeUpdated":
|
||||
remote.implemented[remoteMessageWillBeUpdated] = true
|
||||
case "MessageHasBeenPosted":
|
||||
remote.implemented[remoteMessageHasBeenPosted] = true
|
||||
case "MessageHasBeenUpdated":
|
||||
remote.implemented[remoteMessageHasBeenUpdated] = true
|
||||
}
|
||||
}
|
||||
return remote, nil
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
)
|
||||
|
||||
func testHooksRPC(hooks interface{}, f func(*RemoteHooks)) error {
|
||||
r1, w1 := io.Pipe()
|
||||
r2, w2 := io.Pipe()
|
||||
|
||||
c1 := NewMuxer(NewReadWriteCloser(r1, w2), false)
|
||||
defer c1.Close()
|
||||
|
||||
c2 := NewMuxer(NewReadWriteCloser(r2, w1), true)
|
||||
defer c2.Close()
|
||||
|
||||
id, server := c1.Serve()
|
||||
go ServeHooks(hooks, server, c1)
|
||||
|
||||
remote, err := ConnectHooks(c2.Connect(id), c2, "plugin_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer remote.Close()
|
||||
|
||||
f(remote)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHooks(t *testing.T) {
|
||||
var api plugintest.API
|
||||
var hooks plugintest.Hooks
|
||||
defer hooks.AssertExpectations(t)
|
||||
|
||||
assert.NoError(t, testHooksRPC(&hooks, func(remote *RemoteHooks) {
|
||||
hooks.On("OnActivate", mock.AnythingOfType("*rpcplugin.RemoteAPI")).Return(nil)
|
||||
assert.NoError(t, remote.OnActivate(&api))
|
||||
|
||||
hooks.On("OnDeactivate").Return(nil)
|
||||
assert.NoError(t, remote.OnDeactivate())
|
||||
|
||||
hooks.On("OnConfigurationChange").Return(nil)
|
||||
assert.NoError(t, remote.OnConfigurationChange())
|
||||
|
||||
hooks.On("ServeHTTP", mock.AnythingOfType("*rpcplugin.RemoteHTTPResponseWriter"), mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) {
|
||||
w := args.Get(0).(http.ResponseWriter)
|
||||
r := args.Get(1).(*http.Request)
|
||||
assert.Equal(t, "/foo", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "asdf", string(body))
|
||||
assert.Equal(t, "header", r.Header.Get("Test-Header"))
|
||||
w.Write([]byte("bar"))
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r, err := http.NewRequest("POST", "/foo", strings.NewReader("asdf"))
|
||||
r.Header.Set("Test-Header", "header")
|
||||
assert.NoError(t, err)
|
||||
remote.ServeHTTP(w, r)
|
||||
|
||||
resp := w.Result()
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "bar", string(body))
|
||||
|
||||
hooks.On("ExecuteCommand", &model.CommandArgs{
|
||||
Command: "/foo",
|
||||
}).Return(&model.CommandResponse{
|
||||
Text: "bar",
|
||||
}, nil)
|
||||
commandResponse, appErr := hooks.ExecuteCommand(&model.CommandArgs{
|
||||
Command: "/foo",
|
||||
})
|
||||
assert.Equal(t, "bar", commandResponse.Text)
|
||||
assert.Nil(t, appErr)
|
||||
|
||||
hooks.On("MessageWillBePosted", mock.AnythingOfType("*model.Post")).Return(func(post *model.Post) *model.Post {
|
||||
post.Message += "_testing"
|
||||
return post
|
||||
}, "changemessage")
|
||||
post, changemessage := remote.MessageWillBePosted(&model.Post{Id: "1", Message: "base"})
|
||||
assert.Equal(t, "changemessage", changemessage)
|
||||
assert.Equal(t, "base_testing", post.Message)
|
||||
assert.Equal(t, "1", post.Id)
|
||||
|
||||
hooks.On("MessageWillBeUpdated", mock.AnythingOfType("*model.Post"), mock.AnythingOfType("*model.Post")).Return(func(newPost, oldPost *model.Post) *model.Post {
|
||||
newPost.Message += "_testing"
|
||||
return newPost
|
||||
}, "changemessage2")
|
||||
post2, changemessage2 := remote.MessageWillBeUpdated(&model.Post{Id: "2", Message: "base2"}, &model.Post{Id: "OLD", Message: "OLDMESSAGE"})
|
||||
assert.Equal(t, "changemessage2", changemessage2)
|
||||
assert.Equal(t, "base2_testing", post2.Message)
|
||||
assert.Equal(t, "2", post2.Id)
|
||||
|
||||
hooks.On("MessageHasBeenPosted", mock.AnythingOfType("*model.Post")).Return(nil)
|
||||
remote.MessageHasBeenPosted(&model.Post{})
|
||||
|
||||
hooks.On("MessageHasBeenUpdated", mock.AnythingOfType("*model.Post"), mock.AnythingOfType("*model.Post")).Return(nil)
|
||||
remote.MessageHasBeenUpdated(&model.Post{}, &model.Post{})
|
||||
}))
|
||||
}
|
||||
|
||||
func TestHooks_Concurrency(t *testing.T) {
|
||||
var hooks plugintest.Hooks
|
||||
defer hooks.AssertExpectations(t)
|
||||
|
||||
assert.NoError(t, testHooksRPC(&hooks, func(remote *RemoteHooks) {
|
||||
ch := make(chan bool)
|
||||
|
||||
hooks.On("ServeHTTP", mock.AnythingOfType("*rpcplugin.RemoteHTTPResponseWriter"), mock.AnythingOfType("*http.Request")).Run(func(args mock.Arguments) {
|
||||
r := args.Get(1).(*http.Request)
|
||||
if r.URL.Path == "/1" {
|
||||
<-ch
|
||||
} else {
|
||||
ch <- true
|
||||
}
|
||||
})
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
req, err := http.NewRequest("GET", "/1", nil)
|
||||
require.NoError(t, err)
|
||||
remote.ServeHTTP(rec, req)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
req, err := http.NewRequest("GET", "/2", nil)
|
||||
require.NoError(t, err)
|
||||
remote.ServeHTTP(rec, req)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
}))
|
||||
}
|
||||
|
||||
type testHooks struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (h *testHooks) OnActivate(api plugin.API) error {
|
||||
return h.Called(api).Error(0)
|
||||
}
|
||||
|
||||
func TestHooks_PartiallyImplemented(t *testing.T) {
|
||||
var api plugintest.API
|
||||
var hooks testHooks
|
||||
defer hooks.AssertExpectations(t)
|
||||
|
||||
assert.NoError(t, testHooksRPC(&hooks, func(remote *RemoteHooks) {
|
||||
implemented, err := remote.Implemented()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"OnActivate"}, implemented)
|
||||
|
||||
hooks.On("OnActivate", mock.AnythingOfType("*rpcplugin.RemoteAPI")).Return(nil)
|
||||
assert.NoError(t, remote.OnActivate(&api))
|
||||
|
||||
assert.NoError(t, remote.OnDeactivate())
|
||||
}))
|
||||
}
|
||||
|
||||
type benchmarkHooks struct{}
|
||||
|
||||
func (*benchmarkHooks) OnDeactivate() error { return nil }
|
||||
|
||||
func (*benchmarkHooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ioutil.ReadAll(r.Body)
|
||||
w.Header().Set("Foo-Header", "foo")
|
||||
http.Error(w, "foo", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func BenchmarkHooks_OnDeactivate(b *testing.B) {
|
||||
var hooks benchmarkHooks
|
||||
|
||||
if err := testHooksRPC(&hooks, func(remote *RemoteHooks) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
remote.OnDeactivate()
|
||||
}
|
||||
b.StopTimer()
|
||||
}); err != nil {
|
||||
b.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHooks_ServeHTTP(b *testing.B) {
|
||||
var hooks benchmarkHooks
|
||||
|
||||
if err := testHooksRPC(&hooks, func(remote *RemoteHooks) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
w := httptest.NewRecorder()
|
||||
r, _ := http.NewRequest("POST", "/foo", strings.NewReader("12345678901234567890"))
|
||||
remote.ServeHTTP(w, r)
|
||||
}
|
||||
b.StopTimer()
|
||||
}); err != nil {
|
||||
b.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHooks_Unimplemented(b *testing.B) {
|
||||
var hooks testHooks
|
||||
|
||||
if err := testHooksRPC(&hooks, func(remote *RemoteHooks) {
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
remote.OnDeactivate()
|
||||
}
|
||||
b.StopTimer()
|
||||
}); err != nil {
|
||||
b.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
type LocalHTTPResponseWriter struct {
|
||||
w http.ResponseWriter
|
||||
}
|
||||
|
||||
func (w *LocalHTTPResponseWriter) Header(args struct{}, reply *http.Header) error {
|
||||
*reply = w.w.Header()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *LocalHTTPResponseWriter) Write(args []byte, reply *struct{}) error {
|
||||
_, err := w.w.Write(args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *LocalHTTPResponseWriter) WriteHeader(args int, reply *struct{}) error {
|
||||
w.w.WriteHeader(args)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *LocalHTTPResponseWriter) SyncHeader(args http.Header, reply *struct{}) error {
|
||||
dest := w.w.Header()
|
||||
for k := range dest {
|
||||
if _, ok := args[k]; !ok {
|
||||
delete(dest, k)
|
||||
}
|
||||
}
|
||||
for k, v := range args {
|
||||
dest[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ServeHTTPResponseWriter(w http.ResponseWriter, conn io.ReadWriteCloser) {
|
||||
server := rpc.NewServer()
|
||||
server.Register(&LocalHTTPResponseWriter{
|
||||
w: w,
|
||||
})
|
||||
server.ServeConn(conn)
|
||||
}
|
||||
|
||||
type RemoteHTTPResponseWriter struct {
|
||||
client *rpc.Client
|
||||
header http.Header
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = (*RemoteHTTPResponseWriter)(nil)
|
||||
|
||||
func (w *RemoteHTTPResponseWriter) Header() http.Header {
|
||||
if w.header == nil {
|
||||
w.client.Call("LocalHTTPResponseWriter.Header", struct{}{}, &w.header)
|
||||
}
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *RemoteHTTPResponseWriter) Write(b []byte) (int, error) {
|
||||
if err := w.client.Call("LocalHTTPResponseWriter.SyncHeader", w.header, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := w.client.Call("LocalHTTPResponseWriter.Write", b, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (w *RemoteHTTPResponseWriter) WriteHeader(statusCode int) {
|
||||
if err := w.client.Call("LocalHTTPResponseWriter.SyncHeader", w.header, nil); err != nil {
|
||||
return
|
||||
}
|
||||
w.client.Call("LocalHTTPResponseWriter.WriteHeader", statusCode, nil)
|
||||
}
|
||||
|
||||
func (h *RemoteHTTPResponseWriter) Close() error {
|
||||
return h.client.Close()
|
||||
}
|
||||
|
||||
func ConnectHTTPResponseWriter(conn io.ReadWriteCloser) *RemoteHTTPResponseWriter {
|
||||
return &RemoteHTTPResponseWriter{
|
||||
client: rpc.NewClient(conn),
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testHTTPResponseWriterRPC(w http.ResponseWriter, f func(w http.ResponseWriter)) {
|
||||
r1, w1 := io.Pipe()
|
||||
r2, w2 := io.Pipe()
|
||||
|
||||
c1 := NewMuxer(NewReadWriteCloser(r1, w2), false)
|
||||
defer c1.Close()
|
||||
|
||||
c2 := NewMuxer(NewReadWriteCloser(r2, w1), true)
|
||||
defer c2.Close()
|
||||
|
||||
id, server := c1.Serve()
|
||||
go ServeHTTPResponseWriter(w, server)
|
||||
|
||||
remote := ConnectHTTPResponseWriter(c2.Connect(id))
|
||||
defer remote.Close()
|
||||
|
||||
f(remote)
|
||||
}
|
||||
|
||||
func TestHTTP(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
testHTTPResponseWriterRPC(w, func(w http.ResponseWriter) {
|
||||
headers := w.Header()
|
||||
headers.Set("Test-Header-A", "a")
|
||||
headers.Set("Test-Header-B", "b")
|
||||
w.Header().Set("Test-Header-C", "c")
|
||||
w.WriteHeader(http.StatusPaymentRequired)
|
||||
n, err := w.Write([]byte("this is "))
|
||||
assert.Equal(t, 8, n)
|
||||
assert.NoError(t, err)
|
||||
n, err = w.Write([]byte("a test"))
|
||||
assert.Equal(t, 6, n)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
r := w.Result()
|
||||
defer r.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusPaymentRequired, r.StatusCode)
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "this is a test", body)
|
||||
|
||||
assert.Equal(t, "a", r.Header.Get("Test-Header-A"))
|
||||
assert.Equal(t, "b", r.Header.Get("Test-Header-B"))
|
||||
assert.Equal(t, "c", r.Header.Get("Test-Header-C"))
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Returns a new IPC for the parent process and a set of files to pass on to the child.
|
||||
//
|
||||
// The returned files must be closed after the child process is started.
|
||||
func NewIPC() (io.ReadWriteCloser, []*os.File, error) {
|
||||
parentReader, childWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
childReader, parentWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
parentReader.Close()
|
||||
childWriter.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
return NewReadWriteCloser(parentReader, parentWriter), []*os.File{childReader, childWriter}, nil
|
||||
}
|
||||
|
||||
// Returns the IPC instance inherited by the process from its parent.
|
||||
func InheritedIPC(fd0, fd1 uintptr) (io.ReadWriteCloser, error) {
|
||||
return NewReadWriteCloser(os.NewFile(fd0, ""), os.NewFile(fd1, "")), nil
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
|
||||
)
|
||||
|
||||
func TestIPC(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
pingpong := filepath.Join(dir, "pingpong.exe")
|
||||
rpcplugintest.CompileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ipc, err := rpcplugin.InheritedProcessIPC()
|
||||
if err != nil {
|
||||
log.Fatal("unable to get inherited ipc")
|
||||
}
|
||||
defer ipc.Close()
|
||||
_, err = ipc.Write([]byte("ping"))
|
||||
if err != nil {
|
||||
log.Fatal("unable to write to ipc")
|
||||
}
|
||||
b := make([]byte, 10)
|
||||
n, err := ipc.Read(b)
|
||||
if err != nil {
|
||||
log.Fatal("unable to read from ipc")
|
||||
}
|
||||
if n != 4 || string(b[:4]) != "pong" {
|
||||
log.Fatal("unexpected response")
|
||||
}
|
||||
}
|
||||
`, pingpong)
|
||||
|
||||
p, ipc, err := NewProcess(context.Background(), pingpong)
|
||||
require.NoError(t, err)
|
||||
defer ipc.Close()
|
||||
b := make([]byte, 10)
|
||||
n, err := ipc.Read(b)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 4, n)
|
||||
assert.Equal(t, "ping", string(b[:4]))
|
||||
_, err = ipc.Write([]byte("pong"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, p.Wait())
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Makes a set of hooks available via RPC. This function never returns.
|
||||
func Main(hooks interface{}) {
|
||||
ipc, err := InheritedProcessIPC()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
muxer := NewMuxer(ipc, true)
|
||||
id, conn := muxer.Serve()
|
||||
buf := make([]byte, 11)
|
||||
buf[0] = 0
|
||||
n := binary.PutVarint(buf[1:], id)
|
||||
if _, err := muxer.Write(buf[:1+n]); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
ServeHooks(hooks, conn, muxer)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Returns the hooks being served by a call to Main.
|
||||
func ConnectMain(muxer *Muxer, pluginId string) (*RemoteHooks, error) {
|
||||
buf := make([]byte, 1)
|
||||
if _, err := muxer.Read(buf); err != nil {
|
||||
return nil, err
|
||||
} else if buf[0] != 0 {
|
||||
return nil, fmt.Errorf("unexpected control byte")
|
||||
}
|
||||
reader := bufio.NewReader(muxer)
|
||||
id, err := binary.ReadVarint(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ConnectHooks(muxer.Connect(id), muxer, pluginId)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
|
||||
)
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
// Setup a global logger to catch tests logging outside of app context
|
||||
// The global logger will be stomped by apps initalizing but that's fine for testing. Ideally this won't happen.
|
||||
mlog.InitGlobalLogger(mlog.NewLogger(&mlog.LoggerConfiguration{
|
||||
EnableConsole: true,
|
||||
ConsoleJson: true,
|
||||
ConsoleLevel: "error",
|
||||
EnableFile: false,
|
||||
}))
|
||||
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
plugin := filepath.Join(dir, "plugin.exe")
|
||||
rpcplugintest.CompileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type MyPlugin struct {}
|
||||
|
||||
func main() {
|
||||
rpcplugin.Main(&MyPlugin{})
|
||||
}
|
||||
`, plugin)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p, ipc, err := NewProcess(ctx, plugin)
|
||||
require.NoError(t, err)
|
||||
defer p.Wait()
|
||||
|
||||
muxer := NewMuxer(ipc, false)
|
||||
defer muxer.Close()
|
||||
|
||||
defer cancel()
|
||||
|
||||
var api plugintest.API
|
||||
|
||||
hooks, err := ConnectMain(muxer, "plugin_id")
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, hooks.OnActivate(&api))
|
||||
assert.NoError(t, hooks.OnDeactivate())
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Muxer allows multiple bidirectional streams to be transmitted over a single connection.
|
||||
//
|
||||
// Muxer is safe for use by multiple goroutines.
|
||||
//
|
||||
// Streams opened on the muxer must be periodically drained in order to reclaim read buffer memory.
|
||||
// In other words, readers must consume incoming data as it comes in.
|
||||
type Muxer struct {
|
||||
// writeMutex guards conn writes
|
||||
writeMutex sync.Mutex
|
||||
conn io.ReadWriteCloser
|
||||
|
||||
// didCloseConn is a boolean (0 or 1) used from multiple goroutines via atomic operations
|
||||
didCloseConn int32
|
||||
|
||||
// streamsMutex guards streams and nextId
|
||||
streamsMutex sync.Mutex
|
||||
nextId int64
|
||||
streams map[int64]*muxerStream
|
||||
|
||||
stream0Reader *io.PipeReader
|
||||
stream0Writer *io.PipeWriter
|
||||
result chan error
|
||||
}
|
||||
|
||||
// Creates a new Muxer.
|
||||
//
|
||||
// conn must be safe for simultaneous reads by one goroutine and writes by another.
|
||||
//
|
||||
// For two muxers communicating with each other via a connection, parity must be true for exactly
|
||||
// one of them.
|
||||
func NewMuxer(conn io.ReadWriteCloser, parity bool) *Muxer {
|
||||
s0r, s0w := io.Pipe()
|
||||
muxer := &Muxer{
|
||||
conn: conn,
|
||||
streams: make(map[int64]*muxerStream),
|
||||
result: make(chan error, 1),
|
||||
nextId: 1,
|
||||
stream0Reader: s0r,
|
||||
stream0Writer: s0w,
|
||||
}
|
||||
if parity {
|
||||
muxer.nextId = 2
|
||||
}
|
||||
go muxer.run()
|
||||
return muxer
|
||||
}
|
||||
|
||||
// Opens a new stream with a unique id.
|
||||
//
|
||||
// Writes made to the stream before the other end calls Connect will be discarded.
|
||||
func (m *Muxer) Serve() (int64, io.ReadWriteCloser) {
|
||||
m.streamsMutex.Lock()
|
||||
id := m.nextId
|
||||
m.nextId += 2
|
||||
m.streamsMutex.Unlock()
|
||||
return id, m.Connect(id)
|
||||
}
|
||||
|
||||
// Opens a remotely opened stream.
|
||||
func (m *Muxer) Connect(id int64) io.ReadWriteCloser {
|
||||
m.streamsMutex.Lock()
|
||||
defer m.streamsMutex.Unlock()
|
||||
mutex := &sync.Mutex{}
|
||||
stream := &muxerStream{
|
||||
id: id,
|
||||
muxer: m,
|
||||
mutex: mutex,
|
||||
readWake: sync.NewCond(mutex),
|
||||
}
|
||||
m.streams[id] = stream
|
||||
return stream
|
||||
}
|
||||
|
||||
// Calling Read on the muxer directly performs a read on a dedicated, always-open channel.
|
||||
func (m *Muxer) Read(p []byte) (int, error) {
|
||||
return m.stream0Reader.Read(p)
|
||||
}
|
||||
|
||||
// Calling Write on the muxer directly performs a write on a dedicated, always-open channel.
|
||||
func (m *Muxer) Write(p []byte) (int, error) {
|
||||
return m.write(p, 0)
|
||||
}
|
||||
|
||||
// Closes the muxer.
|
||||
func (m *Muxer) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&m.didCloseConn, 0, 1) {
|
||||
m.conn.Close()
|
||||
}
|
||||
m.stream0Reader.Close()
|
||||
m.stream0Writer.Close()
|
||||
<-m.result
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Muxer) IsClosed() bool {
|
||||
return atomic.LoadInt32(&m.didCloseConn) > 0
|
||||
}
|
||||
|
||||
func (m *Muxer) write(p []byte, sid int64) (int, error) {
|
||||
m.writeMutex.Lock()
|
||||
defer m.writeMutex.Unlock()
|
||||
if m.IsClosed() {
|
||||
return 0, fmt.Errorf("muxer closed")
|
||||
}
|
||||
var buf [10]byte
|
||||
n := binary.PutVarint(buf[:], sid)
|
||||
if _, err := m.conn.Write(buf[:n]); err != nil {
|
||||
m.shutdown(err)
|
||||
return 0, err
|
||||
}
|
||||
n = binary.PutVarint(buf[:], int64(len(p)))
|
||||
if _, err := m.conn.Write(buf[:n]); err != nil {
|
||||
m.shutdown(err)
|
||||
return 0, err
|
||||
}
|
||||
if len(p) > 0 {
|
||||
if _, err := m.conn.Write(p); err != nil {
|
||||
m.shutdown(err)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (m *Muxer) rm(sid int64) {
|
||||
m.streamsMutex.Lock()
|
||||
defer m.streamsMutex.Unlock()
|
||||
delete(m.streams, sid)
|
||||
}
|
||||
|
||||
func (m *Muxer) run() {
|
||||
m.shutdown(m.loop())
|
||||
}
|
||||
|
||||
func (m *Muxer) loop() error {
|
||||
reader := bufio.NewReader(m.conn)
|
||||
|
||||
for {
|
||||
sid, err := binary.ReadVarint(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
len, err := binary.ReadVarint(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sid == 0 {
|
||||
if _, err := io.CopyN(m.stream0Writer, reader, len); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
m.streamsMutex.Lock()
|
||||
stream, ok := m.streams[sid]
|
||||
m.streamsMutex.Unlock()
|
||||
if !ok {
|
||||
if _, err := reader.Discard(int(len)); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
stream.mutex.Lock()
|
||||
if stream.isClosed {
|
||||
stream.mutex.Unlock()
|
||||
if _, err := reader.Discard(int(len)); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len == 0 {
|
||||
stream.remoteClosed = true
|
||||
} else {
|
||||
_, err = io.CopyN(&stream.readBuf, reader, len)
|
||||
}
|
||||
stream.mutex.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stream.readWake.Signal()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Muxer) shutdown(err error) {
|
||||
if atomic.CompareAndSwapInt32(&m.didCloseConn, 0, 1) {
|
||||
m.conn.Close()
|
||||
}
|
||||
go func() {
|
||||
m.streamsMutex.Lock()
|
||||
for _, stream := range m.streams {
|
||||
stream.mutex.Lock()
|
||||
stream.readWake.Signal()
|
||||
stream.mutex.Unlock()
|
||||
}
|
||||
m.streams = make(map[int64]*muxerStream)
|
||||
m.streamsMutex.Unlock()
|
||||
}()
|
||||
m.result <- err
|
||||
}
|
||||
|
||||
type muxerStream struct {
|
||||
id int64
|
||||
muxer *Muxer
|
||||
readBuf bytes.Buffer
|
||||
mutex *sync.Mutex
|
||||
readWake *sync.Cond
|
||||
isClosed bool
|
||||
remoteClosed bool
|
||||
}
|
||||
|
||||
func (s *muxerStream) Read(p []byte) (int, error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
for {
|
||||
if s.muxer.IsClosed() {
|
||||
return 0, fmt.Errorf("muxer closed")
|
||||
} else if s.isClosed {
|
||||
return 0, io.EOF
|
||||
} else if s.readBuf.Len() > 0 {
|
||||
return s.readBuf.Read(p)
|
||||
} else if s.remoteClosed {
|
||||
return 0, io.EOF
|
||||
}
|
||||
s.readWake.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *muxerStream) Write(p []byte) (int, error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
if s.isClosed {
|
||||
return 0, fmt.Errorf("stream closed")
|
||||
}
|
||||
return s.muxer.write(p, s.id)
|
||||
}
|
||||
|
||||
func (s *muxerStream) Close() error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
if !s.isClosed {
|
||||
s.muxer.write(nil, s.id)
|
||||
s.isClosed = true
|
||||
s.muxer.rm(s.id)
|
||||
}
|
||||
s.readWake.Signal()
|
||||
return nil
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMuxer(t *testing.T) {
|
||||
r1, w1 := io.Pipe()
|
||||
r2, w2 := io.Pipe()
|
||||
|
||||
alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
|
||||
defer func() { assert.NoError(t, alice.Close()) }()
|
||||
|
||||
bob := NewMuxer(NewReadWriteCloser(r2, w1), true)
|
||||
defer func() { assert.NoError(t, bob.Close()) }()
|
||||
|
||||
id1, alice1 := alice.Serve()
|
||||
defer func() { assert.NoError(t, alice1.Close()) }()
|
||||
|
||||
id2, bob2 := bob.Serve()
|
||||
defer func() { assert.NoError(t, bob2.Close()) }()
|
||||
|
||||
done1 := make(chan bool)
|
||||
done2 := make(chan bool)
|
||||
|
||||
go func() {
|
||||
bob1 := bob.Connect(id1)
|
||||
defer func() { assert.NoError(t, bob1.Close()) }()
|
||||
|
||||
n, err := bob1.Write([]byte("ping1.0"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, n, 7)
|
||||
|
||||
n, err = bob1.Write([]byte("ping1.1"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, n, 7)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
alice2 := alice.Connect(id2)
|
||||
defer func() { assert.NoError(t, alice2.Close()) }()
|
||||
|
||||
n, err := alice2.Write([]byte("ping2.0"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, n, 7)
|
||||
|
||||
buf := make([]byte, 20)
|
||||
n, err = alice2.Read(buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, n, 7)
|
||||
assert.Equal(t, []byte("pong2.0"), buf[:n])
|
||||
|
||||
done2 <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 7)
|
||||
n, err := io.ReadFull(alice1, buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, n, 7)
|
||||
assert.Equal(t, []byte("ping1.0"), buf[:n])
|
||||
|
||||
n, err = alice1.Read(buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, n, 7)
|
||||
assert.Equal(t, []byte("ping1.1"), buf[:n])
|
||||
|
||||
done1 <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 20)
|
||||
n, err := bob2.Read(buf)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, n, 7)
|
||||
assert.Equal(t, []byte("ping2.0"), buf[:n])
|
||||
|
||||
n, err = bob2.Write([]byte("pong2.0"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, n, 7)
|
||||
}()
|
||||
|
||||
<-done1
|
||||
<-done2
|
||||
}
|
||||
|
||||
// Closing a muxer during a read should unblock, but return an error.
|
||||
func TestMuxer_CloseDuringRead(t *testing.T) {
|
||||
r1, w1 := io.Pipe()
|
||||
r2, w2 := io.Pipe()
|
||||
|
||||
alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
|
||||
|
||||
bob := NewMuxer(NewReadWriteCloser(r2, w1), true)
|
||||
defer func() { assert.NoError(t, bob.Close()) }()
|
||||
|
||||
_, s := alice.Serve()
|
||||
|
||||
go alice.Close()
|
||||
buf := make([]byte, 20)
|
||||
n, err := s.Read(buf)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.NotNil(t, err)
|
||||
assert.NotEqual(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Closing a stream during a read should unblock and return io.EOF since this is the way to
|
||||
// gracefully close a connection.
|
||||
func TestMuxer_StreamCloseDuringRead(t *testing.T) {
|
||||
r1, w1 := io.Pipe()
|
||||
r2, w2 := io.Pipe()
|
||||
|
||||
alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
|
||||
defer func() { assert.NoError(t, alice.Close()) }()
|
||||
|
||||
bob := NewMuxer(NewReadWriteCloser(r2, w1), true)
|
||||
defer func() { assert.NoError(t, bob.Close()) }()
|
||||
|
||||
_, s := alice.Serve()
|
||||
|
||||
go s.Close()
|
||||
buf := make([]byte, 20)
|
||||
n, err := s.Read(buf)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Closing a stream during a read should unblock and return io.EOF since this is the way for the
|
||||
// remote to gracefully close a connection.
|
||||
func TestMuxer_RemoteStreamCloseDuringRead(t *testing.T) {
|
||||
r1, w1 := io.Pipe()
|
||||
r2, w2 := io.Pipe()
|
||||
|
||||
alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
|
||||
defer func() { assert.NoError(t, alice.Close()) }()
|
||||
|
||||
bob := NewMuxer(NewReadWriteCloser(r2, w1), true)
|
||||
defer func() { assert.NoError(t, bob.Close()) }()
|
||||
|
||||
id, as := alice.Serve()
|
||||
bs := bob.Connect(id)
|
||||
|
||||
go func() {
|
||||
as.Write([]byte("foo"))
|
||||
as.Close()
|
||||
}()
|
||||
buf := make([]byte, 20)
|
||||
n, err := bs.Read(buf)
|
||||
assert.Equal(t, 3, n)
|
||||
assert.Equal(t, "foo", string(buf[:n]))
|
||||
n, err = bs.Read(buf)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.Equal(t, io.EOF, err)
|
||||
}
|
||||
|
||||
// Closing a muxer during a write should unblock, but return an error.
|
||||
func TestMuxer_CloseDuringWrite(t *testing.T) {
|
||||
r1, w1 := io.Pipe()
|
||||
r2, w2 := io.Pipe()
|
||||
|
||||
alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
|
||||
|
||||
// Don't connect bob to let writes will block forever.
|
||||
defer r2.Close()
|
||||
defer w1.Close()
|
||||
|
||||
_, s := alice.Serve()
|
||||
|
||||
go alice.Close()
|
||||
buf := make([]byte, 20)
|
||||
n, err := s.Write(buf)
|
||||
assert.Equal(t, 0, n)
|
||||
assert.NotNil(t, err)
|
||||
assert.NotEqual(t, io.EOF, err)
|
||||
}
|
||||
|
||||
func TestMuxer_ReadWrite(t *testing.T) {
|
||||
r1, w1 := io.Pipe()
|
||||
r2, w2 := io.Pipe()
|
||||
|
||||
alice := NewMuxer(NewReadWriteCloser(r1, w2), false)
|
||||
defer func() { assert.NoError(t, alice.Close()) }()
|
||||
|
||||
bob := NewMuxer(NewReadWriteCloser(r2, w1), true)
|
||||
defer func() { assert.NoError(t, bob.Close()) }()
|
||||
|
||||
go alice.Write([]byte("hello"))
|
||||
buf := make([]byte, 20)
|
||||
n, err := bob.Read(buf)
|
||||
assert.Equal(t, 5, n)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("hello"), buf[:n])
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Process interface {
|
||||
// Waits for the process to exit and returns an error if a problem occurred or the process exited
|
||||
// with a non-zero status.
|
||||
Wait() error
|
||||
}
|
||||
|
||||
// NewProcess launches an RPC executable in a new process and returns an IPC that can be used to
|
||||
// communicate with it.
|
||||
func NewProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) {
|
||||
return newProcess(ctx, path)
|
||||
}
|
||||
|
||||
// When called on a process launched with NewProcess, returns the inherited IPC.
|
||||
func InheritedProcessIPC() (io.ReadWriteCloser, error) {
|
||||
return inheritedProcessIPC()
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
|
||||
)
|
||||
|
||||
func TestProcess(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ping := filepath.Join(dir, "ping.exe")
|
||||
rpcplugintest.CompileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ipc, err := rpcplugin.InheritedProcessIPC()
|
||||
if err != nil {
|
||||
log.Fatal("unable to get inherited ipc")
|
||||
}
|
||||
defer ipc.Close()
|
||||
_, err = ipc.Write([]byte("ping"))
|
||||
if err != nil {
|
||||
log.Fatal("unable to write to ipc")
|
||||
}
|
||||
}
|
||||
`, ping)
|
||||
|
||||
p, ipc, err := NewProcess(context.Background(), ping)
|
||||
require.NoError(t, err)
|
||||
defer ipc.Close()
|
||||
b := make([]byte, 10)
|
||||
n, err := ipc.Read(b)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 4, n)
|
||||
assert.Equal(t, "ping", string(b[:4]))
|
||||
require.NoError(t, p.Wait())
|
||||
}
|
||||
|
||||
func TestInvalidProcess(t *testing.T) {
|
||||
p, ipc, err := NewProcess(context.Background(), "thisfileshouldnotexist")
|
||||
require.Nil(t, p)
|
||||
require.Nil(t, ipc)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type process struct {
|
||||
command *exec.Cmd
|
||||
}
|
||||
|
||||
func newProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) {
|
||||
ipc, childFiles, err := NewIPC()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer childFiles[0].Close()
|
||||
defer childFiles[1].Close()
|
||||
|
||||
cmd := exec.CommandContext(ctx, path)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.ExtraFiles = childFiles
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
ipc.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &process{
|
||||
command: cmd,
|
||||
}, ipc, nil
|
||||
}
|
||||
|
||||
func (p *process) Wait() error {
|
||||
return p.command.Wait()
|
||||
}
|
||||
|
||||
func inheritedProcessIPC() (io.ReadWriteCloser, error) {
|
||||
return InheritedIPC(3, 4)
|
||||
}
|
||||
@@ -1,648 +0,0 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type process struct {
|
||||
command *cmd
|
||||
}
|
||||
|
||||
func newProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) {
|
||||
ipc, childFiles, err := NewIPC()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer childFiles[0].Close()
|
||||
defer childFiles[1].Close()
|
||||
|
||||
cmd := commandContext(ctx, path)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.ExtraFiles = childFiles
|
||||
cmd.Env = append(os.Environ(),
|
||||
fmt.Sprintf("MM_IPC_FD0=%v", childFiles[0].Fd()),
|
||||
fmt.Sprintf("MM_IPC_FD1=%v", childFiles[1].Fd()),
|
||||
)
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
ipc.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &process{
|
||||
command: cmd,
|
||||
}, ipc, nil
|
||||
}
|
||||
|
||||
func (p *process) Wait() error {
|
||||
return p.command.Wait()
|
||||
}
|
||||
|
||||
func inheritedProcessIPC() (io.ReadWriteCloser, error) {
|
||||
fd0, err := strconv.ParseUint(os.Getenv("MM_IPC_FD0"), 0, 64)
|
||||
if err != nil {
|
||||
return nil, pkgerrors.Wrapf(err, "unable to get ipc file descriptor 0")
|
||||
}
|
||||
fd1, err := strconv.ParseUint(os.Getenv("MM_IPC_FD1"), 0, 64)
|
||||
if err != nil {
|
||||
return nil, pkgerrors.Wrapf(err, "unable to get ipc file descriptor 1")
|
||||
}
|
||||
return InheritedIPC(uintptr(fd0), uintptr(fd1))
|
||||
}
|
||||
|
||||
// XXX: EVERYTHING BELOW THIS IS COPIED / PASTED STANDARD LIBRARY CODE!
|
||||
// IT CAN BE DELETED IF / WHEN THIS ISSUE IS RESOLVED: https://github.com/golang/go/issues/21085
|
||||
|
||||
// Just about all of os/exec/exec.go is copied / pasted below, altered to use our modified startProcess functions even
|
||||
// further below.
|
||||
|
||||
type cmd struct {
|
||||
// Path is the path of the command to run.
|
||||
//
|
||||
// This is the only field that must be set to a non-zero
|
||||
// value. If Path is relative, it is evaluated relative
|
||||
// to Dir.
|
||||
Path string
|
||||
|
||||
// Args holds command line arguments, including the command as Args[0].
|
||||
// If the Args field is empty or nil, Run uses {Path}.
|
||||
//
|
||||
// In typical use, both Path and Args are set by calling Command.
|
||||
Args []string
|
||||
|
||||
// Env specifies the environment of the process.
|
||||
// If Env is nil, Run uses the current process's environment.
|
||||
Env []string
|
||||
|
||||
// Dir specifies the working directory of the command.
|
||||
// If Dir is the empty string, Run runs the command in the
|
||||
// calling process's current directory.
|
||||
Dir string
|
||||
|
||||
// Stdin specifies the process's standard input.
|
||||
// If Stdin is nil, the process reads from the null device (os.DevNull).
|
||||
// If Stdin is an *os.File, the process's standard input is connected
|
||||
// directly to that file.
|
||||
// Otherwise, during the execution of the command a separate
|
||||
// goroutine reads from Stdin and delivers that data to the command
|
||||
// over a pipe. In this case, Wait does not complete until the goroutine
|
||||
// stops copying, either because it has reached the end of Stdin
|
||||
// (EOF or a read error) or because writing to the pipe returned an error.
|
||||
Stdin io.Reader
|
||||
|
||||
// Stdout and Stderr specify the process's standard output and error.
|
||||
//
|
||||
// If either is nil, Run connects the corresponding file descriptor
|
||||
// to the null device (os.DevNull).
|
||||
//
|
||||
// If Stdout and Stderr are the same writer, at most one
|
||||
// goroutine at a time will call Write.
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
// ExtraFiles specifies additional open files to be inherited by the
|
||||
// new process. It does not include standard input, standard output, or
|
||||
// standard error. If non-nil, entry i becomes file descriptor 3+i.
|
||||
//
|
||||
// BUG(rsc): On OS X 10.6, child processes may sometimes inherit unwanted fds.
|
||||
// https://golang.org/issue/2603
|
||||
ExtraFiles []*os.File
|
||||
|
||||
// SysProcAttr holds optional, operating system-specific attributes.
|
||||
// Run passes it to os.StartProcess as the os.ProcAttr's Sys field.
|
||||
SysProcAttr *syscall.SysProcAttr
|
||||
|
||||
// Process is the underlying process, once started.
|
||||
Process *os.Process
|
||||
|
||||
// ProcessState contains information about an exited process,
|
||||
// available after a call to Wait or Run.
|
||||
ProcessState *os.ProcessState
|
||||
|
||||
ctx context.Context // nil means none
|
||||
lookPathErr error // LookPath error, if any.
|
||||
finished bool // when Wait was called
|
||||
childFiles []*os.File
|
||||
closeAfterStart []io.Closer
|
||||
closeAfterWait []io.Closer
|
||||
goroutine []func() error
|
||||
errch chan error // one send per goroutine
|
||||
waitDone chan struct{}
|
||||
}
|
||||
|
||||
func command(name string, arg ...string) *cmd {
|
||||
cmd := &cmd{
|
||||
Path: name,
|
||||
Args: append([]string{name}, arg...),
|
||||
}
|
||||
if filepath.Base(name) == name {
|
||||
if lp, err := exec.LookPath(name); err != nil {
|
||||
cmd.lookPathErr = err
|
||||
} else {
|
||||
cmd.Path = lp
|
||||
}
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func commandContext(ctx context.Context, name string, arg ...string) *cmd {
|
||||
if ctx == nil {
|
||||
panic("nil Context")
|
||||
}
|
||||
cmd := command(name, arg...)
|
||||
cmd.ctx = ctx
|
||||
return cmd
|
||||
}
|
||||
|
||||
func interfaceEqual(a, b interface{}) bool {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
return a == b
|
||||
}
|
||||
|
||||
func (c *cmd) envv() []string {
|
||||
if c.Env != nil {
|
||||
return c.Env
|
||||
}
|
||||
return os.Environ()
|
||||
}
|
||||
|
||||
func (c *cmd) argv() []string {
|
||||
if len(c.Args) > 0 {
|
||||
return c.Args
|
||||
}
|
||||
return []string{c.Path}
|
||||
}
|
||||
|
||||
var skipStdinCopyError func(error) bool
|
||||
|
||||
func (c *cmd) stdin() (f *os.File, err error) {
|
||||
if c.Stdin == nil {
|
||||
f, err = os.Open(os.DevNull)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.closeAfterStart = append(c.closeAfterStart, f)
|
||||
return
|
||||
}
|
||||
|
||||
if f, ok := c.Stdin.(*os.File); ok {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.closeAfterStart = append(c.closeAfterStart, pr)
|
||||
c.closeAfterWait = append(c.closeAfterWait, pw)
|
||||
c.goroutine = append(c.goroutine, func() error {
|
||||
_, err := io.Copy(pw, c.Stdin)
|
||||
if skip := skipStdinCopyError; skip != nil && skip(err) {
|
||||
err = nil
|
||||
}
|
||||
if err1 := pw.Close(); err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
})
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
func (c *cmd) stdout() (f *os.File, err error) {
|
||||
return c.writerDescriptor(c.Stdout)
|
||||
}
|
||||
|
||||
func (c *cmd) stderr() (f *os.File, err error) {
|
||||
if c.Stderr != nil && interfaceEqual(c.Stderr, c.Stdout) {
|
||||
return c.childFiles[1], nil
|
||||
}
|
||||
return c.writerDescriptor(c.Stderr)
|
||||
}
|
||||
|
||||
func (c *cmd) writerDescriptor(w io.Writer) (f *os.File, err error) {
|
||||
if w == nil {
|
||||
f, err = os.OpenFile(os.DevNull, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.closeAfterStart = append(c.closeAfterStart, f)
|
||||
return
|
||||
}
|
||||
|
||||
if f, ok := w.(*os.File); ok {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.closeAfterStart = append(c.closeAfterStart, pw)
|
||||
c.closeAfterWait = append(c.closeAfterWait, pr)
|
||||
c.goroutine = append(c.goroutine, func() error {
|
||||
_, err := io.Copy(w, pr)
|
||||
pr.Close() // in case io.Copy stopped due to write error
|
||||
return err
|
||||
})
|
||||
return pw, nil
|
||||
}
|
||||
|
||||
func (c *cmd) closeDescriptors(closers []io.Closer) {
|
||||
for _, fd := range closers {
|
||||
fd.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func lookExtensions(path, dir string) (string, error) {
|
||||
if filepath.Base(path) == path {
|
||||
path = filepath.Join(".", path)
|
||||
}
|
||||
if dir == "" {
|
||||
return exec.LookPath(path)
|
||||
}
|
||||
if filepath.VolumeName(path) != "" {
|
||||
return exec.LookPath(path)
|
||||
}
|
||||
if len(path) > 1 && os.IsPathSeparator(path[0]) {
|
||||
return exec.LookPath(path)
|
||||
}
|
||||
dirandpath := filepath.Join(dir, path)
|
||||
// We assume that LookPath will only add file extension.
|
||||
lp, err := exec.LookPath(dirandpath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ext := strings.TrimPrefix(lp, dirandpath)
|
||||
return path + ext, nil
|
||||
}
|
||||
|
||||
// Copied from os/exec/exec.go, altered to use osStartProcess (defined below).
|
||||
func (c *cmd) Start() error {
|
||||
if c.lookPathErr != nil {
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
return c.lookPathErr
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
lp, err := lookExtensions(c.Path, c.Dir)
|
||||
if err != nil {
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
return err
|
||||
}
|
||||
c.Path = lp
|
||||
}
|
||||
if c.Process != nil {
|
||||
return errors.New("exec: already started")
|
||||
}
|
||||
if c.ctx != nil {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
return c.ctx.Err()
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
type F func(*cmd) (*os.File, error)
|
||||
for _, setupFd := range []F{(*cmd).stdin, (*cmd).stdout, (*cmd).stderr} {
|
||||
fd, err := setupFd(c)
|
||||
if err != nil {
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
return err
|
||||
}
|
||||
c.childFiles = append(c.childFiles, fd)
|
||||
}
|
||||
c.childFiles = append(c.childFiles, c.ExtraFiles...)
|
||||
|
||||
var err error
|
||||
c.Process, err = osStartProcess(c.Path, c.argv(), &os.ProcAttr{
|
||||
Dir: c.Dir,
|
||||
Files: c.childFiles,
|
||||
Env: c.envv(),
|
||||
Sys: c.SysProcAttr,
|
||||
})
|
||||
if err != nil {
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
return err
|
||||
}
|
||||
|
||||
c.closeDescriptors(c.closeAfterStart)
|
||||
|
||||
c.errch = make(chan error, len(c.goroutine))
|
||||
for _, fn := range c.goroutine {
|
||||
go func(fn func() error) {
|
||||
c.errch <- fn()
|
||||
}(fn)
|
||||
}
|
||||
|
||||
if c.ctx != nil {
|
||||
c.waitDone = make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
c.Process.Kill()
|
||||
case <-c.waitDone:
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cmd) Wait() error {
|
||||
if c.Process == nil {
|
||||
return errors.New("exec: not started")
|
||||
}
|
||||
if c.finished {
|
||||
return errors.New("exec: Wait was already called")
|
||||
}
|
||||
c.finished = true
|
||||
|
||||
state, err := c.Process.Wait()
|
||||
if c.waitDone != nil {
|
||||
close(c.waitDone)
|
||||
}
|
||||
c.ProcessState = state
|
||||
|
||||
var copyError error
|
||||
for range c.goroutine {
|
||||
if err := <-c.errch; err != nil && copyError == nil {
|
||||
copyError = err
|
||||
}
|
||||
}
|
||||
|
||||
c.closeDescriptors(c.closeAfterWait)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !state.Success() {
|
||||
return &exec.ExitError{ProcessState: state}
|
||||
}
|
||||
|
||||
return copyError
|
||||
}
|
||||
|
||||
// Copied from os/exec_posix.go, altered to use syscallStartProcess (defined below).
|
||||
func osStartProcess(name string, argv []string, attr *os.ProcAttr) (p *os.Process, err error) {
|
||||
// If there is no SysProcAttr (ie. no Chroot or changed
|
||||
// UID/GID), double-check existence of the directory we want
|
||||
// to chdir into. We can make the error clearer this way.
|
||||
if attr != nil && attr.Sys == nil && attr.Dir != "" {
|
||||
if _, err := os.Stat(attr.Dir); err != nil {
|
||||
pe := err.(*os.PathError)
|
||||
pe.Op = "chdir"
|
||||
return nil, pe
|
||||
}
|
||||
}
|
||||
|
||||
sysattr := &syscall.ProcAttr{
|
||||
Dir: attr.Dir,
|
||||
Env: attr.Env,
|
||||
Sys: attr.Sys,
|
||||
}
|
||||
if sysattr.Env == nil {
|
||||
sysattr.Env = os.Environ()
|
||||
}
|
||||
for _, f := range attr.Files {
|
||||
sysattr.Files = append(sysattr.Files, f.Fd())
|
||||
}
|
||||
|
||||
pid, _, e := syscallStartProcess(name, argv, sysattr)
|
||||
if e != nil {
|
||||
return nil, &os.PathError{Op: "fork/exec", Path: name, Err: e}
|
||||
}
|
||||
return os.FindProcess(pid)
|
||||
}
|
||||
|
||||
// Everything from this point on is copied from syscall/exec_windows.go
|
||||
|
||||
func makeCmdLine(args []string) string {
|
||||
var s string
|
||||
for _, v := range args {
|
||||
if s != "" {
|
||||
s += " "
|
||||
}
|
||||
s += syscall.EscapeArg(v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func createEnvBlock(envv []string) *uint16 {
|
||||
if len(envv) == 0 {
|
||||
return &utf16.Encode([]rune("\x00\x00"))[0]
|
||||
}
|
||||
length := 0
|
||||
for _, s := range envv {
|
||||
length += len(s) + 1
|
||||
}
|
||||
length += 1
|
||||
|
||||
b := make([]byte, length)
|
||||
i := 0
|
||||
for _, s := range envv {
|
||||
l := len(s)
|
||||
copy(b[i:i+l], []byte(s))
|
||||
copy(b[i+l:i+l+1], []byte{0})
|
||||
i = i + l + 1
|
||||
}
|
||||
copy(b[i:i+1], []byte{0})
|
||||
|
||||
return &utf16.Encode([]rune(string(b)))[0]
|
||||
}
|
||||
|
||||
func isSlash(c uint8) bool {
|
||||
return c == '\\' || c == '/'
|
||||
}
|
||||
|
||||
func normalizeDir(dir string) (name string, err error) {
|
||||
ndir, err := syscall.FullPath(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ndir) > 2 && isSlash(ndir[0]) && isSlash(ndir[1]) {
|
||||
// dir cannot have \\server\share\path form
|
||||
return "", syscall.EINVAL
|
||||
}
|
||||
return ndir, nil
|
||||
}
|
||||
|
||||
func volToUpper(ch int) int {
|
||||
if 'a' <= ch && ch <= 'z' {
|
||||
ch += 'A' - 'a'
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func joinExeDirAndFName(dir, p string) (name string, err error) {
|
||||
if len(p) == 0 {
|
||||
return "", syscall.EINVAL
|
||||
}
|
||||
if len(p) > 2 && isSlash(p[0]) && isSlash(p[1]) {
|
||||
// \\server\share\path form
|
||||
return p, nil
|
||||
}
|
||||
if len(p) > 1 && p[1] == ':' {
|
||||
// has drive letter
|
||||
if len(p) == 2 {
|
||||
return "", syscall.EINVAL
|
||||
}
|
||||
if isSlash(p[2]) {
|
||||
return p, nil
|
||||
} else {
|
||||
d, err := normalizeDir(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if volToUpper(int(p[0])) == volToUpper(int(d[0])) {
|
||||
return syscall.FullPath(d + "\\" + p[2:])
|
||||
} else {
|
||||
return syscall.FullPath(p)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no drive letter
|
||||
d, err := normalizeDir(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if isSlash(p[0]) {
|
||||
return syscall.FullPath(d[:2] + p)
|
||||
} else {
|
||||
return syscall.FullPath(d + "\\" + p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var zeroProcAttr syscall.ProcAttr
|
||||
var zeroSysProcAttr syscall.SysProcAttr
|
||||
|
||||
// Has minor changes to support file inheritance.
|
||||
func syscallStartProcess(argv0 string, argv []string, attr *syscall.ProcAttr) (pid int, handle uintptr, err error) {
|
||||
if len(argv0) == 0 {
|
||||
return 0, 0, syscall.EWINDOWS
|
||||
}
|
||||
if attr == nil {
|
||||
attr = &zeroProcAttr
|
||||
}
|
||||
sys := attr.Sys
|
||||
if sys == nil {
|
||||
sys = &zeroSysProcAttr
|
||||
}
|
||||
|
||||
if len(attr.Files) < 3 {
|
||||
return 0, 0, syscall.EINVAL
|
||||
}
|
||||
|
||||
if len(attr.Dir) != 0 {
|
||||
// StartProcess assumes that argv0 is relative to attr.Dir,
|
||||
// because it implies Chdir(attr.Dir) before executing argv0.
|
||||
// Windows CreateProcess assumes the opposite: it looks for
|
||||
// argv0 relative to the current directory, and, only once the new
|
||||
// process is started, it does Chdir(attr.Dir). We are adjusting
|
||||
// for that difference here by making argv0 absolute.
|
||||
var err error
|
||||
argv0, err = joinExeDirAndFName(attr.Dir, argv0)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
argv0p, err := syscall.UTF16PtrFromString(argv0)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
var cmdline string
|
||||
// Windows CreateProcess takes the command line as a single string:
|
||||
// use attr.CmdLine if set, else build the command line by escaping
|
||||
// and joining each argument with spaces
|
||||
if sys.CmdLine != "" {
|
||||
cmdline = sys.CmdLine
|
||||
} else {
|
||||
cmdline = makeCmdLine(argv)
|
||||
}
|
||||
|
||||
var argvp *uint16
|
||||
if len(cmdline) != 0 {
|
||||
argvp, err = syscall.UTF16PtrFromString(cmdline)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
var dirp *uint16
|
||||
if len(attr.Dir) != 0 {
|
||||
dirp, err = syscall.UTF16PtrFromString(attr.Dir)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire the fork lock so that no other threads
|
||||
// create new fds that are not yet close-on-exec
|
||||
// before we fork.
|
||||
syscall.ForkLock.Lock()
|
||||
defer syscall.ForkLock.Unlock()
|
||||
|
||||
p, _ := syscall.GetCurrentProcess()
|
||||
fd := make([]syscall.Handle, len(attr.Files))
|
||||
for i := range attr.Files {
|
||||
if attr.Files[i] <= 0 {
|
||||
continue
|
||||
}
|
||||
if i < 3 {
|
||||
err := syscall.DuplicateHandle(p, syscall.Handle(attr.Files[i]), p, &fd[i], 0, true, syscall.DUPLICATE_SAME_ACCESS)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer syscall.CloseHandle(syscall.Handle(fd[i]))
|
||||
} else {
|
||||
// This is the modification that allows files to be inherited.
|
||||
syscall.SetHandleInformation(syscall.Handle(attr.Files[i]), syscall.HANDLE_FLAG_INHERIT, 1)
|
||||
defer syscall.SetHandleInformation(syscall.Handle(attr.Files[i]), syscall.HANDLE_FLAG_INHERIT, 0)
|
||||
}
|
||||
}
|
||||
si := new(syscall.StartupInfo)
|
||||
si.Cb = uint32(unsafe.Sizeof(*si))
|
||||
si.Flags = syscall.STARTF_USESTDHANDLES
|
||||
if sys.HideWindow {
|
||||
si.Flags |= syscall.STARTF_USESHOWWINDOW
|
||||
si.ShowWindow = syscall.SW_HIDE
|
||||
}
|
||||
si.StdInput = fd[0]
|
||||
si.StdOutput = fd[1]
|
||||
si.StdErr = fd[2]
|
||||
|
||||
pi := new(syscall.ProcessInformation)
|
||||
|
||||
flags := sys.CreationFlags | syscall.CREATE_UNICODE_ENVIRONMENT
|
||||
err = syscall.CreateProcess(argv0p, argvp, nil, nil, true, flags, createEnvBlock(attr.Env), dirp, si, pi)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer syscall.CloseHandle(syscall.Handle(pi.Thread))
|
||||
|
||||
return int(pi.ProcessId), uintptr(pi.Process), nil
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugintest
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func CompileGo(t *testing.T, sourceCode, outputPath string) {
|
||||
dir, err := ioutil.TempDir(".", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "main.go"), []byte(sourceCode), 0600))
|
||||
cmd := exec.Command("go", "build", "-o", outputPath, "main.go")
|
||||
cmd.Dir = dir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
require.NoError(t, cmd.Run())
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugintest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
)
|
||||
|
||||
type SupervisorProviderFunc = func(*model.BundleInfo) (plugin.Supervisor, error)
|
||||
|
||||
func TestSupervisorProvider(t *testing.T, sp SupervisorProviderFunc) {
|
||||
for name, f := range map[string]func(*testing.T, SupervisorProviderFunc){
|
||||
"Supervisor": testSupervisor,
|
||||
"Supervisor_InvalidExecutablePath": testSupervisor_InvalidExecutablePath,
|
||||
"Supervisor_NonExistentExecutablePath": testSupervisor_NonExistentExecutablePath,
|
||||
"Supervisor_StartTimeout": testSupervisor_StartTimeout,
|
||||
// "Supervisor_PluginCrash": testSupervisor_PluginCrash,
|
||||
// "Supervisor_PluginRepeatedlyCrash": testSupervisor_PluginRepeatedlyCrash,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) { f(t, sp) })
|
||||
}
|
||||
}
|
||||
|
||||
func testSupervisor(t *testing.T, sp SupervisorProviderFunc) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
backend := filepath.Join(dir, "backend.exe")
|
||||
CompileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type MyPlugin struct {}
|
||||
|
||||
func main() {
|
||||
rpcplugin.Main(&MyPlugin{})
|
||||
}
|
||||
`, backend)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
supervisor, err := sp(bundle)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, supervisor.Start(nil))
|
||||
require.NoError(t, supervisor.Stop())
|
||||
}
|
||||
|
||||
func testSupervisor_InvalidExecutablePath(t *testing.T, sp SupervisorProviderFunc) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "/foo/../../backend.exe"}}`), 0600)
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
supervisor, err := sp(bundle)
|
||||
assert.Nil(t, supervisor)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func testSupervisor_NonExistentExecutablePath(t *testing.T, sp SupervisorProviderFunc) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "thisfileshouldnotexist"}}`), 0600)
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
supervisor, err := sp(bundle)
|
||||
require.NotNil(t, supervisor)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Error(t, supervisor.Start(nil))
|
||||
}
|
||||
|
||||
// If plugin development goes really wrong, let's make sure plugin activation won't block forever.
|
||||
func testSupervisor_StartTimeout(t *testing.T, sp SupervisorProviderFunc) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
backend := filepath.Join(dir, "backend.exe")
|
||||
CompileGo(t, `
|
||||
package main
|
||||
|
||||
func main() {
|
||||
for {
|
||||
}
|
||||
}
|
||||
`, backend)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
supervisor, err := sp(bundle)
|
||||
require.NoError(t, err)
|
||||
require.Error(t, supervisor.Start(nil))
|
||||
}
|
||||
|
||||
// Crashed plugins should be relaunched.
|
||||
func testSupervisor_PluginCrash(t *testing.T, sp SupervisorProviderFunc) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
backend := filepath.Join(dir, "backend.exe")
|
||||
CompileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type Configuration struct {
|
||||
ShouldExit bool
|
||||
}
|
||||
|
||||
type MyPlugin struct {
|
||||
config Configuration
|
||||
}
|
||||
|
||||
func (p *MyPlugin) OnActivate(api plugin.API) error {
|
||||
api.LoadPluginConfiguration(&p.config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) OnDeactivate() error {
|
||||
if p.config.ShouldExit {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
rpcplugin.Main(&MyPlugin{})
|
||||
}
|
||||
`, backend)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
|
||||
|
||||
var api plugintest.API
|
||||
shouldExit := true
|
||||
api.On("LoadPluginConfiguration", mock.MatchedBy(func(x interface{}) bool { return true })).Return(func(dest interface{}) error {
|
||||
err := json.Unmarshal([]byte(fmt.Sprintf(`{"ShouldExit": %v}`, shouldExit)), dest)
|
||||
shouldExit = false
|
||||
return err
|
||||
})
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
supervisor, err := sp(bundle)
|
||||
require.NoError(t, err)
|
||||
|
||||
var supervisorWaitErr error
|
||||
supervisorWaitDone := make(chan bool, 1)
|
||||
go func() {
|
||||
supervisorWaitErr = supervisor.Wait()
|
||||
close(supervisorWaitDone)
|
||||
}()
|
||||
|
||||
require.NoError(t, supervisor.Start(&api))
|
||||
|
||||
failed := false
|
||||
recovered := false
|
||||
for i := 0; i < 30; i++ {
|
||||
if supervisor.Hooks().OnDeactivate() == nil {
|
||||
require.True(t, failed)
|
||||
recovered = true
|
||||
break
|
||||
} else {
|
||||
failed = true
|
||||
}
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
}
|
||||
assert.True(t, recovered)
|
||||
|
||||
select {
|
||||
case <-supervisorWaitDone:
|
||||
require.Fail(t, "supervisor.Wait() unexpectedly returned")
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
|
||||
require.NoError(t, supervisor.Stop())
|
||||
|
||||
select {
|
||||
case <-supervisorWaitDone:
|
||||
require.Nil(t, supervisorWaitErr)
|
||||
case <-time.After(5000 * time.Millisecond):
|
||||
require.Fail(t, "supervisor.Wait() failed to return")
|
||||
}
|
||||
}
|
||||
|
||||
// Crashed plugins should be relaunched at most three times.
|
||||
func testSupervisor_PluginRepeatedlyCrash(t *testing.T, sp SupervisorProviderFunc) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
backend := filepath.Join(dir, "backend.exe")
|
||||
CompileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type MyPlugin struct {
|
||||
crashing bool
|
||||
}
|
||||
|
||||
func (p *MyPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
p.crashing = true
|
||||
go func() {
|
||||
os.Exit(1)
|
||||
}()
|
||||
}
|
||||
|
||||
if p.crashing {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
rpcplugin.Main(&MyPlugin{})
|
||||
}
|
||||
`, backend)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
|
||||
|
||||
var api plugintest.API
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
supervisor, err := sp(bundle)
|
||||
require.NoError(t, err)
|
||||
|
||||
var supervisorWaitErr error
|
||||
supervisorWaitDone := make(chan bool, 1)
|
||||
go func() {
|
||||
supervisorWaitErr = supervisor.Wait()
|
||||
close(supervisorWaitDone)
|
||||
}()
|
||||
|
||||
require.NoError(t, supervisor.Start(&api))
|
||||
|
||||
for attempt := 1; attempt <= 4; attempt++ {
|
||||
// Verify that the plugin is operational
|
||||
response := httptest.NewRecorder()
|
||||
supervisor.Hooks().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/plugins/id", nil))
|
||||
require.Equal(t, http.StatusOK, response.Result().StatusCode)
|
||||
|
||||
// Crash the plugin
|
||||
supervisor.Hooks().ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/plugins/id", nil))
|
||||
|
||||
// Wait for it to potentially recover
|
||||
recovered := false
|
||||
for i := 0; i < 125; i++ {
|
||||
response := httptest.NewRecorder()
|
||||
supervisor.Hooks().ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/plugins/id", nil))
|
||||
if response.Result().StatusCode == http.StatusOK {
|
||||
recovered = true
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
}
|
||||
|
||||
if attempt < 4 {
|
||||
require.Nil(t, supervisorWaitErr)
|
||||
require.True(t, recovered, "failed to recover after attempt %d", attempt)
|
||||
} else {
|
||||
require.False(t, recovered, "unexpectedly recovered after attempt %d", attempt)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-supervisorWaitDone:
|
||||
require.NotNil(t, supervisorWaitErr)
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
require.Fail(t, "supervisor.Wait() failed to return after plugin crashed")
|
||||
}
|
||||
|
||||
require.NoError(t, supervisor.Stop())
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
)
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
// Setup a global logger to catch tests logging outside of app context
|
||||
// The global logger will be stomped by apps initalizing but that's fine for testing. Ideally this won't happen.
|
||||
mlog.InitGlobalLogger(mlog.NewLogger(&mlog.LoggerConfiguration{
|
||||
EnableConsole: true,
|
||||
ConsoleJson: true,
|
||||
ConsoleLevel: "error",
|
||||
EnableFile: false,
|
||||
}))
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type MountPoint struct {
|
||||
Source string
|
||||
Destination string
|
||||
Type string
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
MountPoints []*MountPoint
|
||||
WorkingDirectory string
|
||||
}
|
||||
|
||||
// NewProcess is like rpcplugin.NewProcess, but launches the process in a sandbox.
|
||||
func NewProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) {
|
||||
return newProcess(ctx, config, path)
|
||||
}
|
||||
|
||||
// CheckSupport inspects the platform and environment to determine whether or not there are any
|
||||
// expected issues with sandboxing. If nil is returned, sandboxing should be used.
|
||||
func CheckSupport() error {
|
||||
return checkSupport()
|
||||
}
|
||||
@@ -1,488 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if len(os.Args) < 4 || os.Args[0] != "sandbox.runProcess" {
|
||||
return
|
||||
}
|
||||
|
||||
var config Configuration
|
||||
if err := json.Unmarshal([]byte(os.Args[1]), &config); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := runProcess(&config, os.Args[2], os.Args[3]); err != nil {
|
||||
if eerr, ok := err.(*exec.ExitError); ok {
|
||||
if status, ok := eerr.Sys().(syscall.WaitStatus); ok {
|
||||
os.Exit(status.ExitStatus())
|
||||
}
|
||||
}
|
||||
fmt.Println(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func systemMountPoints() (points []*MountPoint) {
|
||||
points = append(points, &MountPoint{
|
||||
Source: "proc",
|
||||
Destination: "/proc",
|
||||
Type: "proc",
|
||||
}, &MountPoint{
|
||||
Source: "/dev/null",
|
||||
Destination: "/dev/null",
|
||||
}, &MountPoint{
|
||||
Source: "/dev/zero",
|
||||
Destination: "/dev/zero",
|
||||
}, &MountPoint{
|
||||
Source: "/dev/full",
|
||||
Destination: "/dev/full",
|
||||
})
|
||||
|
||||
readOnly := []string{
|
||||
"/dev/random",
|
||||
"/dev/urandom",
|
||||
"/etc/resolv.conf",
|
||||
"/lib",
|
||||
"/lib32",
|
||||
"/lib64",
|
||||
"/usr/lib",
|
||||
"/usr/lib32",
|
||||
"/usr/lib64",
|
||||
"/etc/ca-certificates",
|
||||
"/etc/ssl/certs",
|
||||
"/system/etc/security/cacerts",
|
||||
"/usr/local/share/certs",
|
||||
"/etc/pki/tls/certs",
|
||||
"/etc/openssl/certs",
|
||||
"/etc/ssl/ca-bundle.pem",
|
||||
"/etc/pki/tls/cacert.pem",
|
||||
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
||||
}
|
||||
|
||||
for _, v := range []string{"SSL_CERT_FILE", "SSL_CERT_DIR"} {
|
||||
if path := os.Getenv(v); path != "" {
|
||||
readOnly = append(readOnly, path)
|
||||
}
|
||||
}
|
||||
|
||||
for _, point := range readOnly {
|
||||
points = append(points, &MountPoint{
|
||||
Source: point,
|
||||
Destination: point,
|
||||
ReadOnly: true,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func runProcess(config *Configuration, path, root string) error {
|
||||
if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil {
|
||||
return errors.Wrapf(err, "unable to make root private")
|
||||
}
|
||||
|
||||
if err := mountMountPoints(root, systemMountPoints()); err != nil {
|
||||
return errors.Wrapf(err, "unable to mount sandbox system mount points")
|
||||
}
|
||||
|
||||
if err := mountMountPoints(root, config.MountPoints); err != nil {
|
||||
return errors.Wrapf(err, "unable to mount sandbox config mount points")
|
||||
}
|
||||
|
||||
if err := pivotRoot(root); err != nil {
|
||||
return errors.Wrapf(err, "unable to pivot sandbox root")
|
||||
}
|
||||
|
||||
if err := os.Mkdir("/tmp", 0755); err != nil {
|
||||
return errors.Wrapf(err, "unable to create /tmp")
|
||||
}
|
||||
|
||||
if config.WorkingDirectory != "" {
|
||||
if err := os.Chdir(config.WorkingDirectory); err != nil {
|
||||
return errors.Wrapf(err, "unable to set working directory")
|
||||
}
|
||||
}
|
||||
|
||||
if err := dropInheritableCapabilities(); err != nil {
|
||||
return errors.Wrapf(err, "unable to drop inheritable capabilities")
|
||||
}
|
||||
|
||||
if err := enableSeccompFilter(); err != nil {
|
||||
return errors.Wrapf(err, "unable to enable seccomp filter")
|
||||
}
|
||||
|
||||
return runExecutable(path)
|
||||
}
|
||||
|
||||
func mountMountPoint(root string, mountPoint *MountPoint) error {
|
||||
isDir := true
|
||||
if mountPoint.Type == "" {
|
||||
stat, err := os.Lstat(mountPoint.Source)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if (stat.Mode() & os.ModeSymlink) != 0 {
|
||||
if path, err := filepath.EvalSymlinks(mountPoint.Source); err == nil {
|
||||
newMountPoint := *mountPoint
|
||||
newMountPoint.Source = path
|
||||
if err := mountMountPoint(root, &newMountPoint); err != nil {
|
||||
return errors.Wrapf(err, "unable to mount symbolic link target: "+mountPoint.Source)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
isDir = stat.IsDir()
|
||||
}
|
||||
|
||||
target := filepath.Join(root, mountPoint.Destination)
|
||||
|
||||
if isDir {
|
||||
if err := os.MkdirAll(target, 0755); err != nil {
|
||||
return errors.Wrapf(err, "unable to create directory: "+target)
|
||||
}
|
||||
} else {
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||
return errors.Wrapf(err, "unable to create directory: "+target)
|
||||
}
|
||||
f, err := os.Create(target)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to create file: "+target)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
flags := uintptr(syscall.MS_NOSUID | syscall.MS_NODEV)
|
||||
if mountPoint.Type == "" {
|
||||
flags |= syscall.MS_BIND
|
||||
}
|
||||
if mountPoint.ReadOnly {
|
||||
flags |= syscall.MS_RDONLY
|
||||
}
|
||||
|
||||
if err := syscall.Mount(mountPoint.Source, target, mountPoint.Type, flags, ""); err != nil {
|
||||
return errors.Wrapf(err, "unable to mount "+mountPoint.Source)
|
||||
}
|
||||
|
||||
if (flags & syscall.MS_BIND) != 0 {
|
||||
// If this was a bind mount, our other flags actually got silently ignored during the above syscall:
|
||||
//
|
||||
// If mountflags includes MS_BIND [...] The remaining bits in the mountflags argument are
|
||||
// also ignored, with the exception of MS_REC.
|
||||
//
|
||||
// Furthermore, remounting will fail if we attempt to unset a bit that was inherited from
|
||||
// the mount's parent:
|
||||
//
|
||||
// The mount(2) flags MS_RDONLY, MS_NOSUID, MS_NOEXEC, and the "atime" flags
|
||||
// (MS_NOATIME, MS_NODIRATIME, MS_RELATIME) settings become locked when propagated from
|
||||
// a more privileged to a less privileged mount namespace, and may not be changed in the
|
||||
// less privileged mount namespace.
|
||||
//
|
||||
// So we need to get the actual flags, add our new ones, then do a remount if needed.
|
||||
var stats syscall.Statfs_t
|
||||
if err := syscall.Statfs(target, &stats); err != nil {
|
||||
return errors.Wrap(err, "unable to get mount flags for target: "+target)
|
||||
}
|
||||
const lockedFlagsMask = unix.MS_RDONLY | unix.MS_NOSUID | unix.MS_NOEXEC | unix.MS_NOATIME | unix.MS_NODIRATIME | unix.MS_RELATIME
|
||||
lockedFlags := uintptr(stats.Flags & lockedFlagsMask)
|
||||
if lockedFlags != ((flags | lockedFlags) & lockedFlagsMask) {
|
||||
if err := syscall.Mount("", target, "", flags|lockedFlags|syscall.MS_REMOUNT, ""); err != nil {
|
||||
return errors.Wrapf(err, "unable to remount "+mountPoint.Source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mountMountPoints(root string, mountPoints []*MountPoint) error {
|
||||
for _, mountPoint := range mountPoints {
|
||||
if err := mountMountPoint(root, mountPoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pivotRoot(newRoot string) error {
|
||||
if err := syscall.Mount(newRoot, newRoot, "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
|
||||
return errors.Wrapf(err, "unable to mount new root")
|
||||
}
|
||||
|
||||
prevRoot := filepath.Join(newRoot, ".prev_root")
|
||||
|
||||
if err := os.MkdirAll(prevRoot, 0700); err != nil {
|
||||
return errors.Wrapf(err, "unable to create directory for previous root")
|
||||
}
|
||||
|
||||
if err := syscall.PivotRoot(newRoot, prevRoot); err != nil {
|
||||
return errors.Wrapf(err, "syscall error")
|
||||
}
|
||||
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
return errors.Wrapf(err, "unable to change directory")
|
||||
}
|
||||
|
||||
prevRoot = "/.prev_root"
|
||||
|
||||
if err := syscall.Unmount(prevRoot, syscall.MNT_DETACH); err != nil {
|
||||
return errors.Wrapf(err, "unable to unmount previous root")
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(prevRoot); err != nil {
|
||||
return errors.Wrapf(err, "unable to remove previous root directory")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func dropInheritableCapabilities() error {
|
||||
type capHeader struct {
|
||||
version uint32
|
||||
pid int32
|
||||
}
|
||||
|
||||
type capData struct {
|
||||
effective uint32
|
||||
permitted uint32
|
||||
inheritable uint32
|
||||
}
|
||||
|
||||
var hdr capHeader
|
||||
var data [2]capData
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&hdr)), 0, 0); errno != 0 {
|
||||
return errors.Wrapf(syscall.Errno(errno), "unable to get capabilities version")
|
||||
}
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&hdr)), uintptr(unsafe.Pointer(&data[0])), 0); errno != 0 {
|
||||
return errors.Wrapf(syscall.Errno(errno), "unable to get capabilities")
|
||||
}
|
||||
|
||||
data[0].inheritable = 0
|
||||
data[1].inheritable = 0
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&hdr)), uintptr(unsafe.Pointer(&data[0])), 0); errno != 0 {
|
||||
return errors.Wrapf(syscall.Errno(errno), "unable to set inheritable capabilities")
|
||||
}
|
||||
|
||||
for i := 0; i < 64; i++ {
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_CAPBSET_DROP, uintptr(i), 0); errno != 0 && errno != syscall.EINVAL {
|
||||
return errors.Wrapf(syscall.Errno(errno), "unable to drop bounding set capability")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func enableSeccompFilter() error {
|
||||
return EnableSeccompFilter(SeccompFilter(NATIVE_AUDIT_ARCH, AllowedSyscalls))
|
||||
}
|
||||
|
||||
func runExecutable(path string) error {
|
||||
childFiles := []*os.File{
|
||||
os.NewFile(3, ""), os.NewFile(4, ""),
|
||||
}
|
||||
defer childFiles[0].Close()
|
||||
defer childFiles[1].Close()
|
||||
|
||||
cmd := exec.Command(path)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.ExtraFiles = childFiles
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Pdeathsig: syscall.SIGTERM,
|
||||
}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type process struct {
|
||||
command *exec.Cmd
|
||||
root string
|
||||
}
|
||||
|
||||
func newProcess(ctx context.Context, config *Configuration, path string) (pOut rpcplugin.Process, rwcOut io.ReadWriteCloser, errOut error) {
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ipc, childFiles, err := rpcplugin.NewIPC()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer childFiles[0].Close()
|
||||
defer childFiles[1].Close()
|
||||
|
||||
root, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer func() {
|
||||
if errOut != nil {
|
||||
os.RemoveAll(root)
|
||||
}
|
||||
}()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "/proc/self/exe")
|
||||
cmd.Args = []string{"sandbox.runProcess", string(configJSON), path, root}
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.ExtraFiles = childFiles
|
||||
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER,
|
||||
Pdeathsig: syscall.SIGTERM,
|
||||
GidMappings: []syscall.SysProcIDMap{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: os.Getgid(),
|
||||
Size: 1,
|
||||
},
|
||||
},
|
||||
UidMappings: []syscall.SysProcIDMap{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: os.Getuid(),
|
||||
Size: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
ipc.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &process{
|
||||
command: cmd,
|
||||
root: root,
|
||||
}, ipc, nil
|
||||
}
|
||||
|
||||
func (p *process) Wait() error {
|
||||
defer os.RemoveAll(p.root)
|
||||
return p.command.Wait()
|
||||
}
|
||||
|
||||
func init() {
|
||||
if len(os.Args) < 2 || os.Args[0] != "sandbox.checkSupportInNamespace" {
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkSupportInNamespace(os.Args[1]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func checkSupportInNamespace(root string) error {
|
||||
if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil {
|
||||
return errors.Wrapf(err, "unable to make root private")
|
||||
}
|
||||
|
||||
if err := mountMountPoints(root, systemMountPoints()); err != nil {
|
||||
return errors.Wrapf(err, "unable to mount sandbox system mount points")
|
||||
}
|
||||
|
||||
if err := pivotRoot(root); err != nil {
|
||||
return errors.Wrapf(err, "unable to pivot sandbox root")
|
||||
}
|
||||
|
||||
if err := dropInheritableCapabilities(); err != nil {
|
||||
return errors.Wrapf(err, "unable to drop inheritable capabilities")
|
||||
}
|
||||
|
||||
if err := enableSeccompFilter(); err != nil {
|
||||
return errors.Wrapf(err, "unable to enable seccomp filter")
|
||||
}
|
||||
|
||||
if f, err := os.Create(os.DevNull); err != nil {
|
||||
return errors.Wrapf(err, "unable to open os.DevNull")
|
||||
} else {
|
||||
defer f.Close()
|
||||
if _, err = f.Write([]byte("foo")); err != nil {
|
||||
return errors.Wrapf(err, "unable to write to os.DevNull")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSupport() error {
|
||||
if AllowedSyscalls == nil {
|
||||
return fmt.Errorf("unsupported architecture")
|
||||
}
|
||||
|
||||
stderr := &bytes.Buffer{}
|
||||
|
||||
root, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
cmd := exec.Command("/proc/self/exe")
|
||||
cmd.Args = []string{"sandbox.checkSupportInNamespace", root}
|
||||
cmd.Stderr = stderr
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER,
|
||||
Pdeathsig: syscall.SIGTERM,
|
||||
GidMappings: []syscall.SysProcIDMap{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: os.Getgid(),
|
||||
Size: 1,
|
||||
},
|
||||
},
|
||||
UidMappings: []syscall.SysProcIDMap{
|
||||
{
|
||||
ContainerID: 0,
|
||||
HostID: os.Getuid(),
|
||||
Size: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return errors.Wrapf(err, "unable to create user namespace")
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if _, ok := err.(*exec.ExitError); ok {
|
||||
return errors.Wrapf(fmt.Errorf("%v", stderr.String()), "unable to prepare namespace")
|
||||
}
|
||||
return errors.Wrapf(err, "unable to prepare namespace")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
|
||||
)
|
||||
|
||||
func TestNewProcess(t *testing.T) {
|
||||
if err := CheckSupport(); err != nil {
|
||||
t.Skip("sandboxing not supported:", err)
|
||||
}
|
||||
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ping := filepath.Join(dir, "ping.exe")
|
||||
rpcplugintest.CompileGo(t, `
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
var failures int
|
||||
|
||||
type T struct {}
|
||||
func (T) Errorf(format string, args ...interface{}) {
|
||||
fmt.Printf(format, args...)
|
||||
failures++
|
||||
}
|
||||
func (T) FailNow() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func init() {
|
||||
if len(os.Args) > 0 && os.Args[0] == "exitImmediately" {
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
t := &T{}
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/dir", pwd)
|
||||
|
||||
assert.Equal(t, 0, os.Getgid(), "we should see ourselves as root")
|
||||
assert.Equal(t, 0, os.Getuid(), "we should see ourselves as root")
|
||||
|
||||
f, err := ioutil.TempFile("", "")
|
||||
require.NoError(t, err, "we should be able to create temporary files")
|
||||
f.Close()
|
||||
|
||||
_, err = os.Stat("ping.exe")
|
||||
assert.NoError(t, err, "we should be able to read files in the working directory")
|
||||
|
||||
buf := make([]byte, 20)
|
||||
n, err := rand.Read(buf)
|
||||
assert.Equal(t, 20, n)
|
||||
assert.NoError(t, err, "we should be able to read from /dev/urandom")
|
||||
|
||||
f, err = os.Create("/dev/zero")
|
||||
require.NoError(t, err, "we should be able to write to /dev/zero")
|
||||
defer f.Close()
|
||||
n, err = f.Write([]byte("foo"))
|
||||
assert.Equal(t, 3, n)
|
||||
require.NoError(t, err, "we should be able to write to /dev/zero")
|
||||
|
||||
f, err = os.Create("/dir/foo")
|
||||
if f != nil {
|
||||
defer f.Close()
|
||||
}
|
||||
assert.Error(t, err, "we shouldn't be able to write to this read-only mount point")
|
||||
|
||||
_, err = ioutil.ReadFile("/etc/resolv.conf")
|
||||
require.NoError(t, err, "we should be able to read /etc/resolv.conf")
|
||||
|
||||
resp, err := http.Get("https://github.com")
|
||||
require.NoError(t, err, "we should be able to use the network")
|
||||
resp.Body.Close()
|
||||
|
||||
status, err := ioutil.ReadFile("/proc/self/status")
|
||||
require.NoError(t, err, "we should be able to read from /proc")
|
||||
assert.Regexp(t, status, "CapEff:\\s+0000000000000000", "we should have no effective capabilities")
|
||||
|
||||
require.NoError(t, os.MkdirAll("/tmp/dir2", 0755))
|
||||
err = syscall.Mount("/dir", "/tmp/dir2", "", syscall.MS_BIND, "")
|
||||
assert.Equal(t, syscall.EPERM, err, "we shouldn't be allowed to mount things")
|
||||
|
||||
cmd := exec.Command("/proc/self/exe")
|
||||
cmd.Args = []string{"exitImmediately"}
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Pdeathsig: syscall.SIGTERM,
|
||||
}
|
||||
assert.NoError(t, cmd.Run(), "we should be able to re-exec ourself")
|
||||
|
||||
cmd = exec.Command("/proc/self/exe")
|
||||
cmd.Args = []string{"exitImmediately"}
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWUSER,
|
||||
Pdeathsig: syscall.SIGTERM,
|
||||
}
|
||||
assert.Error(t, cmd.Run(), "we shouldn't be able to create new namespaces anymore")
|
||||
|
||||
ipc, err := rpcplugin.InheritedProcessIPC()
|
||||
require.NoError(t, err)
|
||||
defer ipc.Close()
|
||||
_, err = ipc.Write([]byte("ping"))
|
||||
require.NoError(t, err)
|
||||
|
||||
if failures > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
`, ping)
|
||||
|
||||
p, ipc, err := NewProcess(context.Background(), &Configuration{
|
||||
MountPoints: []*MountPoint{
|
||||
{
|
||||
Source: dir,
|
||||
Destination: "/dir",
|
||||
ReadOnly: true,
|
||||
},
|
||||
},
|
||||
WorkingDirectory: "/dir",
|
||||
}, "/dir/ping.exe")
|
||||
require.NoError(t, err)
|
||||
defer ipc.Close()
|
||||
b := make([]byte, 10)
|
||||
n, err := ipc.Read(b)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 4, n)
|
||||
assert.Equal(t, "ping", string(b[:4]))
|
||||
require.NoError(t, p.Wait())
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// +build !linux
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
func newProcess(ctx context.Context, config *Configuration, path string) (rpcplugin.Process, io.ReadWriteCloser, error) {
|
||||
return nil, nil, checkSupport()
|
||||
}
|
||||
|
||||
func checkSupport() error {
|
||||
return fmt.Errorf("sandboxing is not supported on this platform")
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCheckSupport is here for debugging purposes and has no assertions. You can quickly test
|
||||
// sandboxing support with various systems by compiling the test executable and running this test on
|
||||
// your target systems. For example, with docker, executed from the root of the repo:
|
||||
//
|
||||
// docker run --rm -it -w /go/src/github.com/mattermost/mattermost-server
|
||||
// -v $(pwd):/go/src/github.com/mattermost/mattermost-server golang:1.9
|
||||
// go test -c ./plugin/rpcplugin
|
||||
//
|
||||
// docker run --rm -it --privileged -w /opt/mattermost
|
||||
// -v $(pwd):/opt/mattermost centos:6
|
||||
// ./rpcplugin.test --test.v --test.run TestCheckSupport
|
||||
func TestCheckSupport(t *testing.T) {
|
||||
if err := CheckSupport(); err != nil {
|
||||
t.Log(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/net/bpf"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
SECCOMP_RET_ALLOW = 0x7fff0000
|
||||
SECCOMP_RET_ERRNO = 0x00050000
|
||||
)
|
||||
|
||||
const (
|
||||
EM_X86_64 = 62
|
||||
|
||||
__AUDIT_ARCH_64BIT = 0x80000000
|
||||
__AUDIT_ARCH_LE = 0x40000000
|
||||
|
||||
AUDIT_ARCH_X86_64 = EM_X86_64 | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE
|
||||
|
||||
nrSize = 4
|
||||
archOffset = nrSize
|
||||
ipOffset = archOffset + 4
|
||||
argsOffset = ipOffset + 8
|
||||
)
|
||||
|
||||
type SeccompCondition interface {
|
||||
Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction
|
||||
}
|
||||
|
||||
func seccompArgLowWord(arg int, littleEndian bool) uint32 {
|
||||
offset := uint32(argsOffset + arg*8)
|
||||
if !littleEndian {
|
||||
offset += 4
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
func seccompArgHighWord(arg int, littleEndian bool) uint32 {
|
||||
offset := uint32(argsOffset + arg*8)
|
||||
if littleEndian {
|
||||
offset += 4
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
type SeccompArgHasNoBits struct {
|
||||
Arg int
|
||||
Mask uint64
|
||||
}
|
||||
|
||||
func (c SeccompArgHasNoBits) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction {
|
||||
return []bpf.Instruction{
|
||||
bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4},
|
||||
bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask >> 32), SkipTrue: skipFalseSentinel},
|
||||
bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4},
|
||||
bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask), SkipTrue: skipFalseSentinel},
|
||||
}
|
||||
}
|
||||
|
||||
type SeccompArgHasAnyBit struct {
|
||||
Arg int
|
||||
Mask uint64
|
||||
}
|
||||
|
||||
func (c SeccompArgHasAnyBit) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction {
|
||||
return []bpf.Instruction{
|
||||
bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4},
|
||||
bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask >> 32), SkipTrue: 2},
|
||||
bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4},
|
||||
bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: uint32(c.Mask), SkipFalse: skipFalseSentinel},
|
||||
}
|
||||
}
|
||||
|
||||
type SeccompArgEquals struct {
|
||||
Arg int
|
||||
Value uint64
|
||||
}
|
||||
|
||||
func (c SeccompArgEquals) Filter(littleEndian bool, skipFalseSentinel uint8) []bpf.Instruction {
|
||||
return []bpf.Instruction{
|
||||
bpf.LoadAbsolute{Off: seccompArgHighWord(c.Arg, littleEndian), Size: 4},
|
||||
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(c.Value >> 32), SkipFalse: skipFalseSentinel},
|
||||
bpf.LoadAbsolute{Off: seccompArgLowWord(c.Arg, littleEndian), Size: 4},
|
||||
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(c.Value), SkipFalse: skipFalseSentinel},
|
||||
}
|
||||
}
|
||||
|
||||
type SeccompConditions struct {
|
||||
All []SeccompCondition
|
||||
}
|
||||
|
||||
type SeccompSyscall struct {
|
||||
Syscall uint32
|
||||
Any []SeccompConditions
|
||||
}
|
||||
|
||||
func SeccompFilter(arch uint32, allowedSyscalls []SeccompSyscall) (filter []bpf.Instruction) {
|
||||
filter = append(filter,
|
||||
bpf.LoadAbsolute{Off: archOffset, Size: 4},
|
||||
bpf.JumpIf{Cond: bpf.JumpEqual, Val: arch, SkipTrue: 1},
|
||||
bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)},
|
||||
)
|
||||
|
||||
filter = append(filter, bpf.LoadAbsolute{Off: 0, Size: nrSize})
|
||||
for _, s := range allowedSyscalls {
|
||||
if s.Any != nil {
|
||||
syscallStart := len(filter)
|
||||
filter = append(filter, bpf.Instruction(nil))
|
||||
for _, cs := range s.Any {
|
||||
anyStart := len(filter)
|
||||
for _, c := range cs.All {
|
||||
filter = append(filter, c.Filter((arch&__AUDIT_ARCH_LE) != 0, 255)...)
|
||||
}
|
||||
filter = append(filter, bpf.RetConstant{Val: SECCOMP_RET_ALLOW})
|
||||
for i := anyStart; i < len(filter); i++ {
|
||||
if jump, ok := filter[i].(bpf.JumpIf); ok {
|
||||
if len(filter)-i-1 > 255 {
|
||||
panic("condition too long")
|
||||
}
|
||||
if jump.SkipFalse == 255 {
|
||||
jump.SkipFalse = uint8(len(filter) - i - 1)
|
||||
}
|
||||
if jump.SkipTrue == 255 {
|
||||
jump.SkipTrue = uint8(len(filter) - i - 1)
|
||||
}
|
||||
filter[i] = jump
|
||||
}
|
||||
}
|
||||
}
|
||||
filter = append(filter, bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)})
|
||||
if len(filter)-syscallStart-1 > 255 {
|
||||
panic("conditions too long")
|
||||
}
|
||||
filter[syscallStart] = bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(s.Syscall), SkipFalse: uint8(len(filter) - syscallStart - 1)}
|
||||
} else {
|
||||
filter = append(filter,
|
||||
bpf.JumpIf{Cond: bpf.JumpEqual, Val: uint32(s.Syscall), SkipFalse: 1},
|
||||
bpf.RetConstant{Val: SECCOMP_RET_ALLOW},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return append(filter, bpf.RetConstant{Val: uint32(SECCOMP_RET_ERRNO | unix.EPERM)})
|
||||
}
|
||||
|
||||
func EnableSeccompFilter(filter []bpf.Instruction) error {
|
||||
assembled, err := bpf.Assemble(filter)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to assemble filter")
|
||||
}
|
||||
|
||||
sockFilter := make([]unix.SockFilter, len(filter))
|
||||
for i, instruction := range assembled {
|
||||
sockFilter[i].Code = instruction.Op
|
||||
sockFilter[i].Jt = instruction.Jt
|
||||
sockFilter[i].Jf = instruction.Jf
|
||||
sockFilter[i].K = instruction.K
|
||||
}
|
||||
|
||||
prog := unix.SockFprog{
|
||||
Len: uint16(len(sockFilter)),
|
||||
Filter: &sockFilter[0],
|
||||
}
|
||||
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, uintptr(unsafe.Pointer(&prog))); errno != 0 {
|
||||
return errors.Wrapf(syscall.Errno(errno), "syscall error")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const NATIVE_AUDIT_ARCH = AUDIT_ARCH_X86_64
|
||||
|
||||
var AllowedSyscalls = []SeccompSyscall{
|
||||
{Syscall: unix.SYS_ACCEPT},
|
||||
{Syscall: unix.SYS_ACCEPT4},
|
||||
{Syscall: unix.SYS_ACCESS},
|
||||
{Syscall: unix.SYS_ADJTIMEX},
|
||||
{Syscall: unix.SYS_ALARM},
|
||||
{Syscall: unix.SYS_ARCH_PRCTL},
|
||||
{Syscall: unix.SYS_BIND},
|
||||
{Syscall: unix.SYS_BRK},
|
||||
{Syscall: unix.SYS_CAPGET},
|
||||
{Syscall: unix.SYS_CAPSET},
|
||||
{Syscall: unix.SYS_CHDIR},
|
||||
{Syscall: unix.SYS_CHMOD},
|
||||
{Syscall: unix.SYS_CHOWN},
|
||||
{Syscall: unix.SYS_CLOCK_GETRES},
|
||||
{Syscall: unix.SYS_CLOCK_GETTIME},
|
||||
{Syscall: unix.SYS_CLOCK_NANOSLEEP},
|
||||
{
|
||||
Syscall: unix.SYS_CLONE,
|
||||
Any: []SeccompConditions{{
|
||||
All: []SeccompCondition{SeccompArgHasNoBits{
|
||||
Arg: 0,
|
||||
Mask: unix.CLONE_NEWCGROUP | unix.CLONE_NEWIPC | unix.CLONE_NEWNET | unix.CLONE_NEWNS | unix.CLONE_NEWPID | unix.CLONE_NEWUSER | unix.CLONE_NEWUTS,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
{Syscall: unix.SYS_CLOSE},
|
||||
{Syscall: unix.SYS_CONNECT},
|
||||
{Syscall: unix.SYS_COPY_FILE_RANGE},
|
||||
{Syscall: unix.SYS_CREAT},
|
||||
{Syscall: unix.SYS_DUP},
|
||||
{Syscall: unix.SYS_DUP2},
|
||||
{Syscall: unix.SYS_DUP3},
|
||||
{Syscall: unix.SYS_EPOLL_CREATE},
|
||||
{Syscall: unix.SYS_EPOLL_CREATE1},
|
||||
{Syscall: unix.SYS_EPOLL_CTL},
|
||||
{Syscall: unix.SYS_EPOLL_CTL_OLD},
|
||||
{Syscall: unix.SYS_EPOLL_PWAIT},
|
||||
{Syscall: unix.SYS_EPOLL_WAIT},
|
||||
{Syscall: unix.SYS_EPOLL_WAIT_OLD},
|
||||
{Syscall: unix.SYS_EVENTFD},
|
||||
{Syscall: unix.SYS_EVENTFD2},
|
||||
{Syscall: unix.SYS_EXECVE},
|
||||
{Syscall: unix.SYS_EXECVEAT},
|
||||
{Syscall: unix.SYS_EXIT},
|
||||
{Syscall: unix.SYS_EXIT_GROUP},
|
||||
{Syscall: unix.SYS_FACCESSAT},
|
||||
{Syscall: unix.SYS_FADVISE64},
|
||||
{Syscall: unix.SYS_FALLOCATE},
|
||||
{Syscall: unix.SYS_FANOTIFY_MARK},
|
||||
{Syscall: unix.SYS_FCHDIR},
|
||||
{Syscall: unix.SYS_FCHMOD},
|
||||
{Syscall: unix.SYS_FCHMODAT},
|
||||
{Syscall: unix.SYS_FCHOWN},
|
||||
{Syscall: unix.SYS_FCHOWNAT},
|
||||
{Syscall: unix.SYS_FCNTL},
|
||||
{Syscall: unix.SYS_FDATASYNC},
|
||||
{Syscall: unix.SYS_FGETXATTR},
|
||||
{Syscall: unix.SYS_FLISTXATTR},
|
||||
{Syscall: unix.SYS_FLOCK},
|
||||
{Syscall: unix.SYS_FORK},
|
||||
{Syscall: unix.SYS_FREMOVEXATTR},
|
||||
{Syscall: unix.SYS_FSETXATTR},
|
||||
{Syscall: unix.SYS_FSTAT},
|
||||
{Syscall: unix.SYS_FSTATFS},
|
||||
{Syscall: unix.SYS_FSYNC},
|
||||
{Syscall: unix.SYS_FTRUNCATE},
|
||||
{Syscall: unix.SYS_FUTEX},
|
||||
{Syscall: unix.SYS_FUTIMESAT},
|
||||
{Syscall: unix.SYS_GETCPU},
|
||||
{Syscall: unix.SYS_GETCWD},
|
||||
{Syscall: unix.SYS_GETDENTS},
|
||||
{Syscall: unix.SYS_GETDENTS64},
|
||||
{Syscall: unix.SYS_GETEGID},
|
||||
{Syscall: unix.SYS_GETEUID},
|
||||
{Syscall: unix.SYS_GETGID},
|
||||
{Syscall: unix.SYS_GETGROUPS},
|
||||
{Syscall: unix.SYS_GETITIMER},
|
||||
{Syscall: unix.SYS_GETPEERNAME},
|
||||
{Syscall: unix.SYS_GETPGID},
|
||||
{Syscall: unix.SYS_GETPGRP},
|
||||
{Syscall: unix.SYS_GETPID},
|
||||
{Syscall: unix.SYS_GETPPID},
|
||||
{Syscall: unix.SYS_GETPRIORITY},
|
||||
{Syscall: unix.SYS_GETRANDOM},
|
||||
{Syscall: unix.SYS_GETRESGID},
|
||||
{Syscall: unix.SYS_GETRESUID},
|
||||
{Syscall: unix.SYS_GETRLIMIT},
|
||||
{Syscall: unix.SYS_GET_ROBUST_LIST},
|
||||
{Syscall: unix.SYS_GETRUSAGE},
|
||||
{Syscall: unix.SYS_GETSID},
|
||||
{Syscall: unix.SYS_GETSOCKNAME},
|
||||
{Syscall: unix.SYS_GETSOCKOPT},
|
||||
{Syscall: unix.SYS_GET_THREAD_AREA},
|
||||
{Syscall: unix.SYS_GETTID},
|
||||
{Syscall: unix.SYS_GETTIMEOFDAY},
|
||||
{Syscall: unix.SYS_GETUID},
|
||||
{Syscall: unix.SYS_GETXATTR},
|
||||
{Syscall: unix.SYS_INOTIFY_ADD_WATCH},
|
||||
{Syscall: unix.SYS_INOTIFY_INIT},
|
||||
{Syscall: unix.SYS_INOTIFY_INIT1},
|
||||
{Syscall: unix.SYS_INOTIFY_RM_WATCH},
|
||||
{Syscall: unix.SYS_IO_CANCEL},
|
||||
{Syscall: unix.SYS_IOCTL},
|
||||
{Syscall: unix.SYS_IO_DESTROY},
|
||||
{Syscall: unix.SYS_IO_GETEVENTS},
|
||||
{Syscall: unix.SYS_IOPRIO_GET},
|
||||
{Syscall: unix.SYS_IOPRIO_SET},
|
||||
{Syscall: unix.SYS_IO_SETUP},
|
||||
{Syscall: unix.SYS_IO_SUBMIT},
|
||||
{Syscall: unix.SYS_KILL},
|
||||
{Syscall: unix.SYS_LCHOWN},
|
||||
{Syscall: unix.SYS_LGETXATTR},
|
||||
{Syscall: unix.SYS_LINK},
|
||||
{Syscall: unix.SYS_LINKAT},
|
||||
{Syscall: unix.SYS_LISTEN},
|
||||
{Syscall: unix.SYS_LISTXATTR},
|
||||
{Syscall: unix.SYS_LLISTXATTR},
|
||||
{Syscall: unix.SYS_LREMOVEXATTR},
|
||||
{Syscall: unix.SYS_LSEEK},
|
||||
{Syscall: unix.SYS_LSETXATTR},
|
||||
{Syscall: unix.SYS_LSTAT},
|
||||
{Syscall: unix.SYS_MADVISE},
|
||||
{Syscall: unix.SYS_MEMFD_CREATE},
|
||||
{Syscall: unix.SYS_MINCORE},
|
||||
{Syscall: unix.SYS_MKDIR},
|
||||
{Syscall: unix.SYS_MKDIRAT},
|
||||
{Syscall: unix.SYS_MKNOD},
|
||||
{Syscall: unix.SYS_MKNODAT},
|
||||
{Syscall: unix.SYS_MLOCK},
|
||||
{Syscall: unix.SYS_MLOCK2},
|
||||
{Syscall: unix.SYS_MLOCKALL},
|
||||
{Syscall: unix.SYS_MMAP},
|
||||
{Syscall: unix.SYS_MODIFY_LDT},
|
||||
{Syscall: unix.SYS_MPROTECT},
|
||||
{Syscall: unix.SYS_MQ_GETSETATTR},
|
||||
{Syscall: unix.SYS_MQ_NOTIFY},
|
||||
{Syscall: unix.SYS_MQ_OPEN},
|
||||
{Syscall: unix.SYS_MQ_TIMEDRECEIVE},
|
||||
{Syscall: unix.SYS_MQ_TIMEDSEND},
|
||||
{Syscall: unix.SYS_MQ_UNLINK},
|
||||
{Syscall: unix.SYS_MREMAP},
|
||||
{Syscall: unix.SYS_MSGCTL},
|
||||
{Syscall: unix.SYS_MSGGET},
|
||||
{Syscall: unix.SYS_MSGRCV},
|
||||
{Syscall: unix.SYS_MSGSND},
|
||||
{Syscall: unix.SYS_MSYNC},
|
||||
{Syscall: unix.SYS_MUNLOCK},
|
||||
{Syscall: unix.SYS_MUNLOCKALL},
|
||||
{Syscall: unix.SYS_MUNMAP},
|
||||
{Syscall: unix.SYS_NANOSLEEP},
|
||||
{Syscall: unix.SYS_NEWFSTATAT},
|
||||
{Syscall: unix.SYS_OPEN},
|
||||
{Syscall: unix.SYS_OPENAT},
|
||||
{Syscall: unix.SYS_PAUSE},
|
||||
{
|
||||
Syscall: unix.SYS_PERSONALITY,
|
||||
Any: []SeccompConditions{
|
||||
{All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0}}},
|
||||
{All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 8}}},
|
||||
{All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0x20000}}},
|
||||
{All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0x20008}}},
|
||||
{All: []SeccompCondition{SeccompArgEquals{Arg: 0, Value: 0xffffffff}}},
|
||||
},
|
||||
},
|
||||
{Syscall: unix.SYS_PIPE},
|
||||
{Syscall: unix.SYS_PIPE2},
|
||||
{Syscall: unix.SYS_POLL},
|
||||
{Syscall: unix.SYS_PPOLL},
|
||||
{Syscall: unix.SYS_PRCTL},
|
||||
{Syscall: unix.SYS_PREAD64},
|
||||
{Syscall: unix.SYS_PREADV},
|
||||
{Syscall: unix.SYS_PREADV2},
|
||||
{Syscall: unix.SYS_PRLIMIT64},
|
||||
{Syscall: unix.SYS_PSELECT6},
|
||||
{Syscall: unix.SYS_PWRITE64},
|
||||
{Syscall: unix.SYS_PWRITEV},
|
||||
{Syscall: unix.SYS_PWRITEV2},
|
||||
{Syscall: unix.SYS_READ},
|
||||
{Syscall: unix.SYS_READAHEAD},
|
||||
{Syscall: unix.SYS_READLINK},
|
||||
{Syscall: unix.SYS_READLINKAT},
|
||||
{Syscall: unix.SYS_READV},
|
||||
{Syscall: unix.SYS_RECVFROM},
|
||||
{Syscall: unix.SYS_RECVMMSG},
|
||||
{Syscall: unix.SYS_RECVMSG},
|
||||
{Syscall: unix.SYS_REMAP_FILE_PAGES},
|
||||
{Syscall: unix.SYS_REMOVEXATTR},
|
||||
{Syscall: unix.SYS_RENAME},
|
||||
{Syscall: unix.SYS_RENAMEAT},
|
||||
{Syscall: unix.SYS_RENAMEAT2},
|
||||
{Syscall: unix.SYS_RESTART_SYSCALL},
|
||||
{Syscall: unix.SYS_RMDIR},
|
||||
{Syscall: unix.SYS_RT_SIGACTION},
|
||||
{Syscall: unix.SYS_RT_SIGPENDING},
|
||||
{Syscall: unix.SYS_RT_SIGPROCMASK},
|
||||
{Syscall: unix.SYS_RT_SIGQUEUEINFO},
|
||||
{Syscall: unix.SYS_RT_SIGRETURN},
|
||||
{Syscall: unix.SYS_RT_SIGSUSPEND},
|
||||
{Syscall: unix.SYS_RT_SIGTIMEDWAIT},
|
||||
{Syscall: unix.SYS_RT_TGSIGQUEUEINFO},
|
||||
{Syscall: unix.SYS_SCHED_GETAFFINITY},
|
||||
{Syscall: unix.SYS_SCHED_GETATTR},
|
||||
{Syscall: unix.SYS_SCHED_GETPARAM},
|
||||
{Syscall: unix.SYS_SCHED_GET_PRIORITY_MAX},
|
||||
{Syscall: unix.SYS_SCHED_GET_PRIORITY_MIN},
|
||||
{Syscall: unix.SYS_SCHED_GETSCHEDULER},
|
||||
{Syscall: unix.SYS_SCHED_RR_GET_INTERVAL},
|
||||
{Syscall: unix.SYS_SCHED_SETAFFINITY},
|
||||
{Syscall: unix.SYS_SCHED_SETATTR},
|
||||
{Syscall: unix.SYS_SCHED_SETPARAM},
|
||||
{Syscall: unix.SYS_SCHED_SETSCHEDULER},
|
||||
{Syscall: unix.SYS_SCHED_YIELD},
|
||||
{Syscall: unix.SYS_SECCOMP},
|
||||
{Syscall: unix.SYS_SELECT},
|
||||
{Syscall: unix.SYS_SEMCTL},
|
||||
{Syscall: unix.SYS_SEMGET},
|
||||
{Syscall: unix.SYS_SEMOP},
|
||||
{Syscall: unix.SYS_SEMTIMEDOP},
|
||||
{Syscall: unix.SYS_SENDFILE},
|
||||
{Syscall: unix.SYS_SENDMMSG},
|
||||
{Syscall: unix.SYS_SENDMSG},
|
||||
{Syscall: unix.SYS_SENDTO},
|
||||
{Syscall: unix.SYS_SETFSGID},
|
||||
{Syscall: unix.SYS_SETFSUID},
|
||||
{Syscall: unix.SYS_SETGID},
|
||||
{Syscall: unix.SYS_SETGROUPS},
|
||||
{Syscall: unix.SYS_SETITIMER},
|
||||
{Syscall: unix.SYS_SETPGID},
|
||||
{Syscall: unix.SYS_SETPRIORITY},
|
||||
{Syscall: unix.SYS_SETREGID},
|
||||
{Syscall: unix.SYS_SETRESGID},
|
||||
{Syscall: unix.SYS_SETRESUID},
|
||||
{Syscall: unix.SYS_SETREUID},
|
||||
{Syscall: unix.SYS_SETRLIMIT},
|
||||
{Syscall: unix.SYS_SET_ROBUST_LIST},
|
||||
{Syscall: unix.SYS_SETSID},
|
||||
{Syscall: unix.SYS_SETSOCKOPT},
|
||||
{Syscall: unix.SYS_SET_THREAD_AREA},
|
||||
{Syscall: unix.SYS_SET_TID_ADDRESS},
|
||||
{Syscall: unix.SYS_SETUID},
|
||||
{Syscall: unix.SYS_SETXATTR},
|
||||
{Syscall: unix.SYS_SHMAT},
|
||||
{Syscall: unix.SYS_SHMCTL},
|
||||
{Syscall: unix.SYS_SHMDT},
|
||||
{Syscall: unix.SYS_SHMGET},
|
||||
{Syscall: unix.SYS_SHUTDOWN},
|
||||
{Syscall: unix.SYS_SIGALTSTACK},
|
||||
{Syscall: unix.SYS_SIGNALFD},
|
||||
{Syscall: unix.SYS_SIGNALFD4},
|
||||
{Syscall: unix.SYS_SOCKET},
|
||||
{Syscall: unix.SYS_SOCKETPAIR},
|
||||
{Syscall: unix.SYS_SPLICE},
|
||||
{Syscall: unix.SYS_STAT},
|
||||
{Syscall: unix.SYS_STATFS},
|
||||
{Syscall: unix.SYS_SYMLINK},
|
||||
{Syscall: unix.SYS_SYMLINKAT},
|
||||
{Syscall: unix.SYS_SYNC},
|
||||
{Syscall: unix.SYS_SYNC_FILE_RANGE},
|
||||
{Syscall: unix.SYS_SYNCFS},
|
||||
{Syscall: unix.SYS_SYSINFO},
|
||||
{Syscall: unix.SYS_SYSLOG},
|
||||
{Syscall: unix.SYS_TEE},
|
||||
{Syscall: unix.SYS_TGKILL},
|
||||
{Syscall: unix.SYS_TIME},
|
||||
{Syscall: unix.SYS_TIMER_CREATE},
|
||||
{Syscall: unix.SYS_TIMER_DELETE},
|
||||
{Syscall: unix.SYS_TIMERFD_CREATE},
|
||||
{Syscall: unix.SYS_TIMERFD_GETTIME},
|
||||
{Syscall: unix.SYS_TIMERFD_SETTIME},
|
||||
{Syscall: unix.SYS_TIMER_GETOVERRUN},
|
||||
{Syscall: unix.SYS_TIMER_GETTIME},
|
||||
{Syscall: unix.SYS_TIMER_SETTIME},
|
||||
{Syscall: unix.SYS_TIMES},
|
||||
{Syscall: unix.SYS_TKILL},
|
||||
{Syscall: unix.SYS_TRUNCATE},
|
||||
{Syscall: unix.SYS_UMASK},
|
||||
{Syscall: unix.SYS_UNAME},
|
||||
{Syscall: unix.SYS_UNLINK},
|
||||
{Syscall: unix.SYS_UNLINKAT},
|
||||
{Syscall: unix.SYS_UTIME},
|
||||
{Syscall: unix.SYS_UTIMENSAT},
|
||||
{Syscall: unix.SYS_UTIMES},
|
||||
{Syscall: unix.SYS_VFORK},
|
||||
{Syscall: unix.SYS_VMSPLICE},
|
||||
{Syscall: unix.SYS_WAIT4},
|
||||
{Syscall: unix.SYS_WAITID},
|
||||
{Syscall: unix.SYS_WRITE},
|
||||
{Syscall: unix.SYS_WRITEV},
|
||||
}
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user