Skip to content

Commit 2d5e3af

Browse files
committed
feat: add autoload
1 parent 5a9be1f commit 2d5e3af

File tree

11 files changed

+382
-329
lines changed

11 files changed

+382
-329
lines changed

config.yaml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
#httpproxy: http://127.0.0.1:7890 # 插件 HTTP 服务时会使用的 Proxy
2-
opqurl: http://127.0.0.1:8086 # OPQ 的地址
31
admin:
4-
- 2435932516 # 管理员
2+
- 2435932516
3+
httpproxy: http://127.0.0.1:7890
4+
loglevel: -1
5+
opqurl: http://127.0.0.1:8086
56
plugin:
6-
env:
7-
test: opqbot # 环境变量 将会传递到插件内
7+
autoload: []
8+
env:
9+
test: opqbot

plugin/admin.go

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/opq-osc/Yui/session"
1111
"github.com/spf13/cast"
1212
"github.com/spf13/viper"
13+
"strconv"
1314
"strings"
1415
)
1516

@@ -55,6 +56,30 @@ func (m *Manager) OnGroupMsgAdmin(ctx context.Context, event events.IEvent) bool
5556
} else {
5657
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg("卸载成功").Do(ctx)
5758
}
59+
case "autoList":
60+
plugins := GetAutoLoadPlugins()
61+
msg := []string{"自动启动列表"}
62+
for _, v := range plugins {
63+
msg = append(msg, fmt.Sprintf("%s", v.PluginName))
64+
}
65+
if len(msg) == 0 {
66+
msg = []string{"没有插件呢?"}
67+
}
68+
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg(strings.Join(msg, "\n")).Do(ctx)
69+
case "enable":
70+
err := AddAutoLoadPlugin(cmd[2])
71+
if err != nil {
72+
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg(err.Error()).Do(ctx)
73+
} else {
74+
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg("成功").Do(ctx)
75+
}
76+
case "disable":
77+
err := RemoveAutoLoadPlugin(cmd[2])
78+
if err != nil {
79+
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg(err.Error()).Do(ctx)
80+
} else {
81+
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg("成功").Do(ctx)
82+
}
5883
case "load":
5984
pluginInfo, err := GetPluginInfo(cmd[2])
6085
if err != nil {
@@ -81,17 +106,40 @@ func (m *Manager) OnGroupMsgAdmin(ctx context.Context, event events.IEvent) bool
81106
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg(strings.Join(msg, "\n")).Do(ctx)
82107
case "yun":
83108
switch cmd[2] {
109+
case "install":
110+
listsI, err := s.Get("yum install")
111+
if err == nil && listsI != nil {
112+
lists, ok := listsI.([]repository.Plugin)
113+
if ok {
114+
if index, err := strconv.Atoi(cmd[3]); err == nil {
115+
if index >= 0 && index < len(lists) {
116+
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg("开始下载").Do(ctx)
117+
err = repository.DownloadPlugin(ctx, lists[index])
118+
if err != nil {
119+
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg(err.Error()).Do(ctx)
120+
} else {
121+
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg("下载成功,请自行载入").Do(ctx)
122+
}
123+
} else {
124+
return true
125+
}
126+
}
127+
}
128+
}
129+
s.Delete("yum install")
84130
case "list":
85131
lists, err := repository.GetPluginList(ctx)
86132
if err != nil {
87133
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg(err.Error()).Do(ctx)
88134
return true
89135
}
90-
var msg = []string{"仓库插件:"}
91-
for _, v := range lists {
92-
msg = append(msg, fmt.Sprintf("%s 作者:%s 说明:%s 权限:%v", v.PluginName, v.Author, v.Description, v.Permissions))
136+
var msg = []string{"仓库插件(您可以输入.admin yun install 序号 下载):"}
137+
for i, v := range lists {
138+
msg = append(msg, fmt.Sprintf("[%d]%s 作者:%s 说明:%s 权限:%v", i, v.PluginName, v.Author, v.Description, v.Permissions))
93139
}
94140
S.GetApi(event).SendMsg().GroupMsg().ToUin(event.ParseGroupMsg().GetGroupUin()).TextMsg(strings.Join(msg, "\n")).Do(ctx)
141+
s.Set("yum install", lists)
142+
95143
}
96144
case "permission":
97145
plugin, err := M.GetPlugin(cmd[2])

plugin/autoload.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package plugin
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"crypto/ecdsa"
7+
"crypto/sha256"
8+
"encoding/binary"
9+
"encoding/gob"
10+
"encoding/hex"
11+
"fmt"
12+
"github.com/charmbracelet/log"
13+
"github.com/ethereum/go-ethereum/crypto"
14+
"github.com/knqyf263/go-plugin/types/known/emptypb"
15+
_ "github.com/opq-osc/Yui/config"
16+
"github.com/opq-osc/Yui/plugin/meta"
17+
"github.com/opq-osc/Yui/proto"
18+
"github.com/opq-osc/Yui/proto/library/systemInfo"
19+
systemInfoExport "github.com/opq-osc/Yui/proto/library/systemInfo/export"
20+
cron2 "github.com/robfig/cron/v3"
21+
"github.com/spf13/viper"
22+
"github.com/tetratelabs/wazero"
23+
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
24+
"io"
25+
"math/big"
26+
"os"
27+
"path/filepath"
28+
"strings"
29+
)
30+
31+
// 允许自动加载插件
32+
33+
type AutoLoadPluginCfg struct {
34+
PluginName string `mapstructure:"pluginname"`
35+
PluginSha256 string `mapstructure:"pluginsha256"`
36+
}
37+
38+
func GetAutoLoadPlugins() []AutoLoadPluginCfg {
39+
var autoLoadCfg []AutoLoadPluginCfg
40+
viper.UnmarshalKey("plugin.autoload", &autoLoadCfg)
41+
return autoLoadCfg
42+
}
43+
func RemoveAutoLoadPlugin(name string) error {
44+
var plugins []AutoLoadPluginCfg
45+
viper.UnmarshalKey("plugin.autoload", &plugins)
46+
for i, v := range plugins {
47+
if v.PluginName == name {
48+
plugins = append(plugins[:i], plugins[i+1:]...)
49+
}
50+
}
51+
viper.Set("plugin.autoload", plugins)
52+
return viper.WriteConfig()
53+
}
54+
func AddAutoLoadPlugin(name string) error {
55+
var plugins []AutoLoadPluginCfg
56+
viper.UnmarshalKey("plugin.autoload", &plugins)
57+
for _, v := range plugins {
58+
if v.PluginName == name {
59+
return fmt.Errorf("插件已经添加到自动加载列表")
60+
}
61+
}
62+
data, err := os.ReadFile(filepath.Join("plugins", name+".opq"))
63+
if err != nil {
64+
return err
65+
}
66+
sha := sha256.New()
67+
sha.Write(data)
68+
hash := hex.EncodeToString(sha.Sum(nil))
69+
plugins = append(plugins, AutoLoadPluginCfg{
70+
PluginName: name,
71+
PluginSha256: hash,
72+
})
73+
viper.Set("plugin.autoload", plugins)
74+
return viper.WriteConfig()
75+
}
76+
77+
// LoadPluginWithSha256 自动加载时使用
78+
func (m *Manager) LoadPluginWithSha256(ctx context.Context, pluginInfo AutoLoadPluginCfg) error {
79+
f, err := os.ReadFile(filepath.Join("plugins", pluginInfo.PluginName+".opq"))
80+
if err != nil {
81+
return err
82+
}
83+
sha := sha256.New()
84+
sha.Write(f)
85+
if !strings.EqualFold(hex.EncodeToString(sha.Sum(nil)), pluginInfo.PluginSha256) {
86+
return fmt.Errorf("插件疑似被修改,无法自动载入")
87+
}
88+
reader := bytes.NewReader(f)
89+
sign := [3]byte{}
90+
_, err = reader.Read(sign[:])
91+
if err != nil {
92+
return err
93+
}
94+
if !bytes.Equal(sign[:], []byte("OPQ")) {
95+
return fmt.Errorf("非OPQ插件")
96+
}
97+
var pluginApiVersion int32 = 0
98+
err = binary.Read(reader, binary.LittleEndian, &pluginApiVersion)
99+
if err != nil {
100+
return err
101+
}
102+
if pluginApiVersion < meta.PluginApiVersion {
103+
return fmt.Errorf("插件API版本过低无法载入")
104+
}
105+
var length int32 = 0
106+
// 获取签名信息
107+
r := big.NewInt(0)
108+
s := big.NewInt(0)
109+
var signFlag int32 = 0
110+
err = binary.Read(reader, binary.LittleEndian, &signFlag)
111+
if err != nil {
112+
return err
113+
}
114+
if int(signFlag) == 1 {
115+
err = binary.Read(reader, binary.LittleEndian, &length)
116+
if err != nil {
117+
return err
118+
}
119+
rByte := make([]byte, length)
120+
_, err = reader.Read(rByte)
121+
122+
if err != nil {
123+
return err
124+
}
125+
err = r.UnmarshalText(rByte)
126+
if err != nil {
127+
return err
128+
}
129+
130+
err = binary.Read(reader, binary.LittleEndian, &length)
131+
if err != nil {
132+
return err
133+
}
134+
sByte := make([]byte, length)
135+
_, err = reader.Read(sByte)
136+
if err != nil {
137+
return err
138+
}
139+
err = s.UnmarshalText(sByte)
140+
if err != nil {
141+
return err
142+
}
143+
}
144+
145+
// 读取头信息
146+
err = binary.Read(reader, binary.LittleEndian, &length)
147+
if err != nil {
148+
return err
149+
}
150+
header := make([]byte, length)
151+
_, err = reader.Read(header)
152+
if err != nil {
153+
return err
154+
}
155+
sha = sha256.New()
156+
sha.Write(header)
157+
headerSha256 := sha.Sum(nil)
158+
dec := gob.NewDecoder(bytes.NewBuffer(header))
159+
pluginMeta := meta.PluginMeta{}
160+
if err = dec.Decode(&pluginMeta); err != nil {
161+
return err
162+
}
163+
if _, ok := m.plugins.Load(pluginMeta.PluginName); ok {
164+
return fmt.Errorf("插件名已存在,无法重复加载!")
165+
}
166+
// 依赖
167+
for _, v := range pluginMeta.Dependencies {
168+
if _, ok := m.plugins.Load(v); !ok {
169+
return fmt.Errorf("缺少依赖项%s,插件无法载入", v)
170+
}
171+
}
172+
173+
pluginMeta.Sign = false
174+
if int(signFlag) == 1 {
175+
p, err := hex.DecodeString(publicKey)
176+
if err != nil {
177+
log.Error(err)
178+
}
179+
pubKey, err := crypto.UnmarshalPubkey(p)
180+
if err != nil {
181+
log.Fatal(err)
182+
}
183+
// 公钥验证签名
184+
pluginMeta.Sign = ecdsa.Verify(pubKey, headerSha256, r, s)
185+
}
186+
log.Info("载入插件中", "pluginName", pluginMeta.PluginName, "sha256", pluginMeta.Sha256)
187+
data, err := io.ReadAll(reader)
188+
if err != nil {
189+
return err
190+
}
191+
sha = sha256.New()
192+
sha.Write(data)
193+
sha256Result := hex.EncodeToString(sha.Sum(nil))
194+
log.Info("计算sha256", "result", sha256Result)
195+
if !strings.EqualFold(sha256Result, pluginMeta.Sha256) {
196+
return fmt.Errorf("插件疑似被篡改,禁止载入")
197+
}
198+
dir := filepath.Join("plugins", pluginMeta.PluginName)
199+
err = os.MkdirAll(dir, 0777)
200+
if err != nil {
201+
return err
202+
}
203+
p := Plugin{Meta: pluginMeta, cronJobs: map[string]cron2.EntryID{}}
204+
mc := wazero.NewModuleConfig().
205+
WithStdout(io.Discard). // 丢弃
206+
WithStderr(io.Discard). // 丢弃
207+
WithFSConfig(wazero.NewFSConfig().WithDirMount(dir, "/"))
208+
env := viper.GetStringMapString("plugin.env")
209+
for k, v := range env {
210+
mc = mc.WithEnv(k, v)
211+
}
212+
rc := wazero.NewRuntimeConfig()
213+
214+
pluginVm, err := proto.NewEventPlugin(ctx, proto.WazeroModuleConfig(mc), proto.WazeroRuntime(func(ctx context.Context) (wazero.Runtime, error) {
215+
r := wazero.NewRuntimeWithConfig(ctx, rc)
216+
if _, err := wasi_snapshot_preview1.Instantiate(ctx, r); err != nil {
217+
return nil, err
218+
}
219+
if err = systemInfoExport.Instantiate(ctx, r, &systemInfo.SystemInfo{PluginInfo: pluginMeta}); err != nil {
220+
return nil, err
221+
}
222+
return r, nil
223+
}))
224+
225+
if err != nil {
226+
return err
227+
}
228+
229+
p.event, err = pluginVm.LoadWithBytes(ctx, data, &p)
230+
if err != nil {
231+
return err
232+
}
233+
reply, err := p.event.Init(ctx, &emptypb.Empty{})
234+
if err != nil {
235+
p.event.Close(ctx)
236+
return err
237+
}
238+
if !reply.Ok {
239+
p.event.Close(ctx)
240+
return fmt.Errorf(reply.Message)
241+
}
242+
m.plugins.Store(pluginMeta.PluginName, &p)
243+
return nil
244+
}
245+
246+
func init() {
247+
// auto load plugin
248+
plugins := GetAutoLoadPlugins()
249+
for _, v := range plugins {
250+
err := M.LoadPluginWithSha256(context.Background(), v)
251+
if err != nil {
252+
log.Error("自动加载时遇到错误", "err", err)
253+
}
254+
}
255+
}

plugin/builder/cmd/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,5 +164,5 @@ func init() {
164164
rootCmd.AddCommand(buildCmd)
165165
buildCmd.Flags().StringVarP(&tinyGoPath, "tinyGoPath", "t", "tinygo", "设置tinyGo程序路径")
166166
buildCmd.Flags().StringVarP(&output, "output", "o", "", "设置输出插件文件名,不包含后缀")
167-
buildCmd.Flags().StringVarP(&privateKey, "privateKey", "p", viper.GetString("OPQPrivateKey"), "为插件签名")
167+
buildCmd.Flags().StringVarP(&privateKey, "privateKey", "p", viper.GetString("OPQ_KEY"), "为插件签名")
168168
}

plugin/httpApi.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func (p *Plugin) Http(ctx context.Context, request *proto.HttpReq) (*proto.HttpR
1616
}
1717
log.Debug("插件发起HTTP连接", "plugin", p.Meta.PluginName, "method", request.Method, "url", request.Url)
1818
var r *req.Request
19-
if proxy := viper.GetString("httpProxy"); proxy != "" {
19+
if proxy := viper.GetString("httpPluginProxy"); proxy != "" {
2020
r = req.C().SetProxyURL(proxy).R().SetContext(ctx)
2121
} else {
2222
r = req.R().SetContext(ctx)

plugin/plugin.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ func (m *Manager) GetAllPlugins() map[string]*Plugin {
228228
})
229229
return plugins
230230
}
231-
func GetPluginInfo(pluginPath string) (*meta.PluginMeta, error) {
232-
f, err := os.ReadFile(pluginPath)
231+
func GetPluginInfo(pluginName string) (*meta.PluginMeta, error) {
232+
f, err := os.ReadFile(filepath.Join("plugins", pluginName+".opq"))
233233
if err != nil {
234234
return nil, err
235235
}
@@ -331,7 +331,7 @@ func GetPluginInfo(pluginPath string) (*meta.PluginMeta, error) {
331331
*/
332332

333333
func (m *Manager) LoadPlugin(ctx context.Context, pluginPath string) error {
334-
f, err := os.ReadFile(pluginPath)
334+
f, err := os.ReadFile(filepath.Join("plugins", pluginPath+".opq"))
335335
if err != nil {
336336
return err
337337
}

0 commit comments

Comments
 (0)