diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c38f5e3..ced3b92d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,6 +161,11 @@ FetchContent_Declare( # (gzip/zlib is enough for the broker), so disable it to avoid linking libbrotli. SET(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF CACHE BOOL "" FORCE) +# httplib auto-enables HTTPS by linking system OpenSSL whenever it finds it. The broker serves +# plain HTTP only (TLS, if ever wanted, is terminated by a front reverse proxy), so disable it -- +# otherwise jfjoch_broker would pull in libssl/libcrypto for no reason. +SET(HTTPLIB_USE_OPENSSL_IF_AVAILABLE OFF CACHE BOOL "" FORCE) + # ZeroMQ (libzmq): fetch it here, at the top level, so that THIS project - not # slsDetectorPackage - controls the version. slsDetectorPackage bundles its own libzmq # archive, but only populates it behind `if(NOT libzmq_POPULATED)`; by making libzmq @@ -264,6 +269,35 @@ IF (WIN32) TARGET_INCLUDE_DIRECTORIES(wingetopt PUBLIC tools/wingetopt) ENDIF() +# libcurl: the jfjoch_viewer HTTP client (viewer/JFJochHttpReader). Fetched and STATICALLY linked +# ONLY for viewer builds, so the broker/writer never pull in a TLS/Kerberos stack. Backends are +# OS-native per platform to keep the Windows build self-contained: Schannel (TLS) + SSPI +# (Negotiate/Kerberos) on Windows -- no external OpenSSL/krb5 -- and system OpenSSL + GSSAPI (krb5) +# on Linux. Trimmed to HTTP(S) only, mirroring the libtiff codec pruning above. +IF (JFJOCH_VIEWER_BUILD OR JFJOCH_VIEWER_ONLY) + SET(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE) + SET(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) # static libcurl only, never a shared lib + SET(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE) + SET(BUILD_TESTING OFF CACHE BOOL "" FORCE) + SET(CURL_DISABLE_INSTALL ON CACHE BOOL "" FORCE) + SET(HTTP_ONLY ON CACHE BOOL "" FORCE) # HTTP/HTTPS only; drop FTP/LDAP/SMTP/... + SET(CURL_USE_LIBPSL OFF CACHE BOOL "" FORCE) # avoid the libpsl dependency + SET(CURL_USE_LIBSSH2 OFF CACHE BOOL "" FORCE) + SET(CURL_ZLIB ON CACHE BOOL "" FORCE) # reuse the project's ZLIB + IF (WIN32) + SET(CURL_USE_SCHANNEL ON CACHE BOOL "" FORCE) # native Windows TLS (no OpenSSL) + SET(CURL_USE_SSPI ON CACHE BOOL "" FORCE) # native Negotiate/Kerberos (no krb5) + ELSE() + SET(CURL_USE_OPENSSL ON CACHE BOOL "" FORCE) + SET(CURL_USE_GSSAPI ON CACHE BOOL "" FORCE) # Kerberos via system krb5 (needs krb5-dev) + ENDIF() + FetchContent_Declare(curl + GIT_REPOSITORY https://github.com/curl/curl.git + GIT_TAG curl-8_11_1 + EXCLUDE_FROM_ALL) + FetchContent_MakeAvailable(curl) +ENDIF() + IF (JFJOCH_VIEWER_ONLY) # Minimal subtree: jfjoch_viewer and only the libraries it transitively links. # (broker here provides JFJochAPI only; its service targets are gated out.) diff --git a/docker/rocky8/Dockerfile b/docker/rocky8/Dockerfile index 88089eaa..c2b1fabe 100644 --- a/docker/rocky8/Dockerfile +++ b/docker/rocky8/Dockerfile @@ -51,6 +51,7 @@ RUN dnf -y update && \ mesa-libGL-devel \ mesa-libEGL-devel \ dbus-devel \ + krb5-devel \ zlib-devel \ zlib-static \ glib2-devel \ diff --git a/docker/rocky9/Dockerfile b/docker/rocky9/Dockerfile index 6aa1d5d0..32c0e94d 100644 --- a/docker/rocky9/Dockerfile +++ b/docker/rocky9/Dockerfile @@ -58,6 +58,7 @@ RUN dnf -y update && \ mesa-libGL-devel \ mesa-libEGL-devel \ dbus-devel \ + krb5-devel \ zlib-devel \ zlib-static \ glib2-devel \ diff --git a/docker/ubuntu2204/Dockerfile b/docker/ubuntu2204/Dockerfile index 9e34c8e3..7dbdb35a 100644 --- a/docker/ubuntu2204/Dockerfile +++ b/docker/ubuntu2204/Dockerfile @@ -41,6 +41,7 @@ RUN set -eux; \ dpkg-dev \ fakeroot \ libnuma-dev \ + libkrb5-dev \ libxcb1-dev \ libx11-dev \ libxext-dev \ diff --git a/docker/ubuntu2404/Dockerfile b/docker/ubuntu2404/Dockerfile index 8fb63a26..796ee67b 100644 --- a/docker/ubuntu2404/Dockerfile +++ b/docker/ubuntu2404/Dockerfile @@ -41,6 +41,7 @@ RUN set -eux; \ dpkg-dev \ fakeroot \ libnuma-dev \ + libkrb5-dev \ libxcb1-dev \ libx11-dev \ libxext-dev \ diff --git a/reader/CMakeLists.txt b/reader/CMakeLists.txt index c3377b70..a5168f18 100644 --- a/reader/CMakeLists.txt +++ b/reader/CMakeLists.txt @@ -12,11 +12,12 @@ ADD_LIBRARY(JFJochReader STATIC JFJochReaderImage.h JFJochReaderDataset.cpp JFJochReaderDataset.h - JFJochHttpReader.cpp - JFJochHttpReader.h JFJochReaderSpots.h ) +# NB: JFJochHttpReader (the viewer's live-broker HTTP client) lives in viewer/ and links libcurl; +# it is deliberately NOT part of this always-built library, so the broker/writer never pull in a +# TLS/Kerberos stack. TARGET_LINK_LIBRARIES(JFJochReader JFJochImageAnalysis JFJochAPI JFJochCommon JFJochZMQ JFJochLogger JFJochHDF5Wrappers CBORStream2FrameSerialize - httplib::httplib ${CMAKE_DL_LIBS}) + ${CMAKE_DL_LIBS}) diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index c4efdd09..a606ba3a 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -29,6 +29,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView image_viewer/JFJochSimpleImage.h JFJochImageReadingWorker.cpp JFJochImageReadingWorker.h + JFJochHttpReader.cpp + JFJochHttpReader.h RunData.h JFJochProcessController.cpp JFJochProcessController.h @@ -111,7 +113,7 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView TARGET_LINK_LIBRARIES(jfjoch_viewer Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Charts Qt6::Concurrent Qt6::OpenGL Qt6::OpenGLWidgets JFJochReader JFJochLogger JFJochCommon JFJochWriter JFJochImageAnalysis Rugnux - fftw3f) + fftw3f CURL::libcurl) # Native GUI app per platform: a .app bundle on macOS, GUI subsystem (no console window) on # Windows. Each property is ignored on the platforms it does not apply to, so set both always. diff --git a/reader/JFJochHttpReader.cpp b/viewer/JFJochHttpReader.cpp similarity index 80% rename from reader/JFJochHttpReader.cpp rename to viewer/JFJochHttpReader.cpp index c4e1d782..6cc7f82f 100644 --- a/reader/JFJochHttpReader.cpp +++ b/viewer/JFJochHttpReader.cpp @@ -3,7 +3,9 @@ #include "JFJochHttpReader.h" -#include +#include +#include +#include #include #include "../frame_serialize/CBORStream2Deserializer.h" #include "../broker/gen/model/Image_buffer_status.h" @@ -13,6 +15,60 @@ #include "../common/JFJochMath.h" #include "../image_analysis/bragg_integration/CalcISigma.h" +namespace { + // libcurl write callback: append the received bytes to a std::string (which holds binary fine). + size_t AppendToString(char *ptr, size_t size, size_t nmemb, void *userdata) { + auto *out = static_cast(userdata); + out->append(ptr, size * nmemb); + return size * nmemb; + } + + // libcurl needs a one-time global init before any easy handle is used; do it once and + // thread-safely, as the viewer drives the reader from a worker thread. + void EnsureCurlGlobalInit() { + static std::once_flag once; + std::call_once(once, [] { curl_global_init(CURL_GLOBAL_DEFAULT); }); + } +} + +JFJochHttpReader::HttpResult JFJochHttpReader::Request(const std::string &method, const std::string &path, + const std::string &body, + const std::string &content_type) const { + EnsureCurlGlobalInit(); + + CURL *curl = curl_easy_init(); + if (curl == nullptr) + throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Could not initialize CURL"); + + HttpResult result; + const std::string url = addr + path; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, AppendToString); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result.body); + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); // safe to use from worker threads + + curl_slist *headers = nullptr; + if (method == "PUT") { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.data()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); + if (!content_type.empty()) + headers = curl_slist_append(headers, ("Content-Type: " + content_type).c_str()); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + } + + if (curl_easy_perform(curl) == CURLE_OK) { + result.ok = true; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &result.status); + } + + if (headers != nullptr) + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + return result; +} + void JFJochHttpReader::Close() { std::unique_lock ul(http_mutex); addr = ""; @@ -24,15 +80,13 @@ void JFJochHttpReader::Close() { } ImageBufferStatus JFJochHttpReader::GetImageBufferStatus() const { - httplib::Client cli_cmd(addr); - - auto res = cli_cmd.Get("/image_buffer/status"); - if (!res || res->status != httplib::StatusCode::OK_200) + auto res = Request("GET", "/image_buffer/status"); + if (!res.ok || res.status != 200) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Could not get image buffer status"); try { - org::openapitools::server::model::Image_buffer_status status = nlohmann::json::parse(res->body); + org::openapitools::server::model::Image_buffer_status status = nlohmann::json::parse(res.body); ImageBufferStatus ret{}; ret.max_image_number = status.getMaxImageNumber(); ret.min_image_number = status.getMinImageNumber(); @@ -49,15 +103,13 @@ ImageBufferStatus JFJochHttpReader::GetImageBufferStatus() const { } BrokerStatus JFJochHttpReader::GetBrokerStatus() const { - httplib::Client cli_cmd(addr); - - auto res = cli_cmd.Get("/status"); - if (!res || res->status != httplib::StatusCode::OK_200) + auto res = Request("GET", "/status"); + if (!res.ok || res.status != 200) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Could not get broker status"); try { - org::openapitools::server::model::Broker_status input = nlohmann::json::parse(res->body); + org::openapitools::server::model::Broker_status input = nlohmann::json::parse(res.body); BrokerStatus ret{}; ret.broker_version = input.getBrokerVersion(); ret.gpu_count = input.getGpuCount(); @@ -107,22 +159,19 @@ uint64_t JFJochHttpReader::GetNumberOfImages() const { } std::shared_ptr JFJochHttpReader::UpdateDataset_i() { - httplib::Client cli_cmd(addr); + auto res = Request("GET", "/image_buffer/start.cbor"); - auto res = cli_cmd.Get("/image_buffer/start.cbor"); - - if (!res || (res->status != httplib::StatusCode::OK_200 - && res->status != httplib::StatusCode::NotFound_404)) + if (!res.ok || (res.status != 200 && res.status != 404)) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Could not get image buffer status"); - if (res->status == httplib::StatusCode::NotFound_404) + if (res.status == 404) return {}; - if (res->body.empty()) + if (res.body.empty()) return {}; try { - auto msg = CBORStream2Deserialize(res->body); + auto msg = CBORStream2Deserialize(res.body); if (msg->msg_type != CBORImageType::START) return {}; @@ -259,8 +308,6 @@ bool JFJochHttpReader::LoadImage_i(std::shared_ptr &dataset if (addr.empty()) return false; - httplib::Client cli_cmd(addr); - bool buffer_changed = false; // For autoupdate - if buffer didn't change don't update dataset @@ -285,18 +332,18 @@ bool JFJochHttpReader::LoadImage_i(std::shared_ptr &dataset if (!dataset) return false; - auto res = cli_cmd.Get("/image_buffer/image.cbor?id=" + std::to_string(image_number)); + auto res = Request("GET", "/image_buffer/image.cbor?id=" + std::to_string(image_number)); - if (!res || res->status != httplib::StatusCode::OK_200) + if (!res.ok || res.status != 200) return false; - if (res->body.empty()) { + if (res.body.empty()) { return false; } try { - buffer.resize(res->body.size()); - memcpy(buffer.data(), res->body.data(), res->body.size()); + buffer.resize(res.body.size()); + memcpy(buffer.data(), res.body.data(), res.body.size()); auto msg = CBORStream2Deserialize(buffer); @@ -318,15 +365,14 @@ std::shared_ptr JFJochHttpReader::GetRawImage(int64_t imag if (addr.empty()) return {}; - httplib::Client cli_cmd(addr); - auto res = cli_cmd.Get("/image_buffer/image.cbor?id=" + std::to_string(image_number)); + auto res = Request("GET", "/image_buffer/image.cbor?id=" + std::to_string(image_number)); - if (!res || res->status != httplib::StatusCode::OK_200 || res->body.empty()) + if (!res.ok || res.status != 200 || res.body.empty()) return {}; try { auto msg = - CBORStream2Deserialize(reinterpret_cast(res->body.data()), res->body.size()); + CBORStream2Deserialize(reinterpret_cast(res.body.data()), res.body.size()); if (msg->msg_type != CBORImageType::IMAGE) return {}; @@ -355,26 +401,24 @@ std::vector JFJochHttpReader::GetPlot_i(const std::string &plot_type, flo if (addr.empty()) return {}; - httplib::Client cli_cmd(addr); - - auto res_bin = cli_cmd.Get("/preview/plot.bin?type=" + plot_type); - if (res_bin && res_bin->status == httplib::StatusCode::OK_200) { - if (res_bin->body.size() % sizeof(float) != 0) { + auto res_bin = Request("GET", "/preview/plot.bin?type=" + plot_type); + if (res_bin.ok && res_bin.status == 200) { + if (res_bin.body.size() % sizeof(float) != 0) { throw std::runtime_error("Input size is not a multiple of sizeof(float)"); } - std::vector v(res_bin->body.size() / sizeof(float)); - std::memcpy(v.data(), res_bin->body.data(), res_bin->body.size()); + std::vector v(res_bin.body.size() / sizeof(float)); + std::memcpy(v.data(), res_bin.body.data(), res_bin.body.size()); return v; } - auto res = cli_cmd.Get("/preview/plot?binning=1&experimental_coord=false&fill=" - + std::to_string(fill_value) + "&type=" + plot_type); + auto res = Request("GET", "/preview/plot?binning=1&experimental_coord=false&fill=" + + std::to_string(fill_value) + "&type=" + plot_type); - if (!res || res->status != httplib::StatusCode::OK_200) + if (!res.ok || res.status != 200) return {}; try { - org::openapitools::server::model::Plots plots = nlohmann::json::parse(res->body); + org::openapitools::server::model::Plots plots = nlohmann::json::parse(res.body); auto plot_v = plots.getPlot(); if (plot_v.size() == 1) return plot_v[0].getY(); @@ -397,21 +441,18 @@ void JFJochHttpReader::UploadUserMask(const std::vector& mask) { throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "User mask is empty."); - httplib::Client cli_cmd(addr); - const char* data_ptr = reinterpret_cast(mask.data()); const size_t byte_size = mask.size() * sizeof(uint32_t); - auto res = cli_cmd.Put("/config/user_mask", - data_ptr, - byte_size, - "application/octet-stream"); + auto res = Request("PUT", "/config/user_mask", + std::string(data_ptr, byte_size), + "application/octet-stream"); - if (!res) + if (!res.ok) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Failed to connect to server to upload user mask"); - if (res->status != httplib::StatusCode::OK_200) + if (res.status != 200) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Server rejected user mask upload"); } @@ -421,12 +462,11 @@ ROIDefinition JFJochHttpReader::GetROIDefinitions() const { if (addr.empty()) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "HTTP address not set"); - httplib::Client cli_cmd(addr); - auto res = cli_cmd.Get("/config/roi"); - if (!res || res->status != httplib::StatusCode::OK_200) + auto res = Request("GET", "/config/roi"); + if (!res.ok || res.status != 200) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Could not get ROI definitions"); - org::openapitools::server::model::Roi_definitions input = nlohmann::json::parse(res->body); + org::openapitools::server::model::Roi_definitions input = nlohmann::json::parse(res.body); ROIDefinition out; for (const auto &i : input.getBox().getRois()) out.boxes.emplace_back(i.getName(), i.getMinXPxl(), i.getMaxXPxl(), i.getMinYPxl(), i.getMaxYPxl()); @@ -494,12 +534,11 @@ void JFJochHttpReader::UploadROIDefinitions(const ROIDefinition &rois) const { out.setAzim(al); nlohmann::json j = out; - httplib::Client cli_cmd(addr); - auto res = cli_cmd.Put("/config/roi", j.dump(), "application/json"); - if (!res) + auto res = Request("PUT", "/config/roi", j.dump(), "application/json"); + if (!res.ok) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Failed to connect to server to upload ROIs"); - if (res->status != httplib::StatusCode::OK_200) + if (res.status != 200) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Server rejected ROI upload"); } @@ -513,14 +552,13 @@ std::vector JFJochHttpReader::ReadSpots(int64_t image) const { if (addr.empty()) return {}; - httplib::Client cli_cmd(addr); - auto res = cli_cmd.Get("/image_buffer/image.cbor?id=" + std::to_string(image)); + auto res = Request("GET", "/image_buffer/image.cbor?id=" + std::to_string(image)); - if (!res || res->status != httplib::StatusCode::OK_200 || res->body.empty()) + if (!res.ok || res.status != 200 || res.body.empty()) return {}; try { - auto msg = CBORStream2Deserialize(res->body); + auto msg = CBORStream2Deserialize(res.body); if (msg->msg_type != CBORImageType::IMAGE) return {}; diff --git a/reader/JFJochHttpReader.h b/viewer/JFJochHttpReader.h similarity index 77% rename from reader/JFJochHttpReader.h rename to viewer/JFJochHttpReader.h index e851d49b..dfaf27d0 100644 --- a/reader/JFJochHttpReader.h +++ b/viewer/JFJochHttpReader.h @@ -3,7 +3,7 @@ #pragma once -#include "JFJochReader.h" +#include "../reader/JFJochReader.h" #include "../common/BrokerStatus.h" #include "../common/ImageBuffer.h" #include "../common/ROIDefinition.h" @@ -20,6 +20,18 @@ class JFJochHttpReader : public JFJochReader { std::shared_ptr cached_pixel_mask; std::string cached_pixel_mask_arm_date; + // Minimal HTTP result: whether a response arrived (transport-level success), the HTTP status + // code, and the (possibly binary) response body. + struct HttpResult { + bool ok = false; + long status = 0; + std::string body; + }; + // Single libcurl request helper behind every endpoint call. method is "GET" or "PUT"; for PUT + // the body is sent with the given content_type. + HttpResult Request(const std::string &method, const std::string &path, + const std::string &body = {}, const std::string &content_type = {}) const; + ImageBufferStatus GetImageBufferStatus() const; bool LoadImage_i(std::shared_ptr &dataset, diff --git a/viewer/JFJochImageReadingWorker.h b/viewer/JFJochImageReadingWorker.h index 53a38be0..be9c5cb8 100644 --- a/viewer/JFJochImageReadingWorker.h +++ b/viewer/JFJochImageReadingWorker.h @@ -17,7 +17,7 @@ #include "../reader/JFJochHDF5Reader.h" #include "../common/Logger.h" -#include "../reader/JFJochHttpReader.h" +#include "JFJochHttpReader.h" #include "../image_analysis/MXAnalysisWithoutFPGA.h" #include "../common/BraggIntegrationSettings.h" #include "../common/ScalingSettings.h"