implemented plugin endpoints as shared objects

* the plugin mechanism is an optional extension to the REST
  API. As the plugin mechanism depends on gopher-lua (and a bunch of
  gopher-lua plugins), it adds quite some dependencies to the project.
  Since most of the REST API users won't need the plugin mechanism, it
  makes sense to move that functionality (including all the
  dependencies) to a dedicated shared object, which gets loaded when
  needed.
This commit is contained in:
Bernhard B
2025-02-03 23:16:09 +01:00
parent a02b1ce8eb
commit 49f121a249
5 changed files with 132 additions and 86 deletions

View File

@@ -10,7 +10,6 @@ import (
"strings"
"sync"
"time"
"io"
"github.com/gabriel-vasile/mimetype"
"github.com/gin-gonic/gin"
@@ -20,11 +19,6 @@ import (
"github.com/bbernhard/signal-cli-rest-api/client"
ds "github.com/bbernhard/signal-cli-rest-api/datastructs"
utils "github.com/bbernhard/signal-cli-rest-api/utils"
"github.com/yuin/gopher-lua"
"github.com/cjoudrey/gluahttp"
"layeh.com/gopher-luar"
luajson "layeh.com/gopher-json"
)
const (
@@ -2172,69 +2166,3 @@ func (a *Api) ListContacts(c *gin.Context) {
c.JSON(200, contacts)
}
type PluginInputData struct {
Params map[string]string
Payload string
}
type PluginOutputData struct {
payload string
httpStatusCode int
}
func (p *PluginOutputData) SetPayload(payload string) {
p.payload = payload
}
func (p *PluginOutputData) Payload() string {
return p.payload
}
func (p *PluginOutputData) SetHttpStatusCode(httpStatusCode int) {
p.httpStatusCode = httpStatusCode
}
func (p *PluginOutputData) HttpStatusCode() int {
return p.httpStatusCode
}
func (a *Api) ExecutePlugin(c *gin.Context, pluginConfig utils.PluginConfig) {
jsonData, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - invalid input data"})
log.Error(err.Error())
return
}
pluginInputData := &PluginInputData{
Params: make(map[string]string),
Payload: string(jsonData),
}
pluginOutputData := &PluginOutputData{
payload: "",
httpStatusCode: 200,
}
parts := strings.Split(pluginConfig.Endpoint, "/")
for _, part := range parts {
if strings.HasPrefix(part, ":") {
paramName := strings.TrimPrefix(part, ":")
pluginInputData.Params[paramName] = c.Param(paramName)
}
}
l := lua.NewState()
l.SetGlobal("pluginInputData", luar.New(l, pluginInputData))
l.SetGlobal("pluginOutputData", luar.New(l, pluginOutputData))
l.PreloadModule("http", gluahttp.NewHttpModule(&http.Client{}).Loader)
luajson.Preload(l)
defer l.Close()
if err := l.DoFile(pluginConfig.ScriptPath); err != nil {
c.JSON(400, Error{Msg: err.Error()})
return
}
c.JSON(pluginOutputData.HttpStatusCode(), pluginOutputData.Payload())
}

View File

@@ -16,6 +16,7 @@ import (
"net/http"
"os"
"strconv"
"plugin"
)
// @title Signal Cli REST API
@@ -62,15 +63,6 @@ import (
// @schemes http
// @BasePath /
func PluginHandler(api *api.Api, pluginConfig utils.PluginConfig) gin.HandlerFunc {
fn := func(c *gin.Context) {
api.ExecutePlugin(c, pluginConfig)
}
return gin.HandlerFunc(fn)
}
func main() {
signalCliConfig := flag.String("signal-cli-config", "/home/.local/share/signal-cli/", "Config directory where signal-cli config is stored")
attachmentTmpDir := flag.String("attachment-tmp-dir", "/tmp/", "Attachment tmp directory")
@@ -293,6 +285,22 @@ func main() {
}
if utils.GetEnv("ENABLE_PLUGINS", "false") == "true" {
signalCliRestApiPluginSharedObjDir := utils.GetEnv("SIGNAL_CLI_REST_API_PLUGIN_SHARED_OBJ_DIR", "")
sharedObj, err := plugin.Open(signalCliRestApiPluginSharedObjDir + "signal-cli-rest-api_plugin_loader.so")
if err != nil {
log.Fatal("Couldn't load shared object: ", err)
}
pluginHandlerSymbol, err := sharedObj.Lookup("PluginHandler")
if err != nil {
log.Fatal("Couldn't get PluginHandler: ", err)
}
pluginHandler, ok := pluginHandlerSymbol.(utils.PluginHandler)
if !ok {
log.Fatal("Couldn't cast PluginHandler")
}
plugins := v1.Group("/plugins")
{
pluginConfigs := utils.NewPluginConfigs()
@@ -303,13 +311,13 @@ func main() {
for _, pluginConfig := range pluginConfigs.Configs {
if pluginConfig.Method == "GET" {
plugins.GET(pluginConfig.Endpoint, PluginHandler(api, pluginConfig))
plugins.GET(pluginConfig.Endpoint, pluginHandler.ExecutePlugin(pluginConfig))
} else if pluginConfig.Method == "POST" {
plugins.POST(pluginConfig.Endpoint, PluginHandler(api, pluginConfig))
plugins.POST(pluginConfig.Endpoint, pluginHandler.ExecutePlugin(pluginConfig))
} else if pluginConfig.Method == "DELETE" {
plugins.DELETE(pluginConfig.Endpoint, PluginHandler(api, pluginConfig))
plugins.DELETE(pluginConfig.Endpoint, pluginHandler.ExecutePlugin(pluginConfig))
} else if pluginConfig.Method == "PUT" {
plugins.PUT(pluginConfig.Endpoint, PluginHandler(api, pluginConfig))
plugins.PUT(pluginConfig.Endpoint, pluginHandler.ExecutePlugin(pluginConfig))
}
}
}

95
src/plugin_loader.go Normal file
View File

@@ -0,0 +1,95 @@
package main
import (
"github.com/yuin/gopher-lua"
"github.com/cjoudrey/gluahttp"
"layeh.com/gopher-luar"
luajson "layeh.com/gopher-json"
"github.com/gin-gonic/gin"
"io"
log "github.com/sirupsen/logrus"
"github.com/bbernhard/signal-cli-rest-api/utils"
"github.com/bbernhard/signal-cli-rest-api/api"
"strings"
"net/http"
)
type PluginInputData struct {
Params map[string]string
Payload string
}
type PluginOutputData struct {
payload string
httpStatusCode int
}
func (p *PluginOutputData) SetPayload(payload string) {
p.payload = payload
}
func (p *PluginOutputData) Payload() string {
return p.payload
}
func (p *PluginOutputData) SetHttpStatusCode(httpStatusCode int) {
p.httpStatusCode = httpStatusCode
}
func (p *PluginOutputData) HttpStatusCode() int {
return p.httpStatusCode
}
func execPlugin(c *gin.Context, pluginConfig utils.PluginConfig) {
jsonData, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(400, api.Error{Msg: "Couldn't process request - invalid input data"})
log.Error(err.Error())
return
}
pluginInputData := &PluginInputData{
Params: make(map[string]string),
Payload: string(jsonData),
}
pluginOutputData := &PluginOutputData{
payload: "",
httpStatusCode: 200,
}
parts := strings.Split(pluginConfig.Endpoint, "/")
for _, part := range parts {
if strings.HasPrefix(part, ":") {
paramName := strings.TrimPrefix(part, ":")
pluginInputData.Params[paramName] = c.Param(paramName)
}
}
l := lua.NewState()
l.SetGlobal("pluginInputData", luar.New(l, pluginInputData))
l.SetGlobal("pluginOutputData", luar.New(l, pluginOutputData))
l.PreloadModule("http", gluahttp.NewHttpModule(&http.Client{}).Loader)
luajson.Preload(l)
defer l.Close()
if err := l.DoFile(pluginConfig.ScriptPath); err != nil {
c.JSON(400, api.Error{Msg: err.Error()})
return
}
c.JSON(pluginOutputData.HttpStatusCode(), pluginOutputData.Payload())
}
type plugHandler struct {
}
func (p plugHandler) ExecutePlugin(pluginConfig utils.PluginConfig) gin.HandlerFunc {
fn := func(c *gin.Context) {
execPlugin(c, pluginConfig)
}
return gin.HandlerFunc(fn)
}
//exported
var PluginHandler plugHandler

View File

@@ -0,0 +1,9 @@
package utils
import (
"github.com/gin-gonic/gin"
)
type PluginHandler interface {
ExecutePlugin(pluginConfig PluginConfig) gin.HandlerFunc
}