Add native HTTP server, CLI scripts and docs

Implement a native macOS HTTP server for RMHook and wire it into the app. Adds HttpServer.h/.mm (CFSocket-based) with start/stop/isRunning APIs, broadcasts incoming POST /exportFile and /documentAccepted requests to the QML MessageBroker, and starts the server on port 8080 from reMarkable.m. Update CMakeLists to include HttpServer.mm and link Qt Qml when building qmlrebuild mode. Add documentation (docs/HTTP_SERVER.md) and QML snippets for MessageBroker integration, plus Python helper scripts (scripts/http_server.py and scripts/test_http_server.py) for invoking the endpoints. Also add small MessageBroker examples (src/utils/mb.m, src/utils/mb.qml) and update ResourceUtils to include additional QML resources.
This commit is contained in:
√(noham)²
2026-02-02 19:52:58 +01:00
parent 3e89d8118e
commit 400e698765
12 changed files with 1163 additions and 2 deletions

View File

@@ -9,6 +9,7 @@
#endif
#ifdef BUILD_MODE_QMLREBUILD
#import "MessageBroker.h"
#import "HttpServer.h"
#endif
#import <objc/runtime.h>
#import <Cocoa/Cocoa.h>
@@ -314,6 +315,13 @@ static inline bool shouldPatchURL(const QString &host) {
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

23
src/utils/HttpServer.h Normal file
View 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
View 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

View File

@@ -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
View 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
View 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!");
}
}