MM-10702 Moving plugins to use hashicorp go-plugin. (#8978)

* Moving plugins to use hashicorp go-plugin.

* Tweaks from feedback.
Этот коммит содержится в:
Christopher Speller
2018-06-25 12:33:13 -07:00
коммит произвёл GitHub
родитель ecefa6cdd1
Коммит 1e5c432e10
303 изменённых файлов: 52150 добавлений и 9404 удалений

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

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

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

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

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

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

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

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

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

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