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 +}