mirror of
https://github.com/siyuan-note/siyuan.git
synced 2026-06-28 06:46:12 +00:00
* 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.
504 lines
15 KiB
Go
504 lines
15 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 (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/lxzan/gws"
|
|
"github.com/siyuan-note/logging"
|
|
"github.com/siyuan-note/siyuan/kernel/util"
|
|
)
|
|
|
|
type JsonRpcErrorCode int
|
|
|
|
const (
|
|
JsonRpcVersion = "2.0"
|
|
|
|
JsonRpcErrorCodeParseError JsonRpcErrorCode = -32700
|
|
JsonRpcErrorCodeInvalidRequest JsonRpcErrorCode = -32600
|
|
JsonRpcErrorCodeMethodNotFound JsonRpcErrorCode = -32601
|
|
JsonRpcErrorCodeInvalidParams JsonRpcErrorCode = -32602
|
|
JsonRpcErrorCodeInternalError JsonRpcErrorCode = -32603
|
|
|
|
// Server-defined error codes (-32099 to -32000)
|
|
JsonRpcErrorCodePluginNotLoaded JsonRpcErrorCode = -32001
|
|
JsonRpcErrorCodePluginNotRunning JsonRpcErrorCode = -32002
|
|
)
|
|
|
|
var (
|
|
JsonRpcErrorParseError = &JsonRpcError{Code: JsonRpcErrorCodeParseError, Message: "Parse error"}
|
|
JsonRpcErrorInvalidRequest = &JsonRpcError{Code: JsonRpcErrorCodeInvalidRequest, Message: "Invalid Request"}
|
|
JsonRpcErrorMethodNotFound = &JsonRpcError{Code: JsonRpcErrorCodeMethodNotFound, Message: "Method not found"}
|
|
JsonRpcErrorInvalidParams = &JsonRpcError{Code: JsonRpcErrorCodeInvalidParams, Message: "Invalid params"}
|
|
JsonRpcErrorInternalError = &JsonRpcError{Code: JsonRpcErrorCodeInternalError, Message: "Internal error"}
|
|
|
|
JsonRpcErrorPluginNotLoaded = &JsonRpcError{Code: JsonRpcErrorCodePluginNotLoaded, Message: "Plugin not loaded"}
|
|
JsonRpcErrorPluginNotRunning = &JsonRpcError{Code: JsonRpcErrorCodePluginNotRunning, Message: "Plugin not running"}
|
|
)
|
|
|
|
func (e *JsonRpcError) Error() string {
|
|
return fmt.Sprintf("JSON RPC Error: %d %s", e.Code, e.Message)
|
|
}
|
|
|
|
// JsonRpcRequest represents a JSON-RPC 2.0 request.
|
|
type JsonRpcRequest struct {
|
|
JsonRpc string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params util.Optional[any] `json:"params,omitempty"`
|
|
ID util.Optional[any] `json:"id,omitempty"`
|
|
}
|
|
|
|
func (r JsonRpcRequest) MarshalJSON() ([]byte, error) {
|
|
m := map[string]any{
|
|
"jsonrpc": r.JsonRpc,
|
|
"method": r.Method,
|
|
}
|
|
if r.Params.Exists {
|
|
if r.Params.IsNull {
|
|
m["params"] = nil
|
|
} else {
|
|
m["params"] = r.Params.Value
|
|
}
|
|
}
|
|
if r.ID.Exists {
|
|
if r.ID.IsNull {
|
|
m["id"] = nil
|
|
} else {
|
|
m["id"] = r.ID.Value
|
|
}
|
|
}
|
|
return json.Marshal(m)
|
|
}
|
|
|
|
func (r *JsonRpcRequest) UnmarshalJSON(data []byte) error {
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
// decoder.DisallowUnknownFields() // Reject unknown fields violates the JSON-RPC spec
|
|
type JsonRpcRequestObject struct {
|
|
JsonRpc util.Optional[string] `json:"jsonrpc"`
|
|
Method util.Optional[string] `json:"method"`
|
|
Params util.Optional[any] `json:"params"`
|
|
ID util.Optional[any] `json:"id"`
|
|
}
|
|
request := JsonRpcRequestObject{}
|
|
if err := decoder.Decode(&request); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate jsonrpc field
|
|
if !request.JsonRpc.Exists {
|
|
return fmt.Errorf("missing jsonrpc field")
|
|
}
|
|
if request.JsonRpc.Value != JsonRpcVersion {
|
|
return fmt.Errorf("invalid jsonrpc version: %s", request.JsonRpc.Value)
|
|
}
|
|
|
|
// Validate method field
|
|
if !request.Method.HasValue() {
|
|
return fmt.Errorf("missing method field")
|
|
}
|
|
|
|
// Validate id field
|
|
if !request.ID.Exists {
|
|
} else if request.ID.IsNull {
|
|
} else if _, ok := request.ID.Value.(string); ok {
|
|
} else if _, ok := request.ID.Value.(float64); ok {
|
|
} else {
|
|
return fmt.Errorf("invalid id field: must be string, number, null or omitted")
|
|
}
|
|
|
|
r.JsonRpc = request.JsonRpc.Value
|
|
r.Method = request.Method.Value
|
|
r.Params = request.Params
|
|
r.ID = request.ID
|
|
return nil
|
|
}
|
|
|
|
// IsNotification returns true if this request is a notification (no ID field).
|
|
func (r *JsonRpcRequest) IsNotification() bool {
|
|
return r.ID.Exists == false
|
|
}
|
|
|
|
// Validate validates the JSON-RPC request structure.
|
|
func (r *JsonRpcRequest) Validate() *JsonRpcError {
|
|
// params is optional, but if present must be either an array (for positional parameters) or an object (for named parameters)
|
|
if !r.Params.Exists {
|
|
} else if _, ok := r.Params.Value.([]any); ok {
|
|
} else if _, ok := r.Params.Value.(map[string]any); ok {
|
|
} else {
|
|
return &JsonRpcError{
|
|
Code: JsonRpcErrorCodeInvalidParams,
|
|
Message: JsonRpcErrorInvalidParams.Message,
|
|
Data: "Invalid params: must be array or object if present",
|
|
}
|
|
}
|
|
|
|
// ✅ jsonrpc, method and id fields are validated during unmarshaling, so do not need to validate again here.
|
|
|
|
// if r.JsonRpc != JsonRpcVersion {
|
|
// return JsonRpcErrorInvalidRequest
|
|
// }
|
|
|
|
// if !r.ID.Exists {
|
|
// } else if r.ID.IsNull {
|
|
// } else if _, ok := r.ID.Value.(string); ok {
|
|
// } else if _, ok := r.ID.Value.(float64); ok {
|
|
// } else {
|
|
// return JsonRpcErrorInvalidRequest
|
|
// }
|
|
|
|
return nil
|
|
}
|
|
|
|
// JsonRpcRequestResponse represents a JSON-RPC 2.0 success response.
|
|
// result MUST be present (even if null); error MUST NOT be present.
|
|
type JsonRpcRequestResponse struct {
|
|
JsonRpc string `json:"jsonrpc"`
|
|
Result any `json:"result"`
|
|
ID any `json:"id"`
|
|
}
|
|
|
|
// JsonRpcErrorResponse represents a JSON-RPC 2.0 error response.
|
|
// error MUST be present; result MUST NOT be present.
|
|
type JsonRpcErrorResponse struct {
|
|
JsonRpc string `json:"jsonrpc"`
|
|
Error *JsonRpcError `json:"error"`
|
|
ID any `json:"id"`
|
|
}
|
|
|
|
// JsonRpcError represents a JSON-RPC 2.0 error.
|
|
type JsonRpcError struct {
|
|
Code JsonRpcErrorCode `json:"code"`
|
|
Message string `json:"message"`
|
|
Data any `json:"data,omitempty"`
|
|
}
|
|
|
|
// JsonRpcRequestProcessingResults represents the results of parsing and validating JSON-RPC requests, including any global error and the individual results for each request in a batch.
|
|
type JsonRpcRequestProcessingResults struct {
|
|
Batch bool // Whether the original request was a batch (array) or single request
|
|
GlobalError *JsonRpcErrorResponse // If the entire request is invalid
|
|
Requests []*JsonRpcProcessingRequest
|
|
}
|
|
|
|
// JsonRpcProcessingRequest represents the result of parsing and validating a single JSON-RPC request, including any error if the request is invalid.
|
|
type JsonRpcProcessingRequest struct {
|
|
Request *JsonRpcRequest // The parsed request, or nil if the request was invalid
|
|
Error *JsonRpcErrorResponse // The error if the request was invalid, or nil if the request is valid
|
|
}
|
|
|
|
// JsonRpcProcessingResponse represents the response to a JSON-RPC request, including either the success response or the error response (but not both).
|
|
// - For notifications, both fields will be nil, indicating that no response should be sent.
|
|
// - For successful requests, Response will be non-nil and Error will be nil.
|
|
// - For failed requests, Error will be non-nil and Response will be nil.
|
|
type JsonRpcProcessingResponse struct {
|
|
Response *JsonRpcRequestResponse // The success response, or nil if the request was a notification or the response is an error
|
|
Error *JsonRpcErrorResponse // The error response, or nil if the request was a notification or the response is a success
|
|
}
|
|
|
|
// JsonRpcResponse returns the appropriate response (either success or error) to be sent back to the client, or nil if this is a notification and no response should be sent.
|
|
func (r *JsonRpcProcessingResponse) JsonRpcResponse() any {
|
|
if r.Response != nil {
|
|
return r.Response
|
|
}
|
|
if r.Error != nil {
|
|
return r.Error
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HandleRpcHttp handles POST /api/plugin/rpc/:name
|
|
// Supports single call, batch call, and notification (no response for notification).
|
|
func HandleRpcHttp(c *gin.Context) {
|
|
name := util.GetRequestUrlStringParam(c, "name")
|
|
p := resolveRunningPlugin(c, name, http.StatusOK)
|
|
if p == nil {
|
|
return
|
|
}
|
|
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, &JsonRpcErrorResponse{
|
|
JsonRpc: JsonRpcVersion,
|
|
Error: &JsonRpcError{
|
|
Code: JsonRpcErrorCodeInternalError,
|
|
Message: JsonRpcErrorInternalError.Message,
|
|
Data: fmt.Sprintf("Failed to read request body: %s", err),
|
|
},
|
|
ID: nil,
|
|
})
|
|
return
|
|
}
|
|
|
|
results := parseRpcRequests(body)
|
|
if results.GlobalError != nil {
|
|
c.JSON(http.StatusOK, results.GlobalError)
|
|
return
|
|
}
|
|
|
|
responses := p.dispatchRpcRequests(results.Requests)
|
|
|
|
if !results.Batch {
|
|
// Single request - return single response (or empty for notification)
|
|
if len(responses) > 0 && responses[0] != nil {
|
|
response := responses[0]
|
|
if response.Response != nil {
|
|
c.JSON(http.StatusOK, response.Response)
|
|
return
|
|
} else if response.Error != nil {
|
|
c.JSON(http.StatusOK, response.Error)
|
|
return
|
|
}
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
// Batch request - filter out nil responses (notifications) and return array
|
|
filtered := filterRpcResponses(responses)
|
|
|
|
if len(filtered) > 0 {
|
|
c.JSON(http.StatusOK, filtered)
|
|
} else {
|
|
// All notifications in batch - send nothing per spec (MUST NOT return empty array)
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// HandleRpcWebSocket handles GET /ws/plugin/rpc/:name
|
|
// Supports single call, batch call, notification, and server push notifications.
|
|
func HandleRpcWebSocket(c *gin.Context) {
|
|
name := util.GetRequestUrlStringParam(c, "name")
|
|
p := resolveRunningPlugin(c, name, http.StatusNotFound)
|
|
if p == nil {
|
|
return
|
|
}
|
|
|
|
if c.IsWebsocket() == false {
|
|
c.String(http.StatusBadRequest, "This endpoint only accepts WebSocket connections")
|
|
return
|
|
}
|
|
|
|
h := &WsEventHandler{p: p}
|
|
|
|
h.onMessage = func(socket *gws.Conn, message *gws.Message) {
|
|
defer message.Close()
|
|
|
|
results := parseRpcRequests(message.Bytes())
|
|
if results.GlobalError != nil {
|
|
if respBytes, marshalErr := json.Marshal(results.GlobalError); marshalErr == nil {
|
|
socket.WriteAsync(gws.OpcodeText, respBytes, func(err error) {
|
|
if err != nil {
|
|
logging.LogWarnf("[plugin:%s] RPC WebSocket response write failed: %s", name, err)
|
|
}
|
|
})
|
|
} else {
|
|
logging.LogErrorf("[plugin:%s] RPC WebSocket response marshal failed: %s", name, marshalErr)
|
|
}
|
|
return
|
|
}
|
|
|
|
responses := p.dispatchRpcRequests(results.Requests)
|
|
|
|
var responseBytes []byte
|
|
var marshalErr error
|
|
var needToSend bool
|
|
|
|
if !results.Batch {
|
|
if len(responses) > 0 && responses[0] != nil {
|
|
if response := responses[0].JsonRpcResponse(); response != nil {
|
|
needToSend = true
|
|
responseBytes, marshalErr = json.Marshal(response)
|
|
}
|
|
}
|
|
} else {
|
|
filtered := filterRpcResponses(responses)
|
|
if len(filtered) > 0 {
|
|
needToSend = true
|
|
responseBytes, marshalErr = json.Marshal(filtered)
|
|
}
|
|
}
|
|
|
|
if needToSend {
|
|
if marshalErr != nil {
|
|
logging.LogErrorf("[plugin:%s] RPC response marshal failed: %s", name, marshalErr)
|
|
return
|
|
}
|
|
socket.WriteAsync(gws.OpcodeText, responseBytes, func(err error) {
|
|
if err != nil {
|
|
logging.LogWarnf("[plugin:%s] RPC WebSocket response write failed: %s", name, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
upgrader := gws.NewUpgrader(h, &gws.ServerOption{})
|
|
socket, err := upgrader.Upgrade(c.Writer, c.Request)
|
|
if err != nil {
|
|
logging.LogErrorf("[plugin:%s] RPC WebSocket upgrade failed: %s", name, err)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(p.context)
|
|
|
|
var openOnce sync.Once
|
|
var closeOnce sync.Once
|
|
|
|
doOpen := func() {
|
|
go openOnce.Do(func() {
|
|
p.TrackRpcSocket(socket)
|
|
socket.ReadLoop()
|
|
cancel()
|
|
})
|
|
}
|
|
|
|
doClose := func() {
|
|
closeOnce.Do(func() {
|
|
p.UntrackRpcSocket(socket)
|
|
socket.NetConn().Close()
|
|
cancel()
|
|
})
|
|
}
|
|
|
|
defer doClose()
|
|
doOpen()
|
|
<-ctx.Done()
|
|
}
|
|
|
|
// resolveRunningPlugin looks up the plugin by name and writes an error response if it is
|
|
// not found (-32001) or not running (-32002). Returns nil when the caller should abort.
|
|
func resolveRunningPlugin(c *gin.Context, name string, errStatus int) *KernelPlugin {
|
|
p := GetManager().GetPlugin(name)
|
|
if p == nil {
|
|
c.JSON(errStatus, &JsonRpcErrorResponse{
|
|
JsonRpc: JsonRpcVersion,
|
|
Error: JsonRpcErrorPluginNotLoaded,
|
|
})
|
|
return nil
|
|
}
|
|
if p.State() != PluginStateRunning {
|
|
c.JSON(errStatus, &JsonRpcErrorResponse{
|
|
JsonRpc: JsonRpcVersion,
|
|
Error: JsonRpcErrorPluginNotRunning,
|
|
})
|
|
return nil
|
|
}
|
|
return p
|
|
}
|
|
|
|
// parseRpcRequest parses a single JSON-RPC request from the given body. The body must be a JSON object.
|
|
func parseRpcRequest(body []byte) (parsedRequest JsonRpcProcessingRequest) {
|
|
var request JsonRpcRequest
|
|
if !json.Valid(body) {
|
|
// Invalid JSON
|
|
parsedRequest.Error = &JsonRpcErrorResponse{
|
|
JsonRpc: JsonRpcVersion,
|
|
Error: &JsonRpcError{
|
|
Code: JsonRpcErrorCodeParseError,
|
|
Message: JsonRpcErrorParseError.Message,
|
|
Data: "RPC request is not valid JSON",
|
|
},
|
|
ID: nil,
|
|
}
|
|
return
|
|
}
|
|
if err := json.Unmarshal(body, &request); err != nil {
|
|
// Invalid request structure
|
|
parsedRequest.Error = &JsonRpcErrorResponse{
|
|
JsonRpc: JsonRpcVersion,
|
|
Error: &JsonRpcError{
|
|
Code: JsonRpcErrorCodeInvalidRequest,
|
|
Message: JsonRpcErrorInvalidRequest.Message,
|
|
Data: fmt.Sprintf("RPC request is not a valid JSON-RPC object: %s", err),
|
|
},
|
|
ID: nil,
|
|
}
|
|
return
|
|
}
|
|
parsedRequest.Request = &request
|
|
return
|
|
}
|
|
|
|
// parseRpcRequests parses the given body into one or more JSON-RPC requests, handling both single and batch requests.
|
|
func parseRpcRequests(body []byte) (results JsonRpcRequestProcessingResults) {
|
|
if !json.Valid(body) {
|
|
// Invalid JSON
|
|
results.GlobalError = &JsonRpcErrorResponse{
|
|
JsonRpc: JsonRpcVersion,
|
|
Error: &JsonRpcError{
|
|
Code: JsonRpcErrorCodeParseError,
|
|
Message: JsonRpcErrorParseError.Message,
|
|
Data: "RPC request is not valid JSON",
|
|
},
|
|
ID: nil,
|
|
}
|
|
return
|
|
}
|
|
|
|
var jsonArray []json.RawMessage
|
|
if err := json.Unmarshal(body, &jsonArray); err != nil {
|
|
// single request
|
|
request := parseRpcRequest(body)
|
|
results.Requests = append(results.Requests, &request)
|
|
return
|
|
} else {
|
|
// batch request
|
|
if len(jsonArray) == 0 {
|
|
// per spec, an empty array is invalid
|
|
results.GlobalError = &JsonRpcErrorResponse{
|
|
JsonRpc: JsonRpcVersion,
|
|
Error: &JsonRpcError{
|
|
Code: JsonRpcErrorCodeInvalidRequest,
|
|
Message: JsonRpcErrorInvalidRequest.Message,
|
|
Data: "RPC request is not allowed to be an empty array",
|
|
},
|
|
ID: nil,
|
|
}
|
|
return
|
|
}
|
|
|
|
results.Batch = true
|
|
results.Requests = make([]*JsonRpcProcessingRequest, len(jsonArray))
|
|
for i, raw := range jsonArray {
|
|
request := parseRpcRequest(raw)
|
|
results.Requests[i] = &request
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// filterRpcResponses filters out nil responses (for notifications) and extracts the actual response objects for non-nil responses.
|
|
func filterRpcResponses(responses []*JsonRpcProcessingResponse) []any {
|
|
filtered := make([]any, 0, len(responses))
|
|
for _, response := range responses {
|
|
if response != nil {
|
|
if response.Response != nil {
|
|
filtered = append(filtered, response.Response)
|
|
} else if response.Error != nil {
|
|
filtered = append(filtered, response.Error)
|
|
}
|
|
}
|
|
}
|
|
return filtered
|
|
}
|