From 49f121a2497ef0f5ee4c25a415bad07f066bf63c Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Mon, 3 Feb 2025 23:16:09 +0100 Subject: [PATCH] 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. --- Dockerfile | 8 +++- src/api/api.go | 72 ---------------------------- src/main.go | 34 ++++++++----- src/plugin_loader.go | 95 +++++++++++++++++++++++++++++++++++++ src/utils/plugin_handler.go | 9 ++++ 5 files changed, 132 insertions(+), 86 deletions(-) create mode 100644 src/plugin_loader.go create mode 100644 src/utils/plugin_handler.go diff --git a/Dockerfile b/Dockerfile index f33237f..3adb6d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -136,13 +136,17 @@ COPY src/scripts /tmp/signal-cli-rest-api-src/scripts COPY src/main.go /tmp/signal-cli-rest-api-src/ COPY src/go.mod /tmp/signal-cli-rest-api-src/ COPY src/go.sum /tmp/signal-cli-rest-api-src/ +COPY src/plugin_loader.go /tmp/signal-cli-rest-api-src/ # build signal-cli-rest-api -RUN cd /tmp/signal-cli-rest-api-src && swag init && go test ./client -v && go build +RUN cd /tmp/signal-cli-rest-api-src && swag init && go test ./client -v && go build -o signal-cli-rest-api main.go # build supervisorctl_config_creator RUN cd /tmp/signal-cli-rest-api-src/scripts && go build -o jsonrpc2-helper +# build plugin_loader +RUN cd /tmp/signal-cli-rest-api-src && go build -buildmode=plugin -o signal-cli-rest-api_plugin_loader.so plugin_loader.go + # Start a fresh container for release container # eclipse-temurin doesn't provide a OpenJDK 21 image for armv7 (see https://github.com/adoptium/containers/issues/502). Until this @@ -159,6 +163,7 @@ ARG SIGNAL_CLI_VERSION ARG BUILD_VERSION_ARG ENV BUILD_VERSION=$BUILD_VERSION_ARG +ENV SIGNAL_CLI_REST_API_PLUGIN_SHARED_OBJ_DIR=/usr/bin/ RUN dpkg-reconfigure debconf --frontend=noninteractive \ && apt-get -qq update \ @@ -169,6 +174,7 @@ COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/signal-cli-rest-api /usr COPY --from=buildcontainer /opt/signal-cli-${SIGNAL_CLI_VERSION} /opt/signal-cli-${SIGNAL_CLI_VERSION} COPY --from=buildcontainer /tmp/signal-cli-${SIGNAL_CLI_VERSION}-source/build/native/nativeCompile/signal-cli /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/scripts/jsonrpc2-helper /usr/bin/jsonrpc2-helper +COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/signal-cli-rest-api_plugin_loader.so /usr/bin/signal-cli-rest-api_plugin_loader.so COPY entrypoint.sh /entrypoint.sh diff --git a/src/api/api.go b/src/api/api.go index 6914c1e..cbda348 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -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()) -} diff --git a/src/main.go b/src/main.go index b5f3510..6b5cce8 100644 --- a/src/main.go +++ b/src/main.go @@ -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)) } } } diff --git a/src/plugin_loader.go b/src/plugin_loader.go new file mode 100644 index 0000000..a182680 --- /dev/null +++ b/src/plugin_loader.go @@ -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 diff --git a/src/utils/plugin_handler.go b/src/utils/plugin_handler.go new file mode 100644 index 0000000..d2931b6 --- /dev/null +++ b/src/utils/plugin_handler.go @@ -0,0 +1,9 @@ +package utils + +import ( + "github.com/gin-gonic/gin" +) + +type PluginHandler interface { + ExecutePlugin(pluginConfig PluginConfig) gin.HandlerFunc +}