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 }