This commit is contained in:
parent
bf0aaaca8f
commit
94106bb82f
8 changed files with 430 additions and 1 deletions
12
plugins/api.go
Normal file
12
plugins/api.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package plugins
|
||||
|
||||
import lua "github.com/yuin/gopher-lua"
|
||||
|
||||
// TODO: Decide on the API made available to plugins
|
||||
// Everything has to be a function, assume no internal state
|
||||
// since the used lua state may vary between calls
|
||||
type linstromApi struct{}
|
||||
|
||||
func insertLinstromApiIntoState(l *lua.LState) error {
|
||||
panic("not implemented")
|
||||
}
|
62
plugins/init.go
Normal file
62
plugins/init.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
"github.com/PeerDB-io/gluabit32"
|
||||
"github.com/cjoudrey/gluahttp"
|
||||
"github.com/cosmotek/loguago"
|
||||
"github.com/kohkimakimoto/gluatemplate"
|
||||
gluajson "github.com/layeh/gopher-json"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yuin/gluare"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
|
||||
webshared "git.mstar.dev/mstar/linstrom/web/shared"
|
||||
)
|
||||
|
||||
// "github.com/CuberL/glua-async" // For async support, see module readme for how to
|
||||
// Use "github.com/layeh/gopher-luar" for implementing interface
|
||||
|
||||
// Create a new "blank" lua state for future use.
|
||||
// Each created state has to be properly closed.
|
||||
// A "blank" state has the base libraries preloaded and is technically ready to use.
|
||||
// Plugin specific functions and data is not yet included
|
||||
func NewBlankState() (*lua.LState, error) {
|
||||
l := lua.NewState()
|
||||
for _, pair := range []struct {
|
||||
n string
|
||||
f lua.LGFunction
|
||||
}{
|
||||
{lua.LoadLibName, lua.OpenPackage}, // Must always be first
|
||||
{lua.BaseLibName, lua.OpenBase},
|
||||
{lua.TabLibName, lua.OpenTable},
|
||||
// {lua.IoLibName, lua.OpenIo},
|
||||
{lua.OsLibName, lua.OpenOs},
|
||||
{lua.StringLibName, lua.OpenString},
|
||||
{lua.MathLibName, lua.OpenMath},
|
||||
// {lua.DebugLibName, lua.OpenDebug},
|
||||
{lua.ChannelLibName, lua.OpenChannel},
|
||||
{lua.CoroutineLibName, lua.OpenCoroutine},
|
||||
{"bit32", gluabit32.Loader},
|
||||
{"json", gluajson.Loader},
|
||||
{"log", loguago.NewLogger(log.Logger).Loader}, // TODO: Decide if used logger should be passed in via context
|
||||
{"http", gluahttp.NewHttpModule(&webshared.RequestClient).Loader}, // TODO: Modify this to support signing
|
||||
{"re", gluare.Loader},
|
||||
{"texttemplate", gluatemplate.Loader}, // TODO: Copy this lib, but modify to use html/template instead
|
||||
} {
|
||||
if err := l.CallByParam(lua.P{
|
||||
Fn: l.NewFunction(pair.f),
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}, lua.LString(pair.n)); err != nil {
|
||||
return nil, other.Error(
|
||||
"plugins",
|
||||
fmt.Sprintf("Failed to preload lua library %q into state", pair.n),
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
}
|
67
plugins/pluginHolder.go
Normal file
67
plugins/pluginHolder.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package plugins
|
||||
|
||||
import lua "github.com/yuin/gopher-lua"
|
||||
|
||||
/*
|
||||
Plugin hooks:
|
||||
- Pre local message
|
||||
- Post local message
|
||||
- Pre external message (received but not processed by default stuff)
|
||||
- Post external message (received, internal processing done)
|
||||
- Post new local account
|
||||
- Pre new remote account discovered
|
||||
- Post new remote account discovered
|
||||
*/
|
||||
|
||||
type pluginHolder struct {
|
||||
meta *PluginMetadata
|
||||
table *lua.LTable
|
||||
|
||||
// ---- Section hooks
|
||||
|
||||
postLocalMessage *lua.LFunction
|
||||
postExternalMessage *lua.LFunction
|
||||
postNewLocalAccount *lua.LFunction
|
||||
postRemoteAccountDiscovered *lua.LFunction
|
||||
// preLocalMessage *lua.LFunction
|
||||
// preExternalMessage *lua.LFunction // Might not want this
|
||||
// preRemoteAccountDiscovered *lua.LFunction
|
||||
}
|
||||
|
||||
type PluginMetadata struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
// Don't load major and minor versions from meta file, calc from full version instead
|
||||
Major int `json:"-"`
|
||||
Minor int `json:"-"`
|
||||
Source string `json:"source"`
|
||||
License string `json:"license"`
|
||||
Hashes map[string]string `json:"hashes"`
|
||||
EntryFile string `json:"entry"`
|
||||
HookNames map[string]string `json:"hook_names"`
|
||||
}
|
||||
|
||||
const (
|
||||
HookPostLocalMessage = "post-local-message"
|
||||
HookPostExternalMessage = "post-external-message"
|
||||
HookPostNewLocalAccount = "post-new-local-account"
|
||||
HookPostRemoteAccountDiscovered = "post-remote-account-discovered"
|
||||
)
|
||||
|
||||
// TODO: Figure out signature
|
||||
func (p *pluginHolder) PostLocalMessage(l *lua.LState) error {
|
||||
// Only act on the hook if a hook exists
|
||||
if p.postLocalMessage != nil {
|
||||
err := l.CallByParam(lua.P{
|
||||
Fn: p.postLocalMessage,
|
||||
NRet: 1, // TODO: Adjust based on signature
|
||||
Protect: true,
|
||||
}) // TODO: Translate signature arguments into LValues and include here
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: Translate args on stack back into whats expected from the signature
|
||||
panic("not implemented")
|
||||
}
|
||||
return nil
|
||||
}
|
200
plugins/pluginLoader.go
Normal file
200
plugins/pluginLoader.go
Normal file
|
@ -0,0 +1,200 @@
|
|||
package plugins
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"git.mstar.dev/mstar/goutils/other"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"github.com/yuin/gopher-lua/parse"
|
||||
)
|
||||
|
||||
/*
|
||||
Plugin structure expectation:
|
||||
- meta.json for plugin metadata:
|
||||
- version
|
||||
- name
|
||||
- source location (where the plugin can be sourced from)
|
||||
- license
|
||||
- file hashes (every file in the plugin dir, sha256, hex encoded)
|
||||
- entry file name
|
||||
- Hook to table key name list (which function in the returned table is for what hook)
|
||||
- Entry script returns a table
|
||||
*/
|
||||
|
||||
const PluginMetadataFileName = "meta.json"
|
||||
|
||||
var (
|
||||
ErrNoHashForFile = errors.New("no hash for file in the metadata")
|
||||
ErrInvalidHash = errors.New("hashes don't match")
|
||||
ErrMissingFile = errors.New("a required file is missing")
|
||||
ErrInvalidEntryReturnValue = errors.New(
|
||||
"entry script returned an invalid value. Must be a table",
|
||||
)
|
||||
)
|
||||
|
||||
var compCache = map[string]*lua.FunctionProto{}
|
||||
var compCacheLock = sync.RWMutex{}
|
||||
|
||||
// Load the plugin located in a provided folder into the given state.
|
||||
// Validates the data, compiles it for future reuse and links up the hooks
|
||||
func LoadPlugin(location string, lState *lua.LState) (*pluginHolder, error) {
|
||||
metaFileData, err := os.ReadFile(location + "/" + PluginMetadataFileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata := PluginMetadata{}
|
||||
err = json.Unmarshal(metaFileData, &metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compCacheLock.RLock()
|
||||
precompiled, ok := compCache[metadata.Name+metadata.Version]
|
||||
compCacheLock.RUnlock()
|
||||
if !ok {
|
||||
// This version of a plugin not in the cache yet, validate the plugin dir, compile it and add it to the cache
|
||||
precompiled, err = compilePluginDir(&metadata, location)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compCacheLock.Lock()
|
||||
compCache[metadata.Name+metadata.Version] = precompiled
|
||||
compCacheLock.Unlock()
|
||||
}
|
||||
lFunc := lState.NewFunctionFromProto(precompiled)
|
||||
lState.Push(lFunc)
|
||||
if err = lState.PCall(0, lua.MultRet, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return generatePluginHolder(lState, &metadata)
|
||||
}
|
||||
|
||||
// Compiles a given lua file for future use
|
||||
func compileFile(filePath string) (*lua.FunctionProto, error) {
|
||||
file, err := os.Open(filePath)
|
||||
defer func() { _ = file.Close() }()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader := bufio.NewReader(file)
|
||||
chunk, err := parse.Parse(reader, filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proto, err := lua.Compile(chunk, filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return proto, nil
|
||||
}
|
||||
|
||||
// Validates the given plugin dir and compiles it for future use
|
||||
func compilePluginDir(metadata *PluginMetadata, location string) (*lua.FunctionProto, error) {
|
||||
if err := checkDir(location, "", metadata.Hashes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entryFileName := path.Join(location, metadata.EntryFile)
|
||||
if _, err := os.Stat(entryFileName); err != nil {
|
||||
return nil, other.Error("plugins", fmt.Sprintf("missing entry file %s", entryFileName), err)
|
||||
}
|
||||
compiled, err := compileFile(entryFileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return compiled, nil
|
||||
}
|
||||
|
||||
// Ensures that all files in the given root directory (plus suffix) match the expected hashes provided in the metadata
|
||||
func checkDir(rootPath string, suffix string, expectedHashes map[string]string) error {
|
||||
rootEntries, err := os.ReadDir(path.Join(rootPath, suffix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range rootEntries {
|
||||
if suffix == "" && entry.Name() == PluginMetadataFileName {
|
||||
continue
|
||||
}
|
||||
if entry.IsDir() {
|
||||
if err = checkDir(rootPath, path.Join(suffix, entry.Name()), expectedHashes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fileName := path.Join(rootPath, suffix, entry.Name())
|
||||
expectedHash, ok := expectedHashes[fileName]
|
||||
if !ok {
|
||||
return other.Error(
|
||||
"plugins",
|
||||
fmt.Sprintf("No hash found in metadata for file %s", fileName),
|
||||
ErrNoHashForFile,
|
||||
)
|
||||
}
|
||||
f, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
if err = checkHash(f, expectedHash); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checks if a hash matches the given file
|
||||
func checkHash(file *os.File, expected string) error {
|
||||
hasher := sha256.New()
|
||||
_, err := io.Copy(hasher, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sum := hasher.Sum([]byte{})
|
||||
if expected != hex.EncodeToString(sum) {
|
||||
return ErrInvalidHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Properly inserts the plugin into the state's global context.
|
||||
// Also gets links to all functions found that are named as hook in the metadata.
|
||||
// Assumptions: Called after the entry file has been run, but before any other operations
|
||||
func generatePluginHolder(l *lua.LState, meta *PluginMetadata) (*pluginHolder, error) {
|
||||
pluginTable, ok := l.Get(1).(*lua.LTable)
|
||||
if !ok || pluginTable.MaxN() != 0 {
|
||||
return nil, ErrInvalidEntryReturnValue
|
||||
}
|
||||
l.SetGlobal(meta.Name, pluginTable)
|
||||
pMap := lTableToMap(pluginTable)
|
||||
holder := pluginHolder{
|
||||
meta: meta,
|
||||
table: pluginTable,
|
||||
}
|
||||
if postLocalMessageHookName, ok := meta.HookNames[HookPostLocalMessage]; ok {
|
||||
if lV, ok := pMap[lua.LString(postLocalMessageHookName)].(*lua.LFunction); ok {
|
||||
holder.postLocalMessage = lV
|
||||
}
|
||||
}
|
||||
if postExternalMessageHookName, ok := meta.HookNames[HookPostExternalMessage]; ok {
|
||||
if lV, ok := pMap[lua.LString(postExternalMessageHookName)].(*lua.LFunction); ok {
|
||||
holder.postExternalMessage = lV
|
||||
}
|
||||
}
|
||||
if postNewLocalAccountName, ok := meta.HookNames[HookPostNewLocalAccount]; ok {
|
||||
if lV, ok := pMap[lua.LString(postNewLocalAccountName)].(*lua.LFunction); ok {
|
||||
holder.postNewLocalAccount = lV
|
||||
}
|
||||
}
|
||||
if postRemoteAccountDiscoveredName, ok := meta.HookNames[HookPostRemoteAccountDiscovered]; ok {
|
||||
if lV, ok := pMap[lua.LString(postRemoteAccountDiscoveredName)].(*lua.LFunction); ok {
|
||||
holder.postRemoteAccountDiscovered = lV
|
||||
}
|
||||
}
|
||||
return &holder, nil
|
||||
}
|
23
plugins/runner.go
Normal file
23
plugins/runner.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package plugins
|
||||
|
||||
import lua "github.com/yuin/gopher-lua"
|
||||
|
||||
type Runner struct {
|
||||
state *lua.LState
|
||||
inUse bool
|
||||
}
|
||||
|
||||
func NewRunner() (*Runner, error) {
|
||||
lState, err := NewBlankState()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Runner{
|
||||
lState,
|
||||
false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Runner) IsInUse() bool {
|
||||
return r.inUse
|
||||
}
|
11
plugins/util.go
Normal file
11
plugins/util.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package plugins
|
||||
|
||||
import lua "github.com/yuin/gopher-lua"
|
||||
|
||||
func lTableToMap(t *lua.LTable) map[lua.LValue]lua.LValue {
|
||||
m := map[lua.LValue]lua.LValue{}
|
||||
t.ForEach(func(l1, l2 lua.LValue) {
|
||||
m[l1] = l2
|
||||
})
|
||||
return m
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue