Этот коммит содержится в:
JoramWilander
2018-07-10 15:01:43 -04:00
родитель c042ffa460 1e1a5e5e85
Коммит 6c7dc2d29c
314 изменённых файлов: 54669 добавлений и 9462 удалений

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

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

@@ -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()

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

@@ -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",
},
},

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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()

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

@@ -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) {

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

@@ -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)

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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},
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше