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
|
|
}
|