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

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

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
}