mirror of
https://github.com/NohamR/RMHook.git
synced 2026-08-27 18:29:42 +00:00
Compare commits
1 Commits
feat/mtls-
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
400e698765 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,5 +2,3 @@ build/
|
||||
.DS_Store
|
||||
/.vscode
|
||||
/research
|
||||
/aqt_venv
|
||||
docs/rmfakecloud_hooking.md
|
||||
|
||||
@@ -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/RMHook
|
||||
${PROJECT_ROOT_DIR}/src/reMarkable
|
||||
${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.3/macos")
|
||||
set(_qt_candidate_roots "$ENV{HOME}/Qt/6.10.0")
|
||||
|
||||
foreach(_qt_root ${_qt_candidate_roots})
|
||||
if(_qt_root AND EXISTS "${_qt_root}")
|
||||
@@ -78,29 +78,27 @@ set(COMMON_SOURCES
|
||||
${PROJECT_ROOT_DIR}/src/utils/ResourceUtils.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
|
||||
# reMarkable dylib
|
||||
set(REMARKABLE_SOURCES
|
||||
${PROJECT_ROOT_DIR}/src/reMarkable/reMarkable.m
|
||||
${PROJECT_ROOT_DIR}/src/reMarkable/DevHooks.m
|
||||
)
|
||||
|
||||
add_library(RMHook SHARED
|
||||
add_library(reMarkable SHARED
|
||||
${COMMON_SOURCES}
|
||||
${RMHOOK_SOURCES}
|
||||
${REMARKABLE_SOURCES}
|
||||
)
|
||||
|
||||
# Set source files as Objective-C++
|
||||
set_source_files_properties(
|
||||
${RMHOOK_SOURCES}
|
||||
${REMARKABLE_SOURCES}
|
||||
PROPERTIES LANGUAGE OBJCXX
|
||||
)
|
||||
|
||||
set_target_properties(RMHook PROPERTIES
|
||||
set_target_properties(reMarkable PROPERTIES
|
||||
PREFIX ""
|
||||
SUFFIX ".dylib"
|
||||
OUTPUT_NAME "RMHook"
|
||||
OUTPUT_NAME "reMarkable"
|
||||
LIBRARY_OUTPUT_DIRECTORY "${PROJECT_ROOT_DIR}/build/dylibs"
|
||||
MACOSX_RPATH ON
|
||||
)
|
||||
@@ -109,29 +107,40 @@ add_definitions(-DQT_NO_VERSION_TAGGING)
|
||||
|
||||
# Add build mode compile definitions and conditionally add sources
|
||||
if(BUILD_MODE_RMFAKECLOUD)
|
||||
target_compile_definitions(RMHook PRIVATE BUILD_MODE_RMFAKECLOUD=1)
|
||||
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_RMFAKECLOUD=1)
|
||||
message(STATUS "Build mode: rmfakecloud (cloud redirection)")
|
||||
endif()
|
||||
|
||||
if(BUILD_MODE_QMLREBUILD)
|
||||
target_compile_definitions(RMHook PRIVATE BUILD_MODE_QMLREBUILD=1)
|
||||
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLREBUILD=1)
|
||||
|
||||
# Enable Qt MOC for MessageBroker
|
||||
set_target_properties(RMHook PROPERTIES AUTOMOC ON)
|
||||
# Enable Qt MOC for MessageBroker (HttpServer is pure Obj-C/Foundation, no MOC needed)
|
||||
set_target_properties(reMarkable PROPERTIES AUTOMOC ON)
|
||||
|
||||
# Add MessageBroker source (needs MOC processing)
|
||||
target_sources(RMHook PRIVATE
|
||||
# Add MessageBroker (needs MOC) and HttpServer (native macOS)
|
||||
target_sources(reMarkable 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(RMHook PRIVATE BUILD_MODE_DEV=1)
|
||||
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_DEV=1)
|
||||
message(STATUS "Build mode: dev (reverse engineering)")
|
||||
endif()
|
||||
|
||||
target_link_libraries(RMHook PRIVATE
|
||||
target_link_libraries(reMarkable PRIVATE
|
||||
${LIBS}
|
||||
${QT_LIB_TARGETS}
|
||||
)
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Rivoirard Noham
|
||||
Copyright (c) 2025 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
186
README.md
@@ -6,47 +6,32 @@ 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.28.1 (released 2026-08-24)
|
||||
- reMarkable Desktop v3.24.0 (released 2025-12-03)
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/latest.png" width="45%" />
|
||||
<img src="docs/rm.png" width="45%" />
|
||||
<img src="docs/latest.png" width="40%" />
|
||||
<img src="docs/rm.png" width="50%" />
|
||||
</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.
|
||||
|
||||
### 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
|
||||
### 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
|
||||
@@ -60,9 +45,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:**
|
||||
@@ -73,7 +58,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
|
||||
@@ -88,7 +73,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:
|
||||

|
||||
|
||||
@@ -105,112 +90,25 @@ 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
|
||||
|
||||
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 |
|
||||
| `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) |
|
||||
| Key | Type | Default | Description |
|
||||
|--------|---------|-------------------|--------------------------------|
|
||||
| `host` | String | `example.com` | Your rmfakecloud server host |
|
||||
| `port` | Number | `443` | Your rmfakecloud server port |
|
||||
|
||||
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
|
||||
@@ -243,3 +141,41 @@ 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
|
||||
```
|
||||
54
docs/DocumentAccepted_MessageBroker_snippet.qml
Normal file
54
docs/DocumentAccepted_MessageBroker_snippet.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
67
docs/ExportDialog_MessageBroker_snippet.qml
Normal file
67
docs/ExportDialog_MessageBroker_snippet.qml
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
293
docs/HTTP_SERVER.md
Normal file
293
docs/HTTP_SERVER.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# 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
|
||||
BIN
docs/latest.png
BIN
docs/latest.png
Binary file not shown.
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 228 KiB |
BIN
docs/rm.png
BIN
docs/rm.png
Binary file not shown.
|
Before Width: | Height: | Size: 467 KiB After Width: | Height: | Size: 465 KiB |
@@ -1,28 +0,0 @@
|
||||
#!/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"
|
||||
@@ -29,7 +29,7 @@ case "$BUILD_MODE" in
|
||||
DYLIB_NAME="all.dylib"
|
||||
;;
|
||||
*)
|
||||
DYLIB_NAME="RMHook.dylib"
|
||||
DYLIB_NAME="reMarkable.dylib"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -55,7 +55,7 @@ case "$BUILD_MODE" in
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "🔨 Compiling RMHook.dylib (mode: $BUILD_MODE)..."
|
||||
echo "🔨 Compiling reMarkable.dylib (mode: $BUILD_MODE)..."
|
||||
echo "📦 Qt path: $QT_PATH"
|
||||
|
||||
# Create build directories if necessary
|
||||
@@ -70,12 +70,12 @@ else
|
||||
cmake $CMAKE_OPTIONS ..
|
||||
fi
|
||||
|
||||
make RMHook
|
||||
make reMarkable
|
||||
|
||||
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/RMHook.dylib"
|
||||
DEFAULT_DYLIB="$DYLIB_DIR/reMarkable.dylib"
|
||||
TARGET_DYLIB="$DYLIB_DIR/$DYLIB_NAME"
|
||||
|
||||
if [ -f "$DEFAULT_DYLIB" ]; then
|
||||
|
||||
200
scripts/http_server.py
Normal file
200
scripts/http_server.py
Normal file
@@ -0,0 +1,200 @@
|
||||
#!/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()
|
||||
165
scripts/test_http_server.py
Normal file
165
scripts/test_http_server.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/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()
|
||||
@@ -1,16 +0,0 @@
|
||||
#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);
|
||||
@@ -1,152 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
#include <QtNetwork/QNetworkRequest>
|
||||
|
||||
void SSLConfigLoad(void);
|
||||
void SSLConfigApplyToRequest(QNetworkRequest &request);
|
||||
@@ -1,92 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
@@ -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(@"[RMHook] %s: (null)", label);
|
||||
NSLogger(@"[reMarkable] %s: (null)", label);
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned char *ptr = (unsigned char *)address;
|
||||
NSMutableString *hexLine = [NSMutableString stringWithFormat:@"[RMHook] %s: ", label];
|
||||
NSMutableString *hexLine = [NSMutableString stringWithFormat:@"[reMarkable] %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:@"[RMHook] "];
|
||||
hexLine = [NSMutableString stringWithString:@"[reMarkable] "];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,13 +57,13 @@ void logMemory(const char *label, void *address, size_t length) {
|
||||
}
|
||||
|
||||
void logStackTrace(const char *label) {
|
||||
NSLogger(@"[RMHook] %s - Stack trace:", label);
|
||||
NSLogger(@"[reMarkable] %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(@"[RMHook] #%lu: %@", (unsigned long)i, frame);
|
||||
NSLogger(@"[reMarkable] #%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(@"[RMHook] QIODevice::write called with maxSize: %lld", (long long)maxSize);
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] QIODevice::write result: %zd", result);
|
||||
NSLogger(@"[reMarkable] QIODevice::write result: %zd", result);
|
||||
return result;
|
||||
}
|
||||
NSLogger(@"[RMHook] WARNING: Original QIODevice::write not available, returning 0");
|
||||
NSLogger(@"[reMarkable] WARNING: Original QIODevice::write not available, returning 0");
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int64_t hooked_function_at_0x100011790(uint64_t *a1) {
|
||||
NSLogger(@"[RMHook] Hook at 0x100011790 called!");
|
||||
NSLogger(@"[RMHook] a1 = %p", a1);
|
||||
NSLogger(@"[reMarkable] Hook at 0x100011790 called!");
|
||||
NSLogger(@"[reMarkable] a1 = %p", a1);
|
||||
|
||||
if (a1) {
|
||||
NSLogger(@"[RMHook] *a1 = 0x%llx", (unsigned long long)*a1);
|
||||
NSLogger(@"[reMarkable] *a1 = 0x%llx", (unsigned long long)*a1);
|
||||
logMemory("Memory at a1", (void *)a1, 64);
|
||||
logMemory("Memory at *a1", (void *)(*a1), 64);
|
||||
} else {
|
||||
NSLogger(@"[RMHook] a1 is NULL");
|
||||
NSLogger(@"[reMarkable] a1 is NULL");
|
||||
}
|
||||
|
||||
if (original_function_at_0x100011790) {
|
||||
int64_t result = original_function_at_0x100011790(a1);
|
||||
NSLogger(@"[RMHook] result = 0x%llx", (unsigned long long)result);
|
||||
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
NSLogger(@"[RMHook] WARNING: Original function at 0x100011790 not available, returning 0");
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] ========================================");
|
||||
NSLogger(@"[RMHook] Hook at 0x100011CE0 (QML Type Registration)");
|
||||
NSLogger(@"[RMHook] ========================================");
|
||||
NSLogger(@"[reMarkable] ========================================");
|
||||
NSLogger(@"[reMarkable] Hook at 0x100011CE0 (QML Type Registration)");
|
||||
NSLogger(@"[reMarkable] ========================================");
|
||||
|
||||
NSLogger(@"[RMHook] a1 (typeMetadata?) = 0x%llx", (unsigned long long)a1);
|
||||
NSLogger(@"[reMarkable] 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(@"[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);
|
||||
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);
|
||||
|
||||
if (a1) {
|
||||
logMemory("Memory at a1 (typeMetadata)", (void *)a1, 64);
|
||||
void **vtable = (void **)a1;
|
||||
NSLogger(@"[RMHook] a1 vtable/first ptr = %p", *vtable);
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] a4 as string: \"%.*s\"", len, maybeStr);
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] result (qmlregister return) = %u (0x%x)", (unsigned int)result, (unsigned int)result);
|
||||
NSLogger(@"[RMHook] ========================================");
|
||||
NSLogger(@"[reMarkable] result (qmlregister return) = %u (0x%x)", (unsigned int)result, (unsigned int)result);
|
||||
NSLogger(@"[reMarkable] ========================================");
|
||||
return result;
|
||||
}
|
||||
|
||||
NSLogger(@"[RMHook] WARNING: Original function at 0x100011CE0 not available, returning 0");
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] Hook at 0x10015A130 called!");
|
||||
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
|
||||
NSLogger(@"[RMHook] a2 = 0x%llx", (unsigned long long)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);
|
||||
|
||||
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(@"[RMHook] result = 0x%llx", (unsigned long long)result);
|
||||
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
NSLogger(@"[RMHook] WARNING: Original function at 0x10015A130 not available, returning 0");
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] Hook at 0x10015BC90 called!");
|
||||
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
|
||||
NSLogger(@"[RMHook] a2 = 0x%llx", (unsigned long long)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);
|
||||
|
||||
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(@"[RMHook] original function returned (void)");
|
||||
NSLogger(@"[reMarkable] original function returned (void)");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLogger(@"[RMHook] WARNING: Original function at 0x10015BC90 not available");
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] Hook at 0x10016D520 called!");
|
||||
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
|
||||
NSLogger(@"[RMHook] a2 = %p", a2);
|
||||
NSLogger(@"[reMarkable] Hook at 0x10016D520 called!");
|
||||
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
|
||||
NSLogger(@"[reMarkable] a2 = %p", a2);
|
||||
if (a2) {
|
||||
NSLogger(@"[RMHook] *a2 = 0x%llx", (unsigned long long)*a2);
|
||||
NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2);
|
||||
}
|
||||
NSLogger(@"[RMHook] a3 = %u (0x%x)", a3, a3);
|
||||
NSLogger(@"[RMHook] a4 = 0x%llx", (unsigned long long)a4);
|
||||
NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3);
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] result = 0x%llx", (unsigned long long)result);
|
||||
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
NSLogger(@"[RMHook] WARNING: Original function not available, returning 0");
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] Hook at 0x1001B6EE0 called!");
|
||||
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
|
||||
NSLogger(@"[reMarkable] Hook at 0x1001B6EE0 called!");
|
||||
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
|
||||
|
||||
// At a1 (PdfExporter object):
|
||||
// +0x10 contains a QString (likely document name)
|
||||
NSLogger(@"[RMHook] Reading QString at a1+0x10:");
|
||||
NSLogger(@"[reMarkable] Reading QString at a1+0x10:");
|
||||
logMemory("a1 + 0x10 (raw)", (void *)(a1 + 0x10), 64);
|
||||
|
||||
void **qstrPtr = (void **)(a1 + 0x10);
|
||||
void *dataPtr = *qstrPtr;
|
||||
|
||||
if (!dataPtr) {
|
||||
NSLogger(@"[RMHook] QString has null data pointer");
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] QString plausible size=%d (found at -%d)", size, delta);
|
||||
NSLogger(@"[reMarkable] QString plausible size=%d (found at -%d)", size, delta);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (size > 0) {
|
||||
NSString *qstringValue = [[NSString alloc] initWithCharacters:(unichar *)dataPtr length:size];
|
||||
NSLogger(@"[RMHook] QString value: \"%@\"", qstringValue);
|
||||
NSLogger(@"[reMarkable] QString value: \"%@\"", qstringValue);
|
||||
} else {
|
||||
NSLogger(@"[RMHook] QString: could not find valid size");
|
||||
NSLogger(@"[reMarkable] QString: could not find valid size");
|
||||
}
|
||||
|
||||
NSLogger(@"[RMHook] a2 = %p", a2);
|
||||
NSLogger(@"[reMarkable] a2 = %p", a2);
|
||||
if (a2) {
|
||||
NSLogger(@"[RMHook] *a2 = 0x%llx", (unsigned long long)*a2);
|
||||
NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2);
|
||||
}
|
||||
NSLogger(@"[RMHook] a3 = %u (0x%x)", a3, a3);
|
||||
NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3);
|
||||
|
||||
if (original_function_at_0x1001B6EE0) {
|
||||
original_function_at_0x1001B6EE0(a1, a2, a3);
|
||||
NSLogger(@"[RMHook] Original function at 0x1001B6EE0 executed");
|
||||
NSLogger(@"[reMarkable] Original function at 0x1001B6EE0 executed");
|
||||
} else {
|
||||
NSLogger(@"[RMHook] WARNING: Original function not available");
|
||||
NSLogger(@"[reMarkable] WARNING: Original function not available");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,19 +306,19 @@ extern "C" int64_t hooked_qmlregister(
|
||||
int a9,
|
||||
int64_t 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);
|
||||
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);
|
||||
|
||||
// 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(@"[RMHook] typeName (a4) = \"%.*s\"", len, typeName);
|
||||
NSLogger(@"[reMarkable] typeName (a4) = \"%.*s\"", len, typeName);
|
||||
|
||||
if (len == 15 && strncmp(typeName, "PlatformHelpers", 15) == 0) {
|
||||
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);
|
||||
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);
|
||||
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(@"[RMHook] a2 as string: \"%.*s\"", len, maybeStr);
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] result = 0x%llx (%lld)", (unsigned long long)result, (long long)result);
|
||||
NSLogger(@"[reMarkable] result = 0x%llx (%lld)", (unsigned long long)result, (long long)result);
|
||||
} else {
|
||||
NSLogger(@"[RMHook] WARNING: Original qmlregister not available!");
|
||||
NSLogger(@"[reMarkable] WARNING: Original qmlregister not available!");
|
||||
}
|
||||
|
||||
NSLogger(@"[RMHook] ========================================");
|
||||
NSLogger(@"[reMarkable] ========================================");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface RMHook : NSObject
|
||||
@interface reMarkable : NSObject
|
||||
|
||||
@end
|
||||
@@ -1,16 +1,15 @@
|
||||
#import "RMHook.h"
|
||||
#import "reMarkable.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>
|
||||
@@ -20,7 +19,6 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <string>
|
||||
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
#include <QtNetwork/QNetworkRequest>
|
||||
@@ -31,9 +29,113 @@
|
||||
#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;
|
||||
@@ -105,22 +207,24 @@ static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
@interface RMHookDylib : NSObject
|
||||
@interface reMarkableDylib : NSObject
|
||||
|
||||
- (BOOL)hook;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RMHook
|
||||
@implementation reMarkable
|
||||
|
||||
+ (void)load {
|
||||
NSLogger(@"RMHook dylib loaded successfully");
|
||||
NSLogger(@"reMarkable dylib loaded successfully");
|
||||
|
||||
RMHookDylib *dylib = [[RMHookDylib alloc] init];
|
||||
// Initialize the hook
|
||||
reMarkableDylib *dylib = [[reMarkableDylib alloc] init];
|
||||
[dylib hook];
|
||||
|
||||
#ifdef BUILD_MODE_RMFAKECLOUD
|
||||
NSString *configPath = ConfigFilePath();
|
||||
// Add custom Help menu entry to open config file
|
||||
NSString *configPath = ReMarkableConfigFilePath();
|
||||
NSString *fileURL = [NSString stringWithFormat:@"file://%@", configPath];
|
||||
[MenuActionController addCustomHelpMenuEntry:@"Open rmfakecloud config"
|
||||
withURL:fileURL
|
||||
@@ -130,7 +234,7 @@ static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
@implementation RMHookDylib
|
||||
@implementation reMarkableDylib
|
||||
|
||||
#ifdef BUILD_MODE_RMFAKECLOUD
|
||||
static QNetworkReply *(*original_qNetworkAccessManager_createRequest)(
|
||||
@@ -142,17 +246,6 @@ 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
|
||||
@@ -163,6 +256,8 @@ static int (*original_qRegisterResourceData)(
|
||||
const unsigned char *) = NULL;
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
#ifdef BUILD_MODE_RMFAKECLOUD
|
||||
static inline bool shouldPatchURL(const QString &host) {
|
||||
if (host.isEmpty()) {
|
||||
@@ -188,63 +283,115 @@ 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(@"[RMHook] Starting hooks...");
|
||||
NSLogger(@"[reMarkable] Starting hooks...");
|
||||
|
||||
#ifdef BUILD_MODE_RMFAKECLOUD
|
||||
NSLogger(@"[RMHook] Build mode: rmfakecloud");
|
||||
ConfigLoadOrCreate();
|
||||
SSLConfigLoad();
|
||||
NSLogger(@"[RMHook] Using override host %@ and port %@", gConfiguredHostObjC, gConfiguredPortObjC);
|
||||
NSLogger(@"[reMarkable] Build mode: rmfakecloud");
|
||||
ReMarkableLoadOrCreateConfig();
|
||||
NSLogger(@"[reMarkable] Using override host %@ and port %@", gConfiguredHost, gConfiguredPort);
|
||||
|
||||
[MemoryUtils hookSymbol:@"QtNetwork"
|
||||
symbolName:@"__ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice"
|
||||
hookFunction:(void *)hooked_qNetworkAccessManager_createRequest
|
||||
originalFunction:(void **)&original_qNetworkAccessManager_createRequest
|
||||
logPrefix:@"[RMHook]"];
|
||||
logPrefix:@"[reMarkable]"];
|
||||
|
||||
[MemoryUtils hookSymbol:@"QtWebSockets"
|
||||
symbolName:@"__ZN10QWebSocket4openERK15QNetworkRequest"
|
||||
hookFunction:(void *)hooked_qWebSocket_open
|
||||
originalFunction:(void **)&original_qWebSocket_open
|
||||
logPrefix:@"[RMHook]"];
|
||||
|
||||
[MemoryUtils hookSymbol:@"libpaho-mqtt3as.1.dylib"
|
||||
symbolName:@"_MQTTAsync_createWithOptions"
|
||||
hookFunction:(void *)hooked_MQTTAsync_createWithOptions
|
||||
originalFunction:(void **)&original_MQTTAsync_createWithOptions
|
||||
logPrefix:@"[RMHook]"];
|
||||
logPrefix:@"[reMarkable]"];
|
||||
#endif
|
||||
|
||||
#ifdef BUILD_MODE_QMLREBUILD
|
||||
NSLogger(@"[RMHook] Build mode: qmlrebuild");
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] Native callback received signal '%s' with value '%s'", signal, value);
|
||||
NSLogger(@"[reMarkable] 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:@"[RMHook]"];
|
||||
logPrefix:@"[reMarkable]"];
|
||||
|
||||
// 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(@"[RMHook] Build mode: dev/reverse engineering");
|
||||
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]"];
|
||||
|
||||
#endif
|
||||
|
||||
return YES;
|
||||
@@ -259,15 +406,14 @@ 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(gConfiguredHostObjC);
|
||||
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost);
|
||||
newUrl.setHost(overrideHost);
|
||||
newUrl.setPort([gConfiguredPortObjC intValue]);
|
||||
newUrl.setPort([gConfiguredPort intValue]);
|
||||
newReq.setUrl(newUrl);
|
||||
|
||||
SSLConfigApplyToRequest(newReq);
|
||||
|
||||
if (original_qNetworkAccessManager_createRequest) {
|
||||
return original_qNetworkAccessManager_createRequest(self, op, newReq, outgoingData);
|
||||
}
|
||||
@@ -291,92 +437,25 @@ extern "C" void hooked_qWebSocket_open(
|
||||
const QString host = req.url().host();
|
||||
if (shouldPatchURL(host)) {
|
||||
QUrl newUrl = req.url();
|
||||
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHostObjC);
|
||||
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost);
|
||||
newUrl.setHost(overrideHost);
|
||||
newUrl.setPort([gConfiguredPortObjC intValue]);
|
||||
newUrl.setPort([gConfiguredPort 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,
|
||||
@@ -401,21 +480,23 @@ extern "C" int hooked_qRegisterResourceData(
|
||||
.entriesAffected = 0,
|
||||
};
|
||||
|
||||
NSLogger(@"[RMHook] Registering Qt resource version %d tree:%p name:%p data:%p",
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] Failed to allocate tree buffer");
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] Processing done! Entries affected: %d, dataSize: %zu, originalDataSize: %zu",
|
||||
NSLogger(@"[reMarkable] Processing done! Entries affected: %d, dataSize: %zu, originalDataSize: %zu",
|
||||
resource.entriesAffected, resource.dataSize, resource.originalDataSize);
|
||||
|
||||
const unsigned char *finalTree = tree;
|
||||
@@ -423,25 +504,30 @@ extern "C" int hooked_qRegisterResourceData(
|
||||
uint8_t *newDataBuffer = NULL;
|
||||
|
||||
if (resource.entriesAffected > 0) {
|
||||
NSLogger(@"[RMHook] Rebuilding data tables... (entries: %d)", resource.entriesAffected);
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] Failed to allocate new data buffer (%zu bytes)", resource.dataSize);
|
||||
NSLogger(@"[reMarkable] 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(@"[RMHook] Copied replacement for node %d at offset %zu (%zu bytes)",
|
||||
NSLogger(@"[reMarkable] Copied replacement for node %d at offset %zu (%zu bytes)",
|
||||
entry->node, entry->copyToOffset, entry->size);
|
||||
|
||||
entry = entry->next;
|
||||
@@ -450,16 +536,19 @@ extern "C" int hooked_qRegisterResourceData(
|
||||
finalTree = resource.tree;
|
||||
finalData = newDataBuffer;
|
||||
|
||||
NSLogger(@"[RMHook] Data buffer rebuilt: original %zu bytes -> new %zu bytes",
|
||||
NSLogger(@"[reMarkable] 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;
|
||||
23
src/utils/HttpServer.h
Normal file
23
src/utils/HttpServer.h
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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
|
||||
305
src/utils/HttpServer.mm
Normal file
305
src/utils/HttpServer.mm
Normal file
@@ -0,0 +1,305 @@
|
||||
// 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
|
||||
@@ -348,6 +348,8 @@ 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
|
||||
};
|
||||
|
||||
|
||||
17
src/utils/mb.m
Normal file
17
src/utils/mb.m
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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!");
|
||||
}
|
||||
16
src/utils/mb.qml
Normal file
16
src/utils/mb.qml
Normal file
@@ -0,0 +1,16 @@
|
||||
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!");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user