9 Commits

Author SHA1 Message Date
√(noham)²
bde78170be Move 'How it works' section in README 2026-05-10 18:57:51 +02:00
√(noham)²
35423e1c1a Update auto-install.sh 2026-05-10 16:30:04 +02:00
√(noham)²
190fd02092 Add auto-install script and update README 2026-05-10 16:28:01 +02:00
√(noham)²
8991f7fbcb Update README.md 2026-05-09 23:25:26 +02:00
√(noham)²
49aa0ec507 Update README.md 2026-05-09 18:15:42 +02:00
√(noham)²
90f50ec2a0 Add MQTT URI patching and hook for Paho 2026-05-09 18:13:53 +02:00
√(noham)²
0bb96ecceb Update license year, README release, and image 2026-05-07 23:29:37 +02:00
√(noham)²
427ee012c9 Bump reMarkable Desktop compatibility to v3.27 2026-05-06 18:44:57 +02:00
√(noham)²
03b2b4c794 Update README compatibility to v3.26.0 2026-03-27 09:46:06 +01:00
6 changed files with 148 additions and 18 deletions

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 Rivoirard Noham Copyright (c) 2026 Rivoirard Noham
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -6,15 +6,20 @@ A dynamic library injection tool for the reMarkable Desktop macOS application, e
RMHook hooks into the reMarkable Desktop app's network layer to redirect API calls from reMarkable's official cloud services to your own [rmfakecloud](https://github.com/ddvk/rmfakecloud) server. This allows you to maintain full control over your documents and data. RMHook hooks into the reMarkable Desktop app's network layer to redirect API calls from reMarkable's official cloud services to your own [rmfakecloud](https://github.com/ddvk/rmfakecloud) server. This allows you to maintain full control over your documents and data.
### Windows Port
Looking for a Windows version? Check out **[RMHook-Win](https://github.com/NohamR/RMHook-Win)**: A Windows port of RMHook for the reMarkable Desktop application.
## Features ## Features
- Network request interception and redirection - Network request interception and redirection
- WebSocket connection patching - WebSocket connection patching
- MQTT URI modification for screen sharing features
## Compatibility ## Compatibility
**Tested and working on:** **Tested and working on:**
- reMarkable Desktop v3.25.0 (released 2026-02-02) - reMarkable Desktop v3.27.1 (released 2026-06-07)
<p align="center"> <p align="center">
<img src="docs/latest.png" width="40%" /> <img src="docs/latest.png" width="40%" />
@@ -23,15 +28,22 @@ RMHook hooks into the reMarkable Desktop app's network layer to redirect API cal
## Installation and usage ## Installation and usage
### Important legal notice
⚠️ **For legal reasons, this repository does not include a pre-patched reMarkable app.** However, the latest compiled dylib is available in the [Releases](https://github.com/NohamR/RMHook/releases/latest) section. ⚠️ **For legal reasons, this repository does not include a pre-patched reMarkable app.** However, the latest compiled dylib is available in the [Releases](https://github.com/NohamR/RMHook/releases/latest) section.
### Step 1: Prepare the reMarkable app ### Auto installation
Run in a terminal:
```bash
bash <(curl -sL https://raw.githubusercontent.com/NohamR/RMHook/refs/heads/main/scripts/auto-install.sh)
```
### Manual installation
#### Step 1: Prepare the reMarkable app
Uses the reMarkable Desktop app from your Applications folder or download it fresh from the [Mac App Store](https://apps.apple.com/app/remarkable-desktop/id1276493162). Uses the reMarkable Desktop app from your Applications folder or download it fresh from the [Mac App Store](https://apps.apple.com/app/remarkable-desktop/id1276493162).
### Step 2: Inject the dylib #### Step 2: Inject the dylib
Use the provided injection script: Use the provided injection script:
```bash ```bash
@@ -45,9 +57,9 @@ This script will:
- Remove the `_MASReceipt` folder - Remove the `_MASReceipt` folder
- Fix file ownership - Fix file ownership
### Step 3: Handle document storage #### Step 3: Handle document storage
#### Important path changes ##### Important path changes
The original Mac App Store version stores data in sandboxed locations: The original Mac App Store version stores data in sandboxed locations:
**Original sandboxed paths:** **Original sandboxed paths:**
@@ -58,7 +70,7 @@ The original Mac App Store version stores data in sandboxed locations:
- Config: `~/Library/Preferences/rmfakecloud.config` - Config: `~/Library/Preferences/rmfakecloud.config`
- Documents: `~/Library/Application Support/remarkable` - Documents: `~/Library/Application Support/remarkable`
#### Migration options ##### Migration options
**Option 1: Create a symbolic link** (recommended) **Option 1: Create a symbolic link** (recommended)
```bash ```bash
@@ -73,7 +85,7 @@ mv ~/Library/Containers/com.remarkable.desktop/Data/Library/Application\ Support
~/Library/Application\ Support/remarkable ~/Library/Application\ Support/remarkable
``` ```
### Step 4: Configure rmfakecloud server #### Step 4: Configure rmfakecloud server
Quickly access the configuration file from the app's Help menu: Quickly access the configuration file from the app's Help menu:
![help-config.png](docs/help-config.png) ![help-config.png](docs/help-config.png)
@@ -90,14 +102,7 @@ Example configuration:
} }
``` ```
### Step 5: Launch the patched app :p #### Step 5: Launch the patched app :p
## How it works
RMHook uses [tinyhook](https://github.com/Antibioticss/tinyhook/) to hook into Qt framework functions at runtime:
1. **QNetworkAccessManager::createRequest** - Intercepts HTTP/HTTPS requests
2. **QWebSocket::open** - Patches WebSocket connections
When the app attempts to connect to reMarkable's servers (e.g., `internal.cloud.remarkable.com`), the hooks redirect these requests to your configured host and port.
## Configuration ## Configuration
@@ -120,6 +125,14 @@ If the config file doesn't exist, it will be created automatically with default
- Ensure your rmfakecloud server is running and accessible - Ensure your rmfakecloud server is running and accessible
- Verify the storage path migration was completed - Verify the storage path migration was completed
## How it works
RMHook uses [tinyhook](https://github.com/Antibioticss/tinyhook/) to hook into Qt framework functions at runtime:
1. **QNetworkAccessManager::createRequest** - Intercepts HTTP/HTTPS requests
2. **QWebSocket::open** - Patches WebSocket connections
3. **MQTTAsync_createWithOptions** - Modifies MQTT URIs for screen sharing features
When the app attempts to connect to reMarkable's servers (e.g., `internal.cloud.remarkable.com`), the hooks redirect these requests to your configured host and port.
## Credits ## Credits
- xovi-rmfakecloud: [asivery/xovi-rmfakecloud](https://github.com/asivery/xovi-rmfakecloud) - Original hooking information - xovi-rmfakecloud: [asivery/xovi-rmfakecloud](https://github.com/asivery/xovi-rmfakecloud) - Original hooking information
- rm-xovi-extensions: [asivery/rm-xovi-extensions](https://github.com/asivery/rm-xovi-extensions) - Extension framework for reMarkable, used as reference for hooking Qt functions - rm-xovi-extensions: [asivery/rm-xovi-extensions](https://github.com/asivery/rm-xovi-extensions) - Extension framework for reMarkable, used as reference for hooking Qt functions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 KiB

After

Width:  |  Height:  |  Size: 467 KiB

28
scripts/auto-install.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
REPO="NohamR/RMHook"
FILE="rmfakecloud.dylib"
APP_PATH="/Applications/remarkable.app"
echo "[INFO] Downloading $FILE..."
curl -sL \
-o "/tmp/$FILE" \
"https://github.com/$REPO/releases/latest/download/$FILE"
# Fix the sandbox
echo "[INFO] Linking sandbox directory..."
ln -sf ~/Library/Containers/com.remarkable.desktop/Data/Library/Application\ Support/remarkable \
~/Library/Application\ Support/remarkable
echo "[INFO] Downloading inject script..."
curl -sL \
-o "/tmp/inject.sh" \
"https://raw.githubusercontent.com/$REPO/refs/heads/main/scripts/inject.sh"
echo "[INFO] Downloading optool..."
curl -sL \
-o "/tmp/optool" \
"https://raw.githubusercontent.com/$REPO/refs/heads/main/scripts/optool"
chmod +x /tmp/inject.sh /tmp/optool
echo "[INFO] Running inject script..."
/tmp/inject.sh "/tmp/$FILE" "$APP_PATH"

View File

@@ -18,6 +18,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <dispatch/dispatch.h> #include <dispatch/dispatch.h>
#include <string>
#include <QtNetwork/QNetworkAccessManager> #include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest> #include <QtNetwork/QNetworkRequest>
@@ -245,6 +246,17 @@ static QNetworkReply *(*original_qNetworkAccessManager_createRequest)(
static void (*original_qWebSocket_open)( static void (*original_qWebSocket_open)(
QWebSocket *self, QWebSocket *self,
const QNetworkRequest &request) = NULL; const QNetworkRequest &request) = NULL;
typedef void* MQTTAsync;
typedef void* MQTTAsync_createOptions;
static int (*original_MQTTAsync_createWithOptions)(
MQTTAsync *handle,
const char *serverURI,
const char *clientId,
int persistence_type,
void *persistence_context,
MQTTAsync_createOptions *options) = NULL;
#endif #endif
#ifdef BUILD_MODE_QMLREBUILD #ifdef BUILD_MODE_QMLREBUILD
@@ -301,6 +313,12 @@ static inline bool shouldPatchURL(const QString &host) {
hookFunction:(void *)hooked_qWebSocket_open hookFunction:(void *)hooked_qWebSocket_open
originalFunction:(void **)&original_qWebSocket_open originalFunction:(void **)&original_qWebSocket_open
logPrefix:@"[reMarkable]"]; logPrefix:@"[reMarkable]"];
[MemoryUtils hookSymbol:@"libpaho-mqtt3as.1.dylib"
symbolName:@"_MQTTAsync_createWithOptions"
hookFunction:(void *)hooked_MQTTAsync_createWithOptions
originalFunction:(void **)&original_MQTTAsync_createWithOptions
logPrefix:@"[reMarkable]"];
#endif #endif
#ifdef BUILD_MODE_QMLREBUILD #ifdef BUILD_MODE_QMLREBUILD
@@ -442,6 +460,77 @@ extern "C" void hooked_qWebSocket_open(
original_qWebSocket_open(self, req); original_qWebSocket_open(self, req);
} }
// Patch a paho URI: "ssl://host.remarkable.com:port" -> "ssl://proxy:port"
// Returns patched string, or empty if no patch needed.
static std::string PatchMqttUri(const char* uri)
{
if (!uri) return {};
const std::string original(uri);
size_t schemeEnd = original.find("://");
size_t hostStart = (schemeEnd != std::string::npos) ? schemeEnd + 3 : 0;
size_t hostEnd = original.find_first_of(":/", hostStart);
if (hostEnd == std::string::npos) hostEnd = original.size();
const std::string origHost = original.substr(hostStart, hostEnd - hostStart);
// Match any *.remarkable.com or *.remarkable.engineering host
static const char* kSuffixes[] = {
".remarkable.com",
".remarkable.engineering",
nullptr
};
bool shouldPatch = false;
for (int i = 0; kSuffixes[i]; ++i)
{
const std::string suffix(kSuffixes[i]);
if (origHost.size() >= suffix.size() &&
origHost.compare(origHost.size() - suffix.size(),
suffix.size(), suffix) == 0)
{
shouldPatch = true;
break;
}
}
if (!shouldPatch) return {};
std::string patched = original;
std::string proxyHost = [gConfiguredHost UTF8String];
patched.replace(hostStart, hostEnd - hostStart, proxyHost);
// Fix port
size_t colonPos = patched.find(':', hostStart + proxyHost.size());
if (colonPos != std::string::npos)
{
size_t numEnd = patched.find_first_not_of("0123456789", colonPos + 1);
if (numEnd == std::string::npos) numEnd = patched.size();
patched.replace(colonPos + 1, numEnd - colonPos - 1,
std::to_string([gConfiguredPort intValue]));
}
return patched;
}
extern "C" int hooked_MQTTAsync_createWithOptions(
MQTTAsync *handle,
const char *serverURI,
const char *clientId,
int persistence_type,
void *persistence_context,
MQTTAsync_createOptions *options)
{
if (!original_MQTTAsync_createWithOptions) {
return -1; // error code for MQTTAsync_create failure
}
std::string patchedUri = PatchMqttUri(serverURI);
if (!patchedUri.empty()) {
NSLogger(@"[reMarkable] Patching MQTT URI from %s to %s", serverURI, patchedUri.c_str());
return original_MQTTAsync_createWithOptions(handle, patchedUri.c_str(), clientId, persistence_type, persistence_context, options);
}
return original_MQTTAsync_createWithOptions(handle, serverURI, clientId, persistence_type, persistence_context, options);
}
#endif // BUILD_MODE_RMFAKECLOUD #endif // BUILD_MODE_RMFAKECLOUD
#ifdef BUILD_MODE_QMLREBUILD #ifdef BUILD_MODE_QMLREBUILD