mirror of
https://github.com/NohamR/RMHook.git
synced 2026-05-25 12:27:13 +00:00
Compare commits
9 Commits
v1.1.1
...
bde78170be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bde78170be | ||
|
|
35423e1c1a | ||
|
|
190fd02092 | ||
|
|
8991f7fbcb | ||
|
|
49aa0ec507 | ||
|
|
90f50ec2a0 | ||
|
|
0bb96ecceb | ||
|
|
427ee012c9 | ||
|
|
03b2b4c794 |
2
LICENSE
2
LICENSE
@@ -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
|
||||||
|
|||||||
47
README.md
47
README.md
@@ -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:
|
||||||

|

|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
BIN
docs/latest.png
BIN
docs/latest.png
Binary file not shown.
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 234 KiB |
BIN
docs/rm.png
BIN
docs/rm.png
Binary file not shown.
|
Before Width: | Height: | Size: 465 KiB After Width: | Height: | Size: 467 KiB |
28
scripts/auto-install.sh
Executable file
28
scripts/auto-install.sh
Executable 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"
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user