viewer: move JFJochHttpReader to the viewer and switch it to libcurl

JFJochHttpReader (the viewer's live-broker HTTP client) is the only HTTP client
in the reader library and the only piece that needs TLS + Kerberos. Moving it out
of the always-built JFJochReader keeps the broker/writer free of a TLS/Kerberos
stack; switching it from cpp-httplib to libcurl gives the viewer HTTPS and, later,
GSSAPI/Bearer auth.

- Move reader/JFJochHttpReader.{cpp,h} -> viewer/, compiled into jfjoch_viewer.
- Rewrite the 10 httplib call sites onto one libcurl easy-handle Request() helper
  (GET + PUT, binary/JSON bodies; status/transport handling preserved).
- CMake: FetchContent libcurl, STATIC, HTTP(S)-only, only for viewer builds.
  OS-native backends keep Windows self-contained (Schannel+SSPI); Linux uses
  system OpenSSL + GSSAPI (krb5). Drop httplib from JFJochReader.
- Block httplib OpenSSL auto-link (HTTPLIB_USE_OPENSSL_IF_AVAILABLE OFF) so the
  broker stops pulling in libssl/libcrypto for HTTPS it never serves.
- docker: add krb5 dev headers (libkrb5-dev / krb5-devel) for CURL_USE_GSSAPI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 15:19:58 +02:00
co-authored by Claude Opus 4.8
parent efd5f3513a
commit 49d078563c
10 changed files with 158 additions and 67 deletions
+34
View File
@@ -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.)
+1
View File
@@ -51,6 +51,7 @@ RUN dnf -y update && \
mesa-libGL-devel \
mesa-libEGL-devel \
dbus-devel \
krb5-devel \
zlib-devel \
zlib-static \
glib2-devel \
+1
View File
@@ -58,6 +58,7 @@ RUN dnf -y update && \
mesa-libGL-devel \
mesa-libEGL-devel \
dbus-devel \
krb5-devel \
zlib-devel \
zlib-static \
glib2-devel \
+1
View File
@@ -41,6 +41,7 @@ RUN set -eux; \
dpkg-dev \
fakeroot \
libnuma-dev \
libkrb5-dev \
libxcb1-dev \
libx11-dev \
libxext-dev \
+1
View File
@@ -41,6 +41,7 @@ RUN set -eux; \
dpkg-dev \
fakeroot \
libnuma-dev \
libkrb5-dev \
libxcb1-dev \
libx11-dev \
libxext-dev \
+4 -3
View File
@@ -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})
+3 -1
View File
@@ -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.
@@ -3,7 +3,9 @@
#include "JFJochHttpReader.h"
#include <httplib.h>
#include <curl/curl.h>
#include <cstring>
#include <mutex>
#include <nlohmann/json.hpp>
#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<std::string *>(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<long>(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<JFJochReaderDataset> 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<JFJochReaderDataset> &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<JFJochReaderDataset> &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<JFJochReaderRawImage> 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<uint8_t *>(res->body.data()), res->body.size());
CBORStream2Deserialize(reinterpret_cast<uint8_t *>(res.body.data()), res.body.size());
if (msg->msg_type != CBORImageType::IMAGE)
return {};
@@ -355,26 +401,24 @@ std::vector<float> 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<float> v(res_bin->body.size() / sizeof(float));
std::memcpy(v.data(), res_bin->body.data(), res_bin->body.size());
std::vector<float> 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<uint32_t>& mask) {
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"User mask is empty.");
httplib::Client cli_cmd(addr);
const char* data_ptr = reinterpret_cast<const char*>(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<SpotToSave> 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 {};
@@ -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<const PixelMask> 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<JFJochReaderDataset> &dataset,
+1 -1
View File
@@ -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"