mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
182 lines
5.4 KiB
Go
182 lines
5.4 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
package interfaces
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"quad4/reticulum-go/pkg/common"
|
|
"quad4/reticulum-go/pkg/debug"
|
|
)
|
|
|
|
// ExternalFactory builds a custom interface for an unknown config type.
|
|
// Embedders register factories for in-process drivers. Operators can also
|
|
// drop manifests or executables under {config_dir}/interfaces/.
|
|
type ExternalFactory func(name string, cfg *common.InterfaceConfig, ctx *FromConfigContext) (Interface, error)
|
|
|
|
var (
|
|
externalFactoriesMu sync.RWMutex
|
|
externalFactories = map[string]ExternalFactory{}
|
|
)
|
|
|
|
// RegisterExternalFactory registers an in-process builder for typeName.
|
|
// Replaces any prior registration for the same name.
|
|
func RegisterExternalFactory(typeName string, factory ExternalFactory) {
|
|
if typeName == "" || factory == nil {
|
|
return
|
|
}
|
|
externalFactoriesMu.Lock()
|
|
externalFactories[typeName] = factory
|
|
externalFactoriesMu.Unlock()
|
|
}
|
|
|
|
// UnregisterExternalFactory removes a previously registered factory.
|
|
func UnregisterExternalFactory(typeName string) {
|
|
externalFactoriesMu.Lock()
|
|
delete(externalFactories, typeName)
|
|
externalFactoriesMu.Unlock()
|
|
}
|
|
|
|
type pluginManifest struct {
|
|
Driver string `json:"driver"`
|
|
Command string `json:"command"`
|
|
RespawnDelay int `json:"respawn_delay"`
|
|
}
|
|
|
|
// loadExternalInterface resolves unknown types via registered factories,
|
|
// JSON manifests, or executable pipe drivers under interfaces/.
|
|
func loadExternalInterface(name string, cfg *common.InterfaceConfig, ctx *FromConfigContext) (Interface, error) {
|
|
if cfg == nil || cfg.Type == "" {
|
|
return nil, errors.New("nil interface config")
|
|
}
|
|
typeName := cfg.Type
|
|
|
|
externalFactoriesMu.RLock()
|
|
factory := externalFactories[typeName]
|
|
externalFactoriesMu.RUnlock()
|
|
if factory != nil {
|
|
debug.Log(debug.DebugInfo, "Loading external interface from registered factory", "type", typeName, "name", name)
|
|
return factory(name, cfg, ctx)
|
|
}
|
|
|
|
dir := interfacesPluginDir(ctx)
|
|
if dir == "" {
|
|
return nil, fmt.Errorf("unsupported interface type %q (no interfaces plugin directory)", typeName)
|
|
}
|
|
if err := validatePluginTypeName(typeName); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, base := range []string{
|
|
filepath.Join(dir, typeName+".json"),
|
|
filepath.Join(dir, typeName+".manifest"),
|
|
} {
|
|
if !pluginPathContained(dir, base) {
|
|
return nil, fmt.Errorf("invalid plugin type %q", typeName)
|
|
}
|
|
iface, err := loadManifestInterface(name, cfg, ctx, base)
|
|
if err == nil {
|
|
debug.Log(debug.DebugInfo, "Loading external interface from manifest", "path", base, "name", name)
|
|
return iface, nil
|
|
}
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return nil, fmt.Errorf("external interface %q: %w", typeName, err)
|
|
}
|
|
}
|
|
|
|
execPath := filepath.Join(dir, typeName)
|
|
if !pluginPathContained(dir, execPath) {
|
|
return nil, fmt.Errorf("invalid plugin type %q", typeName)
|
|
}
|
|
if st, err := os.Stat(execPath); err == nil && !st.IsDir() && isExecutable(st) {
|
|
debug.Log(debug.DebugInfo, "Loading external interface from executable", "path", execPath, "name", name)
|
|
return newPipeFromPlugin(name, cfg, ctx, execPath, 0)
|
|
}
|
|
|
|
return nil, fmt.Errorf("unsupported interface type %q (no factory, manifest, or executable in %s)", typeName, dir)
|
|
}
|
|
|
|
func validatePluginTypeName(typeName string) error {
|
|
if typeName == "" || filepath.IsAbs(typeName) || typeName != filepath.Base(typeName) {
|
|
return fmt.Errorf("invalid plugin type %q", typeName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func pluginPathContained(dir, path string) bool {
|
|
cleanDir := filepath.Clean(dir)
|
|
cleanPath := filepath.Clean(path)
|
|
rel, err := filepath.Rel(cleanDir, cleanPath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator))
|
|
}
|
|
|
|
func interfacesPluginDir(ctx *FromConfigContext) string {
|
|
if ctx != nil && ctx.ConfigDir != "" {
|
|
return filepath.Join(ctx.ConfigDir, "interfaces")
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return filepath.Join(home, ".reticulum-go", "interfaces")
|
|
}
|
|
|
|
func loadManifestInterface(name string, cfg *common.InterfaceConfig, ctx *FromConfigContext, path string) (Interface, error) {
|
|
data, err := os.ReadFile(path) // #nosec G304 -- path under plugin dir after validatePluginTypeName and pluginPathContained
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var man pluginManifest
|
|
if err := json.Unmarshal(data, &man); err != nil {
|
|
return nil, fmt.Errorf("parse %s: %w", path, err)
|
|
}
|
|
driver := man.Driver
|
|
if driver == "" {
|
|
driver = "pipe"
|
|
}
|
|
switch driver {
|
|
case "pipe":
|
|
cmd := man.Command
|
|
if cmd == "" {
|
|
return nil, fmt.Errorf("%s: pipe driver requires command", path)
|
|
}
|
|
return newPipeFromPlugin(name, cfg, ctx, cmd, man.RespawnDelay)
|
|
default:
|
|
return nil, fmt.Errorf("%s: unknown driver %q", path, driver)
|
|
}
|
|
}
|
|
|
|
func newPipeFromPlugin(name string, cfg *common.InterfaceConfig, ctx *FromConfigContext, command string, respawnDelaySec int) (Interface, error) {
|
|
delaySec := respawnDelaySec
|
|
if cfg != nil && cfg.RespawnDelay > 0 {
|
|
delaySec = cfg.RespawnDelay
|
|
}
|
|
if delaySec <= 0 {
|
|
delaySec = 5
|
|
}
|
|
if cfg != nil && cfg.Command != "" {
|
|
command = cfg.Command
|
|
}
|
|
panicOnErr := ctx != nil && ctx.PanicOnInterfaceError
|
|
enabled := true
|
|
if cfg != nil {
|
|
enabled = cfg.Enabled
|
|
}
|
|
return NewPipeInterface(name, command, enabled, time.Duration(delaySec)*time.Second, panicOnErr)
|
|
}
|
|
|
|
func isExecutable(st os.FileInfo) bool {
|
|
mode := st.Mode()
|
|
return mode&0o111 != 0
|
|
}
|