node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View File

@@ -0,0 +1,33 @@
package goja_bindings
import (
"errors"
"github.com/dop251/goja"
)
func gojaValueIsDefined(v goja.Value) bool {
return v != nil && !goja.IsUndefined(v) && !goja.IsNull(v)
}
func NewErrorString(vm *goja.Runtime, err string) goja.Value {
return vm.ToValue(vm.NewGoError(errors.New(err)))
}
func NewError(vm *goja.Runtime, err error) goja.Value {
return vm.ToValue(vm.NewGoError(err))
}
func PanicThrowError(vm *goja.Runtime, err error) {
goError := vm.NewGoError(err)
panic(vm.ToValue(goError))
}
func PanicThrowErrorString(vm *goja.Runtime, err string) {
goError := vm.NewGoError(errors.New(err))
panic(vm.ToValue(goError))
}
func PanicThrowTypeError(vm *goja.Runtime, err string) {
panic(vm.NewTypeError(err))
}

View File

@@ -0,0 +1,229 @@
package goja_bindings_test
import (
"errors"
"fmt"
"os"
"seanime/internal/extension"
"seanime/internal/extension_repo"
"seanime/internal/util"
"testing"
"time"
"github.com/dop251/goja"
"github.com/dop251/goja/parser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTestVM(t *testing.T) *goja.Runtime {
vm := goja.New()
vm.SetParserOptions(parser.WithDisableSourceMaps)
// Bind the shared bindings
extension_repo.ShareBinds(vm, util.NewLogger())
fm := extension_repo.FieldMapper{}
vm.SetFieldNameMapper(fm)
return vm
}
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func TestDivideFunction(t *testing.T) {
vm := goja.New()
vm.Set("divide", divide)
// Case 1: Successful division
result, err := vm.RunString("divide(10, 3);")
assert.NoError(t, err)
assert.Equal(t, 3.3333333333333335, result.Export())
// Case 2: Division by zero should throw an exception
_, err = vm.RunString("divide(10, 0);")
assert.Error(t, err)
assert.Contains(t, err.Error(), "division by zero")
// Case 3: Handling error with try-catch in JS
result, err = vm.RunString(`
try {
divide(10, 0);
} catch (e) {
e.toString();
}
`)
assert.NoError(t, err)
assert.Equal(t, "GoError: division by zero", result.Export())
}
func multipleReturns() (int, string, float64) {
return 42, "hello", 3.14
}
func TestMultipleReturns(t *testing.T) {
vm := goja.New()
vm.Set("multiReturn", multipleReturns)
v, err := vm.RunString("multiReturn();")
assert.NoError(t, err)
util.Spew(v.Export())
}
func TestUserConfig(t *testing.T) {
vm := setupTestVM(t)
defer vm.ClearInterrupt()
ext := &extension.Extension{
UserConfig: &extension.UserConfig{
Fields: []extension.ConfigField{
{
Name: "test",
},
{
Name: "test2",
Default: "Default value",
},
},
},
SavedUserConfig: &extension.SavedUserConfig{
Values: map[string]string{
"test": "Hello World!",
},
},
}
extension_repo.ShareBinds(vm, util.NewLogger())
extension_repo.BindUserConfig(vm, ext, util.NewLogger())
vm.RunString(`
const result = $getUserPreference("test");
console.log(result);
const result2 = $getUserPreference("test2");
console.log(result2);
`)
}
func TestByteSliceToUint8Array(t *testing.T) {
// Initialize a new Goja VM
vm := goja.New()
// Create a Go byte slice
data := []byte("hello")
// Set the byte slice in the Goja VM
vm.Set("data", data)
extension_repo.ShareBinds(vm, util.NewLogger())
// JavaScript code to verify the type and contents of 'data'
jsCode := `
console.log(typeof data, data);
const dataArrayBuffer = new ArrayBuffer(5);
const uint8Array = new Uint8Array(dataArrayBuffer);
uint8Array[0] = 104;
uint8Array[1] = 101;
uint8Array[2] = 108;
uint8Array[3] = 108;
uint8Array[4] = 111;
console.log(typeof uint8Array, uint8Array);
console.log("toString", $toString(uint8Array));
console.log("toString", uint8Array.toString());
true; // Return true if all checks pass
`
// Run the JavaScript code in the Goja VM
result, err := vm.RunString(jsCode)
if err != nil {
t.Fatalf("JavaScript error: %v", err)
}
// Assert that the result is true
assert.Equal(t, true, result.Export())
}
func TestGojaDocument(t *testing.T) {
vm := setupTestVM(t)
defer vm.ClearInterrupt()
tests := []struct {
entry string
}{
{entry: "./js/test/doc-example.ts"},
{entry: "./js/test/doc-example-2.ts"},
}
for _, tt := range tests {
t.Run(tt.entry, func(t *testing.T) {
fileB, err := os.ReadFile(tt.entry)
require.NoError(t, err)
now := time.Now()
source, err := extension_repo.JSVMTypescriptToJS(string(fileB))
require.NoError(t, err)
_, err = vm.RunString(source)
require.NoError(t, err)
_, err = vm.RunString(`function NewProvider() { return new Provider() }`)
require.NoError(t, err)
newProviderFunc, ok := goja.AssertFunction(vm.Get("NewProvider"))
require.True(t, ok)
classObjVal, err := newProviderFunc(goja.Undefined())
require.NoError(t, err)
classObj := classObjVal.ToObject(vm)
testFunc, ok := goja.AssertFunction(classObj.Get("test"))
require.True(t, ok)
ret, err := testFunc(classObj)
require.NoError(t, err)
promise := ret.Export().(*goja.Promise)
for promise.State() == goja.PromiseStatePending {
time.Sleep(10 * time.Millisecond)
}
if promise.State() == goja.PromiseStateFulfilled {
t.Logf("Fulfilled: %v", promise.Result())
} else {
t.Fatalf("Rejected: %v", promise.Result())
}
fmt.Println(time.Since(now).Seconds())
})
}
}
func TestOptionalParams(t *testing.T) {
vm := setupTestVM(t)
defer vm.ClearInterrupt()
type Options struct {
Add int `json:"add"`
}
vm.Set("test", func(a int, opts Options) int {
fmt.Println("opts", opts)
return a + opts.Add
})
vm.RunString(`
const result = test(1);
console.log(result);
const result2 = test(1, { add: 10 });
console.log(result2);
`)
}

View File

@@ -0,0 +1,200 @@
package goja_bindings
import (
"encoding/json"
"fmt"
"seanime/internal/events"
"seanime/internal/extension"
"strings"
"github.com/dop251/goja"
"github.com/rs/zerolog"
"github.com/samber/mo"
)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Console
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
type console struct {
logger *zerolog.Logger
vm *goja.Runtime
wsEventManager mo.Option[events.WSEventManagerInterface]
ext *extension.Extension
}
// BindConsole binds the console to the VM
func BindConsole(vm *goja.Runtime, logger *zerolog.Logger) error {
return BindConsoleWithWS(nil, vm, logger, nil)
}
// BindConsoleWithWS binds the console to the VM and sends logs messages to the websocket manager
// in order to be printed in the browser console
func BindConsoleWithWS(ext *extension.Extension, vm *goja.Runtime, logger *zerolog.Logger, wsEventManager events.WSEventManagerInterface) error {
c := &console{
logger: logger,
vm: vm,
wsEventManager: mo.None[events.WSEventManagerInterface](),
ext: ext,
}
if wsEventManager != nil {
c.wsEventManager = mo.Some(wsEventManager)
}
consoleObj := vm.NewObject()
consoleObj.Set("log", c.logFunc("log"))
consoleObj.Set("error", c.logFunc("error"))
consoleObj.Set("warn", c.logFunc("warn"))
consoleObj.Set("info", c.logFunc("info"))
consoleObj.Set("debug", c.logFunc("debug"))
vm.Set("console", consoleObj)
return nil
}
func (c *console) logFunc(t string) (ret func(c goja.FunctionCall) goja.Value) {
defer func() {
if r := recover(); r != nil {
c.logger.Error().Msgf("extension: Panic from console: %v", r)
ret = func(call goja.FunctionCall) goja.Value {
return goja.Undefined()
}
}
}()
return func(call goja.FunctionCall) goja.Value {
var ret []string
for _, arg := range call.Arguments {
// Check if the argument is a goja.Object
if obj, ok := arg.(*goja.Object); ok {
// First check if it's a Go error
if _, ok := obj.Export().(error); ok {
ret = append(ret, fmt.Sprintf("%+v", obj.Export()))
continue
}
// Then check if it's a JavaScript Error object by checking its constructor name
constructor := obj.Get("constructor")
if constructor != nil && !goja.IsUndefined(constructor) && !goja.IsNull(constructor) {
if constructorObj, ok := constructor.(*goja.Object); ok {
if name := constructorObj.Get("name"); name != nil && !goja.IsUndefined(name) && !goja.IsNull(name) {
if name.String() == "Error" || strings.HasSuffix(name.String(), "Error") {
message := obj.Get("message")
stack := obj.Get("stack")
errStr := name.String()
if message != nil && !goja.IsUndefined(message) && !goja.IsNull(message) {
errStr += ": " + fmt.Sprintf("%+v", message.Export())
}
if stack != nil && !goja.IsUndefined(stack) && !goja.IsNull(stack) {
errStr += "\nStack: " + fmt.Sprintf("%+v", stack.Export())
}
ret = append(ret, errStr)
continue
}
}
}
}
// Fallback for other objects: Try calling toString() if available
if hasOwnPropFn, ok := goja.AssertFunction(obj.Get("hasOwnProperty")); ok {
if retVal, err := hasOwnPropFn(obj, c.vm.ToValue("toString")); err == nil && retVal.ToBoolean() {
tsVal := obj.Get("toString")
if fn, ok := goja.AssertFunction(tsVal); ok {
strVal, err := fn(obj)
if err == nil {
// Avoid double logging if toString() is just "[object Object]"
if strVal.String() != "[object Object]" {
ret = append(ret, strVal.String())
continue // Skip default handling if toString() worked
}
}
}
}
}
}
// Original default handling
switch v := arg.Export().(type) {
case nil:
ret = append(ret, "undefined")
case bool:
ret = append(ret, fmt.Sprintf("%t", v))
case int64, float64:
ret = append(ret, fmt.Sprintf("%v", v))
case string:
if v == "" {
ret = append(ret, fmt.Sprintf("%q", v))
break
}
ret = append(ret, fmt.Sprintf("%s", v))
case []byte:
ret = append(ret, fmt.Sprintf("Uint8Array %s", fmt.Sprint(v)))
case map[string]interface{}:
// Try to marshal the value to JSON
bs, err := json.Marshal(v)
if err != nil {
ret = append(ret, fmt.Sprintf("%+v", v))
} else {
ret = append(ret, fmt.Sprintf("%s", string(bs)))
}
default:
// Try to marshal the value to JSON
bs, err := json.Marshal(v)
if err != nil {
ret = append(ret, fmt.Sprintf("%+v", v))
} else {
ret = append(ret, fmt.Sprintf("%s", string(bs)))
}
}
}
switch t {
case "log", "warn", "info", "debug":
c.logger.Debug().Msgf("extension: (console.%s): %s", t, strings.Join(ret, " "))
case "error":
c.logger.Error().Msgf("extension: (console.error): %s", strings.Join(ret, " "))
}
if wsEventManager, found := c.wsEventManager.Get(); found && c.ext != nil {
wsEventManager.SendEvent(events.ConsoleLog, fmt.Sprintf("%s (console.%s): %s", c.ext.ID, t, strings.Join(ret, " ")))
}
return goja.Undefined()
}
}
//func (c *console) logFunc(t string) (ret func(c goja.FunctionCall) goja.Value) {
// defer func() {
// if r := recover(); r != nil {
// c.logger.Error().Msgf("extension: Panic from console: %v", r)
// ret = func(call goja.FunctionCall) goja.Value {
// return goja.Undefined()
// }
// }
// }()
//
// return func(call goja.FunctionCall) goja.Value {
// var ret []string
// for _, arg := range call.Arguments {
// if arg == nil || arg.Export() == nil || arg.ExportType() == nil {
// ret = append(ret, "undefined")
// continue
// }
// if bytes, ok := arg.Export().([]byte); ok {
// ret = append(ret, fmt.Sprintf("%s", string(bytes)))
// continue
// }
// if arg.ExportType().Kind() == reflect.Struct || arg.ExportType().Kind() == reflect.Map || arg.ExportType().Kind() == reflect.Slice {
// ret = append(ret, strings.ReplaceAll(spew.Sprint(arg.Export()), "\n", ""))
// } else {
// ret = append(ret, fmt.Sprintf("%+v", arg.Export()))
// }
// }
//
// switch t {
// case "log", "warn", "info", "debug":
// c.logger.Debug().Msgf("extension: [console.%s] %s", t, strings.Join(ret, " "))
// case "error":
// c.logger.Error().Msgf("extension: [console.error] %s", strings.Join(ret, " "))
// }
// return goja.Undefined()
// }
//}

View File

@@ -0,0 +1,330 @@
package goja_bindings
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"github.com/dop251/goja"
)
type wordArray struct {
vm *goja.Runtime
iv []byte
data []byte
}
func BindCrypto(vm *goja.Runtime) error {
err := vm.Set("CryptoJS", map[string]interface{}{
"AES": map[string]interface{}{
"encrypt": cryptoAESEncryptFunc(vm),
"decrypt": cryptoAESDecryptFunc(vm),
},
"enc": map[string]interface{}{
"Utf8": map[string]interface{}{
"parse": cryptoEncUtf8ParseFunc(vm),
"stringify": cryptoEncUtf8StringifyFunc(vm),
},
"Base64": map[string]interface{}{
"parse": cryptoEncBase64ParseFunc(vm),
"stringify": cryptoEncBase64StringifyFunc(vm),
},
"Hex": map[string]interface{}{
"parse": cryptoEncHexParseFunc(vm),
"stringify": cryptoEncHexStringifyFunc(vm),
},
"Latin1": map[string]interface{}{
"parse": cryptoEncLatin1ParseFunc(vm),
"stringify": cryptoEncLatin1StringifyFunc(vm),
},
"Utf16": map[string]interface{}{
"parse": cryptoEncUtf16ParseFunc(vm),
"stringify": cryptoEncUtf16StringifyFunc(vm),
},
"Utf16LE": map[string]interface{}{
"parse": cryptoEncUtf16LEParseFunc(vm),
"stringify": cryptoEncUtf16LEStringifyFunc(vm),
},
},
})
if err != nil {
return err
}
return nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func newWordArrayGojaValue(vm *goja.Runtime, data []byte, iv []byte) goja.Value {
wa := &wordArray{
vm: vm,
iv: iv,
data: data,
}
// WordArray // Utf8
// WordArray.toString(): string // Uses Base64.stringify
// WordArray.toString(encoder: Encoder): string
obj := vm.NewObject()
obj.Prototype().Set("toString", wa.toStringFunc)
obj.Set("toString", wa.toStringFunc)
obj.Set("iv", iv)
return obj
}
func (wa *wordArray) toStringFunc(call goja.FunctionCall) goja.Value {
if len(call.Arguments) == 0 {
return wa.vm.ToValue(base64Stringify(wa.data))
}
encoder, ok := call.Argument(0).Export().(map[string]interface{})
if !ok {
panic(wa.vm.ToValue("TypeError: encoder parameter must be a CryptoJS.enc object"))
}
var ret string
if f, ok := encoder["stringify"]; ok {
if stringify, ok := f.(func(functionCall goja.FunctionCall) goja.Value); ok {
ret = stringify(goja.FunctionCall{Arguments: []goja.Value{wa.vm.ToValue(wa.data)}}).String()
} else {
panic(wa.vm.ToValue("TypeError: encoder.stringify must be a function"))
}
} else {
ret = string(wa.data)
}
return wa.vm.ToValue(ret)
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CryptoJS.AES.encrypt(message: string, key: string, cfg?: { iv: ArrayBuffer }): WordArray
func cryptoAESEncryptFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) (ret goja.Value) {
if len(call.Arguments) < 2 {
panic(vm.ToValue("TypeError: AES.encrypt requires at least 2 arguments"))
}
message := call.Argument(0).String()
var keyBytes []byte
switch call.Argument(1).Export().(type) {
case string:
key := call.Argument(1).String()
keyBytes = adjustKeyLength([]byte(key))
case []byte:
keyBytes = call.Argument(1).Export().([]byte)
keyBytes = adjustKeyLength(keyBytes)
default:
panic(vm.ToValue("TypeError: key parameter must be a string or an ArrayBuffer"))
}
usedRandomIV := false
// Check if IV is provided
var ivBytes []byte
if len(call.Arguments) > 2 {
cfg := call.Argument(2).Export().(map[string]interface{})
var ok bool
iv, ok := cfg["iv"].([]byte)
if !ok {
panic(vm.ToValue("TypeError: iv parameter must be an ArrayBuffer"))
}
ivBytes = iv
if len(ivBytes) != aes.BlockSize {
panic(vm.ToValue("TypeError: IV length must be equal to block size (16 bytes for AES)"))
}
} else {
// Generate a random IV
ivBytes = make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, ivBytes); err != nil {
panic(vm.ToValue(fmt.Sprintf("Failed to generate IV: %v", err)))
}
usedRandomIV = true
}
defer func() {
if r := recover(); r != nil {
ret = vm.ToValue(fmt.Sprintf("Encryption failed: %v", r))
}
}()
// Encrypt the message
encryptedMessage := encryptAES(vm, message, keyBytes, ivBytes)
if usedRandomIV {
// Prepend the IV to the encrypted message
encryptedMessage = append(ivBytes, encryptedMessage...)
}
return newWordArrayGojaValue(vm, encryptedMessage, ivBytes)
}
}
// CryptoJS.AES.decrypt(encryptedMessage: string | WordArray, key: string, cfg?: { iv: ArrayBuffer }): WordArray
func cryptoAESDecryptFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) (ret goja.Value) {
if len(call.Arguments) < 2 {
panic(vm.ToValue("TypeError: AES.decrypt requires at least 2 arguments"))
}
// Can be string or WordArray
// If WordArray, String() will call WordArray.toString() which will return the base64 encoded string
encryptedMessage := call.Argument(0).String()
var keyBytes []byte
var originalPassword []byte
switch call.Argument(1).Export().(type) {
case string:
key := call.Argument(1).String()
originalPassword = []byte(key)
keyBytes = adjustKeyLength([]byte(key))
case []byte:
keyBytes = call.Argument(1).Export().([]byte)
originalPassword = keyBytes
keyBytes = adjustKeyLength(keyBytes)
default:
panic(vm.ToValue("TypeError: key parameter must be a string or an ArrayBuffer"))
}
var ivBytes []byte
var cipherText []byte
// If IV is provided in the third argument
if len(call.Arguments) > 2 {
cfg := call.Argument(2).Export().(map[string]interface{})
var ok bool
iv, ok := cfg["iv"].([]byte)
if !ok {
panic(vm.ToValue("TypeError: iv parameter must be an ArrayBuffer"))
}
ivBytes = iv
if len(ivBytes) != aes.BlockSize {
panic(vm.ToValue("TypeError: IV length must be equal to block size (16 bytes for AES)"))
}
var err error
decodedMessage, err := base64.StdEncoding.DecodeString(encryptedMessage)
if err != nil {
panic(vm.ToValue(fmt.Sprintf("Failed to decode ciphertext: %v", err)))
}
cipherText = decodedMessage
} else {
// Decode the base64 encoded string
decodedMessage, err := base64.StdEncoding.DecodeString(encryptedMessage)
if err != nil {
panic(vm.ToValue(fmt.Sprintf("Failed to decode ciphertext: %v", err)))
}
// Check if openssl
if len(decodedMessage) >= 16 && string(decodedMessage[:8]) == "Salted__" {
salt := decodedMessage[8:16]
cipherText = decodedMessage[16:]
derivedKey, derivedIV := evpBytesToKey(originalPassword, salt, 32, aes.BlockSize)
keyBytes = derivedKey
ivBytes = derivedIV
} else {
// Extract the IV from the beginning of the message
ivBytes = decodedMessage[:aes.BlockSize]
cipherText = decodedMessage[aes.BlockSize:]
}
}
// Decrypt the message
decrypted := decryptAES(vm, cipherText, keyBytes, ivBytes)
return newWordArrayGojaValue(vm, decrypted, ivBytes)
}
}
// Adjusts the key length to match AES key length requirements (16, 24, or 32 bytes).
// If the key length is not 16, 24, or 32 bytes, it is hashed using SHA-256 and truncated to 32 bytes (AES-256).
func adjustKeyLength(keyBytes []byte) []byte {
switch len(keyBytes) {
case 16, 24, 32:
// Valid AES key lengths: 16 bytes (AES-128), 24 bytes (AES-192), 32 bytes (AES-256)
return keyBytes
default:
// Hash the key to 32 bytes (AES-256)
hash := sha256.Sum256(keyBytes)
return hash[:]
}
}
func encryptAES(vm *goja.Runtime, message string, key []byte, iv []byte) (ret []byte) {
defer func() {
if r := recover(); r != nil {
ret = nil
}
}()
block, err := aes.NewCipher(key)
if err != nil {
panic(vm.ToValue(fmt.Sprintf("%v", err)))
}
messageBytes := []byte(message)
messageBytes = pkcs7Padding(messageBytes, aes.BlockSize)
cipherText := make([]byte, len(messageBytes))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(cipherText, messageBytes)
return cipherText
}
func decryptAES(vm *goja.Runtime, cipherText []byte, key []byte, iv []byte) (ret []byte) {
defer func() {
if r := recover(); r != nil {
ret = nil
}
}()
block, err := aes.NewCipher(key)
if err != nil {
panic(vm.ToValue(fmt.Sprintf("%v", err)))
}
plainText := make([]byte, len(cipherText))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(plainText, cipherText)
plainText = pkcs7Trimming(plainText)
return plainText
}
func pkcs7Padding(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, padText...)
}
func pkcs7Trimming(data []byte) []byte {
length := len(data)
up := int(data[length-1])
return data[:(length - up)]
}
func evpBytesToKey(password []byte, salt []byte, keyLen, ivLen int) ([]byte, []byte) {
d := make([]byte, 0)
dI := make([]byte, 0)
for len(d) < (keyLen + ivLen) {
h := md5.New()
h.Write(dI)
h.Write(password)
h.Write(salt)
dI = h.Sum(nil)
d = append(d, dI...)
}
return d[:keyLen], d[keyLen : keyLen+ivLen]
}

View File

@@ -0,0 +1,296 @@
package goja_bindings
import (
"encoding/base64"
"encoding/hex"
"github.com/dop251/goja"
"golang.org/x/text/encoding/charmap"
"unicode/utf16"
)
// UTF-8 Encode
func utf8Parse(input string) []byte {
return []byte(input)
}
// UTF-8 Decode
func utf8Stringify(input []byte) string {
return string(input)
}
// Base64 Encode
func base64Parse(input string) []byte {
data, err := base64.StdEncoding.DecodeString(input)
if err != nil {
return nil
}
return data
}
// Base64 Decode
func base64Stringify(input []byte) string {
return base64.StdEncoding.EncodeToString(input)
}
// Hex Encode
func hexParse(input string) []byte {
data, err := hex.DecodeString(input)
if err != nil {
return nil
}
return data
}
// Hex Decode
func hexStringify(input []byte) string {
return hex.EncodeToString(input)
}
// Latin1 Encode
func latin1Parse(input string) []byte {
encoder := charmap.ISO8859_1.NewEncoder()
data, _ := encoder.Bytes([]byte(input))
return data
}
// Latin1 Decode
func latin1Stringify(input []byte) string {
decoder := charmap.ISO8859_1.NewDecoder()
data, _ := decoder.Bytes(input)
return string(data)
}
// UTF-16 Encode
func utf16Parse(input string) []byte {
encoded := utf16.Encode([]rune(input))
result := make([]byte, len(encoded)*2)
for i, val := range encoded {
result[i*2] = byte(val >> 8)
result[i*2+1] = byte(val)
}
return result
}
// UTF-16 Decode
func utf16Stringify(input []byte) string {
if len(input)%2 != 0 {
return ""
}
decoded := make([]uint16, len(input)/2)
for i := 0; i < len(decoded); i++ {
decoded[i] = uint16(input[i*2])<<8 | uint16(input[i*2+1])
}
return string(utf16.Decode(decoded))
}
// UTF-16LE Encode
func utf16LEParse(input string) []byte {
encoded := utf16.Encode([]rune(input))
result := make([]byte, len(encoded)*2)
for i, val := range encoded {
result[i*2] = byte(val)
result[i*2+1] = byte(val >> 8)
}
return result
}
// UTF-16LE Decode
func utf16LEStringify(input []byte) string {
if len(input)%2 != 0 {
return ""
}
decoded := make([]uint16, len(input)/2)
for i := 0; i < len(decoded); i++ {
decoded[i] = uint16(input[i*2]) | uint16(input[i*2+1])<<8
}
return string(utf16.Decode(decoded))
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// CryptoJS.enc.Utf8.parse(input: string): WordArray
func cryptoEncUtf8ParseFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Utf8.parse requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val := call.Argument(0).String()
return vm.ToValue(utf8Parse(val))
}
}
// CryptoJS.enc.Utf8.stringify(wordArray: WordArray): string
func cryptoEncUtf8StringifyFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Utf8.stringify requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().([]byte)
if !ok {
return vm.ToValue("")
}
return vm.ToValue(utf8Stringify(val))
}
}
// CryptoJS.enc.Base64.parse(input: string): WordArray
// e.g.
func cryptoEncBase64ParseFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Base64.parse requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val := call.Argument(0).String()
return vm.ToValue(base64Parse(val))
}
}
// CryptoJS.enc.Base64.stringify(wordArray: WordArray): string
func cryptoEncBase64StringifyFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Base64.stringify requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().([]byte)
if !ok {
return vm.ToValue("")
}
return vm.ToValue(base64Stringify(val))
}
}
// CryptoJS.enc.Hex.parse(input: string): WordArray
func cryptoEncHexParseFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Hex.parse requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val := call.Argument(0).String()
return vm.ToValue(hexParse(val))
}
}
// CryptoJS.enc.Hex.stringify(wordArray: WordArray): string
func cryptoEncHexStringifyFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Hex.stringify requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().([]byte)
if !ok {
return vm.ToValue("")
}
return vm.ToValue(hexStringify(val))
}
}
// CryptoJS.enc.Latin1.parse(input: string): WordArray
func cryptoEncLatin1ParseFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Latin1.parse requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val := call.Argument(0).String()
return vm.ToValue(latin1Parse(val))
}
}
// CryptoJS.enc.Latin1.stringify(wordArray: WordArray): string
func cryptoEncLatin1StringifyFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Latin1.stringify requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().([]byte)
if !ok {
return vm.ToValue("")
}
return vm.ToValue(latin1Stringify(val))
}
}
// CryptoJS.enc.Utf16.parse(input: string): WordArray
func cryptoEncUtf16ParseFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Utf16.parse requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val := call.Argument(0).String()
return vm.ToValue(utf16Parse(val))
}
}
// CryptoJS.enc.Utf16.stringify(wordArray: WordArray): string
func cryptoEncUtf16StringifyFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Utf16.stringify requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().([]byte)
if !ok {
return vm.ToValue("")
}
return vm.ToValue(utf16Stringify(val))
}
}
// CryptoJS.enc.Utf16LE.parse(input: string): WordArray
func cryptoEncUtf16LEParseFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Utf16LE.parse requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val := call.Argument(0).String()
return vm.ToValue(utf16LEParse(val))
}
}
// CryptoJS.enc.Utf16LE.stringify(wordArray: WordArray): string
func cryptoEncUtf16LEStringifyFunc(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.enc.Utf16LE.stringify requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().([]byte)
if !ok {
return vm.ToValue("")
}
return vm.ToValue(utf16LEStringify(val))
}
}

View File

@@ -0,0 +1,120 @@
package goja_bindings
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"github.com/dop251/goja"
"golang.org/x/crypto/ripemd160"
"golang.org/x/crypto/sha3"
)
// MD5 Hash
func cryptoMD5Func(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.MD5 requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().(string)
if !ok {
panic(vm.ToValue("TypeError: argument is not a string"))
}
hash := md5.Sum([]byte(val))
return vm.ToValue(hash[:])
}
}
// SHA1 Hash
func cryptoSHA1Func(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.SHA1 requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().(string)
if !ok {
panic(vm.ToValue("TypeError: argument is not a string"))
}
hash := sha1.Sum([]byte(val))
return vm.ToValue(hash[:])
}
}
// SHA256 Hash
func cryptoSHA256Func(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.SHA256 requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().(string)
if !ok {
panic(vm.ToValue("TypeError: argument is not a string"))
}
hash := sha256.Sum256([]byte(val))
return vm.ToValue(hash[:])
}
}
// SHA512 Hash
func cryptoSHA512Func(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.SHA512 requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().(string)
if !ok {
panic(vm.ToValue("TypeError: argument is not a string"))
}
hash := sha512.Sum512([]byte(val))
return vm.ToValue(hash[:])
}
}
// SHA3 Hash
func cryptoSHA3Func(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.SHA3 requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().(string)
if !ok {
panic(vm.ToValue("TypeError: argument is not a string"))
}
hash := sha3.Sum256([]byte(val))
return vm.ToValue(hash[:])
}
}
// RIPEMD-160 Hash
func cryptoRIPEMD160Func(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
return func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: CryptoJS.RIPEMD160 requires at least 1 argument"))
}
if !gojaValueIsDefined(call.Arguments[0]) {
return vm.ToValue("")
}
val, ok := call.Argument(0).Export().(string)
if !ok {
panic(vm.ToValue("TypeError: argument is not a string"))
}
hasher := ripemd160.New()
hasher.Write([]byte(val))
return vm.ToValue(hasher.Sum(nil))
}
}

View File

@@ -0,0 +1,194 @@
package goja_bindings
import (
"seanime/internal/util"
"testing"
"time"
"github.com/dop251/goja"
gojabuffer "github.com/dop251/goja_nodejs/buffer"
gojarequire "github.com/dop251/goja_nodejs/require"
"github.com/stretchr/testify/require"
)
func TestGojaCrypto(t *testing.T) {
vm := goja.New()
defer vm.ClearInterrupt()
registry := new(gojarequire.Registry)
registry.Enable(vm)
gojabuffer.Enable(vm)
BindCrypto(vm)
BindConsole(vm, util.NewLogger())
_, err := vm.RunString(`
async function run() {
try {
console.log("\nTesting Buffer encoding/decoding")
const originalString = "Hello, this is a string to encode!"
const base64String = Buffer.from(originalString).toString("base64")
console.log("Original String:", originalString)
console.log("Base64 Encoded:", base64String)
const decodedString = Buffer.from(base64String, "base64").toString("utf-8")
console.log("Base64 Decoded:", decodedString)
}
catch (e) {
console.error(e)
}
try {
console.log("\nTesting AES")
let message = "seanime"
let key = CryptoJS.enc.Utf8.parse("secret key")
console.log("Message:", message)
let encrypted = CryptoJS.AES.encrypt(message, key)
console.log("Encrypted without IV:", encrypted) // map[iv toString]
console.log("Encrypted.toString():", encrypted.toString()) // AoHrnhJfbRht2idLHM82WdkIEpRbXufnA6+ozty9fbk=
console.log("Encrypted.toString(CryptoJS.enc.Base64):", encrypted.toString(CryptoJS.enc.Base64)) // AoHrnhJfbRht2idLHM82WdkIEpRbXufnA6+ozty9fbk=
let decrypted = CryptoJS.AES.decrypt(encrypted, key)
console.log("Decrypted:", decrypted.toString(CryptoJS.enc.Utf8))
let iv = CryptoJS.enc.Utf8.parse("3134003223491201")
encrypted = CryptoJS.AES.encrypt(message, key, { iv: iv })
console.log("Encrypted with IV:", encrypted) // map[iv toString]
decrypted = CryptoJS.AES.decrypt(encrypted, key)
console.log("Decrypted without IV:", decrypted.toString(CryptoJS.enc.Utf8))
decrypted = CryptoJS.AES.decrypt(encrypted, key, { iv: iv })
console.log("Decrypted with IV:", decrypted.toString(CryptoJS.enc.Utf8)) // seanime
}
catch (e) {
console.error(e)
}
try {
console.log("\nTesting encoders")
console.log("")
let a = CryptoJS.enc.Utf8.parse("Hello, World!")
console.log("Base64 Parsed:", a)
let b = CryptoJS.enc.Base64.stringify(a)
console.log("Base64 Stringified:", b)
let c = CryptoJS.enc.Base64.parse(b)
console.log("Base64 Parsed:", c)
let d = CryptoJS.enc.Utf8.stringify(c)
console.log("Base64 Stringified:", d)
console.log("")
let words = CryptoJS.enc.Latin1.parse("Hello, World!")
console.log("Latin1 Parsed:", words)
let latin1 = CryptoJS.enc.Latin1.stringify(words)
console.log("Latin1 Stringified", latin1)
words = CryptoJS.enc.Hex.parse("48656c6c6f2c20576f726c6421")
console.log("Hex Parsed:", words)
let hex = CryptoJS.enc.Hex.stringify(words)
console.log("Hex Stringified", hex)
words = CryptoJS.enc.Utf8.parse("𔭢")
console.log("Utf8 Parsed:", words)
let utf8 = CryptoJS.enc.Utf8.stringify(words)
console.log("Utf8 Stringified", utf8)
words = CryptoJS.enc.Utf16.parse("Hello, World!")
console.log("Utf16 Parsed:", words)
let utf16 = CryptoJS.enc.Utf16.stringify(words)
console.log("Utf16 Stringified", utf16)
words = CryptoJS.enc.Utf16LE.parse("Hello, World!")
console.log("Utf16LE Parsed:", words)
utf16 = CryptoJS.enc.Utf16LE.stringify(words)
console.log("Utf16LE Stringified", utf16)
}
catch (e) {
console.error("Error:", e)
}
}
`)
require.NoError(t, err)
runFunc, ok := goja.AssertFunction(vm.Get("run"))
require.True(t, ok)
ret, err := runFunc(goja.Undefined())
require.NoError(t, err)
promise := ret.Export().(*goja.Promise)
for promise.State() == goja.PromiseStatePending {
time.Sleep(10 * time.Millisecond)
}
if promise.State() == goja.PromiseStateRejected {
err := promise.Result()
t.Fatal(err)
}
}
func TestGojaCryptoOpenSSL(t *testing.T) {
vm := goja.New()
defer vm.ClearInterrupt()
registry := new(gojarequire.Registry)
registry.Enable(vm)
gojabuffer.Enable(vm)
BindCrypto(vm)
BindConsole(vm, util.NewLogger())
_, err := vm.RunString(`
async function run() {
try {
console.log("\nTesting Buffer encoding/decoding")
const payload = "U2FsdGVkX19ZanX9W5jQGgNGOIOBGxhY6gxa1EHnRi3yHL8Ml4cMmQeryf9p04N12VuOjiBas21AcU0Ypc4dB4AWOdc9Cn1wdA2DuQhryUonKYHwV/XXJ53DBn1OIqAvrIAxrN8S2j9Rk5z/F/peu1Kk/d3m82jiKvhTWQcxDeDW8UzCMZbbFnm4qJC3k19+PD5Pal5sBcVTGRXNCpvSSpYb56FcP9Xs+3DyBWhNUqJuO+Wwm3G1J5HhklxCWZ7tcn7TE5Y8d5ORND7t51Padrw4LgEOootqHtfHuBVX6EqlvJslXt0kFgcXJUIO+hw0q5SJ+tiS7o/2OShJ7BCk4XzfQmhFJdBJYGjQ8WPMHYzLuMzDkf6zk2+m7YQtUTXx8SVoLXFOt8gNZeD942snGrWA5+CdYveOfJ8Yv7owoOueMzzYqr5rzG7GVapVI0HzrA24LR4AjRDICqTsJEy6Yg=="
const key = "6315b93606d60f48c964b67b14701f3848ef25af01296cf7e6a98c9460e1d2ac"
console.log("Original String:", payload)
const decrypted = CryptoJS.AES.decrypt(payload, key)
console.log("Decrypted:", decrypted.toString(CryptoJS.enc.Utf8))
}
catch (e) {
console.error(e)
}
}
`)
require.NoError(t, err)
runFunc, ok := goja.AssertFunction(vm.Get("run"))
require.True(t, ok)
ret, err := runFunc(goja.Undefined())
require.NoError(t, err)
promise := ret.Export().(*goja.Promise)
for promise.State() == goja.PromiseStatePending {
time.Sleep(10 * time.Millisecond)
}
if promise.State() == goja.PromiseStateRejected {
err := promise.Result()
t.Fatal(err)
}
}

View File

@@ -0,0 +1,679 @@
package goja_bindings
import (
"fmt"
"github.com/PuerkitoBio/goquery"
"github.com/dop251/goja"
"strings"
)
type doc struct {
vm *goja.Runtime
doc *goquery.Document
docSelection *docSelection
}
type docSelection struct {
doc *doc
selection *goquery.Selection
}
func setSelectionObjectProperties(obj *goja.Object, docS *docSelection) {
_ = obj.Set("length", docS.Length)
_ = obj.Set("html", docS.Html)
_ = obj.Set("text", docS.Text)
_ = obj.Set("attr", docS.Attr)
_ = obj.Set("find", docS.Find)
_ = obj.Set("children", docS.Children)
_ = obj.Set("each", docS.Each)
_ = obj.Set("text", docS.Text)
_ = obj.Set("parent", docS.Parent)
_ = obj.Set("parentsUntil", docS.ParentsUntil)
_ = obj.Set("parents", docS.Parents)
_ = obj.Set("end", docS.End)
_ = obj.Set("closest", docS.Closest)
_ = obj.Set("map", docS.Map)
_ = obj.Set("first", docS.First)
_ = obj.Set("last", docS.Last)
_ = obj.Set("eq", docS.Eq)
_ = obj.Set("contents", docS.Contents)
_ = obj.Set("contentsFiltered", docS.ContentsFiltered)
_ = obj.Set("filter", docS.Filter)
_ = obj.Set("not", docS.Not)
_ = obj.Set("is", docS.Is)
_ = obj.Set("has", docS.Has)
_ = obj.Set("next", docS.Next)
_ = obj.Set("nextAll", docS.NextAll)
_ = obj.Set("nextUntil", docS.NextUntil)
_ = obj.Set("prev", docS.Prev)
_ = obj.Set("prevAll", docS.PrevAll)
_ = obj.Set("prevUntil", docS.PrevUntil)
_ = obj.Set("siblings", docS.Siblings)
_ = obj.Set("data", docS.Data)
_ = obj.Set("attrs", docS.Attrs)
}
func BindDocument(vm *goja.Runtime) error {
// Set Doc "class"
err := vm.Set("Doc", func(call goja.ConstructorCall) *goja.Object {
obj := call.This
if len(call.Arguments) != 1 {
return goja.Undefined().ToObject(vm)
}
html := call.Arguments[0].String()
goqueryDoc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
return goja.Undefined().ToObject(vm)
}
d := &doc{
vm: vm,
doc: goqueryDoc,
docSelection: &docSelection{
doc: nil,
selection: goqueryDoc.Selection,
},
}
d.docSelection.doc = d
setSelectionObjectProperties(obj, d.docSelection)
return obj
})
if err != nil {
return err
}
// Set "LoadDoc" function
err = vm.Set("LoadDoc", func(call goja.FunctionCall) goja.Value {
if len(call.Arguments) != 1 {
panic(vm.ToValue("missing argument"))
}
html := call.Arguments[0].String()
goqueryDoc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
return goja.Null()
}
d := &doc{
vm: vm,
doc: goqueryDoc,
docSelection: &docSelection{
doc: nil,
selection: goqueryDoc.Selection,
},
}
d.docSelection.doc = d
docSelectionFunction := func(call goja.FunctionCall) goja.Value {
selectorStr, ok := call.Argument(0).Export().(string)
if !ok {
panic(vm.NewTypeError("argument is not a string").ToString())
}
return newDocSelectionGojaValue(d, d.doc.Find(selectorStr))
}
return vm.ToValue(docSelectionFunction)
})
return nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Document
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func newDocSelectionGojaValue(d *doc, selection *goquery.Selection) goja.Value {
ds := &docSelection{
doc: d,
selection: selection,
}
obj := d.vm.NewObject()
setSelectionObjectProperties(obj, ds)
return obj
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Selection
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (s *docSelection) getFirstStringArg(call goja.FunctionCall) string {
selectorStr, ok := call.Argument(0).Export().(string)
if !ok {
panic(s.doc.vm.NewTypeError("argument is not a string").ToString())
}
return selectorStr
}
func (s *docSelection) Length(call goja.FunctionCall) goja.Value {
if s.selection == nil {
return s.doc.vm.ToValue(0)
}
return s.doc.vm.ToValue(s.selection.Length())
}
// Find gets the descendants of each element in the current set of matched elements, filtered by a selector.
//
// find(selector: string): DocSelection;
func (s *docSelection) Find(call goja.FunctionCall) (ret goja.Value) {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.Find(selectorStr))
}
func (s *docSelection) Html(call goja.FunctionCall) goja.Value {
if s.selection == nil {
return goja.Null()
}
htmlStr, err := s.selection.Html()
if err != nil {
return goja.Null()
}
return s.doc.vm.ToValue(htmlStr)
}
func (s *docSelection) Text(call goja.FunctionCall) goja.Value {
if s.selection == nil {
return s.doc.vm.ToValue("")
}
return s.doc.vm.ToValue(s.selection.Text())
}
// Attr gets the specified attribute's value for the first element in the Selection. To get the value for each element individually, use a
// looping construct such as Each or Map method.
//
// attr(name: string): string | undefined;
func (s *docSelection) Attr(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
attr, found := s.selection.Attr(s.getFirstStringArg(call))
if !found {
return goja.Undefined()
}
return s.doc.vm.ToValue(attr)
}
// Attrs gets all attributes for the first element in the Selection.
//
// attrs(): { [key: string]: string };
func (s *docSelection) Attrs(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
attrs := make(map[string]string)
for _, v := range s.selection.Get(0).Attr {
attrs[v.Key] = v.Val
}
return s.doc.vm.ToValue(attrs)
}
// Data gets data associated with the matched elements or return the value at the named data store for the first element in the set of matched elements.
//
// data(name?: string): { [key: string]: string } | string | undefined;
func (s *docSelection) Data(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
var data map[string]string
n := s.selection.Get(0)
if n == nil {
return goja.Undefined()
}
for _, v := range n.Attr {
if strings.HasPrefix(v.Key, "data-") {
if data == nil {
data = make(map[string]string)
}
data[v.Key] = v.Val
}
}
return s.doc.vm.ToValue(data)
}
name := call.Argument(0).String()
n := s.selection.Get(0)
if n == nil {
return goja.Undefined()
}
data, found := s.selection.Attr(fmt.Sprintf("data-%s", name))
if !found {
return goja.Undefined()
}
return s.doc.vm.ToValue(data)
}
// Parent gets the parent of each element in the Selection. It returns a new Selection object containing the matched elements.
//
// parent(selector?: string): DocSelection;
func (s *docSelection) Parent(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
return newDocSelectionGojaValue(s.doc, s.selection.Parent())
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.ParentFiltered(selectorStr))
}
// Parents gets the ancestors of each element in the current Selection. It returns a new Selection object with the matched elements.
//
// parents(selector?: string): DocSelection;
func (s *docSelection) Parents(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
return newDocSelectionGojaValue(s.doc, s.selection.Parents())
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.ParentsFiltered(selectorStr))
}
// ParentsUntil gets the ancestors of each element in the Selection, up to but not including the element matched by the selector. It returns a
// new Selection object containing the matched elements.
//
// parentsUntil(selector?: string, until?: string): DocSelection;
func (s *docSelection) ParentsUntil(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
selectorStr := s.getFirstStringArg(call)
if len(call.Arguments) < 2 {
return newDocSelectionGojaValue(s.doc, s.selection.ParentsUntil(selectorStr))
}
untilStr := call.Argument(1).String()
return newDocSelectionGojaValue(s.doc, s.selection.ParentsFilteredUntil(selectorStr, untilStr))
}
// End ends the most recent filtering operation in the current chain and returns the set of matched elements to its previous state.
//
// end(): DocSelection;
func (s *docSelection) End(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
return newDocSelectionGojaValue(s.doc, s.selection.End())
}
// Closest gets the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
//
// closest(selector?: string): DocSelection;
func (s *docSelection) Closest(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
return newDocSelectionGojaValue(s.doc, s.selection.Closest(""))
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.Closest(selectorStr))
}
// Next gets the next sibling of each selected element, optionally filtered by a selector.
//
// next(selector?: string): DocSelection;
func (s *docSelection) Next(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
return newDocSelectionGojaValue(s.doc, s.selection.Next())
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.NextFiltered(selectorStr))
}
// NextAll gets all following siblings of each element in the Selection, optionally filtered by a selector.
//
// nextAll(selector?: string): DocSelection;
func (s *docSelection) NextAll(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
return newDocSelectionGojaValue(s.doc, s.selection.NextAll())
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.NextAllFiltered(selectorStr))
}
// NextUntil gets all following siblings of each element up to but not including the element matched by the selector.
//
// nextUntil(selector: string, until?: string): DocSelection;
func (s *docSelection) NextUntil(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
selectorStr := s.getFirstStringArg(call)
if len(call.Arguments) < 2 {
return newDocSelectionGojaValue(s.doc, s.selection.NextUntil(selectorStr))
}
untilStr := call.Argument(1).String()
return newDocSelectionGojaValue(s.doc, s.selection.NextFilteredUntil(selectorStr, untilStr))
}
// Prev gets the previous sibling of each selected element optionally filtered by a selector.
//
// prev(selector?: string): DocSelection;
func (s *docSelection) Prev(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
return newDocSelectionGojaValue(s.doc, s.selection.Prev())
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.PrevFiltered(selectorStr))
}
// PrevAll gets all preceding siblings of each element in the Selection, optionally filtered by a selector.
//
// prevAll(selector?: string): DocSelection;
func (s *docSelection) PrevAll(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
return newDocSelectionGojaValue(s.doc, s.selection.PrevAll())
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.PrevAllFiltered(selectorStr))
}
// PrevUntil gets all preceding siblings of each element up to but not including the element matched by the selector.
//
// prevUntil(selector: string, until?: string): DocSelection;
func (s *docSelection) PrevUntil(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
selectorStr := s.getFirstStringArg(call)
if len(call.Arguments) < 2 {
return newDocSelectionGojaValue(s.doc, s.selection.PrevUntil(selectorStr))
}
untilStr := call.Argument(1).String()
return newDocSelectionGojaValue(s.doc, s.selection.PrevFilteredUntil(selectorStr, untilStr))
}
// Siblings gets the siblings of each element (excluding the element) in the set of matched elements, optionally filtered by a selector.
//
// siblings(selector?: string): DocSelection;
func (s *docSelection) Siblings(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
return newDocSelectionGojaValue(s.doc, s.selection.Siblings())
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.SiblingsFiltered(selectorStr))
}
// Children gets the element children of each element in the set of matched elements.
//
// children(selector?: string): DocSelection;
func (s *docSelection) Children(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
return newDocSelectionGojaValue(s.doc, s.selection.Children())
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.ChildrenFiltered(selectorStr))
}
// Contents gets the children of each element in the Selection, including text and comment nodes. It returns a new Selection object containing
// these elements.
//
// contents(): DocSelection;
func (s *docSelection) Contents(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
return newDocSelectionGojaValue(s.doc, s.selection.Contents())
}
// ContentsFiltered gets the children of each element in the Selection, filtered by the specified selector. It returns a new Selection object
// containing these elements. Since selectors only act on Element nodes, this function is an alias to ChildrenFiltered unless the selector is
// empty, in which case it is an alias to Contents.
//
// contentsFiltered(selector: string): DocSelection;
func (s *docSelection) ContentsFiltered(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.ContentsFiltered(selectorStr))
}
// Filter reduces the set of matched elements to those that match the selector string. It returns a new Selection object for this subset of
// matching elements.
//
// filter(selector: string | (index: number, element: DocSelection) => boolean): DocSelection;
func (s *docSelection) Filter(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
panic(s.doc.vm.ToValue("missing argument"))
}
switch call.Argument(0).Export().(type) {
case string:
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.Filter(selectorStr))
case func(call goja.FunctionCall) goja.Value:
callback := call.Argument(0).Export().(func(call goja.FunctionCall) goja.Value)
return newDocSelectionGojaValue(s.doc, s.selection.FilterFunction(func(i int, selection *goquery.Selection) bool {
ret, ok := callback(goja.FunctionCall{Arguments: []goja.Value{
s.doc.vm.ToValue(i),
newDocSelectionGojaValue(s.doc, selection),
}}).Export().(bool)
if !ok {
panic(s.doc.vm.NewTypeError("callback did not return a boolean").ToString())
}
return ret
}))
default:
panic(s.doc.vm.NewTypeError("argument is not a string or function").ToString())
}
}
// Not removes elements from the Selection that match the selector string. It returns a new Selection object with the matching elements removed.
//
// not(selector: string | (index: number, element: DocSelection) => boolean): DocSelection;
func (s *docSelection) Not(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
panic(s.doc.vm.ToValue("missing argument"))
}
switch call.Argument(0).Export().(type) {
case string:
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.Not(selectorStr))
case func(call goja.FunctionCall) goja.Value:
callback := call.Argument(0).Export().(func(call goja.FunctionCall) goja.Value)
return newDocSelectionGojaValue(s.doc, s.selection.NotFunction(func(i int, selection *goquery.Selection) bool {
ret, ok := callback(goja.FunctionCall{Arguments: []goja.Value{
s.doc.vm.ToValue(i),
newDocSelectionGojaValue(s.doc, selection),
}}).Export().(bool)
if !ok {
panic(s.doc.vm.NewTypeError("callback did not return a boolean").ToString())
}
return ret
}))
default:
panic(s.doc.vm.NewTypeError("argument is not a string or function").ToString())
}
}
// Is checks the current matched set of elements against a selector and returns true if at least one of these elements matches.
//
// is(selector: string | (index: number, element: DocSelection) => boolean): boolean;
func (s *docSelection) Is(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
if len(call.Arguments) == 0 || !gojaValueIsDefined(call.Argument(0)) {
panic(s.doc.vm.ToValue("missing argument"))
}
switch call.Argument(0).Export().(type) {
case string:
selectorStr := s.getFirstStringArg(call)
return s.doc.vm.ToValue(s.selection.Is(selectorStr))
case func(call goja.FunctionCall) goja.Value:
callback := call.Argument(0).Export().(func(call goja.FunctionCall) goja.Value)
return s.doc.vm.ToValue(s.selection.IsFunction(func(i int, selection *goquery.Selection) bool {
ret, ok := callback(goja.FunctionCall{Arguments: []goja.Value{
s.doc.vm.ToValue(i),
newDocSelectionGojaValue(s.doc, selection),
}}).Export().(bool)
if !ok {
panic(s.doc.vm.NewTypeError("callback did not return a boolean").ToString())
}
return ret
}))
default:
panic(s.doc.vm.NewTypeError("argument is not a string or function").ToString())
}
}
// Has reduces the set of matched elements to those that have a descendant that matches the selector. It returns a new Selection object with the
// matching elements.
//
// has(selector: string): DocSelection;
func (s *docSelection) Has(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
selectorStr := s.getFirstStringArg(call)
return newDocSelectionGojaValue(s.doc, s.selection.Has(selectorStr))
}
// Each iterates over a Selection object, executing a function for each matched element. It returns the current Selection object. The function f
// is called for each element in the selection with the index of the element in that selection starting at 0, and a *Selection that contains only
// that element.
//
// each(callback: (index: number, element: DocSelection) => void): DocSelection;
func (s *docSelection) Each(call goja.FunctionCall) (ret goja.Value) {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
callback, ok := call.Argument(0).Export().(func(call goja.FunctionCall) goja.Value)
if !ok {
panic(s.doc.vm.NewTypeError("argument is not a function").ToString())
}
s.selection.Each(func(i int, selection *goquery.Selection) {
callback(goja.FunctionCall{Arguments: []goja.Value{
s.doc.vm.ToValue(i),
newDocSelectionGojaValue(s.doc, selection),
}})
})
return goja.Undefined()
}
// Map passes each element in the current matched set through a function, producing a slice of string holding the returned values. The function f
// is called for each element in the selection with the index of the element in that selection starting at 0, and a *Selection that contains only
// that element.
//
// map(callback: (index: number, element: DocSelection) => DocSelection): DocSelection[];
func (s *docSelection) Map(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
callback, ok := call.Argument(0).Export().(func(call goja.FunctionCall) goja.Value)
if !ok {
panic(s.doc.vm.NewTypeError("argument is not a function").ToString())
}
var retStr []interface{}
var retDocSelection map[string]interface{}
s.selection.Each(func(i int, selection *goquery.Selection) {
val := callback(goja.FunctionCall{Arguments: []goja.Value{
s.doc.vm.ToValue(i),
newDocSelectionGojaValue(s.doc, selection),
}})
if valExport, ok := val.Export().(map[string]interface{}); ok {
retDocSelection = valExport
}
retStr = append(retStr, val.Export())
})
if len(retStr) > 0 {
return s.doc.vm.ToValue(retStr)
}
return s.doc.vm.ToValue(retDocSelection)
}
// First reduces the set of matched elements to the first in the set. It returns a new Selection object, and an empty Selection object if the
// selection is empty.
//
// first(): DocSelection;
func (s *docSelection) First(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
return newDocSelectionGojaValue(s.doc, s.selection.First())
}
// Last reduces the set of matched elements to the last in the set. It returns a new Selection object, and an empty Selection object if the
// selection is empty.
//
// last(): DocSelection;
func (s *docSelection) Last(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
return newDocSelectionGojaValue(s.doc, s.selection.Last())
}
// Eq reduces the set of matched elements to the one at the specified index. If a negative index is given, it counts backwards starting at the
// end of the set. It returns a new Selection object, and an empty Selection object if the index is invalid.
//
// eq(index: number): DocSelection;
func (s *docSelection) Eq(call goja.FunctionCall) goja.Value {
if s.selection == nil {
panic(s.doc.vm.ToValue("selection is nil"))
}
index, ok := call.Argument(0).Export().(int64)
if !ok {
panic(s.doc.vm.NewTypeError("argument is not a number").String())
}
return newDocSelectionGojaValue(s.doc, s.selection.Eq(int(index)))
}

View File

@@ -0,0 +1,307 @@
package goja_bindings
import (
"encoding/json"
"io"
"strings"
"time"
"github.com/dop251/goja"
"github.com/imroc/req/v3"
"github.com/rs/zerolog/log"
)
const (
maxConcurrentRequests = 50
defaultTimeout = 35 * time.Second
)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Fetch
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
var (
clientWithCloudFlareBypass = req.C().
SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36").
SetTimeout(defaultTimeout).
EnableInsecureSkipVerify().
ImpersonateChrome()
clientWithoutBypass = req.C().
SetTimeout(defaultTimeout)
)
type Fetch struct {
vm *goja.Runtime
fetchSem chan struct{}
vmResponseCh chan func()
}
func NewFetch(vm *goja.Runtime) *Fetch {
return &Fetch{
vm: vm,
fetchSem: make(chan struct{}, maxConcurrentRequests),
vmResponseCh: make(chan func(), maxConcurrentRequests),
}
}
func (f *Fetch) ResponseChannel() <-chan func() {
return f.vmResponseCh
}
func (f *Fetch) Close() {
defer func() {
if r := recover(); r != nil {
}
}()
close(f.vmResponseCh)
}
type fetchOptions struct {
Method string
Body goja.Value
Headers map[string]string
Timeout int // seconds
NoCloudFlareBypass bool
}
type fetchResult struct {
body []byte
request *req.Request
response *req.Response
json interface{}
}
// BindFetch binds the fetch function to the VM
func BindFetch(vm *goja.Runtime) *Fetch {
// Create a new Fetch instance
f := NewFetch(vm)
_ = vm.Set("fetch", f.Fetch)
go func() {
for fn := range f.ResponseChannel() {
defer func() {
if r := recover(); r != nil {
log.Warn().Msgf("extension: response channel panic: %v", r)
}
}()
fn()
}
}()
return f
}
func (f *Fetch) Fetch(call goja.FunctionCall) goja.Value {
defer func() {
if r := recover(); r != nil {
log.Warn().Msgf("extension: fetch panic: %v", r)
}
}()
promise, resolve, reject := f.vm.NewPromise()
// Input validation
if len(call.Arguments) == 0 {
PanicThrowTypeError(f.vm, "fetch requires at least 1 argument")
}
url, ok := call.Argument(0).Export().(string)
if !ok {
PanicThrowTypeError(f.vm, "URL parameter must be a string")
}
// Parse options
options := fetchOptions{
Method: "GET",
Timeout: int(defaultTimeout.Seconds()),
NoCloudFlareBypass: false,
}
var reqBody interface{}
var reqContentType string
if len(call.Arguments) > 1 {
rawOpts := call.Argument(1).ToObject(f.vm)
if rawOpts != nil && !goja.IsUndefined(rawOpts) {
if o := rawOpts.Get("method"); o != nil && !goja.IsUndefined(o) {
if v, ok := o.Export().(string); ok {
options.Method = strings.ToUpper(v)
}
}
if o := rawOpts.Get("timeout"); o != nil && !goja.IsUndefined(o) {
if v, ok := o.Export().(int); ok {
options.Timeout = v
}
}
if o := rawOpts.Get("headers"); o != nil && !goja.IsUndefined(o) {
if v, ok := o.Export().(map[string]interface{}); ok {
for k, interf := range v {
if str, ok := interf.(string); ok {
if options.Headers == nil {
options.Headers = make(map[string]string)
}
options.Headers[k] = str
}
}
}
}
options.Body = rawOpts.Get("body")
if o := rawOpts.Get("noCloudflareBypass"); o != nil && !goja.IsUndefined(o) {
if v, ok := o.Export().(bool); ok {
options.NoCloudFlareBypass = v
}
}
}
}
if options.Body != nil && !goja.IsUndefined(options.Body) {
switch v := options.Body.Export().(type) {
case string:
reqBody = v
case io.Reader:
reqBody = v
case []byte:
reqBody = v
case *goja.ArrayBuffer:
reqBody = v.Bytes()
case goja.ArrayBuffer:
reqBody = v.Bytes()
case *formData:
body, mp := v.GetBuffer()
reqBody = body
reqContentType = mp.FormDataContentType()
case map[string]interface{}:
reqBody = v
reqContentType = "application/json"
default:
reqBody = options.Body.String()
}
}
go func() {
// Acquire semaphore
f.fetchSem <- struct{}{}
defer func() { <-f.fetchSem }()
log.Trace().Str("url", url).Str("method", options.Method).Msgf("extension: Network request")
var client *req.Client
if options.NoCloudFlareBypass {
client = clientWithoutBypass
} else {
client = clientWithCloudFlareBypass
}
// Create request with timeout
reqClient := client.Clone().SetTimeout(time.Duration(options.Timeout) * time.Second)
request := reqClient.R()
// Set headers
for k, v := range options.Headers {
request.SetHeader(k, v)
}
if reqContentType != "" {
request.SetContentType(reqContentType)
}
// Set body if present
if reqBody != nil {
request.SetBody(reqBody)
}
var result fetchResult
var resp *req.Response
var err error
switch options.Method {
case "GET":
resp, err = request.Get(url)
case "POST":
resp, err = request.Post(url)
case "PUT":
resp, err = request.Put(url)
case "DELETE":
resp, err = request.Delete(url)
case "PATCH":
resp, err = request.Patch(url)
case "HEAD":
resp, err = request.Head(url)
case "OPTIONS":
resp, err = request.Options(url)
default:
resp, err = request.Send(options.Method, url)
}
if err != nil {
f.vmResponseCh <- func() {
_ = reject(NewError(f.vm, err))
}
return
}
rawBody := resp.Bytes()
result.body = rawBody
result.response = resp
result.request = request
if len(rawBody) > 0 {
var data interface{}
if err := json.Unmarshal(rawBody, &data); err != nil {
result.json = nil
} else {
result.json = data
}
}
f.vmResponseCh <- func() {
_ = resolve(result.toGojaObject(f.vm))
return
}
}()
return f.vm.ToValue(promise)
}
func (f *fetchResult) toGojaObject(vm *goja.Runtime) *goja.Object {
obj := vm.NewObject()
_ = obj.Set("status", f.response.StatusCode)
_ = obj.Set("statusText", f.response.Status)
_ = obj.Set("method", f.request.Method)
_ = obj.Set("rawHeaders", f.response.Header)
_ = obj.Set("ok", f.response.IsSuccessState())
_ = obj.Set("url", f.response.Request.URL.String())
_ = obj.Set("body", f.body)
headers := make(map[string]string)
for k, v := range f.response.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
_ = obj.Set("headers", headers)
cookies := make(map[string]string)
for _, cookie := range f.response.Cookies() {
cookies[cookie.Name] = cookie.Value
}
_ = obj.Set("cookies", cookies)
_ = obj.Set("redirected", f.response.Request.URL != f.response.Request.URL) // req handles redirects automatically
_ = obj.Set("contentType", f.response.Header.Get("Content-Type"))
_ = obj.Set("contentLength", f.response.ContentLength)
_ = obj.Set("text", func() string {
return string(f.body)
})
_ = obj.Set("json", func(call goja.FunctionCall) (ret goja.Value) {
return vm.ToValue(f.json)
})
return obj
}

View File

@@ -0,0 +1,324 @@
package goja_bindings
import (
"fmt"
"net/http"
"net/http/httptest"
"seanime/internal/util"
"sync"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/dop251/goja"
gojabuffer "github.com/dop251/goja_nodejs/buffer"
gojarequire "github.com/dop251/goja_nodejs/require"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFetch_ThreadSafety(t *testing.T) {
// Create a test server that simulates different response times
var serverRequestCount int
var serverMu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
serverMu.Lock()
serverRequestCount++
currentRequest := serverRequestCount
serverMu.Unlock()
// Simulate varying response times to increase chance of race conditions
time.Sleep(time.Duration(currentRequest%3) * 50 * time.Millisecond)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"request": %d}`, currentRequest)
}))
defer server.Close()
// Create JavaScript test code that makes concurrent fetch calls
jsCode := fmt.Sprintf(`
const url = %q;
const promises = [];
// Function to make a fetch request and verify response
async function makeFetch(i) {
const response = await fetch(url);
const data = await response.json();
return { index: i, data };
}
// Create multiple concurrent requests
for (let i = 0; i < 50; i++) {
promises.push(makeFetch(i));
}
// Wait for all requests to complete
Promise.all(promises)
`, server.URL)
// Run the code multiple times to increase chance of catching race conditions
for i := 0; i < 5; i++ {
t.Run(fmt.Sprintf("Iteration_%d", i), func(t *testing.T) {
// Create a new VM for each iteration
vm := goja.New()
BindFetch(vm)
// Execute the JavaScript code
v, err := vm.RunString(jsCode)
assert.NoError(t, err)
// Get the Promise
promise, ok := v.Export().(*goja.Promise)
assert.True(t, ok)
// Wait for the Promise to resolve
for promise.State() == goja.PromiseStatePending {
time.Sleep(10 * time.Millisecond)
}
// Verify the Promise resolved successfully
assert.Equal(t, goja.PromiseStateFulfilled, promise.State())
// Verify we got an array of results
results, ok := promise.Result().Export().([]interface{})
assert.True(t, ok)
assert.Len(t, results, 50)
// Verify each result has the expected structure
for _, result := range results {
resultMap, ok := result.(map[string]interface{})
assert.True(t, ok)
assert.Contains(t, resultMap, "index")
assert.Contains(t, resultMap, "data")
data, ok := resultMap["data"].(map[string]interface{})
assert.True(t, ok)
assert.Contains(t, data, "request")
}
})
}
}
func TestFetch_VMIsolation(t *testing.T) {
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"test": "data"}`)
}))
defer server.Close()
// Create multiple VMs and make concurrent requests
const numVMs = 5
const requestsPerVM = 40
var wg sync.WaitGroup
for i := 0; i < numVMs; i++ {
wg.Add(1)
go func(vmIndex int) {
defer wg.Done()
// Create a new VM for this goroutine
vm := goja.New()
BindFetch(vm)
// Create JavaScript code that makes multiple requests
jsCode := fmt.Sprintf(`
const url = %q;
const promises = [];
for (let i = 0; i < %d; i++) {
promises.push(fetch(url).then(r => r.json()));
}
Promise.all(promises)
`, server.URL, requestsPerVM)
// Execute the code
v, err := vm.RunString(jsCode)
assert.NoError(t, err)
// Get and wait for the Promise
promise := v.Export().(*goja.Promise)
for promise.State() == goja.PromiseStatePending {
time.Sleep(10 * time.Millisecond)
}
// Verify the Promise resolved successfully
assert.Equal(t, goja.PromiseStateFulfilled, promise.State())
// Verify we got the expected number of results
results := promise.Result().Export().([]interface{})
assert.Len(t, results, requestsPerVM)
}(i)
}
wg.Wait()
}
func TestGojaPromiseAll(t *testing.T) {
vm := goja.New()
BindFetch(vm)
registry := new(gojarequire.Registry)
registry.Enable(vm)
gojabuffer.Enable(vm)
BindConsole(vm, util.NewLogger())
_, err := vm.RunString(`
async function run() {
const [a, b, c] = await Promise.all([
fetch("https://jsonplaceholder.typicode.com/todos/1"),
fetch("https://jsonplaceholder.typicode.com/todos/2"),
fetch("https://jsonplaceholder.typicode.com/todos/3"),
fetch("https://jsonplaceholder.typicode.com/todos/4"),
fetch("https://jsonplaceholder.typicode.com/todos/5"),
fetch("https://jsonplaceholder.typicode.com/todos/6"),
fetch("https://jsonplaceholder.typicode.com/todos/7"),
fetch("https://jsonplaceholder.typicode.com/todos/8"),
])
const dataA = await a.json();
const dataB = await b.json();
const dataC = await c.json();
console.log("Data A:", dataA.title);
console.log("Data B:", dataB);
console.log("Data C:", dataC);
}
`)
require.NoError(t, err)
runFunc, ok := goja.AssertFunction(vm.Get("run"))
require.True(t, ok)
ret, err := runFunc(goja.Undefined())
require.NoError(t, err)
promise := ret.Export().(*goja.Promise)
for promise.State() == goja.PromiseStatePending {
time.Sleep(10 * time.Millisecond)
}
}
func TestGojaFormDataAndFetch(t *testing.T) {
vm := goja.New()
BindFetch(vm)
registry := new(gojarequire.Registry)
registry.Enable(vm)
gojabuffer.Enable(vm)
BindConsole(vm, util.NewLogger())
_, err := vm.RunString(`
async function run() {
const formData = new FormData();
formData.append("username", "John");
formData.append("accountnum", 123456);
console.log(formData.get("username")); // John
const fData = new URLSearchParams();
for (const pair of formData.entries()) {
fData.append(pair[0], pair[1]);
}
const response = await fetch('https://httpbin.org/post', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData
});
const data = await response.json();
console.log(data);
console.log("Echoed GojaFormData content:");
if (data.form) {
for (const key in data.form) {
console.log(key, data.form[key]);
}
} else {
console.log("No form data echoed in the response.");
}
return data;
}
`)
require.NoError(t, err)
runFunc, ok := goja.AssertFunction(vm.Get("run"))
require.True(t, ok)
ret, err := runFunc(goja.Undefined())
require.NoError(t, err)
promise := ret.Export().(*goja.Promise)
for promise.State() == goja.PromiseStatePending {
time.Sleep(10 * time.Millisecond)
}
if promise.State() == goja.PromiseStateFulfilled {
spew.Dump(promise.Result())
} else {
err := promise.Result()
spew.Dump(err)
}
}
func TestGojaFetchPostJSON(t *testing.T) {
vm := goja.New()
BindFetch(vm)
registry := new(gojarequire.Registry)
registry.Enable(vm)
gojabuffer.Enable(vm)
BindConsole(vm, util.NewLogger())
_, err := vm.RunString(`
async function run() {
const response = await fetch('https://httpbin.org/post', {
method: 'POST',
body: { name: "John Doe", age: 30 },
});
const data = await response.json();
console.log(data);
console.log("Echoed content:");
if (data.json) {
for (const key in data.json) {
console.log(key, data.json[key]);
}
} else {
console.log("No form data echoed in the response.");
}
return data;
}
`)
require.NoError(t, err)
runFunc, ok := goja.AssertFunction(vm.Get("run"))
require.True(t, ok)
ret, err := runFunc(goja.Undefined())
require.NoError(t, err)
promise := ret.Export().(*goja.Promise)
for promise.State() == goja.PromiseStatePending {
time.Sleep(10 * time.Millisecond)
}
if promise.State() == goja.PromiseStateFulfilled {
spew.Dump(promise.Result())
} else {
err := promise.Result()
spew.Dump(err)
}
}

View File

@@ -0,0 +1,67 @@
package goja_bindings
import (
"reflect"
"strings"
"unicode"
"github.com/dop251/goja"
)
var (
_ goja.FieldNameMapper = (*DefaultFieldMapper)(nil)
)
// DefaultFieldMapper provides custom mapping between Go and JavaScript methods names.
//
// It is similar to the builtin "uncapFieldNameMapper" but also converts
// all uppercase identifiers to their lowercase equivalent (eg. "GET" -> "get").
type DefaultFieldMapper struct {
}
// FieldName implements the [FieldNameMapper.FieldName] interface method.
func (u DefaultFieldMapper) FieldName(_ reflect.Type, f reflect.StructField) string {
return f.Name
}
// MethodName implements the [FieldNameMapper.MethodName] interface method.
func (u DefaultFieldMapper) MethodName(_ reflect.Type, m reflect.Method) string {
return convertGoToJSName(m.Name)
}
var nameExceptions = map[string]string{"OAuth2": "oauth2"}
func convertGoToJSName(name string) string {
if v, ok := nameExceptions[name]; ok {
return v
}
startUppercase := make([]rune, 0, len(name))
for _, c := range name {
if c != '_' && !unicode.IsUpper(c) && !unicode.IsDigit(c) {
break
}
startUppercase = append(startUppercase, c)
}
totalStartUppercase := len(startUppercase)
// all uppercase eg. "JSON" -> "json"
if len(name) == totalStartUppercase {
return strings.ToLower(name)
}
// eg. "JSONField" -> "jsonField"
if totalStartUppercase > 1 {
return strings.ToLower(name[0:totalStartUppercase-1]) + name[totalStartUppercase-1:]
}
// eg. "GetField" -> "getField"
if totalStartUppercase == 1 {
return strings.ToLower(name[0:1]) + name[1:]
}
return name
}

View File

@@ -0,0 +1,231 @@
package goja_bindings
import (
"bytes"
"io"
"mime/multipart"
"strconv"
"strings"
"github.com/dop251/goja"
)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// formData
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func BindFormData(vm *goja.Runtime) error {
err := vm.Set("FormData", func(call goja.ConstructorCall) *goja.Object {
fd := newFormData(vm)
instanceValue := vm.ToValue(fd).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
if err != nil {
return err
}
return nil
}
type formData struct {
runtime *goja.Runtime
buf *bytes.Buffer
writer *multipart.Writer
fieldNames map[string]struct{}
values map[string][]string
closed bool
}
func newFormData(runtime *goja.Runtime) *formData {
buf := &bytes.Buffer{}
writer := multipart.NewWriter(buf)
return &formData{
runtime: runtime,
buf: buf,
writer: writer,
fieldNames: make(map[string]struct{}),
values: make(map[string][]string),
closed: false,
}
}
func (fd *formData) Append(call goja.FunctionCall) goja.Value {
if fd.closed {
return fd.runtime.ToValue("cannot append to closed FormData")
}
fieldName := call.Argument(0).String()
value := call.Argument(1).String()
fieldName = strings.TrimSpace(fieldName)
fd.values[fieldName] = append(fd.values[fieldName], value)
if _, exists := fd.fieldNames[fieldName]; !exists {
fd.fieldNames[fieldName] = struct{}{}
writer, err := fd.writer.CreateFormField(fieldName)
if err != nil {
return fd.runtime.ToValue(err.Error())
}
_, err = writer.Write([]byte(value))
if err != nil {
return fd.runtime.ToValue(err.Error())
}
}
return goja.Undefined()
}
func (fd *formData) Delete(call goja.FunctionCall) goja.Value {
if fd.closed {
return fd.runtime.ToValue("cannot delete from closed FormData")
}
fieldName := call.Argument(0).String()
fieldName = strings.TrimSpace(fieldName)
delete(fd.fieldNames, fieldName)
delete(fd.values, fieldName)
return goja.Undefined()
}
func (fd *formData) Entries(call goja.FunctionCall) goja.Value {
if fd.closed {
return fd.runtime.ToValue("cannot get entries from closed FormData")
}
iter := fd.runtime.NewArray()
index := 0
for key, values := range fd.values {
for _, value := range values {
entry := fd.runtime.NewObject()
entry.Set("0", key)
entry.Set("1", value)
iter.Set(strconv.Itoa(index), entry)
index++
}
}
return iter
}
func (fd *formData) Get(call goja.FunctionCall) goja.Value {
if fd.closed {
return fd.runtime.ToValue("cannot get value from closed FormData")
}
fieldName := call.Argument(0).String()
fieldName = strings.TrimSpace(fieldName)
if values, exists := fd.values[fieldName]; exists && len(values) > 0 {
return fd.runtime.ToValue(values[0])
}
return goja.Undefined()
}
func (fd *formData) GetAll(call goja.FunctionCall) goja.Value {
if fd.closed {
return fd.runtime.ToValue("cannot get all values from closed FormData")
}
fieldName := call.Argument(0).String()
fieldName = strings.TrimSpace(fieldName)
iter := fd.runtime.NewArray()
if values, exists := fd.values[fieldName]; exists {
for i, value := range values {
iter.Set(strconv.Itoa(i), value)
}
}
return iter
}
func (fd *formData) Has(call goja.FunctionCall) goja.Value {
if fd.closed {
return fd.runtime.ToValue("cannot check key in closed FormData")
}
fieldName := call.Argument(0).String()
fieldName = strings.TrimSpace(fieldName)
_, exists := fd.fieldNames[fieldName]
return fd.runtime.ToValue(exists)
}
func (fd *formData) Keys(call goja.FunctionCall) goja.Value {
if fd.closed {
return fd.runtime.ToValue("cannot get keys from closed FormData")
}
iter := fd.runtime.NewArray()
index := 0
for key := range fd.fieldNames {
iter.Set(strconv.Itoa(index), key)
index++
}
return iter
}
func (fd *formData) Set(call goja.FunctionCall) goja.Value {
if fd.closed {
return fd.runtime.ToValue("cannot set value in closed FormData")
}
fieldName := call.Argument(0).String()
value := call.Argument(1).String()
fieldName = strings.TrimSpace(fieldName)
fd.values[fieldName] = []string{value}
if _, exists := fd.fieldNames[fieldName]; !exists {
fd.fieldNames[fieldName] = struct{}{}
writer, err := fd.writer.CreateFormField(fieldName)
if err != nil {
return fd.runtime.ToValue(err.Error())
}
_, err = writer.Write([]byte(value))
if err != nil {
return fd.runtime.ToValue(err.Error())
}
}
return goja.Undefined()
}
func (fd *formData) Values(call goja.FunctionCall) goja.Value {
if fd.closed {
return fd.runtime.ToValue("cannot get values from closed FormData")
}
iter := fd.runtime.NewArray()
index := 0
for _, values := range fd.values {
for _, value := range values {
iter.Set(strconv.Itoa(index), value)
index++
}
}
return iter
}
func (fd *formData) GetContentType() goja.Value {
if !fd.closed {
fd.writer.Close()
fd.closed = true
}
return fd.runtime.ToValue(fd.writer.FormDataContentType())
}
func (fd *formData) GetBuffer() (io.Reader, *multipart.Writer) {
if !fd.closed {
fd.writer.Close()
fd.closed = true
}
return bytes.NewReader(fd.buf.Bytes()), fd.writer
}

View File

@@ -0,0 +1,49 @@
package goja_bindings
import (
"seanime/internal/util"
"testing"
"github.com/dop251/goja"
gojabuffer "github.com/dop251/goja_nodejs/buffer"
gojarequire "github.com/dop251/goja_nodejs/require"
"github.com/stretchr/testify/require"
)
func TestGojaFormData(t *testing.T) {
vm := goja.New()
defer vm.ClearInterrupt()
BindFormData(vm)
registry := new(gojarequire.Registry)
registry.Enable(vm)
gojabuffer.Enable(vm)
BindConsole(vm, util.NewLogger())
_, err := vm.RunString(`
var fd = new FormData();
fd.append("name", "John Doe");
fd.append("age", 30);
console.log("Has 'name':", fd.has("name")); // true
console.log("Get 'name':", fd.get("name")); // John Doe
console.log("GetAll 'name':", fd.getAll("name")); // ["John Doe"]
console.log("Keys:", Array.from(fd.keys())); // ["name", "age"]
console.log("Values:", Array.from(fd.values())); // ["John Doe", 30]
fd.delete("name");
console.log("Has 'name' after delete:", fd.has("name")); // false
console.log("Entries:");
for (let entry of fd.entries()) {
console.log(entry[0], entry[1]);
}
var contentType = fd.getContentType();
var buffer = fd.getBuffer();
console.log("Content-Type:", contentType);
console.log("Buffer:", buffer);
`)
require.NoError(t, err)
}

View File

@@ -0,0 +1,16 @@
/// <reference path="../doc.d.ts" />
class Provider {
async test() {
try {
const data = await fetch("https://cryptojs.gitbook.io/docs")
const $ = LoadDoc(await data.text())
console.log($("header h1").text())
}
catch (e) {
console.error(e)
}
}
}

View File

@@ -0,0 +1,82 @@
/// <reference path="../doc.d.ts" />
class Provider {
async test() {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Document</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header>
<h1>Main Title</h1>
<nav>
<ul id="main-nav" class="nav-list">
<li><a href="#home">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
</header>
<section id="content">
<article class="post" data-id="1">
<h2>First Post</h2>
<p>This is the first post.</p>
<a href="https://example.com/first-post" class="read-more">Read more</a>
</article>
<article class="post" data-id="2">
<h2>Second Post</h2>
<p>This is the second post.</p>
<a href="https://example.com/second-post" class="read-more">Read more</a>
</article>
<article class="post" data-id="3">
<h2>Third Post</h2>
<p>This is the third post.</p>
<a href="https://example.com/third-post" class="read-more">Read more</a>
</article>
</section>
<aside>
<h2>Sidebar</h2>
<ul class="sidebar-list">
<li><a href="#link1">Link 1</a></li>
<li><a href="#link2">Link 2</a></li>
<li><a href="#link3">Link 3</a></li>
</ul>
</aside>
<footer>
<p>&copy; 2024 Example Company. All rights reserved.</p>
<p><a href="mailto:info@example.com">Contact Us</a></p>
</footer>
</body>
</html>`
const $ = new Doc(html)
console.log("Document created")
console.log(">>> Last post by string selector")
const article = $.find("article:last-child")
console.log(article.html())
console.log(">>> Post titles (map to string)")
const titles = $.find("section")
.children("article.post")
.filter((i, e) => {
return e.attr("data-id") !== "1"
})
.map((i, e) => {
return e.children("h2").text()
})
console.log(titles)
console.log(">>> END")
}
}

View File

@@ -0,0 +1,46 @@
package goja_bindings
import (
"seanime/internal/torrents/torrent"
"github.com/dop251/goja"
)
func BindTorrentUtils(vm *goja.Runtime) error {
torrentUtils := vm.NewObject()
torrentUtils.Set("getMagnetLinkFromTorrentData", getMagnetLinkFromTorrentDataFunc(vm))
vm.Set("$torrentUtils", torrentUtils)
return nil
}
func getMagnetLinkFromTorrentDataFunc(vm *goja.Runtime) (ret func(c goja.FunctionCall) goja.Value) {
defer func() {
if r := recover(); r != nil {
}
}()
return func(call goja.FunctionCall) goja.Value {
defer func() {
if r := recover(); r != nil {
panic(vm.ToValue("selection is nil"))
}
}()
if len(call.Arguments) < 1 {
panic(vm.ToValue("TypeError: getMagnetLinkFromTorrentData requires at least 1 argument"))
}
str, ok := call.Argument(0).Export().(string)
if !ok {
panic(vm.ToValue(vm.NewTypeError("argument is not a string")))
}
magnet, err := torrent.StrDataToMagnetLink(str)
if err != nil {
return vm.ToValue("")
}
return vm.ToValue(magnet)
}
}

View File

@@ -0,0 +1,61 @@
package goja_bindings
import (
"seanime/internal/util"
"testing"
"time"
"github.com/dop251/goja"
gojabuffer "github.com/dop251/goja_nodejs/buffer"
gojarequire "github.com/dop251/goja_nodejs/require"
"github.com/stretchr/testify/require"
)
func TestGojaTorrentUtils(t *testing.T) {
vm := goja.New()
registry := new(gojarequire.Registry)
registry.Enable(vm)
gojabuffer.Enable(vm)
BindTorrentUtils(vm)
BindConsole(vm, util.NewLogger())
BindFetch(vm)
_, err := vm.RunString(`
async function run() {
try {
console.log("\nTesting torrent file to magnet link")
const url = "https://animetosho.org/storage/torrent/da9aad67b6f8bb82757bb3ef95235b42624c34f7/%5BSubsPlease%5D%20Make%20Heroine%20ga%20Oosugiru%21%20-%2011%20%281080p%29%20%5B58B3496A%5D.torrent"
const data = await (await fetch(url)).text()
const magnetLink = getMagnetLinkFromTorrentData(data)
console.log("Magnet link:", magnetLink)
}
catch (e) {
console.error(e)
}
}
`)
require.NoError(t, err)
runFunc, ok := goja.AssertFunction(vm.Get("run"))
require.True(t, ok)
ret, err := runFunc(goja.Undefined())
require.NoError(t, err)
promise := ret.Export().(*goja.Promise)
for promise.State() == goja.PromiseStatePending {
time.Sleep(10 * time.Millisecond)
}
if promise.State() == goja.PromiseStateRejected {
err := promise.Result()
t.Fatal(err)
}
}