From 4bbbd45d25cf917e19368010049da5a1d86e5cb4 Mon Sep 17 00:00:00 2001 From: arm64v8a <48624112+arm64v8a@users.noreply.github.com> Date: Sun, 4 Dec 2022 16:12:11 +0900 Subject: [PATCH] feat: hook.js --- .gitignore | 2 +- .gitmodules | 3 + 3rdparty/qjs | 1 + CMakeLists.txt | 10 +++ db/ConfigBuilder.cpp | 41 +++++++++- main/NekoRay.cpp | 1 + main/NekoRay_DataStore.hpp | 1 + main/QJS.cpp | 143 +++++++++++++++++++++++++++++++++++ main/QJS.hpp | 23 ++++++ sub/GroupUpdater.cpp | 12 +++ translations/zh_CN.ts | 4 + ui/dialog_basic_settings.cpp | 2 + ui/dialog_basic_settings.ui | 7 ++ 13 files changed, 247 insertions(+), 3 deletions(-) create mode 160000 3rdparty/qjs create mode 100644 main/QJS.cpp create mode 100644 main/QJS.hpp diff --git a/.gitignore b/.gitignore index e0a619e..f184f2f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ tags .DS_Store .directory *.debug -Makefile* +/Makefile* *.prl *.app moc_*.cpp diff --git a/.gitmodules b/.gitmodules index 72a74b8..971caf6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "3rdparty/QHotkey"] path = 3rdparty/QHotkey url = https://github.com/Skycoder42/QHotkey.git +[submodule "3rdparty/qjs"] + path = 3rdparty/qjs + url = https://github.com/MatsuriDayo/qjs diff --git a/3rdparty/qjs b/3rdparty/qjs new file mode 160000 index 0000000..578534f --- /dev/null +++ b/3rdparty/qjs @@ -0,0 +1 @@ +Subproject commit 578534fb41a85ab8b2ed37f87b2488ae7ab2fd3a diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e3e4e9..f38b5ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,15 @@ if (NKR_NO_EXTERNAL) set(NKR_NO_YAML 1) set(NKR_NO_ZXING 1) set(NKR_NO_QHOTKEY 1) + set(NKR_NO_QUICKJS 1) +endif () + +# quickjs (static submodule) +if (NKR_NO_QUICKJS) + nkr_add_compile_definitions(NKR_NO_QUICKJS) +else () + add_subdirectory(3rdparty/qjs) + list(APPEND NKR_EXTERNAL_TARGETS quickjs) endif () # grpc @@ -124,6 +133,7 @@ set(PROJECT_SOURCES main/main.cpp main/NekoRay.cpp main/NekoRay_Utils.cpp + main/QJS.cpp 3rdparty/base64.cpp 3rdparty/qrcodegen.cpp diff --git a/db/ConfigBuilder.cpp b/db/ConfigBuilder.cpp index 1199fc6..4aa193c 100644 --- a/db/ConfigBuilder.cpp +++ b/db/ConfigBuilder.cpp @@ -2,6 +2,7 @@ #include "db/Database.hpp" #include "fmt/includes.h" #include "fmt/Preset.hpp" +#include "main/QJS.hpp" #include #include @@ -12,10 +13,26 @@ namespace NekoRay { // Common QSharedPointer BuildConfig(const QSharedPointer &ent, bool forTest, bool forExport) { + QSharedPointer result; if (IS_NEKO_BOX) { - return BuildConfigSingBox(ent, forTest, forExport); + result = BuildConfigSingBox(ent, forTest, forExport); + } else { + result = BuildConfigV2Ray(ent, forTest, forExport); } - return BuildConfigV2Ray(ent, forTest, forExport); + // hook.js + if (result->error.isEmpty()) { + auto source = qjs::ReadHookJS(); + if (!source.isEmpty()) { + qjs::QJS js(source); + auto js_result = js.EvalFunction("hook.hook_core_config", QJsonObject2QString(result->coreConfig, true)); + auto js_result_json = QString2QJsonObject(js_result); + if (!js_result_json.isEmpty() && result->coreConfig != js_result_json) { + MW_show_log("hook.js modified your " + software_core_name + " json config."); + result->coreConfig = js_result_json; + } + } + } + return result; } QString BuildChain(int chainId, const QSharedPointer &status) { @@ -878,6 +895,16 @@ namespace NekoRay { .replace("%TUN_NAME%", tun_name) .replace("%STRICT_ROUTE%", dataStore->vpn_strict_route ? "true" : "false") .replace("%PORT%", Int2String(dataStore->inbound_socks_port)); + // hook.js + auto source = qjs::ReadHookJS(); + if (!source.isEmpty()) { + qjs::QJS js(source); + auto js_result = js.EvalFunction("hook.hook_vpn_config", config); + if (config != js_result) { + MW_show_log("hook.js modified your VPN config."); + config = js_result; + } + } // write config QFile file; file.setFileName(QFileInfo(configFn).fileName()); @@ -897,6 +924,16 @@ namespace NekoRay { .replace("$PROTECT_LISTEN_PATH", protectPath) .replace("$CONFIG_PATH", configPath) .replace("$TABLE_FWMARK", "514"); + // hook.js + auto source = qjs::ReadHookJS(); + if (!source.isEmpty()) { + qjs::QJS js(source); + auto js_result = js.EvalFunction("hook.hook_vpn_script", script); + if (script != js_result) { + MW_show_log("hook.js modified your VPN script."); + script = js_result; + } + } // write script QFile file2; file2.setFileName(QFileInfo(scriptFn).fileName()); diff --git a/main/NekoRay.cpp b/main/NekoRay.cpp index c39a3b1..3263ac8 100644 --- a/main/NekoRay.cpp +++ b/main/NekoRay.cpp @@ -66,6 +66,7 @@ namespace NekoRay { _add(new configItem("sp_format", &system_proxy_format, itemType::string)); _add(new configItem("sub_clear", &sub_clear, itemType::boolean)); _add(new configItem("sub_insecure", &sub_insecure, itemType::boolean)); + _add(new configItem("enable_js_hook", &enable_js_hook, itemType::boolean)); } void DataStore::UpdateStartedId(int id) { diff --git a/main/NekoRay_DataStore.hpp b/main/NekoRay_DataStore.hpp index 5548813..94d354c 100644 --- a/main/NekoRay_DataStore.hpp +++ b/main/NekoRay_DataStore.hpp @@ -86,6 +86,7 @@ namespace NekoRay { // Security bool insecure_hint = true; bool skip_cert = false; + bool enable_js_hook = false; // Remember int remember_spmode = NekoRay::SystemProxyMode::DISABLE; diff --git a/main/QJS.cpp b/main/QJS.cpp new file mode 100644 index 0000000..8844adc --- /dev/null +++ b/main/QJS.cpp @@ -0,0 +1,143 @@ +#include "QJS.hpp" + +#include "3rdparty/qjs/nekoray_qjs.h" +#include "main/NekoRay.hpp" + +namespace NekoRay::qjs { +#ifndef NKR_NO_QUICKJS + namespace exception { + static void js_dump_obj(JSContext *ctx, QString &out, JSValueConst val) { + const char *str; + + str = JS_ToCString(ctx, val); + if (str) { + out.append(str); + out.append('\n'); + JS_FreeCString(ctx, str); + } else { + out += "[exception]\n"; + } + } + + static void js_std_dump_error1(JSContext *ctx, QString &out, JSValueConst exception_val) { + JSValue val; + auto is_error = JS_IsError(ctx, exception_val); + js_dump_obj(ctx, out, exception_val); + if (is_error) { + val = JS_GetPropertyStr(ctx, exception_val, "stack"); + if (!JS_IsUndefined(val)) { + js_dump_obj(ctx, out, val); + } + JS_FreeValue(ctx, val); + } + } + + QString js_std_dump_error(JSContext *ctx) { + QString result; + JSValue exception_val; + + exception_val = JS_GetException(ctx); + js_std_dump_error1(ctx, result, exception_val); + JS_FreeValue(ctx, exception_val); + + return result; + } + } // namespace exception + + JSValue func_log(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { + QString qString; + + int i; + const char *str; + size_t len; + + for (i = 0; i < argc; i++) { + if (i != 0) qString.append(' '); + str = JS_ToCStringLen(ctx, &len, argv[i]); + if (!str) + return JS_EXCEPTION; + qString.append(str); + JS_FreeCString(ctx, str); + } + + MW_show_log(qString); + qDebug() << "func_log:" << qString; + + return JS_UNDEFINED; + } +#endif + +#define NEKO_CTX ((nekoray_qjs_context *) this->neko_ctx) + + QJS::QJS() { +#ifndef NKR_NO_QUICKJS + MW_show_log("loading quickjs......"); + // + this->neko_ctx = malloc(sizeof(nekoray_qjs_context)); + nekoray_qjs_new_arg arg; + arg.neko_ctx = NEKO_CTX; + arg.func_log = func_log; + nekoray_qjs_new(arg); +#endif + } + + QJS::QJS(const QByteArray &jsSource) : QJS() { + this->Eval(jsSource); + } + + QJS::~QJS() { +#ifndef NKR_NO_QUICKJS + nekoray_qjs_free(NEKO_CTX); + free(this->neko_ctx); +#endif + } + + QString QJS::Eval(const QByteArray &jsSource) const { +#ifndef NKR_NO_QUICKJS + auto result = nekoray_qjs_eval(NEKO_CTX, jsSource.data(), jsSource.length()); + if (JS_IsException(result)) { + MW_show_log(exception::js_std_dump_error(NEKO_CTX->ctx)); + return {}; + } + auto cString = JS_ToCString(NEKO_CTX->ctx, result); + QString qString(cString); + JS_FreeCString(NEKO_CTX->ctx, cString); + JS_FreeValue(NEKO_CTX->ctx, result); + return qString; +#else + return {}; +#endif + } + + QString QJS::Eval(const QString &jsSource) const { + return this->Eval(jsSource.toUtf8()); + } + + QString QJS::EvalFile(const QString &jsPath) const { + return this->Eval(ReadFile(jsPath)); + } + + QString QJS::EvalFunction(const QString &funcName, const QString &arg) const { +#ifndef NKR_NO_QUICKJS + auto ba1 = arg.toUtf8(); + JSValue globalObj = JS_GetGlobalObject(NEKO_CTX->ctx); + JSValue tempObj = JS_NewStringLen(NEKO_CTX->ctx, ba1.data(), ba1.length()); + JS_SetPropertyStr(NEKO_CTX->ctx, globalObj, "tempObj", tempObj); + auto result = this->Eval(QString("%1(tempObj)").arg(funcName)); + JS_DeleteProperty(NEKO_CTX->ctx, globalObj, JS_NewAtom(NEKO_CTX->ctx, "tempObj"), 1); // Free tempObj + JS_FreeValue(NEKO_CTX->ctx, globalObj); + return result; +#else + return {}; +#endif + } + + QByteArray ReadHookJS() { +#ifndef NKR_NO_QUICKJS + if (NekoRay::dataStore->enable_js_hook) { + return ReadFile(QString("./hook.%1.js").arg(software_name.toLower())); + } +#endif + return {}; + } +} // namespace NekoRay::qjs diff --git a/main/QJS.hpp b/main/QJS.hpp new file mode 100644 index 0000000..466d761 --- /dev/null +++ b/main/QJS.hpp @@ -0,0 +1,23 @@ +#pragma once + +class QByteArray; +class QString; + +namespace NekoRay::qjs { + class QJS { + public: + QJS(); + explicit QJS(const QByteArray &jsSource); + ~QJS(); + + QString Eval(const QByteArray &jsSource) const; + QString Eval(const QString &jsSource) const; + QString EvalFile(const QString &jsPath) const; + QString EvalFunction(const QString &funcName, const QString &arg) const; + + private: + void *neko_ctx; + }; + + QByteArray ReadHookJS(); +} // namespace NekoRay::qjs diff --git a/sub/GroupUpdater.cpp b/sub/GroupUpdater.cpp index 00168c1..7872cad 100644 --- a/sub/GroupUpdater.cpp +++ b/sub/GroupUpdater.cpp @@ -4,6 +4,7 @@ #include "db/ProfileFilter.hpp" #include "fmt/includes.h" #include "fmt/Preset.hpp" +#include "main/QJS.hpp" #include "GroupUpdater.hpp" @@ -394,6 +395,17 @@ namespace NekoRay::sub { } } + // hook.js + auto source = qjs::ReadHookJS(); + if (!source.isEmpty()) { + qjs::QJS js(source); + auto js_result = js.EvalFunction("hook.hook_import", content); + if (content != js_result) { + MW_show_log("hook.js modified your import content."); + content = js_result; + } + } + // 解析并添加 profile rawUpdater->update(content); diff --git a/translations/zh_CN.ts b/translations/zh_CN.ts index a96e612..6e13e98 100644 --- a/translations/zh_CN.ts +++ b/translations/zh_CN.ts @@ -203,6 +203,10 @@ Ignore TLS errors when updating subscription 更新订阅时忽略 TLS 错误 + + Enable hook.js + 启用 hook.js 功能 + DialogEditGroup diff --git a/ui/dialog_basic_settings.cpp b/ui/dialog_basic_settings.cpp index 52f3001..82abc32 100644 --- a/ui/dialog_basic_settings.cpp +++ b/ui/dialog_basic_settings.cpp @@ -228,6 +228,7 @@ DialogBasicSettings::DialogBasicSettings(QWidget *parent) D_LOAD_BOOL(insecure_hint) D_LOAD_BOOL(skip_cert) + D_LOAD_BOOL(enable_js_hook) } DialogBasicSettings::~DialogBasicSettings() { @@ -278,6 +279,7 @@ void DialogBasicSettings::accept() { D_SAVE_BOOL(insecure_hint) D_SAVE_BOOL(skip_cert) + D_SAVE_BOOL(enable_js_hook) MW_dialog_message(Dialog_DialogBasicSettings, "UpdateDataStore"); QDialog::accept(); diff --git a/ui/dialog_basic_settings.ui b/ui/dialog_basic_settings.ui index e616baf..8b615ea 100644 --- a/ui/dialog_basic_settings.ui +++ b/ui/dialog_basic_settings.ui @@ -578,6 +578,13 @@ + + + + Enable hook.js + + +