diff --git a/.gitignore b/.gitignore index db4709ed..69e162bf 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ obj/ xcuserdata/ *.suo .idea/ -.vscode +.vscode/ *.iml test/dist/ +extension/native-messaging-host/build/ diff --git a/extension/native-messaging-host/.clang-format b/extension/native-messaging-host/.clang-format new file mode 100644 index 00000000..be1ea221 --- /dev/null +++ b/extension/native-messaging-host/.clang-format @@ -0,0 +1,14 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +ColumnLimit: 100 +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^' + Priority: 2 + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +SortIncludes: true \ No newline at end of file diff --git a/extension/native-messaging-host/CMakeLists.txt b/extension/native-messaging-host/CMakeLists.txt new file mode 100644 index 00000000..a9af8126 --- /dev/null +++ b/extension/native-messaging-host/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.7) + +project(keeweb-native-messaging-host) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +include(FetchContent) + +FetchContent_Declare( + libuv + GIT_REPOSITORY https://github.com/libuv/libuv.git + GIT_TAG v1.41.0 +) + +FetchContent_MakeAvailable(libuv) + +set(OUTPUT_NAME ${PROJECT_NAME}) +set(SOURCES src/main.cpp) + +add_executable(${PROJECT_NAME} ${SOURCES}) + +target_link_libraries(${PROJECT_NAME} PRIVATE uv_a) +target_include_directories(${PROJECT_NAME} PRIVATE ${libuv_SOURCE_DIR}/include) +target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror) + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=address,undefined) + target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=address,undefined) +endif() diff --git a/extension/native-messaging-host/Makefile b/extension/native-messaging-host/Makefile new file mode 100644 index 00000000..b32ffeb1 --- /dev/null +++ b/extension/native-messaging-host/Makefile @@ -0,0 +1,13 @@ +all: + cmake -B build . + cmake --build build --config MinSizeRel + +debug: + cmake -B build . + cmake --build build --config Debug + +format: + clang-format -i src/*.cpp + +run: + echo -n 020000007b7d | xxd -r -p | build/keeweb-native-messaging-host chrome-extension://enjifmdnhaddmajefhfaoglcfdobkcpj diff --git a/extension/native-messaging-host/src/main.cpp b/extension/native-messaging-host/src/main.cpp new file mode 100644 index 00000000..e898f532 --- /dev/null +++ b/extension/native-messaging-host/src/main.cpp @@ -0,0 +1,208 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +// https://developer.chrome.com/docs/apps/nativeMessaging/#native-messaging-host-protocol +// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#app_side + +struct State { + uv_stream_t *tty_in = nullptr; + uv_stream_t *tty_out = nullptr; + uv_stream_t *keeweb_pipe = nullptr; + std::queue pending_to_keeweb{}; + std::queue pending_to_stdout{}; + bool write_to_keeweb_in_progress = false; + bool write_to_stdout_in_progress = false; +}; + +State state{}; + +constexpr std::array allowed_origins = { + std::string_view("chrome-extension://enjifmdnhaddmajefhfaoglcfdobkcpj")}; + +void process_keeweb_queue(); +void process_stdout_queue(); +void close_keeweb_pipe(); + +bool check_args(int argc, char *argv[]) { + if (argc < 2) { + std::cerr << "Expected origin"; + return false; + } + + std::string origin = argv[1]; + auto found = std::find(allowed_origins.begin(), allowed_origins.end(), origin); + if (found == allowed_origins.end()) { + std::cerr << "Invalid origin"; + return false; + } + + return true; +} + +void alloc_buf(uv_handle_t *, size_t size, uv_buf_t *buf) { + buf->base = new char[size]; + buf->len = size; +} + +void quit_after_stdio_error() { + if (state.keeweb_pipe) { + close_keeweb_pipe(); + } else { + uv_read_stop(state.tty_in); + uv_loop_close(uv_default_loop()); + } +} + +void stdin_read_cb(uv_stream_t *, ssize_t nread, const uv_buf_t *buf) { + if (nread > 0) { + auto write_buf = new uv_buf_t{.base = buf->base, .len = static_cast(nread)}; + state.pending_to_keeweb.emplace(write_buf); + process_keeweb_queue(); + } else if (nread < 0) { + quit_after_stdio_error(); + } +} + +void stdout_write_cb(uv_write_t *req, int status) { + delete req; + + auto buf = state.pending_to_stdout.front(); + state.pending_to_stdout.pop(); + delete buf->base; + delete buf; + + state.write_to_stdout_in_progress = false; + + auto success = status >= 0; + if (success) { + process_stdout_queue(); + } else { + quit_after_stdio_error(); + } +} + +void process_stdout_queue() { + if (state.write_to_stdout_in_progress || state.pending_to_stdout.empty()) { + return; + } + auto buf = state.pending_to_stdout.front(); + + auto write_req = new uv_write_t{}; + uv_write(write_req, state.tty_out, buf, 1, stdout_write_cb); + + state.write_to_stdout_in_progress = true; +} + +void keeweb_pipe_close_cb(uv_handle_t *pipe) { + delete pipe; + uv_read_stop(state.tty_in); + uv_loop_close(uv_default_loop()); +} + +void close_keeweb_pipe() { + if (!state.keeweb_pipe) { + return; + } + auto pipe = state.keeweb_pipe; + state.keeweb_pipe = nullptr; + uv_read_stop(pipe); + uv_close(reinterpret_cast(pipe), keeweb_pipe_close_cb); +} + +void keeweb_write_cb(uv_write_t *req, int status) { + delete req; + + auto buf = state.pending_to_keeweb.front(); + state.pending_to_keeweb.pop(); + delete buf->base; + delete buf; + + state.write_to_keeweb_in_progress = false; + + auto success = status >= 0; + if (success) { + process_keeweb_queue(); + } else { + close_keeweb_pipe(); + } +} + +void process_keeweb_queue() { + if (!state.keeweb_pipe || state.write_to_keeweb_in_progress || + state.pending_to_keeweb.empty()) { + return; + } + auto buf = state.pending_to_keeweb.front(); + + auto write_req = new uv_write_t{}; + uv_write(write_req, state.keeweb_pipe, buf, 1, keeweb_write_cb); + + state.write_to_keeweb_in_progress = true; +} + +void keeweb_pipe_read_cb(uv_stream_t *, ssize_t nread, const uv_buf_t *buf) { + if (nread > 0) { + auto write_buf = new uv_buf_t{.base = buf->base, .len = static_cast(nread)}; + state.pending_to_stdout.emplace(write_buf); + process_stdout_queue(); + } else if (nread < 0) { + close_keeweb_pipe(); + } +} + +void keeweb_pipe_connect_cb(uv_connect_t *req, int status) { + auto pipe = req->handle; + delete req; + auto connected = status >= 0; + if (connected) { + state.keeweb_pipe = pipe; + uv_read_start(pipe, alloc_buf, keeweb_pipe_read_cb); + process_keeweb_queue(); + } else { + std::cerr << "Cannot connect to KeeWeb"; + // TODO: start KeeWeb + } +} + +void connect_keeweb_pipe() { + auto temp_path = std::filesystem::temp_directory_path(); + auto keeweb_pipe_path = temp_path / "keeweb-example.sock"; + auto keeweb_pipe_name = keeweb_pipe_path.c_str(); + + auto keeweb_pipe = new uv_pipe_t{}; + uv_pipe_init(uv_default_loop(), keeweb_pipe, false); + + auto connect_req = new uv_connect_t(); + uv_pipe_connect(connect_req, keeweb_pipe, keeweb_pipe_name, keeweb_pipe_connect_cb); +} + +void start_reading_stdin() { uv_read_start(state.tty_in, alloc_buf, stdin_read_cb); } + +void init_tty() { + auto stdin_tty = new uv_tty_t{}; + uv_tty_init(uv_default_loop(), stdin_tty, fileno(stdin), 0); + state.tty_in = reinterpret_cast(stdin_tty); + + auto stdout_tty = new uv_tty_t{}; + uv_tty_init(uv_default_loop(), stdout_tty, fileno(stdout), 0); + state.tty_out = reinterpret_cast(stdout_tty); +} + +int main(int argc, char *argv[]) { + if (!check_args(argc, argv)) { + return 1; + } + + init_tty(); + start_reading_stdin(); + connect_keeweb_pipe(); + + uv_run(uv_default_loop(), UV_RUN_DEFAULT); +} diff --git a/extension/native-messaging-host/test/native-host-test.js b/extension/native-messaging-host/test/native-host-test.js new file mode 100644 index 00000000..98f5cdff --- /dev/null +++ b/extension/native-messaging-host/test/native-host-test.js @@ -0,0 +1,18 @@ +const os = require('os'); +const net = require('net'); +const path = require('path'); +const fs = require('fs'); + +const sockPath = path.join(os.tmpdir(), 'keeweb-example.sock'); + +try { + fs.unlinkSync(sockPath); +} catch {} + +const server = net.createServer((socket) => { + socket.on('data', (data) => { + socket.write(data); + }); + socket.on('end', () => {}); +}); +server.listen(sockPath);