Files
siyuan/kernel/plugin/api_rpc.go
Yingyi / 颖逸 c59b8ec7f7 🎨 Harden RPC handling and improve error reporting in kernel plugin (#17655)
* perf(kernel-plugin): strengthen RPC, sandbox, and form parsing

Validate and harden plugin RPC and request handling: ensure RPC API call first argument is a string; treat missing method using HasValue(); return InvalidParams for malformed params; bail out early when kernel is incompatible or missing. Fix sandbox promise invocation to return after reporting errors to avoid continuing on nil/invalid values. Change RequestForm files to []*RequestFile, allocate pointer entries, properly open/read/close uploaded files, and clone request headers before modifying them. These changes prevent nil derefs, resource leaks, and improve error reporting.

* perf(kernel-plugin): Skip empty Content-Type; use safe type assertions

Avoid setting an empty Content-Type header in the proxy when gin.Context.ContentType() is empty. Replace unsafe type assertions with comma-ok checks when converting request and file body bytes to Data objects to prevent panics on unexpected types or nil pointers. Also comment out assignments of c.Request.Context().Err() in plugin request handlers to avoid overwriting other error state on context cancellation. Affected files: kernel/api/network.go, kernel/plugin/plugin.go, kernel/plugin/sandbox.go.
2026-05-09 19:38:54 +08:00

188 lines
5.3 KiB
Go

// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package plugin
import (
"fmt"
"github.com/dop251/goja"
"github.com/samber/lo"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/util"
)
// injectRpc adds siyuan.rpc method for RPC method registration.
func injectRpc(p *KernelPlugin, rt *goja.Runtime, siyuan *goja.Object) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("injectRpc: %v", r)
}
}()
rpc := rt.NewObject()
lo.Must0(rpc.Set("bind", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
promise, resolve, reject := rt.NewPromise()
runErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {
var name string
var method goja.Callable
var descriptions []string
if len(call.Arguments) < 2 {
err = fmt.Errorf("method name and function required")
return
}
nameArg := call.Argument(0)
methodArg := call.Argument(1)
descArgs := call.Arguments[2:]
if goja.IsString(nameArg) {
name = nameArg.String()
} else {
err = fmt.Errorf("first argument must be method name string")
return
}
if methodJs, ok := goja.AssertFunction(methodArg); ok {
method = methodJs
} else {
err = fmt.Errorf("second argument must be a function")
}
descriptions = make([]string, len(descArgs))
for i, a := range descArgs {
descriptions[i] = a.String()
}
err = p.bindRpcMethod(name, method, descriptions...)
return
}, func(rt *goja.Runtime, result any, err error) {
if lo.IsNil(err) {
if resolveErr := resolve(result); resolveErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.rpc.bind resolve: %v", p.Name, resolveErr)
}
} else {
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.rpc.bind reject: %v", p.Name, rejectErr)
}
}
})
if runErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.rpc.bind worker run: %v", p.Name, runErr)
}
return rt.ToValue(promise)
})))
lo.Must0(rpc.Set("unbind", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
promise, resolve, reject := rt.NewPromise()
runErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {
var name string
if len(call.Arguments) < 1 {
err = fmt.Errorf("method name required")
return
}
nameArg := call.Argument(0)
if goja.IsString(nameArg) {
name = nameArg.String()
} else {
err = fmt.Errorf("first argument must be method name string")
return
}
err = p.unbindRpcMethod(name)
return
}, func(rt *goja.Runtime, result any, err error) {
if lo.IsNil(err) {
if resolveErr := resolve(result); resolveErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.rpc.unbind resolve: %v", p.Name, resolveErr)
}
} else {
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.rpc.unbind reject: %v", p.Name, rejectErr)
}
}
})
if runErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.rpc.unbind worker run: %v", p.Name, runErr)
}
return rt.ToValue(promise)
})))
lo.Must0(rpc.Set("broadcast", rt.ToValue(func(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
promise, resolve, reject := rt.NewPromise()
runErr := p.worker.Run(func(rt *goja.Runtime) (result any, err error) {
if len(call.Arguments) < 1 {
err = fmt.Errorf("method required")
return
}
var method string
if m := call.Argument(0); goja.IsString(m) {
method = m.String()
} else {
err = fmt.Errorf("first argument must be method name string")
return
}
var params util.Optional[any]
arg := call.Argument(1)
if goja.IsUndefined(arg) {
params.Value = nil
params.Exists = false
params.IsNull = false
} else if goja.IsNull(arg) {
params.Value = nil
params.Exists = true
params.IsNull = true
} else {
params.Value = arg.Export()
params.Exists = true
params.IsNull = false
}
p.BroadcastNotification(method, params)
return
}, func(rt *goja.Runtime, result any, err error) {
if lo.IsNil(err) {
if resolveErr := resolve(result); resolveErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.rpc.broadcast resolve: %v", p.Name, resolveErr)
}
} else {
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.rpc.broadcast reject: %v", p.Name, rejectErr)
}
}
})
if runErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.rpc.broadcast worker run: %v", p.Name, runErr)
}
return rt.ToValue(promise)
})))
lo.Must0(ObjectFreeze(rt, rpc))
lo.Must0(siyuan.Set("rpc", rpc))
return
}