From 1e5c432e1029601a664454388ae366ef69618d62 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Mon, 25 Jun 2018 12:33:13 -0700 Subject: MM-10702 Moving plugins to use hashicorp go-plugin. (#8978) * Moving plugins to use hashicorp go-plugin. * Tweaks from feedback. --- plugin/rpcplugin/api.go | 718 ------------------------ plugin/rpcplugin/api_test.go | 300 ---------- plugin/rpcplugin/hooks.go | 398 ------------- plugin/rpcplugin/hooks_test.go | 237 -------- plugin/rpcplugin/http.go | 91 --- plugin/rpcplugin/http_test.go | 61 -- plugin/rpcplugin/io.go | 63 --- plugin/rpcplugin/ipc.go | 31 - plugin/rpcplugin/ipc_test.go | 63 --- plugin/rpcplugin/main.go | 47 -- plugin/rpcplugin/main_test.go | 63 --- plugin/rpcplugin/muxer.go | 264 --------- plugin/rpcplugin/muxer_test.go | 197 ------- plugin/rpcplugin/process.go | 26 - plugin/rpcplugin/process_test.go | 60 -- plugin/rpcplugin/process_unix.go | 48 -- plugin/rpcplugin/process_windows.go | 648 --------------------- plugin/rpcplugin/rpcplugintest/rpcplugintest.go | 26 - plugin/rpcplugin/rpcplugintest/supervisor.go | 312 ---------- plugin/rpcplugin/sandbox/main_test.go | 18 - plugin/rpcplugin/sandbox/sandbox.go | 34 -- plugin/rpcplugin/sandbox/sandbox_linux.go | 488 ---------------- plugin/rpcplugin/sandbox/sandbox_linux_test.go | 159 ------ plugin/rpcplugin/sandbox/sandbox_other.go | 22 - plugin/rpcplugin/sandbox/sandbox_test.go | 25 - plugin/rpcplugin/sandbox/seccomp_linux.go | 178 ------ plugin/rpcplugin/sandbox/seccomp_linux_amd64.go | 301 ---------- plugin/rpcplugin/sandbox/seccomp_linux_other.go | 10 - plugin/rpcplugin/sandbox/seccomp_linux_test.go | 210 ------- plugin/rpcplugin/sandbox/supervisor.go | 33 -- plugin/rpcplugin/sandbox/supervisor_test.go | 18 - plugin/rpcplugin/supervisor.go | 176 ------ plugin/rpcplugin/supervisor_test.go | 14 - 33 files changed, 5339 deletions(-) delete mode 100644 plugin/rpcplugin/api.go delete mode 100644 plugin/rpcplugin/api_test.go delete mode 100644 plugin/rpcplugin/hooks.go delete mode 100644 plugin/rpcplugin/hooks_test.go delete mode 100644 plugin/rpcplugin/http.go delete mode 100644 plugin/rpcplugin/http_test.go delete mode 100644 plugin/rpcplugin/io.go delete mode 100644 plugin/rpcplugin/ipc.go delete mode 100644 plugin/rpcplugin/ipc_test.go delete mode 100644 plugin/rpcplugin/main.go delete mode 100644 plugin/rpcplugin/main_test.go delete mode 100644 plugin/rpcplugin/muxer.go delete mode 100644 plugin/rpcplugin/muxer_test.go delete mode 100644 plugin/rpcplugin/process.go delete mode 100644 plugin/rpcplugin/process_test.go delete mode 100644 plugin/rpcplugin/process_unix.go delete mode 100644 plugin/rpcplugin/process_windows.go delete mode 100644 plugin/rpcplugin/rpcplugintest/rpcplugintest.go delete mode 100644 plugin/rpcplugin/rpcplugintest/supervisor.go delete mode 100644 plugin/rpcplugin/sandbox/main_test.go delete mode 100644 plugin/rpcplugin/sandbox/sandbox.go delete mode 100644 plugin/rpcplugin/sandbox/sandbox_linux.go delete mode 100644 plugin/rpcplugin/sandbox/sandbox_linux_test.go delete mode 100644 plugin/rpcplugin/sandbox/sandbox_other.go delete mode 100644 plugin/rpcplugin/sandbox/sandbox_test.go delete mode 100644 plugin/rpcplugin/sandbox/seccomp_linux.go delete mode 100644 plugin/rpcplugin/sandbox/seccomp_linux_amd64.go delete mode 100644 plugin/rpcplugin/sandbox/seccomp_linux_other.go delete mode 100644 plugin/rpcplugin/sandbox/seccomp_linux_test.go delete mode 100644 plugin/rpcplugin/sandbox/supervisor.go delete mode 100644 plugin/rpcplugin/sandbox/supervisor_test.go delete mode 100644 plugin/rpcplugin/supervisor.go delete mode 100644 plugin/rpcplugin/supervisor_test.go (limited to 'plugin/rpcplugin') diff --git a/plugin/rpcplugin/api.go b/plugin/rpcplugin/api.go deleted file mode 100644 index c81bbb7c5..000000000 --- a/plugin/rpcplugin/api.go +++ /dev/null @@ -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{}{}) -} diff --git a/plugin/rpcplugin/api_test.go b/plugin/rpcplugin/api_test.go deleted file mode 100644 index 04d8e5d86..000000000 --- a/plugin/rpcplugin/api_test.go +++ /dev/null @@ -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) - }) -} diff --git a/plugin/rpcplugin/hooks.go b/plugin/rpcplugin/hooks.go deleted file mode 100644 index 6af98873a..000000000 --- a/plugin/rpcplugin/hooks.go +++ /dev/null @@ -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 -} diff --git a/plugin/rpcplugin/hooks_test.go b/plugin/rpcplugin/hooks_test.go deleted file mode 100644 index a7bac982e..000000000 --- a/plugin/rpcplugin/hooks_test.go +++ /dev/null @@ -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()) - } -} diff --git a/plugin/rpcplugin/http.go b/plugin/rpcplugin/http.go deleted file mode 100644 index 72b1aa445..000000000 --- a/plugin/rpcplugin/http.go +++ /dev/null @@ -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), - } -} diff --git a/plugin/rpcplugin/http_test.go b/plugin/rpcplugin/http_test.go deleted file mode 100644 index afaaf7756..000000000 --- a/plugin/rpcplugin/http_test.go +++ /dev/null @@ -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")) -} diff --git a/plugin/rpcplugin/io.go b/plugin/rpcplugin/io.go deleted file mode 100644 index 44b89956c..000000000 --- a/plugin/rpcplugin/io.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package rpcplugin - -import ( - "bufio" - "encoding/binary" - "io" -) - -type rwc struct { - io.ReadCloser - io.WriteCloser -} - -func (rwc *rwc) Close() (err error) { - err = rwc.WriteCloser.Close() - if rerr := rwc.ReadCloser.Close(); err == nil { - err = rerr - } - return -} - -func NewReadWriteCloser(r io.ReadCloser, w io.WriteCloser) io.ReadWriteCloser { - return &rwc{r, w} -} - -type RemoteIOReader struct { - conn io.ReadWriteCloser -} - -func (r *RemoteIOReader) Read(b []byte) (int, error) { - var buf [10]byte - n := binary.PutVarint(buf[:], int64(len(b))) - if _, err := r.conn.Write(buf[:n]); err != nil { - return 0, err - } - return r.conn.Read(b) -} - -func (r *RemoteIOReader) Close() error { - return r.conn.Close() -} - -func ConnectIOReader(conn io.ReadWriteCloser) io.ReadCloser { - return &RemoteIOReader{conn} -} - -func ServeIOReader(r io.Reader, conn io.ReadWriteCloser) { - cr := bufio.NewReader(conn) - defer conn.Close() - buf := make([]byte, 32*1024) - for { - n, err := binary.ReadVarint(cr) - if err != nil { - break - } - if written, err := io.CopyBuffer(conn, io.LimitReader(r, n), buf); err != nil || written < n { - break - } - } -} diff --git a/plugin/rpcplugin/ipc.go b/plugin/rpcplugin/ipc.go deleted file mode 100644 index e8dd43c04..000000000 --- a/plugin/rpcplugin/ipc.go +++ /dev/null @@ -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 -} diff --git a/plugin/rpcplugin/ipc_test.go b/plugin/rpcplugin/ipc_test.go deleted file mode 100644 index 76699a11e..000000000 --- a/plugin/rpcplugin/ipc_test.go +++ /dev/null @@ -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()) -} diff --git a/plugin/rpcplugin/main.go b/plugin/rpcplugin/main.go deleted file mode 100644 index efb880605..000000000 --- a/plugin/rpcplugin/main.go +++ /dev/null @@ -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) -} diff --git a/plugin/rpcplugin/main_test.go b/plugin/rpcplugin/main_test.go deleted file mode 100644 index 06423106c..000000000 --- a/plugin/rpcplugin/main_test.go +++ /dev/null @@ -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()) -} diff --git a/plugin/rpcplugin/muxer.go b/plugin/rpcplugin/muxer.go deleted file mode 100644 index a7260c399..000000000 --- a/plugin/rpcplugin/muxer.go +++ /dev/null @@ -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 -} diff --git a/plugin/rpcplugin/muxer_test.go b/plugin/rpcplugin/muxer_test.go deleted file mode 100644 index 795a4fb1d..000000000 --- a/plugin/rpcplugin/muxer_test.go +++ /dev/null @@ -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]) -} diff --git a/plugin/rpcplugin/process.go b/plugin/rpcplugin/process.go deleted file mode 100644 index a795be133..000000000 --- a/plugin/rpcplugin/process.go +++ /dev/null @@ -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() -} diff --git a/plugin/rpcplugin/process_test.go b/plugin/rpcplugin/process_test.go deleted file mode 100644 index 8d1794293..000000000 --- a/plugin/rpcplugin/process_test.go +++ /dev/null @@ -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) -} diff --git a/plugin/rpcplugin/process_unix.go b/plugin/rpcplugin/process_unix.go deleted file mode 100644 index 142043cc6..000000000 --- a/plugin/rpcplugin/process_unix.go +++ /dev/null @@ -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) -} diff --git a/plugin/rpcplugin/process_windows.go b/plugin/rpcplugin/process_windows.go deleted file mode 100644 index 069f147c1..000000000 --- a/plugin/rpcplugin/process_windows.go +++ /dev/null @@ -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 -} diff --git a/plugin/rpcplugin/rpcplugintest/rpcplugintest.go b/plugin/rpcplugin/rpcplugintest/rpcplugintest.go deleted file mode 100644 index 185f741c1..000000000 --- a/plugin/rpcplugin/rpcplugintest/rpcplugintest.go +++ /dev/null @@ -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()) -} diff --git a/plugin/rpcplugin/rpcplugintest/supervisor.go b/plugin/rpcplugin/rpcplugintest/supervisor.go deleted file mode 100644 index f3ff847a2..000000000 --- a/plugin/rpcplugin/rpcplugintest/supervisor.go +++ /dev/null @@ -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()) -} diff --git a/plugin/rpcplugin/sandbox/main_test.go b/plugin/rpcplugin/sandbox/main_test.go deleted file mode 100644 index 4be4a42af..000000000 --- a/plugin/rpcplugin/sandbox/main_test.go +++ /dev/null @@ -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, - })) -} diff --git a/plugin/rpcplugin/sandbox/sandbox.go b/plugin/rpcplugin/sandbox/sandbox.go deleted file mode 100644 index 96eff02dd..000000000 --- a/plugin/rpcplugin/sandbox/sandbox.go +++ /dev/null @@ -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() -} diff --git a/plugin/rpcplugin/sandbox/sandbox_linux.go b/plugin/rpcplugin/sandbox/sandbox_linux.go deleted file mode 100644 index beb00995d..000000000 --- a/plugin/rpcplugin/sandbox/sandbox_linux.go +++ /dev/null @@ -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 -} diff --git a/plugin/rpcplugin/sandbox/sandbox_linux_test.go b/plugin/rpcplugin/sandbox/sandbox_linux_test.go deleted file mode 100644 index 2bcbf0c57..000000000 --- a/plugin/rpcplugin/sandbox/sandbox_linux_test.go +++ /dev/null @@ -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()) -} diff --git a/plugin/rpcplugin/sandbox/sandbox_other.go b/plugin/rpcplugin/sandbox/sandbox_other.go deleted file mode 100644 index 3889ecdcc..000000000 --- a/plugin/rpcplugin/sandbox/sandbox_other.go +++ /dev/null @@ -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") -} diff --git a/plugin/rpcplugin/sandbox/sandbox_test.go b/plugin/rpcplugin/sandbox/sandbox_test.go deleted file mode 100644 index e0149e28d..000000000 --- a/plugin/rpcplugin/sandbox/sandbox_test.go +++ /dev/null @@ -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()) - } -} diff --git a/plugin/rpcplugin/sandbox/seccomp_linux.go b/plugin/rpcplugin/sandbox/seccomp_linux.go deleted file mode 100644 index afe86e90a..000000000 --- a/plugin/rpcplugin/sandbox/seccomp_linux.go +++ /dev/null @@ -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 -} diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go b/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go deleted file mode 100644 index 7338ebbe0..000000000 --- a/plugin/rpcplugin/sandbox/seccomp_linux_amd64.go +++ /dev/null @@ -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}, -} diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_other.go b/plugin/rpcplugin/sandbox/seccomp_linux_other.go deleted file mode 100644 index 5573943cd..000000000 --- a/plugin/rpcplugin/sandbox/seccomp_linux_other.go +++ /dev/null @@ -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 diff --git a/plugin/rpcplugin/sandbox/seccomp_linux_test.go b/plugin/rpcplugin/sandbox/seccomp_linux_test.go deleted file mode 100644 index 46fe38fe0..000000000 --- a/plugin/rpcplugin/sandbox/seccomp_linux_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/plugin/rpcplugin/sandbox/supervisor.go b/plugin/rpcplugin/sandbox/supervisor.go deleted file mode 100644 index 0e63954fd..000000000 --- a/plugin/rpcplugin/sandbox/supervisor.go +++ /dev/null @@ -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)) - }) -} diff --git a/plugin/rpcplugin/sandbox/supervisor_test.go b/plugin/rpcplugin/sandbox/supervisor_test.go deleted file mode 100644 index 245185dd5..000000000 --- a/plugin/rpcplugin/sandbox/supervisor_test.go +++ /dev/null @@ -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) -} diff --git a/plugin/rpcplugin/supervisor.go b/plugin/rpcplugin/supervisor.go deleted file mode 100644 index 246747c89..000000000 --- a/plugin/rpcplugin/supervisor.go +++ /dev/null @@ -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 -} diff --git a/plugin/rpcplugin/supervisor_test.go b/plugin/rpcplugin/supervisor_test.go deleted file mode 100644 index 06c1fafeb..000000000 --- a/plugin/rpcplugin/supervisor_test.go +++ /dev/null @@ -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) -} -- cgit v1.2.3-1-g7c22