MM-10702 Moving plugins to use hashicorp go-plugin. (#8978)
* Moving plugins to use hashicorp go-plugin. * Tweaks from feedback.
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ecefa6cdd1
Коммит
1e5c432e10
@@ -4,6 +4,7 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
@@ -104,17 +105,18 @@ 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
|
||||
}
|
||||
|
||||
var Handshake = plugin.HandshakeConfig{
|
||||
ProtocolVersion: 1,
|
||||
MagicCookieKey: "MATTERMOST_PLUGIN",
|
||||
MagicCookieValue: "Securely message teams, anywhere.",
|
||||
}
|
||||
|
||||
51
plugin/client.go
Обычный файл
51
plugin/client.go
Обычный файл
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
// Starts the serving of a Mattermost plugin over rpc or gRPC
|
||||
// Call this when your plugin is ready to start
|
||||
func ClientMain(pluginImplementation interface{}) {
|
||||
if impl, ok := pluginImplementation.(interface {
|
||||
SetAPI(api API)
|
||||
SetSelfRef(ref interface{})
|
||||
}); !ok {
|
||||
panic("Plugin implementation given must embed plugin.MattermostPlugin")
|
||||
} else {
|
||||
impl.SetAPI(nil)
|
||||
impl.SetSelfRef(pluginImplementation)
|
||||
}
|
||||
|
||||
pluginMap := map[string]plugin.Plugin{
|
||||
"hooks": &HooksPlugin{hooks: pluginImplementation},
|
||||
}
|
||||
|
||||
plugin.Serve(&plugin.ServeConfig{
|
||||
HandshakeConfig: Handshake,
|
||||
Plugins: pluginMap,
|
||||
})
|
||||
}
|
||||
|
||||
type MattermostPlugin struct {
|
||||
API API
|
||||
selfRef interface{} // This is so we can unmarshal into our parent
|
||||
}
|
||||
|
||||
func (p *MattermostPlugin) SetAPI(api API) {
|
||||
p.API = api
|
||||
}
|
||||
|
||||
func (p *MattermostPlugin) SetSelfRef(ref interface{}) {
|
||||
p.selfRef = ref
|
||||
}
|
||||
|
||||
func (p *MattermostPlugin) OnConfigurationChange() error {
|
||||
if p.selfRef != nil {
|
||||
return p.API.LoadPluginConfiguration(p.selfRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
331
plugin/client_rpc.go
Обычный файл
331
plugin/client_rpc.go
Обычный файл
@@ -0,0 +1,331 @@
|
||||
// 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"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
"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
|
||||
log *mlog.Logger
|
||||
}
|
||||
|
||||
// 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 // Where does this go?
|
||||
}
|
||||
|
||||
// Settings for this should come from the parent process, for now just set it up
|
||||
// though stdout.
|
||||
logger := mlog.NewLogger(&mlog.LoggerConfiguration{
|
||||
EnableConsole: true,
|
||||
ConsoleJson: true,
|
||||
ConsoleLevel: mlog.LevelDebug,
|
||||
EnableFile: false,
|
||||
})
|
||||
logger = logger.With(mlog.Bool("plugin_subprocess", true))
|
||||
|
||||
s.log = logger
|
||||
|
||||
s.apiRPCClient = &APIRPCClient{
|
||||
client: rpc.NewClient(connection),
|
||||
log: logger,
|
||||
}
|
||||
|
||||
if mmplugin, ok := s.impl.(interface {
|
||||
SetAPI(api API)
|
||||
OnConfigurationChange() error
|
||||
}); !ok {
|
||||
} else {
|
||||
mmplugin.SetAPI(s.apiRPCClient)
|
||||
mmplugin.OnConfigurationChange()
|
||||
}
|
||||
|
||||
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
|
||||
RequestBodyStream uint32
|
||||
}
|
||||
|
||||
func (g *HooksRPCClient) ServeHTTP(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{
|
||||
ResponseWriterStream: serveHTTPStreamId,
|
||||
Request: forwardedRequest,
|
||||
RequestBodyStream: requestBodyStreamId,
|
||||
}, nil); err != nil {
|
||||
mlog.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 {
|
||||
s.log.Debug("Can't connect to remote response writer stream", mlog.Err(err))
|
||||
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 {
|
||||
s.log.Debug("Can't connect to remote response writer stream", mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
r.Body = ConnectIOReader(connection)
|
||||
} else {
|
||||
r.Body = ioutil.NopCloser(&bytes.Buffer{})
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if hook, ok := s.impl.(http.Handler); ok {
|
||||
hook.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1103
plugin/client_rpc_generated.go
Обычный файл
1103
plugin/client_rpc_generated.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
260
plugin/environment.go
Обычный файл
260
plugin/environment.go
Обычный файл
@@ -0,0 +1,260 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type APIImplCreatorFunc func(*model.Manifest) API
|
||||
type SupervisorCreatorFunc func(*model.BundleInfo, *mlog.Logger, API) (*Supervisor, error)
|
||||
|
||||
// Hooks will be the hooks API for the plugin
|
||||
// Return value should be true if we should continue calling more plugins
|
||||
type MultliPluginHookRunnerFunc func(hooks Hooks) bool
|
||||
|
||||
type ActivePlugin struct {
|
||||
BundleInfo *model.BundleInfo
|
||||
State int
|
||||
Supervisor *Supervisor
|
||||
}
|
||||
|
||||
type Environment struct {
|
||||
activePlugins map[string]ActivePlugin
|
||||
mutex sync.RWMutex
|
||||
logger *mlog.Logger
|
||||
newAPIImpl APIImplCreatorFunc
|
||||
pluginDir string
|
||||
webappPluginDir string
|
||||
}
|
||||
|
||||
func NewEnvironment(newAPIImpl APIImplCreatorFunc, pluginDir string, webappPluginDir string, logger *mlog.Logger) (*Environment, error) {
|
||||
return &Environment{
|
||||
activePlugins: make(map[string]ActivePlugin),
|
||||
logger: logger,
|
||||
newAPIImpl: newAPIImpl,
|
||||
pluginDir: pluginDir,
|
||||
webappPluginDir: webappPluginDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Performs a full scan of the given path.
|
||||
//
|
||||
// This function will return info for all subdirectories that appear to be plugins (i.e. all
|
||||
// subdirectories containing plugin manifest files, regardless of whether they could actually be
|
||||
// parsed).
|
||||
//
|
||||
// Plugins are found non-recursively and paths beginning with a dot are always ignored.
|
||||
func ScanSearchPath(path string) ([]*model.BundleInfo, error) {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var ret []*model.BundleInfo
|
||||
for _, file := range files {
|
||||
if !file.IsDir() || file.Name()[0] == '.' {
|
||||
continue
|
||||
}
|
||||
if info := model.BundleInfoForPath(filepath.Join(path, file.Name())); info.ManifestPath != "" {
|
||||
ret = append(ret, info)
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Returns a list of all plugins within the environment.
|
||||
func (env *Environment) Available() ([]*model.BundleInfo, error) {
|
||||
return ScanSearchPath(env.pluginDir)
|
||||
}
|
||||
|
||||
// Returns a list of all currently active plugins within the environment.
|
||||
func (env *Environment) Active() []*model.BundleInfo {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
activePlugins := []*model.BundleInfo{}
|
||||
for _, p := range env.activePlugins {
|
||||
activePlugins = append(activePlugins, p.BundleInfo)
|
||||
}
|
||||
|
||||
return activePlugins
|
||||
}
|
||||
|
||||
func (env *Environment) IsActive(id string) bool {
|
||||
_, ok := env.activePlugins[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Returns a list of plugin statuses reprensenting the state of every plugin
|
||||
func (env *Environment) Statuses() (model.PluginStatuses, error) {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
plugins, err := env.Available()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to get plugin statuses")
|
||||
}
|
||||
|
||||
pluginStatuses := make(model.PluginStatuses, 0, len(plugins))
|
||||
for _, plugin := range plugins {
|
||||
// For now we don't handle bad manifests, we should
|
||||
if plugin.Manifest == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pluginState := model.PluginStateNotRunning
|
||||
if plugin, ok := env.activePlugins[plugin.Manifest.Id]; ok {
|
||||
pluginState = plugin.State
|
||||
}
|
||||
|
||||
status := &model.PluginStatus{
|
||||
PluginId: plugin.Manifest.Id,
|
||||
PluginPath: filepath.Dir(plugin.ManifestPath),
|
||||
State: pluginState,
|
||||
Name: plugin.Manifest.Name,
|
||||
Description: plugin.Manifest.Description,
|
||||
Version: plugin.Manifest.Version,
|
||||
}
|
||||
|
||||
pluginStatuses = append(pluginStatuses, status)
|
||||
}
|
||||
|
||||
return pluginStatuses, nil
|
||||
}
|
||||
|
||||
func (env *Environment) Activate(id string) (reterr error) {
|
||||
env.mutex.Lock()
|
||||
defer env.mutex.Unlock()
|
||||
|
||||
// Check if we are already active
|
||||
if _, ok := env.activePlugins[id]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
plugins, err := env.Available()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var pluginInfo *model.BundleInfo
|
||||
for _, p := range plugins {
|
||||
if p.Manifest != nil && p.Manifest.Id == id {
|
||||
if pluginInfo != nil {
|
||||
return fmt.Errorf("multiple plugins found: %v", id)
|
||||
}
|
||||
pluginInfo = p
|
||||
}
|
||||
}
|
||||
if pluginInfo == nil {
|
||||
return fmt.Errorf("plugin not found: %v", id)
|
||||
}
|
||||
|
||||
activePlugin := ActivePlugin{BundleInfo: pluginInfo}
|
||||
defer func() {
|
||||
if reterr == nil {
|
||||
activePlugin.State = model.PluginStateRunning
|
||||
} else {
|
||||
activePlugin.State = model.PluginStateFailedToStart
|
||||
}
|
||||
env.activePlugins[pluginInfo.Manifest.Id] = activePlugin
|
||||
}()
|
||||
|
||||
if pluginInfo.Manifest.Webapp != nil {
|
||||
bundlePath := filepath.Clean(pluginInfo.Manifest.Webapp.BundlePath)
|
||||
if bundlePath == "" || bundlePath[0] == '.' {
|
||||
return fmt.Errorf("invalid webapp bundle path")
|
||||
}
|
||||
bundlePath = filepath.Join(env.pluginDir, id, bundlePath)
|
||||
|
||||
webappBundle, err := ioutil.ReadFile(bundlePath)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to read webapp bundle: %v", id)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(fmt.Sprintf("%s/%s_bundle.js", env.webappPluginDir, id), webappBundle, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to write webapp bundle: %v", id)
|
||||
}
|
||||
}
|
||||
|
||||
if pluginInfo.Manifest.Backend != nil {
|
||||
supervisor, err := NewSupervisor(pluginInfo, env.logger, env.newAPIImpl(pluginInfo.Manifest))
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to start plugin: %v", id)
|
||||
}
|
||||
activePlugin.Supervisor = supervisor
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deactivates the plugin with the given id.
|
||||
func (env *Environment) Deactivate(id string) {
|
||||
env.mutex.Lock()
|
||||
defer env.mutex.Unlock()
|
||||
|
||||
if activePlugin, ok := env.activePlugins[id]; !ok {
|
||||
return
|
||||
} else {
|
||||
delete(env.activePlugins, id)
|
||||
if activePlugin.Supervisor != nil {
|
||||
if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil {
|
||||
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", activePlugin.BundleInfo.Manifest.Id), mlog.Err(err))
|
||||
}
|
||||
activePlugin.Supervisor.Shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deactivates all plugins and gracefully shuts down the environment.
|
||||
func (env *Environment) Shutdown() {
|
||||
env.mutex.Lock()
|
||||
defer env.mutex.Unlock()
|
||||
|
||||
for _, activePlugin := range env.activePlugins {
|
||||
if activePlugin.Supervisor != nil {
|
||||
if err := activePlugin.Supervisor.Hooks().OnDeactivate(); err != nil {
|
||||
env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", activePlugin.BundleInfo.Manifest.Id), mlog.Err(err))
|
||||
}
|
||||
activePlugin.Supervisor.Shutdown()
|
||||
}
|
||||
}
|
||||
env.activePlugins = make(map[string]ActivePlugin)
|
||||
return
|
||||
}
|
||||
|
||||
// Returns the hooks API for the plugin ID specified
|
||||
// You should probably use RunMultiPluginHook instead.
|
||||
func (env *Environment) HooksForPlugin(id string) (Hooks, error) {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
if plug, ok := env.activePlugins[id]; ok && plug.Supervisor != nil {
|
||||
return plug.Supervisor.Hooks(), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("plugin not found: %v", id)
|
||||
}
|
||||
|
||||
// Calls hookRunnerFunc with the hooks for each active plugin that implments the given HookId
|
||||
// If hookRunnerFunc returns false, then iteration will not continue.
|
||||
func (env *Environment) RunMultiPluginHook(hookRunnerFunc MultliPluginHookRunnerFunc, mustImplement int) {
|
||||
env.mutex.RLock()
|
||||
defer env.mutex.RUnlock()
|
||||
|
||||
for _, activePlugin := range env.activePlugins {
|
||||
if activePlugin.Supervisor == nil || !activePlugin.Supervisor.Implements(mustImplement) {
|
||||
continue
|
||||
}
|
||||
if !hookRunnerFunc(activePlugin.Supervisor.Hooks()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package plugin_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type HelloUserPlugin struct {
|
||||
api plugin.API
|
||||
}
|
||||
|
||||
func (p *HelloUserPlugin) OnActivate(api plugin.API) error {
|
||||
// Just save api for later when we need to look up users.
|
||||
p.api = api
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HelloUserPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if userId := r.Header.Get("Mattermost-User-Id"); userId == "" {
|
||||
// Our visitor is unauthenticated.
|
||||
fmt.Fprintf(w, "Hello, stranger!")
|
||||
} else if user, err := p.api.GetUser(userId); err == nil {
|
||||
// Greet the user by name!
|
||||
fmt.Fprintf(w, "Welcome back, %v!", user.Username)
|
||||
} else {
|
||||
// This won't happen in normal circumstances, but let's just be safe.
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// This example demonstrates a plugin that handles HTTP requests which respond by greeting the user
|
||||
// by name.
|
||||
func Example_helloUser() {
|
||||
rpcplugin.Main(&HelloUserPlugin{})
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package plugin_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
type HelloWorldPlugin struct{}
|
||||
|
||||
func (p *HelloWorldPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hello, world!")
|
||||
}
|
||||
|
||||
// This example demonstrates a plugin that handles HTTP requests which respond by greeting the
|
||||
// world.
|
||||
func Example_helloWorld() {
|
||||
rpcplugin.Main(&HelloWorldPlugin{})
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package plugin_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
)
|
||||
|
||||
func TestHelloUserPlugin(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "billybob",
|
||||
}
|
||||
|
||||
api := &plugintest.API{}
|
||||
api.On("GetUser", user.Id).Return(user, nil)
|
||||
defer api.AssertExpectations(t)
|
||||
|
||||
p := &HelloUserPlugin{}
|
||||
p.OnActivate(api)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
r.Header.Add("Mattermost-User-Id", user.Id)
|
||||
p.ServeHTTP(w, r)
|
||||
body, err := ioutil.ReadAll(w.Result().Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Welcome back, billybob!", string(body))
|
||||
}
|
||||
73
plugin/hclog_adapter.go
Обычный файл
73
plugin/hclog_adapter.go
Обычный файл
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"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{}) {
|
||||
h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...)))
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) Debug(msg string, args ...interface{}) {
|
||||
h.wrappedLogger.Debug(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...)))
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) Info(msg string, args ...interface{}) {
|
||||
h.wrappedLogger.Info(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...)))
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) Warn(msg string, args ...interface{}) {
|
||||
h.wrappedLogger.Warn(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...)))
|
||||
}
|
||||
|
||||
func (h *HclogAdapter) Error(msg string, args ...interface{}) {
|
||||
h.wrappedLogger.Error(msg, mlog.String(h.extrasKey, fmt.Sprintln(args...)))
|
||||
}
|
||||
|
||||
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,32 @@ 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
|
||||
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,7 +48,7 @@ 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(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// ExecuteCommand executes a command that has been previously registered via the RegisterCommand
|
||||
// API.
|
||||
|
||||
91
plugin/http.go
Обычный файл
91
plugin/http.go
Обычный файл
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
type HTTPResponseWriterRPCServer struct {
|
||||
w http.ResponseWriter
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCServer) Header(args struct{}, reply *http.Header) error {
|
||||
*reply = w.w.Header()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCServer) Write(args []byte, reply *struct{}) error {
|
||||
_, err := w.w.Write(args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCServer) WriteHeader(args int, reply *struct{}) error {
|
||||
w.w.WriteHeader(args)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCServer) SyncHeader(args http.Header, reply *struct{}) error {
|
||||
dest := w.w.Header()
|
||||
for k := range dest {
|
||||
if _, ok := args[k]; !ok {
|
||||
delete(dest, k)
|
||||
}
|
||||
}
|
||||
for k, v := range args {
|
||||
dest[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ServeHTTPResponseWriter(w http.ResponseWriter, conn io.ReadWriteCloser) {
|
||||
server := rpc.NewServer()
|
||||
server.Register(&HTTPResponseWriterRPCServer{
|
||||
w: w,
|
||||
})
|
||||
server.ServeConn(conn)
|
||||
}
|
||||
|
||||
type HTTPResponseWriterRPCClient struct {
|
||||
client *rpc.Client
|
||||
header http.Header
|
||||
}
|
||||
|
||||
var _ http.ResponseWriter = (*HTTPResponseWriterRPCClient)(nil)
|
||||
|
||||
func (w *HTTPResponseWriterRPCClient) Header() http.Header {
|
||||
if w.header == nil {
|
||||
w.client.Call("Plugin.Header", struct{}{}, &w.header)
|
||||
}
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCClient) Write(b []byte) (int, error) {
|
||||
if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := w.client.Call("Plugin.Write", b, nil); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (w *HTTPResponseWriterRPCClient) WriteHeader(statusCode int) {
|
||||
if err := w.client.Call("Plugin.SyncHeader", w.header, nil); err != nil {
|
||||
return
|
||||
}
|
||||
w.client.Call("Plugin.WriteHeader", statusCode, nil)
|
||||
}
|
||||
|
||||
func (h *HTTPResponseWriterRPCClient) Close() error {
|
||||
return h.client.Close()
|
||||
}
|
||||
|
||||
func ConnectHTTPResponseWriter(conn io.ReadWriteCloser) *HTTPResponseWriterRPCClient {
|
||||
return &HTTPResponseWriterRPCClient{
|
||||
client: rpc.NewClient(conn),
|
||||
}
|
||||
}
|
||||
377
plugin/interface_generator/main.go
Обычный файл
377
plugin/interface_generator/main.go
Обычный файл
@@ -0,0 +1,377 @@
|
||||
// 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 {
|
||||
if len(field.Names) == 0 {
|
||||
result = append(result, structPrefix+string(nextLetter))
|
||||
nextLetter += 1
|
||||
} else {
|
||||
for range field.Names {
|
||||
result = append(result, structPrefix+string(nextLetter))
|
||||
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 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}})
|
||||
}
|
||||
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}})
|
||||
}
|
||||
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"
|
||||
@@ -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
|
||||
@@ -114,7 +113,7 @@ func (_m *APIMOCKINTERNAL) CreateTeam(team *model.Team) (*model.Team, *model.App
|
||||
}
|
||||
|
||||
// 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 +138,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 +154,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 +170,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 +186,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
|
||||
@@ -203,7 +202,7 @@ func (_m *APIMOCKINTERNAL) DeleteTeam(teamId string) *model.AppError {
|
||||
}
|
||||
|
||||
// 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 +218,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 +243,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 +268,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
|
||||
@@ -294,7 +293,7 @@ func (_m *APIMOCKINTERNAL) GetChannelMember(channelId string, userId string) (*m
|
||||
}
|
||||
|
||||
// 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 +318,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 +343,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
|
||||
@@ -369,7 +368,7 @@ func (_m *APIMOCKINTERNAL) GetPost(postId string) (*model.Post, *model.AppError)
|
||||
}
|
||||
|
||||
// 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 +393,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
|
||||
@@ -419,7 +418,7 @@ func (_m *APIMOCKINTERNAL) GetTeamByName(name string) (*model.Team, *model.AppEr
|
||||
}
|
||||
|
||||
// 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 +443,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 +468,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 +492,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 +550,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
|
||||
@@ -524,7 +564,7 @@ func (_m *APIMOCKINTERNAL) LoadPluginConfiguration(dest interface{}) error {
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -538,7 +578,7 @@ func (_m *APIMOCKINTERNAL) RegisterCommand(command *model.Command) error {
|
||||
}
|
||||
|
||||
// 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 +592,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 +617,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 +642,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 +667,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 +692,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
|
||||
@@ -677,7 +717,7 @@ func (_m *APIMOCKINTERNAL) UpdateTeam(team *model.Team) (*model.Team, *model.App
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -7,7 +7,6 @@ package plugintest
|
||||
import http "net/http"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import model "github.com/mattermost/mattermost-server/model"
|
||||
import plugin "github.com/mattermost/mattermost-server/plugin"
|
||||
|
||||
// Hooks is an autogenerated mock type for the Hooks type
|
||||
type Hooks struct {
|
||||
@@ -39,6 +38,29 @@ func (_m *Hooks) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// MessageHasBeenPosted provides a mock function with given fields: post
|
||||
func (_m *Hooks) MessageHasBeenPosted(post *model.Post) {
|
||||
_m.Called(post)
|
||||
@@ -95,13 +117,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 +159,7 @@ 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: w, r
|
||||
func (_m *Hooks) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
_m.Called(w, r)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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},
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// +build linux,!amd64
|
||||
|
||||
package sandbox
|
||||
|
||||
const NATIVE_AUDIT_ARCH = 0
|
||||
|
||||
var AllowedSyscalls []SeccompSyscall
|
||||
@@ -1,210 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/bpf"
|
||||
)
|
||||
|
||||
func seccompData(nr int32, arch uint32, ip uint64, args ...uint64) []byte {
|
||||
var buf [64]byte
|
||||
binary.BigEndian.PutUint32(buf[0:], uint32(nr))
|
||||
binary.BigEndian.PutUint32(buf[4:], arch)
|
||||
binary.BigEndian.PutUint64(buf[8:], ip)
|
||||
for i := 0; i < 6 && i < len(args); i++ {
|
||||
binary.BigEndian.PutUint64(buf[16+i*8:], args[i])
|
||||
}
|
||||
return buf[:]
|
||||
}
|
||||
|
||||
func TestSeccompFilter(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
Filter []bpf.Instruction
|
||||
Data []byte
|
||||
Expected bool
|
||||
}{
|
||||
"Allowed": {
|
||||
Filter: SeccompFilter(0xf00, []SeccompSyscall{
|
||||
{Syscall: syscall.SYS_READ},
|
||||
{Syscall: syscall.SYS_WRITE},
|
||||
}),
|
||||
Data: seccompData(syscall.SYS_READ, 0xf00, 0),
|
||||
Expected: true,
|
||||
},
|
||||
"AllFail": {
|
||||
Filter: SeccompFilter(0xf00, []SeccompSyscall{
|
||||
{
|
||||
Syscall: syscall.SYS_READ,
|
||||
Any: []SeccompConditions{
|
||||
{All: []SeccompCondition{
|
||||
&SeccompArgHasAnyBit{Arg: 0, Mask: 2},
|
||||
&SeccompArgHasAnyBit{Arg: 1, Mask: 2},
|
||||
&SeccompArgHasAnyBit{Arg: 2, Mask: 2},
|
||||
&SeccompArgHasAnyBit{Arg: 3, Mask: 2},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{Syscall: syscall.SYS_WRITE},
|
||||
}),
|
||||
Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4),
|
||||
Expected: false,
|
||||
},
|
||||
"AllPass": {
|
||||
Filter: SeccompFilter(0xf00, []SeccompSyscall{
|
||||
{
|
||||
Syscall: syscall.SYS_READ,
|
||||
Any: []SeccompConditions{
|
||||
{All: []SeccompCondition{
|
||||
&SeccompArgHasAnyBit{Arg: 0, Mask: 7},
|
||||
&SeccompArgHasAnyBit{Arg: 1, Mask: 7},
|
||||
&SeccompArgHasAnyBit{Arg: 2, Mask: 7},
|
||||
&SeccompArgHasAnyBit{Arg: 3, Mask: 7},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{Syscall: syscall.SYS_WRITE},
|
||||
}),
|
||||
Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4),
|
||||
Expected: true,
|
||||
},
|
||||
"AnyFail": {
|
||||
Filter: SeccompFilter(0xf00, []SeccompSyscall{
|
||||
{
|
||||
Syscall: syscall.SYS_READ,
|
||||
Any: []SeccompConditions{
|
||||
{All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 0, Mask: 8}}},
|
||||
{All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 1, Mask: 8}}},
|
||||
{All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 2, Mask: 8}}},
|
||||
{All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 3, Mask: 8}}},
|
||||
},
|
||||
},
|
||||
{Syscall: syscall.SYS_WRITE},
|
||||
}),
|
||||
Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4),
|
||||
Expected: false,
|
||||
},
|
||||
"AnyPass": {
|
||||
Filter: SeccompFilter(0xf00, []SeccompSyscall{
|
||||
{
|
||||
Syscall: syscall.SYS_READ,
|
||||
Any: []SeccompConditions{
|
||||
{All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 0, Mask: 2}}},
|
||||
{All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 1, Mask: 2}}},
|
||||
{All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 2, Mask: 2}}},
|
||||
{All: []SeccompCondition{&SeccompArgHasAnyBit{Arg: 3, Mask: 2}}},
|
||||
},
|
||||
},
|
||||
{Syscall: syscall.SYS_WRITE},
|
||||
}),
|
||||
Data: seccompData(syscall.SYS_READ, 0xf00, 0, 1, 2, 3, 4),
|
||||
Expected: true,
|
||||
},
|
||||
"BadArch": {
|
||||
Filter: SeccompFilter(0xf00, []SeccompSyscall{
|
||||
{Syscall: syscall.SYS_READ},
|
||||
{Syscall: syscall.SYS_WRITE},
|
||||
}),
|
||||
Data: seccompData(syscall.SYS_MOUNT, 0xf01, 0),
|
||||
Expected: false,
|
||||
},
|
||||
"BadSyscall": {
|
||||
Filter: SeccompFilter(0xf00, []SeccompSyscall{
|
||||
{Syscall: syscall.SYS_READ},
|
||||
{Syscall: syscall.SYS_WRITE},
|
||||
}),
|
||||
Data: seccompData(syscall.SYS_MOUNT, 0xf00, 0),
|
||||
Expected: false,
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
vm, err := bpf.NewVM(tc.Filter)
|
||||
require.NoError(t, err)
|
||||
result, err := vm.Run(tc.Data)
|
||||
require.NoError(t, err)
|
||||
if tc.Expected {
|
||||
assert.Equal(t, SECCOMP_RET_ALLOW, result)
|
||||
} else {
|
||||
assert.Equal(t, int(SECCOMP_RET_ERRNO|syscall.EPERM), result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeccompFilter_Conditions(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
Condition SeccompCondition
|
||||
Args []uint64
|
||||
Expected bool
|
||||
}{
|
||||
"ArgHasAnyBitFail": {
|
||||
Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x0004},
|
||||
Args: []uint64{0x0400008000},
|
||||
Expected: false,
|
||||
},
|
||||
"ArgHasAnyBitPass1": {
|
||||
Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x400000004},
|
||||
Args: []uint64{0x8000008004},
|
||||
Expected: true,
|
||||
},
|
||||
"ArgHasAnyBitPass2": {
|
||||
Condition: SeccompArgHasAnyBit{Arg: 0, Mask: 0x400000004},
|
||||
Args: []uint64{0x8400008000},
|
||||
Expected: true,
|
||||
},
|
||||
"ArgHasNoBitsFail1": {
|
||||
Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x1100000011},
|
||||
Args: []uint64{0x0000008007},
|
||||
Expected: false,
|
||||
},
|
||||
"ArgHasNoBitsFail2": {
|
||||
Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x1100000011},
|
||||
Args: []uint64{0x0700008000},
|
||||
Expected: false,
|
||||
},
|
||||
"ArgHasNoBitsPass": {
|
||||
Condition: SeccompArgHasNoBits{Arg: 0, Mask: 0x400000004},
|
||||
Args: []uint64{0x8000008000},
|
||||
Expected: true,
|
||||
},
|
||||
"ArgEqualsPass": {
|
||||
Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF},
|
||||
Args: []uint64{0x123456789ABCDEF},
|
||||
Expected: true,
|
||||
},
|
||||
"ArgEqualsFail1": {
|
||||
Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF},
|
||||
Args: []uint64{0x023456789ABCDEF},
|
||||
Expected: false,
|
||||
},
|
||||
"ArgEqualsFail2": {
|
||||
Condition: SeccompArgEquals{Arg: 0, Value: 0x123456789ABCDEF},
|
||||
Args: []uint64{0x123456789ABCDE0},
|
||||
Expected: false,
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
filter := SeccompFilter(0xf00, []SeccompSyscall{
|
||||
{
|
||||
Syscall: 1,
|
||||
Any: []SeccompConditions{{All: []SeccompCondition{tc.Condition}}},
|
||||
},
|
||||
})
|
||||
vm, err := bpf.NewVM(filter)
|
||||
require.NoError(t, err)
|
||||
result, err := vm.Run(seccompData(1, 0xf00, 0, tc.Args...))
|
||||
require.NoError(t, err)
|
||||
if tc.Expected {
|
||||
assert.Equal(t, SECCOMP_RET_ALLOW, result)
|
||||
} else {
|
||||
assert.Equal(t, int(SECCOMP_RET_ERRNO|syscall.EPERM), result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin"
|
||||
)
|
||||
|
||||
func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) {
|
||||
return rpcplugin.SupervisorWithNewProcessFunc(bundle, func(ctx context.Context) (rpcplugin.Process, io.ReadWriteCloser, error) {
|
||||
executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable))
|
||||
if strings.HasPrefix(executable, "..") {
|
||||
return nil, nil, fmt.Errorf("invalid backend executable")
|
||||
}
|
||||
return NewProcess(ctx, &Configuration{
|
||||
MountPoints: []*MountPoint{{
|
||||
Source: bundle.Path,
|
||||
Destination: "/plugin",
|
||||
ReadOnly: true,
|
||||
}},
|
||||
WorkingDirectory: "/plugin",
|
||||
}, filepath.Join("/plugin", executable))
|
||||
})
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package sandbox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
|
||||
)
|
||||
|
||||
func TestSupervisorProvider(t *testing.T) {
|
||||
if err := CheckSupport(); err != nil {
|
||||
t.Skip("sandboxing not supported:", err)
|
||||
}
|
||||
|
||||
rpcplugintest.TestSupervisorProvider(t, SupervisorProvider)
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxProcessRestarts = 3
|
||||
)
|
||||
|
||||
// Supervisor implements a plugin.Supervisor that launches the plugin in a separate process and
|
||||
// communicates via RPC.
|
||||
//
|
||||
// If the plugin unexpectedly exits, the supervisor will relaunch it after a short delay, but will
|
||||
// only restart a plugin at most three times.
|
||||
type Supervisor struct {
|
||||
hooks atomic.Value
|
||||
done chan bool
|
||||
cancel context.CancelFunc
|
||||
newProcess func(context.Context) (Process, io.ReadWriteCloser, error)
|
||||
pluginId string
|
||||
pluginErr error
|
||||
}
|
||||
|
||||
var _ plugin.Supervisor = (*Supervisor)(nil)
|
||||
|
||||
// Starts the plugin. This method will block until the plugin is successfully launched for the first
|
||||
// time and will return an error if the plugin cannot be launched at all.
|
||||
func (s *Supervisor) Start(api plugin.API) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.done = make(chan bool, 1)
|
||||
start := make(chan error, 1)
|
||||
go s.run(ctx, start, api)
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second * 3):
|
||||
cancel()
|
||||
<-s.done
|
||||
return fmt.Errorf("timed out waiting for plugin")
|
||||
case err := <-start:
|
||||
s.cancel = cancel
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Waits for the supervisor to stop (on demand or of its own accord), returning any error that
|
||||
// triggered the supervisor to stop.
|
||||
func (s *Supervisor) Wait() error {
|
||||
<-s.done
|
||||
return s.pluginErr
|
||||
}
|
||||
|
||||
// Stops the plugin.
|
||||
func (s *Supervisor) Stop() error {
|
||||
s.cancel()
|
||||
<-s.done
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the hooks used to communicate with the plugin. The hooks may change if the plugin is
|
||||
// restarted, so the return value should not be cached.
|
||||
func (s *Supervisor) Hooks() plugin.Hooks {
|
||||
return s.hooks.Load().(plugin.Hooks)
|
||||
}
|
||||
|
||||
func (s *Supervisor) run(ctx context.Context, start chan<- error, api plugin.API) {
|
||||
defer func() {
|
||||
close(s.done)
|
||||
}()
|
||||
done := ctx.Done()
|
||||
for i := 0; i <= MaxProcessRestarts; i++ {
|
||||
s.runPlugin(ctx, start, api)
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
start = nil
|
||||
if i < MaxProcessRestarts {
|
||||
mlog.Error("Plugin terminated unexpectedly", mlog.String("plugin_id", s.pluginId))
|
||||
time.Sleep(time.Duration((1 + i*i)) * time.Second)
|
||||
} else {
|
||||
s.pluginErr = fmt.Errorf("plugin terminated unexpectedly too many times")
|
||||
mlog.Error("Plugin shutdown", mlog.String("plugin_id", s.pluginId), mlog.Int("max_process_restarts", MaxProcessRestarts), mlog.Err(s.pluginErr))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Supervisor) runPlugin(ctx context.Context, start chan<- error, api plugin.API) error {
|
||||
if start == nil {
|
||||
mlog.Debug("Restarting plugin", mlog.String("plugin_id", s.pluginId))
|
||||
}
|
||||
|
||||
p, ipc, err := s.newProcess(ctx)
|
||||
if err != nil {
|
||||
if start != nil {
|
||||
start <- err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
muxer := NewMuxer(ipc, false)
|
||||
closeMuxer := make(chan bool, 1)
|
||||
muxerClosed := make(chan error, 1)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-closeMuxer:
|
||||
break
|
||||
}
|
||||
muxerClosed <- muxer.Close()
|
||||
}()
|
||||
|
||||
hooks, err := ConnectMain(muxer, s.pluginId)
|
||||
if err == nil {
|
||||
err = hooks.OnActivate(api)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if start != nil {
|
||||
start <- err
|
||||
}
|
||||
closeMuxer <- true
|
||||
<-muxerClosed
|
||||
p.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
s.hooks.Store(hooks)
|
||||
|
||||
if start != nil {
|
||||
start <- nil
|
||||
}
|
||||
p.Wait()
|
||||
closeMuxer <- true
|
||||
<-muxerClosed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SupervisorProvider(bundle *model.BundleInfo) (plugin.Supervisor, error) {
|
||||
return SupervisorWithNewProcessFunc(bundle, func(ctx context.Context) (Process, io.ReadWriteCloser, error) {
|
||||
executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable))
|
||||
if strings.HasPrefix(executable, "..") {
|
||||
return nil, nil, fmt.Errorf("invalid backend executable")
|
||||
}
|
||||
return NewProcess(ctx, filepath.Join(bundle.Path, executable))
|
||||
})
|
||||
}
|
||||
|
||||
func SupervisorWithNewProcessFunc(bundle *model.BundleInfo, newProcess func(context.Context) (Process, io.ReadWriteCloser, error)) (plugin.Supervisor, error) {
|
||||
if bundle.Manifest == nil {
|
||||
return nil, fmt.Errorf("no manifest available")
|
||||
} else if bundle.Manifest.Backend == nil || bundle.Manifest.Backend.Executable == "" {
|
||||
return nil, fmt.Errorf("no backend executable specified")
|
||||
}
|
||||
executable := filepath.Clean(filepath.Join(".", bundle.Manifest.Backend.Executable))
|
||||
if strings.HasPrefix(executable, "..") {
|
||||
return nil, fmt.Errorf("invalid backend executable")
|
||||
}
|
||||
return &Supervisor{pluginId: bundle.Manifest.Id, newProcess: newProcess}, nil
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/plugin/rpcplugin/rpcplugintest"
|
||||
)
|
||||
|
||||
func TestSupervisorProvider(t *testing.T) {
|
||||
rpcplugintest.TestSupervisorProvider(t, SupervisorProvider)
|
||||
}
|
||||
@@ -1,13 +1,99 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
// Supervisor provides the interface for an object that controls the execution of a plugin. This
|
||||
// type is only relevant to the server, and isn't used by the plugins themselves.
|
||||
type Supervisor interface {
|
||||
Start(API) error
|
||||
Wait() error
|
||||
Stop() error
|
||||
Hooks() Hooks
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
)
|
||||
|
||||
type Supervisor struct {
|
||||
pluginId string
|
||||
client *plugin.Client
|
||||
hooks Hooks
|
||||
implemented [TotalHooksId]bool
|
||||
}
|
||||
|
||||
func NewSupervisor(pluginInfo *model.BundleInfo, parentLogger *mlog.Logger, apiImpl API) (*Supervisor, error) {
|
||||
supervisor := Supervisor{}
|
||||
|
||||
wrappedLogger := pluginInfo.WrapLogger(parentLogger)
|
||||
|
||||
hclogAdaptedLogger := &HclogAdapter{
|
||||
wrappedLogger: wrappedLogger,
|
||||
extrasKey: "wrapped_extras",
|
||||
}
|
||||
|
||||
pluginMap := map[string]plugin.Plugin{
|
||||
"hooks": &HooksPlugin{
|
||||
log: wrappedLogger,
|
||||
apiImpl: apiImpl,
|
||||
},
|
||||
}
|
||||
|
||||
executable := filepath.Clean(filepath.Join(".", pluginInfo.Manifest.Backend.Executable))
|
||||
if strings.HasPrefix(executable, "..") {
|
||||
return nil, fmt.Errorf("invalid backend executable")
|
||||
}
|
||||
executable = filepath.Join(pluginInfo.Path, executable)
|
||||
|
||||
supervisor.client = plugin.NewClient(&plugin.ClientConfig{
|
||||
HandshakeConfig: Handshake,
|
||||
Plugins: pluginMap,
|
||||
Cmd: exec.Command(executable),
|
||||
SyncStdout: os.Stdout,
|
||||
SyncStderr: os.Stdout,
|
||||
Logger: hclogAdaptedLogger,
|
||||
StartTimeout: time.Second * 3,
|
||||
})
|
||||
|
||||
rpcClient, err := supervisor.client.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
raw, err := rpcClient.Dispense("hooks")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supervisor.hooks = raw.(Hooks)
|
||||
|
||||
if impl, err := supervisor.hooks.Implemented(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for _, hookName := range impl {
|
||||
if hookId, ok := HookNameToId[hookName]; ok {
|
||||
supervisor.implemented[hookId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = supervisor.Hooks().OnActivate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &supervisor, nil
|
||||
}
|
||||
|
||||
func (sup *Supervisor) Shutdown() {
|
||||
sup.client.Kill()
|
||||
}
|
||||
|
||||
func (sup *Supervisor) Hooks() Hooks {
|
||||
return sup.hooks
|
||||
}
|
||||
|
||||
func (sup *Supervisor) Implements(hookId int) bool {
|
||||
return sup.implemented[hookId]
|
||||
}
|
||||
|
||||
148
plugin/supervisor_test.go
Обычный файл
148
plugin/supervisor_test.go
Обычный файл
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/plugin/plugintest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSupervisor(t *testing.T) {
|
||||
for name, f := range map[string]func(*testing.T){
|
||||
"Supervisor": testSupervisor,
|
||||
"Supervisor_InvalidExecutablePath": testSupervisor_InvalidExecutablePath,
|
||||
"Supervisor_NonExistentExecutablePath": testSupervisor_NonExistentExecutablePath,
|
||||
"Supervisor_StartTimeout": testSupervisor_StartTimeout,
|
||||
} {
|
||||
t.Run(name, f)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
func testSupervisor(t *testing.T) {
|
||||
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"
|
||||
)
|
||||
|
||||
type MyPlugin struct {
|
||||
plugin.MattermostPlugin
|
||||
}
|
||||
|
||||
func main() {
|
||||
plugin.ClientMain(&MyPlugin{})
|
||||
}
|
||||
`, backend)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend.exe"}}`), 0600)
|
||||
|
||||
bundle := model.BundleInfoForPath(dir)
|
||||
var api plugintest.API
|
||||
api.On("LoadPluginConfiguration", mock.Anything).Return(nil)
|
||||
log := mlog.NewLogger(&mlog.LoggerConfiguration{
|
||||
EnableConsole: true,
|
||||
ConsoleJson: true,
|
||||
ConsoleLevel: "error",
|
||||
EnableFile: false,
|
||||
})
|
||||
supervisor, err := NewSupervisor(bundle, log, &api)
|
||||
require.NoError(t, err)
|
||||
supervisor.Shutdown()
|
||||
}
|
||||
|
||||
func testSupervisor_InvalidExecutablePath(t *testing.T) {
|
||||
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)
|
||||
log := mlog.NewLogger(&mlog.LoggerConfiguration{
|
||||
EnableConsole: true,
|
||||
ConsoleJson: true,
|
||||
ConsoleLevel: "error",
|
||||
EnableFile: false,
|
||||
})
|
||||
supervisor, err := NewSupervisor(bundle, log, nil)
|
||||
assert.Nil(t, supervisor)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func testSupervisor_NonExistentExecutablePath(t *testing.T) {
|
||||
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)
|
||||
log := mlog.NewLogger(&mlog.LoggerConfiguration{
|
||||
EnableConsole: true,
|
||||
ConsoleJson: true,
|
||||
ConsoleLevel: "error",
|
||||
EnableFile: false,
|
||||
})
|
||||
supervisor, err := NewSupervisor(bundle, log, nil)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, supervisor)
|
||||
}
|
||||
|
||||
// If plugin development goes really wrong, let's make sure plugin activation won't block forever.
|
||||
func testSupervisor_StartTimeout(t *testing.T) {
|
||||
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)
|
||||
log := mlog.NewLogger(&mlog.LoggerConfiguration{
|
||||
EnableConsole: true,
|
||||
ConsoleJson: true,
|
||||
ConsoleLevel: "error",
|
||||
EnableFile: false,
|
||||
})
|
||||
supervisor, err := NewSupervisor(bundle, log, nil)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, supervisor)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user