mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-08 01:17:09 +08:00
utils.fetch is a synchronous, read-only GET or HEAD to the channel Base URL host or the plugin's allowedHosts, through the channel proxy used for polling. It is available in buildSubmitRequest, the query and batch build and parse hooks, and buildContentRequest; every other hook throws. Each hook call gets at most 4 fetches, 15 s per fetch, 30 s in total and 1 MiB per body. Redirects are returned, not followed. responseType "bytes" returns an ArrayBuffer for binary data such as a media file header. While a fetch waits, the runtime gives up its execution slot and the watchdog pauses. BuildContentRequest now receives the request context, and the media proxy shares the plugin request header rules.
312 lines
11 KiB
Go
312 lines
11 KiB
Go
package jsplugin
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Calcium-Ion/moejs"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type volcSignRequest struct {
|
|
Method string `json:"method"`
|
|
URL string `json:"url"`
|
|
Headers map[string]string `json:"headers"`
|
|
Body string `json:"body"`
|
|
AccessKey string `json:"accessKey"`
|
|
SecretKey string `json:"secretKey"`
|
|
Region string `json:"region"`
|
|
Service string `json:"service"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
// injectGlobals installs utils and console. currentCall returns the hook call
|
|
// running on runtime, or nil outside one; utils.fetch reads its fetcher and
|
|
// budget there, so no request data lives in the runtime's globals.
|
|
func injectGlobals(runtime *moejs.Runtime, identity func() string, now func() time.Time, logOutput func(string), currentCall func() *runtimeCall) error {
|
|
utils := map[string]any{
|
|
"fetch": runtime.Function("fetch", 1, func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
|
|
call := currentCall()
|
|
if call == nil {
|
|
return moejs.Undefined(), fmt.Errorf("utils.fetch is not available outside hooks")
|
|
}
|
|
return call.fetch(r, moejs.Arg(args, 0))
|
|
}),
|
|
"hasCapability": runtime.Function("hasCapability", 1, func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
|
|
name, err := stringValue(r, moejs.Arg(args, 0))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
return moejs.Bool(HasCapability(name)), nil
|
|
}),
|
|
"json": map[string]any{
|
|
"clone": runtime.Function("clone", 1, func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
|
|
if moejs.Arg(args, 0).IsUndefined() {
|
|
return moejs.Undefined(), r.TypeError("json.clone requires a JSON value")
|
|
}
|
|
budget := &jsonStateBudget{ctx: context.Background(), bytes: MaxJSONToolBytes, nodes: maxJSONToolNodes, realm: r}
|
|
value, err := newJSONStateNode(moejs.Arg(args, 0), 0, budget)
|
|
if err != nil {
|
|
var exception *moejs.Exception
|
|
var interrupted *moejs.InterruptedError
|
|
if errors.As(err, &exception) || errors.As(err, &interrupted) {
|
|
return moejs.Undefined(), err
|
|
}
|
|
return moejs.Undefined(), r.TypeError("json.clone: %s", err)
|
|
}
|
|
return value.jsValue(r)
|
|
}),
|
|
},
|
|
"unixNow": runtime.Function("unixNow", 0, func(*moejs.Realm, moejs.Value, []moejs.Value) (moejs.Value, error) {
|
|
return moejs.Int(now().Unix()), nil
|
|
}),
|
|
"jwtSignHS256": runtime.Function("jwtSignHS256", 2, func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
|
|
var claims map[string]any
|
|
if argument := moejs.Arg(args, 0); !argument.IsNullish() {
|
|
exported, err := r.ToGoStrict(argument)
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
var ok bool
|
|
if claims, ok = exported.(map[string]any); !ok {
|
|
return moejs.Undefined(), r.TypeError("jwtSignHS256 claims must be an object")
|
|
}
|
|
}
|
|
secret, err := stringValue(r, moejs.Arg(args, 1))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(claims)).SignedString([]byte(secret))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
return moejs.String(token), nil
|
|
}),
|
|
"hmacSHA256": runtime.Function("hmacSHA256", 2, func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
|
|
message, err := stringValue(r, moejs.Arg(args, 0))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
secret, err := stringValue(r, moejs.Arg(args, 1))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
_, _ = mac.Write([]byte(message))
|
|
return moejs.String(hex.EncodeToString(mac.Sum(nil))), nil
|
|
}),
|
|
"base64": runtime.Function("base64", 1, func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
|
|
value, err := stringValue(r, moejs.Arg(args, 0))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
return moejs.String(base64.StdEncoding.EncodeToString([]byte(value))), nil
|
|
}),
|
|
"base64URL": runtime.Function("base64URL", 1, func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
|
|
value, err := stringValue(r, moejs.Arg(args, 0))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
return moejs.String(base64.RawURLEncoding.EncodeToString([]byte(value))), nil
|
|
}),
|
|
"base64URLDecode": runtime.Function("base64URLDecode", 1, func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
|
|
value, err := stringValue(r, moejs.Arg(args, 0))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
decoded, err := base64.RawURLEncoding.DecodeString(value)
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
return moejs.String(string(decoded)), nil
|
|
}),
|
|
"uuid": runtime.Function("uuid", 0, func(*moejs.Realm, moejs.Value, []moejs.Value) (moejs.Value, error) {
|
|
return moejs.String(uuid.NewString()), nil
|
|
}),
|
|
"volcSignV4": runtime.Function("volcSignV4", 1, func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
|
|
// Fields are read by their Go names, as plugins have always
|
|
// passed them.
|
|
var request volcSignRequest
|
|
argument := moejs.Arg(args, 0)
|
|
if !argument.IsNullish() && !argument.IsObject() {
|
|
return moejs.Undefined(), r.TypeError("volcSignV4 request must be an object")
|
|
}
|
|
if argument.IsObject() {
|
|
object := argument.AsObject()
|
|
fields := []struct {
|
|
name string
|
|
target *string
|
|
}{
|
|
{"Method", &request.Method}, {"URL", &request.URL}, {"Body", &request.Body},
|
|
{"AccessKey", &request.AccessKey}, {"SecretKey", &request.SecretKey},
|
|
{"Region", &request.Region}, {"Service", &request.Service},
|
|
}
|
|
for _, field := range fields {
|
|
value, err := object.GetProp(r, r.KeyFromGoString(field.name))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
if *field.target, err = stringValue(r, value); err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
}
|
|
headers, err := object.GetProp(r, r.KeyFromGoString("Headers"))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
if headers.IsObject() {
|
|
keys, err := r.EnumerableOwnKeys(headers.AsObject())
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
request.Headers = make(map[string]string, len(keys))
|
|
for _, key := range keys {
|
|
value, err := headers.AsObject().GetProp(r, key)
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
if request.Headers[key.GoString()], err = stringValue(r, value); err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
}
|
|
}
|
|
timestamp, err := object.GetProp(r, r.KeyFromGoString("Timestamp"))
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
if !timestamp.IsNullish() {
|
|
seconds, err := r.ToNumber(timestamp)
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
if !math.IsNaN(seconds) && !math.IsInf(seconds, 0) {
|
|
request.Timestamp = int64(seconds)
|
|
}
|
|
}
|
|
}
|
|
signed, err := signVolcV4(request, now)
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
return r.FromGo(signed)
|
|
}),
|
|
}
|
|
if err := runtime.SetGlobal("utils", utils); err != nil {
|
|
return err
|
|
}
|
|
return runtime.SetGlobal("console", map[string]any{
|
|
"log": runtime.Function("log", 0, func(r *moejs.Realm, _ moejs.Value, args []moejs.Value) (moejs.Value, error) {
|
|
parts := make([]string, len(args))
|
|
for i, argument := range args {
|
|
if argument.IsSymbol() {
|
|
parts[i] = argument.String()
|
|
continue
|
|
}
|
|
text, err := r.ToString(argument)
|
|
if err != nil {
|
|
return moejs.Undefined(), err
|
|
}
|
|
parts[i] = text.GoString()
|
|
}
|
|
if logOutput != nil {
|
|
logOutput(identity() + " " + strings.Join(parts, " "))
|
|
}
|
|
return moejs.Undefined(), nil
|
|
}),
|
|
})
|
|
}
|
|
|
|
// stringValue reads a host function's string parameter: undefined and null
|
|
// read as "", other values as JavaScript's String(value).
|
|
func stringValue(r *moejs.Realm, value moejs.Value) (string, error) {
|
|
if value.IsNullish() {
|
|
return "", nil
|
|
}
|
|
if value.IsString() {
|
|
return value.AsString().GoString(), nil
|
|
}
|
|
text, err := r.ToString(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return text.GoString(), nil
|
|
}
|
|
|
|
func signVolcV4(request volcSignRequest, now func() time.Time) (map[string]string, error) {
|
|
parsedURL, err := url.Parse(request.URL)
|
|
if err != nil || parsedURL.Host == "" {
|
|
return nil, fmt.Errorf("invalid Volcengine signing URL")
|
|
}
|
|
region := request.Region
|
|
if region == "" {
|
|
region = "cn-north-1"
|
|
}
|
|
service := request.Service
|
|
if service == "" {
|
|
service = "cv"
|
|
}
|
|
timestamp := now().UTC()
|
|
if request.Timestamp != 0 {
|
|
timestamp = time.Unix(request.Timestamp, 0).UTC()
|
|
}
|
|
xDate := timestamp.Format("20060102T150405Z")
|
|
shortDate := timestamp.Format("20060102")
|
|
bodyHash := sha256.Sum256([]byte(request.Body))
|
|
requestPath := parsedURL.EscapedPath()
|
|
if requestPath == "" {
|
|
requestPath = "/"
|
|
}
|
|
|
|
headers := make(map[string]string, len(request.Headers)+3)
|
|
for name, value := range request.Headers {
|
|
headers[strings.ToLower(name)] = strings.TrimSpace(value)
|
|
}
|
|
headers["host"] = parsedURL.Host
|
|
headers["x-date"] = xDate
|
|
headers["x-content-sha256"] = hex.EncodeToString(bodyHash[:])
|
|
keys := make([]string, 0, len(headers))
|
|
for name := range headers {
|
|
keys = append(keys, name)
|
|
}
|
|
sort.Strings(keys)
|
|
var canonicalHeaders strings.Builder
|
|
for _, name := range keys {
|
|
canonicalHeaders.WriteString(name)
|
|
canonicalHeaders.WriteByte(':')
|
|
canonicalHeaders.WriteString(headers[name])
|
|
canonicalHeaders.WriteByte('\n')
|
|
}
|
|
signedHeaders := strings.Join(keys, ";")
|
|
canonicalRequest := strings.Join([]string{
|
|
strings.ToUpper(request.Method), requestPath, parsedURL.Query().Encode(),
|
|
canonicalHeaders.String(), signedHeaders, hex.EncodeToString(bodyHash[:]),
|
|
}, "\n")
|
|
canonicalHash := sha256.Sum256([]byte(canonicalRequest))
|
|
scope := fmt.Sprintf("%s/%s/%s/request", shortDate, region, service)
|
|
stringToSign := fmt.Sprintf("HMAC-SHA256\n%s\n%s\n%s", xDate, scope, hex.EncodeToString(canonicalHash[:]))
|
|
sign := func(key []byte, value string) []byte {
|
|
mac := hmac.New(sha256.New, key)
|
|
_, _ = mac.Write([]byte(value))
|
|
return mac.Sum(nil)
|
|
}
|
|
signingKey := sign(sign(sign([]byte(request.SecretKey), shortDate), region), service)
|
|
signingKey = sign(signingKey, "request")
|
|
signature := hex.EncodeToString(sign(signingKey, stringToSign))
|
|
return map[string]string{
|
|
"Authorization": fmt.Sprintf("HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", request.AccessKey, scope, signedHeaders, signature),
|
|
"X-Date": xDate,
|
|
"X-Content-Sha256": hex.EncodeToString(bodyHash[:]),
|
|
}, nil
|
|
}
|