From 698edb05569a81b79b626ce1044c02634c996c25 Mon Sep 17 00:00:00 2001 From: Joel Rosdahl Date: Fri, 2 Jan 2026 10:41:15 +0100 Subject: [PATCH] First version --- .clang-format | 46 ++++ .github/workflows/build.yaml | 31 ++- .gitignore | 1 + CHANGELOG.md | 7 + CMakeLists.txt | 63 +++++ LICENSE | 26 ++ README.md | 128 ++++++++++ conanfile.txt | 12 + src/config.cpp | 110 +++++++++ src/config.hpp | 34 +++ src/ipc_server.cpp | 359 +++++++++++++++++++++++++++ src/ipc_server.hpp | 62 +++++ src/logger.cpp | 51 ++++ src/logger.hpp | 38 +++ src/main.cpp | 77 ++++++ src/storage_client.cpp | 460 +++++++++++++++++++++++++++++++++++ src/storage_client.hpp | 91 +++++++ 17 files changed, 1593 insertions(+), 3 deletions(-) create mode 100644 .clang-format create mode 100644 CHANGELOG.md create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 conanfile.txt create mode 100644 src/config.cpp create mode 100644 src/config.hpp create mode 100644 src/ipc_server.cpp create mode 100644 src/ipc_server.hpp create mode 100644 src/logger.cpp create mode 100644 src/logger.hpp create mode 100644 src/main.cpp create mode 100644 src/storage_client.cpp create mode 100644 src/storage_client.hpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..fc02490 --- /dev/null +++ b/.clang-format @@ -0,0 +1,46 @@ +# This configuration should work with Clang-Format 18 and higher. +--- +Language: Cpp +BasedOnStyle: LLVM + +AlignArrayOfStructures: Left +AllowAllConstructorInitializersOnNextLine: false +AllowShortFunctionsOnASingleLine: Inline +AlwaysBreakBeforeMultilineStrings: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterFunction: true + AfterStruct: true + AfterUnion: true + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +ColumnLimit: 100 +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 2 +ContinuationIndentWidth: 2 +IncludeBlocks: Regroup +IncludeCategories: + # Relative headers + - Regex: '^"' + Priority: 1 + # Dependency headers: + - Regex: '^<(uv\.h|curl/curl\.h)>$' + Priority: 3 + # System headers: + - Regex: '\.h.*>$' + Priority: 4 + # C++ headers: + - Regex: '^<' + Priority: 5 +IndentPPDirectives: AfterHash +InsertNewlineAtEOF: true +KeepEmptyLinesAtTheStartOfBlocks: false +PackConstructorInitializers: Never +PointerAlignment: Left +SpaceAfterTemplateKeyword: false +Standard: c++17 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ea12d01..4804032 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,8 +1,33 @@ name: Build -on: [push] +on: + push: + branches: [main] + pull_request: + branches: [main] + jobs: - noop: + build: runs-on: ubuntu-24.04 + steps: - - run: echo "Actions enabled" + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libcurl4-openssl-dev libuv1-dev cmake build-essential + + - name: Configure + run: cmake -B build -D CMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build + + - name: Install + run: sudo cmake --install build + + - name: Verify installation + run: | + test -f /usr/local/bin/ccache-storage-http + test -L /usr/local/bin/ccache-storage-https diff --git a/.gitignore b/.gitignore index b25c15b..29db1c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *~ +/build/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f531d5f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## [0.1] - 2026-01-18 + +### Added + +- Implemented first version. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..88f8cf2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,63 @@ +cmake_minimum_required(VERSION 3.16) +project(ccache-storage-http VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +find_package(CURL QUIET CONFIG) +if(NOT CURL_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(CURL REQUIRED IMPORTED_TARGET libcurl) +endif() + +find_package(libuv QUIET CONFIG) +if(NOT libuv_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBUV REQUIRED IMPORTED_TARGET libuv) +endif() + +set( + SOURCES + src/config.cpp + src/ipc_server.cpp + src/logger.cpp + src/main.cpp + src/storage_client.cpp +) + +add_executable(ccache-storage-http ${SOURCES}) + +if(TARGET CURL::libcurl) + target_link_libraries(ccache-storage-http PRIVATE CURL::libcurl) +else() + target_link_libraries(ccache-storage-http PRIVATE PkgConfig::CURL) +endif() + +if(TARGET libuv::uv_a) + target_link_libraries(ccache-storage-http PRIVATE libuv::uv_a) +elseif(TARGET libuv::libuv) + target_link_libraries(ccache-storage-http PRIVATE libuv::libuv) +else() + target_link_libraries(ccache-storage-http PRIVATE PkgConfig::LIBUV) +endif() + +if(WIN32) + target_compile_definitions(ccache-storage-http PRIVATE _WIN32_WINNT=0x0601 WIN32_LEAN_AND_MEAN NOGDI NOMINMAX) + if(MSVC) + target_compile_options(ccache-storage-http PRIVATE /W2) + else() + target_compile_options(ccache-storage-http PRIVATE -Wall -Wextra) + endif() +else() + target_compile_options(ccache-storage-http PRIVATE -Wall -Wextra) +endif() + +install(TARGETS ccache-storage-http RUNTIME DESTINATION bin) +install(CODE "execute_process(COMMAND \${CMAKE_COMMAND} -E create_symlink ccache-storage-http \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/bin/ccache-storage-https)") + +find_program(CLANG_FORMAT clang-format) +if(CLANG_FORMAT) + file(GLOB_RECURSE ALL_SOURCE_FILES src/*.cpp src/*.hpp) + add_custom_target(format COMMAND ${CLANG_FORMAT} -i ${ALL_SOURCE_FILES} COMMENT "Running clang-format on all source files" VERBATIM) +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8a0bf57 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright 2026 Joel Rosdahl + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0767635 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# ccache-storage-http-cpp + +A [ccache remote storage helper](https://ccache.dev/storage-helpers.html) for +HTTP/HTTPS storage, written in **C++**. + +## Overview + +This is a storage helper for [ccache] that enables caching compilation results +on HTTP/HTTPS servers. It implements the [ccache remote storage helper +protocol]. + +This project aims to: + +1. Provide a high-performance, production-ready HTTP(S) ccache storage helper. +2. Serve as an example implementation of a ccache storage helper in **C++**. + Feel free to use it as a starting point for implementing helpers for other + storage service protocols. + +See also the similar [ccache-remote-http-go] project for an example (and +production ready) **Go** implementation. + +[ccache]: https://ccache.dev +[ccache remote storage helper protocol]: https://github.com/jrosdahl/ccache/blob/crsh/doc/remote_storage_helper_spec.md +[ccache-remote-http-go]: https://github.com/ccache/ccache-storage-http-go + +## Features + +- Supports HTTP and HTTPS +- High-performance concurrent request handling +- HTTP keep-alive for efficient connection reuse +- Cross-platform: Linux, macOS, Windows +- Multiple layout modes: `flat`, `subdirs`, `bazel` +- Bearer token authentication support +- Support for custom HTTP headers +- Optional debug logging + +## Installation + +The helper should be installed in a [location where ccache searches for helper +programs]. Install it as the name `ccache-storage-http` for HTTP support and/or +`ccache-storage-https` for HTTPS support. + +[location where ccache searches for helper programs]: https://github.com/jrosdahl/ccache/blob/crsh/doc/manual.adoc#storage-helper-process + +### Building from source + +Make sure you have needed dependencies installed: + +- [libcurl](https://curl.se/libcurl/) +- [libuv](https://libuv.org) +- [CMake](https://cmake.org) 3.16+ +- C++17 compiler + +(You can also install dependencies and build the project using +[Conan](https://docs.conan.io/2/).) + +Clone the repository: + +```bash +git clone https://github.com/ccache/ccache-storage-http-cpp +cd ccache-storage-http-cpp +``` + +Build and install: + +```bash +cmake -B build -D CMAKE_BUILD_TYPE=Release +cmake --build build +cmake --install build +``` + +This will install both `ccache-storage-http` and `ccache-storage-https` to +`/usr/local/bin`. Pass `-D CMAKE_INSTALL_PREFIX=/example/dir` to `cmake` to +install elsewhere. + +## Configuration + +The helper is configured via ccache's [`remote_storage` configuration]. The +binary is automatically invoked by ccache when needed. + +For example: + +```bash +# Set the CCACHE_REMOTE_STORAGE environment variable: +export CCACHE_REMOTE_STORAGE="https://cache.example.com" + +# Or set remote_storage in ccache's configuration file: +ccache -o remote_storage="https://cache.example.com" +``` + +[`remote_storage` configuration]: https://github.com/jrosdahl/ccache/blob/crsh/doc/manual.adoc#remote-storage-backends + +See also the [HTTP storage wiki page] for tips on how to set up a storage server. + +[HTTP storage wiki page]: https://github.com/ccache/ccache/wiki/HTTP-storage + +### Configuration attributes + +The helper supports the following custom attributes: + +- `@bearer-token`: Bearer token for `Authorization` header +- `@header`: Custom HTTP headers (can be specified multiple times) +- `@layout`: Storage layout mode + - `subdirs` (default): First 2 hex chars as subdirectory + - `flat`: All files in root directory + - `bazel`: Bazel Remote Execution API compatible layout + +Example: + +```bash +export CCACHE_REMOTE_STORAGE="https://cache.example.com @header=Content-Type=application/octet-stream" +``` + +## Optional debug logging + +You can set the `CRSH_LOGFILE` environment variable to enable debug logging to a +file: + +```bash +export CRSH_LOGFILE=/path/to/debug.log +``` + +Note: The helper process is spawned by ccache, so the environment variable must +be set before ccache is invoked. + +## Contributing + +Contributions are welcome! Please submit pull requests or open issues on GitHub. diff --git a/conanfile.txt b/conanfile.txt new file mode 100644 index 0000000..5e39643 --- /dev/null +++ b/conanfile.txt @@ -0,0 +1,12 @@ +[requires] +libcurl/8.11.1 +libuv/1.49.2 + +[generators] +CMakeDeps +CMakeToolchain + +[options] + +[layout] +cmake_layout diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..2995e04 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Joel Rosdahl + +#include "config.hpp" + +#include "logger.hpp" + +#include +#include +#include + +template std::optional parse_int(std::string_view str, int base = 10) +{ + T value{}; + auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), value, base); + if (ec == std::errc{} && ptr == str.data() + str.size()) { + return value; + } + return std::nullopt; +} + +static UrlLayout parse_layout(const std::string& str) +{ + if (str == "bazel") { + return UrlLayout::BAZEL; + } else if (str == "flat") { + return UrlLayout::FLAT; + } else { + return UrlLayout::SUBDIRS; // Default + } +} + +std::optional parse_config() +{ + Config config; + + const char* ipc_endpoint = std::getenv("CRSH_IPC_ENDPOINT"); + if (!ipc_endpoint || ipc_endpoint[0] == '\0') { + LOG("CRSH_IPC_ENDPOINT not set"); + return std::nullopt; + } +#ifdef _WIN32 + config.ipc_endpoint = std::string("\\\\.\\pipe\\") + ipc_endpoint; +#else + config.ipc_endpoint = ipc_endpoint; +#endif + + const char* url = std::getenv("CRSH_URL"); + if (!url || url[0] == '\0') { + LOG("CRSH_URL not set"); + return std::nullopt; + } + config.url = url; + + const char* idle_timeout = std::getenv("CRSH_IDLE_TIMEOUT"); + if (!idle_timeout || idle_timeout[0] == '\0') { + idle_timeout = "0"; + } + auto idle_val = parse_int(idle_timeout); + if (!idle_val) { + LOG("CRSH_IDLE_TIMEOUT must be a non-negative integer"); + return std::nullopt; + } + config.idle_timeout_seconds = *idle_val; + + const char* num_attr_str = std::getenv("CRSH_NUM_ATTR"); + if (!num_attr_str || num_attr_str[0] == '\0') { + num_attr_str = "0"; + } + auto num_attr_val = parse_int(num_attr_str); + if (!num_attr_val) { + LOG("CRSH_NUM_ATTR must be a non-negative integer"); + return std::nullopt; + } + size_t num_attr = *num_attr_val; + + for (size_t i = 0; i < num_attr; ++i) { + std::string key_env = "CRSH_ATTR_KEY_" + std::to_string(i); + std::string value_env = "CRSH_ATTR_VALUE_" + std::to_string(i); + + const char* key = std::getenv(key_env.c_str()); + if (!key) { + LOG(key_env + " not set"); + return std::nullopt; + } + const char* value = std::getenv(value_env.c_str()); + if (!value) { + LOG(value_env + " not set"); + return std::nullopt; + } + + std::string key_str(key); + std::string value_str(value); + + if (key_str == "bearer-token") { + config.bearer_token = value_str; + } else if (key_str == "layout") { + config.layout = parse_layout(value_str); + } else if (key_str == "header") { + size_t eq_pos = value_str.find('='); + if (eq_pos != std::string::npos) { + std::string header_name = value_str.substr(0, eq_pos); + std::string header_value = value_str.substr(eq_pos + 1); + config.headers.emplace_back(header_name, header_value); + } + } + } + + return config; +} diff --git a/src/config.hpp b/src/config.hpp new file mode 100644 index 0000000..6126097 --- /dev/null +++ b/src/config.hpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Joel Rosdahl + +#pragma once + +#include +#include +#include +#include +#include + +#ifndef _WIN32 +# include +#endif + +enum class UrlLayout { + BAZEL, // ac/ + 64 hex digits + FLAT, // key directly appended + SUBDIRS, // first 2 chars / rest of key +}; + +struct Config +{ + std::string ipc_endpoint; + std::string url; + unsigned int idle_timeout_seconds = 0; + + // Attributes from CRSH_ATTR_* + std::optional bearer_token; + UrlLayout layout = UrlLayout::SUBDIRS; + std::vector> headers; +}; + +std::optional parse_config(); diff --git a/src/ipc_server.cpp b/src/ipc_server.cpp new file mode 100644 index 0000000..30f17a6 --- /dev/null +++ b/src/ipc_server.cpp @@ -0,0 +1,359 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Joel Rosdahl + +#include "ipc_server.hpp" + +#include "logger.hpp" + +#include +#include +#include +#include +#include + +#ifndef _WIN32 +# include +# include +#endif + +namespace { + +constexpr uint8_t PROTOCOL_VERSION = 0x01; +constexpr uint8_t CAP_GET_PUT_REMOVE_STOP = 0x00; + +constexpr uint8_t STATUS_OK = 0x00; +constexpr uint8_t STATUS_NOOP = 0x01; +constexpr uint8_t STATUS_ERR = 0x02; + +constexpr uint8_t REQ_GET = 0x00; +constexpr uint8_t REQ_PUT = 0x01; +constexpr uint8_t REQ_REMOVE = 0x02; +constexpr uint8_t REQ_STOP = 0x03; + +constexpr uint8_t PUT_FLAG_OVERWRITE = 0x01; +constexpr size_t MAX_MSG_LEN = 255; + +static std::string format_hex(const uint8_t* data, size_t len) +{ + std::ostringstream oss; + oss << std::hex << std::setfill('0'); + for (size_t i = 0; i < len; ++i) { + oss << std::setw(2) << static_cast(data[i]); + } + return oss.str(); +} + +static uint64_t read_u64_host_byte_order(const uint8_t* data) +{ + uint64_t value; + std::memcpy(&value, data, sizeof(value)); + return value; +} + +static void write_u64_host_byte_order(std::vector& buf, uint64_t value) +{ + size_t offset = buf.size(); + buf.resize(offset + sizeof(value)); + std::memcpy(buf.data() + offset, &value, sizeof(value)); +} + +} // namespace + +IpcServer::IpcServer(uv_loop_t& loop, const Config& config, StorageClient& storage_client) + : _loop(loop), + _config(config), + _storage_client(storage_client) +{ +} + +bool IpcServer::init() +{ + int r = uv_pipe_init(&_loop, &_server_pipe, 0); + if (r != 0) { + LOG("Failed to initialize pipe: " + std::string(uv_strerror(r))); + return false; + } + _server_pipe.data = this; + +#ifdef _WIN32 + r = uv_pipe_bind(&_server_pipe, _config.ipc_endpoint.c_str()); +#else + unlink(_config.ipc_endpoint.c_str()); + mode_t old_umask = umask(0077); + r = uv_pipe_bind(&_server_pipe, _config.ipc_endpoint.c_str()); + umask(old_umask); +#endif + + if (r != 0) { + LOG("Failed to bind to IPC endpoint: " + std::string(uv_strerror(r))); + return false; + } + + r = uv_listen(reinterpret_cast(&_server_pipe), 128, on_new_connection); + if (r != 0) { + LOG("Failed to listen on IPC endpoint: " + std::string(uv_strerror(r))); + return false; + } + + r = uv_timer_init(&_loop, &_idle_timer); + if (r != 0) { + LOG("Failed to initialize idle timer: " + std::string(uv_strerror(r))); + return false; + } + + _idle_timer.data = this; + reset_idle_timer(); + + LOG("IPC server listening on " + _config.ipc_endpoint); + return true; +} + +void IpcServer::stop() +{ + LOG("Shutting down"); +#ifndef _WIN32 + unlink(_config.ipc_endpoint.c_str()); +#endif + uv_stop(&_loop); +} + +void IpcServer::reset_idle_timer() +{ + if (_config.idle_timeout_seconds == 0) { + return; + } + uint64_t timeout_ms = static_cast(_config.idle_timeout_seconds) * 1000; + uv_timer_start(&_idle_timer, on_idle_timeout, timeout_ms, 0); +} + +void IpcServer::on_idle_timeout(uv_timer_t* handle) +{ + IpcServer* server = static_cast(handle->data); + LOG("Idle timeout reached, shutting down"); + server->stop(); +} + +void IpcServer::on_new_connection(uv_stream_t* server_stream, int status) +{ + if (status < 0) { + LOG("Connection error: " + std::string(uv_strerror(status))); + return; + } + + IpcServer* server = static_cast(server_stream->data); + server->reset_idle_timer(); + + auto client = std::make_unique(); + client->server = server; + + int r = uv_pipe_init(&server->_loop, &client->handle, 0); + if (r != 0) { + LOG("Failed to initialize client pipe: " + std::string(uv_strerror(r))); + return; + } + client->handle.data = client.get(); + + r = uv_accept(server_stream, reinterpret_cast(&client->handle)); + if (r != 0) { + LOG("Failed to accept connection: " + std::string(uv_strerror(r))); + uv_close(reinterpret_cast(&client->handle), on_close); + client.release(); // Will be deleted in on_close + return; + } + + LOG("Client connected"); + + // Send greeting: version(u8) + num_capabilities(u8) + capabilities... + std::vector greeting = {PROTOCOL_VERSION, 1, CAP_GET_PUT_REMOVE_STOP}; + server->send_response(*client, std::move(greeting)); + + r = uv_read_start(reinterpret_cast(&client->handle), alloc_buffer, on_client_read); + if (r != 0) { + LOG("Failed to start reading: " + std::string(uv_strerror(r))); + uv_close(reinterpret_cast(&client->handle), on_close); + } + + client.release(); // Ownership transferred to libuv callbacks +} + +void IpcServer::alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) +{ + ClientConnection* client = static_cast(handle->data); + client->alloc_buf.resize(suggested_size); + buf->base = client->alloc_buf.data(); + buf->len = suggested_size; +} + +void IpcServer::on_client_read(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) +{ + if (nread > 0) { + ClientConnection* client = static_cast(stream->data); + IpcServer* server = client->server; + server->reset_idle_timer(); + client->read_buf.insert(client->read_buf.end(), buf->base, buf->base + nread); + server->process_client_data(*client); + } else if (nread < 0) { + if (nread != UV_EOF) { + LOG("Read error: " + std::string(uv_strerror(static_cast(nread)))); + } + uv_close(reinterpret_cast(stream), on_close); + } +} + +void IpcServer::process_client_data(ClientConnection& client) +{ + auto& buf = client.read_buf; + + while (!buf.empty()) { + const uint8_t* data = buf.data(); + size_t len = buf.size(); + uint8_t request_type = data[0]; + size_t offset = 1; + + if (request_type == REQ_STOP) { + buf.erase(buf.begin(), buf.begin() + offset); + LOG("STOP request received"); + send_response(client, std::vector{STATUS_OK}); + stop(); + return; + } + + if (request_type != REQ_GET && request_type != REQ_PUT && request_type != REQ_REMOVE) { + LOG("Unknown request type: " + std::to_string(request_type)); + stop(); + return; + } + + if (len < offset + 1) { + return; // incomplete message + } + uint8_t key_len = data[offset++]; + if (len < offset + key_len) { + return; // incomplete message + } + std::string hex_key = format_hex(data + offset, key_len); + offset += key_len; + + switch (request_type) { + case REQ_GET: + LOG("GET request for key " + hex_key); + _storage_client.get(hex_key, [&](StorageResponse&& response) { + if (response.result == StorageResult::OK) { + std::vector header; + header.reserve(9); + header.push_back(STATUS_OK); + write_u64_host_byte_order(header, response.data.size()); + send_response(client, std::move(header)); + send_response(client, std::move(response.data)); + } else { + send_simple_response(client, "GET", response); + } + }); + break; + + case REQ_PUT: { + if (len < offset + 1) { + return; // incomplete message + } + uint8_t flags = data[offset++]; + if (len < offset + sizeof(uint64_t)) { + return; // incomplete message + } + + uint64_t value_len = read_u64_host_byte_order(data + offset); + offset += sizeof(uint64_t); + if (len < offset + value_len) { + return; // incomplete message + } + + std::vector value(data + offset, data + offset + value_len); + offset += value_len; + bool overwrite = (flags & PUT_FLAG_OVERWRITE) != 0; + LOG("PUT request for key " + hex_key + " (" + std::to_string(value.size()) + " bytes)"); + + _storage_client.put(hex_key, std::move(value), overwrite, [&](StorageResponse&& response) { + send_simple_response(client, "PUT", response); + }); + break; + } + + case REQ_REMOVE: + LOG("REMOVE request for key " + hex_key); + _storage_client.remove(hex_key, [&](StorageResponse&& response) { + send_simple_response(client, "REMOVE", response); + }); + break; + } + + buf.erase(buf.begin(), buf.begin() + offset); + } +} + +void IpcServer::send_simple_response(ClientConnection& client, + const std::string& operation, + const StorageResponse& response) +{ + switch (response.result) { + case StorageResult::OK: + send_response(client, std::vector{STATUS_OK}); + break; + case StorageResult::NOOP: + send_response(client, std::vector{STATUS_NOOP}); + break; + case StorageResult::ERROR: + LOG(operation + " failed: " + response.error); + std::vector err_resp; + err_resp.push_back(STATUS_ERR); + uint8_t msg_len = std::min(response.error.size(), MAX_MSG_LEN); + err_resp.push_back(msg_len); + err_resp.insert(err_resp.end(), response.error.begin(), response.error.begin() + msg_len); + send_response(client, err_resp); + break; + } +} + +void IpcServer::flush_write_queue(ClientConnection& client) +{ + if (client.writing || client.write_queue.empty()) { + return; + } + + client.writing = true; + auto write_req = std::make_unique(); + auto data = std::make_unique>(std::move(client.write_queue.front())); + client.write_queue.erase(client.write_queue.begin()); + + uv_buf_t buf = + uv_buf_init(reinterpret_cast(data->data()), static_cast(data->size())); + write_req->data = data.get(); + + auto stream = reinterpret_cast(&client.handle); + int r = uv_write(write_req.get(), stream, &buf, 1, on_write_complete); + if (r != 0) { + LOG("Write error: " + std::string(uv_strerror(r))); + client.writing = false; + return; + } + + // uv_write succeeded; data and write_req will be deleted in on_write_complete + data.release(); + write_req.release(); +} + +void IpcServer::on_write_complete(uv_write_t* req, int status) +{ + if (status < 0) { + LOG("Write failed: " + std::string(uv_strerror(status))); + } + auto client = static_cast(req->handle->data); + client->writing = false; + client->server->flush_write_queue(*client); + delete static_cast*>(req->data); + delete req; +} + +void IpcServer::on_close(uv_handle_t* handle) +{ + LOG("Client disconnected"); + delete static_cast(handle->data); +} diff --git a/src/ipc_server.hpp b/src/ipc_server.hpp new file mode 100644 index 0000000..35f31bb --- /dev/null +++ b/src/ipc_server.hpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Joel Rosdahl + +#pragma once + +#include "config.hpp" +#include "storage_client.hpp" + +#include + +#include +#include +#include +#include + +class IpcServer; + +struct ClientConnection +{ + uv_pipe_t handle; + IpcServer* server; + std::vector read_buf; + std::vector alloc_buf; // Reusable buffer for libuv reads + bool writing = false; + std::vector> write_queue; +}; + +class IpcServer +{ +public: + IpcServer(uv_loop_t& loop, const Config& config, StorageClient& storage_client); + + bool init(); + void stop(); + void reset_idle_timer(); + + template void send_response(ClientConnection& client, T&& data) + { + client.write_queue.push_back(std::forward(data)); + flush_write_queue(client); + } + +private: + static void on_new_connection(uv_stream_t* server, int status); + static void on_client_read(uv_stream_t* client, ssize_t nread, const uv_buf_t* buf); + static void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf); + static void on_write_complete(uv_write_t* req, int status); + static void on_close(uv_handle_t* handle); + static void on_idle_timeout(uv_timer_t* handle); + + void process_client_data(ClientConnection& client); + void flush_write_queue(ClientConnection& client); + void send_simple_response(ClientConnection& client, + const std::string& operation, + const StorageResponse& response); + + uv_loop_t& _loop; + const Config& _config; + StorageClient& _storage_client; + uv_pipe_t _server_pipe; + uv_timer_t _idle_timer; +}; diff --git a/src/logger.cpp b/src/logger.cpp new file mode 100644 index 0000000..1762541 --- /dev/null +++ b/src/logger.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Joel Rosdahl + +#include "logger.hpp" + +#include +#include + +Logger g_logger; + +void init_logger() +{ + const char* log_file = std::getenv("CRSH_LOGFILE"); + if (log_file) { + g_logger.init(log_file); + } +} + +void Logger::init(const std::string& log_file_path) +{ + if (log_file_path.empty()) { + _enabled = false; + return; + } + + _file.open(log_file_path, std::ios::app); + if (_file.is_open()) { + _enabled = true; + } +} + +void Logger::log(const std::string& msg) +{ + if (!_enabled || !_file.is_open()) { + return; + } + + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; + + struct tm tm; +#ifdef _WIN32 + localtime_s(&tm, &time); +#else + localtime_r(&time, &tm); +#endif + + _file << "[" << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S") << '.' << std::setfill('0') + << std::setw(3) << ms.count() << "] " << msg << std::endl; +} diff --git a/src/logger.hpp b/src/logger.hpp new file mode 100644 index 0000000..bd91bb3 --- /dev/null +++ b/src/logger.hpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Joel Rosdahl + +#pragma once + +#include +#include +#include +#include + +class Logger +{ +public: + Logger() = default; + + Logger(const Logger&) = delete; + Logger& operator=(const Logger&) = delete; + + void init(const std::string& log_file_path); + void log(const std::string& msg); + + bool is_enabled() const { return _enabled; } + +private: + std::ofstream _file; + bool _enabled = false; +}; + +extern Logger g_logger; + +void init_logger(); + +#define LOG(msg) \ + do { \ + if (g_logger.is_enabled()) { \ + g_logger.log(msg); \ + } \ + } while (false) diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..4cdc524 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Joel Rosdahl + +#include "config.hpp" +#include "ipc_server.hpp" +#include "logger.hpp" +#include "storage_client.hpp" + +#include + +#include +#include + +static constexpr auto USAGE = + "This is a ccache HTTP(S) storage helper, usually started automatically by ccache\n" + "when needed. More information here: https://ccache.dev/storage-helpers.html\n" + "\n" + "Project: https://github.com/ccache/ccache-storage-http-cpp\n" + "Version: 0.1\n"; + +int main() +{ + if (!std::getenv("CRSH_IPC_ENDPOINT") || !std::getenv("CRSH_URL")) { + std::cerr << USAGE; + return 1; + } + + init_logger(); + + auto config = parse_config(); + if (!config) { + LOG("Failed to parse configuration"); + return 1; + } + + LOG("Starting"); + LOG("IPC endpoint: " + config->ipc_endpoint); + LOG("URL: " + config->url); + LOG("Idle timeout: " + std::to_string(config->idle_timeout_seconds)); + + uv_loop_t* loop = uv_default_loop(); + if (!loop) { + LOG("Failed to create event loop"); + return 1; + } + + StorageClient storage_client(*loop, *config); + if (!storage_client.init()) { + LOG("Failed to initialize storage client"); + return 1; + } + + IpcServer ipc_server(*loop, *config, storage_client); + if (!ipc_server.init()) { + LOG("Failed to initialize IPC server"); + return 1; + } + + int result = uv_run(loop, UV_RUN_DEFAULT); + LOG("Event loop exited with code " + std::to_string(result)); + + uv_walk( + loop, + [](uv_handle_t* handle, void* /*arg*/) { + if (!uv_is_closing(handle)) { + uv_close(handle, nullptr); + } + }, + nullptr); + + // Run loop again to process close callbacks. + uv_run(loop, UV_RUN_DEFAULT); + uv_loop_close(loop); + + LOG("Shutdown complete"); + return 0; +} diff --git a/src/storage_client.cpp b/src/storage_client.cpp new file mode 100644 index 0000000..e062102 --- /dev/null +++ b/src/storage_client.cpp @@ -0,0 +1,460 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Joel Rosdahl + +#include "storage_client.hpp" + +#include "logger.hpp" + +#include +#include + +namespace { + +static std::string build_url(const Config& config, const std::string& hex_key) +{ + std::string base_url = config.url; + + if (base_url.empty() || base_url.back() != '/') { + base_url += '/'; + } + + std::ostringstream url; + url << base_url; + + switch (config.layout) { + case UrlLayout::BAZEL: { + // Bazel format: ac/ + 64 hex digits, so pad shorter keys by repeating the key prefix to reach + // the expected SHA256 size. + constexpr size_t sha256_hex_size = 64; + url << "ac/"; + if (hex_key.size() >= sha256_hex_size) { + url << hex_key.substr(0, sha256_hex_size); + } else { + url << hex_key << hex_key.substr(0, sha256_hex_size - hex_key.size()); + } + break; + } + + case UrlLayout::FLAT: + url << hex_key; + break; + + case UrlLayout::SUBDIRS: + if (hex_key.size() >= 2) { + url << hex_key.substr(0, 2) << "/" << hex_key.substr(2); + } else { + url << hex_key; + } + break; + } + + return url.str(); +} + +} // namespace + +StorageClient::StorageClient(uv_loop_t& loop, const Config& config) + : _loop(loop), + _config(config) +{ +} + +StorageClient::~StorageClient() +{ + if (_multi_handle) { + for (auto& pair : _active_requests) { + curl_multi_remove_handle(_multi_handle, pair.first); + if (pair.second->headers) { + curl_slist_free_all(pair.second->headers); + } + curl_easy_cleanup(pair.first); + } + _active_requests.clear(); + curl_multi_cleanup(_multi_handle); + } + curl_global_cleanup(); +} + +bool StorageClient::init() +{ + CURLcode result = curl_global_init(CURL_GLOBAL_DEFAULT); + if (result != CURLE_OK) { + LOG("Failed to initialize curl: " + std::string(curl_easy_strerror(result))); + return false; + } + + _multi_handle = curl_multi_init(); + if (!_multi_handle) { + LOG("Failed to initialize curl multi handle"); + return false; + } + + curl_multi_setopt(_multi_handle, CURLMOPT_SOCKETFUNCTION, socket_callback); + curl_multi_setopt(_multi_handle, CURLMOPT_SOCKETDATA, this); + curl_multi_setopt(_multi_handle, CURLMOPT_TIMERFUNCTION, timer_callback); + curl_multi_setopt(_multi_handle, CURLMOPT_TIMERDATA, this); + curl_multi_setopt(_multi_handle, CURLMOPT_MAX_HOST_CONNECTIONS, 16L); + curl_multi_setopt(_multi_handle, CURLMOPT_MAXCONNECTS, 16L); + + uv_timer_init(&_loop, &_timeout_timer); + _timeout_timer.data = this; + + return true; +} + +void StorageClient::get(const std::string& hex_key, StorageCallback&& callback) +{ + auto request = std::make_unique(); + request->operation = HttpOperation::GET; + request->url = build_url(_config, hex_key); + request->callback = std::move(callback); + + LOG("GET " + request->url); + + CURL* handle = create_easy_handle(request.get()); + if (!handle) { + request->callback(StorageResponse{StorageResult::ERROR, "Failed to create curl handle", {}}); + return; + } + + curl_easy_setopt(handle, CURLOPT_HTTPGET, 1L); + _active_requests[handle] = std::move(request); + curl_multi_add_handle(_multi_handle, handle); +} + +void StorageClient::put(const std::string& hex_key, + std::vector&& data, + bool overwrite, + StorageCallback&& callback) +{ + LOG("PUT " + hex_key + " (" + std::to_string(data.size()) + + " bytes, overwrite=" + (overwrite ? "true" : "false") + ")"); + + if (overwrite) { + do_put(hex_key, std::move(data), std::move(callback)); + } else { + std::string url = build_url(_config, hex_key); + auto request = std::make_unique(); + request->operation = HttpOperation::HEAD; + request->url = url; + + CURL* handle = create_easy_handle(request.get()); + if (!handle) { + callback(StorageResponse{StorageResult::ERROR, "Failed to create curl handle", {}}); + return; + } + + request->callback = [this, hex_key, data = std::move(data), callback = std::move(callback)]( + StorageResponse&& response) mutable { + if (response.result == StorageResult::NOOP) { + LOG("HEAD check: resource doesn't exist, proceeding with PUT"); + do_put(hex_key, std::move(data), std::move(callback)); + } else if (response.result == StorageResult::OK) { + LOG("HEAD check: resource exists, not overwriting"); + callback(StorageResponse{StorageResult::NOOP, "", {}}); + } else { + callback(std::move(response)); + } + }; + + curl_easy_setopt(handle, CURLOPT_NOBODY, 1L); + _active_requests[handle] = std::move(request); + curl_multi_add_handle(_multi_handle, handle); + } +} + +void StorageClient::do_put(const std::string& hex_key, + std::vector&& data, + StorageCallback&& callback) +{ + size_t data_size = data.size(); + auto request = std::make_unique(); + request->operation = HttpOperation::PUT; + request->url = build_url(_config, hex_key); + request->request_data = std::move(data); + request->callback = std::move(callback); + + CURL* handle = create_easy_handle(request.get()); + if (!handle) { + request->callback(StorageResponse{StorageResult::ERROR, "Failed to create curl handle", {}}); + return; + } + + curl_easy_setopt(handle, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(handle, CURLOPT_INFILESIZE_LARGE, static_cast(data_size)); + curl_easy_setopt(handle, CURLOPT_READFUNCTION, read_callback); + curl_easy_setopt(handle, CURLOPT_READDATA, request.get()); + _active_requests[handle] = std::move(request); + curl_multi_add_handle(_multi_handle, handle); +} + +void StorageClient::remove(const std::string& hex_key, StorageCallback&& callback) +{ + auto request = std::make_unique(); + request->operation = HttpOperation::DELETE; + request->url = build_url(_config, hex_key); + request->callback = std::move(callback); + + LOG("DELETE " + request->url); + + CURL* handle = create_easy_handle(request.get()); + if (!handle) { + request->callback(StorageResponse{StorageResult::ERROR, "Failed to create curl handle", {}}); + return; + } + + curl_easy_setopt(handle, CURLOPT_CUSTOMREQUEST, "DELETE"); + _active_requests[handle] = std::move(request); + curl_multi_add_handle(_multi_handle, handle); +} + +CURL* StorageClient::create_easy_handle(HttpRequest* request) +{ + CURL* handle = curl_easy_init(); + if (!handle) { + return nullptr; + } + + curl_easy_setopt(handle, CURLOPT_ERRORBUFFER, request->error_buf); + curl_easy_setopt(handle, CURLOPT_EXPECT_100_TIMEOUT_MS, 0L); + curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS); + curl_easy_setopt(handle, CURLOPT_MAXREDIRS, 5L); + curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(handle, CURLOPT_PRIVATE, request); + curl_easy_setopt(handle, CURLOPT_TCP_KEEPALIVE, 1L); + curl_easy_setopt(handle, CURLOPT_URL, request->url.c_str()); + curl_easy_setopt(handle, CURLOPT_WRITEDATA, request); + curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, write_callback); + + curl_slist* headers = nullptr; + + if (_config.bearer_token) { + std::string auth_header = "Authorization: Bearer " + *_config.bearer_token; + headers = curl_slist_append(headers, auth_header.c_str()); + } + + for (const auto& header : _config.headers) { + std::string header_line = header.first + ": " + header.second; + headers = curl_slist_append(headers, header_line.c_str()); + } + + if (headers) { + curl_easy_setopt(handle, CURLOPT_HTTPHEADER, headers); + request->headers = headers; + } + + return handle; +} + +CurlSocketContext* StorageClient::create_socket_context(curl_socket_t sockfd) +{ + auto ctx = new CurlSocketContext; + ctx->sockfd = sockfd; + ctx->client = this; + uv_poll_init_socket(&_loop, &ctx->poll_handle, sockfd); + ctx->poll_handle.data = ctx; + return ctx; +} + +void StorageClient::destroy_socket_context(CurlSocketContext* ctx) +{ + uv_poll_stop(&ctx->poll_handle); + uv_close(reinterpret_cast(&ctx->poll_handle), + [](uv_handle_t* handle) { delete static_cast(handle->data); }); +} + +void StorageClient::check_multi_info() +{ + CURLMsg* msg; + int msgs_left; + + while ((msg = curl_multi_info_read(_multi_handle, &msgs_left))) { + if (msg->msg != CURLMSG_DONE) { + continue; + } + + CURL* handle = msg->easy_handle; + CURLcode result = msg->data.result; + HttpRequest* request = nullptr; + curl_easy_getinfo(handle, CURLINFO_PRIVATE, &request); + + long http_code = 0; + curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &http_code); + + StorageResult http_result = StorageResult::ERROR; + std::string error; + + if (result != CURLE_OK) { + error = request->error_buf[0] ? request->error_buf : curl_easy_strerror(result); + LOG("Curl error: " + error); + http_result = StorageResult::ERROR; + } else { + switch (request->operation) { + case HttpOperation::GET: + if (http_code == 200) { + http_result = StorageResult::OK; + } else if (http_code == 404) { + // Not found means key doesn't exist -> NOOP + http_result = StorageResult::NOOP; + request->response_data.clear(); + } else { + error = "HTTP " + std::to_string(http_code); + http_result = StorageResult::ERROR; + } + break; + + case HttpOperation::HEAD: + // HEAD is used to check if resource exists before PUT. + if (http_code == 200) { + // Resource exists -> OK (callback will convert to NOOP if needed) + http_result = StorageResult::OK; + } else if (http_code == 404) { + // Resource doesn't exist -> NOOP (callback will proceed with PUT) + http_result = StorageResult::NOOP; + } else { + // Other HTTP error from HEAD + error = "HTTP " + std::to_string(http_code); + http_result = StorageResult::ERROR; + } + break; + + case HttpOperation::PUT: + if (http_code >= 200 && http_code < 300) { + http_result = StorageResult::OK; + } else if (http_code == 412 || http_code == 409) { + // Precondition failed or conflict -> NOOP (key already exists, not overwritten) + http_result = StorageResult::NOOP; + } else { + error = "HTTP " + std::to_string(http_code); + http_result = StorageResult::ERROR; + } + break; + + case HttpOperation::DELETE: + if (http_code >= 200 && http_code < 300) { + http_result = StorageResult::OK; + } else if (http_code == 404) { + // Key not found -> NOOP (nothing to remove) + http_result = StorageResult::NOOP; + } else { + error = "HTTP " + std::to_string(http_code); + http_result = StorageResult::ERROR; + } + break; + } + } + + LOG("Request completed: " + request->url + " HTTP " + std::to_string(http_code)); + + auto it = _active_requests.find(handle); + if (it != _active_requests.end()) { + auto req = std::move(it->second); + _active_requests.erase(it); + curl_multi_remove_handle(_multi_handle, handle); + if (req->headers) { + curl_slist_free_all(req->headers); + } + curl_easy_cleanup(handle); + req->callback(StorageResponse{http_result, std::move(error), std::move(req->response_data)}); + } + } +} + +int StorageClient::socket_callback( + CURL* /*handle*/, curl_socket_t s, int what, void* userp, void* socketp) +{ + StorageClient* client = static_cast(userp); + CurlSocketContext* ctx = static_cast(socketp); + + if (what == CURL_POLL_REMOVE) { + if (ctx) { + client->destroy_socket_context(ctx); + } + return 0; + } + + if (!ctx) { + ctx = client->create_socket_context(s); + curl_multi_assign(client->_multi_handle, s, ctx); + } + + int events = 0; + if (what & CURL_POLL_IN) { + events |= UV_READABLE; + } + if (what & CURL_POLL_OUT) { + events |= UV_WRITABLE; + } + + uv_poll_start(&ctx->poll_handle, events, on_poll); + + return 0; +} + +int StorageClient::timer_callback(CURLM* /*handle*/, long timeout_ms, void* userp) +{ + StorageClient* client = static_cast(userp); + + if (timeout_ms < 0) { + uv_timer_stop(&client->_timeout_timer); + } else { + uv_timer_start(&client->_timeout_timer, on_timeout, timeout_ms, 0); + } + + return 0; +} + +void StorageClient::on_timeout(uv_timer_t* handle) +{ + StorageClient* client = static_cast(handle->data); + int running_handles; + curl_multi_socket_action(client->_multi_handle, CURL_SOCKET_TIMEOUT, 0, &running_handles); + client->check_multi_info(); +} + +void StorageClient::on_poll(uv_poll_t* handle, int status, int events) +{ + CurlSocketContext* ctx = static_cast(handle->data); + StorageClient* client = ctx->client; + + int flags = 0; + if (status < 0) { + flags = CURL_CSELECT_ERR; + } else { + if (events & UV_READABLE) { + flags |= CURL_CSELECT_IN; + } + if (events & UV_WRITABLE) { + flags |= CURL_CSELECT_OUT; + } + } + + int running_handles; + curl_multi_socket_action(client->_multi_handle, ctx->sockfd, flags, &running_handles); + client->check_multi_info(); +} + +size_t StorageClient::write_callback(char* ptr, size_t size, size_t nmemb, void* userdata) +{ + HttpRequest* request = static_cast(userdata); + size_t total = size * nmemb; + request->response_data.insert(request->response_data.end(), ptr, ptr + total); + return total; +} + +size_t StorageClient::read_callback(char* ptr, size_t size, size_t nmemb, void* userdata) +{ + HttpRequest* request = static_cast(userdata); + + size_t max_bytes = size * nmemb; + const std::vector& data = request->request_data; + size_t remaining = (request->upload_pos < data.size()) ? (data.size() - request->upload_pos) : 0; + size_t to_copy = std::min(remaining, max_bytes); + if (to_copy > 0) { + std::memcpy(ptr, data.data() + request->upload_pos, to_copy); + request->upload_pos += to_copy; + } + + return to_copy; +} diff --git a/src/storage_client.hpp b/src/storage_client.hpp new file mode 100644 index 0000000..9309f68 --- /dev/null +++ b/src/storage_client.hpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Joel Rosdahl + +#pragma once + +#include "config.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +enum class StorageResult { OK, NOOP, ERROR }; + +struct StorageResponse +{ + StorageResult result; + std::string error; + std::vector data; +}; + +using StorageCallback = std::function; + +#undef DELETE // needed on Windows +enum class HttpOperation { GET, PUT, DELETE, HEAD }; + +struct HttpRequest +{ + HttpOperation operation; + std::string url; + std::vector request_data; // For PUT + std::vector response_data; + StorageCallback callback; + struct curl_slist* headers = nullptr; + char error_buf[CURL_ERROR_SIZE] = {0}; + size_t upload_pos = 0; +}; + +class StorageClient; + +struct CurlSocketContext +{ + uv_poll_t poll_handle; + curl_socket_t sockfd; + StorageClient* client; +}; + +class StorageClient +{ +public: + StorageClient(uv_loop_t& loop, const Config& config); + ~StorageClient(); + + bool init(); + + void get(const std::string& hex_key, StorageCallback&& callback); + void put(const std::string& hex_key, + std::vector&& data, + bool overwrite, + StorageCallback&& callback); + void remove(const std::string& hex_key, StorageCallback&& callback); + +private: + void do_put(const std::string& hex_key, std::vector&& data, StorageCallback&& callback); + void check_multi_info(); + + CURL* create_easy_handle(HttpRequest* request); + CurlSocketContext* create_socket_context(curl_socket_t sockfd); + void destroy_socket_context(CurlSocketContext* ctx); + + // Static callbacks for curl: + static int socket_callback(CURL* handle, curl_socket_t s, int action, void* userp, void* socketp); + static int timer_callback(CURLM* multi, long timeout_ms, void* userp); + static size_t write_callback(char* ptr, size_t size, size_t nmemb, void* userdata); + static size_t read_callback(char* ptr, size_t size, size_t nmemb, void* userdata); + + // Static callbacks for libuv: + static void on_timeout(uv_timer_t* handle); + static void on_poll(uv_poll_t* handle, int status, int events); + + uv_loop_t& _loop; + const Config& _config; + CURLM* _multi_handle = nullptr; + uv_timer_t _timeout_timer; + std::unordered_map> _active_requests; +};