From 273a6ec1f34cb2aaf9f9e4f953f5d3e2c882cb82 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Thu, 9 Jan 2025 20:51:48 +0100 Subject: [PATCH 01/20] serializer/broker: Expose to_broker_event() and to_zeek_event() This is useful for reuse by WebSocket clients that use the JSON v1 encoding. --- src/cluster/serializer/broker/Serializer.cc | 22 ++------------------- src/cluster/serializer/broker/Serializer.h | 22 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/cluster/serializer/broker/Serializer.cc b/src/cluster/serializer/broker/Serializer.cc index 11a7ed86c0..f2308cb627 100644 --- a/src/cluster/serializer/broker/Serializer.cc +++ b/src/cluster/serializer/broker/Serializer.cc @@ -20,15 +20,7 @@ using namespace zeek::cluster; -namespace { - -/** - * Convert a cluster::detail::Event to a broker::zeek::Event. - * - * @param ev The cluster::detail::Event - * @return A broker::zeek::Event to be serialized, or nullopt in case of errors. - */ -std::optional to_broker_event(const detail::Event& ev) { +std::optional detail::to_broker_event(const detail::Event& ev) { broker::vector xs; xs.reserve(ev.args.size()); @@ -51,15 +43,7 @@ std::optional to_broker_event(const detail::Event& ev) { return broker::zeek::Event(ev.HandlerName(), xs, broker::to_timestamp(ev.timestamp)); } -/** - * Convert a broker::zeek::Event to cluster::detail::Event by looking - * it up in Zeek's event handler registry and converting event arguments - * to the appropriate Val instances. - * - * @param broker_ev The broker side event. - * @returns A zeek::cluster::detail::Event instance, or std::nullopt if the conversion failed. - */ -std::optional to_zeek_event(const broker::zeek::Event& ev) { +std::optional detail::to_zeek_event(const broker::zeek::Event& ev) { auto&& name = ev.name(); auto&& args = ev.args(); @@ -117,8 +101,6 @@ std::optional to_zeek_event(const broker::zeek::Event& ev) { return detail::Event{handler, std::move(vl), ts}; } -} // namespace - bool detail::BrokerBinV1_Serializer::SerializeEvent(detail::byte_buffer& buf, const detail::Event& event) { auto ev = to_broker_event(event); if ( ! ev ) diff --git a/src/cluster/serializer/broker/Serializer.h b/src/cluster/serializer/broker/Serializer.h index 00973b90c1..4b9adae241 100644 --- a/src/cluster/serializer/broker/Serializer.h +++ b/src/cluster/serializer/broker/Serializer.h @@ -4,8 +4,30 @@ #include "zeek/cluster/Serializer.h" +namespace broker::zeek { +class Event; +} + namespace zeek::cluster::detail { +/** + * Convert a broker::zeek::Event to cluster::detail::Event by looking + * it up in Zeek's event handler registry and converting event arguments + * to the appropriate Val instances. + * + * @param ev The broker side event. + * @returns A zeek::cluster::detail::Event instance, or std::nullopt if the conversion failed. + */ +std::optional to_zeek_event(const broker::zeek::Event& ev); + +/** + * Convert a cluster::detail::Event to a broker::zeek::Event. + * + * @param ev The cluster::detail::Event + * @return A broker::zeek::Event to be serialized, or nullopt in case of errors. + */ +std::optional to_broker_event(const detail::Event& ev); + // Implementation of the EventSerializer using the existing broker::detail::val_to_data() // and broker::format::bin::v1::encode(). class BrokerBinV1_Serializer : public EventSerializer { From 5dee77e6f21e158564a68afdfb21d482de70cfe3 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Tue, 21 Jan 2025 17:26:36 +0100 Subject: [PATCH 02/20] cluster/OnLoop: Introduce helper template class --- src/cluster/OnLoop.h | 186 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 src/cluster/OnLoop.h diff --git a/src/cluster/OnLoop.h b/src/cluster/OnLoop.h new file mode 100644 index 0000000000..88edfa21bc --- /dev/null +++ b/src/cluster/OnLoop.h @@ -0,0 +1,186 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +#pragma once + +#include +#include +#include +#include +#include + +#include "zeek/Flare.h" +#include "zeek/Reporter.h" +#include "zeek/iosource/IOSource.h" +#include "zeek/iosource/Manager.h" + +namespace zeek::detail { +/** + * Template class allowing work items to be queued by threads and processed + * in Zeek's main thread. + * + * This is similar to MsgThread::SendOut(), but doesn't require usage of BasicThread + * or MsgThread instances. Some libraries spawn their own threads or invoke callbacks + * from arbitrary threads. OnLoopProcess::QueueForProcessing() can be used to transfer + * work from such callbacks onto Zeek's main thread. + * + * There's currently no explicit way to transfer a result back. If this is needed, + * have the queueing thread block on a semaphore or condition variable and update + * it from Process(). + * + * Note that QueueForProcessing() puts the queueing thread to sleep if there's + * too many items in the queue. + */ +template +class OnLoopProcess : public zeek::iosource::IOSource { +public: + /** + * Constructor. + * + * @param proc The instance processing. + * @param tag The tag to use as the IOSource's tag. + */ + OnLoopProcess(Proc* proc, std::string tag, size_t max_queue_size = 10, + std::chrono::microseconds block_duration = std::chrono::microseconds(100), + std::thread::id main_thread_id = std::this_thread::get_id()) + : max_queue_size(max_queue_size), + block_duration(block_duration), + proc(proc), + tag(std::move(tag)), + main_thread_id(main_thread_id) {} + + /** + * Register this instance with the IO loop. + * + * The IO loop will manage the lifetime of this + * IO source instance. + * + * @param dont_count If false, prevents Zeek from terminating as long as the IO source is open. + */ + void Register(bool dont_count = true) { + zeek::iosource_mgr->Register(this, dont_count, /*manage_lifetime=*/true); + + if ( ! zeek::iosource_mgr->RegisterFd(flare.FD(), this) ) + zeek::reporter->InternalError("Failed to register IO source FD %d for OnLoopProcess %s", flare.FD(), + tag.c_str()); + } + + /** + * Close the IO source. + */ + void Close() { + zeek::iosource_mgr->UnregisterFd(flare.FD(), this); + + { + // Close under lock to guarantee visibility for + // any pending queuers QueueForProcessing() calls. + std::scoped_lock lock(mtx); + SetClosed(true); + + // Don't attempt to Process anymore. + proc = nullptr; + } + + // Wait for any active queuers to vanish, should be quick. + while ( queuers > 0 ) + std::this_thread::sleep_for(std::chrono::microseconds(10)); + } + + /** + * Implements IOSource::Process() + * + * Runs in Zeek's main thread, invoked by the IO loop. + */ + void Process() override { + std::list to_process; + { + std::scoped_lock lock(mtx); + to_process.splice(to_process.end(), queue); + flare.Extinguish(); + } + + // We've been closed, so proc will most likely + // be invalid at this point and we'll discard + // whatever was left to do. + if ( ! IsOpen() ) + return; + + for ( auto& work : to_process ) + proc->Process(std::move(work)); + } + + /** + * Implements IOSource::Tag() + */ + const char* Tag() override { return tag.c_str(); } + + /** + * Implements IOSource::GetNextTimeout() + */ + double GetNextTimeout() override { return -1; }; + + /** + * Queue the given Work item to be processed on Zeek's main thread. + * + * If there's too many items in the queue, this method sleeps using + * std::this_thread::sleep() for the *block_duration* passed to the + * constructor. + * + * Calling this method from the main thread will result in an abort(). + */ + void QueueForProcessing(Work&& work) { + ++queuers; + std::list to_queue{std::move(work)}; + + if ( std::this_thread::get_id() == main_thread_id ) { + fprintf(stderr, "OnLoopProcess::QueueForProcessing() called by main thread!"); + abort(); + } + + bool fire = false; + size_t qs = 0; + + while ( ! to_queue.empty() ) { + { + std::scoped_lock lock(mtx); + + if ( ! IsOpen() ) { + // IO Source is being removed. + fire = false; + break; + } + + qs = queue.size(); + if ( qs < max_queue_size ) { + queue.splice(queue.end(), to_queue); + fire = fire || qs == 0; + assert(to_queue.empty()); + assert(! queue.empty()); + } + } + + if ( ! to_queue.empty() ) { + std::this_thread::sleep_for(block_duration); + fire = true; + } + } + + if ( fire ) + flare.Fire(); + + --queuers; + } + +private: + zeek::detail::Flare flare; + std::mutex mtx; + std::list queue; + size_t max_queue_size; + std::chrono::microseconds block_duration; + Proc* proc; + std::string tag; + std::atomic queuers = 0; + std::thread::id main_thread_id; +}; + + +} // namespace zeek::detail From 23405194a0fd750e5eca61f3afe0c3d149ff737f Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Tue, 21 Jan 2025 18:46:58 +0100 Subject: [PATCH 03/20] cluster/ThreadedBackend: Switch to OnLoopProcess --- src/cluster/Backend.cc | 61 +++++++++--------------------------------- src/cluster/Backend.h | 46 ++++++++++++++++--------------- 2 files changed, 36 insertions(+), 71 deletions(-) diff --git a/src/cluster/Backend.cc b/src/cluster/Backend.cc index 177997238b..e46d989476 100644 --- a/src/cluster/Backend.cc +++ b/src/cluster/Backend.cc @@ -10,8 +10,8 @@ #include "zeek/Func.h" #include "zeek/Reporter.h" #include "zeek/Type.h" +#include "zeek/cluster/OnLoop.h" #include "zeek/cluster/Serializer.h" -#include "zeek/iosource/Manager.h" #include "zeek/logging/Manager.h" #include "zeek/util.h" @@ -158,63 +158,26 @@ bool ThreadedBackend::ProcessBackendMessage(int tag, detail::byte_buffer_span pa return DoProcessBackendMessage(tag, payload); } -namespace { - -bool register_io_source(zeek::iosource::IOSource* src, int fd, bool dont_count) { - constexpr bool manage_lifetime = true; - - zeek::iosource_mgr->Register(src, dont_count, manage_lifetime); - - if ( ! zeek::iosource_mgr->RegisterFd(fd, src) ) { - zeek::reporter->Error("Failed to register messages_flare with IO manager"); - return false; - } - - return true; +ThreadedBackend::ThreadedBackend(std::unique_ptr es, std::unique_ptr ls, + std::unique_ptr ehs) + : Backend(std::move(es), std::move(ls), std::move(ehs)) { + onloop = new zeek::detail::OnLoopProcess(this, "ThreadedBackend"); + onloop->Register(true); // Register as don't count first } -} // namespace bool ThreadedBackend::DoInit() { - // Register as counting during DoInit() to avoid Zeek from shutting down. - return register_io_source(this, messages_flare.FD(), false); -} - -void ThreadedBackend::DoInitPostScript() { - // Register non-counting after parsing scripts. - register_io_source(this, messages_flare.FD(), true); + // Have the backend count so Zeek does not terminate. + onloop->Register(/*dont_count=*/false); + return true; } void ThreadedBackend::QueueForProcessing(QueueMessages&& qmessages) { - bool fire = false; - - // Enqueue under lock. - { - std::scoped_lock lock(messages_mtx); - fire = messages.empty(); - - if ( messages.empty() ) { - messages = std::move(qmessages); - } - else { - messages.reserve(messages.size() + qmessages.size()); - for ( auto& qmsg : qmessages ) - messages.emplace_back(std::move(qmsg)); - } - } - - if ( fire ) - messages_flare.Fire(); + onloop->QueueForProcessing(std::move(qmessages)); } -void ThreadedBackend::Process() { - QueueMessages to_process; - { - std::scoped_lock lock(messages_mtx); - to_process = std::move(messages); - messages_flare.Extinguish(); - messages.clear(); - } +void ThreadedBackend::Process() { onloop->Process(); } +void ThreadedBackend::Process(QueueMessages&& to_process) { for ( const auto& msg : to_process ) { // sonarlint wants to use std::visit. not sure... if ( auto* emsg = std::get_if(&msg) ) { diff --git a/src/cluster/Backend.h b/src/cluster/Backend.h index 5953c7b8b2..2feadb89a6 100644 --- a/src/cluster/Backend.h +++ b/src/cluster/Backend.h @@ -5,17 +5,14 @@ #pragma once #include -#include #include #include #include #include "zeek/EventHandler.h" -#include "zeek/Flare.h" #include "zeek/IntrusivePtr.h" #include "zeek/Span.h" #include "zeek/cluster/Serializer.h" -#include "zeek/iosource/IOSource.h" #include "zeek/logging/Types.h" namespace zeek { @@ -28,6 +25,11 @@ class Val; using ValPtr = IntrusivePtr; using ArgsSpan = Span; +namespace detail { +template +class OnLoopProcess; +} + namespace cluster { namespace detail { @@ -465,11 +467,14 @@ using QueueMessages = std::vector; * Support for backends that use background threads or invoke * callbacks on non-main threads. */ -class ThreadedBackend : public Backend, public zeek::iosource::IOSource { -public: - using Backend::Backend; - +class ThreadedBackend : public Backend { protected: + /** + * Constructor. + */ + ThreadedBackend(std::unique_ptr es, std::unique_ptr ls, + std::unique_ptr ehs); + /** * To be used by implementations to enqueue messages for processing on the IO loop. * @@ -479,20 +484,11 @@ protected: */ void QueueForProcessing(QueueMessages&& messages); - void Process() override; - - double GetNextTimeout() override { return -1; } - /** - * The DoInitPostScript() implementation of ThreadedBackend - * registers itself as a non-counting IO source. - * - * Classes deriving from ThreadedBackend and providing their - * own DoInitPostScript() method should invoke the ThreadedBackend's - * implementation to register themselves as a non-counting - * IO source with the IO loop. + * Delegate to onloop->Process() to trigger processing + * of outstanding queued messages explicitly, if any. */ - void DoInitPostScript() override; + void Process(); /** * The default DoInit() implementation of ThreadedBackend @@ -518,10 +514,16 @@ private: */ virtual bool DoProcessBackendMessage(int tag, detail::byte_buffer_span payload) { return false; }; + /** + * Hook method for OnLooProcess. + */ + void Process(QueueMessages&& messages); + + // Allow access to Process(QueueMessages) + friend class zeek::detail::OnLoopProcess; + // Members used for communication with the main thread. - std::mutex messages_mtx; - std::vector messages; - zeek::detail::Flare messages_flare; + zeek::detail::OnLoopProcess* onloop; }; From 827eccb732362911bfe7182c0dd01044e23be600 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Tue, 21 Jan 2025 18:52:49 +0100 Subject: [PATCH 04/20] cluster/zeromq: Adapt for OnLoopProcess changes --- src/cluster/backend/zeromq/ZeroMQ.cc | 4 +--- src/cluster/backend/zeromq/ZeroMQ.h | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/cluster/backend/zeromq/ZeroMQ.cc b/src/cluster/backend/zeromq/ZeroMQ.cc index 99207e82f5..2b739c905b 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.cc +++ b/src/cluster/backend/zeromq/ZeroMQ.cc @@ -77,8 +77,6 @@ ZeroMQBackend::~ZeroMQBackend() { } void ZeroMQBackend::DoInitPostScript() { - ThreadedBackend::DoInitPostScript(); - listen_xpub_endpoint = zeek::id::find_val("Cluster::Backend::ZeroMQ::listen_xpub_endpoint")->ToStdString(); listen_xsub_endpoint = @@ -98,8 +96,8 @@ void ZeroMQBackend::DoInitPostScript() { event_subscription = zeek::event_registry->Register("Cluster::Backend::ZeroMQ::subscription"); } - void ZeroMQBackend::DoTerminate() { + ThreadedBackend::DoTerminate(); ZEROMQ_DEBUG("Shutting down ctx"); ctx.shutdown(); ZEROMQ_DEBUG("Joining self_thread"); diff --git a/src/cluster/backend/zeromq/ZeroMQ.h b/src/cluster/backend/zeromq/ZeroMQ.h index 3a6783ff5c..ab33917cb0 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.h +++ b/src/cluster/backend/zeromq/ZeroMQ.h @@ -62,8 +62,6 @@ private: bool DoPublishLogWrites(const logging::detail::LogWriteHeader& header, const std::string& format, cluster::detail::byte_buffer& buf) override; - const char* Tag() override { return "ZeroMQ"; } - bool DoProcessBackendMessage(int tag, detail::byte_buffer_span payload) override; // Script level variables. From 09ccb2e2502caaa5c73f456f9bacdaf9c7c6b0cb Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Wed, 22 Jan 2025 12:09:32 +0100 Subject: [PATCH 05/20] cluster/Backend: Queue a single message only The ZeroMQ backend would accumulate multiple messages and enqueue them all at once. However, as this could potentially result in huge batches of events being queued into the event loop at once, switch to a one message at a time model. If there's too many messages queued already, OnLoop::QueueForProcessing() will block the ZeroMQ thread until there's room available again. --- src/cluster/Backend.cc | 53 ++++++++++++++++++++++++------------------ src/cluster/Backend.h | 11 +++++---- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/cluster/Backend.cc b/src/cluster/Backend.cc index e46d989476..92d9dd4624 100644 --- a/src/cluster/Backend.cc +++ b/src/cluster/Backend.cc @@ -161,7 +161,7 @@ bool ThreadedBackend::ProcessBackendMessage(int tag, detail::byte_buffer_span pa ThreadedBackend::ThreadedBackend(std::unique_ptr es, std::unique_ptr ls, std::unique_ptr ehs) : Backend(std::move(es), std::move(ls), std::move(ehs)) { - onloop = new zeek::detail::OnLoopProcess(this, "ThreadedBackend"); + onloop = new zeek::detail::OnLoopProcess(this, "ThreadedBackend"); onloop->Register(true); // Register as don't count first } @@ -171,26 +171,35 @@ bool ThreadedBackend::DoInit() { return true; } -void ThreadedBackend::QueueForProcessing(QueueMessages&& qmessages) { - onloop->QueueForProcessing(std::move(qmessages)); -} - -void ThreadedBackend::Process() { onloop->Process(); } - -void ThreadedBackend::Process(QueueMessages&& to_process) { - for ( const auto& msg : to_process ) { - // sonarlint wants to use std::visit. not sure... - if ( auto* emsg = std::get_if(&msg) ) { - ProcessEventMessage(emsg->topic, emsg->format, emsg->payload_span()); - } - else if ( auto* lmsg = std::get_if(&msg) ) { - ProcessLogMessage(lmsg->format, lmsg->payload_span()); - } - else if ( auto* bmsg = std::get_if(&msg) ) { - ProcessBackendMessage(bmsg->tag, bmsg->payload_span()); - } - else { - zeek::reporter->FatalError("Unimplemented QueueMessage %zu", msg.index()); - } +void ThreadedBackend::DoTerminate() { + if ( onloop ) { + onloop->Close(); + onloop = nullptr; + } +} + +void ThreadedBackend::QueueForProcessing(QueueMessage&& qmessages) { + if ( onloop ) + onloop->QueueForProcessing(std::move(qmessages)); +} + +void ThreadedBackend::Process() { + if ( onloop ) + onloop->Process(); +} + +void ThreadedBackend::Process(QueueMessage&& msg) { + // sonarlint wants to use std::visit. not sure... + if ( auto* emsg = std::get_if(&msg) ) { + ProcessEventMessage(emsg->topic, emsg->format, emsg->payload_span()); + } + else if ( auto* lmsg = std::get_if(&msg) ) { + ProcessLogMessage(lmsg->format, lmsg->payload_span()); + } + else if ( auto* bmsg = std::get_if(&msg) ) { + ProcessBackendMessage(bmsg->tag, bmsg->payload_span()); + } + else { + zeek::reporter->FatalError("Unimplemented QueueMessage %zu", msg.index()); } } diff --git a/src/cluster/Backend.h b/src/cluster/Backend.h index 2feadb89a6..794b270eff 100644 --- a/src/cluster/Backend.h +++ b/src/cluster/Backend.h @@ -461,7 +461,6 @@ struct BackendMessage { }; using QueueMessage = std::variant; -using QueueMessages = std::vector; /** * Support for backends that use background threads or invoke @@ -482,7 +481,7 @@ protected: * * @param messages Messages to be enqueued. */ - void QueueForProcessing(QueueMessages&& messages); + void QueueForProcessing(QueueMessage&& messages); /** * Delegate to onloop->Process() to trigger processing @@ -502,6 +501,8 @@ protected: */ bool DoInit() override; + void DoTerminate() override; + private: /** * Process a backend specific message queued as BackendMessage. @@ -517,13 +518,13 @@ private: /** * Hook method for OnLooProcess. */ - void Process(QueueMessages&& messages); + void Process(QueueMessage&& messages); // Allow access to Process(QueueMessages) - friend class zeek::detail::OnLoopProcess; + friend class zeek::detail::OnLoopProcess; // Members used for communication with the main thread. - zeek::detail::OnLoopProcess* onloop; + zeek::detail::OnLoopProcess* onloop = nullptr; }; From 94ec3af2b0d590500adedd0667b85c1f5ed6c520 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Wed, 29 Jan 2025 15:39:26 +0100 Subject: [PATCH 06/20] cluster/zeromq: Queue one message at a time Queueing multiple messages can easily overload the IO loop without creating any backpressure. --- src/cluster/backend/zeromq/ZeroMQ.cc | 31 +++++++++------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/cluster/backend/zeromq/ZeroMQ.cc b/src/cluster/backend/zeromq/ZeroMQ.cc index 2b739c905b..ac70d927cd 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.cc +++ b/src/cluster/backend/zeromq/ZeroMQ.cc @@ -366,9 +366,6 @@ void ZeroMQBackend::Run() { using MultipartMessage = std::vector; auto HandleLogMessages = [this](const std::vector& msgs) { - QueueMessages qmsgs; - qmsgs.reserve(msgs.size()); - for ( const auto& msg : msgs ) { // sender, format, type, payload if ( msg.size() != 4 ) { @@ -377,11 +374,11 @@ void ZeroMQBackend::Run() { } detail::byte_buffer payload{msg[3].data(), msg[3].data() + msg[3].size()}; - qmsgs.emplace_back(LogMessage{.format = std::string(msg[2].data(), msg[2].size()), - .payload = std::move(payload)}); - } + LogMessage lm{.format = std::string(msg[2].data(), msg[2].size()), + .payload = std::move(payload)}; - QueueForProcessing(std::move(qmsgs)); + QueueForProcessing(std::move(lm)); + } }; auto HandleInprocMessages = [this](std::vector& msgs) { @@ -434,9 +431,6 @@ void ZeroMQBackend::Run() { }; auto HandleXPubMessages = [this](const std::vector& msgs) { - QueueMessages qmsgs; - qmsgs.reserve(msgs.size()); - for ( const auto& msg : msgs ) { if ( msg.size() != 1 ) { ZEROMQ_THREAD_PRINTF("xpub: error: expected 1 part, have %zu!\n", msg.size()); @@ -462,17 +456,12 @@ void ZeroMQBackend::Run() { continue; } - qmsgs.emplace_back(std::move(qm)); + QueueForProcessing(std::move(qm)); } } - - QueueForProcessing(std::move(qmsgs)); }; auto HandleXSubMessages = [this](const std::vector& msgs) { - QueueMessages qmsgs; - qmsgs.reserve(msgs.size()); - for ( const auto& msg : msgs ) { if ( msg.size() != 4 ) { ZEROMQ_THREAD_PRINTF("xsub: error: expected 4 parts, have %zu!\n", msg.size()); @@ -485,12 +474,12 @@ void ZeroMQBackend::Run() { continue; detail::byte_buffer payload{msg[3].data(), msg[3].data() + msg[3].size()}; - qmsgs.emplace_back(EventMessage{.topic = std::string(msg[0].data(), msg[0].size()), - .format = std::string(msg[2].data(), msg[2].size()), - .payload = std::move(payload)}); - } + EventMessage em{.topic = std::string(msg[0].data(), msg[0].size()), + .format = std::string(msg[2].data(), msg[2].size()), + .payload = std::move(payload)}; - QueueForProcessing(std::move(qmsgs)); + QueueForProcessing(std::move(em)); + } }; // Helper class running at destruction. From 540d9da5ef27e7cd4a301fe1f12bd507d9006fbd Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Wed, 29 Jan 2025 15:37:43 +0100 Subject: [PATCH 07/20] cluster/zeromq: Handle EINTR at shutdown Read ::signal_val and early exit a DoPublish() in case termination happened while blocked in inproc.send() --- src/cluster/backend/zeromq/ZeroMQ.cc | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/cluster/backend/zeromq/ZeroMQ.cc b/src/cluster/backend/zeromq/ZeroMQ.cc index ac70d927cd..feee788ce8 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.cc +++ b/src/cluster/backend/zeromq/ZeroMQ.cc @@ -25,6 +25,8 @@ #include "zeek/cluster/backend/zeromq/ZeroMQ-Proxy.h" #include "zeek/util.h" +extern int signal_val; + namespace zeek { namespace plugin::Zeek_Cluster_Backend_ZeroMQ { @@ -266,7 +268,21 @@ bool ZeroMQBackend::DoPublishEvent(const std::string& topic, const std::string& // This should never fail, it will instead block // when HWM is reached. I guess we need to see if // and how this can happen :-/ - main_inproc.send(parts[i], flags); + try { + main_inproc.send(parts[i], flags); + } catch ( zmq::error_t& err ) { + // If send() was interrupted and Zeek caught an interrupt or term signal, + // fail the publish as we're about to shutdown. There's nothing the user + // can do, but it indicates an overload situation as send() was blocking. + if ( err.num() == EINTR && (signal_val == SIGINT || signal_val == SIGTERM) ) { + zeek::reporter->Error("Failed publish() using ZeroMQ backend at shutdown: %s (signal_val=%d)", + err.what(), signal_val); + return false; + } + + zeek::reporter->Error("Unexpected ZeroMQ::DoPublishEvent() error: %s", err.what()); + return false; + } } return true; From ba7b605a971a11eb6d765a30342bead211e0dd45 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Wed, 29 Jan 2025 15:35:15 +0100 Subject: [PATCH 08/20] cluster/zeromq: Move variable lookups from DoInit() to DoInitPostScript() --- .../policy/frameworks/cluster/backend/zeromq/main.zeek | 6 +++--- src/cluster/backend/zeromq/ZeroMQ.cc | 9 +++++---- src/cluster/backend/zeromq/ZeroMQ.h | 2 ++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/scripts/policy/frameworks/cluster/backend/zeromq/main.zeek b/scripts/policy/frameworks/cluster/backend/zeromq/main.zeek index a86d150983..24b35603ed 100644 --- a/scripts/policy/frameworks/cluster/backend/zeromq/main.zeek +++ b/scripts/policy/frameworks/cluster/backend/zeromq/main.zeek @@ -134,12 +134,12 @@ export { ## Do not silently drop messages if high-water-mark is reached. ## ## Whether to configure ``ZMQ_XPUB_NODROP`` on the XPUB socket - ## to detect when sending a message fails due to reaching - ## the high-water-mark. + ## connecting to the proxy to detect when sending a message fails + ## due to reaching the high-water-mark. ## ## See ZeroMQ's `ZMQ_XPUB_NODROP documentation `_ ## for more details. - const xpub_nodrop: bool = T &redef; + const connect_xpub_nodrop: bool = T &redef; ## Do not silently drop messages if high-water-mark is reached. ## diff --git a/src/cluster/backend/zeromq/ZeroMQ.cc b/src/cluster/backend/zeromq/ZeroMQ.cc index feee788ce8..45962bbb25 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.cc +++ b/src/cluster/backend/zeromq/ZeroMQ.cc @@ -89,8 +89,12 @@ void ZeroMQBackend::DoInitPostScript() { zeek::id::find_val("Cluster::Backend::ZeroMQ::connect_xpub_endpoint")->ToStdString(); connect_xsub_endpoint = zeek::id::find_val("Cluster::Backend::ZeroMQ::connect_xsub_endpoint")->ToStdString(); + connect_xpub_nodrop = + zeek::id::find_val("Cluster::Backend::ZeroMQ::connect_xpub_nodrop")->AsBool() ? 1 : 0; listen_log_endpoint = zeek::id::find_val("Cluster::Backend::ZeroMQ::listen_log_endpoint")->ToStdString(); + + linger_ms = static_cast(zeek::id::find_val("Cluster::Backend::ZeroMQ::linger_ms")->AsInt()); poll_max_messages = zeek::id::find_val("Cluster::Backend::ZeroMQ::poll_max_messages")->Get(); debug_flags = zeek::id::find_val("Cluster::Backend::ZeroMQ::debug_flags")->Get(); @@ -131,16 +135,13 @@ bool ZeroMQBackend::DoInit() { log_pull = zmq::socket_t(ctx, zmq::socket_type::pull); child_inproc = zmq::socket_t(ctx, zmq::socket_type::pair); - auto linger_ms = static_cast(zeek::id::find_val("Cluster::Backend::ZeroMQ::linger_ms")->AsInt()); - int xpub_nodrop = zeek::id::find_val("Cluster::Backend::ZeroMQ::xpub_nodrop")->AsBool() ? 1 : 0; - xpub.set(zmq::sockopt::linger, linger_ms); - xpub.set(zmq::sockopt::xpub_nodrop, xpub_nodrop); // Enable XPUB_VERBOSE unconditional to enforce nodes receiving // notifications about any new subscriptions, even if they have // seen them before. This is needed to for the subscribe callback // functionality to work reliably. + xpub.set(zmq::sockopt::xpub_nodrop, connect_xpub_nodrop); xpub.set(zmq::sockopt::xpub_verbose, 1); try { diff --git a/src/cluster/backend/zeromq/ZeroMQ.h b/src/cluster/backend/zeromq/ZeroMQ.h index ab33917cb0..a815180b11 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.h +++ b/src/cluster/backend/zeromq/ZeroMQ.h @@ -67,11 +67,13 @@ private: // Script level variables. std::string connect_xsub_endpoint; std::string connect_xpub_endpoint; + int connect_xpub_nodrop = 1; std::string listen_xsub_endpoint; std::string listen_xpub_endpoint; std::string listen_log_endpoint; int listen_xpub_nodrop = 1; + int linger_ms = 0; zeek_uint_t poll_max_messages = 0; zeek_uint_t debug_flags = 0; From aad512c616bbd0157b172f8851eac61e5d339086 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Wed, 22 Jan 2025 17:32:28 +0100 Subject: [PATCH 09/20] cluster/zeromq: Support configuring IO threads for proxy thread --- .../policy/frameworks/cluster/backend/zeromq/main.zeek | 8 ++++++++ src/cluster/backend/zeromq/ZeroMQ-Proxy.cc | 2 ++ src/cluster/backend/zeromq/ZeroMQ-Proxy.h | 8 ++++++-- src/cluster/backend/zeromq/ZeroMQ.cc | 5 ++++- src/cluster/backend/zeromq/ZeroMQ.h | 1 + 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts/policy/frameworks/cluster/backend/zeromq/main.zeek b/scripts/policy/frameworks/cluster/backend/zeromq/main.zeek index 24b35603ed..457f5dbd35 100644 --- a/scripts/policy/frameworks/cluster/backend/zeromq/main.zeek +++ b/scripts/policy/frameworks/cluster/backend/zeromq/main.zeek @@ -55,6 +55,14 @@ export { ## By default, this is set to ``T`` on the manager and ``F`` elsewhere. const run_proxy_thread: bool = F &redef; + ## How many IO threads to configure for the ZeroMQ context that + ## acts as a central broker. + + ## See ZeroMQ's `ZMQ_IO_THREADS documentation `_ + ## and the `I/O threads ` + ## section in the ZeroMQ guide for details. + const proxy_io_threads = 2 &redef; + ## XSUB listen endpoint for the central broker. ## ## This setting is used for the XSUB socket of the central broker started diff --git a/src/cluster/backend/zeromq/ZeroMQ-Proxy.cc b/src/cluster/backend/zeromq/ZeroMQ-Proxy.cc index 5185e77c62..5cf36fbefc 100644 --- a/src/cluster/backend/zeromq/ZeroMQ-Proxy.cc +++ b/src/cluster/backend/zeromq/ZeroMQ-Proxy.cc @@ -36,6 +36,8 @@ void thread_fun(ProxyThread::Args* args) { } // namespace bool ProxyThread::Start() { + ctx.set(zmq::ctxopt::io_threads, io_threads); + zmq::socket_t xpub(ctx, zmq::socket_type::xpub); zmq::socket_t xsub(ctx, zmq::socket_type::xsub); diff --git a/src/cluster/backend/zeromq/ZeroMQ-Proxy.h b/src/cluster/backend/zeromq/ZeroMQ-Proxy.h index de33d3da1c..63be24ef25 100644 --- a/src/cluster/backend/zeromq/ZeroMQ-Proxy.h +++ b/src/cluster/backend/zeromq/ZeroMQ-Proxy.h @@ -21,8 +21,11 @@ public: * @param xsub_endpoint the XSUB socket address to listen on. * @param xpub_nodrop the xpub_nodrop option to use on the XPUB socket. */ - ProxyThread(std::string xpub_endpoint, std::string xsub_endpoint, int xpub_nodrop) - : xpub_endpoint(std::move(xpub_endpoint)), xsub_endpoint(std::move(xsub_endpoint)), xpub_nodrop(xpub_nodrop) {} + ProxyThread(std::string xpub_endpoint, std::string xsub_endpoint, int xpub_nodrop, int io_threads) + : xpub_endpoint(std::move(xpub_endpoint)), + xsub_endpoint(std::move(xsub_endpoint)), + xpub_nodrop(xpub_nodrop), + io_threads(io_threads) {} ~ProxyThread() { Shutdown(); } @@ -52,5 +55,6 @@ private: std::string xpub_endpoint; std::string xsub_endpoint; int xpub_nodrop = 1; + int io_threads = 2; }; } // namespace zeek::cluster::zeromq diff --git a/src/cluster/backend/zeromq/ZeroMQ.cc b/src/cluster/backend/zeromq/ZeroMQ.cc index 45962bbb25..576302ca46 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.cc +++ b/src/cluster/backend/zeromq/ZeroMQ.cc @@ -97,6 +97,8 @@ void ZeroMQBackend::DoInitPostScript() { linger_ms = static_cast(zeek::id::find_val("Cluster::Backend::ZeroMQ::linger_ms")->AsInt()); poll_max_messages = zeek::id::find_val("Cluster::Backend::ZeroMQ::poll_max_messages")->Get(); debug_flags = zeek::id::find_val("Cluster::Backend::ZeroMQ::debug_flags")->Get(); + proxy_io_threads = + static_cast(zeek::id::find_val("Cluster::Backend::ZeroMQ::proxy_io_threads")->Get()); event_unsubscription = zeek::event_registry->Register("Cluster::Backend::ZeroMQ::unsubscription"); event_subscription = zeek::event_registry->Register("Cluster::Backend::ZeroMQ::subscription"); @@ -240,7 +242,8 @@ bool ZeroMQBackend::DoInit() { } bool ZeroMQBackend::SpawnZmqProxyThread() { - proxy_thread = std::make_unique(listen_xpub_endpoint, listen_xsub_endpoint, listen_xpub_nodrop); + proxy_thread = + std::make_unique(listen_xpub_endpoint, listen_xsub_endpoint, listen_xpub_nodrop, proxy_io_threads); return proxy_thread->Start(); } diff --git a/src/cluster/backend/zeromq/ZeroMQ.h b/src/cluster/backend/zeromq/ZeroMQ.h index a815180b11..9b3fc0e63b 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.h +++ b/src/cluster/backend/zeromq/ZeroMQ.h @@ -99,6 +99,7 @@ private: std::thread self_thread; + int proxy_io_threads = 2; std::unique_ptr proxy_thread; // Tracking the subscriptions on the local XPUB socket. From 8a1abfa8efd7cdfd975b41e7bd9295fcd4403932 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Thu, 6 Feb 2025 15:35:58 +0100 Subject: [PATCH 10/20] cluster/zeromq: No assert on inproc handling This might happen if we didn't succeed in completely sending a multipart message and stop early. --- src/cluster/backend/zeromq/ZeroMQ.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cluster/backend/zeromq/ZeroMQ.cc b/src/cluster/backend/zeromq/ZeroMQ.cc index 576302ca46..7066bc2c29 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.cc +++ b/src/cluster/backend/zeromq/ZeroMQ.cc @@ -405,11 +405,10 @@ void ZeroMQBackend::Run() { // Forward messages from the inprocess bridge to XSUB for subscription // subscription handling (1 part) or XPUB for publishing (4 parts). for ( auto& msg : msgs ) { - assert(msg.size() == 1 || msg.size() == 4); if ( msg.size() == 1 ) { xsub.send(msg[0], zmq::send_flags::none); } - else { + else if ( msg.size() == 4 ) { for ( auto& part : msg ) { zmq::send_flags flags = zmq::send_flags::dontwait; if ( part.more() ) @@ -447,6 +446,9 @@ void ZeroMQBackend::Run() { } while ( ! result ); } } + else { + ZEROMQ_THREAD_PRINTF("inproc: error: expected 1 or 4 parts, have %zu!\n", msg.size()); + } } }; From b82dcfafa4452b849b5061c0a3a4423509ccaa98 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Tue, 4 Mar 2025 15:24:08 +0100 Subject: [PATCH 11/20] cluster/zeromq: Catch exceptions as const zmq::error_t& --- src/cluster/backend/zeromq/ZeroMQ.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cluster/backend/zeromq/ZeroMQ.cc b/src/cluster/backend/zeromq/ZeroMQ.cc index 7066bc2c29..0fd15d95a5 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.cc +++ b/src/cluster/backend/zeromq/ZeroMQ.cc @@ -274,7 +274,7 @@ bool ZeroMQBackend::DoPublishEvent(const std::string& topic, const std::string& // and how this can happen :-/ try { main_inproc.send(parts[i], flags); - } catch ( zmq::error_t& err ) { + } catch ( const zmq::error_t& err ) { // If send() was interrupted and Zeek caught an interrupt or term signal, // fail the publish as we're about to shutdown. There's nothing the user // can do, but it indicates an overload situation as send() was blocking. @@ -299,7 +299,7 @@ bool ZeroMQBackend::DoSubscribe(const std::string& topic_prefix, SubscribeCallba // This is the XSUB API instead of setsockopt(ZMQ_SUBSCRIBE). std::string msg = "\x01" + topic_prefix; main_inproc.send(zmq::const_buffer(msg.data(), msg.size())); - } catch ( zmq::error_t& err ) { + } catch ( const zmq::error_t& err ) { zeek::reporter->Error("Failed to subscribe to topic %s: %s", topic_prefix.c_str(), err.what()); if ( cb ) cb(topic_prefix, {CallbackStatus::Error, err.what()}); @@ -321,7 +321,7 @@ bool ZeroMQBackend::DoUnsubscribe(const std::string& topic_prefix) { // This is the XSUB API instead of setsockopt(ZMQ_SUBSCRIBE). std::string msg = '\0' + topic_prefix; main_inproc.send(zmq::const_buffer(msg.data(), msg.size())); - } catch ( zmq::error_t& err ) { + } catch ( const zmq::error_t& err ) { zeek::reporter->Error("Failed to unsubscribe from topic %s: %s", topic_prefix.c_str(), err.what()); return false; } From eb1f9f9a42c2a593a2a5df371b246542e65b89bc Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Tue, 4 Mar 2025 15:36:04 +0100 Subject: [PATCH 12/20] cluster/zeromq: Catch log_push.send() exception --- src/cluster/backend/zeromq/ZeroMQ.cc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/cluster/backend/zeromq/ZeroMQ.cc b/src/cluster/backend/zeromq/ZeroMQ.cc index 0fd15d95a5..e34cdc3ab8 100644 --- a/src/cluster/backend/zeromq/ZeroMQ.cc +++ b/src/cluster/backend/zeromq/ZeroMQ.cc @@ -347,13 +347,19 @@ bool ZeroMQBackend::DoPublishLogWrites(const logging::detail::LogWriteHeader& he zmq::const_buffer{buf.data(), buf.size()}, }; - zmq::send_result_t result; for ( size_t i = 0; i < parts.size(); i++ ) { zmq::send_flags flags = zmq::send_flags::dontwait; if ( i < parts.size() - 1 ) flags = flags | zmq::send_flags::sndmore; - result = log_push.send(parts[i], flags); + zmq::send_result_t result; + try { + result = log_push.send(parts[i], flags); + } catch ( const zmq::error_t& err ) { + zeek::reporter->Error("Failed to send log write part %zu: %s", i, err.what()); + return false; + } + if ( ! result ) { // XXX: Not exactly clear what we should do if we reach HWM. // we could block and hope a logger comes along that empties From 3a0216728c8f772bb8a9a842658dd04b3004c306 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Wed, 29 Jan 2025 15:50:40 +0100 Subject: [PATCH 13/20] auxil/broker: Bump to latest master version --- auxil/broker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auxil/broker b/auxil/broker index b40f3fd3e9..c99696a69e 160000 --- a/auxil/broker +++ b/auxil/broker @@ -1 +1 @@ -Subproject commit b40f3fd3e93e906fe4f134a4d3279eacd6dbdb45 +Subproject commit c99696a69e5ced0a91bf7c19098d391a57f279ce From 91eb2786be2bc2fdd2b9b29d2dbc238783562301 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Thu, 30 Jan 2025 10:09:35 +0100 Subject: [PATCH 14/20] auxil/libunistd: Bump for ssize_t typedef --- auxil/libunistd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auxil/libunistd b/auxil/libunistd index b38e9c8ebf..d2bfec9295 160000 --- a/auxil/libunistd +++ b/auxil/libunistd @@ -1 +1 @@ -Subproject commit b38e9c8ebff08959a712a5663ba25e0624a3af00 +Subproject commit d2bfec929540c1fec5d1d45f0bcee3cff1eb7fa5 From 9f768d1896925d1feb749e43d98cec49d7110aac Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Thu, 30 Jan 2025 11:23:46 +0100 Subject: [PATCH 15/20] ci: Install websockets from pip for all distros The cluster/websocket tests were developed against websockets 14.2, but Ubuntu and Alpine ship too old versions. Switch to installing the latest version from pip instead, so we don't need to bother making tests compatible with very old Python packages shipped by distributions. --- ci/alpine/Dockerfile | 3 +-- ci/debian-12/Dockerfile | 3 +-- ci/opensuse-tumbleweed/Dockerfile | 3 +-- ci/ubuntu-24.04/Dockerfile | 3 +-- ci/ubuntu-24.10/Dockerfile | 3 +-- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/ci/alpine/Dockerfile b/ci/alpine/Dockerfile index 7bb6e67901..ea9a163ea5 100644 --- a/ci/alpine/Dockerfile +++ b/ci/alpine/Dockerfile @@ -26,10 +26,9 @@ RUN apk add --no-cache \ openssl-dev \ procps \ py3-pip \ - py3-websockets \ python3 \ python3-dev \ swig \ zlib-dev -RUN pip3 install --break-system-packages junit2html +RUN pip3 install --break-system-packages websockets junit2html diff --git a/ci/debian-12/Dockerfile b/ci/debian-12/Dockerfile index cc94969f48..bc66a7d39e 100644 --- a/ci/debian-12/Dockerfile +++ b/ci/debian-12/Dockerfile @@ -28,7 +28,6 @@ RUN apt-get update && apt-get -y install \ python3 \ python3-dev \ python3-pip\ - python3-websockets \ sqlite3 \ swig \ wget \ @@ -39,4 +38,4 @@ RUN apt-get update && apt-get -y install \ # Debian bookworm really doesn't like using pip to install system wide stuff, but # doesn't seem there's a python3-junit2html package, so not sure what we'd break. -RUN pip3 install --break-system-packages junit2html +RUN pip3 install --break-system-packages websockets junit2html diff --git a/ci/opensuse-tumbleweed/Dockerfile b/ci/opensuse-tumbleweed/Dockerfile index 27191b665d..bbc33a47a1 100644 --- a/ci/opensuse-tumbleweed/Dockerfile +++ b/ci/opensuse-tumbleweed/Dockerfile @@ -32,7 +32,6 @@ RUN zypper refresh \ python3 \ python3-devel \ python3-pip \ - python3-websockets \ swig \ tar \ util-linux \ @@ -40,4 +39,4 @@ RUN zypper refresh \ zlib-devel \ && rm -rf /var/cache/zypp -RUN pip3 install --break-system-packages junit2html +RUN pip3 install --break-system-packages websockets junit2html diff --git a/ci/ubuntu-24.04/Dockerfile b/ci/ubuntu-24.04/Dockerfile index 7b13638c30..85f0e36d7f 100644 --- a/ci/ubuntu-24.04/Dockerfile +++ b/ci/ubuntu-24.04/Dockerfile @@ -31,7 +31,6 @@ RUN apt-get update && apt-get -y install \ python3 \ python3-dev \ python3-pip \ - python3-websockets \ ruby \ sqlite3 \ swig \ @@ -43,7 +42,7 @@ RUN apt-get update && apt-get -y install \ && apt autoclean \ && rm -rf /var/lib/apt/lists/* -RUN pip3 install --break-system-packages junit2html +RUN pip3 install --break-system-packages websockets junit2html RUN gem install coveralls-lcov # Download a newer pre-built ccache version that recognizes -fprofile-update=atomic diff --git a/ci/ubuntu-24.10/Dockerfile b/ci/ubuntu-24.10/Dockerfile index a937c7b3e7..52fb682c10 100644 --- a/ci/ubuntu-24.10/Dockerfile +++ b/ci/ubuntu-24.10/Dockerfile @@ -31,7 +31,6 @@ RUN apt-get update && apt-get -y install \ python3 \ python3-dev \ python3-pip \ - python3-websockets \ ruby \ sqlite3 \ swig \ @@ -43,5 +42,5 @@ RUN apt-get update && apt-get -y install \ && apt autoclean \ && rm -rf /var/lib/apt/lists/* -RUN pip3 install --break-system-packages junit2html +RUN pip3 install --break-system-packages websockets junit2html RUN gem install coveralls-lcov From 0b49eac057c6bed33ae38cf277872942872fc813 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Thu, 6 Mar 2025 14:19:46 +0100 Subject: [PATCH 16/20] ci/alpine: Install openssl package for testing --- ci/alpine/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/alpine/Dockerfile b/ci/alpine/Dockerfile index ea9a163ea5..968771d5fb 100644 --- a/ci/alpine/Dockerfile +++ b/ci/alpine/Dockerfile @@ -2,7 +2,7 @@ FROM alpine:latest # A version field to invalidate Cirrus's build cache when needed, as suggested in # https://github.com/cirruslabs/cirrus-ci-docs/issues/544#issuecomment-566066822 -ENV DOCKERFILE_VERSION 20241024 +ENV DOCKERFILE_VERSION 20250306 RUN apk add --no-cache \ bash \ @@ -23,6 +23,7 @@ RUN apk add --no-cache \ linux-headers \ make \ openssh-client \ + openssl \ openssl-dev \ procps \ py3-pip \ From 1e757b2b59367cd333cf0fe1a073e5a8426932d8 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Wed, 29 Jan 2025 15:45:36 +0100 Subject: [PATCH 17/20] cluster/websocket: Add IXWebsocket submodule --- .gitmodules | 3 +++ src/cluster/websocket/auxil/IXWebSocket | 1 + 2 files changed, 4 insertions(+) create mode 160000 src/cluster/websocket/auxil/IXWebSocket diff --git a/.gitmodules b/.gitmodules index 43d56e17f4..7736a3c40c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -79,3 +79,6 @@ [submodule "src/cluster/backend/zeromq/auxil/cppzmq"] path = src/cluster/backend/zeromq/auxil/cppzmq url = https://github.com/zeromq/cppzmq +[submodule "src/cluster/websocket/auxil/IXWebSocket"] + path = src/cluster/websocket/auxil/IXWebSocket + url = https://github.com/zeek/IXWebSocket.git diff --git a/src/cluster/websocket/auxil/IXWebSocket b/src/cluster/websocket/auxil/IXWebSocket new file mode 160000 index 0000000000..80e6c4fe48 --- /dev/null +++ b/src/cluster/websocket/auxil/IXWebSocket @@ -0,0 +1 @@ +Subproject commit 80e6c4fe48dcad816a0e684dbb269957f9073e79 From 6032741868b3db46e76c3cc46f9fb3254506cf71 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Wed, 29 Jan 2025 15:49:44 +0100 Subject: [PATCH 18/20] cluster/websocket: Implement WebSocket server --- scripts/base/frameworks/cluster/main.zeek | 72 ++- src/cluster/BifSupport.cc | 32 +- src/cluster/BifSupport.h | 25 + src/cluster/CMakeLists.txt | 1 + src/cluster/Manager.cc | 34 ++ src/cluster/Manager.h | 27 + src/cluster/cluster.bif | 54 ++ src/cluster/websocket/CMakeLists.txt | 8 + src/cluster/websocket/Plugin.cc | 19 + src/cluster/websocket/Plugin.h | 16 + src/cluster/websocket/README | 5 + .../websocket/WebSocket-IXWebSocket.cc | 170 ++++++ src/cluster/websocket/WebSocket.cc | 504 ++++++++++++++++++ src/cluster/websocket/WebSocket.h | 321 +++++++++++ src/cluster/websocket/auxil/CMakeLists.txt | 7 + src/cluster/websocket/events.bif | 13 + src/script_opt/FuncInfo.cc | 1 + src/zeek-setup.cc | 1 + .../.stderr | 4 + .../cluster.websocket.tls-usage-error/.stderr | 3 + .../..client..stderr | 1 + .../..client.out | 7 + .../..manager..stderr | 4 + .../..manager.out | 4 + .../..client..stderr | 1 + .../..client.out | 5 + .../..manager..stderr | 2 + .../..manager.out | 6 + .../..client..stderr | 1 + .../cluster.websocket.bad-url/..client.out | 4 + .../..manager..stderr | 2 + .../cluster.websocket.bad-url/..manager.out | 3 + .../..client..stderr | 1 + .../..client.out | 3 + .../..manager..stderr | 2 + .../..manager.cluster.log.cannonified | 7 + .../..manager.out | 7 + .../.stderr | 4 + .../..client..stderr | 1 + .../..client.out | 17 + .../..manager..stderr | 2 + .../..manager.out | 8 + .../cluster.websocket.one/..client..stderr | 1 + .../cluster.websocket.one/..client.out | 17 + .../cluster.websocket.one/..manager..stderr | 2 + .../cluster.websocket.one/..manager.out | 8 + .../cluster.websocket.three/..client..stderr | 1 + .../cluster.websocket.three/..client.out | 83 +++ .../cluster.websocket.three/..manager..stderr | 2 + .../cluster.websocket.three/..manager.out | 23 + .../cluster.websocket.tls-usage-error/.stderr | 3 + .../cluster.websocket.tls/..client..stderr | 1 + .../cluster.websocket.tls/..client.out | 17 + .../cluster.websocket.tls/..manager..stderr | 2 + .../cluster.websocket.tls/..manager.out | 8 + .../..client..stderr | 1 + .../..client.out | 54 ++ .../..manager..stderr | 2 + .../..manager.out.sorted | 15 + .../canonified_loaded_scripts.log | 1 + .../canonified_loaded_scripts.log | 1 + .../Baseline/opt.ZAM-bif-tracking/output | 2 +- testing/btest/Baseline/plugins.hooks/output | 12 + .../cluster/websocket/bad-event-args.zeek | 126 +++++ .../cluster/websocket/bad-subscriptions.zeek | 102 ++++ testing/btest/cluster/websocket/bad-url.zeek | 101 ++++ .../btest/cluster/websocket/cluster-log.zeek | 111 ++++ .../cluster/websocket/listen-idempotent.zeek | 50 ++ .../cluster/websocket/one-pipelining.zeek | 123 +++++ testing/btest/cluster/websocket/one.zeek | 120 +++++ testing/btest/cluster/websocket/three.zeek | 137 +++++ .../cluster/websocket/tls-usage-error.zeek | 19 + testing/btest/cluster/websocket/tls.zeek | 151 ++++++ .../cluster/websocket/two-pipelining.zeek | 158 ++++++ testing/btest/opt/ZAM-bif-tracking.zeek | 1 + 75 files changed, 2860 insertions(+), 4 deletions(-) create mode 100644 src/cluster/websocket/CMakeLists.txt create mode 100644 src/cluster/websocket/Plugin.cc create mode 100644 src/cluster/websocket/Plugin.h create mode 100644 src/cluster/websocket/README create mode 100644 src/cluster/websocket/WebSocket-IXWebSocket.cc create mode 100644 src/cluster/websocket/WebSocket.cc create mode 100644 src/cluster/websocket/WebSocket.h create mode 100644 src/cluster/websocket/auxil/CMakeLists.txt create mode 100644 src/cluster/websocket/events.bif create mode 100644 testing/btest/Baseline.zam/cluster.websocket.listen-idempotent/.stderr create mode 100644 testing/btest/Baseline.zam/cluster.websocket.tls-usage-error/.stderr create mode 100644 testing/btest/Baseline/cluster.websocket.bad-event-args/..client..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.bad-event-args/..client.out create mode 100644 testing/btest/Baseline/cluster.websocket.bad-event-args/..manager..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.bad-event-args/..manager.out create mode 100644 testing/btest/Baseline/cluster.websocket.bad-subscriptions/..client..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.bad-subscriptions/..client.out create mode 100644 testing/btest/Baseline/cluster.websocket.bad-subscriptions/..manager..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.bad-subscriptions/..manager.out create mode 100644 testing/btest/Baseline/cluster.websocket.bad-url/..client..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.bad-url/..client.out create mode 100644 testing/btest/Baseline/cluster.websocket.bad-url/..manager..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.bad-url/..manager.out create mode 100644 testing/btest/Baseline/cluster.websocket.cluster-log/..client..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.cluster-log/..client.out create mode 100644 testing/btest/Baseline/cluster.websocket.cluster-log/..manager..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.cluster-log/..manager.cluster.log.cannonified create mode 100644 testing/btest/Baseline/cluster.websocket.cluster-log/..manager.out create mode 100644 testing/btest/Baseline/cluster.websocket.listen-idempotent/.stderr create mode 100644 testing/btest/Baseline/cluster.websocket.one-pipelining/..client..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.one-pipelining/..client.out create mode 100644 testing/btest/Baseline/cluster.websocket.one-pipelining/..manager..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.one-pipelining/..manager.out create mode 100644 testing/btest/Baseline/cluster.websocket.one/..client..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.one/..client.out create mode 100644 testing/btest/Baseline/cluster.websocket.one/..manager..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.one/..manager.out create mode 100644 testing/btest/Baseline/cluster.websocket.three/..client..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.three/..client.out create mode 100644 testing/btest/Baseline/cluster.websocket.three/..manager..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.three/..manager.out create mode 100644 testing/btest/Baseline/cluster.websocket.tls-usage-error/.stderr create mode 100644 testing/btest/Baseline/cluster.websocket.tls/..client..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.tls/..client.out create mode 100644 testing/btest/Baseline/cluster.websocket.tls/..manager..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.tls/..manager.out create mode 100644 testing/btest/Baseline/cluster.websocket.two-pipelining/..client..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.two-pipelining/..client.out create mode 100644 testing/btest/Baseline/cluster.websocket.two-pipelining/..manager..stderr create mode 100644 testing/btest/Baseline/cluster.websocket.two-pipelining/..manager.out.sorted create mode 100644 testing/btest/cluster/websocket/bad-event-args.zeek create mode 100644 testing/btest/cluster/websocket/bad-subscriptions.zeek create mode 100644 testing/btest/cluster/websocket/bad-url.zeek create mode 100644 testing/btest/cluster/websocket/cluster-log.zeek create mode 100644 testing/btest/cluster/websocket/listen-idempotent.zeek create mode 100644 testing/btest/cluster/websocket/one-pipelining.zeek create mode 100644 testing/btest/cluster/websocket/one.zeek create mode 100644 testing/btest/cluster/websocket/three.zeek create mode 100644 testing/btest/cluster/websocket/tls-usage-error.zeek create mode 100644 testing/btest/cluster/websocket/tls.zeek create mode 100644 testing/btest/cluster/websocket/two-pipelining.zeek diff --git a/scripts/base/frameworks/cluster/main.zeek b/scripts/base/frameworks/cluster/main.zeek index bff45bf29e..36a19885d6 100644 --- a/scripts/base/frameworks/cluster/main.zeek +++ b/scripts/base/frameworks/cluster/main.zeek @@ -327,11 +327,62 @@ export { ## The arguments for the event. args: vector of any; }; + + ## The TLS options for a WebSocket server. + ## + ## If cert_file and key_file are set, TLS is enabled. If both + ## are unset, TLS is disabled. Any other combination is an error. + type WebSocketTLSOptions: record { + ## The cert file to use. + cert_file: string &optional; + ## The key file to use. + key_file: string &optional; + ## Expect peers to send client certificates. + enable_peer_verification: bool &default=F; + ## The CA certificate or CA bundle used for peer verification. + ## Empty will use the implementations's default when + ## ``enable_peer_verification`` is T. + ca_file: string &default=""; + ## The ciphers to use. Empty will use the implementation's defaults. + ciphers: string &default=""; + }; + + ## WebSocket server options to pass to :zeek:see:`Cluster::listen_websocket`. + type WebSocketServerOptions: record { + ## The host address to listen on. + listen_host: string; + ## The port the WebSocket server is supposed to listen on. + listen_port: port; + ## The TLS options used for this WebSocket server. By default, + ## TLS is disabled. See also :zeek:see:`Cluster::WebSocketTLSOptions`. + tls_options: WebSocketTLSOptions &default=WebSocketTLSOptions(); + }; + + ## Start listening on a WebSocket address. + ## + ## options: The server :zeek:see:`Cluster::WebSocketServerOptions` to use. + ## + ## Returns: T on success, else F. + global listen_websocket: function(options: WebSocketServerOptions): bool; + + ## Network information of an endpoint. + type NetworkInfo: record { + ## The IP address or hostname where the endpoint listens. + address: string; + ## The port where the endpoint is bound to. + bound_port: port; + }; + + ## Information about a WebSocket endpoint. + type EndpointInfo: record { + id: string; + network: NetworkInfo; + }; } # Needs declaration of Cluster::Event type. @load base/bif/cluster.bif - +@load base/bif/plugins/Zeek_Cluster_WebSocket.events.bif.zeek # Track active nodes per type. global active_node_ids: table[NodeType] of set[string]; @@ -597,3 +648,22 @@ function unsubscribe(topic: string): bool { return Cluster::__unsubscribe(topic); } + +function listen_websocket(options: WebSocketServerOptions): bool + { + return Cluster::__listen_websocket(options); + } + +event websocket_client_added(endpoint: EndpointInfo, subscriptions: string_vec) + { + local msg = fmt("WebSocket client '%s' (%s:%d) subscribed to %s", + endpoint$id, endpoint$network$address, endpoint$network$bound_port, subscriptions); + Cluster::log(msg); + } + +event websocket_client_lost(endpoint: EndpointInfo) + { + local msg = fmt("WebSocket client '%s' (%s:%d) gone", + endpoint$id, endpoint$network$address, endpoint$network$bound_port); + Cluster::log(msg); + } diff --git a/src/cluster/BifSupport.cc b/src/cluster/BifSupport.cc index 9d3b0ac5c4..5eb5db72eb 100644 --- a/src/cluster/BifSupport.cc +++ b/src/cluster/BifSupport.cc @@ -65,7 +65,7 @@ zeek::RecordValPtr make_event(zeek::ArgsSpan args) { } const auto func = zeek::FuncValPtr{zeek::NewRef{}, maybe_func_val->AsFuncVal()}; - auto checked_args = cluster::detail::check_args(func, args.subspan(1)); + auto checked_args = zeek::cluster::detail::check_args(func, args.subspan(1)); if ( ! checked_args ) return rec; @@ -109,7 +109,7 @@ zeek::ValPtr publish_event(const zeek::ValPtr& topic, zeek::ArgsSpan args) { } else if ( args[0]->GetType()->Tag() == zeek::TYPE_RECORD ) { if ( args[0]->GetType() == cluster_event_type ) { // Handling Cluster::Event record type - auto ev = to_cluster_event(cast_intrusive(args[0])); + auto ev = to_cluster_event(zeek::cast_intrusive(args[0])); if ( ! ev ) return zeek::val_mgr->False(); @@ -148,4 +148,32 @@ bool is_cluster_pool(const zeek::Val* pool) { return pool->GetType() == pool_type; } + +zeek::RecordValPtr make_endpoint_info(const std::string& id, const std::string& address, uint32_t port, + TransportProto proto) { + static const auto ep_info_type = zeek::id::find_type("Cluster::EndpointInfo"); + static const auto net_info_type = zeek::id::find_type("Cluster::NetworkInfo"); + + auto net_rec = zeek::make_intrusive(net_info_type); + net_rec->Assign(0, address); + net_rec->Assign(1, zeek::val_mgr->Port(port, proto)); + + auto ep_rec = zeek::make_intrusive(ep_info_type); + ep_rec->Assign(0, id); + ep_rec->Assign(1, net_rec); + + return ep_rec; +} + +zeek::VectorValPtr make_string_vec(zeek::Span strings) { + static const auto string_vec_type = zeek::id::find_type("string_vec"); + auto vec = zeek::make_intrusive(string_vec_type); + vec->Reserve(strings.size()); + + for ( const auto& s : strings ) + vec->Append(zeek::make_intrusive(s)); + + return vec; +} + } // namespace zeek::cluster::detail::bif diff --git a/src/cluster/BifSupport.h b/src/cluster/BifSupport.h index b02fc97bd0..938962ec58 100644 --- a/src/cluster/BifSupport.h +++ b/src/cluster/BifSupport.h @@ -4,6 +4,7 @@ #include "zeek/IntrusivePtr.h" #include "zeek/Span.h" +#include "zeek/net_util.h" // Helpers for cluster.bif @@ -15,6 +16,8 @@ class Frame; class RecordVal; using RecordValPtr = IntrusivePtr; +class VectorVal; +using VectorValPtr = IntrusivePtr; class Val; using ValPtr = IntrusivePtr; @@ -46,6 +49,28 @@ zeek::ValPtr publish_event(const zeek::ValPtr& topic, zeek::ArgsSpan args); bool is_cluster_pool(const zeek::Val* pool); +/** + * Create a Cluster::EndpointInfo record with a nested Cluster::NetworkInfo record. + * + * @param id The string to use as id in the record. + * @param address The string to use as address in the network record. + * @param port The port to use in the network record. + * @param proto The proto for the given port value. + * + * @returns A record value of type Cluster::EndpointInfo filled with the provided info. + */ +zeek::RecordValPtr make_endpoint_info(const std::string& id, const std::string& address, uint32_t port, + TransportProto proto); + +/** + * Helper to go from a vector or array of std::strings to a zeek::VectorVal. + * + * @param strings The std::string instances. + * + * @return a VectorVal instance of type string_vec filled with strings. + */ +zeek::VectorValPtr make_string_vec(zeek::Span strings); + } // namespace cluster::detail::bif } // namespace zeek diff --git a/src/cluster/CMakeLists.txt b/src/cluster/CMakeLists.txt index bb421c6c01..fa2093be63 100644 --- a/src/cluster/CMakeLists.txt +++ b/src/cluster/CMakeLists.txt @@ -13,3 +13,4 @@ zeek_add_subdir_library( add_subdirectory(backend) add_subdirectory(serializer) +add_subdirectory(websocket) diff --git a/src/cluster/Manager.cc b/src/cluster/Manager.cc index ffac82c396..28b1055f59 100644 --- a/src/cluster/Manager.cc +++ b/src/cluster/Manager.cc @@ -2,7 +2,10 @@ #include "zeek/cluster/Manager.h" +#include "zeek/Func.h" #include "zeek/cluster/Serializer.h" +#include "zeek/cluster/websocket/WebSocket.h" +#include "zeek/util.h" using namespace zeek::cluster; @@ -11,6 +14,15 @@ Manager::Manager() event_serializers(plugin::ComponentManager("Cluster", "EventSerializerTag")), log_serializers(plugin::ComponentManager("Cluster", "LogSerializerTag")) {} +// Force destructor definition into compilation unit to avoid needing the +// full websocket::Server declaration in cluster/Manager.h. +Manager::~Manager() = default; + +void Manager::Terminate() { + for ( const auto& [_, entry] : websocket_servers ) + entry.server->Terminate(); +} + std::unique_ptr Manager::InstantiateBackend( const zeek::EnumValPtr& tag, std::unique_ptr event_serializer, std::unique_ptr log_serializer, @@ -30,3 +42,25 @@ std::unique_ptr Manager::InstantiateLogSerializer(const zeek::Enu const LogSerializerComponent* c = LogSerializers().Lookup(tag); return c ? c->Factory()() : nullptr; } + +bool Manager::ListenWebSocket(const websocket::detail::ServerOptions& options) { + WebSocketServerKey key{options.host, options.port}; + + if ( websocket_servers.count(key) != 0 ) { + const auto& entry = websocket_servers[key]; + if ( entry.options == options ) + return true; + + zeek::emit_builtin_error(zeek::util::fmt("Already listening on %s:%d", options.host.c_str(), options.port)); + return false; + } + + auto server = + websocket::detail::StartServer(std::make_unique(), options); + + if ( ! server ) + return false; + + websocket_servers.insert({key, WebSocketServerEntry{options, std::move(server)}}); + return true; +} diff --git a/src/cluster/Manager.h b/src/cluster/Manager.h index 3e64924039..d1cc8628fb 100644 --- a/src/cluster/Manager.h +++ b/src/cluster/Manager.h @@ -2,10 +2,12 @@ #pragma once +#include #include #include "zeek/cluster/Component.h" #include "zeek/cluster/Serializer.h" +#include "zeek/cluster/websocket/WebSocket.h" #include "zeek/plugin/ComponentManager.h" namespace zeek::cluster { @@ -19,6 +21,15 @@ namespace zeek::cluster { class Manager { public: Manager(); + ~Manager(); + + /** + * Terminate the cluster manager. + * + * This shuts down any WebSocket servers that were started + * at termination time. + */ + void Terminate(); /** * Instantiate a cluster backend with the given enum value and @@ -69,10 +80,26 @@ public: */ plugin::ComponentManager& LogSerializers() { return log_serializers; }; + /** + * Start a WebSocket server for the given address and port pair. + * + * @param options The options to use for the WebSocket server. + * + * @return True on success, else false. + */ + bool ListenWebSocket(const websocket::detail::ServerOptions& options); + private: plugin::ComponentManager backends; plugin::ComponentManager event_serializers; plugin::ComponentManager log_serializers; + + using WebSocketServerKey = std::pair; + struct WebSocketServerEntry { + websocket::detail::ServerOptions options; + std::unique_ptr server; + }; + std::map websocket_servers; }; // This manager instance only exists for plugins to register components, diff --git a/src/cluster/cluster.bif b/src/cluster/cluster.bif index ec2c8f115e..cd39cde68b 100644 --- a/src/cluster/cluster.bif +++ b/src/cluster/cluster.bif @@ -3,6 +3,8 @@ #include "zeek/cluster/Backend.h" #include "zeek/cluster/BifSupport.h" +#include "zeek/cluster/Manager.h" +#include "zeek/cluster/websocket/WebSocket.h" using namespace zeek::cluster::detail::bif; @@ -11,6 +13,7 @@ using namespace zeek::cluster::detail::bif; module Cluster; type Cluster::Event: record; +type Cluster::WebSocketTLSOptions: record; ## Publishes an event to a given topic. ## @@ -147,3 +150,54 @@ function Cluster::publish_hrw%(pool: Pool, key: any, ...%): bool ScriptLocationScope scope{frame}; return publish_event(topic, args); %} + +function Cluster::__listen_websocket%(options: WebSocketServerOptions%): bool + %{ + using namespace zeek::cluster::websocket::detail; + + const auto& server_options_type = zeek::id::find_type("Cluster::WebSocketServerOptions"); + const auto& tls_options_type = zeek::id::find_type("Cluster::WebSocketTLSOptions"); + + if ( options->GetType() != server_options_type ) { + zeek::emit_builtin_error("expected type Cluster::WebSocketServerOptions for options"); + return zeek::val_mgr->False(); + } + + auto options_rec = zeek::IntrusivePtr{zeek::NewRef{}, options->AsRecordVal()}; + auto tls_options_rec = options_rec->GetFieldOrDefault("tls_options"); + + if ( tls_options_rec->GetType() != tls_options_type ) { + zeek::emit_builtin_error("expected type Cluster::WebSocketTLSOptions for tls_options"); + return zeek::val_mgr->False(); + } + + bool have_cert = tls_options_rec->HasField("cert_file"); + bool have_key = tls_options_rec->HasField("key_file"); + + if ( (have_cert || have_key) && ! (have_cert && have_key) ) { + std::string error = "Invalid tls_options: "; + if ( have_cert ) + error += "No key_file field"; + else + error += "No cert_file field"; + zeek::emit_builtin_error(error.c_str()); + return zeek::val_mgr->False(); + } + + struct TLSOptions tls_options = { + have_cert ? std::optional{tls_options_rec->GetField("cert_file")->ToStdString()} : std::nullopt, + have_key ? std::optional{tls_options_rec->GetField("key_file")->ToStdString()} : std::nullopt, + tls_options_rec->GetFieldOrDefault("enable_peer_verification")->Get(), + tls_options_rec->GetFieldOrDefault("ca_file")->ToStdString(), + tls_options_rec->GetFieldOrDefault("ciphers")->ToStdString(), + }; + + struct ServerOptions server_options { + options_rec->GetField("listen_host")->ToStdString(), + static_cast(options_rec->GetField("listen_port")->Port()), + }; + server_options.tls_options = std::move(tls_options); + + auto result = zeek::cluster::manager->ListenWebSocket(server_options); + return zeek::val_mgr->Bool(result); + %} diff --git a/src/cluster/websocket/CMakeLists.txt b/src/cluster/websocket/CMakeLists.txt new file mode 100644 index 0000000000..1c41cf14b8 --- /dev/null +++ b/src/cluster/websocket/CMakeLists.txt @@ -0,0 +1,8 @@ +add_subdirectory(auxil) + +zeek_add_plugin( + Zeek Cluster_WebSocket + INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} + DEPENDENCIES ixwebsocket::ixwebsocket + SOURCES Plugin.cc WebSocket.cc WebSocket-IXWebSocket.cc + BIFS events.bif) diff --git a/src/cluster/websocket/Plugin.cc b/src/cluster/websocket/Plugin.cc new file mode 100644 index 0000000000..89a6e9d21f --- /dev/null +++ b/src/cluster/websocket/Plugin.cc @@ -0,0 +1,19 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +#include "zeek/cluster/websocket/Plugin.h" + +namespace zeek::plugin::Cluster_WebSocket { +// Definition of plugin. +Plugin plugin; +}; // namespace zeek::plugin::Cluster_WebSocket + +namespace zeek::plugin::Cluster_WebSocket { + +zeek::plugin::Configuration Plugin::Configure() { + zeek::plugin::Configuration config; + config.name = "Zeek::Cluster_WebSocket"; + config.description = "Provides WebSocket access to a Zeek cluster"; + return config; +} + +} // namespace zeek::plugin::Cluster_WebSocket diff --git a/src/cluster/websocket/Plugin.h b/src/cluster/websocket/Plugin.h new file mode 100644 index 0000000000..97e2a15db8 --- /dev/null +++ b/src/cluster/websocket/Plugin.h @@ -0,0 +1,16 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +#pragma once + +#include "zeek/plugin/Plugin.h" + +namespace zeek::plugin::Cluster_WebSocket { + +class Plugin : public zeek::plugin::Plugin { +public: + zeek::plugin::Configuration Configure() override; +}; + +extern Plugin plugin; + +} // namespace zeek::plugin::Cluster_WebSocket diff --git a/src/cluster/websocket/README b/src/cluster/websocket/README new file mode 100644 index 0000000000..5f80cc6a64 --- /dev/null +++ b/src/cluster/websocket/README @@ -0,0 +1,5 @@ +A Zeek Cluster's WebSocket interface +==================================== + +This directory contains code that allows external applications +to connect to Zeek using WebSocket connections. diff --git a/src/cluster/websocket/WebSocket-IXWebSocket.cc b/src/cluster/websocket/WebSocket-IXWebSocket.cc new file mode 100644 index 0000000000..d2b8cb289b --- /dev/null +++ b/src/cluster/websocket/WebSocket-IXWebSocket.cc @@ -0,0 +1,170 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +// Implementation of a WebSocket server and clients using the IXWebSocket client library. +#include "zeek/cluster/websocket/WebSocket.h" + +#include +#include + +#include "zeek/Reporter.h" + +#include "ixwebsocket/IXConnectionState.h" +#include "ixwebsocket/IXSocketTLSOptions.h" +#include "ixwebsocket/IXWebSocket.h" +#include "ixwebsocket/IXWebSocketSendData.h" +#include "ixwebsocket/IXWebSocketServer.h" + +namespace zeek::cluster::websocket::detail::ixwebsocket { + +/** + * Implementation of WebSocketClient for the IXWebsocket library. + */ +class IxWebSocketClient : public WebSocketClient { +public: + IxWebSocketClient(std::shared_ptr cs, std::shared_ptr ws) + : cs(std::move(cs)), ws(std::move(ws)) { + if ( ! this->cs || ! this->ws ) + throw std::invalid_argument("expected ws and cs to be set"); + } + + bool IsTerminated() const override { + if ( cs->isTerminated() ) + return true; + + auto rs = ws->getReadyState(); + return rs == ix::ReadyState::Closing || rs == ix::ReadyState::Closed; + } + + void Close(uint16_t code, const std::string& reason) override { ws->close(code, reason); } + + SendInfo SendText(std::string_view sv) override { + if ( cs->isTerminated() ) + return {true}; // small lie + + auto send_info = ws->sendUtf8Text(ix::IXWebSocketSendData{sv.data(), sv.size()}); + return SendInfo{send_info.success}; + } + + const std::string& getId() override { return cs->getId(); } + const std::string& getRemoteIp() override { return cs->getRemoteIp(); } + int getRemotePort() override { return cs->getRemotePort(); } + +private: + std::shared_ptr cs; + std::shared_ptr ws; +}; + +/** + * Implementation of WebSocketServer using the IXWebsocket library. + */ +class IXWebSocketServer : public WebSocketServer { +public: + IXWebSocketServer(std::unique_ptr dispatcher, std::unique_ptr server) + : WebSocketServer(std::move(dispatcher)), server(std::move(server)) {} + +private: + void DoTerminate() override { + // Stop the server. + server->stop(); + } + + std::unique_ptr server; +}; + +std::unique_ptr StartServer(std::unique_ptr dispatcher, + const ServerOptions& options) { + auto server = + std::make_unique(options.port, options.host, ix::SocketServer::kDefaultTcpBacklog, + options.max_connections, + ix::WebSocketServer::kDefaultHandShakeTimeoutSecs, + ix::SocketServer::kDefaultAddressFamily, options.ping_interval_seconds); + + if ( ! options.per_message_deflate ) + server->disablePerMessageDeflate(); + + const auto& tls_options = options.tls_options; + if ( tls_options.TlsEnabled() ) { + ix::SocketTLSOptions ix_tls_options{}; + ix_tls_options.tls = true; + ix_tls_options.certFile = tls_options.cert_file.value(); + ix_tls_options.keyFile = tls_options.key_file.value(); + + if ( tls_options.enable_peer_verification ) { + if ( ! tls_options.ca_file.empty() ) + ix_tls_options.caFile = tls_options.ca_file; + } + else { + // This is the IXWebSocket library's way of + // disabling peer verification. + ix_tls_options.caFile = "NONE"; + } + + if ( ! tls_options.ciphers.empty() ) + ix_tls_options.ciphers = tls_options.ciphers; + + server->setTLSOptions(ix_tls_options); + } + + // Using the legacy IXWebsocketAPI API to acquire a shared_ptr to the ix::WebSocket instance. + ix::WebSocketServer::OnConnectionCallback connection_callback = + [dispatcher = dispatcher.get()](std::weak_ptr websocket, + std::shared_ptr cs) -> void { + // Hold a shared_ptr to the WebSocket object until we see the close. + std::shared_ptr ws = websocket.lock(); + + // Client already gone or terminated? Weird... + if ( ! ws || cs->isTerminated() ) + return; + + auto id = cs->getId(); + int remotePort = cs->getRemotePort(); + std::string remoteIp = cs->getRemoteIp(); + + auto ixws = std::make_shared(std::move(cs), ws); + + // These callbacks run in per client threads. The actual processing happens + // on the main thread via a single WebSocketDemux instance. + ix::OnMessageCallback message_callback = [dispatcher, id, remotePort, remoteIp, + ixws](const ix::WebSocketMessagePtr& msg) mutable { + if ( msg->type == ix::WebSocketMessageType::Open ) { + dispatcher->QueueForProcessing( + WebSocketOpen{id, msg->openInfo.uri, msg->openInfo.protocol, std::move(ixws)}); + } + else if ( msg->type == ix::WebSocketMessageType::Message ) { + dispatcher->QueueForProcessing(WebSocketMessage{id, msg->str}); + } + else if ( msg->type == ix::WebSocketMessageType::Close ) { + dispatcher->QueueForProcessing(WebSocketClose{id}); + } + else if ( msg->type == ix::WebSocketMessageType::Error ) { + dispatcher->QueueForProcessing(WebSocketClose{id}); + } + }; + + ws->setOnMessageCallback(message_callback); + }; + + server->setOnConnectionCallback(connection_callback); + + const auto [success, reason] = server->listen(); + if ( ! success ) { + zeek::reporter->Error("WebSocket: Unable to listen on %s:%d: %s", options.host.c_str(), options.port, + reason.c_str()); + return nullptr; + } + + server->start(); + + return std::make_unique(std::move(dispatcher), std::move(server)); +} + + +} // namespace zeek::cluster::websocket::detail::ixwebsocket + +using namespace zeek::cluster::websocket::detail; + +std::unique_ptr zeek::cluster::websocket::detail::StartServer( + std::unique_ptr dispatcher, const ServerOptions& options) { + // Just delegate to the above IXWebSocket specific implementation. + return ixwebsocket::StartServer(std::move(dispatcher), options); +} diff --git a/src/cluster/websocket/WebSocket.cc b/src/cluster/websocket/WebSocket.cc new file mode 100644 index 0000000000..115db9aec8 --- /dev/null +++ b/src/cluster/websocket/WebSocket.cc @@ -0,0 +1,504 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +// Implement Broker's WebSocket client handling in Zeek. + +#include "zeek/cluster/websocket/WebSocket.h" + +#include +#include +#include + +#include "zeek/Reporter.h" +#include "zeek/cluster/Backend.h" +#include "zeek/cluster/BifSupport.h" +#include "zeek/cluster/Manager.h" +#include "zeek/cluster/OnLoop.h" +#include "zeek/cluster/Serializer.h" +#include "zeek/cluster/serializer/broker/Serializer.h" +#include "zeek/cluster/websocket/Plugin.h" +#include "zeek/cluster/websocket/events.bif.h" +#include "zeek/net_util.h" +#include "zeek/threading/MsgThread.h" + +#include "broker/data.bif.h" +#include "broker/data_envelope.hh" +#include "broker/error.hh" +#include "broker/format/json.hh" +#include "broker/zeek.hh" +#include "rapidjson/document.h" +#include "rapidjson/rapidjson.h" + + +#define WS_DEBUG(...) PLUGIN_DBG_LOG(zeek::plugin::Cluster_WebSocket::plugin, __VA_ARGS__) + +namespace zeek { +const char* zeek_version(); +} + +using namespace zeek::cluster::websocket::detail; + +namespace { + +class WebSocketEventHandlingStrategy : public zeek::cluster::detail::EventHandlingStrategy { +public: + WebSocketEventHandlingStrategy(std::shared_ptr ws, WebSocketEventDispatcher* dispatcher) + : wsc(std::move(ws)), dispatcher(dispatcher) {} + +private: + /** + * Any received remote event is encoded into Broker's JSON v1 format and + * send over to the WebSocket client. + * + * We leverage low-level Broker encoding functions here directly. This + * will need some abstractions if client's can opt to use different encodings + * of events in the future. + */ + bool DoHandleRemoteEvent(std::string_view topic, zeek::cluster::detail::Event e) override { + // If the client has left, no point in sending it any pending event. + if ( wsc->IsTerminated() ) + return true; + + + // Any events received from the backend before an Ack was sent + // are discarded. + if ( ! wsc->IsAcked() ) + return true; + + // XXX The serialization is somewhat slow, it would be good to offload + // it to a thread, or try to go from Val's directly to JSON and see + // if that's faster. + auto ev = zeek::cluster::detail::to_broker_event(e); + if ( ! ev ) { + fprintf(stderr, "[ERROR] Unable to go from detail::Event to broker::event\n"); + return false; + } + + buffer.clear(); + auto envelope = broker::data_envelope::make(topic, ev->as_data()); + broker::format::json::v1::encode(envelope, std::back_inserter(buffer)); + + dispatcher->QueueReply(WebSocketSendReply{wsc, buffer}); + return true; + } + + /** + * Events from backends aren't enqueued into the event loop when + * running for WebSocket clients. + */ + void DoEnqueueLocalEvent(zeek::EventHandlerPtr h, zeek::Args args) override {} + + std::string buffer; + std::shared_ptr wsc; + WebSocketEventDispatcher* dispatcher; +}; + +class ReplyInputMessage : public zeek::threading::BasicInputMessage { +public: + ReplyInputMessage(WebSocketReply work) : zeek::threading::BasicInputMessage("ReplyInput"), work(std::move(work)) {}; + bool Process() override { + return std::visit([this](auto& item) -> bool { return Process(item); }, work); + }; + +private: + bool Process(const WebSocketSendReply& sr) { + const auto& wsc = sr.wsc; + if ( wsc->IsTerminated() ) + return true; + + auto send_info = wsc->SendText(sr.msg); + if ( ! send_info.success && ! wsc->IsTerminated() ) + fprintf(stderr, "[ERROR] Failed to send reply to WebSocket client %s (%s:%d)\n", wsc->getId().c_str(), + wsc->getRemoteIp().c_str(), wsc->getRemotePort()); + + return true; + } + + bool Process(const WebSocketCloseReply& cr) { + const auto& wsc = cr.wsc; + if ( ! wsc->IsTerminated() ) + wsc->Close(cr.code, cr.reason); + + return true; + } + + WebSocketReply work; +}; + +} // namespace + + +// Inspired by broker/internal/json_client.cc +WebSocketClient::SendInfo WebSocketClient::SendError(std::string_view code, std::string_view message) { + std::string buf; + buf.reserve(code.size() + message.size() + 32); + auto out = std::back_inserter(buf); + *out++ = '{'; + broker::format::json::v1::append_field("type", "error", out); + *out++ = ','; + broker::format::json::v1::append_field("code", code, out); + *out++ = ','; + broker::format::json::v1::append_field("message", message, out); + *out++ = '}'; + return SendText(buf); +} + +// Inspired by broker/internal/json_client.cc +WebSocketClient::SendInfo WebSocketClient::SendAck(std::string_view endpoint, std::string_view version) { + std::string buf; + buf.reserve(endpoint.size() + version.size() + 32); + auto out = std::back_inserter(buf); + *out++ = '{'; + broker::format::json::v1::append_field("type", "ack", out); + *out++ = ','; + broker::format::json::v1::append_field("endpoint", endpoint, out); + *out++ = ','; + broker::format::json::v1::append_field("version", version, out); + *out++ = '}'; + auto r = SendText(buf); + acked = true; + return r; +} + +void WebSocketClient::SetSubscriptions(const std::vector& topic_prefixes) { + for ( const auto& topic_prefix : topic_prefixes ) + subscriptions_state[topic_prefix] = false; +} + +void WebSocketClient::SetSubscriptionActive(const std::string& topic_prefix) { + if ( subscriptions_state.count(topic_prefix) == 0 ) { + zeek::reporter->InternalWarning("Unknown topic_prefix for WebSocket client %s!", topic_prefix.c_str()); + return; + } + + subscriptions_state[topic_prefix] = true; +} + +bool WebSocketClient::AllSubscriptionsActive() const { + for ( const auto& [_, status] : subscriptions_state ) { + if ( ! status ) + return false; + } + + return true; +} + +const std::vector WebSocketClient::GetSubscriptions() const { + std::vector subs; + subs.reserve(subscriptions_state.size()); + + for ( const auto& [topic, _] : subscriptions_state ) + subs.emplace_back(topic); + + return subs; +} + +class zeek::cluster::websocket::detail::ReplyMsgThread : public zeek::threading::MsgThread { +public: + ReplyMsgThread() : zeek::threading::MsgThread() { SetName("ws-reply-thread"); } + + void Run() override { + zeek::util::detail::set_thread_name("zk/ws-reply-thread"); + MsgThread::Run(); + } + + bool OnHeartbeat(double network_time, double current_time) override { return true; } + + bool OnFinish(double network_time) override { return true; } +}; + +WebSocketEventDispatcher::WebSocketEventDispatcher() { + onloop = + new zeek::detail::OnLoopProcess(this, "WebSocketEventDispatcher"); + // Register the onloop instance the IO loop. Lifetime will be managed by the loop. + onloop->Register(false); + + reply_msg_thread = new ReplyMsgThread(); + reply_msg_thread->Start(); +} + +WebSocketEventDispatcher::~WebSocketEventDispatcher() { + // Freed by threading manager. + reply_msg_thread = nullptr; +} + +void WebSocketEventDispatcher::Terminate() { + WS_DEBUG("Terminating WebSocketEventDispatcher"); + + for ( auto& [_, client] : clients ) { + const auto& wsc = client.wsc; + const auto& backend = client.backend; + WS_DEBUG("Sending close to WebSocket client %s (%s:%d)", wsc->getId().c_str(), wsc->getRemoteIp().c_str(), + wsc->getRemotePort()); + + QueueReply(WebSocketCloseReply{wsc, 1001, "Terminating"}); + backend->Terminate(); + } + + clients.clear(); +} + +void WebSocketEventDispatcher::QueueForProcessing(WebSocketEvent&& event) { + // Just delegate to onloop. The work will be done in Process() + onloop->QueueForProcessing(std::move(event)); +} + +void WebSocketEventDispatcher::QueueReply(WebSocketReply&& reply) { + // Delegate to the reply thread. + reply_msg_thread->SendIn(new ReplyInputMessage(std::move(reply))); +} + +// WebSocketDemux::Process() runs in the main thread. +// +// XXX: How is this going to work with class broker? With +// ZeroMQ, each WebSocket client has its own XPUB/XSUB +// connectivity to a central broker and similarly with NATS. +// But with broker we need to do something different. +// Maybe connect to the local endpoint. +// +// We cannot actually instantiate a Broker backend :-( +// +// We could also have InitPostScript() recognize Broker +// and start its internal server instead. +void WebSocketEventDispatcher::Process(const WebSocketEvent& event) { + std::visit([this](auto&& arg) { Process(arg); }, event); +} + +void WebSocketEventDispatcher::Process(const WebSocketOpen& open) { + const auto& wsc = open.wsc; + const auto& id = open.id; + const auto& it = clients.find(id); + if ( it != clients.end() ) { + // This shouldn't happen! + reporter->Error("Open for existing WebSocket client with id %s!", id.c_str()); + QueueReply(WebSocketCloseReply{wsc, 1001, "Internal error"}); + return; + } + + // As of now, terminate clients coming to anything other than /v1/messages/json. + if ( open.uri != "/v1/messages/json" ) { + open.wsc->SendError("invalid_uri", "Invalid URI - use /v1/messages/json"); + open.wsc->Close(1008, "Invalid URI - use /v1/messages/json"); + + // Still create an entry as we might see messages and close events coming in. + clients[id] = WebSocketClientEntry{id, wsc, nullptr}; + return; + } + + // Generate an ID for this client. + auto ws_id = cluster::backend->NodeId() + "-websocket-" + id; + + const auto& event_serializer_val = id::find_val("Cluster::event_serializer"); + auto event_serializer = cluster::manager->InstantiateEventSerializer(event_serializer_val); + const auto& cluster_backend_val = id::find_val("Cluster::backend"); + auto event_handling_strategy = std::make_unique(wsc, this); + auto backend = zeek::cluster::manager->InstantiateBackend(cluster_backend_val, std::move(event_serializer), nullptr, + std::move(event_handling_strategy)); + + WS_DEBUG("New WebSocket client %s (%s:%d) - using id %s backend=%p", id.c_str(), wsc->getRemoteIp().c_str(), + wsc->getRemotePort(), ws_id.c_str(), backend.get()); + + // XXX: We call InitPostScript to populate member vars required for connectivity. + backend->InitPostScript(); + backend->Init(std::move(ws_id)); + + clients[id] = WebSocketClientEntry{id, wsc, std::move(backend)}; +} + +void WebSocketEventDispatcher::Process(const WebSocketClose& close) { + const auto& id = close.id; + const auto& it = clients.find(id); + + if ( it == clients.end() ) { + reporter->Error("Close from non-existing WebSocket client with id %s!", id.c_str()); + return; + } + + auto& wsc = it->second.wsc; + auto& backend = it->second.backend; + + WS_DEBUG("Close from client %s (%s:%d) backend=%p", wsc->getId().c_str(), wsc->getRemoteIp().c_str(), + wsc->getRemotePort(), backend.get()); + + // If the client doesn't have a backend, it wasn't ever properly instantiated. + if ( backend ) { + auto rec = zeek::cluster::detail::bif::make_endpoint_info(backend->NodeId(), wsc->getRemoteIp(), + wsc->getRemotePort(), TRANSPORT_TCP); + zeek::event_mgr.Enqueue(Cluster::websocket_client_lost, std::move(rec)); + + backend->Terminate(); + } + + clients.erase(it); +} + +// SubscribeFinished is produced internally. +void WebSocketEventDispatcher::Process(const WebSocketSubscribeFinished& fin) { + const auto& it = clients.find(fin.id); + if ( it == clients.end() ) { + reporter->Error("Subscribe finished from non-existing WebSocket client with id %s!", fin.id.c_str()); + return; + } + + auto& entry = it->second; + auto& wsc = entry.wsc; + + entry.wsc->SetSubscriptionActive(fin.topic_prefix); + + if ( ! entry.wsc->AllSubscriptionsActive() ) { + // More subscriptions to come. + return; + } + + auto rec = zeek::cluster::detail::bif::make_endpoint_info(backend->NodeId(), wsc->getRemoteIp(), + wsc->getRemotePort(), TRANSPORT_TCP); + auto subscriptions_vec = zeek::cluster::detail::bif::make_string_vec(wsc->GetSubscriptions()); + zeek::event_mgr.Enqueue(Cluster::websocket_client_added, std::move(rec), std::move(subscriptions_vec)); + + entry.wsc->SendAck(entry.backend->NodeId(), zeek::zeek_version()); + + WS_DEBUG("Sent Ack to %s %s\n", fin.id.c_str(), entry.backend->NodeId().c_str()); + + // Process any queued messages now. + for ( auto& msg : entry.queue ) { + assert(entry.msg_count > 1); + Process(msg); + } +} + +void WebSocketEventDispatcher::HandleSubscriptions(WebSocketClientEntry& entry, std::string_view buf) { + rapidjson::Document doc; + doc.Parse(buf.data(), buf.size()); + if ( ! doc.IsArray() ) { + entry.wsc->SendError(broker::enum_str(broker::ec::deserialization_failed), "subscriptions not an array"); + return; + } + + std::vector subscriptions; + + for ( rapidjson::SizeType i = 0; i < doc.Size(); i++ ) { + if ( ! doc[i].IsString() ) { + entry.wsc->SendError(broker::enum_str(broker::ec::deserialization_failed), + "individual subscription not a string"); + return; + } + + subscriptions.emplace_back(doc[i].GetString()); + } + + entry.wsc->SetSubscriptions(subscriptions); + + auto cb = [this, id = entry.id, wsc = entry.wsc](const std::string& topic, + const Backend::SubscriptionCallbackInfo& info) { + if ( info.status == Backend::CallbackStatus::Error ) { + zeek::reporter->Error("Subscribe for WebSocket client failed!"); + + // Is this going to work out? + QueueReply(WebSocketCloseReply{wsc, 1011, "Could not subscribe. Something bad happened!"}); + } + else { + Process(WebSocketSubscribeFinished{id, topic}); + } + }; + + for ( const auto& subscription : subscriptions ) { + if ( ! entry.backend->Subscribe(subscription, cb) ) { + zeek::reporter->Error("Subscribe for WebSocket client failed!"); + QueueReply(WebSocketCloseReply{entry.wsc, 1011, "Could not subscribe. Something bad happened!"}); + } + } +} + +void WebSocketEventDispatcher::HandleEvent(WebSocketClientEntry& entry, std::string_view buf) { + // Unserialize the message as an event. + broker::variant res; + auto err = broker::format::json::v1::decode(buf, res); + if ( err ) { + entry.wsc->SendError(broker::enum_str(broker::ec::deserialization_failed), "failed to decode JSON object"); + return; + } + + std::string topic = std::string(res->shared_envelope()->topic()); + + if ( topic == broker::topic::reserved ) { + entry.wsc->SendError(broker::enum_str(broker::ec::deserialization_failed), "no topic in top-level JSON object"); + return; + } + + broker::zeek::Event broker_ev(std::move(res)); + + // This is not guaranteed to work! If the node running the WebSocket + // API does not have the declaration of the event that another node + // is sending, it cannot instantiate the zeek::cluster::Event for + // re-publishing to a cluster backend. + // + // Does that make conceptional sense? Basically the WebSocket API + // has Zeek-script awareness. + // + // It works with Broker today because Broker treats messages opaquely. + // It knows how to convert from JSON into Broker binary format as these + // are compatible. + // + // However, the broker format is under specified (vectors are used for various + // types without being tagged explicitly), so it's not possible to determine + // the final Zeek type without having access to the script-layer. + // + // I'm not sure this is a real problem, other than it being unfortunate that + // the Zeek process running the WebSocket API requires access to all declarations + // of events being transmitted via WebSockets. Though this might be a given anyhow. + // + // See broker/Data.cc for broker::vector conversion to see the collisions: + // vector, list, func, record, pattern, opaque are all encoded using + // broker::vector rather than dedicated types. + // + // Switching to a JSON v2 format that ensures all Zeek types are represented + // explicitly would help. + const auto& zeek_ev = cluster::detail::to_zeek_event(broker_ev); + if ( ! zeek_ev ) { + entry.wsc->SendError(broker::enum_str(broker::ec::deserialization_failed), "failed to create Zeek event"); + return; + } + + WS_DEBUG("Publishing event %s to topic '%s'", std::string(zeek_ev->HandlerName()).c_str(), topic.c_str()); + entry.backend->PublishEvent(topic, *zeek_ev); +} + +// Process a WebSocket message from a client. +// +// If it's the first message, the code is expecting a subscriptions +// array, otherwise it'll be a remote event. +void WebSocketEventDispatcher::Process(const WebSocketMessage& msg) { + const auto& id = msg.id; + + const auto& it = clients.find(id); + if ( it == clients.end() ) { + reporter->Error("WebSocket message from non-existing WebSocket client %s", id.c_str()); + return; + } + + // Client without backend wasn't accepted, just discard its message. + if ( ! it->second.backend ) + return; + + auto& entry = it->second; + const auto& wsc = entry.wsc; + entry.msg_count++; + + WS_DEBUG("Message %" PRIu64 " size=%zu from %s (%s:%d) backend=%p", entry.msg_count, msg.msg.size(), + wsc->getId().c_str(), wsc->getRemoteIp().c_str(), wsc->getRemotePort(), entry.backend.get()); + + // First message is the subscription message. + if ( entry.msg_count == 1 ) { + WS_DEBUG("Subscriptions from client: %s: (%s:%d)\n", id.c_str(), wsc->getRemoteIp().c_str(), + wsc->getRemotePort()); + HandleSubscriptions(entry, msg.msg); + } + else { + if ( ! wsc->IsAcked() ) { + WS_DEBUG("Client sending messages before receiving ack!"); + entry.queue.push_back(msg); + return; + } + + HandleEvent(entry, msg.msg); + } +} diff --git a/src/cluster/websocket/WebSocket.h b/src/cluster/websocket/WebSocket.h new file mode 100644 index 0000000000..8d8e51ceee --- /dev/null +++ b/src/cluster/websocket/WebSocket.h @@ -0,0 +1,321 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace zeek { + +namespace detail { + +template +class OnLoopProcess; +} + +namespace cluster { + +class Backend; + +namespace websocket::detail { + + +/** + * Library independent interface for a WebSocket client. + * + * All methods should be safe to be called from Zeek's + * main thread, though some may fail if the client has vanished + * or vanishes during an operation. + */ +class WebSocketClient { +public: + virtual ~WebSocketClient() = default; + + /** + * @returns true if the WebSocket client has terminated + */ + virtual bool IsTerminated() const = 0; + + /** + * Close the WebSocket connection with the given code/reason. + */ + virtual void Close(uint16_t code = 1000, const std::string& reason = "Normal closure") = 0; + + /** + * Information about the send operation. + */ + struct SendInfo { + bool success; + }; + + /** + * Thread safe sending. + * + * This might be called from Zeek's main thread and + * must be safe to be called whether or not the connection + * with the client is still alive. + * + * @param sv The buffer to send as a WebSocket message. + */ + virtual SendInfo SendText(std::string_view sv) = 0; + + /** + * Send an error in Broker JSON/v1 format to the client. + */ + SendInfo SendError(std::string_view code, std::string_view ctx); + + /** + * Send an ACK message Broker JSON/v1 format to the client. + */ + SendInfo SendAck(std::string_view endpoint, std::string_view version); + + /** + * @return - has an ACK been sent to the client? + */ + bool IsAcked() const { return acked; } + + /** + * @return The WebSocket client's identifier. + */ + virtual const std::string& getId() = 0; + + /** + * @return The WebSocket client's remote IP address. + */ + virtual const std::string& getRemoteIp() = 0; + + /** + * @return The WebSocket client's remote port. + */ + virtual int getRemotePort() = 0; + + /** + * Store the client's subscriptions as "not active". + */ + void SetSubscriptions(const std::vector& topic_prefixes); + + /** + * @return The client's subscriptions. + */ + const std::vector GetSubscriptions() const; + + /** + * Store the client's subscriptions as "not active". + */ + void SetSubscriptionActive(const std::string& topic_prefix); + + /** + * @return true if all subscriptions have an active status. + */ + bool AllSubscriptionsActive() const; + +private: + bool acked = false; + std::map subscriptions_state; +}; + +// An new WebSocket client connected. Client is locally identified by `id`. +struct WebSocketOpen { + std::string id; + std::string uri; + std::string protocol; + std::shared_ptr wsc; +}; + +// A WebSocket client disconnected. +struct WebSocketClose { + std::string id; +}; + +// A WebSocket client send a message. +struct WebSocketMessage { + std::string id; + std::string msg; +}; + +// Produced internally when a WebSocket client's +// subscription has completed. +struct WebSocketSubscribeFinished { + std::string id; + std::string topic_prefix; +}; + +using WebSocketEvent = std::variant; + +struct WebSocketSendReply { + std::shared_ptr wsc; + std::string msg; +}; + +struct WebSocketCloseReply { + std::shared_ptr wsc; + uint16_t code = 1000; + std::string reason = "Normal closure"; +}; + +using WebSocketReply = std::variant; + + +class ReplyMsgThread; + +/** + * Handle events produced by WebSocket clients. + * + * Any thread may call QueueForProcessing(). Process() runs + * on Zeek's main thread. + */ +class WebSocketEventDispatcher { +public: + WebSocketEventDispatcher(); + + ~WebSocketEventDispatcher(); + + /** + * Called shutting down a WebSocket server. + */ + void Terminate(); + + /** + * Queue the given WebSocket event to be processed on Zeek's main loop. + * + * @param work The WebSocket event to process. + */ + void QueueForProcessing(WebSocketEvent&& event); + + /** + * Send a reply to the given websocket client. + * + * The dispatcher has an internal thread for serializing + * and sending out the event. + */ + void QueueReply(WebSocketReply&& reply); + +private: + /** + * Main processing function of the dispatcher. + * + * This runs on Zeek's main thread. + */ + void Process(const WebSocketEvent& event); + + void Process(const WebSocketOpen& open); + void Process(const WebSocketSubscribeFinished& fin); + void Process(const WebSocketMessage& msg); + void Process(const WebSocketClose& close); + + + /** + * Data structure for tracking WebSocket clients. + */ + struct WebSocketClientEntry { + std::string id; + std::shared_ptr wsc; + std::shared_ptr backend; + uint64_t msg_count = 0; + std::list queue; + }; + + + void HandleSubscriptions(WebSocketClientEntry& entry, std::string_view buf); + void HandleEvent(WebSocketClientEntry& entry, std::string_view buf); + + // Allow access to Process(WebSocketEvent) + friend zeek::detail::OnLoopProcess; + + // Clients that this dispatcher is tracking. + std::map clients; + + // Connector to the IO loop. + zeek::detail::OnLoopProcess* onloop = nullptr; + + // Thread replying to clients. Zeek's threading manager takes ownership. + ReplyMsgThread* reply_msg_thread = nullptr; +}; + +/** + * An abstract WebSocket server. + */ +class WebSocketServer { +public: + WebSocketServer(std::unique_ptr demux) : dispatcher(std::move(demux)) {} + virtual ~WebSocketServer() = default; + + /** + * Stop this server. + */ + void Terminate() { + dispatcher->Terminate(); + + DoTerminate(); + } + + WebSocketEventDispatcher& Dispatcher() { return *dispatcher; } + +private: + /** + * Hook to be implemented when a server is terminated. + */ + virtual void DoTerminate() = 0; + + std::unique_ptr dispatcher; +}; + +/** + * TLS configuration for a WebSocket server. + */ +struct TLSOptions { + std::optional cert_file; + std::optional key_file; + bool enable_peer_verification = false; + std::string ca_file; + std::string ciphers; + + /** + * Is TLS enabled? + */ + bool TlsEnabled() const { return cert_file.has_value() && key_file.has_value(); } + + bool operator==(const TLSOptions& o) const { + return cert_file == o.cert_file && key_file == o.key_file && + enable_peer_verification == o.enable_peer_verification && ca_file == o.ca_file && ciphers == o.ciphers; + } +}; + +/** + * Options for a WebSocket server. + */ +struct ServerOptions { + std::string host; + uint16_t port = 0; + int ping_interval_seconds = 5; + int max_connections = 100; + bool per_message_deflate = false; + struct TLSOptions tls_options; + + bool operator==(const ServerOptions& o) const { + return host == o.host && port == o.port && ping_interval_seconds == o.ping_interval_seconds && + max_connections == o.max_connections && per_message_deflate == o.per_message_deflate && + tls_options == o.tls_options; + } +}; + + +/** + * Start a WebSocket server. + * + * @param dispatcher The dispatcher to use for the server. + * @param options Options for the server. + * + * @return Pointer to a new WebSocketServer instance or nullptr on error. + */ +std::unique_ptr StartServer(std::unique_ptr dispatcher, + const ServerOptions& options); + +} // namespace websocket::detail +} // namespace cluster +} // namespace zeek diff --git a/src/cluster/websocket/auxil/CMakeLists.txt b/src/cluster/websocket/auxil/CMakeLists.txt new file mode 100644 index 0000000000..5131dbf0ac --- /dev/null +++ b/src/cluster/websocket/auxil/CMakeLists.txt @@ -0,0 +1,7 @@ +option(IXWEBSOCKET_INSTALL "Install IXWebSocket" OFF) + +set(BUILD_SHARED_LIBS OFF) +set(USE_TLS ON) +set(USE_OPEN_SSL ON) + +add_subdirectory(IXWebSocket) diff --git a/src/cluster/websocket/events.bif b/src/cluster/websocket/events.bif new file mode 100644 index 0000000000..d730b42419 --- /dev/null +++ b/src/cluster/websocket/events.bif @@ -0,0 +1,13 @@ +module Cluster; + +## Generated when a new WebSocket client has connected. +## +## endpoint: Various information about the WebSocket client. +## +## subscriptions: The WebSocket client's subscriptions as provided in the handshake. +event websocket_client_added%(endpoint: EndpointInfo, subscriptions: string_vec%); + +## Generated when a WebSocket client was lost. +## +## endpoint: Various information about the WebSocket client. +event websocket_client_lost%(endpoint: EndpointInfo%); diff --git a/src/script_opt/FuncInfo.cc b/src/script_opt/FuncInfo.cc index 8b7a19b81f..8d836facb3 100644 --- a/src/script_opt/FuncInfo.cc +++ b/src/script_opt/FuncInfo.cc @@ -74,6 +74,7 @@ static std::unordered_map func_attrs = { {"Analyzer::__tag", ATTR_FOLDABLE}, {"Cluster::Backend::ZeroMQ::spawn_zmq_proxy_thread", ATTR_NO_SCRIPT_SIDE_EFFECTS}, {"Cluster::Backend::__init", ATTR_NO_SCRIPT_SIDE_EFFECTS}, + {"Cluster::__listen_websocket", ATTR_NO_SCRIPT_SIDE_EFFECTS}, {"Cluster::__subscribe", ATTR_NO_SCRIPT_SIDE_EFFECTS}, {"Cluster::__unsubscribe", ATTR_NO_SCRIPT_SIDE_EFFECTS}, {"Cluster::make_event", ATTR_NO_SCRIPT_SIDE_EFFECTS}, diff --git a/src/zeek-setup.cc b/src/zeek-setup.cc index 5af17f0d8f..05e4e0fb52 100644 --- a/src/zeek-setup.cc +++ b/src/zeek-setup.cc @@ -378,6 +378,7 @@ static void terminate_zeek() { notifier::detail::registry.Terminate(); log_mgr->Terminate(); input_mgr->Terminate(); + cluster::manager->Terminate(); thread_mgr->Terminate(); broker_mgr->Terminate(); diff --git a/testing/btest/Baseline.zam/cluster.websocket.listen-idempotent/.stderr b/testing/btest/Baseline.zam/cluster.websocket.listen-idempotent/.stderr new file mode 100644 index 0000000000..889b895763 --- /dev/null +++ b/testing/btest/Baseline.zam/cluster.websocket.listen-idempotent/.stderr @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +error in <...>/main.zeek, line 654: Already listening on 127.0.0.1: (Cluster::__listen_websocket(ws_opts_x)) +error in <...>/main.zeek, line 654: Already listening on 127.0.0.1: (Cluster::__listen_websocket(ws_opts_wss_port)) +received termination signal diff --git a/testing/btest/Baseline.zam/cluster.websocket.tls-usage-error/.stderr b/testing/btest/Baseline.zam/cluster.websocket.tls-usage-error/.stderr new file mode 100644 index 0000000000..0e09ca4de9 --- /dev/null +++ b/testing/btest/Baseline.zam/cluster.websocket.tls-usage-error/.stderr @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +error in <...>/main.zeek, line 654: Invalid tls_options: No key_file field (Cluster::__listen_websocket(Cluster::options.0)) +error in <...>/main.zeek, line 654: Invalid tls_options: No cert_file field (Cluster::__listen_websocket(Cluster::options.3)) diff --git a/testing/btest/Baseline/cluster.websocket.bad-event-args/..client..stderr b/testing/btest/Baseline/cluster.websocket.bad-event-args/..client..stderr new file mode 100644 index 0000000000..49d861c74c --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-event-args/..client..stderr @@ -0,0 +1 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. diff --git a/testing/btest/Baseline/cluster.websocket.bad-event-args/..client.out b/testing/btest/Baseline/cluster.websocket.bad-event-args/..client.out new file mode 100644 index 0000000000..f9e01b1fea --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-event-args/..client.out @@ -0,0 +1,7 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Connected! +err1 {'type': 'error', 'code': 'deserialization_failed', 'message': 'failed to decode JSON object'} +err2 {'type': 'error', 'code': 'deserialization_failed', 'message': 'failed to create Zeek event'} +err3 {'type': 'error', 'code': 'deserialization_failed', 'message': 'failed to create Zeek event'} +pong {'@data-type': 'string', 'data': 'pong'} {'@data-type': 'vector', 'data': [{'@data-type': 'string', 'data': 'Hello'}, {'@data-type': 'count', 'data': 42}]} +err4 {'type': 'error', 'code': 'deserialization_failed', 'message': 'failed to decode JSON object'} diff --git a/testing/btest/Baseline/cluster.websocket.bad-event-args/..manager..stderr b/testing/btest/Baseline/cluster.websocket.bad-event-args/..manager..stderr new file mode 100644 index 0000000000..2141fb2cd1 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-event-args/..manager..stderr @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +error: Unserialize error 'ping' arg_types.size()=2 and args.size()=1 +error: Unserialize error for event 'ping': broker value '42' type 'count' to Zeek type 'string string' failed +received termination signal diff --git a/testing/btest/Baseline/cluster.websocket.bad-event-args/..manager.out b/testing/btest/Baseline/cluster.websocket.bad-event-args/..manager.out new file mode 100644 index 0000000000..78d2341e0b --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-event-args/..manager.out @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Cluster::websocket_client_added, [/zeek/event/my_topic] +got ping: Hello, 42 +Cluster::websocket_client_lost diff --git a/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..client..stderr b/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..client..stderr new file mode 100644 index 0000000000..49d861c74c --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..client..stderr @@ -0,0 +1 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. diff --git a/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..client.out b/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..client.out new file mode 100644 index 0000000000..228c6687c9 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..client.out @@ -0,0 +1,5 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +broken array response {'type': 'error', 'code': 'deserialization_failed', 'message': 'subscriptions not an array'} +non string error {'type': 'error', 'code': 'deserialization_failed', 'message': 'individual subscription not a string'} +mix error {'type': 'error', 'code': 'deserialization_failed', 'message': 'individual subscription not a string'} +ack True diff --git a/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..manager..stderr b/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..manager..stderr new file mode 100644 index 0000000000..e3f6131b1d --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..manager..stderr @@ -0,0 +1,2 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +received termination signal diff --git a/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..manager.out b/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..manager.out new file mode 100644 index 0000000000..29ee473cda --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-subscriptions/..manager.out @@ -0,0 +1,6 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Cluster::websocket_client_lost, 1 +Cluster::websocket_client_lost, 2 +Cluster::websocket_client_lost, 3 +Cluster::websocket_client_added, 1, [/duplicate, /is/okay, /topic/good] +Cluster::websocket_client_lost, 4 diff --git a/testing/btest/Baseline/cluster.websocket.bad-url/..client..stderr b/testing/btest/Baseline/cluster.websocket.bad-url/..client..stderr new file mode 100644 index 0000000000..49d861c74c --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-url/..client..stderr @@ -0,0 +1 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. diff --git a/testing/btest/Baseline/cluster.websocket.bad-url/..client.out b/testing/btest/Baseline/cluster.websocket.bad-url/..client.out new file mode 100644 index 0000000000..360a9982a2 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-url/..client.out @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Connected ws_good! +Connected ws_bad! +Error for ws_bad {'type': 'error', 'code': 'invalid_uri', 'message': 'Invalid URI - use /v1/messages/json'} diff --git a/testing/btest/Baseline/cluster.websocket.bad-url/..manager..stderr b/testing/btest/Baseline/cluster.websocket.bad-url/..manager..stderr new file mode 100644 index 0000000000..e3f6131b1d --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-url/..manager..stderr @@ -0,0 +1,2 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +received termination signal diff --git a/testing/btest/Baseline/cluster.websocket.bad-url/..manager.out b/testing/btest/Baseline/cluster.websocket.bad-url/..manager.out new file mode 100644 index 0000000000..725d3a7f80 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.bad-url/..manager.out @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Cluster::websocket_client_added, [hello-good] +Cluster::websocket_client_lost diff --git a/testing/btest/Baseline/cluster.websocket.cluster-log/..client..stderr b/testing/btest/Baseline/cluster.websocket.cluster-log/..client..stderr new file mode 100644 index 0000000000..49d861c74c --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.cluster-log/..client..stderr @@ -0,0 +1 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. diff --git a/testing/btest/Baseline/cluster.websocket.cluster-log/..client.out b/testing/btest/Baseline/cluster.websocket.cluster-log/..client.out new file mode 100644 index 0000000000..bc1ccc095a --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.cluster-log/..client.out @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Connected! +unique ids 3 diff --git a/testing/btest/Baseline/cluster.websocket.cluster-log/..manager..stderr b/testing/btest/Baseline/cluster.websocket.cluster-log/..manager..stderr new file mode 100644 index 0000000000..e3f6131b1d --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.cluster-log/..manager..stderr @@ -0,0 +1,2 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +received termination signal diff --git a/testing/btest/Baseline/cluster.websocket.cluster-log/..manager.cluster.log.cannonified b/testing/btest/Baseline/cluster.websocket.cluster-log/..manager.cluster.log.cannonified new file mode 100644 index 0000000000..bc1ec612fb --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.cluster-log/..manager.cluster.log.cannonified @@ -0,0 +1,7 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +manager WebSocket client (127.0.0.1:) subscribed to [/topic/ws/1, /topic/ws/all] +manager WebSocket client (127.0.0.1:) subscribed to [/topic/ws/2, /topic/ws/all] +manager WebSocket client (127.0.0.1:) subscribed to [/topic/ws/3, /topic/ws/all] +manager WebSocket client (127.0.0.1:) gone +manager WebSocket client (127.0.0.1:) gone +manager WebSocket client (127.0.0.1:) gone diff --git a/testing/btest/Baseline/cluster.websocket.cluster-log/..manager.out b/testing/btest/Baseline/cluster.websocket.cluster-log/..manager.out new file mode 100644 index 0000000000..958fdd87a3 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.cluster-log/..manager.out @@ -0,0 +1,7 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Cluster::websocket_client_added, 1, [/topic/ws/1, /topic/ws/all] +Cluster::websocket_client_added, 2, [/topic/ws/2, /topic/ws/all] +Cluster::websocket_client_added, 3, [/topic/ws/3, /topic/ws/all] +Cluster::websocket_client_lost, 1 +Cluster::websocket_client_lost, 2 +Cluster::websocket_client_lost, 3 diff --git a/testing/btest/Baseline/cluster.websocket.listen-idempotent/.stderr b/testing/btest/Baseline/cluster.websocket.listen-idempotent/.stderr new file mode 100644 index 0000000000..e2bea8f00f --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.listen-idempotent/.stderr @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +error in <...>/listen-idempotent.zeek, line 43: Already listening on 127.0.0.1: (Cluster::listen_websocket(ws_opts_x)) +error in <...>/listen-idempotent.zeek, line 47: Already listening on 127.0.0.1: (Cluster::listen_websocket(ws_opts_wss_port)) +received termination signal diff --git a/testing/btest/Baseline/cluster.websocket.one-pipelining/..client..stderr b/testing/btest/Baseline/cluster.websocket.one-pipelining/..client..stderr new file mode 100644 index 0000000000..49d861c74c --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.one-pipelining/..client..stderr @@ -0,0 +1 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. diff --git a/testing/btest/Baseline/cluster.websocket.one-pipelining/..client.out b/testing/btest/Baseline/cluster.websocket.one-pipelining/..client.out new file mode 100644 index 0000000000..b8dcb923f8 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.one-pipelining/..client.out @@ -0,0 +1,17 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Connected! +Sending ping 0 +Sending ping 1 +Sending ping 2 +Sending ping 3 +Sending ping 4 +Receiving pong 0 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 1}] +Receiving pong 1 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 2}] +Receiving pong 2 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 3}] +Receiving pong 3 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 4}] +Receiving pong 4 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 5}] diff --git a/testing/btest/Baseline/cluster.websocket.one-pipelining/..manager..stderr b/testing/btest/Baseline/cluster.websocket.one-pipelining/..manager..stderr new file mode 100644 index 0000000000..e3f6131b1d --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.one-pipelining/..manager..stderr @@ -0,0 +1,2 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +received termination signal diff --git a/testing/btest/Baseline/cluster.websocket.one-pipelining/..manager.out b/testing/btest/Baseline/cluster.websocket.one-pipelining/..manager.out new file mode 100644 index 0000000000..88a473b3b8 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.one-pipelining/..manager.out @@ -0,0 +1,8 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Cluster::websocket_client_added, [/zeek/event/my_topic] +got ping: python-websocket-client, 0 +got ping: python-websocket-client, 1 +got ping: python-websocket-client, 2 +got ping: python-websocket-client, 3 +got ping: python-websocket-client, 4 +Cluster::websocket_client_lost diff --git a/testing/btest/Baseline/cluster.websocket.one/..client..stderr b/testing/btest/Baseline/cluster.websocket.one/..client..stderr new file mode 100644 index 0000000000..49d861c74c --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.one/..client..stderr @@ -0,0 +1 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. diff --git a/testing/btest/Baseline/cluster.websocket.one/..client.out b/testing/btest/Baseline/cluster.websocket.one/..client.out new file mode 100644 index 0000000000..f637d596f8 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.one/..client.out @@ -0,0 +1,17 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Connected! +Sending ping 0 +Receiving pong 0 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 1}] +Sending ping 1 +Receiving pong 1 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 2}] +Sending ping 2 +Receiving pong 2 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 3}] +Sending ping 3 +Receiving pong 3 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 4}] +Sending ping 4 +Receiving pong 4 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 5}] diff --git a/testing/btest/Baseline/cluster.websocket.one/..manager..stderr b/testing/btest/Baseline/cluster.websocket.one/..manager..stderr new file mode 100644 index 0000000000..e3f6131b1d --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.one/..manager..stderr @@ -0,0 +1,2 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +received termination signal diff --git a/testing/btest/Baseline/cluster.websocket.one/..manager.out b/testing/btest/Baseline/cluster.websocket.one/..manager.out new file mode 100644 index 0000000000..88a473b3b8 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.one/..manager.out @@ -0,0 +1,8 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Cluster::websocket_client_added, [/zeek/event/my_topic] +got ping: python-websocket-client, 0 +got ping: python-websocket-client, 1 +got ping: python-websocket-client, 2 +got ping: python-websocket-client, 3 +got ping: python-websocket-client, 4 +Cluster::websocket_client_lost diff --git a/testing/btest/Baseline/cluster.websocket.three/..client..stderr b/testing/btest/Baseline/cluster.websocket.three/..client..stderr new file mode 100644 index 0000000000..49d861c74c --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.three/..client..stderr @@ -0,0 +1 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. diff --git a/testing/btest/Baseline/cluster.websocket.three/..client.out b/testing/btest/Baseline/cluster.websocket.three/..client.out new file mode 100644 index 0000000000..ab54483ded --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.three/..client.out @@ -0,0 +1,83 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Connected! +unique ids 3 +ws1 sending ping 0 +receiving pong 0 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 1}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 1}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 1}] +ws2 sending ping 1 +receiving pong 1 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 2}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 2}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 2}] +ws3 sending ping 2 +receiving pong 2 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 3}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 3}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 3}] +ws1 sending ping 3 +receiving pong 3 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 4}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 4}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 4}] +ws2 sending ping 4 +receiving pong 4 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 5}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 5}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 5}] +ws3 sending ping 5 +receiving pong 5 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 6}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 6}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 6}] +ws1 sending ping 6 +receiving pong 6 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 7}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 7}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 7}] +ws2 sending ping 7 +receiving pong 7 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 8}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 8}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 8}] +ws3 sending ping 8 +receiving pong 8 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 9}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 9}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 9}] +ws1 sending ping 9 +receiving pong 9 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 10}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 10}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 10}] +ws2 sending ping 10 +receiving pong 10 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 11}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 11}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 11}] +ws3 sending ping 11 +receiving pong 11 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 12}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 12}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 12}] +ws1 sending ping 12 +receiving pong 12 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 13}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 13}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 13}] +ws2 sending ping 13 +receiving pong 13 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 14}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 14}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws2'}, {'@data-type': 'count', 'data': 14}] +ws3 sending ping 14 +receiving pong 14 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 15}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 15}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws3'}, {'@data-type': 'count', 'data': 15}] +ws1 sending ping 15 +receiving pong 15 +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 16}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 16}] +ev: topic /test/clients event name pong args [{'@data-type': 'string', 'data': 'orig_msg=ws1'}, {'@data-type': 'count', 'data': 16}] diff --git a/testing/btest/Baseline/cluster.websocket.three/..manager..stderr b/testing/btest/Baseline/cluster.websocket.three/..manager..stderr new file mode 100644 index 0000000000..e3f6131b1d --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.three/..manager..stderr @@ -0,0 +1,2 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +received termination signal diff --git a/testing/btest/Baseline/cluster.websocket.three/..manager.out b/testing/btest/Baseline/cluster.websocket.three/..manager.out new file mode 100644 index 0000000000..30dca05f31 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.three/..manager.out @@ -0,0 +1,23 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Cluster::websocket_client_added, 1, [/test/clients] +Cluster::websocket_client_added, 2, [/test/clients] +Cluster::websocket_client_added, 3, [/test/clients] +got ping: ws1, 0 +got ping: ws2, 1 +got ping: ws3, 2 +got ping: ws1, 3 +got ping: ws2, 4 +got ping: ws3, 5 +got ping: ws1, 6 +got ping: ws2, 7 +got ping: ws3, 8 +got ping: ws1, 9 +got ping: ws2, 10 +got ping: ws3, 11 +got ping: ws1, 12 +got ping: ws2, 13 +got ping: ws3, 14 +got ping: ws1, 15 +Cluster::websocket_client_lost, 1 +Cluster::websocket_client_lost, 2 +Cluster::websocket_client_lost, 3 diff --git a/testing/btest/Baseline/cluster.websocket.tls-usage-error/.stderr b/testing/btest/Baseline/cluster.websocket.tls-usage-error/.stderr new file mode 100644 index 0000000000..46b508db58 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.tls-usage-error/.stderr @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +error in <...>/tls-usage-error.zeek, line 17: Invalid tls_options: No key_file field (Cluster::listen_websocket((coerce [$listen_host=127.0.0.1, $listen_port=1234/tcp, $tls_options=tls_options_no_key] to Cluster::WebSocketServerOptions))) +error in <...>/tls-usage-error.zeek, line 18: Invalid tls_options: No cert_file field (Cluster::listen_websocket((coerce [$listen_host=127.0.0.1, $listen_port=1234/tcp, $tls_options=tls_options_no_cert] to Cluster::WebSocketServerOptions))) diff --git a/testing/btest/Baseline/cluster.websocket.tls/..client..stderr b/testing/btest/Baseline/cluster.websocket.tls/..client..stderr new file mode 100644 index 0000000000..49d861c74c --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.tls/..client..stderr @@ -0,0 +1 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. diff --git a/testing/btest/Baseline/cluster.websocket.tls/..client.out b/testing/btest/Baseline/cluster.websocket.tls/..client.out new file mode 100644 index 0000000000..f637d596f8 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.tls/..client.out @@ -0,0 +1,17 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Connected! +Sending ping 0 +Receiving pong 0 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 1}] +Sending ping 1 +Receiving pong 1 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 2}] +Sending ping 2 +Receiving pong 2 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 3}] +Sending ping 3 +Receiving pong 3 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 4}] +Sending ping 4 +Receiving pong 4 +topic /zeek/event/my_topic event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 5}] diff --git a/testing/btest/Baseline/cluster.websocket.tls/..manager..stderr b/testing/btest/Baseline/cluster.websocket.tls/..manager..stderr new file mode 100644 index 0000000000..e3f6131b1d --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.tls/..manager..stderr @@ -0,0 +1,2 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +received termination signal diff --git a/testing/btest/Baseline/cluster.websocket.tls/..manager.out b/testing/btest/Baseline/cluster.websocket.tls/..manager.out new file mode 100644 index 0000000000..88a473b3b8 --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.tls/..manager.out @@ -0,0 +1,8 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Cluster::websocket_client_added, [/zeek/event/my_topic] +got ping: python-websocket-client, 0 +got ping: python-websocket-client, 1 +got ping: python-websocket-client, 2 +got ping: python-websocket-client, 3 +got ping: python-websocket-client, 4 +Cluster::websocket_client_lost diff --git a/testing/btest/Baseline/cluster.websocket.two-pipelining/..client..stderr b/testing/btest/Baseline/cluster.websocket.two-pipelining/..client..stderr new file mode 100644 index 0000000000..49d861c74c --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.two-pipelining/..client..stderr @@ -0,0 +1 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. diff --git a/testing/btest/Baseline/cluster.websocket.two-pipelining/..client.out b/testing/btest/Baseline/cluster.websocket.two-pipelining/..client.out new file mode 100644 index 0000000000..616a268fed --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.two-pipelining/..client.out @@ -0,0 +1,54 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +Connected! +Sending ping 0 - ws1 +Sending ping 0 - ws2 +Sending ping 1 - ws1 +Sending ping 1 - ws2 +Sending ping 2 - ws1 +Sending ping 2 - ws2 +Sending ping 3 - ws1 +Sending ping 3 - ws2 +Sending ping 4 - ws1 +Sending ping 4 - ws2 +Receiving ack - ws1 +Receiving ack - ws2 +Receiving pong 0 - ws0 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 1}] +Receiving pong 0 - ws1 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 1}] +Receiving pong 1 - ws0 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 2}] +Receiving pong 1 - ws1 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 2}] +Receiving pong 2 - ws0 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 3}] +Receiving pong 2 - ws1 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 3}] +Receiving pong 3 - ws0 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 4}] +Receiving pong 3 - ws1 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 4}] +Receiving pong 4 - ws0 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 5}] +Receiving pong 4 - ws1 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 5}] +Receiving pong 5 - ws0 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 6}] +Receiving pong 5 - ws1 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 6}] +Receiving pong 6 - ws0 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 7}] +Receiving pong 6 - ws1 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 7}] +Receiving pong 7 - ws0 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 8}] +Receiving pong 7 - ws1 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 8}] +Receiving pong 8 - ws0 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 9}] +Receiving pong 8 - ws1 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 9}] +Receiving pong 9 - ws0 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 10}] +Receiving pong 9 - ws1 +topic /zeek/event/to_client event name pong args [{'@data-type': 'string', 'data': 'my-message'}, {'@data-type': 'count', 'data': 10}] diff --git a/testing/btest/Baseline/cluster.websocket.two-pipelining/..manager..stderr b/testing/btest/Baseline/cluster.websocket.two-pipelining/..manager..stderr new file mode 100644 index 0000000000..e3f6131b1d --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.two-pipelining/..manager..stderr @@ -0,0 +1,2 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +received termination signal diff --git a/testing/btest/Baseline/cluster.websocket.two-pipelining/..manager.out.sorted b/testing/btest/Baseline/cluster.websocket.two-pipelining/..manager.out.sorted new file mode 100644 index 0000000000..ae8f79c02d --- /dev/null +++ b/testing/btest/Baseline/cluster.websocket.two-pipelining/..manager.out.sorted @@ -0,0 +1,15 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +A Cluster::websocket_client_added, 1, [/zeek/event/to_client] +A Cluster::websocket_client_added, 2, [/zeek/event/to_client] +B got ping: ws1, 0 +B got ping: ws1, 1 +B got ping: ws1, 2 +B got ping: ws1, 3 +B got ping: ws1, 4 +B got ping: ws2, 0 +B got ping: ws2, 1 +B got ping: ws2, 2 +B got ping: ws2, 3 +B got ping: ws2, 4 +C Cluster::websocket_client_lost +C Cluster::websocket_client_lost diff --git a/testing/btest/Baseline/coverage.bare-load-baseline/canonified_loaded_scripts.log b/testing/btest/Baseline/coverage.bare-load-baseline/canonified_loaded_scripts.log index 0afe9db25e..d99e27e5ec 100644 --- a/testing/btest/Baseline/coverage.bare-load-baseline/canonified_loaded_scripts.log +++ b/testing/btest/Baseline/coverage.bare-load-baseline/canonified_loaded_scripts.log @@ -137,6 +137,7 @@ scripts/base/init-frameworks-and-bifs.zeek scripts/base/frameworks/control/__load__.zeek scripts/base/frameworks/control/main.zeek build/scripts/base/bif/cluster.bif.zeek + build/scripts/base/bif/plugins/Zeek_Cluster_WebSocket.events.bif.zeek scripts/base/frameworks/cluster/pools.zeek scripts/base/utils/hash_hrw.zeek scripts/base/frameworks/config/__load__.zeek diff --git a/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log b/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log index 9300733959..4e61a3a752 100644 --- a/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log +++ b/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log @@ -137,6 +137,7 @@ scripts/base/init-frameworks-and-bifs.zeek scripts/base/frameworks/control/__load__.zeek scripts/base/frameworks/control/main.zeek build/scripts/base/bif/cluster.bif.zeek + build/scripts/base/bif/plugins/Zeek_Cluster_WebSocket.events.bif.zeek scripts/base/frameworks/cluster/pools.zeek scripts/base/utils/hash_hrw.zeek scripts/base/frameworks/config/__load__.zeek diff --git a/testing/btest/Baseline/opt.ZAM-bif-tracking/output b/testing/btest/Baseline/opt.ZAM-bif-tracking/output index 7434167583..0731586167 100644 --- a/testing/btest/Baseline/opt.ZAM-bif-tracking/output +++ b/testing/btest/Baseline/opt.ZAM-bif-tracking/output @@ -1,2 +1,2 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. -545 seen BiFs, 0 unseen BiFs (), 0 new BiFs () +546 seen BiFs, 0 unseen BiFs (), 0 new BiFs () diff --git a/testing/btest/Baseline/plugins.hooks/output b/testing/btest/Baseline/plugins.hooks/output index 32330c2da9..9174cce539 100644 --- a/testing/btest/Baseline/plugins.hooks/output +++ b/testing/btest/Baseline/plugins.hooks/output @@ -339,6 +339,7 @@ 0.000000 MetaHookPost LoadFile(0, ./Zeek_BinaryReader.binary.bif.zeek, <...>/Zeek_BinaryReader.binary.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, ./Zeek_BitTorrent.events.bif.zeek, <...>/Zeek_BitTorrent.events.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, ./Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek, <...>/Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek) -> -1 +0.000000 MetaHookPost LoadFile(0, ./Zeek_Cluster_WebSocket.events.bif.zeek, <...>/Zeek_Cluster_WebSocket.events.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, ./Zeek_ConfigReader.config.bif.zeek, <...>/Zeek_ConfigReader.config.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, ./Zeek_ConnSize.events.bif.zeek, <...>/Zeek_ConnSize.events.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, ./Zeek_ConnSize.functions.bif.zeek, <...>/Zeek_ConnSize.functions.bif.zeek) -> -1 @@ -533,6 +534,7 @@ 0.000000 MetaHookPost LoadFile(0, base/init-frameworks-and-bifs.zeek, <...>/init-frameworks-and-bifs.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, base/packet-protocols, <...>/packet-protocols) -> -1 0.000000 MetaHookPost LoadFile(0, base<...>/CPP-load.bif, <...>/CPP-load.bif.zeek) -> -1 +0.000000 MetaHookPost LoadFile(0, base<...>/Zeek_Cluster_WebSocket.events.bif.zeek, <...>/Zeek_Cluster_WebSocket.events.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, base<...>/Zeek_GTPv1.events.bif, <...>/Zeek_GTPv1.events.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, base<...>/Zeek_GTPv1.functions.bif, <...>/Zeek_GTPv1.functions.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, base<...>/Zeek_KRB.types.bif, <...>/Zeek_KRB.types.bif.zeek) -> -1 @@ -647,6 +649,7 @@ 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_BinaryReader.binary.bif.zeek, <...>/Zeek_BinaryReader.binary.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_BitTorrent.events.bif.zeek, <...>/Zeek_BitTorrent.events.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek, <...>/Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek) -> (-1, ) +0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_Cluster_WebSocket.events.bif.zeek, <...>/Zeek_Cluster_WebSocket.events.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_ConfigReader.config.bif.zeek, <...>/Zeek_ConfigReader.config.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_ConnSize.events.bif.zeek, <...>/Zeek_ConnSize.events.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_ConnSize.functions.bif.zeek, <...>/Zeek_ConnSize.functions.bif.zeek) -> (-1, ) @@ -841,6 +844,7 @@ 0.000000 MetaHookPost LoadFileExtended(0, base/init-frameworks-and-bifs.zeek, <...>/init-frameworks-and-bifs.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, base/packet-protocols, <...>/packet-protocols) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, base<...>/CPP-load.bif, <...>/CPP-load.bif.zeek) -> (-1, ) +0.000000 MetaHookPost LoadFileExtended(0, base<...>/Zeek_Cluster_WebSocket.events.bif.zeek, <...>/Zeek_Cluster_WebSocket.events.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, base<...>/Zeek_GTPv1.events.bif, <...>/Zeek_GTPv1.events.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, base<...>/Zeek_GTPv1.functions.bif, <...>/Zeek_GTPv1.functions.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, base<...>/Zeek_KRB.types.bif, <...>/Zeek_KRB.types.bif.zeek) -> (-1, ) @@ -1288,6 +1292,7 @@ 0.000000 MetaHookPre LoadFile(0, ./Zeek_BinaryReader.binary.bif.zeek, <...>/Zeek_BinaryReader.binary.bif.zeek) 0.000000 MetaHookPre LoadFile(0, ./Zeek_BitTorrent.events.bif.zeek, <...>/Zeek_BitTorrent.events.bif.zeek) 0.000000 MetaHookPre LoadFile(0, ./Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek, <...>/Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek) +0.000000 MetaHookPre LoadFile(0, ./Zeek_Cluster_WebSocket.events.bif.zeek, <...>/Zeek_Cluster_WebSocket.events.bif.zeek) 0.000000 MetaHookPre LoadFile(0, ./Zeek_ConfigReader.config.bif.zeek, <...>/Zeek_ConfigReader.config.bif.zeek) 0.000000 MetaHookPre LoadFile(0, ./Zeek_ConnSize.events.bif.zeek, <...>/Zeek_ConnSize.events.bif.zeek) 0.000000 MetaHookPre LoadFile(0, ./Zeek_ConnSize.functions.bif.zeek, <...>/Zeek_ConnSize.functions.bif.zeek) @@ -1482,6 +1487,7 @@ 0.000000 MetaHookPre LoadFile(0, base/init-frameworks-and-bifs.zeek, <...>/init-frameworks-and-bifs.zeek) 0.000000 MetaHookPre LoadFile(0, base/packet-protocols, <...>/packet-protocols) 0.000000 MetaHookPre LoadFile(0, base<...>/CPP-load.bif, <...>/CPP-load.bif.zeek) +0.000000 MetaHookPre LoadFile(0, base<...>/Zeek_Cluster_WebSocket.events.bif.zeek, <...>/Zeek_Cluster_WebSocket.events.bif.zeek) 0.000000 MetaHookPre LoadFile(0, base<...>/Zeek_GTPv1.events.bif, <...>/Zeek_GTPv1.events.bif.zeek) 0.000000 MetaHookPre LoadFile(0, base<...>/Zeek_GTPv1.functions.bif, <...>/Zeek_GTPv1.functions.bif.zeek) 0.000000 MetaHookPre LoadFile(0, base<...>/Zeek_KRB.types.bif, <...>/Zeek_KRB.types.bif.zeek) @@ -1596,6 +1602,7 @@ 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_BinaryReader.binary.bif.zeek, <...>/Zeek_BinaryReader.binary.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_BitTorrent.events.bif.zeek, <...>/Zeek_BitTorrent.events.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek, <...>/Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek) +0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_Cluster_WebSocket.events.bif.zeek, <...>/Zeek_Cluster_WebSocket.events.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_ConfigReader.config.bif.zeek, <...>/Zeek_ConfigReader.config.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_ConnSize.events.bif.zeek, <...>/Zeek_ConnSize.events.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_ConnSize.functions.bif.zeek, <...>/Zeek_ConnSize.functions.bif.zeek) @@ -1790,6 +1797,7 @@ 0.000000 MetaHookPre LoadFileExtended(0, base/init-frameworks-and-bifs.zeek, <...>/init-frameworks-and-bifs.zeek) 0.000000 MetaHookPre LoadFileExtended(0, base/packet-protocols, <...>/packet-protocols) 0.000000 MetaHookPre LoadFileExtended(0, base<...>/CPP-load.bif, <...>/CPP-load.bif.zeek) +0.000000 MetaHookPre LoadFileExtended(0, base<...>/Zeek_Cluster_WebSocket.events.bif.zeek, <...>/Zeek_Cluster_WebSocket.events.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, base<...>/Zeek_GTPv1.events.bif, <...>/Zeek_GTPv1.events.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, base<...>/Zeek_GTPv1.functions.bif, <...>/Zeek_GTPv1.functions.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, base<...>/Zeek_KRB.types.bif, <...>/Zeek_KRB.types.bif.zeek) @@ -2236,6 +2244,7 @@ 0.000000 | HookLoadFile ./Zeek_BinaryReader.binary.bif.zeek <...>/Zeek_BinaryReader.binary.bif.zeek 0.000000 | HookLoadFile ./Zeek_BitTorrent.events.bif.zeek <...>/Zeek_BitTorrent.events.bif.zeek 0.000000 | HookLoadFile ./Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek <...>/Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek +0.000000 | HookLoadFile ./Zeek_Cluster_WebSocket.events.bif.zeek <...>/Zeek_Cluster_WebSocket.events.bif.zeek 0.000000 | HookLoadFile ./Zeek_ConfigReader.config.bif.zeek <...>/Zeek_ConfigReader.config.bif.zeek 0.000000 | HookLoadFile ./Zeek_ConnSize.events.bif.zeek <...>/Zeek_ConnSize.events.bif.zeek 0.000000 | HookLoadFile ./Zeek_ConnSize.functions.bif.zeek <...>/Zeek_ConnSize.functions.bif.zeek @@ -2443,6 +2452,7 @@ 0.000000 | HookLoadFile base/init-frameworks-and-bifs.zeek <...>/init-frameworks-and-bifs.zeek 0.000000 | HookLoadFile base/packet-protocols <...>/packet-protocols 0.000000 | HookLoadFile base<...>/CPP-load.bif <...>/CPP-load.bif.zeek +0.000000 | HookLoadFile base<...>/Zeek_Cluster_WebSocket.events.bif.zeek <...>/Zeek_Cluster_WebSocket.events.bif.zeek 0.000000 | HookLoadFile base<...>/Zeek_GTPv1.events.bif <...>/Zeek_GTPv1.events.bif.zeek 0.000000 | HookLoadFile base<...>/Zeek_GTPv1.functions.bif <...>/Zeek_GTPv1.functions.bif.zeek 0.000000 | HookLoadFile base<...>/Zeek_KRB.types.bif <...>/Zeek_KRB.types.bif.zeek @@ -2544,6 +2554,7 @@ 0.000000 | HookLoadFileExtended ./Zeek_BinaryReader.binary.bif.zeek <...>/Zeek_BinaryReader.binary.bif.zeek 0.000000 | HookLoadFileExtended ./Zeek_BitTorrent.events.bif.zeek <...>/Zeek_BitTorrent.events.bif.zeek 0.000000 | HookLoadFileExtended ./Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek <...>/Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek +0.000000 | HookLoadFileExtended ./Zeek_Cluster_WebSocket.events.bif.zeek <...>/Zeek_Cluster_WebSocket.events.bif.zeek 0.000000 | HookLoadFileExtended ./Zeek_ConfigReader.config.bif.zeek <...>/Zeek_ConfigReader.config.bif.zeek 0.000000 | HookLoadFileExtended ./Zeek_ConnSize.events.bif.zeek <...>/Zeek_ConnSize.events.bif.zeek 0.000000 | HookLoadFileExtended ./Zeek_ConnSize.functions.bif.zeek <...>/Zeek_ConnSize.functions.bif.zeek @@ -2751,6 +2762,7 @@ 0.000000 | HookLoadFileExtended base/init-frameworks-and-bifs.zeek <...>/init-frameworks-and-bifs.zeek 0.000000 | HookLoadFileExtended base/packet-protocols <...>/packet-protocols 0.000000 | HookLoadFileExtended base<...>/CPP-load.bif <...>/CPP-load.bif.zeek +0.000000 | HookLoadFileExtended base<...>/Zeek_Cluster_WebSocket.events.bif.zeek <...>/Zeek_Cluster_WebSocket.events.bif.zeek 0.000000 | HookLoadFileExtended base<...>/Zeek_GTPv1.events.bif <...>/Zeek_GTPv1.events.bif.zeek 0.000000 | HookLoadFileExtended base<...>/Zeek_GTPv1.functions.bif <...>/Zeek_GTPv1.functions.bif.zeek 0.000000 | HookLoadFileExtended base<...>/Zeek_KRB.types.bif <...>/Zeek_KRB.types.bif.zeek diff --git a/testing/btest/cluster/websocket/bad-event-args.zeek b/testing/btest/cluster/websocket/bad-event-args.zeek new file mode 100644 index 0000000000..01aabb7808 --- /dev/null +++ b/testing/btest/cluster/websocket/bad-event-args.zeek @@ -0,0 +1,126 @@ +# @TEST-DOC: A WebSocket client sending invalid data for an event. +# +# @TEST-REQUIRES: have-zeromq +# @TEST-REQUIRES: python3 -c 'import websockets.sync' +# +# @TEST-GROUP: cluster-zeromq +# +# @TEST-PORT: XPUB_PORT +# @TEST-PORT: XSUB_PORT +# @TEST-PORT: LOG_PULL_PORT +# @TEST-PORT: WEBSOCKET_PORT +# +# @TEST-EXEC: cp $FILES/zeromq/cluster-layout-simple.zeek cluster-layout.zeek +# @TEST-EXEC: cp $FILES/zeromq/test-bootstrap.zeek zeromq-test-bootstrap.zeek +# +# @TEST-EXEC: zeek -b --parse-only manager.zeek +# @TEST-EXEC: python3 -m py_compile client.py +# +# @TEST-EXEC: btest-bg-run manager "ZEEKPATH=$ZEEKPATH:.. && CLUSTER_NODE=manager zeek -b ../manager.zeek >out" +# @TEST-EXEC: btest-bg-run client "python3 ../client.py >out" +# +# @TEST-EXEC: btest-bg-wait 30 +# @TEST-EXEC: btest-diff ./manager/out +# @TEST-EXEC: btest-diff ./manager/.stderr +# @TEST-EXEC: btest-diff ./client/out +# @TEST-EXEC: btest-diff ./client/.stderr + +# @TEST-START-FILE manager.zeek +@load ./zeromq-test-bootstrap +redef exit_only_after_terminate = T; + +global ping: event(msg: string, c: count) &is_used; +global pong: event(msg: string, c: count) &is_used; + +event zeek_init() + { + Cluster::subscribe("/zeek/event/my_topic"); + Cluster::listen_websocket([$listen_host="127.0.0.1", $listen_port=to_port(getenv("WEBSOCKET_PORT"))]); + } + +event ping(msg: string, n: count) &is_used + { + print fmt("got ping: %s, %s", msg, n); + local e = Cluster::make_event(pong, msg, n); + Cluster::publish("/zeek/event/my_topic", e); + } + +event Cluster::websocket_client_added(info: Cluster::EndpointInfo, subscriptions: string_vec) + { + print "Cluster::websocket_client_added", subscriptions; + } + +event Cluster::websocket_client_lost(info: Cluster::EndpointInfo) + { + print "Cluster::websocket_client_lost"; + terminate(); + } +# @TEST-END-FILE + + +@TEST-START-FILE client.py +import json, os, time +from websockets.sync.client import connect + +ws_port = os.environ['WEBSOCKET_PORT'].split('/')[0] +ws_url = f'ws://127.0.0.1:{ws_port}/v1/messages/json' +topic = '/zeek/event/my_topic' + +def make_ping(event_args): + return { + "type": "data-message", + "topic": topic, + "@data-type": "vector", + "data": [ + {"@data-type": "count", "data": 1}, # Format + {"@data-type": "count", "data": 1}, # Type + {"@data-type": "vector", "data": [ + { "@data-type": "string", "data": "ping"}, # Event name + { "@data-type": "vector", "data": event_args }, + ], }, + ], + } + +def run(ws_url): + with connect(ws_url) as ws: + print("Connected!") + # Send subscriptions + ws.send(json.dumps([topic])) + ack = json.loads(ws.recv()) + assert "type" in ack + assert ack["type"] == "ack" + assert "endpoint" in ack + assert "version" in ack + + ws.send(json.dumps(make_ping(42))) + err1 = json.loads(ws.recv()) + print("err1", err1) + ws.send(json.dumps(make_ping([{"@data-type": "string", "data": "Hello"}]))) + err2 = json.loads(ws.recv()) + print("err2", err2) + ws.send(json.dumps(make_ping([{"@data-type": "count", "data": 42}, {"@data-type": "string", "data": "Hello"}]))) + err3 = json.loads(ws.recv()) + print("err3", err3) + + # This should be good ping(string, count) + ws.send(json.dumps(make_ping([{"@data-type": "string", "data": "Hello"}, {"@data-type": "count", "data": 42}]))) + pong = json.loads(ws.recv()) + name, args, _ = pong["data"][2]["data"] + print("pong", name, args) + + # This one fails again + ws.send(json.dumps(make_ping([{"@data-type": "money", "data": 0}]))) + err4 = json.loads(ws.recv()) + print("err4", err4) + +def main(): + for _ in range(100): + try: + run(ws_url) + break + except ConnectionRefusedError: + time.sleep(0.1) + +if __name__ == "__main__": + main() +@TEST-END-FILE diff --git a/testing/btest/cluster/websocket/bad-subscriptions.zeek b/testing/btest/cluster/websocket/bad-subscriptions.zeek new file mode 100644 index 0000000000..f6acb33aab --- /dev/null +++ b/testing/btest/cluster/websocket/bad-subscriptions.zeek @@ -0,0 +1,102 @@ +# @TEST-DOC: Clients sends broken subscription arrays +# +# @TEST-REQUIRES: have-zeromq +# @TEST-REQUIRES: python3 -c 'import websockets.sync' +# +# @TEST-GROUP: cluster-zeromq +# +# @TEST-PORT: XPUB_PORT +# @TEST-PORT: XSUB_PORT +# @TEST-PORT: LOG_PULL_PORT +# @TEST-PORT: WEBSOCKET_PORT +# +# @TEST-EXEC: cp $FILES/zeromq/cluster-layout-simple.zeek cluster-layout.zeek +# @TEST-EXEC: cp $FILES/zeromq/test-bootstrap.zeek zeromq-test-bootstrap.zeek +# +# @TEST-EXEC: zeek -b --parse-only manager.zeek +# @TEST-EXEC: python3 -m py_compile client.py +# +# @TEST-EXEC: btest-bg-run manager "ZEEKPATH=$ZEEKPATH:.. && CLUSTER_NODE=manager zeek -b ../manager.zeek >out" +# @TEST-EXEC: btest-bg-run client "python3 ../client.py >out" +# +# @TEST-EXEC: btest-bg-wait 30 +# @TEST-EXEC: btest-diff ./manager/out +# @TEST-EXEC: btest-diff ./manager/.stderr +# @TEST-EXEC: btest-diff ./client/out +# @TEST-EXEC: btest-diff ./client/.stderr + +# @TEST-START-FILE manager.zeek +@load ./zeromq-test-bootstrap +redef exit_only_after_terminate = T; + +global event_count = 0; + +global ping: event(msg: string, c: count) &is_used; +global pong: event(msg: string, c: count) &is_used; + +event zeek_init() + { + Cluster::subscribe("/zeek/event/my_topic"); + Cluster::listen_websocket([$listen_host="127.0.0.1", $listen_port=to_port(getenv("WEBSOCKET_PORT"))]); + } + +global added = 0; +global lost = 0; + +event Cluster::websocket_client_added(info: Cluster::EndpointInfo, subscriptions: string_vec) + { + ++added; + print "Cluster::websocket_client_added", added, subscriptions; + } + +event Cluster::websocket_client_lost(info: Cluster::EndpointInfo) + { + ++lost; + print "Cluster::websocket_client_lost", lost; + if ( lost == 4 ) + terminate(); + } +# @TEST-END-FILE + + +@TEST-START-FILE client.py +import json, os, time +from websockets.sync.client import connect + +ws_port = os.environ['WEBSOCKET_PORT'].split('/')[0] +ws_url = f'ws://127.0.0.1:{ws_port}/v1/messages/json' +topic = '/zeek/event/my_topic' + +def run(ws_url): + with connect(ws_url) as ws: + ws.send('["broken", "brrr') + err = json.loads(ws.recv()) + print("broken array response", err) + + with connect(ws_url) as ws: + ws.send('[1, 2]') + err = json.loads(ws.recv()) + print("non string error", err) + + with connect(ws_url) as ws: + ws.send('[1, "/my_topic"]') + err = json.loads(ws.recv()) + print("mix error", err) + + # This should work - maybe duplicate isn't great, but good for testing. + with connect(ws_url) as ws: + ws.send('["/topic/good", "/duplicate", "/duplicate", "/is/okay"]') + ack = json.loads(ws.recv()) + print("ack", ack["type"] == "ack") + +def main(): + for _ in range(100): + try: + run(ws_url) + break + except ConnectionRefusedError: + time.sleep(0.1) + +if __name__ == "__main__": + main() +@TEST-END-FILE diff --git a/testing/btest/cluster/websocket/bad-url.zeek b/testing/btest/cluster/websocket/bad-url.zeek new file mode 100644 index 0000000000..6bc6f9b185 --- /dev/null +++ b/testing/btest/cluster/websocket/bad-url.zeek @@ -0,0 +1,101 @@ +# @TEST-DOC: Run a single node cluster (manager) with a websocket server and have a single client connect. +# +# @TEST-REQUIRES: have-zeromq +# @TEST-REQUIRES: python3 -c 'import websockets.sync' +# +# @TEST-GROUP: cluster-zeromq +# +# @TEST-PORT: XPUB_PORT +# @TEST-PORT: XSUB_PORT +# @TEST-PORT: LOG_PULL_PORT +# @TEST-PORT: WEBSOCKET_PORT +# +# @TEST-EXEC: cp $FILES/zeromq/cluster-layout-simple.zeek cluster-layout.zeek +# @TEST-EXEC: cp $FILES/zeromq/test-bootstrap.zeek zeromq-test-bootstrap.zeek +# +# @TEST-EXEC: zeek -b --parse-only manager.zeek +# @TEST-EXEC: python3 -m py_compile client.py +# +# @TEST-EXEC: btest-bg-run manager "ZEEKPATH=$ZEEKPATH:.. && CLUSTER_NODE=manager zeek -b ../manager.zeek >out" +# @TEST-EXEC: btest-bg-run client "python3 ../client.py >out" +# +# @TEST-EXEC: btest-bg-wait 5 +# @TEST-EXEC: btest-diff ./manager/out +# @TEST-EXEC: btest-diff ./manager/.stderr +# @TEST-EXEC: btest-diff ./client/out +# @TEST-EXEC: btest-diff ./client/.stderr + +# @TEST-START-FILE manager.zeek +@load ./zeromq-test-bootstrap +redef exit_only_after_terminate = T; + +global ping_count = 0; + +global ping: event(msg: string, c: count) &is_used; +global pong: event(msg: string, c: count) &is_used; + +event zeek_init() + { + Cluster::subscribe("/zeek/event/my_topic"); + Cluster::listen_websocket([$listen_host="127.0.0.1", $listen_port=to_port(getenv("WEBSOCKET_PORT"))]); + } + +event ping(msg: string, n: count) &is_used + { + ++ping_count; + print fmt("got ping: %s, %s", msg, n); + local e = Cluster::make_event(pong, "my-message", ping_count); + Cluster::publish("/zeek/event/my_topic", e); + } + +event Cluster::websocket_client_added(info: Cluster::EndpointInfo, subscriptions: string_vec) + { + print "Cluster::websocket_client_added", subscriptions; + } + +event Cluster::websocket_client_lost(info: Cluster::EndpointInfo) + { + print "Cluster::websocket_client_lost"; + terminate(); + } +# @TEST-END-FILE + + +@TEST-START-FILE client.py +import json, os, time +import websockets.exceptions +from websockets.sync.client import connect + +ws_port = os.environ['WEBSOCKET_PORT'].split('/')[0] +ws_prefix = f'ws://127.0.0.1:{ws_port}' +topic = '/zeek/event/my_topic' + + +def run(ws_prefix): + with connect(ws_prefix + '/v1/messages/json') as ws_good: + print('Connected ws_good!') + with connect(ws_prefix + '/v0/messages/json') as ws_bad: + print('Connected ws_bad!') + try: + err = json.loads(ws_bad.recv()) + except websockets.exceptions.ConnectionClosedError as e: + pass + + print('Error for ws_bad', err) + + ws_good.send(json.dumps(['hello-good'])) + ack = json.loads(ws_good.recv()) + assert 'type' in ack + assert ack['type'] == 'ack' + +def main(): + for _ in range(100): + try: + run(ws_prefix) + break + except ConnectionRefusedError: + time.sleep(0.1) + +if __name__ == '__main__': + main() +@TEST-END-FILE diff --git a/testing/btest/cluster/websocket/cluster-log.zeek b/testing/btest/cluster/websocket/cluster-log.zeek new file mode 100644 index 0000000000..e8e6c6f001 --- /dev/null +++ b/testing/btest/cluster/websocket/cluster-log.zeek @@ -0,0 +1,111 @@ +# @TEST-DOC: Test websocket clients appearing in cluster.log +# +# @TEST-REQUIRES: have-zeromq +# @TEST-REQUIRES: python3 -c 'import websockets.sync' +# +# @TEST-GROUP: cluster-zeromq +# +# @TEST-PORT: XPUB_PORT +# @TEST-PORT: XSUB_PORT +# @TEST-PORT: LOG_PULL_PORT +# @TEST-PORT: WEBSOCKET_PORT +# +# @TEST-EXEC: cp $FILES/zeromq/cluster-layout-simple.zeek cluster-layout.zeek +# @TEST-EXEC: cp $FILES/zeromq/test-bootstrap.zeek zeromq-test-bootstrap.zeek +# +# @TEST-EXEC: zeek -b --parse-only manager.zeek +# @TEST-EXEC: python3 -m py_compile client.py +# +# @TEST-EXEC: btest-bg-run manager "ZEEKPATH=$ZEEKPATH:.. && CLUSTER_NODE=manager zeek -b ../manager.zeek >out" +# @TEST-EXEC: btest-bg-run client "python3 ../client.py >out" +# +# @TEST-EXEC: btest-bg-wait 30 +# @TEST-EXEC: btest-diff ./manager/out +# @TEST-EXEC: btest-diff ./manager/.stderr +# @TEST-EXEC: zeek-cut node message < ./manager/cluster.log | sed -r "s/client '.+' /client /g" | sed -r "s/:[0-9]+/:/g" > ./manager/cluster.log.cannonified +# @TEST-EXEC: btest-diff ./manager/cluster.log.cannonified +# @TEST-EXEC: btest-diff ./client/out +# @TEST-EXEC: btest-diff ./client/.stderr + +# @TEST-START-FILE manager.zeek +@load ./zeromq-test-bootstrap +redef exit_only_after_terminate = T; + +# Have the manager create cluster.log +redef Log::enable_local_logging = T; +redef Log::default_rotation_interval = 0sec; + +global ping_count = 0; + +global ping: event(msg: string, c: count) &is_used; +global pong: event(msg: string, c: count) &is_used; + +event zeek_init() + { + Cluster::subscribe("/test/manager"); + Cluster::listen_websocket([$listen_host="127.0.0.1", $listen_port=to_port(getenv("WEBSOCKET_PORT"))]); + } + +event ping(msg: string, n: count) &is_used + { + ++ping_count; + print fmt("got ping: %s, %s", msg, n); + local e = Cluster::make_event(pong, fmt("orig_msg=%s", msg), ping_count); + Cluster::publish("/test/clients", e); + } + +global added = 0; +global lost = 0; + +event Cluster::websocket_client_added(info: Cluster::EndpointInfo, subscriptions: string_vec) + { + ++added; + print "Cluster::websocket_client_added", added, subscriptions; + } + +event Cluster::websocket_client_lost(info: Cluster::EndpointInfo) + { + ++lost; + print "Cluster::websocket_client_lost", lost; + if ( lost == 3 ) + terminate(); + } +# @TEST-END-FILE + + +@TEST-START-FILE client.py +import json, os, time +from websockets.sync.client import connect + +ws_port = os.environ['WEBSOCKET_PORT'].split('/')[0] +ws_url = f'ws://127.0.0.1:{ws_port}/v1/messages/json' + +def run(ws_url): + with connect(ws_url) as ws1: + with connect(ws_url) as ws2: + with connect(ws_url) as ws3: + clients = [ws1, ws2, ws3] + print("Connected!") + ids = set() + for i, c in enumerate(clients, 1): + c.send(json.dumps([f"/topic/ws/{i}", "/topic/ws/all"])) + ack = json.loads(c.recv()) + assert "type" in ack, repr(ack) + assert ack["type"] == "ack" + assert "endpoint" in ack, repr(ack) + assert "version" in ack + ids.add(ack["endpoint"]) + + print("unique ids", len(ids)) + +def main(): + for _ in range(100): + try: + run(ws_url) + break + except ConnectionRefusedError: + time.sleep(0.1) + +if __name__ == "__main__": + main() +@TEST-END-FILE diff --git a/testing/btest/cluster/websocket/listen-idempotent.zeek b/testing/btest/cluster/websocket/listen-idempotent.zeek new file mode 100644 index 0000000000..448ab5e1ac --- /dev/null +++ b/testing/btest/cluster/websocket/listen-idempotent.zeek @@ -0,0 +1,50 @@ +# @TEST-DOC: Allow listening with the same tls_options on the same port, but fail for disagreeing tls_options. +# +# @TEST-EXEC: zeek -b %INPUT +# @TEST-EXEC: TEST_DIFF_CANONIFIER='sed -E "s/127.0.0.1:[0-9]+/127.0.0.1:/g" | $SCRIPTS/diff-remove-abspath' btest-diff .stderr +# +# @TEST-PORT: WEBSOCKET_PORT +# @TEST-PORT: WEBSOCKET_SECURE_PORT + +event zeek_init() + { + local tls_options = Cluster::WebSocketTLSOptions( + $cert_file="../localhost.crt", + $key_file="../localhost.key", + ); + + local tls_options_2 = Cluster::WebSocketTLSOptions( + $cert_file="../localhost.crt", + $key_file="../localhost.key", + ); + local ws_port = to_port(getenv("WEBSOCKET_PORT")); + local wss_port = to_port(getenv("WEBSOCKET_SECURE_PORT")); + + local ws_opts = Cluster::WebSocketServerOptions($listen_host="127.0.0.1", $listen_port=ws_port); + local ws_opts_x = copy(ws_opts); + ws_opts_x$tls_options = tls_options; + + local ws_opts_wss_port = Cluster::WebSocketServerOptions($listen_host="127.0.0.1", $listen_port=wss_port); + + local ws_tls_opts = Cluster::WebSocketServerOptions( + $listen_host="127.0.0.1", + $listen_port=wss_port, + $tls_options=tls_options, + ); + # Same as ws_tls_opts + local ws_tls_opts_copy = Cluster::WebSocketServerOptions( + $listen_host="127.0.0.1", + $listen_port=wss_port, + $tls_options=tls_options_2, + ); + + assert Cluster::listen_websocket(ws_opts); + assert Cluster::listen_websocket(ws_opts); + assert ! Cluster::listen_websocket(ws_opts_x); + assert Cluster::listen_websocket(ws_tls_opts); + assert Cluster::listen_websocket(ws_tls_opts); + assert Cluster::listen_websocket(ws_tls_opts_copy); + assert ! Cluster::listen_websocket(ws_opts_wss_port); + + terminate(); + } diff --git a/testing/btest/cluster/websocket/one-pipelining.zeek b/testing/btest/cluster/websocket/one-pipelining.zeek new file mode 100644 index 0000000000..3bf6096ee5 --- /dev/null +++ b/testing/btest/cluster/websocket/one-pipelining.zeek @@ -0,0 +1,123 @@ +# @TEST-DOC: Send subscriptions and events without waiting for pong, should be okay, the websocket server will queue this a bit. +# +# @TEST-REQUIRES: have-zeromq +# @TEST-REQUIRES: python3 -c 'import websockets.sync' +# +# @TEST-GROUP: cluster-zeromq +# +# @TEST-PORT: XPUB_PORT +# @TEST-PORT: XSUB_PORT +# @TEST-PORT: LOG_PULL_PORT +# @TEST-PORT: WEBSOCKET_PORT +# +# @TEST-EXEC: cp $FILES/zeromq/cluster-layout-simple.zeek cluster-layout.zeek +# @TEST-EXEC: cp $FILES/zeromq/test-bootstrap.zeek zeromq-test-bootstrap.zeek +# +# @TEST-EXEC: zeek -b --parse-only manager.zeek +# @TEST-EXEC: python3 -m py_compile client.py +# +# @TEST-EXEC: btest-bg-run manager "ZEEKPATH=$ZEEKPATH:.. && CLUSTER_NODE=manager zeek -b ../manager.zeek >out" +# @TEST-EXEC: btest-bg-run client "python3 ../client.py >out" +# +# @TEST-EXEC: btest-bg-wait 30 +# @TEST-EXEC: btest-diff ./manager/out +# @TEST-EXEC: btest-diff ./manager/.stderr +# @TEST-EXEC: btest-diff ./client/out +# @TEST-EXEC: btest-diff ./client/.stderr + +# @TEST-START-FILE manager.zeek +@load ./zeromq-test-bootstrap +redef exit_only_after_terminate = T; + +global ping_count = 0; + +global ping: event(msg: string, c: count) &is_used; +global pong: event(msg: string, c: count) &is_used; + +event zeek_init() + { + Cluster::subscribe("/zeek/event/my_topic"); + Cluster::listen_websocket([$listen_host="127.0.0.1", $listen_port=to_port(getenv("WEBSOCKET_PORT"))]); + } + +event ping(msg: string, n: count) &is_used + { + ++ping_count; + print fmt("got ping: %s, %s", msg, n); + local e = Cluster::make_event(pong, "my-message", ping_count); + Cluster::publish("/zeek/event/my_topic", e); + } + +event Cluster::websocket_client_added(info: Cluster::EndpointInfo, subscriptions: string_vec) + { + print "Cluster::websocket_client_added", subscriptions; + } + +event Cluster::websocket_client_lost(info: Cluster::EndpointInfo) + { + print "Cluster::websocket_client_lost"; + terminate(); + } +# @TEST-END-FILE + + +@TEST-START-FILE client.py +import json, os, time +from websockets.sync.client import connect + +ws_port = os.environ['WEBSOCKET_PORT'].split('/')[0] +ws_url = f'ws://127.0.0.1:{ws_port}/v1/messages/json' +topic = '/zeek/event/my_topic' + +def make_ping(c): + return { + "type": "data-message", + "topic": topic, + "@data-type": "vector", + "data": [ + {"@data-type": "count", "data": 1}, # Format + {"@data-type": "count", "data": 1}, # Type + {"@data-type": "vector", "data": [ + { "@data-type": "string", "data": "ping"}, # Event name + { "@data-type": "vector", "data": [ # event args + {"@data-type": "string", "data": f"python-websocket-client"}, + {"@data-type": "count", "data": c}, + ], }, + ], }, + ], + } + +def run(ws_url): + with connect(ws_url) as ws: + print("Connected!") + # Send subscriptions + ws.send(json.dumps([topic])) + + for i in range(5): + print("Sending ping", i) + ws.send(json.dumps(make_ping(i))) + + ack = json.loads(ws.recv()) + assert "type" in ack + assert ack["type"] == "ack" + assert "endpoint" in ack + assert "version" in ack + + for i in range(5): + print("Receiving pong", i) + pong = json.loads(ws.recv()) + assert pong["@data-type"] == "vector" + ev = pong["data"][2]["data"] + print("topic", pong["topic"], "event name", ev[0]["data"], "args", ev[1]["data"]) + +def main(): + for _ in range(100): + try: + run(ws_url) + break + except ConnectionRefusedError: + time.sleep(0.1) + +if __name__ == "__main__": + main() +@TEST-END-FILE diff --git a/testing/btest/cluster/websocket/one.zeek b/testing/btest/cluster/websocket/one.zeek new file mode 100644 index 0000000000..e3ecf77d4f --- /dev/null +++ b/testing/btest/cluster/websocket/one.zeek @@ -0,0 +1,120 @@ +# @TEST-DOC: Run a single node cluster (manager) with a websocket server and have a single client connect. +# +# @TEST-REQUIRES: have-zeromq +# @TEST-REQUIRES: python3 -c 'import websockets.sync' +# +# @TEST-GROUP: cluster-zeromq +# +# @TEST-PORT: XPUB_PORT +# @TEST-PORT: XSUB_PORT +# @TEST-PORT: LOG_PULL_PORT +# @TEST-PORT: WEBSOCKET_PORT +# +# @TEST-EXEC: cp $FILES/zeromq/cluster-layout-simple.zeek cluster-layout.zeek +# @TEST-EXEC: cp $FILES/zeromq/test-bootstrap.zeek zeromq-test-bootstrap.zeek +# +# @TEST-EXEC: zeek -b --parse-only manager.zeek +# @TEST-EXEC: python3 -m py_compile client.py +# +# @TEST-EXEC: btest-bg-run manager "ZEEKPATH=$ZEEKPATH:.. && CLUSTER_NODE=manager zeek -b ../manager.zeek >out" +# @TEST-EXEC: btest-bg-run client "python3 ../client.py >out" +# +# @TEST-EXEC: btest-bg-wait 30 +# @TEST-EXEC: btest-diff ./manager/out +# @TEST-EXEC: btest-diff ./manager/.stderr +# @TEST-EXEC: btest-diff ./client/out +# @TEST-EXEC: btest-diff ./client/.stderr + +# @TEST-START-FILE manager.zeek +@load ./zeromq-test-bootstrap +redef exit_only_after_terminate = T; + +global ping_count = 0; + +global ping: event(msg: string, c: count) &is_used; +global pong: event(msg: string, c: count) &is_used; + +event zeek_init() + { + Cluster::subscribe("/zeek/event/my_topic"); + Cluster::listen_websocket([$listen_host="127.0.0.1", $listen_port=to_port(getenv("WEBSOCKET_PORT"))]); + } + +event ping(msg: string, n: count) &is_used + { + ++ping_count; + print fmt("got ping: %s, %s", msg, n); + local e = Cluster::make_event(pong, "my-message", ping_count); + Cluster::publish("/zeek/event/my_topic", e); + } + +event Cluster::websocket_client_added(info: Cluster::EndpointInfo, subscriptions: string_vec) + { + print "Cluster::websocket_client_added", subscriptions; + } + +event Cluster::websocket_client_lost(info: Cluster::EndpointInfo) + { + print "Cluster::websocket_client_lost"; + terminate(); + } +# @TEST-END-FILE + + +@TEST-START-FILE client.py +import json, os, time +from websockets.sync.client import connect + +ws_port = os.environ['WEBSOCKET_PORT'].split('/')[0] +ws_url = f'ws://127.0.0.1:{ws_port}/v1/messages/json' +topic = '/zeek/event/my_topic' + +def make_ping(c): + return { + "type": "data-message", + "topic": topic, + "@data-type": "vector", + "data": [ + {"@data-type": "count", "data": 1}, # Format + {"@data-type": "count", "data": 1}, # Type + {"@data-type": "vector", "data": [ + { "@data-type": "string", "data": "ping"}, # Event name + { "@data-type": "vector", "data": [ # event args + {"@data-type": "string", "data": f"python-websocket-client"}, + {"@data-type": "count", "data": c}, + ], }, + ], }, + ], + } + +def run(ws_url): + with connect(ws_url) as ws: + print("Connected!") + # Send subscriptions + ws.send(json.dumps([topic])) + ack = json.loads(ws.recv()) + assert "type" in ack + assert ack["type"] == "ack" + assert "endpoint" in ack + assert "version" in ack + + for i in range(5): + print("Sending ping", i) + ws.send(json.dumps(make_ping(i))) + print("Receiving pong", i) + pong = json.loads(ws.recv()) + assert pong["@data-type"] == "vector" + ev = pong["data"][2]["data"] + print("topic", pong["topic"], "event name", ev[0]["data"], "args", ev[1]["data"]) + +def main(): + for _ in range(100): + try: + run(ws_url) + break + except ConnectionRefusedError: + time.sleep(0.1) + +if __name__ == "__main__": + main() +@TEST-END-FILE diff --git a/testing/btest/cluster/websocket/three.zeek b/testing/btest/cluster/websocket/three.zeek new file mode 100644 index 0000000000..3810c04e3c --- /dev/null +++ b/testing/btest/cluster/websocket/three.zeek @@ -0,0 +1,137 @@ +# @TEST-DOC: Run a single node cluster (manager) with a websocket server, have three clients connect. +# +# @TEST-REQUIRES: have-zeromq +# @TEST-REQUIRES: python3 -c 'import websockets.sync' +# +# @TEST-GROUP: cluster-zeromq +# +# @TEST-PORT: XPUB_PORT +# @TEST-PORT: XSUB_PORT +# @TEST-PORT: LOG_PULL_PORT +# @TEST-PORT: WEBSOCKET_PORT +# +# @TEST-EXEC: cp $FILES/zeromq/cluster-layout-simple.zeek cluster-layout.zeek +# @TEST-EXEC: cp $FILES/zeromq/test-bootstrap.zeek zeromq-test-bootstrap.zeek +# +# @TEST-EXEC: zeek -b --parse-only manager.zeek +# @TEST-EXEC: python3 -m py_compile client.py +# +# @TEST-EXEC: btest-bg-run manager "ZEEKPATH=$ZEEKPATH:.. && CLUSTER_NODE=manager zeek -b ../manager.zeek >out" +# @TEST-EXEC: btest-bg-run client "python3 ../client.py >out" +# +# @TEST-EXEC: btest-bg-wait 30 +# @TEST-EXEC: btest-diff ./manager/out +# @TEST-EXEC: btest-diff ./manager/.stderr +# @TEST-EXEC: btest-diff ./client/out +# @TEST-EXEC: btest-diff ./client/.stderr + +# @TEST-START-FILE manager.zeek +@load ./zeromq-test-bootstrap +redef exit_only_after_terminate = T; + +global ping_count = 0; + +global ping: event(msg: string, c: count) &is_used; +global pong: event(msg: string, c: count) &is_used; + +event zeek_init() + { + Cluster::subscribe("/test/manager"); + Cluster::listen_websocket([$listen_host="127.0.0.1", $listen_port=to_port(getenv("WEBSOCKET_PORT"))]); + } + +event ping(msg: string, n: count) &is_used + { + ++ping_count; + print fmt("got ping: %s, %s", msg, n); + local e = Cluster::make_event(pong, fmt("orig_msg=%s", msg), ping_count); + Cluster::publish("/test/clients", e); + } + +global added = 0; +global lost = 0; + +event Cluster::websocket_client_added(info: Cluster::EndpointInfo, subscriptions: string_vec) + { + ++added; + print "Cluster::websocket_client_added", added, subscriptions; + } + +event Cluster::websocket_client_lost(info: Cluster::EndpointInfo) + { + ++lost; + print "Cluster::websocket_client_lost", lost; + if ( lost == 3 ) + terminate(); + } +# @TEST-END-FILE + + +@TEST-START-FILE client.py +import json, os, time +from websockets.sync.client import connect + +ws_port = os.environ['WEBSOCKET_PORT'].split('/')[0] +ws_url = f'ws://127.0.0.1:{ws_port}/v1/messages/json' +topic = '/test/clients' + +def make_ping(c, who): + return { + "type": "data-message", + "topic": "/test/manager", + "@data-type": "vector", + "data": [ + {"@data-type": "count", "data": 1}, # Format + {"@data-type": "count", "data": 1}, # Type + {"@data-type": "vector", "data": [ + { "@data-type": "string", "data": "ping"}, # Event name + { "@data-type": "vector", "data": [ # event args + {"@data-type": "string", "data": who}, + {"@data-type": "count", "data": c}, + ], }, + ], }, + ], + } + +def run(ws_url): + with connect(ws_url) as ws1: + with connect(ws_url) as ws2: + with connect(ws_url) as ws3: + clients = [ws1, ws2, ws3] + print("Connected!") + ids = set() + for c in clients: + c.send(json.dumps([topic])) + for c in clients: + ack = json.loads(c.recv()) + assert "type" in ack, repr(ack) + assert ack["type"] == "ack" + assert "endpoint" in ack, repr(ack) + assert "version" in ack + ids.add(ack["endpoint"]) + + print("unique ids", len(ids)) + + for i in range(16): + c = clients[i % len(clients)] + name = f"ws{(i % len(clients)) + 1}" + print(name, "sending ping", i) + c.send(json.dumps(make_ping(i, name))) + + print("receiving pong", i) + for c in clients: + pong = json.loads(c.recv()) + ev = pong["data"][2]["data"] + print("ev: topic", pong["topic"], "event name", ev[0]["data"], "args", ev[1]["data"]) + +def main(): + for _ in range(100): + try: + run(ws_url) + break + except ConnectionRefusedError: + time.sleep(0.1) + +if __name__ == "__main__": + main() +@TEST-END-FILE diff --git a/testing/btest/cluster/websocket/tls-usage-error.zeek b/testing/btest/cluster/websocket/tls-usage-error.zeek new file mode 100644 index 0000000000..d6357324dd --- /dev/null +++ b/testing/btest/cluster/websocket/tls-usage-error.zeek @@ -0,0 +1,19 @@ +# @TEST-DOC: Calling listen_websocket() with badly configured WebSocketTLSOptions. +# +# @TEST-EXEC: zeek -b %INPUT +# @TEST-EXEC: TEST_DIFF_CANONIFIER=$SCRIPTS/diff-remove-abspath btest-diff .stderr + + +event zeek_init() + { + local tls_options_no_key = Cluster::WebSocketTLSOptions( + $cert_file="../localhost.crt", + ); + + local tls_options_no_cert = Cluster::WebSocketTLSOptions( + $key_file="../localhost.key", + ); + + assert ! Cluster::listen_websocket([$listen_host="127.0.0.1", $listen_port=1234/tcp, $tls_options=tls_options_no_key]); + assert ! Cluster::listen_websocket([$listen_host="127.0.0.1", $listen_port=1234/tcp, $tls_options=tls_options_no_cert]); + } diff --git a/testing/btest/cluster/websocket/tls.zeek b/testing/btest/cluster/websocket/tls.zeek new file mode 100644 index 0000000000..171dc6f5c2 --- /dev/null +++ b/testing/btest/cluster/websocket/tls.zeek @@ -0,0 +1,151 @@ +# @TEST-DOC: Run a single node cluster (manager) with a websocket server that has TLS enabled. +# +# @TEST-REQUIRES: have-zeromq +# @TEST-REQUIRES: python3 -c 'import websockets.asyncio' +# +# @TEST-GROUP: cluster-zeromq +# +# @TEST-PORT: XPUB_PORT +# @TEST-PORT: XSUB_PORT +# @TEST-PORT: LOG_PULL_PORT +# @TEST-PORT: WEBSOCKET_PORT +# +# @TEST-EXEC: cp $FILES/zeromq/cluster-layout-simple.zeek cluster-layout.zeek +# @TEST-EXEC: cp $FILES/zeromq/test-bootstrap.zeek zeromq-test-bootstrap.zeek +# +# @TEST-EXEC: zeek -b --parse-only manager.zeek +# @TEST-EXEC: python3 -m py_compile client.py +# +# @TEST-EXEC: chmod +x gen-localhost-certs.sh +# @TEST-EXEC: ./gen-localhost-certs.sh +# @TEST-EXEC: btest-bg-run manager "ZEEKPATH=$ZEEKPATH:.. && CLUSTER_NODE=manager zeek -b ../manager.zeek >out" +# @TEST-EXEC: btest-bg-run client "python3 ../client.py >out" +# +# @TEST-EXEC: btest-bg-wait 30 +# @TEST-EXEC: btest-diff ./manager/out +# @TEST-EXEC: btest-diff ./manager/.stderr +# @TEST-EXEC: btest-diff ./client/out +# @TEST-EXEC: btest-diff ./client/.stderr + +# @TEST-START-FILE manager.zeek +@load ./zeromq-test-bootstrap +redef exit_only_after_terminate = T; + +global ping_count = 0; + +global ping: event(msg: string, c: count) &is_used; +global pong: event(msg: string, c: count) &is_used; + +event zeek_init() + { + Cluster::subscribe("/zeek/event/my_topic"); + + local tls_options = Cluster::WebSocketTLSOptions( + $cert_file="../localhost.crt", + $key_file="../localhost.key", + ); + + local ws_server_options = Cluster::WebSocketServerOptions( + $listen_host="127.0.0.1", + $listen_port=to_port(getenv("WEBSOCKET_PORT")), + $tls_options=tls_options, + ); + + Cluster::listen_websocket(ws_server_options); + } + +event ping(msg: string, n: count) &is_used + { + ++ping_count; + print fmt("got ping: %s, %s", msg, n); + local e = Cluster::make_event(pong, "my-message", ping_count); + Cluster::publish("/zeek/event/my_topic", e); + } + +event Cluster::websocket_client_added(info: Cluster::EndpointInfo, subscriptions: string_vec) + { + print "Cluster::websocket_client_added", subscriptions; + } + +event Cluster::websocket_client_lost(info: Cluster::EndpointInfo) + { + print "Cluster::websocket_client_lost"; + terminate(); + } +# @TEST-END-FILE + + +@TEST-START-FILE client.py +import asyncio, json, os, socket, time +from websockets.asyncio.client import connect + +ws_port = os.environ['WEBSOCKET_PORT'].split('/')[0] +ws_url = f'wss://localhost:{ws_port}/v1/messages/json' +topic = '/zeek/event/my_topic' + +# Make the websockets library use the custom server cert. +# https://stackoverflow.com/a/55856969 +os.environ["SSL_CERT_FILE"] = "../localhost.crt" + +def make_ping(c): + return { + "type": "data-message", + "topic": topic, + "@data-type": "vector", + "data": [ + {"@data-type": "count", "data": 1}, # Format + {"@data-type": "count", "data": 1}, # Type + {"@data-type": "vector", "data": [ + { "@data-type": "string", "data": "ping"}, # Event name + { "@data-type": "vector", "data": [ # event args + {"@data-type": "string", "data": f"python-websocket-client"}, + {"@data-type": "count", "data": c}, + ], }, + ], }, + ], + } + +async def run(): + async with connect(ws_url, family=socket.AF_INET) as ws: + print("Connected!") + # Send subscriptions + await ws.send(json.dumps([topic])) + ack = json.loads(await ws.recv()) + assert "type" in ack + assert ack["type"] == "ack" + assert "endpoint" in ack + assert "version" in ack + + for i in range(5): + print("Sending ping", i) + await ws.send(json.dumps(make_ping(i))) + print("Receiving pong", i) + pong = json.loads(await ws.recv()) + assert pong["@data-type"] == "vector" + ev = pong["data"][2]["data"] + print("topic", pong["topic"], "event name", ev[0]["data"], "args", ev[1]["data"]) + +def main(): + for _ in range(100): + try: + asyncio.run(run()) + break + except ConnectionRefusedError: + time.sleep(0.1) + +if __name__ == "__main__": + main() +@TEST-END-FILE + +# The cert and key were generated with OpenSSL using the following command, +# taken from https://letsencrypt.org/docs/certificates-for-localhost/ +# +# The test will generate the script, but the certificate is valid +# for 10 years. +@TEST-START-FILE gen-localhost-certs.sh +#!/usr/bin/env bash +openssl req -x509 -out localhost.crt -keyout localhost.key \ + -newkey rsa:2048 -nodes -sha256 -days 3650 \ + -subj '/CN=localhost' -extensions EXT -config <( \ + printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nbasicConstraints=CA:TRUE") +@TEST-END-FILE diff --git a/testing/btest/cluster/websocket/two-pipelining.zeek b/testing/btest/cluster/websocket/two-pipelining.zeek new file mode 100644 index 0000000000..cfef86522f --- /dev/null +++ b/testing/btest/cluster/websocket/two-pipelining.zeek @@ -0,0 +1,158 @@ +# @TEST-DOC: Send subscriptions and events without waiting for pong, should be okay, the websocket server will queue this a bit. +# +# @TEST-REQUIRES: have-zeromq +# @TEST-REQUIRES: python3 -c 'import websockets.sync' +# +# @TEST-GROUP: cluster-zeromq +# +# @TEST-PORT: XPUB_PORT +# @TEST-PORT: XSUB_PORT +# @TEST-PORT: LOG_PULL_PORT +# @TEST-PORT: WEBSOCKET_PORT +# +# @TEST-EXEC: cp $FILES/zeromq/cluster-layout-simple.zeek cluster-layout.zeek +# @TEST-EXEC: cp $FILES/zeromq/test-bootstrap.zeek zeromq-test-bootstrap.zeek +# +# @TEST-EXEC: zeek -b --parse-only manager.zeek +# @TEST-EXEC: python3 -m py_compile client.py +# +# @TEST-EXEC: btest-bg-run manager "ZEEKPATH=$ZEEKPATH:.. && CLUSTER_NODE=manager zeek -b ../manager.zeek >out" +# @TEST-EXEC: btest-bg-run client "python3 ../client.py >out" +# +# @TEST-EXEC: btest-bg-wait 30 +# @TEST-EXEC: sort ./manager/out > ./manager/out.sorted +# @TEST-EXEC: btest-diff ./manager/out.sorted +# @TEST-EXEC: btest-diff ./manager/.stderr +# @TEST-EXEC: btest-diff ./client/out +# @TEST-EXEC: btest-diff ./client/.stderr + +# @TEST-START-FILE manager.zeek +@load ./zeromq-test-bootstrap +redef exit_only_after_terminate = T; + +global ping_count = 0; + +global ping: event(msg: string, c: count) &is_used; +global pong: event(msg: string, c: count) &is_used; + +event zeek_init() + { + Cluster::subscribe("/zeek/event/to_manager"); + Cluster::listen_websocket([$listen_host="127.0.0.1", $listen_port=to_port(getenv("WEBSOCKET_PORT"))]); + } + +global added = 0; +global lost = 0; + +type Item: record { + msg: string; + n: count; +}; + +global queue: vector of Item; + +event ping(msg: string, n: count) &is_used + { + # Queue the pings if we haven't seen both clients yet. + if ( added < 2 ) + { + queue += Item($msg=msg, $n=n); + return; + } + + ++ping_count; + print fmt("B got ping: %s, %s", msg, n); + local e = Cluster::make_event(pong, "my-message", ping_count); + Cluster::publish("/zeek/event/to_client", e); + } + +event Cluster::websocket_client_added(info: Cluster::EndpointInfo, subscriptions: string_vec) + { + ++added; + print "A Cluster::websocket_client_added", added, subscriptions; + + if ( added == 2 ) + { + # Anything in the queue? + for ( _, item in queue ) + event ping(item$msg, item$n); + } + } + +event Cluster::websocket_client_lost(info: Cluster::EndpointInfo) + { + ++lost; + print "C Cluster::websocket_client_lost"; + if ( lost == 2 ) + terminate(); + } +# @TEST-END-FILE + + +@TEST-START-FILE client.py +import json, os, time +from websockets.sync.client import connect + +ws_port = os.environ['WEBSOCKET_PORT'].split('/')[0] +ws_url = f'ws://127.0.0.1:{ws_port}/v1/messages/json' +topic = '/zeek/event/to_client' + +def make_ping(c, who): + return { + "type": "data-message", + "topic": "/zeek/event/to_manager", + "@data-type": "vector", + "data": [ + {"@data-type": "count", "data": 1}, # Format + {"@data-type": "count", "data": 1}, # Type + {"@data-type": "vector", "data": [ + { "@data-type": "string", "data": "ping"}, # Event name + { "@data-type": "vector", "data": [ # event args + {"@data-type": "string", "data": who}, + {"@data-type": "count", "data": c}, + ], }, + ], }, + ], + } + +def run(ws_url): + with connect(ws_url) as ws1: + with connect(ws_url) as ws2: + clients = [ws1, ws2] + print("Connected!") + # Send subscriptions + for ws in clients: + ws.send(json.dumps([topic])) + + for i in range(5): + for c, ws in enumerate(clients, 1): + print(f"Sending ping {i} - ws{c}") + ws.send(json.dumps(make_ping(i, f"ws{c}"))) + + for c, ws in enumerate(clients, 1): + print(f"Receiving ack - ws{c}") + ack = json.loads(ws.recv()) + assert "type" in ack + assert ack["type"] == "ack" + assert "endpoint" in ack + assert "version" in ack + + for i in range(10): + for c, ws in enumerate(clients): + print(f"Receiving pong {i} - ws{c}") + pong = json.loads(ws.recv()) + assert pong["@data-type"] == "vector" + ev = pong["data"][2]["data"] + print("topic", pong["topic"], "event name", ev[0]["data"], "args", ev[1]["data"]) + +def main(): + for _ in range(100): + try: + run(ws_url) + break + except ConnectionRefusedError: + time.sleep(0.1) + +if __name__ == "__main__": + main() +@TEST-END-FILE diff --git a/testing/btest/opt/ZAM-bif-tracking.zeek b/testing/btest/opt/ZAM-bif-tracking.zeek index 33d387f408..1668c4af35 100644 --- a/testing/btest/opt/ZAM-bif-tracking.zeek +++ b/testing/btest/opt/ZAM-bif-tracking.zeek @@ -98,6 +98,7 @@ global known_BiFs = set( "Broker::make_event", "Broker::publish", "Cluster::Backend::__init", + "Cluster::__listen_websocket", "Cluster::__subscribe", "Cluster::__unsubscribe", "Cluster::make_event", From 70f5430e7c0f0c6def4667a938cbbbb215469c69 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Mon, 10 Mar 2025 18:24:32 +0100 Subject: [PATCH 19/20] ci/macos/prepare: Install python@3 explicitly It seems Homebrew's Python 3.12 doesn't install default symlinks or python3 symlinks[1]. I believe this results in prepare.sh using the system's Python rather than Homebrew's. Install python@3 explicitly to put the symlinks in place. [1] https://stackoverflow.com/a/77655631 --- ci/macos/prepare.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ci/macos/prepare.sh b/ci/macos/prepare.sh index 25ad859b33..4494fdbc9a 100755 --- a/ci/macos/prepare.sh +++ b/ci/macos/prepare.sh @@ -7,8 +7,9 @@ set -x brew update brew upgrade cmake -brew install cppzmq openssl@3 swig bison flex ccache libmaxminddb dnsmasq +brew install cppzmq openssl@3 python@3 swig bison flex ccache libmaxminddb dnsmasq + +which python3 +python3 --version -# Upgrade pip so we have the --break-system-packages option. -python3 -m pip install --upgrade pip python3 -m pip install --user --break-system-packages websockets From 81acc4509dd05ed39293d02580d5bacd9ff0e048 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Tue, 11 Mar 2025 10:12:55 +0100 Subject: [PATCH 20/20] ci/opensuse-tumpleweed: Bust cache Got a build failure because the old container images had python3-devel for Python3.11 or something older, but then prepare.sh would install Python 3.13 and Zeek's configure failing due to trying to find the devel headers from python313-devel which wasn't installed by prepare.sh --- ci/opensuse-tumbleweed/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/opensuse-tumbleweed/Dockerfile b/ci/opensuse-tumbleweed/Dockerfile index bbc33a47a1..49056ae3e4 100644 --- a/ci/opensuse-tumbleweed/Dockerfile +++ b/ci/opensuse-tumbleweed/Dockerfile @@ -2,7 +2,7 @@ FROM opensuse/tumbleweed # A version field to invalidate Cirrus's build cache when needed, as suggested in # https://github.com/cirruslabs/cirrus-ci-docs/issues/544#issuecomment-566066822 -ENV DOCKERFILE_VERSION 20241024 +ENV DOCKERFILE_VERSION 20250311 # Remove the repo-openh264 repository, it caused intermittent issues # and we should not be needing any packages from it.