17 Commits
dev ... v1.3

Author SHA1 Message Date
√(noham)²
36d4b1258f Add configurable client key algorithm 2026-08-27 16:21:40 +02:00
√(noham)²
3f1b4fd728 Proper implementation 2026-08-27 16:15:57 +02:00
√(noham)²
ce1df6f27d PoC 2026-08-27 16:06:10 +02:00
√(noham)²
e00ab1d5ed Update README.md 2026-08-26 00:04:09 +02:00
√(noham)²
a74031f228 Update compatibility for v3.28.1 2026-08-25 17:50:39 +02:00
√(noham)²
9089839f9b Update compatibility for Desktop v3.28 2026-08-20 16:33:57 +02:00
√(noham)²
8f95c65ba9 Update tested version to v3.27.2 and refresh image 2026-05-13 19:05:22 +02:00
√(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
√(noham)²
0db8a14ef7 Bump reMarkable Desktop compatibility to v3.25.0 2026-02-02 19:56:33 +01:00
26 changed files with 656 additions and 1538 deletions

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@ build/
.DS_Store
/.vscode
/research
/aqt_venv
docs/rmfakecloud_hooking.md

View File

@@ -28,7 +28,7 @@ set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR})
include_directories(
${PROJECT_ROOT_DIR}/src/core
${PROJECT_ROOT_DIR}/src/utils
${PROJECT_ROOT_DIR}/src/reMarkable
${PROJECT_ROOT_DIR}/src/RMHook
${PROJECT_ROOT_DIR}/libs/include
)
@@ -49,7 +49,7 @@ set(LIBS
# Locate Qt libraries
set(QT_LIB_TARGETS "")
set(_qt_candidate_roots "$ENV{HOME}/Qt/6.10.0")
set(_qt_candidate_roots "$ENV{HOME}/Qt/6.10.3/macos")
foreach(_qt_root ${_qt_candidate_roots})
if(_qt_root AND EXISTS "${_qt_root}")
@@ -78,27 +78,29 @@ set(COMMON_SOURCES
${PROJECT_ROOT_DIR}/src/utils/ResourceUtils.m
)
# reMarkable dylib
set(REMARKABLE_SOURCES
${PROJECT_ROOT_DIR}/src/reMarkable/reMarkable.m
${PROJECT_ROOT_DIR}/src/reMarkable/DevHooks.m
# RMHook dylib
set(RMHOOK_SOURCES
${PROJECT_ROOT_DIR}/src/RMHook/RMHook.m
${PROJECT_ROOT_DIR}/src/RMHook/Config.m
${PROJECT_ROOT_DIR}/src/RMHook/SSLConfig.m
${PROJECT_ROOT_DIR}/src/RMHook/DevHooks.m
)
add_library(reMarkable SHARED
add_library(RMHook SHARED
${COMMON_SOURCES}
${REMARKABLE_SOURCES}
${RMHOOK_SOURCES}
)
# Set source files as Objective-C++
set_source_files_properties(
${REMARKABLE_SOURCES}
${RMHOOK_SOURCES}
PROPERTIES LANGUAGE OBJCXX
)
set_target_properties(reMarkable PROPERTIES
set_target_properties(RMHook PROPERTIES
PREFIX ""
SUFFIX ".dylib"
OUTPUT_NAME "reMarkable"
OUTPUT_NAME "RMHook"
LIBRARY_OUTPUT_DIRECTORY "${PROJECT_ROOT_DIR}/build/dylibs"
MACOSX_RPATH ON
)
@@ -107,40 +109,29 @@ add_definitions(-DQT_NO_VERSION_TAGGING)
# Add build mode compile definitions and conditionally add sources
if(BUILD_MODE_RMFAKECLOUD)
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_RMFAKECLOUD=1)
target_compile_definitions(RMHook PRIVATE BUILD_MODE_RMFAKECLOUD=1)
message(STATUS "Build mode: rmfakecloud (cloud redirection)")
endif()
if(BUILD_MODE_QMLREBUILD)
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLREBUILD=1)
target_compile_definitions(RMHook PRIVATE BUILD_MODE_QMLREBUILD=1)
# Enable Qt MOC for MessageBroker (HttpServer is pure Obj-C/Foundation, no MOC needed)
set_target_properties(reMarkable PROPERTIES AUTOMOC ON)
# Enable Qt MOC for MessageBroker
set_target_properties(RMHook PROPERTIES AUTOMOC ON)
# Add MessageBroker (needs MOC) and HttpServer (native macOS)
target_sources(reMarkable PRIVATE
# Add MessageBroker source (needs MOC processing)
target_sources(RMHook PRIVATE
${PROJECT_ROOT_DIR}/src/utils/MessageBroker.mm
${PROJECT_ROOT_DIR}/src/utils/HttpServer.mm
)
find_package(Qt6 COMPONENTS Qml QUIET)
if(Qt6Qml_FOUND)
target_link_libraries(reMarkable PRIVATE Qt6::Qml)
else()
find_package(Qt5 COMPONENTS Qml QUIET)
if(Qt5Qml_FOUND)
target_link_libraries(reMarkable PRIVATE Qt5::Qml)
endif()
endif()
message(STATUS "Build mode: qmlrebuild (resource hooking)")
endif()
if(BUILD_MODE_DEV)
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_DEV=1)
target_compile_definitions(RMHook PRIVATE BUILD_MODE_DEV=1)
message(STATUS "Build mode: dev (reverse engineering)")
endif()
target_link_libraries(reMarkable PRIVATE
target_link_libraries(RMHook PRIVATE
${LIBS}
${QT_LIB_TARGETS}
)

View File

@@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal

186
README.md
View File

@@ -6,32 +6,47 @@ 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.
## Other platforms
- **[RMHook-Win](https://github.com/NohamR/RMHook-Win)**: Windows Desktop
- **[RMHook-iOS](https://github.com/NohamR/RMHook-iOS)**: iOS
- **[RMHook-Android](https://github.com/NohamR/RMHook-Android)**: Android
## Features
- Network request interception and redirection
- WebSocket connection patching
- MQTT URI modification for screen sharing features
- mTLS support for external access via Cloudflare Tunnel
## Compatibility
**Tested and working on:**
- reMarkable Desktop v3.24.0 (released 2025-12-03)
- reMarkable Desktop v3.28.1 (released 2026-08-24)
<p align="center">
<img src="docs/latest.png" width="40%" />
<img src="docs/rm.png" width="50%" />
<img src="docs/latest.png" width="45%" />
<img src="docs/rm.png" width="45%" />
</p>
## 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.
### 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).
### Step 2: Inject the dylib
#### Step 2: Inject the dylib
Use the provided injection script:
```bash
@@ -45,9 +60,9 @@ This script will:
- Remove the `_MASReceipt` folder
- 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:
**Original sandboxed paths:**
@@ -58,7 +73,7 @@ The original Mac App Store version stores data in sandboxed locations:
- Config: `~/Library/Preferences/rmfakecloud.config`
- Documents: `~/Library/Application Support/remarkable`
#### Migration options
##### Migration options
**Option 1: Create a symbolic link** (recommended)
```bash
@@ -73,7 +88,7 @@ mv ~/Library/Containers/com.remarkable.desktop/Data/Library/Application\ Support
~/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:
![help-config.png](docs/help-config.png)
@@ -90,25 +105,112 @@ Example configuration:
}
```
### 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.
#### Step 5: Launch the patched app :p
## Configuration
The config file (`~/Library/Preferences/rmfakecloud.config`) supports the following keys:
| Key | Type | Default | Description |
|--------|---------|-------------------|--------------------------------|
| `host` | String | `example.com` | Your rmfakecloud server host |
| `port` | Number | `443` | Your rmfakecloud server port |
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `host` | String | `example.com` | Your rmfakecloud server host |
| `port` | Number | `443` | Your rmfakecloud server port |
| `client_cert` | String | (none) | Path to client certificate file (PEM) for mTLS |
| `client_key` | String | (none) | Path to client private key file (PEM) for mTLS |
| `key_algorithm` | String | `rsa` | Key algorithm: `rsa`, `ec`, `dsa`, `dh`, or `opaque` |
| `ca_cert` | String | (none) | Path to custom CA certificate file (PEM) |
| `disable_ssl_verification` | Boolean | `false` | Disable SSL peer verification (not recommended) |
If the config file doesn't exist, it will be created automatically with default values on first launch.
### External Access (Cloudflare Tunnel with Client Certificate)
If your rmfakecloud instance is exposed via Cloudflare Tunnel with client certificate authentication, configure the additional TLS settings in `~/Library/Preferences/rmfakecloud.config`:
```json
{
"host": "rmfakecloud.example.com",
"port": 443,
"client_cert": "/path/to/client.crt",
"client_key": "/path/to/client.key"
}
```
If your server uses a self-signed certificate or a certificate signed by a private CA:
```json
{
"host": "rmfakecloud.example.com",
"port": 443,
"client_cert": "/path/to/client.crt",
"client_key": "/path/to/client.key",
"ca_cert": "/path/to/ca.crt"
}
```
See the [rmfakecloud external access guide](https://ddvk.github.io/rmfakecloud/install/external-access/) for generating the client certificate.
## Building
1. **Clone the repository:**
```bash
git clone http://github.com/NohamR/RMHook
cd RMHook
```
Create a Python environment and install `aqtinstall`:
```bash
python3 -m venv aqt_venv
source aqt_venv/bin/activate
pip install aqtinstall
aqt install-qt mac desktop 6.10.3 -m qtwebsockets --outputdir ~/Qt
```
2. **Compile the dylib:**
```bash
./scripts/build.sh [mode]
```
### Build modes
The build script supports different modes for various use cases:
| Mode | Description |
|------|-------------|
| `rmfakecloud` | Redirect reMarkable cloud to rmfakecloud server (default) |
| `qmlrebuild` | Qt resource data registration hooking for QML replacement |
| `dev` | Development/reverse engineering mode with all hooks |
| `all` | Enable all modes |
**Note (qmlrebuild mode):** When using the `qmlrebuild` feature, you must clear the Qt QML cache before launching the app:
```bash
rm -rf ~/Library/Caches/remarkable
```
Qt caches compiled QML files, so changes to QML resources won't take effect until the cache is cleared.
Examples:
```bash
./scripts/build.sh # Build with rmfakecloud mode (default)
./scripts/build.sh rmfakecloud # Explicitly build rmfakecloud mode
./scripts/build.sh dev # Build with dev/reverse engineering hooks
./scripts/build.sh all # Build with all modes enabled
```
## Debugging
You can stream the macOS console logs to see output from the hooks:
```bash
log stream --predicate 'process == "reMarkable"' --level debug
```
## 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.
## Troubleshooting
### App won't launch
@@ -141,41 +243,3 @@ This project is not affiliated with, endorsed by, or sponsored by reMarkable AS.
## Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
## Building
1. **Clone the repository:**
```bash
git clone http://github.com/NohamR/RMHook
cd RMHook
```
2. **Compile the dylib:**
```bash
./scripts/build.sh [mode]
```
### Build modes
The build script supports different modes for various use cases:
| Mode | Description |
|------|-------------|
| `rmfakecloud` | Redirect reMarkable cloud to rmfakecloud server (default) |
| `qmlrebuild` | Qt resource data registration hooking for QML replacement |
| `dev` | Development/reverse engineering mode with all hooks |
| `all` | Enable all modes |
**Note (qmlrebuild mode):** When using the `qmlrebuild` feature, you must clear the Qt QML cache before launching the app:
```bash
rm -rf ~/Library/Caches/remarkable
```
Qt caches compiled QML files, so changes to QML resources won't take effect until the cache is cleared.
Examples:
```bash
./scripts/build.sh # Build with rmfakecloud mode (default)
./scripts/build.sh rmfakecloud # Explicitly build rmfakecloud mode
./scripts/build.sh dev # Build with dev/reverse engineering hooks
./scripts/build.sh all # Build with all modes enabled
```

View File

@@ -1,54 +0,0 @@
// Add this MessageBroker to a QML component that has access to PlatformHelpers
// This listens for documentAccepted signals from the HTTP server
// and calls PlatformHelpers.documentAccepted()
import net.noham.MessageBroker
MessageBroker {
id: documentAcceptedBroker
listeningFor: ["documentAccepted"]
onSignalReceived: (signal, message) => {
console.log("[DocumentAccepted.MessageBroker] Received signal:", signal);
console.log("[DocumentAccepted.MessageBroker] Message data:", message);
try {
// Parse JSON message from HTTP server
const data = JSON.parse(message);
console.log("[DocumentAccepted.MessageBroker] Parsed request:", JSON.stringify(data));
// Extract parameters with defaults
const url = data.url || "";
const password = data.password || "";
const directoryId = data.directoryId || "";
const flag1 = data.flag1 !== undefined ? data.flag1 : false;
const flag2 = data.flag2 !== undefined ? data.flag2 : false;
console.log("[DocumentAccepted.MessageBroker] Parameters:");
console.log("[DocumentAccepted.MessageBroker] url:", url);
console.log("[DocumentAccepted.MessageBroker] password:", password ? "(set)" : "(empty)");
console.log("[DocumentAccepted.MessageBroker] directoryId:", directoryId);
console.log("[DocumentAccepted.MessageBroker] flag1:", flag1);
console.log("[DocumentAccepted.MessageBroker] flag2:", flag2);
// Validate required parameters
if (!url) {
console.error("[DocumentAccepted.MessageBroker] ERROR: Missing 'url' parameter");
return;
}
if (!directoryId) {
console.error("[DocumentAccepted.MessageBroker] ERROR: Missing 'directoryId' parameter");
return;
}
// Call PlatformHelpers.documentAccepted
console.log("[DocumentAccepted.MessageBroker] Calling PlatformHelpers.documentAccepted...");
PlatformHelpers.documentAccepted(url, password, directoryId, flag1, flag2);
console.log("[DocumentAccepted.MessageBroker] Document accepted successfully");
} catch (error) {
console.error("[DocumentAccepted.MessageBroker] ERROR parsing request:", error);
console.error("[DocumentAccepted.MessageBroker] Message was:", message);
}
}
}

View File

@@ -1,67 +0,0 @@
// Add this MessageBroker to ExportDialog.qml after the PopupDialog definition
// This should be added near the top of the component, after property definitions
import net.noham.MessageBroker
// ... existing properties ...
// MessageBroker for HTTP server export requests
MessageBroker {
id: exportBroker
listeningFor: ["exportFile"]
onSignalReceived: (signal, message) => {
console.log("[ExportDialog.MessageBroker] Received signal:", signal);
console.log("[ExportDialog.MessageBroker] Message data:", message);
try {
// Parse JSON message from HTTP server
const data = JSON.parse(message);
console.log("[ExportDialog.MessageBroker] Parsed export request:", JSON.stringify(data));
// Extract parameters
const target = data.target || "";
const documentId = data.id || data.documentId || "";
const format = data.format !== undefined ? data.format : PlatformHelpers.ExportPdf;
const password = data.password || "";
const keepPassword = data.keepPassword !== undefined ? data.keepPassword : true;
const grayscale = data.grayscale !== undefined ? data.grayscale : false;
const pageSelection = data.pageSelection || [];
console.log("[ExportDialog.MessageBroker] Export parameters:");
console.log("[ExportDialog.MessageBroker] target:", target);
console.log("[ExportDialog.MessageBroker] documentId:", documentId);
console.log("[ExportDialog.MessageBroker] format:", format);
console.log("[ExportDialog.MessageBroker] keepPassword:", keepPassword);
console.log("[ExportDialog.MessageBroker] grayscale:", grayscale);
console.log("[ExportDialog.MessageBroker] pageSelection:", JSON.stringify(pageSelection));
// Validate required parameters
if (!target) {
console.error("[ExportDialog.MessageBroker] ERROR: Missing 'target' parameter");
return;
}
if (!documentId) {
console.error("[ExportDialog.MessageBroker] ERROR: Missing 'id' or 'documentId' parameter");
return;
}
// Call PlatformHelpers.exportFile
console.log("[ExportDialog.MessageBroker] Calling PlatformHelpers.exportFile...");
if (pageSelection && pageSelection.length > 0) {
console.log("[ExportDialog.MessageBroker] Exporting with page selection");
PlatformHelpers.exportFile(target, documentId, format, password, keepPassword, grayscale, pageSelection);
} else {
console.log("[ExportDialog.MessageBroker] Exporting full document");
PlatformHelpers.exportFile(target, documentId, format, password, keepPassword, grayscale);
}
console.log("[ExportDialog.MessageBroker] Export completed successfully");
} catch (error) {
console.error("[ExportDialog.MessageBroker] ERROR parsing export request:", error);
console.error("[ExportDialog.MessageBroker] Message was:", message);
}
}
}

View File

@@ -1,293 +0,0 @@
# HTTP Server for Export Requests
The RMHook dylib includes an HTTP server that accepts export requests and forwards them to the reMarkable application via MessageBroker.
## Server Details
- **Host**: `localhost`
- **Port**: `8080`
- **Base URL**: `http://localhost:8080`
## Endpoints
### `POST /exportFile`
Trigger a document export from the reMarkable application.
**Request Body** (JSON):
```json
{
"target": "file:///Users/username/Desktop/output.pdf",
"id": "document-uuid-here",
"format": 0,
"password": "",
"keepPassword": true,
"grayscale": false,
"pageSelection": []
}
```
**Parameters**:
- `target` (string, required): File path or folder URL for the export. Use `file://` prefix for local paths.
- `id` or `documentId` (string, required): The UUID of the document to export.
- `format` (integer, optional): Export format. Default: `0` (PDF)
- `0`: PDF
- `1`: PNG
- `2`: SVG
- `3`: RmBundle
- `4`: RmHtml
- `password` (string, optional): Password for password-protected documents. Default: `""`
- `keepPassword` (boolean, optional): Whether to keep password protection on PDF exports. Default: `true`
- `grayscale` (boolean, optional): Export with grayscale pens. Default: `false`
- `pageSelection` (array, optional): Array of page indices to export. If empty or omitted, exports all pages. Example: `[0, 1, 2]`
**Response**:
```json
{
"status": "success",
"message": "Export request sent to application"
}
```
**Error Response**:
```json
{
"error": "Error description"
}
```
### `POST /documentAccepted`
Import/accept a document into the reMarkable application.
**Request Body** (JSON):
```json
{
"url": "file:///Users/username/Desktop/test.pdf",
"password": "",
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
"flag1": false,
"flag2": false
}
```
**Parameters**:
- `url` (string, required): File URL to import. Use `file://` prefix for local paths.
- `password` (string, optional): Password for password-protected documents. Default: `""`
- `directoryId` (string, required): The UUID of the target directory/folder where the document should be imported.
- `flag1` (boolean, optional): Purpose unclear. Default: `false`
- `flag2` (boolean, optional): Purpose unclear. Default: `false`
**Response**:
```json
{
"status": "success",
"message": "Document accepted request sent to application"
}
```
**Error Response**:
```json
{
"error": "Error description"
}
```
### `GET /health`
Health check endpoint.
**Response**:
```json
{
"status": "ok",
"service": "RMHook HTTP Server"
}
```
## Example Requests
### Export a document to PDF
```bash
curl -X POST http://localhost:8080/exportFile \
-H "Content-Type: application/json" \
-d '{
"target": "file:///Users/noham/Desktop/export.pdf",
"id": "12345678-1234-1234-1234-123456789abc",
"format": 0,
"grayscale": false,
"keepPassword": true
}'
```
### Export specific pages as PNG
```bash
curl -X POST http://localhost:8080/exportFile \
-H "Content-Type: application/json" \
-d '{
"target": "file:///Users/noham/Desktop/pages",
"id": "12345678-1234-1234-1234-123456789abc",
"format": 1,
"pageSelection": [0, 1, 2],
"grayscale": true
}'
```
### Export to RmBundle format
```bash
curl -X POST http://localhost:8080/exportFile \
-H "Content-Type: application/json" \
-d '{
"target": "file:///Users/noham/Desktop/MyDocument",
"id": "12345678-1234-1234-1234-123456789abc",
"format": 3
}'
```
### Import/Accept a document
```bash
curl -X POST http://localhost:8080/documentAccepted \
-H "Content-Type: application/json" \
-d '{
"url": "file:///Users/noham/Desktop/test.pdf",
"password": "",
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
"flag1": false,
"flag2": false
}'
```
### Python Example - Export
```python
import requests
import json
# Export configuration
export_data = {
"target": "file:///Users/noham/Desktop/output.pdf",
"id": "12345678-1234-1234-1234-123456789abc",
"format": 0, # PDF
"grayscale": False,
"keepPassword": True
}
# Send request
response = requests.post(
"http://localhost:8080/exportFile",
json=export_data
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
```
### Python Example - Import Document
```python
import requests
# Import configuration
import_data = {
"url": "file:///Users/noham/Desktop/test.pdf",
"password": "",
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
"flag1": False,
"flag2": False
}
# Send request
response = requests.post(
"http://localhost:8080/documentAccepted",
json=import_data
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
```
### JavaScript Example - Export
```javascript
// Export configuration
const exportData = {
target: "file:///Users/noham/Desktop/output.pdf",
id: "12345678-1234-1234-1234-123456789abc",
format: 0, // PDF
grayscale: false,
keepPassword: true
};
// Send request
fetch("http://localhost:8080/exportFile", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(exportData)
})
.then(response => response.json())
.then(data => console.log("Success:", data))
.catch(error => console.error("Error:", error));
```
### JavaScript Example - Import Document
```javascript
// Import configuration
const importData = {
url: "file:///Users/noham/Desktop/test.pdf",
password: "",
directoryId: "2166c19d-d2cc-456c-9f0e-49482031092a",
flag1: false,
flag2: false
};
// Send request
fetch("http://localhost:8080/documentAccepted", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(importData)
})
.then(response => response.json())
.then(data => console.log("Success:", data))
.catch(error => console.error("Error:", error));
```
## Integration with QML
### Export Dialog Integration
Add the MessageBroker snippet from `docs/ExportDialog_MessageBroker_snippet.qml` to your ExportDialog.qml replacement file. This will enable the QML side to receive export requests from the HTTP server.
The MessageBroker listens for "exportFile" signals and automatically calls `PlatformHelpers.exportFile()` with the provided parameters.
### Document Import Integration
Add the MessageBroker snippet from `docs/DocumentAccepted_MessageBroker_snippet.qml` to a QML component (such as GeneralSettings.qml) that has access to PlatformHelpers. This will enable the QML side to receive document import requests from the HTTP server.
The MessageBroker listens for "documentAccepted" signals and automatically calls `PlatformHelpers.documentAccepted()` with the provided parameters.
## Document ID and Directory ID Discovery
To find document and directory IDs, you can:
1. Check the reMarkable application logs when opening documents or folders
2. Use the reMarkable Cloud API
3. Access the local database at `~/Library/Application Support/remarkable/desktop-app/`
4. For the root directory ID, check the logs when navigating to "My Files"
## Troubleshooting
- Ensure the HTTP server started successfully by checking the logs: `2025-12-08 17:32:22.288 reMarkable[19574:1316287] [HttpServer] HTTP server started successfully on http://localhost:8080`
- Test the health endpoint: `curl http://localhost:8080/health`
- Check the Console.app for detailed logging from the MessageBroker and HttpServer
- Verify the document ID and directory ID are correct UUIDs
- Ensure the target/url path is accessible and uses the `file://` prefix
- For imports, verify the target directory exists and is accessible

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 245 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

@@ -29,7 +29,7 @@ case "$BUILD_MODE" in
DYLIB_NAME="all.dylib"
;;
*)
DYLIB_NAME="reMarkable.dylib"
DYLIB_NAME="RMHook.dylib"
;;
esac
@@ -55,7 +55,7 @@ case "$BUILD_MODE" in
;;
esac
echo "🔨 Compiling reMarkable.dylib (mode: $BUILD_MODE)..."
echo "🔨 Compiling RMHook.dylib (mode: $BUILD_MODE)..."
echo "📦 Qt path: $QT_PATH"
# Create build directories if necessary
@@ -70,12 +70,12 @@ else
cmake $CMAKE_OPTIONS ..
fi
make reMarkable
make RMHook
if [ $? -eq 0 ]; then
# Rename the produced dylib so each build mode has a distinct file name
DYLIB_DIR="$PROJECT_DIR/build/dylibs"
DEFAULT_DYLIB="$DYLIB_DIR/reMarkable.dylib"
DEFAULT_DYLIB="$DYLIB_DIR/RMHook.dylib"
TARGET_DYLIB="$DYLIB_DIR/$DYLIB_NAME"
if [ -f "$DEFAULT_DYLIB" ]; then

View File

@@ -1,200 +0,0 @@
#!/usr/bin/env python3
import requests
import json
import sys
import argparse
BASE_URL = "http://localhost:8080"
def export_document(document_id, target_path, format_type=0, grayscale=False,
keep_password=True, password="", page_selection=None):
"""
Export a document via HTTP API
Args:
document_id: UUID of the document to export
target_path: Target path for the export (use file:// prefix)
format_type: Export format (0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml)
grayscale: Export with grayscale pens
keep_password: Keep password protection (for PDFs)
password: Password for protected documents
page_selection: List of page indices to export (None = all pages)
"""
print(f"\nExporting document {document_id}...")
print(f"Target: {target_path}")
print(f"Format: {format_type}")
data = {
"target": target_path,
"id": document_id,
"format": format_type,
"grayscale": grayscale,
"keepPassword": keep_password,
"password": password
}
if page_selection:
data["pageSelection"] = page_selection
print(f"Pages: {page_selection}")
try:
response = requests.post(
f"{BASE_URL}/exportFile",
json=data,
timeout=10
)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
return response.status_code == 200
except Exception as e:
print(f"Error: {e}")
return False
def import_document(file_url, directory_id, password="", flag1=False, flag2=False):
"""
Import a document via HTTP API
Args:
file_url: File URL to import (use file:// prefix)
directory_id: UUID of the target directory
password: Password for protected documents (optional)
flag1: Additional flag parameter
flag2: Additional flag parameter
"""
print(f"\nImporting document from {file_url}...")
print(f"Target directory: {directory_id}")
data = {
"url": file_url,
"password": password,
"directoryId": directory_id,
"flag1": flag1,
"flag2": flag2
}
try:
response = requests.post(
f"{BASE_URL}/documentAccepted",
json=data,
timeout=10
)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
return response.status_code == 200
except Exception as e:
print(f"Error: {e}")
return False
def main():
parser = argparse.ArgumentParser(
description='reMarkable HTTP Server API Client',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
Export a document as PDF:
%(prog)s export 12345678-1234-1234-1234-123456789abc file:///Users/noham/Desktop/test.pdf
Export as PNG with grayscale:
%(prog)s export <doc-id> <target> --format 1 --grayscale
Export specific pages:
%(prog)s export <doc-id> <target> --pages 0 1 2
Import a document:
%(prog)s import file:///Users/noham/Desktop/test.pdf 2166c19d-d2cc-456c-9f0e-49482031092a
Import with password:
%(prog)s import <file-url> <directory-id> --password mypassword
'''
)
subparsers = parser.add_subparsers(dest='command', help='Available commands')
# Export command
export_parser = subparsers.add_parser('export', help='Export a document')
export_parser.add_argument('document_id', help='UUID of the document to export')
export_parser.add_argument('target_path', help='Target path for export (use file:// prefix)')
export_parser.add_argument(
'--format', '-f',
type=int,
default=0,
choices=[0, 1, 2, 3, 4],
help='Export format: 0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml (default: 0)'
)
export_parser.add_argument(
'--grayscale', '-g',
action='store_true',
help='Export with grayscale pens'
)
export_parser.add_argument(
'--no-keep-password',
action='store_true',
help='Do not keep password protection (for PDFs)'
)
export_parser.add_argument(
'--password', '-p',
default='',
help='Password for protected documents'
)
export_parser.add_argument(
'--pages',
type=int,
nargs='+',
help='List of page indices to export (default: all pages)'
)
# Import command
import_parser = subparsers.add_parser('import', help='Import a document')
import_parser.add_argument('file_url', help='File URL to import (use file:// prefix)')
import_parser.add_argument('directory_id', help='UUID of the target directory')
import_parser.add_argument(
'--password', '-p',
default='',
help='Password for protected documents'
)
import_parser.add_argument(
'--flag1',
action='store_true',
help='Additional flag parameter'
)
import_parser.add_argument(
'--flag2',
action='store_true',
help='Additional flag parameter'
)
args = parser.parse_args()
if args.command == 'export':
success = export_document(
document_id=args.document_id,
target_path=args.target_path,
format_type=args.format,
grayscale=args.grayscale,
keep_password=not args.no_keep_password,
password=args.password,
page_selection=args.pages
)
if success:
print("\n✅ Export request sent successfully!")
else:
print("\n❌ Export request failed!")
sys.exit(1)
elif args.command == 'import':
success = import_document(
file_url=args.file_url,
directory_id=args.directory_id,
password=args.password,
flag1=args.flag1,
flag2=args.flag2
)
if success:
print("\n✅ Import request sent successfully!")
else:
print("\n❌ Import request failed!")
sys.exit(1)
else:
parser.print_help()
if __name__ == "__main__":
main()

View File

@@ -1,165 +0,0 @@
#!/usr/bin/env python3
"""
Test script for RMHook HTTP Server
Demonstrates how to trigger exports and imports via HTTP API
"""
import requests
import json
import sys
BASE_URL = "http://localhost:8080"
def test_health():
"""Test the health endpoint"""
print("Testing /health endpoint...")
try:
response = requests.get(f"{BASE_URL}/health", timeout=5)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
return response.status_code == 200
except Exception as e:
print(f"Error: {e}")
return False
def export_document(document_id, target_path, format_type=0, grayscale=False,
keep_password=True, password="", page_selection=None):
"""
Export a document via HTTP API
Args:
document_id: UUID of the document to export
target_path: Target path for the export (use file:// prefix)
format_type: Export format (0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml)
grayscale: Export with grayscale pens
keep_password: Keep password protection (for PDFs)
password: Password for protected documents
page_selection: List of page indices to export (None = all pages)
"""
print(f"\nExporting document {document_id}...")
print(f"Target: {target_path}")
print(f"Format: {format_type}")
data = {
"target": target_path,
"id": document_id,
"format": format_type,
"grayscale": grayscale,
"keepPassword": keep_password,
"password": password
}
if page_selection:
data["pageSelection"] = page_selection
print(f"Pages: {page_selection}")
try:
response = requests.post(
f"{BASE_URL}/exportFile",
json=data,
timeout=10
)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
return response.status_code == 200
except Exception as e:
print(f"Error: {e}")
return False
def import_document(file_url, directory_id, password=""):
"""
Import a document via HTTP API
Args:
file_url: File URL to import (use file:// prefix)
directory_id: UUID of the target directory
password: Password for protected documents (optional)
"""
print(f"\nImporting document from {file_url}...")
print(f"Target directory: {directory_id}")
data = {
"url": file_url,
"password": password,
"directoryId": directory_id,
"flag1": False,
"flag2": False
}
try:
response = requests.post(
f"{BASE_URL}/documentAccepted",
json=data,
timeout=10
)
print(f"Status: {response.status_code}")
print(f"Response: {json.dumps(response.json(), indent=2)}")
return response.status_code == 200
except Exception as e:
print(f"Error: {e}")
return False
def main():
print("=" * 60)
print("RMHook HTTP Server Test Script")
print("=" * 60)
# Test health endpoint
if not test_health():
print("\n❌ Health check failed. Is the server running?")
print("Make sure reMarkable app is running with the dylib injected.")
sys.exit(1)
print("\n✅ Server is running!")
# Command line interface
if len(sys.argv) < 2:
print("\n" + "=" * 60)
print("Usage Examples")
print("=" * 60)
print("\n1. Export a document:")
print(' python3 test_http_server.py export <doc-id> <target-path> [format] [grayscale]')
print("\n Example:")
print(' python3 test_http_server.py export "abc-123" "file:///Users/noham/Desktop/test.pdf" 0 false')
print("\n2. Import a document:")
print(' python3 test_http_server.py import <file-url> <directory-id>')
print("\n Example:")
print(' python3 test_http_server.py import "file:///Users/noham/Desktop/test.pdf" "2166c19d-d2cc-456c-9f0e-49482031092a"')
sys.exit(0)
command = sys.argv[1].lower()
if command == "export" and len(sys.argv) >= 4:
doc_id = sys.argv[2]
target = sys.argv[3]
format_type = int(sys.argv[4]) if len(sys.argv) > 4 else 0
grayscale = sys.argv[5].lower() == "true" if len(sys.argv) > 5 else False
success = export_document(doc_id, target, format_type, grayscale)
if success:
print("\n✅ Export request sent successfully!")
else:
print("\n❌ Export request failed!")
sys.exit(1)
elif command == "import" and len(sys.argv) >= 4:
file_url = sys.argv[2]
directory_id = sys.argv[3]
password = sys.argv[4] if len(sys.argv) > 4 else ""
success = import_document(file_url, directory_id, password)
if success:
print("\n✅ Import request sent successfully!")
else:
print("\n❌ Import request failed!")
sys.exit(1)
else:
print(f"\n❌ Invalid command or missing arguments: {' '.join(sys.argv[1:])}")
print("Run without arguments to see usage examples.")
sys.exit(1)
if __name__ == "__main__":
main()

16
src/RMHook/Config.h Normal file
View File

@@ -0,0 +1,16 @@
#import <Foundation/Foundation.h>
#include <QtCore/QString>
#include <QtNetwork/QSsl>
extern NSString *gConfiguredHostObjC;
extern NSNumber *gConfiguredPortObjC;
extern QString gConfiguredHost;
extern NSNumber *gConfiguredPort;
extern QString gConfiguredClientCertPath;
extern QString gConfiguredClientKeyPath;
extern QString gConfiguredCACertPath;
extern QSsl::KeyAlgorithm gConfiguredKeyAlgorithm;
extern bool gDisableSSLVerification;
void ConfigLoadOrCreate(void);
NSString *ConfigFilePath(void);

152
src/RMHook/Config.m Normal file
View File

@@ -0,0 +1,152 @@
#import "Config.h"
#import "Logger.h"
#include <QtCore/QString>
static NSString *const kConfigFileName = @"rmfakecloud.config";
static NSString *const kConfigHostKey = @"host";
static NSString *const kConfigPortKey = @"port";
static NSString *const kConfigClientCertKey = @"client_cert";
static NSString *const kConfigClientKeyKey = @"client_key";
static NSString *const kConfigCACertKey = @"ca_cert";
static NSString *const kConfigKeyAlgorithmKey = @"key_algorithm";
static NSString *const kConfigDisableSSLVerifyKey = @"disable_ssl_verification";
static NSString *const kDefaultHost = @"example.com";
static NSNumber *const kDefaultPort = @(443);
NSString *gConfiguredHostObjC = @"example.com";
NSNumber *gConfiguredPortObjC = @(443);
QString gConfiguredHost = QString::fromUtf8("example.com");
NSNumber *gConfiguredPort = @(443);
QString gConfiguredClientCertPath;
QString gConfiguredClientKeyPath;
QString gConfiguredCACertPath;
QSsl::KeyAlgorithm gConfiguredKeyAlgorithm = QSsl::Rsa;
bool gDisableSSLVerification = false;
static NSString *PreferencesDirectory(void) {
NSArray<NSString *> *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libraryDir = [libraryPaths firstObject];
if (![libraryDir length]) {
libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
}
return [libraryDir stringByAppendingPathComponent:@"Preferences"];
}
NSString *ConfigFilePath(void) {
return [PreferencesDirectory() stringByAppendingPathComponent:kConfigFileName];
}
static BOOL WriteConfig(NSString *path, NSDictionary<NSString *, id> *config) {
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:config options:NSJSONWritingPrettyPrinted error:&error];
if (!jsonData || error) {
NSLogger(@"[RMHook] Failed to serialize config: %@", error);
return NO;
}
if (![jsonData writeToFile:path atomically:YES]) {
NSLogger(@"[RMHook] Failed to write config file at %@", path);
return NO;
}
return YES;
}
static inline QString QStringFromNSStringSafe(NSString *string) {
if (!string) {
return QString();
}
return QString::fromUtf8([string UTF8String]);
}
void ConfigLoadOrCreate(void) {
NSString *configPath = ConfigFilePath();
NSString *directory = [configPath stringByDeletingLastPathComponent];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDirectory = NO;
NSError *error = nil;
if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] || !isDirectory) {
if (![fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLogger(@"[RMHook] Failed to create config directory %@: %@", directory, error);
}
}
NSDictionary<NSString *, id> *defaults = @{kConfigHostKey : kDefaultHost,
kConfigPortKey : kDefaultPort};
if ([fileManager fileExistsAtPath:configPath isDirectory:&isDirectory] && !isDirectory) {
NSData *data = [NSData dataWithContentsOfFile:configPath];
if ([data length] > 0) {
NSError *jsonError = nil;
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (!jsonError && [jsonObject isKindOfClass:[NSDictionary class]]) {
NSDictionary *configDict = (NSDictionary *)jsonObject;
NSString *hostValue = configDict[kConfigHostKey];
NSNumber *portValue = configDict[kConfigPortKey];
NSString *resolvedHost = ([hostValue isKindOfClass:[NSString class]] && [hostValue length]) ? hostValue : kDefaultHost;
NSInteger portCandidate = kDefaultPort.integerValue;
if ([portValue respondsToSelector:@selector(integerValue)]) {
NSInteger candidate = [portValue integerValue];
if (candidate > 0 && candidate <= 65535) {
portCandidate = candidate;
} else {
NSLogger(@"[RMHook] Ignoring invalid port value %@, falling back to default.", portValue);
}
}
gConfiguredHostObjC = [resolvedHost copy];
gConfiguredPortObjC = @(portCandidate);
gConfiguredHost = QStringFromNSStringSafe(resolvedHost);
gConfiguredPort = @(portCandidate);
NSString *certPathValue = configDict[kConfigClientCertKey];
NSString *keyPathValue = configDict[kConfigClientKeyKey];
NSString *caPathValue = configDict[kConfigCACertKey];
NSNumber *disableSSLValue = configDict[kConfigDisableSSLVerifyKey];
if ([certPathValue isKindOfClass:[NSString class]] && [certPathValue length]) {
gConfiguredClientCertPath = QStringFromNSStringSafe(certPathValue);
}
if ([keyPathValue isKindOfClass:[NSString class]] && [keyPathValue length]) {
gConfiguredClientKeyPath = QStringFromNSStringSafe(keyPathValue);
}
if ([caPathValue isKindOfClass:[NSString class]] && [caPathValue length]) {
gConfiguredCACertPath = QStringFromNSStringSafe(caPathValue);
}
NSString *keyAlgoValue = configDict[kConfigKeyAlgorithmKey];
if ([keyAlgoValue isKindOfClass:[NSString class]] && [keyAlgoValue length]) {
NSString *lower = [keyAlgoValue lowercaseString];
if ([lower isEqualToString:@"ec"]) {
gConfiguredKeyAlgorithm = QSsl::Ec;
} else if ([lower isEqualToString:@"dsa"]) {
gConfiguredKeyAlgorithm = QSsl::Dsa;
} else if ([lower isEqualToString:@"dh"]) {
gConfiguredKeyAlgorithm = QSsl::Dh;
} else if ([lower isEqualToString:@"opaque"]) {
gConfiguredKeyAlgorithm = QSsl::Opaque;
} else {
gConfiguredKeyAlgorithm = QSsl::Rsa;
}
}
if ([disableSSLValue respondsToSelector:@selector(boolValue)]) {
gDisableSSLVerification = [disableSSLValue boolValue];
}
NSLogger(@"[RMHook] Loaded config from %@ with host %@ and port %@", configPath, gConfiguredHostObjC, gConfiguredPortObjC);
return;
} else {
NSLogger(@"[RMHook] Failed to parse config file %@: %@", configPath, jsonError);
}
} else {
NSLogger(@"[RMHook] Config file %@ was empty, rewriting with defaults.", configPath);
}
}
if (WriteConfig(configPath, defaults)) {
NSLogger(@"[RMHook] Created default config at %@", configPath);
}
gConfiguredHostObjC = [kDefaultHost copy];
gConfiguredPortObjC = kDefaultPort;
gConfiguredHost = QString::fromUtf8("example.com");
gConfiguredPort = kDefaultPort;
}

View File

@@ -35,18 +35,18 @@ void (*original_function_at_0x1001B6EE0)(int64_t a1, int64_t *a2, unsigned int a
void logMemory(const char *label, void *address, size_t length) {
if (!address) {
NSLogger(@"[reMarkable] %s: (null)", label);
NSLogger(@"[RMHook] %s: (null)", label);
return;
}
unsigned char *ptr = (unsigned char *)address;
NSMutableString *hexLine = [NSMutableString stringWithFormat:@"[reMarkable] %s: ", label];
NSMutableString *hexLine = [NSMutableString stringWithFormat:@"[RMHook] %s: ", label];
for (size_t i = 0; i < length; i++) {
[hexLine appendFormat:@"%02x ", ptr[i]];
if ((i + 1) % 16 == 0 && i < length - 1) {
NSLogger(@"%@", hexLine);
hexLine = [NSMutableString stringWithString:@"[reMarkable] "];
hexLine = [NSMutableString stringWithString:@"[RMHook] "];
}
}
@@ -57,13 +57,13 @@ void logMemory(const char *label, void *address, size_t length) {
}
void logStackTrace(const char *label) {
NSLogger(@"[reMarkable] %s - Stack trace:", label);
NSLogger(@"[RMHook] %s - Stack trace:", label);
NSArray<NSString *> *callStack = [NSThread callStackSymbols];
NSUInteger count = [callStack count];
for (NSUInteger i = 0; i < count; i++) {
NSString *frame = callStack[i];
NSLogger(@"[reMarkable] #%lu: %@", (unsigned long)i, frame);
NSLogger(@"[RMHook] #%lu: %@", (unsigned long)i, frame);
}
}
@@ -73,39 +73,39 @@ extern "C" ssize_t hooked_qIODevice_write(
QIODevice *self,
const char *data,
int64_t maxSize) {
NSLogger(@"[reMarkable] QIODevice::write called with maxSize: %lld", (long long)maxSize);
NSLogger(@"[RMHook] QIODevice::write called with maxSize: %lld", (long long)maxSize);
logStackTrace("QIODevice::write call stack");
logMemory("Data to write", (void *)data, (size_t)(maxSize < 64 ? maxSize : 64));
if (original_qIODevice_write) {
ssize_t result = original_qIODevice_write(self, data, maxSize);
NSLogger(@"[reMarkable] QIODevice::write result: %zd", result);
NSLogger(@"[RMHook] QIODevice::write result: %zd", result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original QIODevice::write not available, returning 0");
NSLogger(@"[RMHook] WARNING: Original QIODevice::write not available, returning 0");
return 0;
}
extern "C" int64_t hooked_function_at_0x100011790(uint64_t *a1) {
NSLogger(@"[reMarkable] Hook at 0x100011790 called!");
NSLogger(@"[reMarkable] a1 = %p", a1);
NSLogger(@"[RMHook] Hook at 0x100011790 called!");
NSLogger(@"[RMHook] a1 = %p", a1);
if (a1) {
NSLogger(@"[reMarkable] *a1 = 0x%llx", (unsigned long long)*a1);
NSLogger(@"[RMHook] *a1 = 0x%llx", (unsigned long long)*a1);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at *a1", (void *)(*a1), 64);
} else {
NSLogger(@"[reMarkable] a1 is NULL");
NSLogger(@"[RMHook] a1 is NULL");
}
if (original_function_at_0x100011790) {
int64_t result = original_function_at_0x100011790(a1);
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
NSLogger(@"[RMHook] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function at 0x100011790 not available, returning 0");
NSLogger(@"[RMHook] WARNING: Original function at 0x100011790 not available, returning 0");
return 0;
}
@@ -126,25 +126,25 @@ extern "C" int64_t hooked_function_at_0x100011CE0(
// - a4: stored at offset +0x18 (likely URI or type info pointer)
// - a5: ExternalRefCountData* for shared pointer ref counting
NSLogger(@"[reMarkable] ========================================");
NSLogger(@"[reMarkable] Hook at 0x100011CE0 (QML Type Registration)");
NSLogger(@"[reMarkable] ========================================");
NSLogger(@"[RMHook] ========================================");
NSLogger(@"[RMHook] Hook at 0x100011CE0 (QML Type Registration)");
NSLogger(@"[RMHook] ========================================");
NSLogger(@"[reMarkable] a1 (typeMetadata?) = 0x%llx", (unsigned long long)a1);
NSLogger(@"[RMHook] a1 (typeMetadata?) = 0x%llx", (unsigned long long)a1);
uint16_t a2_low = (uint16_t)(uintptr_t)a2;
uint16_t combined_v17 = (a2_low << 8) | a3;
NSLogger(@"[reMarkable] a2 (raw) = %p (0x%llx)", a2, (unsigned long long)(uintptr_t)a2);
NSLogger(@"[reMarkable] a2 low 16 bits = 0x%04x (%u)", a2_low, a2_low);
NSLogger(@"[reMarkable] a3 (flags/version) = 0x%02x (%u)", a3, a3);
NSLogger(@"[reMarkable] v17 = (a2<<8)|a3 = 0x%04x (%u)", combined_v17, combined_v17);
NSLogger(@"[reMarkable] a4 (typeInfo/URI?) = 0x%llx", (unsigned long long)a4);
NSLogger(@"[reMarkable] a5 (refCountData) = %p", a5);
NSLogger(@"[RMHook] a2 (raw) = %p (0x%llx)", a2, (unsigned long long)(uintptr_t)a2);
NSLogger(@"[RMHook] a2 low 16 bits = 0x%04x (%u)", a2_low, a2_low);
NSLogger(@"[RMHook] a3 (flags/version) = 0x%02x (%u)", a3, a3);
NSLogger(@"[RMHook] v17 = (a2<<8)|a3 = 0x%04x (%u)", combined_v17, combined_v17);
NSLogger(@"[RMHook] a4 (typeInfo/URI?) = 0x%llx", (unsigned long long)a4);
NSLogger(@"[RMHook] a5 (refCountData) = %p", a5);
if (a1) {
logMemory("Memory at a1 (typeMetadata)", (void *)a1, 64);
void **vtable = (void **)a1;
NSLogger(@"[reMarkable] a1 vtable/first ptr = %p", *vtable);
NSLogger(@"[RMHook] a1 vtable/first ptr = %p", *vtable);
}
if (a4) {
@@ -160,7 +160,7 @@ extern "C" int64_t hooked_function_at_0x100011CE0(
len++;
}
if (isPrintable && len > 0) {
NSLogger(@"[reMarkable] a4 as string: \"%.*s\"", len, maybeStr);
NSLogger(@"[RMHook] a4 as string: \"%.*s\"", len, maybeStr);
}
}
@@ -172,59 +172,59 @@ extern "C" int64_t hooked_function_at_0x100011CE0(
if (original_function_at_0x100011CE0) {
int64_t result = original_function_at_0x100011CE0(a1, a2, a3, a4, a5);
NSLogger(@"[reMarkable] result (qmlregister return) = %u (0x%x)", (unsigned int)result, (unsigned int)result);
NSLogger(@"[reMarkable] ========================================");
NSLogger(@"[RMHook] result (qmlregister return) = %u (0x%x)", (unsigned int)result, (unsigned int)result);
NSLogger(@"[RMHook] ========================================");
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function at 0x100011CE0 not available, returning 0");
NSLogger(@"[RMHook] WARNING: Original function at 0x100011CE0 not available, returning 0");
return 0;
}
extern "C" int64_t hooked_function_at_0x10015A130(int64_t a1, int64_t a2) {
NSLogger(@"[reMarkable] Hook at 0x10015A130 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = 0x%llx", (unsigned long long)a2);
NSLogger(@"[RMHook] Hook at 0x10015A130 called!");
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[RMHook] a2 = 0x%llx", (unsigned long long)a2);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (original_function_at_0x10015A130) {
int64_t result = original_function_at_0x10015A130(a1, a2);
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
NSLogger(@"[RMHook] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function at 0x10015A130 not available, returning 0");
NSLogger(@"[RMHook] WARNING: Original function at 0x10015A130 not available, returning 0");
return 0;
}
extern "C" void hooked_function_at_0x10015BC90(int64_t a1, int64_t a2) {
NSLogger(@"[reMarkable] Hook at 0x10015BC90 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = 0x%llx", (unsigned long long)a2);
NSLogger(@"[RMHook] Hook at 0x10015BC90 called!");
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[RMHook] a2 = 0x%llx", (unsigned long long)a2);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (original_function_at_0x10015BC90) {
original_function_at_0x10015BC90(a1, a2);
NSLogger(@"[reMarkable] original function returned (void)");
NSLogger(@"[RMHook] original function returned (void)");
return;
}
NSLogger(@"[reMarkable] WARNING: Original function at 0x10015BC90 not available");
NSLogger(@"[RMHook] WARNING: Original function at 0x10015BC90 not available");
}
extern "C" int64_t hooked_function_at_0x10016D520(int64_t a1, int64_t *a2, unsigned int a3, int64_t a4) {
NSLogger(@"[reMarkable] Hook at 0x10016D520 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = %p", a2);
NSLogger(@"[RMHook] Hook at 0x10016D520 called!");
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[RMHook] a2 = %p", a2);
if (a2) {
NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2);
NSLogger(@"[RMHook] *a2 = 0x%llx", (unsigned long long)*a2);
}
NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3);
NSLogger(@"[reMarkable] a4 = 0x%llx", (unsigned long long)a4);
NSLogger(@"[RMHook] a3 = %u (0x%x)", a3, a3);
NSLogger(@"[RMHook] a4 = 0x%llx", (unsigned long long)a4);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
@@ -237,28 +237,28 @@ extern "C" int64_t hooked_function_at_0x10016D520(int64_t a1, int64_t *a2, unsig
if (original_function_at_0x10016D520) {
int64_t result = original_function_at_0x10016D520(a1, a2, a3, a4);
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
NSLogger(@"[RMHook] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function not available, returning 0");
NSLogger(@"[RMHook] WARNING: Original function not available, returning 0");
return 0;
}
extern "C" void hooked_function_at_0x1001B6EE0(int64_t a1, int64_t *a2, unsigned int a3) {
NSLogger(@"[reMarkable] Hook at 0x1001B6EE0 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[RMHook] Hook at 0x1001B6EE0 called!");
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
// At a1 (PdfExporter object):
// +0x10 contains a QString (likely document name)
NSLogger(@"[reMarkable] Reading QString at a1+0x10:");
NSLogger(@"[RMHook] Reading QString at a1+0x10:");
logMemory("a1 + 0x10 (raw)", (void *)(a1 + 0x10), 64);
void **qstrPtr = (void **)(a1 + 0x10);
void *dataPtr = *qstrPtr;
if (!dataPtr) {
NSLogger(@"[reMarkable] QString has null data pointer");
NSLogger(@"[RMHook] QString has null data pointer");
return;
}
@@ -268,29 +268,29 @@ extern "C" void hooked_function_at_0x1001B6EE0(int64_t a1, int64_t *a2, unsigned
int32_t candidate = *(int32_t *)((char *)dataPtr - delta);
if (candidate > 0 && candidate < 10000) {
size = candidate;
NSLogger(@"[reMarkable] QString plausible size=%d (found at -%d)", size, delta);
NSLogger(@"[RMHook] QString plausible size=%d (found at -%d)", size, delta);
break;
}
}
if (size > 0) {
NSString *qstringValue = [[NSString alloc] initWithCharacters:(unichar *)dataPtr length:size];
NSLogger(@"[reMarkable] QString value: \"%@\"", qstringValue);
NSLogger(@"[RMHook] QString value: \"%@\"", qstringValue);
} else {
NSLogger(@"[reMarkable] QString: could not find valid size");
NSLogger(@"[RMHook] QString: could not find valid size");
}
NSLogger(@"[reMarkable] a2 = %p", a2);
NSLogger(@"[RMHook] a2 = %p", a2);
if (a2) {
NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2);
NSLogger(@"[RMHook] *a2 = 0x%llx", (unsigned long long)*a2);
}
NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3);
NSLogger(@"[RMHook] a3 = %u (0x%x)", a3, a3);
if (original_function_at_0x1001B6EE0) {
original_function_at_0x1001B6EE0(a1, a2, a3);
NSLogger(@"[reMarkable] Original function at 0x1001B6EE0 executed");
NSLogger(@"[RMHook] Original function at 0x1001B6EE0 executed");
} else {
NSLogger(@"[reMarkable] WARNING: Original function not available");
NSLogger(@"[RMHook] WARNING: Original function not available");
}
}
@@ -306,19 +306,19 @@ extern "C" int64_t hooked_qmlregister(
int a9,
int64_t a10) {
NSLogger(@"[reMarkable] ========================================");
NSLogger(@"[reMarkable] QQmlPrivate::qmlregister called!");
NSLogger(@"[reMarkable] ========================================");
NSLogger(@"[reMarkable] a1 (RegistrationType) = 0x%llx (%lld)", (unsigned long long)a1, (long long)a1);
NSLogger(@"[reMarkable] a2 = 0x%llx (%lld)", (unsigned long long)a2, (long long)a2);
NSLogger(@"[reMarkable] a3 = 0x%llx (%lld)", (unsigned long long)a3, (long long)a3);
NSLogger(@"[reMarkable] a4 = 0x%llx (%lld)", (unsigned long long)a4, (long long)a4);
NSLogger(@"[reMarkable] a5 = 0x%llx (%lld)", (unsigned long long)a5, (long long)a5);
NSLogger(@"[reMarkable] a6 = 0x%llx (%lld)", (unsigned long long)a6, (long long)a6);
NSLogger(@"[reMarkable] a7 = 0x%x (%d)", a7, a7);
NSLogger(@"[reMarkable] a8 = 0x%llx (%lld)", (unsigned long long)a8, (long long)a8);
NSLogger(@"[reMarkable] a9 = 0x%x (%d)", a9, a9);
NSLogger(@"[reMarkable] a10 = 0x%llx (%lld)", (unsigned long long)a10, (long long)a10);
NSLogger(@"[RMHook] ========================================");
NSLogger(@"[RMHook] QQmlPrivate::qmlregister called!");
NSLogger(@"[RMHook] ========================================");
NSLogger(@"[RMHook] a1 (RegistrationType) = 0x%llx (%lld)", (unsigned long long)a1, (long long)a1);
NSLogger(@"[RMHook] a2 = 0x%llx (%lld)", (unsigned long long)a2, (long long)a2);
NSLogger(@"[RMHook] a3 = 0x%llx (%lld)", (unsigned long long)a3, (long long)a3);
NSLogger(@"[RMHook] a4 = 0x%llx (%lld)", (unsigned long long)a4, (long long)a4);
NSLogger(@"[RMHook] a5 = 0x%llx (%lld)", (unsigned long long)a5, (long long)a5);
NSLogger(@"[RMHook] a6 = 0x%llx (%lld)", (unsigned long long)a6, (long long)a6);
NSLogger(@"[RMHook] a7 = 0x%x (%d)", a7, a7);
NSLogger(@"[RMHook] a8 = 0x%llx (%lld)", (unsigned long long)a8, (long long)a8);
NSLogger(@"[RMHook] a9 = 0x%x (%d)", a9, a9);
NSLogger(@"[RMHook] a10 = 0x%llx (%lld)", (unsigned long long)a10, (long long)a10);
// Check for PlatformHelpers registration
// a1 == 0 means TypeRegistration (object registration)
@@ -341,14 +341,14 @@ extern "C" int64_t hooked_qmlregister(
}
if (isValid && len > 0) {
NSLogger(@"[reMarkable] typeName (a4) = \"%.*s\"", len, typeName);
NSLogger(@"[RMHook] typeName (a4) = \"%.*s\"", len, typeName);
if (len == 15 && strncmp(typeName, "PlatformHelpers", 15) == 0) {
NSLogger(@"[reMarkable] !!! FOUND PlatformHelpers type registration !!!");
NSLogger(@"[reMarkable] factory ptr (a2) = %p", (void *)a2);
NSLogger(@"[reMarkable] a3 (metaObject?) = %p", (void *)a3);
NSLogger(@"[reMarkable] a5 = %p", (void *)a5);
NSLogger(@"[reMarkable] a6 = %p", (void *)a6);
NSLogger(@"[RMHook] !!! FOUND PlatformHelpers type registration !!!");
NSLogger(@"[RMHook] factory ptr (a2) = %p", (void *)a2);
NSLogger(@"[RMHook] a3 (metaObject?) = %p", (void *)a3);
NSLogger(@"[RMHook] a5 = %p", (void *)a5);
NSLogger(@"[RMHook] a6 = %p", (void *)a6);
logMemory("Factory ptr memory", (void *)a2, 64);
logMemory("a3 memory (metaObject?)", (void *)a3, 64);
logStackTrace("PlatformHelpers registration");
@@ -370,19 +370,19 @@ extern "C" int64_t hooked_qmlregister(
len++;
}
if (isPrintable && len > 0) {
NSLogger(@"[reMarkable] a2 as string: \"%.*s\"", len, maybeStr);
NSLogger(@"[RMHook] a2 as string: \"%.*s\"", len, maybeStr);
}
}
int64_t result = 0;
if (original_qmlregister) {
result = original_qmlregister(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
NSLogger(@"[reMarkable] result = 0x%llx (%lld)", (unsigned long long)result, (long long)result);
NSLogger(@"[RMHook] result = 0x%llx (%lld)", (unsigned long long)result, (long long)result);
} else {
NSLogger(@"[reMarkable] WARNING: Original qmlregister not available!");
NSLogger(@"[RMHook] WARNING: Original qmlregister not available!");
}
NSLogger(@"[reMarkable] ========================================");
NSLogger(@"[RMHook] ========================================");
return result;
}

View File

@@ -1,5 +1,5 @@
#import <Foundation/Foundation.h>
@interface reMarkable : NSObject
@interface RMHook : NSObject
@end

View File

@@ -1,15 +1,16 @@
#import "reMarkable.h"
#import "RMHook.h"
#import <Foundation/Foundation.h>
#import "Constant.h"
#import "MemoryUtils.h"
#import "Logger.h"
#import "ResourceUtils.h"
#import "Config.h"
#import "SSLConfig.h"
#ifdef BUILD_MODE_DEV
#import "DevHooks.h"
#endif
#ifdef BUILD_MODE_QMLREBUILD
#import "MessageBroker.h"
#import "HttpServer.h"
#endif
#import <objc/runtime.h>
#import <Cocoa/Cocoa.h>
@@ -19,6 +20,7 @@
#include <stdlib.h>
#include <string.h>
#include <dispatch/dispatch.h>
#include <string>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
@@ -29,113 +31,9 @@
#include <QtCore/QString>
#include <QtCore/Qt>
#include <QtWebSockets/QWebSocket>
#include <QtCore/QSettings>
#include <QtCore/QVariant>
#include <QtCore/QAnyStringView>
static NSString *const kReMarkableConfigFileName = @"rmfakecloud.config";
static NSString *const kReMarkableConfigHostKey = @"host";
static NSString *const kReMarkableConfigPortKey = @"port";
static NSString *const kReMarkableDefaultHost = @"example.com";
static NSNumber *const kReMarkableDefaultPort = @(443);
static NSString *gConfiguredHost = @"example.com";
static NSNumber *gConfiguredPort = @(443);
static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER;
static NSString *ReMarkablePreferencesDirectory(void);
static NSString *ReMarkablePreferencesDirectory(void) {
NSArray<NSString *> *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libraryDir = [libraryPaths firstObject];
if (![libraryDir length]) {
libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
}
return [libraryDir stringByAppendingPathComponent:@"Preferences"];
}
static NSString *ReMarkableConfigFilePath(void) {
return [ReMarkablePreferencesDirectory() stringByAppendingPathComponent:kReMarkableConfigFileName];
}
static BOOL ReMarkableWriteConfig(NSString *path, NSDictionary<NSString *, id> *config) {
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:config options:NSJSONWritingPrettyPrinted error:&error];
if (!jsonData || error) {
NSLogger(@"[reMarkable] Failed to serialize config: %@", error);
return NO;
}
if (![jsonData writeToFile:path atomically:YES]) {
NSLogger(@"[reMarkable] Failed to write config file at %@", path);
return NO;
}
return YES;
}
static void ReMarkableLoadOrCreateConfig(void) {
NSString *configPath = ReMarkableConfigFilePath();
NSString *directory = [configPath stringByDeletingLastPathComponent];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDirectory = NO;
NSError *error = nil;
if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] || !isDirectory) {
if (![fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLogger(@"[reMarkable] Failed to create config directory %@: %@", directory, error);
}
}
NSDictionary<NSString *, id> *defaults = @{kReMarkableConfigHostKey : kReMarkableDefaultHost,
kReMarkableConfigPortKey : kReMarkableDefaultPort};
if ([fileManager fileExistsAtPath:configPath isDirectory:&isDirectory] && !isDirectory) {
NSData *data = [NSData dataWithContentsOfFile:configPath];
if ([data length] > 0) {
NSError *jsonError = nil;
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (!jsonError && [jsonObject isKindOfClass:[NSDictionary class]]) {
NSDictionary *configDict = (NSDictionary *)jsonObject;
NSString *hostValue = configDict[kReMarkableConfigHostKey];
NSNumber *portValue = configDict[kReMarkableConfigPortKey];
NSString *resolvedHost = ([hostValue isKindOfClass:[NSString class]] && [hostValue length]) ? hostValue : kReMarkableDefaultHost;
NSInteger portCandidate = kReMarkableDefaultPort.integerValue;
if ([portValue respondsToSelector:@selector(integerValue)]) {
NSInteger candidate = [portValue integerValue];
if (candidate > 0 && candidate <= 65535) {
portCandidate = candidate;
} else {
NSLogger(@"[reMarkable] Ignoring invalid port value %@, falling back to default.", portValue);
}
}
gConfiguredHost = [resolvedHost copy];
gConfiguredPort = @(portCandidate);
NSLogger(@"[reMarkable] Loaded config from %@ with host %@ and port %@", configPath, gConfiguredHost, gConfiguredPort);
return;
} else {
NSLogger(@"[reMarkable] Failed to parse config file %@: %@", configPath, jsonError);
}
} else {
NSLogger(@"[reMarkable] Config file %@ was empty, rewriting with defaults.", configPath);
}
}
if (ReMarkableWriteConfig(configPath, defaults)) {
NSLogger(@"[reMarkable] Created default config at %@", configPath);
}
gConfiguredHost = [kReMarkableDefaultHost copy];
gConfiguredPort = kReMarkableDefaultPort;
}
static inline QString QStringFromNSStringSafe(NSString *string) {
if (!string) {
return QString();
}
return QString::fromUtf8([string UTF8String]);
}
@interface MenuActionController : NSObject
@property (strong, nonatomic) NSURL *targetURL;
- (void)openURLAction:(id)sender;
@@ -207,24 +105,22 @@ static inline QString QStringFromNSStringSafe(NSString *string) {
@end
@interface reMarkableDylib : NSObject
@interface RMHookDylib : NSObject
- (BOOL)hook;
@end
@implementation reMarkable
@implementation RMHook
+ (void)load {
NSLogger(@"reMarkable dylib loaded successfully");
NSLogger(@"RMHook dylib loaded successfully");
// Initialize the hook
reMarkableDylib *dylib = [[reMarkableDylib alloc] init];
RMHookDylib *dylib = [[RMHookDylib alloc] init];
[dylib hook];
#ifdef BUILD_MODE_RMFAKECLOUD
// Add custom Help menu entry to open config file
NSString *configPath = ReMarkableConfigFilePath();
NSString *configPath = ConfigFilePath();
NSString *fileURL = [NSString stringWithFormat:@"file://%@", configPath];
[MenuActionController addCustomHelpMenuEntry:@"Open rmfakecloud config"
withURL:fileURL
@@ -234,7 +130,7 @@ static inline QString QStringFromNSStringSafe(NSString *string) {
@end
@implementation reMarkableDylib
@implementation RMHookDylib
#ifdef BUILD_MODE_RMFAKECLOUD
static QNetworkReply *(*original_qNetworkAccessManager_createRequest)(
@@ -246,6 +142,17 @@ static QNetworkReply *(*original_qNetworkAccessManager_createRequest)(
static void (*original_qWebSocket_open)(
QWebSocket *self,
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
#ifdef BUILD_MODE_QMLREBUILD
@@ -256,8 +163,6 @@ static int (*original_qRegisterResourceData)(
const unsigned char *) = NULL;
#endif
#ifdef BUILD_MODE_RMFAKECLOUD
static inline bool shouldPatchURL(const QString &host) {
if (host.isEmpty()) {
@@ -283,115 +188,63 @@ static inline bool shouldPatchURL(const QString &host) {
}
#endif
static inline QString QStringFromNSStringSafe(NSString *string) {
if (!string) {
return QString();
}
return QString::fromUtf8([string UTF8String]);
}
- (BOOL)hook {
NSLogger(@"[reMarkable] Starting hooks...");
NSLogger(@"[RMHook] Starting hooks...");
#ifdef BUILD_MODE_RMFAKECLOUD
NSLogger(@"[reMarkable] Build mode: rmfakecloud");
ReMarkableLoadOrCreateConfig();
NSLogger(@"[reMarkable] Using override host %@ and port %@", gConfiguredHost, gConfiguredPort);
NSLogger(@"[RMHook] Build mode: rmfakecloud");
ConfigLoadOrCreate();
SSLConfigLoad();
NSLogger(@"[RMHook] Using override host %@ and port %@", gConfiguredHostObjC, gConfiguredPortObjC);
[MemoryUtils hookSymbol:@"QtNetwork"
symbolName:@"__ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice"
hookFunction:(void *)hooked_qNetworkAccessManager_createRequest
originalFunction:(void **)&original_qNetworkAccessManager_createRequest
logPrefix:@"[reMarkable]"];
logPrefix:@"[RMHook]"];
[MemoryUtils hookSymbol:@"QtWebSockets"
symbolName:@"__ZN10QWebSocket4openERK15QNetworkRequest"
hookFunction:(void *)hooked_qWebSocket_open
originalFunction:(void **)&original_qWebSocket_open
logPrefix:@"[reMarkable]"];
logPrefix:@"[RMHook]"];
[MemoryUtils hookSymbol:@"libpaho-mqtt3as.1.dylib"
symbolName:@"_MQTTAsync_createWithOptions"
hookFunction:(void *)hooked_MQTTAsync_createWithOptions
originalFunction:(void **)&original_MQTTAsync_createWithOptions
logPrefix:@"[RMHook]"];
#endif
#ifdef BUILD_MODE_QMLREBUILD
NSLogger(@"[reMarkable] Build mode: qmlrebuild");
NSLogger(@"[RMHook] Build mode: qmlrebuild");
// Register MessageBroker QML type for dylib <-> QML communication
messagebroker::registerQmlType();
// Register native callback to receive signals from QML
messagebroker::setNativeCallback([](const char *signal, const char *value) {
NSLogger(@"[reMarkable] Native callback received signal '%s' with value '%s'", signal, value);
NSLogger(@"[RMHook] Native callback received signal '%s' with value '%s'", signal, value);
});
// Start HTTP server for export requests
if (httpserver::start(8080)) {
NSLogger(@"[reMarkable] HTTP server started on http://localhost:8080");
} else {
NSLogger(@"[reMarkable] Failed to start HTTP server");
}
[MemoryUtils hookSymbol:@"QtCore"
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
hookFunction:(void *)hooked_qRegisterResourceData
originalFunction:(void **)&original_qRegisterResourceData
logPrefix:@"[reMarkable]"];
logPrefix:@"[RMHook]"];
// Send a delayed broadcast to QML (after UI has loaded)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
messagebroker::broadcast("signalName", "Hello from dylib!");
});
#endif
#ifdef BUILD_MODE_DEV
NSLogger(@"[reMarkable] Build mode: dev/reverse engineering");
// [MemoryUtils hookSymbol:@"QtCore"
// symbolName:@"__ZN9QIODevice5writeEPKcx"
// hookFunction:(void *)hooked_qIODevice_write
// originalFunction:(void **)&original_qIODevice_write
// logPrefix:@"[reMarkable]"];
// // Hook function at address 0x10015A130
// [MemoryUtils hookAddress:@"reMarkable"
// staticAddress:0x10015A130
// hookFunction:(void *)hooked_function_at_0x10015A130
// originalFunction:(void **)&original_function_at_0x10015A130
// logPrefix:@"[reMarkable]"];
// // Hook function at address 0x10015BC90
// [MemoryUtils hookAddress:@"reMarkable"
// staticAddress:0x10015BC90
// hookFunction:(void *)hooked_function_at_0x10015BC90
// originalFunction:(void **)&original_function_at_0x10015BC90
// logPrefix:@"[reMarkable]"];
// // Hook function at address 0x10016D520
// [MemoryUtils hookAddress:@"reMarkable"
// staticAddress:0x10016D520
// hookFunction:(void *)hooked_function_at_0x10016D520
// originalFunction:(void **)&original_function_at_0x10016D520
// logPrefix:@"[reMarkable]"];
// // Hook function at address 0x1001B6EE0
// [MemoryUtils hookAddress:@"reMarkable"
// staticAddress:0x1001B6EE0
// hookFunction:(void *)hooked_function_at_0x1001B6EE0
// originalFunction:(void **)&original_function_at_0x1001B6EE0
// logPrefix:@"[reMarkable]"];
// PlatformHelpers.exportFile implementation WIP
// // Hook function at address 0x100011790
// [MemoryUtils hookAddress:@"reMarkable"
// staticAddress:0x100011790
// hookFunction:(void *)hooked_function_at_0x100011790
// originalFunction:(void **)&original_function_at_0x100011790
// logPrefix:@"[reMarkable]"];
// // Hook function at address 0x100011CE0
// [MemoryUtils hookAddress:@"reMarkable"
// staticAddress:0x100011CE0
// hookFunction:(void *)hooked_function_at_0x100011CE0
// originalFunction:(void **)&original_function_at_0x100011CE0
// logPrefix:@"[reMarkable]"];
// [MemoryUtils hookSymbol:@"QtQml"
// symbolName:@"__ZN11QQmlPrivate11qmlregisterENS_16RegistrationTypeEPv"
// hookFunction:(void *)hooked_qmlregister
// originalFunction:(void **)&original_qmlregister
// logPrefix:@"[reMarkable]"];
NSLogger(@"[RMHook] Build mode: dev/reverse engineering");
#endif
return YES;
@@ -406,14 +259,15 @@ extern "C" QNetworkReply* hooked_qNetworkAccessManager_createRequest(
) {
const QString host = req.url().host();
if (shouldPatchURL(host)) {
// Clone request to keep original immutable
QNetworkRequest newReq(req);
QUrl newUrl = req.url();
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost);
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHostObjC);
newUrl.setHost(overrideHost);
newUrl.setPort([gConfiguredPort intValue]);
newUrl.setPort([gConfiguredPortObjC intValue]);
newReq.setUrl(newUrl);
SSLConfigApplyToRequest(newReq);
if (original_qNetworkAccessManager_createRequest) {
return original_qNetworkAccessManager_createRequest(self, op, newReq, outgoingData);
}
@@ -437,25 +291,92 @@ extern "C" void hooked_qWebSocket_open(
const QString host = req.url().host();
if (shouldPatchURL(host)) {
QUrl newUrl = req.url();
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost);
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHostObjC);
newUrl.setHost(overrideHost);
newUrl.setPort([gConfiguredPort intValue]);
newUrl.setPort([gConfiguredPortObjC intValue]);
QNetworkRequest newReq(req);
newReq.setUrl(newUrl);
SSLConfigApplyToRequest(newReq);
original_qWebSocket_open(self, newReq);
return;
}
original_qWebSocket_open(self, req);
}
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);
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 = [gConfiguredHostObjC UTF8String];
patched.replace(hostStart, hostEnd - hostStart, proxyHost);
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([gConfiguredPortObjC 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;
}
std::string patchedUri = PatchMqttUri(serverURI);
if (!patchedUri.empty()) {
NSLogger(@"[RMHook] 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
#ifdef BUILD_MODE_QMLREBUILD
// See https://deepwiki.com/search/once-the-qrr-file-parsed-take_871f24a0-8636-4aee-bddf-7405b6e32584 for details on qmlrebuild replacement strategy
extern "C" int hooked_qRegisterResourceData(
int version,
const unsigned char *tree,
@@ -480,23 +401,21 @@ extern "C" int hooked_qRegisterResourceData(
.entriesAffected = 0,
};
NSLogger(@"[reMarkable] Registering Qt resource version %d tree:%p name:%p data:%p",
NSLogger(@"[RMHook] Registering Qt resource version %d tree:%p name:%p data:%p",
version, tree, name, data);
statArchive(&resource, 0);
// Make a writable copy of the tree (we need to modify offsets)
resource.tree = (uint8_t *)malloc(resource.treeSize);
if (!resource.tree) {
NSLogger(@"[reMarkable] Failed to allocate tree buffer");
NSLogger(@"[RMHook] Failed to allocate tree buffer");
pthread_mutex_unlock(&gResourceMutex);
return original_qRegisterResourceData(version, tree, name, data);
}
memcpy(resource.tree, tree, resource.treeSize);
// Process nodes and mark replacements
processNode(&resource, 0, "");
NSLogger(@"[reMarkable] Processing done! Entries affected: %d, dataSize: %zu, originalDataSize: %zu",
NSLogger(@"[RMHook] Processing done! Entries affected: %d, dataSize: %zu, originalDataSize: %zu",
resource.entriesAffected, resource.dataSize, resource.originalDataSize);
const unsigned char *finalTree = tree;
@@ -504,30 +423,25 @@ extern "C" int hooked_qRegisterResourceData(
uint8_t *newDataBuffer = NULL;
if (resource.entriesAffected > 0) {
NSLogger(@"[reMarkable] Rebuilding data tables... (entries: %d)", resource.entriesAffected);
NSLogger(@"[RMHook] Rebuilding data tables... (entries: %d)", resource.entriesAffected);
// Allocate new data buffer (original size + space for replacements)
newDataBuffer = (uint8_t *)malloc(resource.dataSize);
if (!newDataBuffer) {
NSLogger(@"[reMarkable] Failed to allocate new data buffer (%zu bytes)", resource.dataSize);
NSLogger(@"[RMHook] Failed to allocate new data buffer (%zu bytes)", resource.dataSize);
free(resource.tree);
clearReplacementEntries();
pthread_mutex_unlock(&gResourceMutex);
return original_qRegisterResourceData(version, tree, name, data);
}
// Copy original data
memcpy(newDataBuffer, data, resource.originalDataSize);
// Copy replacement entries to their designated offsets
struct ReplacementEntry *entry = getReplacementEntries();
while (entry) {
// Write size prefix (4 bytes, big-endian)
writeUint32(newDataBuffer, (int)entry->copyToOffset, (uint32_t)entry->size);
// Write data after size prefix
memcpy(newDataBuffer + entry->copyToOffset + 4, entry->data, entry->size);
NSLogger(@"[reMarkable] Copied replacement for node %d at offset %zu (%zu bytes)",
NSLogger(@"[RMHook] Copied replacement for node %d at offset %zu (%zu bytes)",
entry->node, entry->copyToOffset, entry->size);
entry = entry->next;
@@ -536,23 +450,20 @@ extern "C" int hooked_qRegisterResourceData(
finalTree = resource.tree;
finalData = newDataBuffer;
NSLogger(@"[reMarkable] Data buffer rebuilt: original %zu bytes -> new %zu bytes",
NSLogger(@"[RMHook] Data buffer rebuilt: original %zu bytes -> new %zu bytes",
resource.originalDataSize, resource.dataSize);
}
int status = original_qRegisterResourceData(version, finalTree, name, finalData);
// Cleanup
clearReplacementEntries();
if (resource.tree && resource.entriesAffected == 0) {
free(resource.tree);
}
// Note: We intentionally don't free newDataBuffer or resource.tree when entriesAffected > 0
// because Qt will use these buffers for the lifetime of the application
pthread_mutex_unlock(&gResourceMutex);
return status;
}
#endif // BUILD_MODE_QMLREBUILD
@end
@end

4
src/RMHook/SSLConfig.h Normal file
View File

@@ -0,0 +1,4 @@
#include <QtNetwork/QNetworkRequest>
void SSLConfigLoad(void);
void SSLConfigApplyToRequest(QNetworkRequest &request);

92
src/RMHook/SSLConfig.m Normal file
View File

@@ -0,0 +1,92 @@
#import "SSLConfig.h"
#import "Config.h"
#import "Logger.h"
#include <QtNetwork/QSslConfiguration>
#include <QtNetwork/QSslCertificate>
#include <QtNetwork/QSslKey>
#include <QtNetwork/QSslSocket>
#include <QtCore/QFile>
#include <QtCore/QIODevice>
static QSslCertificate gClientCert;
static QSslKey gClientKey;
static QSslCertificate gCACert;
static bool gSSLInitialized = false;
void SSLConfigLoad(void) {
if (gSSLInitialized) return;
if (!gConfiguredClientCertPath.isEmpty() && !gConfiguredClientKeyPath.isEmpty()) {
QString certPath = gConfiguredClientCertPath;
QString keyPath = gConfiguredClientKeyPath;
QFile certFile(QString::fromUtf8(certPath.toUtf8()));
if (certFile.open(QIODevice::ReadOnly)) {
gClientCert = QSslCertificate(certFile.readAll(), QSsl::Pem);
certFile.close();
if (gClientCert.isNull()) {
NSLogger(@"[RMHook] Failed to parse client certificate from %s", certPath.toUtf8().constData());
} else {
NSLogger(@"[RMHook] Loaded client certificate from %s", certPath.toUtf8().constData());
}
} else {
NSLogger(@"[RMHook] Failed to open client certificate file %s", certPath.toUtf8().constData());
}
QFile keyFile(QString::fromUtf8(keyPath.toUtf8()));
if (keyFile.open(QIODevice::ReadOnly)) {
gClientKey = QSslKey(keyFile.readAll(), gConfiguredKeyAlgorithm, QSsl::Pem, QSsl::PrivateKey);
keyFile.close();
if (gClientKey.isNull()) {
NSLogger(@"[RMHook] Failed to parse client key from %s", keyPath.toUtf8().constData());
} else {
NSLogger(@"[RMHook] Loaded client key from %s", keyPath.toUtf8().constData());
}
} else {
NSLogger(@"[RMHook] Failed to open client key file %s", keyPath.toUtf8().constData());
}
}
if (!gConfiguredCACertPath.isEmpty()) {
QString caPath = gConfiguredCACertPath;
QFile caFile(QString::fromUtf8(caPath.toUtf8()));
if (caFile.open(QIODevice::ReadOnly)) {
gCACert = QSslCertificate(caFile.readAll(), QSsl::Pem);
caFile.close();
if (gCACert.isNull()) {
NSLogger(@"[RMHook] Failed to parse CA certificate from %s", caPath.toUtf8().constData());
} else {
NSLogger(@"[RMHook] Loaded CA certificate from %s", caPath.toUtf8().constData());
}
} else {
NSLogger(@"[RMHook] Failed to open CA certificate file %s", caPath.toUtf8().constData());
}
}
gSSLInitialized = true;
}
void SSLConfigApplyToRequest(QNetworkRequest &request) {
if (!gSSLInitialized) return;
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
if (!gClientCert.isNull() && !gClientKey.isNull()) {
QList<QSslCertificate> localCerts = sslConfig.localCertificateChain();
localCerts.append(gClientCert);
sslConfig.setLocalCertificateChain(localCerts);
sslConfig.setPrivateKey(gClientKey);
}
if (!gCACert.isNull()) {
QList<QSslCertificate> caCerts = sslConfig.caCertificates();
caCerts.append(gCACert);
sslConfig.setCaCertificates(caCerts);
}
if (gDisableSSLVerification) {
sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
}
request.setSslConfiguration(sslConfig);
}

View File

@@ -1,23 +0,0 @@
// HTTP Server for RMHook - native macOS implementation
#pragma once
#import <Foundation/Foundation.h>
#ifdef __cplusplus
extern "C" {
#endif
namespace httpserver {
// Start HTTP server on specified port
bool start(uint16_t port = 8080);
// Stop HTTP server
void stop();
// Check if server is running
bool isRunning();
}
#ifdef __cplusplus
}
#endif

View File

@@ -1,305 +0,0 @@
// HTTP Server for RMHook - native macOS implementation using CFSocket
#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "HttpServer.h"
#include "MessageBroker.h"
#include "Logger.h"
static CFSocketRef g_serverSocket = NULL;
static uint16_t g_serverPort = 0;
static bool g_isRunning = false;
// Forward declarations
static void handleClientConnection(int clientSocket);
static void sendResponse(int clientSocket, int statusCode, NSString *body, NSString *contentType);
static void handleExportFileRequest(int clientSocket, NSDictionary *jsonData);
static void handleDocumentAcceptedRequest(int clientSocket, NSDictionary *jsonData);
// Socket callback
static void socketCallback(CFSocketRef socket, CFSocketCallBackType type,
CFDataRef address, const void *data, void *info)
{
if (type == kCFSocketAcceptCallBack) {
CFSocketNativeHandle clientSocket = *(CFSocketNativeHandle *)data;
NSLogger(@"[HttpServer] New connection accepted, socket: %d", clientSocket);
// Handle client in background
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
handleClientConnection(clientSocket);
});
}
}
static void handleClientConnection(int clientSocket)
{
@autoreleasepool {
// Read request
char buffer[4096];
ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
if (bytesRead <= 0) {
close(clientSocket);
return;
}
buffer[bytesRead] = '\0';
NSString *request = [NSString stringWithUTF8String:buffer];
NSLogger(@"[HttpServer] Received request (%ld bytes)", (long)bytesRead);
// Parse request line
NSArray *lines = [request componentsSeparatedByString:@"\r\n"];
if (lines.count == 0) {
sendResponse(clientSocket, 400, @"{\"error\": \"Invalid request\"}", @"application/json");
close(clientSocket);
return;
}
NSArray *requestLine = [lines[0] componentsSeparatedByString:@" "];
if (requestLine.count < 3) {
sendResponse(clientSocket, 400, @"{\"error\": \"Invalid request line\"}", @"application/json");
close(clientSocket);
return;
}
NSString *method = requestLine[0];
NSString *path = requestLine[1];
NSLogger(@"[HttpServer] %@ %@", method, path);
// Find body (after \r\n\r\n)
NSRange bodyRange = [request rangeOfString:@"\r\n\r\n"];
NSString *body = nil;
if (bodyRange.location != NSNotFound) {
body = [request substringFromIndex:bodyRange.location + 4];
}
// Route requests
if ([path isEqualToString:@"/exportFile"] && [method isEqualToString:@"POST"]) {
if (!body || body.length == 0) {
sendResponse(clientSocket, 400, @"{\"error\": \"Missing request body\"}", @"application/json");
close(clientSocket);
return;
}
NSData *jsonData = [body dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error || ![json isKindOfClass:[NSDictionary class]]) {
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Invalid JSON: %@\"}",
error ? error.localizedDescription : @"Not an object"];
sendResponse(clientSocket, 400, errorMsg, @"application/json");
close(clientSocket);
return;
}
handleExportFileRequest(clientSocket, (NSDictionary *)json);
} else if ([path isEqualToString:@"/documentAccepted"] && [method isEqualToString:@"POST"]) {
if (!body || body.length == 0) {
sendResponse(clientSocket, 400, @"{\"error\": \"Missing request body\"}", @"application/json");
close(clientSocket);
return;
}
NSData *jsonData = [body dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error || ![json isKindOfClass:[NSDictionary class]]) {
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Invalid JSON: %@\"}",
error ? error.localizedDescription : @"Not an object"];
sendResponse(clientSocket, 400, errorMsg, @"application/json");
close(clientSocket);
return;
}
handleDocumentAcceptedRequest(clientSocket, (NSDictionary *)json);
} else if ([path isEqualToString:@"/health"] || [path isEqualToString:@"/"]) {
sendResponse(clientSocket, 200, @"{\"status\": \"ok\", \"service\": \"RMHook HTTP Server\"}", @"application/json");
} else {
sendResponse(clientSocket, 404, @"{\"error\": \"Endpoint not found\"}", @"application/json");
}
close(clientSocket);
}
}
static void handleExportFileRequest(int clientSocket, NSDictionary *jsonData)
{
NSLogger(@"[HttpServer] Processing /exportFile request");
// Convert to JSON string for MessageBroker
NSError *error = nil;
NSData *jsonDataEncoded = [NSJSONSerialization dataWithJSONObject:jsonData
options:0
error:&error];
if (error) {
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Failed to encode JSON: %@\"}",
error.localizedDescription];
sendResponse(clientSocket, 500, errorMsg, @"application/json");
return;
}
NSString *jsonStr = [[NSString alloc] initWithData:jsonDataEncoded encoding:NSUTF8StringEncoding];
NSLogger(@"[HttpServer] Broadcasting exportFile signal with data: %@", jsonStr);
// Broadcast to MessageBroker
messagebroker::broadcast("exportFile", [jsonStr UTF8String]);
// Send success response
sendResponse(clientSocket, 200,
@"{\"status\": \"success\", \"message\": \"Export request sent to application\"}",
@"application/json");
}
static void handleDocumentAcceptedRequest(int clientSocket, NSDictionary *jsonData)
{
NSLogger(@"[HttpServer] Processing /documentAccepted request");
// Convert to JSON string for MessageBroker
NSError *error = nil;
NSData *jsonDataEncoded = [NSJSONSerialization dataWithJSONObject:jsonData
options:0
error:&error];
if (error) {
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Failed to encode JSON: %@\"}",
error.localizedDescription];
sendResponse(clientSocket, 500, errorMsg, @"application/json");
return;
}
NSString *jsonStr = [[NSString alloc] initWithData:jsonDataEncoded encoding:NSUTF8StringEncoding];
NSLogger(@"[HttpServer] Broadcasting documentAccepted signal with data: %@", jsonStr);
// Broadcast to MessageBroker
messagebroker::broadcast("documentAccepted", [jsonStr UTF8String]);
// Send success response
sendResponse(clientSocket, 200,
@"{\"status\": \"success\", \"message\": \"Document accepted request sent to application\"}",
@"application/json");
}
static void sendResponse(int clientSocket, int statusCode, NSString *body, NSString *contentType)
{
NSString *statusText;
switch (statusCode) {
case 200: statusText = @"OK"; break;
case 400: statusText = @"Bad Request"; break;
case 404: statusText = @"Not Found"; break;
case 500: statusText = @"Internal Server Error"; break;
default: statusText = @"Unknown"; break;
}
NSData *bodyData = [body dataUsingEncoding:NSUTF8StringEncoding];
NSString *response = [NSString stringWithFormat:
@"HTTP/1.1 %d %@\r\n"
@"Content-Type: %@; charset=utf-8\r\n"
@"Content-Length: %lu\r\n"
@"Access-Control-Allow-Origin: *\r\n"
@"Connection: close\r\n"
@"\r\n"
@"%@",
statusCode, statusText, contentType, (unsigned long)bodyData.length, body
];
NSData *responseData = [response dataUsingEncoding:NSUTF8StringEncoding];
send(clientSocket, responseData.bytes, responseData.length, 0);
}
namespace httpserver {
bool start(uint16_t port)
{
if (g_isRunning) {
NSLogger(@"[HttpServer] Server already running on port %d", g_serverPort);
return true;
}
// Create socket
CFSocketContext context = {0, NULL, NULL, NULL, NULL};
g_serverSocket = CFSocketCreate(kCFAllocatorDefault,
PF_INET,
SOCK_STREAM,
IPPROTO_TCP,
kCFSocketAcceptCallBack,
socketCallback,
&context);
if (!g_serverSocket) {
NSLogger(@"[HttpServer] Failed to create socket");
return false;
}
// Set socket options
int yes = 1;
setsockopt(CFSocketGetNative(g_serverSocket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
// Bind to address
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_len = sizeof(addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // localhost only
CFDataRef addressData = CFDataCreate(kCFAllocatorDefault,
(const UInt8 *)&addr,
sizeof(addr));
CFSocketError error = CFSocketSetAddress(g_serverSocket, addressData);
CFRelease(addressData);
if (error != kCFSocketSuccess) {
NSLogger(@"[HttpServer] Failed to bind to port %d (error: %ld)", port, (long)error);
CFRelease(g_serverSocket);
g_serverSocket = NULL;
return false;
}
// Add to run loop
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, g_serverSocket, 0);
CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopCommonModes);
CFRelease(source);
g_serverPort = port;
g_isRunning = true;
NSLogger(@"[HttpServer] HTTP server started successfully on http://localhost:%d", port);
return true;
}
void stop()
{
if (!g_isRunning) {
return;
}
if (g_serverSocket) {
CFSocketInvalidate(g_serverSocket);
CFRelease(g_serverSocket);
g_serverSocket = NULL;
}
g_isRunning = false;
NSLogger(@"[HttpServer] HTTP server stopped");
}
bool isRunning()
{
return g_isRunning;
}
} // namespace httpserver

View File

@@ -348,8 +348,6 @@ void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char
static const char *kFilesToReplace[] = {
"/qml/client/dialogs/ExportDialog.qml",
"/qml/client/settings/GeneralSettings.qml",
"/qml/client/dialogs/ExportUtils.js",
"/qml/client/desktop/FileImportDialog.qml",
NULL // Sentinel to mark end of list
};

View File

@@ -1,17 +0,0 @@
// Example usage of MessageBroker from C++/Objective-C
#include <QObject>
#include <QProcess>
#include <QString>
#include <QQmlApplicationEngine>
#include "MessageBroker.h"
// Example: Register MessageBroker QML type (called from dylib init)
void initMessageBroker() {
messagebroker::registerQmlType();
}
// Example: Send a signal from C++ to QML
void sendSignal() {
messagebroker::broadcast("demoSignal", "Hello from C!");
}

View File

@@ -1,16 +0,0 @@
import net.noham.MessageBroker
MessageBroker {
id: demoBroker
listeningFor: ["demoSignal"]
onSignalReceived: (signal, message) => {
console.log("Got message", signal, message);
}
}
MouseArea {
onClicked: () => {
demoBroker.sendSignal("mySignalName", "Hello from QML!");
}
}