Refactor config access and URL hook redirects

Replace global host/port variables with a `HookConfig` struct and mutex-protected getters/setters to make configuration access thread-safe and consistently validated. In request/websocket hooks, centralize host patching with `maybeRedirect()` and a `QSet` whitelist, skipping redirects when config is invalid. Also consolidate symbol-hook setup into a shared helper with clearer error logging and library constants.
This commit is contained in:
√(noham)²
2026-08-25 23:24:26 +02:00
parent 4313f8a6eb
commit b672c6596f
3 changed files with 128 additions and 102 deletions

View File

@@ -9,18 +9,43 @@
#include <cstring> #include <cstring>
#include <string> #include <string>
#include <QtCore/QMutex>
#include <QtCore/QMutexLocker>
#define TAG "RMHook" #define TAG "RMHook"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
QString gConfiguredHost; extern "C" void startHooks();
int gConfiguredPort = 0;
static QMutex gConfigMutex;
static HookConfig gConfig;
HookConfig getConfiguration() {
QMutexLocker locker(&gConfigMutex);
return gConfig;
}
static bool setConfiguration(const QString &host, int port) {
HookConfig candidate{host.trimmed(), port};
if (!candidate.isValid()) {
return false;
}
QMutexLocker locker(&gConfigMutex);
gConfig = candidate;
return true;
}
static bool hasValidConfiguration() {
QMutexLocker locker(&gConfigMutex);
return gConfig.isValid();
}
static JavaVM *gJvm = nullptr; static JavaVM *gJvm = nullptr;
static jobject gContext = nullptr; static jobject gContext = nullptr;
extern "C" void startHooks();
static JNIEnv *getJNIEnv() { static JNIEnv *getJNIEnv() {
if (!gJvm) { if (!gJvm) {
return nullptr; return nullptr;
@@ -47,20 +72,6 @@ static bool clearJavaException(JNIEnv *env, const char *operation) {
return true; return true;
} }
static bool isValidConfig(const QString &host, int port) {
return !host.trimmed().isEmpty() && port > 0 && port <= 65535;
}
static bool setConfiguration(const QString &host, int port) {
if (!isValidConfig(host, port)) {
return false;
}
gConfiguredHost = host.trimmed();
gConfiguredPort = port;
return true;
}
static std::string trimLine(const char *value) { static std::string trimLine(const char *value) {
std::string line(value ? value : ""); std::string line(value ? value : "");
while (!line.empty() && (line.back() == '\n' || line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) { while (!line.empty() && (line.back() == '\n' || line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) {
@@ -101,8 +112,9 @@ static bool loadFromConfigFile(const char *path) {
return false; return false;
} }
const HookConfig cfg = getConfiguration();
LOGI("Loaded config from %s - Host: %s, Port: %d", path, LOGI("Loaded config from %s - Host: %s, Port: %d", path,
gConfiguredHost.toStdString().c_str(), gConfiguredPort); cfg.host.toStdString().c_str(), cfg.port);
return true; return true;
} }
@@ -163,8 +175,9 @@ static bool loadFromSharedPrefs() {
return false; return false;
} }
const HookConfig cfg = getConfiguration();
LOGI("Loaded config from SharedPreferences - Host: %s, Port: %d", LOGI("Loaded config from SharedPreferences - Host: %s, Port: %d",
gConfiguredHost.toStdString().c_str(), gConfiguredPort); cfg.host.toStdString().c_str(), cfg.port);
return true; return true;
} }
@@ -257,7 +270,7 @@ static void saveToSharedPrefs(const char *host, int port) {
extern "C" { extern "C" {
void loadConfiguration(void) { void loadConfiguration(void) {
if (gConfiguredPort > 0) { if (hasValidConfiguration()) {
return; return;
} }
@@ -270,10 +283,10 @@ void loadConfiguration(void) {
return; return;
} }
gConfiguredHost = "example.com"; setConfiguration("example.com", 443);
gConfiguredPort = 443; const HookConfig cfg = getConfiguration();
LOGI("No saved config found. Using defaults - Host: %s, Port: %d", LOGI("No saved config found. Using defaults - Host: %s, Port: %d",
gConfiguredHost.toStdString().c_str(), gConfiguredPort); cfg.host.toStdString().c_str(), cfg.port);
} }
void saveConfiguration(const char *host, int port) { void saveConfiguration(const char *host, int port) {
@@ -282,9 +295,10 @@ void saveConfiguration(const char *host, int port) {
return; return;
} }
QByteArray hostBytes = gConfiguredHost.toUtf8(); const HookConfig cfg = getConfiguration();
saveToSharedPrefs(hostBytes.constData(), gConfiguredPort); QByteArray hostBytes = cfg.host.toUtf8();
LOGI("Saved config - Host: %s, Port: %d", gConfiguredHost.toStdString().c_str(), gConfiguredPort); saveToSharedPrefs(hostBytes.constData(), cfg.port);
LOGI("Saved config - Host: %s, Port: %d", cfg.host.toStdString().c_str(), cfg.port);
} }
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
@@ -312,7 +326,8 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
} }
loadConfiguration(); loadConfiguration();
LOGI("JNI_OnLoad: config loaded (%s:%d)", gConfiguredHost.toStdString().c_str(), gConfiguredPort); const HookConfig cfg = getConfiguration();
LOGI("JNI_OnLoad: config loaded (%s:%d)", cfg.host.toStdString().c_str(), cfg.port);
startHooks(); startHooks();
return JNI_VERSION_1_6; return JNI_VERSION_1_6;
} }

View File

@@ -3,14 +3,23 @@
#include <QtCore/QString> #include <QtCore/QString>
extern QString gConfiguredHost; struct HookConfig {
extern int gConfiguredPort; QString host;
int port = 0;
bool isValid() const {
return !host.trimmed().isEmpty() && port > 0 && port <= 65535;
}
};
HookConfig getConfiguration();
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
void loadConfiguration(void); void loadConfiguration(void);
void saveConfiguration(const char *host, int port); void saveConfiguration(const char *host, int port);
#ifdef __cplusplus #ifdef __cplusplus

View File

@@ -8,6 +8,7 @@
#include <stdint.h> #include <stdint.h>
#include <QtCore/QObject> #include <QtCore/QObject>
#include <QtCore/QSet>
#include <QtCore/QString> #include <QtCore/QString>
#include <QtCore/QUrl> #include <QtCore/QUrl>
#include <QtCore/QIODevice> #include <QtCore/QIODevice>
@@ -27,27 +28,54 @@
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
static const char *const kLibQtNetwork = "libQt6Network_arm64-v8a.so";
static const char *const kLibQtWebSockets = "libQt6WebSockets_arm64-v8a.so";
// https://github.com/asivery/xovi-rmfakecloud/blob/main/src/commons.cpp#L47
static const QSet<QString> &patchableHosts() {
static const QSet<QString> hosts = {
QStringLiteral("hwr-production-dot-remarkable-production.appspot.com"),
QStringLiteral("service-manager-production-dot-remarkable-production.appspot.com"),
QStringLiteral("local.appspot.com"),
QStringLiteral("my.remarkable.com"),
QStringLiteral("ping.remarkable.com"),
QStringLiteral("internal.cloud.remarkable.com"),
QStringLiteral("eu.tectonic.remarkable.com"),
QStringLiteral("backtrace-proxy.cloud.remarkable.engineering"),
QStringLiteral("dev.ping.remarkable.com"),
QStringLiteral("dev.tectonic.remarkable.com"),
QStringLiteral("dev.internal.cloud.remarkable.com"),
QStringLiteral("eu.internal.tctn.cloud.remarkable.com"),
QStringLiteral("webapp-prod.cloud.remarkable.engineering"),
};
return hosts;
}
static bool shouldPatchURL(const QString &host) { static bool shouldPatchURL(const QString &host) {
if (host.isEmpty()) { if (host.isEmpty()) {
return false; return false;
} }
return patchableHosts().contains(host.toLower());
}
return QString(R"""( static QNetworkRequest maybeRedirect(const QNetworkRequest &req) {
hwr-production-dot-remarkable-production.appspot.com const QString host = req.url().host();
service-manager-production-dot-remarkable-production.appspot.com if (!shouldPatchURL(host)) {
local.appspot.com return req;
my.remarkable.com }
ping.remarkable.com
internal.cloud.remarkable.com const HookConfig cfg = getConfiguration();
eu.tectonic.remarkable.com if (!cfg.isValid()) {
backtrace-proxy.cloud.remarkable.engineering LOGW("Skipping redirect for %s: no valid configuration", host.toStdString().c_str());
dev.ping.remarkable.com return req;
dev.tectonic.remarkable.com }
dev.internal.cloud.remarkable.com
eu.internal.tctn.cloud.remarkable.com QNetworkRequest newReq(req);
webapp-prod.cloud.remarkable.engineering QUrl newUrl = req.url();
)""") newUrl.setHost(cfg.host);
.contains(host, Qt::CaseInsensitive); newUrl.setPort(cfg.port);
newReq.setUrl(newUrl);
return newReq;
} }
// QObject *QNetworkAccessManager::createRequest(Operation op, const QNetworkRequest &req, QIODevice *outgoingData) // QObject *QNetworkAccessManager::createRequest(Operation op, const QNetworkRequest &req, QIODevice *outgoingData)
@@ -64,25 +92,12 @@ QNetworkReply* hooked_qNetworkAccessManager_createRequest(
const QNetworkRequest& req, const QNetworkRequest& req,
QIODevice* outgoingData QIODevice* outgoingData
) { ) {
LOGI("createRequest called for URL: %s", req.url().toString().toStdString().c_str()); LOGI("createRequest called for host: %s", req.url().host().toStdString().c_str());
const QString host = req.url().host();
if (shouldPatchURL(host)) {
QNetworkRequest newReq(req);
QUrl newUrl = req.url();
newUrl.setHost(gConfiguredHost);
newUrl.setPort(gConfiguredPort);
newReq.setUrl(newUrl);
if (original_qNetworkAccessManager_createRequest) { if (!original_qNetworkAccessManager_createRequest) {
return original_qNetworkAccessManager_createRequest(self, op, newReq, outgoingData);
}
return nullptr; return nullptr;
} }
return original_qNetworkAccessManager_createRequest(self, op, maybeRedirect(req), outgoingData);
if (original_qNetworkAccessManager_createRequest) {
return original_qNetworkAccessManager_createRequest(self, op, req, outgoingData);
}
return nullptr;
} }
// void QWebSocket::open(const QNetworkRequest &req) // void QWebSocket::open(const QNetworkRequest &req)
@@ -110,25 +125,30 @@ void hooked_qWebSocket_open(
QWebSocket* self, QWebSocket* self,
const QNetworkRequest& req const QNetworkRequest& req
) { ) {
LOGI("QWebSocket::open called for URL: %s", req.url().toString().toStdString().c_str()); LOGI("QWebSocket::open called for host: %s", req.url().host().toStdString().c_str());
if (!original_qWebSocket_open) { if (!original_qWebSocket_open) {
return; return;
} }
original_qWebSocket_open(self, maybeRedirect(req));
const QString host = req.url().host();
if (shouldPatchURL(host)) {
QUrl newUrl = req.url();
newUrl.setHost(gConfiguredHost);
newUrl.setPort(gConfiguredPort);
QNetworkRequest newReq(req);
newReq.setUrl(newUrl);
original_qWebSocket_open(self, newReq);
return;
} }
original_qWebSocket_open(self, req); static bool hookSymbol(const char *library, const char *symbol, const char *label,
void *replacement, void **originalOut) {
void *address = resolve_qt_symbol(library, symbol);
if (!address) {
LOGE("Skipping hook for %s: symbol not found", label);
return false;
}
void *stub = shadowhook_hook_sym_addr(address, replacement, originalOut);
if (!stub) {
LOGE("Failed to hook %s: %s", label, shadowhook_to_errmsg(shadowhook_get_errno()));
return false;
}
LOGI("Hooked %s", label);
return true;
} }
static void install_hooks() { static void install_hooks() {
@@ -141,39 +161,21 @@ static void install_hooks() {
return; return;
} }
// Hook QNetworkAccessManager::createRequest hookSymbol(
void *create_request = resolve_qt_symbol( kLibQtNetwork,
"libQt6Network_arm64-v8a.so", "_ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice",
"_ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice"); "createRequest",
void *stub1 = shadowhook_hook_sym_addr(
create_request,
(void *)hooked_qNetworkAccessManager_createRequest, (void *)hooked_qNetworkAccessManager_createRequest,
(void **)&original_qNetworkAccessManager_createRequest); (void **)&original_qNetworkAccessManager_createRequest);
if (stub1 != nullptr) { hookSymbol(
LOGI("Hooked createRequest"); kLibQtWebSockets,
} else { "_ZN10QWebSocket4openERK15QNetworkRequest",
LOGE("Failed to hook createRequest: %s", "QWebSocket::open",
shadowhook_to_errmsg(shadowhook_get_errno()));
}
// Hook QWebSocket::open
void *websocket_open = resolve_qt_symbol(
"libQt6WebSockets_arm64-v8a.so",
"_ZN10QWebSocket4openERK15QNetworkRequest");
void *stub2 = shadowhook_hook_sym_addr(
websocket_open,
(void *)hooked_qWebSocket_open, (void *)hooked_qWebSocket_open,
(void **)&original_qWebSocket_open); (void **)&original_qWebSocket_open);
if (stub2 != nullptr) { LOGI("Hook installation finished");
LOGI("Hooked QWebSocket::open");
} else {
LOGE("Failed to hook QWebSocket::open: %s",
shadowhook_to_errmsg(shadowhook_get_errno()));
}
LOGI("All hooks active");
} }
extern "C" void startHooks() { extern "C" void startHooks() {