Merge remote-tracking branch 'origin/topic/awelzel/4136-cluster-websocket-support'

* origin/topic/awelzel/4136-cluster-websocket-support:
  ci/opensuse-tumpleweed: Bust cache
  ci/macos/prepare: Install python@3 explicitly
  cluster/websocket: Implement WebSocket server
  cluster/websocket: Add IXWebsocket submodule
  ci/alpine: Install openssl package for testing
  ci: Install websockets from pip for all distros
  auxil/libunistd: Bump for ssize_t typedef
  auxil/broker: Bump to latest master version
  cluster/zeromq: Catch log_push.send() exception
  cluster/zeromq: Catch exceptions as const zmq::error_t&
  cluster/zeromq: No assert on inproc handling
  cluster/zeromq: Support configuring IO threads for proxy thread
  cluster/zeromq: Move variable lookups from DoInit() to DoInitPostScript()
  cluster/zeromq: Handle EINTR at shutdown
  cluster/zeromq: Queue one message at a time
  cluster/Backend: Queue a single message only
  cluster/zeromq: Adapt for OnLoopProcess changes
  cluster/ThreadedBackend: Switch to OnLoopProcess
  cluster/OnLoop: Introduce helper template class
  serializer/broker: Expose to_broker_event() and to_zeek_event()
This commit is contained in:
Arne Welzel 2025-03-11 10:50:49 +01:00
commit bb58148c64
97 changed files with 3300 additions and 170 deletions

3
.gitmodules vendored
View file

@ -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

78
CHANGES
View file

@ -1,3 +1,81 @@
7.2.0-dev.310 | 2025-03-11 10:50:49 +0100
* ci/opensuse-tumpleweed: Bust cache (Arne Welzel, Corelight)
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/macos/prepare: Install python@3 explicitly (Arne Welzel, Corelight)
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
* cluster/websocket: Implement WebSocket server (Arne Welzel, Corelight)
* cluster/websocket: Add IXWebsocket submodule (Arne Welzel, Corelight)
* ci/alpine: Install openssl package for testing (Arne Welzel, Corelight)
* ci: Install websockets from pip for all distros (Arne Welzel, Corelight)
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.
* auxil/libunistd: Bump for ssize_t typedef (Arne Welzel, Corelight)
* auxil/broker: Bump to latest master version (Arne Welzel, Corelight)
* cluster/zeromq: Catch log_push.send() exception (Arne Welzel, Corelight)
* cluster/zeromq: Catch exceptions as const zmq::error_t& (Arne Welzel, Corelight)
* cluster/zeromq: No assert on inproc handling (Arne Welzel, Corelight)
This might happen if we didn't succeed in completely sending a multipart
message and stop early.
* cluster/zeromq: Support configuring IO threads for proxy thread (Arne Welzel, Corelight)
* cluster/zeromq: Move variable lookups from DoInit() to DoInitPostScript() (Arne Welzel, Corelight)
* cluster/zeromq: Handle EINTR at shutdown (Arne Welzel, Corelight)
Read ::signal_val and early exit a DoPublish() in case termination
happened while blocked in inproc.send()
* cluster/zeromq: Queue one message at a time (Arne Welzel, Corelight)
Queueing multiple messages can easily overload the IO loop without
creating any backpressure.
* cluster/Backend: Queue a single message only (Arne Welzel, Corelight)
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.
* cluster/zeromq: Adapt for OnLoopProcess changes (Arne Welzel, Corelight)
* cluster/ThreadedBackend: Switch to OnLoopProcess (Arne Welzel, Corelight)
* cluster/OnLoop: Introduce helper template class (Arne Welzel, Corelight)
* serializer/broker: Expose to_broker_event() and to_zeek_event() (Arne Welzel, Corelight)
This is useful for reuse by WebSocket clients that use
the JSON v1 encoding.
7.2.0-dev.288 | 2025-03-10 19:16:57 +0100
* btest/javascript: Add file_sniff() and file_state_remove() test (Arne Welzel, Corelight)

View file

@ -1 +1 @@
7.2.0-dev.288
7.2.0-dev.310

@ -1 +1 @@
Subproject commit b40f3fd3e93e906fe4f134a4d3279eacd6dbdb45
Subproject commit c99696a69e5ced0a91bf7c19098d391a57f279ce

@ -1 +1 @@
Subproject commit b38e9c8ebff08959a712a5663ba25e0624a3af00
Subproject commit d2bfec929540c1fec5d1d45f0bcee3cff1eb7fa5

View file

@ -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,13 +23,13 @@ RUN apk add --no-cache \
linux-headers \
make \
openssh-client \
openssl \
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

View file

@ -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

View file

@ -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

View file

@ -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.
@ -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

View file

@ -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

View file

@ -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

View file

@ -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);
}

View file

@ -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 <http://api.zeromq.org/4-2:zmq-ctx-set#toc4>`_
## and the `I/O threads <https://zguide.zeromq.org/docs/chapter2/#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
@ -134,12 +142,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 <http://api.zeromq.org/4-2:zmq-setsockopt#toc61>`_
## 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.
##

View file

@ -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,76 +158,48 @@ 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<EventSerializer> es, std::unique_ptr<LogSerializer> ls,
std::unique_ptr<detail::EventHandlingStrategy> ehs)
: Backend(std::move(es), std::move(ls), std::move(ehs)) {
onloop = new zeek::detail::OnLoopProcess<ThreadedBackend, QueueMessage>(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);
// Have the backend count so Zeek does not terminate.
onloop->Register(/*dont_count=*/false);
return true;
}
void ThreadedBackend::DoInitPostScript() {
// Register non-counting after parsing scripts.
register_io_source(this, messages_flare.FD(), 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));
}
void ThreadedBackend::DoTerminate() {
if ( onloop ) {
onloop->Close();
onloop = nullptr;
}
}
if ( fire )
messages_flare.Fire();
void ThreadedBackend::QueueForProcessing(QueueMessage&& qmessages) {
if ( onloop )
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();
}
if ( onloop )
onloop->Process();
}
for ( const auto& msg : to_process ) {
// sonarlint wants to use std::visit. not sure...
if ( auto* emsg = std::get_if<EventMessage>(&msg) ) {
ProcessEventMessage(emsg->topic, emsg->format, emsg->payload_span());
}
else if ( auto* lmsg = std::get_if<LogMessage>(&msg) ) {
ProcessLogMessage(lmsg->format, lmsg->payload_span());
}
else if ( auto* bmsg = std::get_if<BackendMessage>(&msg) ) {
ProcessBackendMessage(bmsg->tag, bmsg->payload_span());
}
else {
zeek::reporter->FatalError("Unimplemented QueueMessage %zu", msg.index());
}
void ThreadedBackend::Process(QueueMessage&& msg) {
// sonarlint wants to use std::visit. not sure...
if ( auto* emsg = std::get_if<EventMessage>(&msg) ) {
ProcessEventMessage(emsg->topic, emsg->format, emsg->payload_span());
}
else if ( auto* lmsg = std::get_if<LogMessage>(&msg) ) {
ProcessLogMessage(lmsg->format, lmsg->payload_span());
}
else if ( auto* bmsg = std::get_if<BackendMessage>(&msg) ) {
ProcessBackendMessage(bmsg->tag, bmsg->payload_span());
}
else {
zeek::reporter->FatalError("Unimplemented QueueMessage %zu", msg.index());
}
}

View file

@ -5,17 +5,14 @@
#pragma once
#include <memory>
#include <mutex>
#include <optional>
#include <string_view>
#include <variant>
#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<Val>;
using ArgsSpan = Span<const ValPtr>;
namespace detail {
template<class Proc, class Work>
class OnLoopProcess;
}
namespace cluster {
namespace detail {
@ -459,17 +461,19 @@ struct BackendMessage {
};
using QueueMessage = std::variant<EventMessage, LogMessage, BackendMessage>;
using QueueMessages = std::vector<QueueMessage>;
/**
* 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<EventSerializer> es, std::unique_ptr<LogSerializer> ls,
std::unique_ptr<detail::EventHandlingStrategy> ehs);
/**
* To be used by implementations to enqueue messages for processing on the IO loop.
*
@ -477,22 +481,13 @@ protected:
*
* @param messages Messages to be enqueued.
*/
void QueueForProcessing(QueueMessages&& messages);
void Process() override;
double GetNextTimeout() override { return -1; }
void QueueForProcessing(QueueMessage&& messages);
/**
* 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
@ -506,6 +501,8 @@ protected:
*/
bool DoInit() override;
void DoTerminate() override;
private:
/**
* Process a backend specific message queued as BackendMessage.
@ -518,10 +515,16 @@ private:
*/
virtual bool DoProcessBackendMessage(int tag, detail::byte_buffer_span payload) { return false; };
/**
* Hook method for OnLooProcess.
*/
void Process(QueueMessage&& messages);
// Allow access to Process(QueueMessages)
friend class zeek::detail::OnLoopProcess<ThreadedBackend, QueueMessage>;
// Members used for communication with the main thread.
std::mutex messages_mtx;
std::vector<QueueMessage> messages;
zeek::detail::Flare messages_flare;
zeek::detail::OnLoopProcess<ThreadedBackend, QueueMessage>* onloop = nullptr;
};

View file

@ -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<zeek::RecordVal>(args[0]));
auto ev = to_cluster_event(zeek::cast_intrusive<zeek::RecordVal>(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<zeek::RecordType>("Cluster::EndpointInfo");
static const auto net_info_type = zeek::id::find_type<zeek::RecordType>("Cluster::NetworkInfo");
auto net_rec = zeek::make_intrusive<zeek::RecordVal>(net_info_type);
net_rec->Assign(0, address);
net_rec->Assign(1, zeek::val_mgr->Port(port, proto));
auto ep_rec = zeek::make_intrusive<zeek::RecordVal>(ep_info_type);
ep_rec->Assign(0, id);
ep_rec->Assign(1, net_rec);
return ep_rec;
}
zeek::VectorValPtr make_string_vec(zeek::Span<const std::string> strings) {
static const auto string_vec_type = zeek::id::find_type<zeek::VectorType>("string_vec");
auto vec = zeek::make_intrusive<zeek::VectorVal>(string_vec_type);
vec->Reserve(strings.size());
for ( const auto& s : strings )
vec->Append(zeek::make_intrusive<zeek::StringVal>(s));
return vec;
}
} // namespace zeek::cluster::detail::bif

View file

@ -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<RecordVal>;
class VectorVal;
using VectorValPtr = IntrusivePtr<VectorVal>;
class Val;
using ValPtr = IntrusivePtr<Val>;
@ -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<const std::string> strings);
} // namespace cluster::detail::bif
} // namespace zeek

View file

@ -13,3 +13,4 @@ zeek_add_subdir_library(
add_subdirectory(backend)
add_subdirectory(serializer)
add_subdirectory(websocket)

View file

@ -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<EventSerializerComponent>("Cluster", "EventSerializerTag")),
log_serializers(plugin::ComponentManager<LogSerializerComponent>("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<Backend> Manager::InstantiateBackend(
const zeek::EnumValPtr& tag, std::unique_ptr<EventSerializer> event_serializer,
std::unique_ptr<LogSerializer> log_serializer,
@ -30,3 +42,25 @@ std::unique_ptr<LogSerializer> 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<websocket::detail::WebSocketEventDispatcher>(), options);
if ( ! server )
return false;
websocket_servers.insert({key, WebSocketServerEntry{options, std::move(server)}});
return true;
}

View file

@ -2,10 +2,12 @@
#pragma once
#include <map>
#include <memory>
#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<LogSerializerComponent>& 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<BackendComponent> backends;
plugin::ComponentManager<EventSerializerComponent> event_serializers;
plugin::ComponentManager<LogSerializerComponent> log_serializers;
using WebSocketServerKey = std::pair<std::string, uint16_t>;
struct WebSocketServerEntry {
websocket::detail::ServerOptions options;
std::unique_ptr<websocket::detail::WebSocketServer> server;
};
std::map<WebSocketServerKey, WebSocketServerEntry> websocket_servers;
};
// This manager instance only exists for plugins to register components,

186
src/cluster/OnLoop.h Normal file
View file

@ -0,0 +1,186 @@
// See the file "COPYING" in the main distribution directory for copyright.
#pragma once
#include <atomic>
#include <chrono>
#include <list>
#include <mutex>
#include <thread>
#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 Proc, class Work>
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<Work> 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<Work> 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<Work> queue;
size_t max_queue_size;
std::chrono::microseconds block_duration;
Proc* proc;
std::string tag;
std::atomic<int> queuers = 0;
std::thread::id main_thread_id;
};
} // namespace zeek::detail

View file

@ -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);

View file

@ -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

View file

@ -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 {
@ -77,8 +79,6 @@ ZeroMQBackend::~ZeroMQBackend() {
}
void ZeroMQBackend::DoInitPostScript() {
ThreadedBackend::DoInitPostScript();
listen_xpub_endpoint =
zeek::id::find_val<zeek::StringVal>("Cluster::Backend::ZeroMQ::listen_xpub_endpoint")->ToStdString();
listen_xsub_endpoint =
@ -89,17 +89,23 @@ void ZeroMQBackend::DoInitPostScript() {
zeek::id::find_val<zeek::StringVal>("Cluster::Backend::ZeroMQ::connect_xpub_endpoint")->ToStdString();
connect_xsub_endpoint =
zeek::id::find_val<zeek::StringVal>("Cluster::Backend::ZeroMQ::connect_xsub_endpoint")->ToStdString();
connect_xpub_nodrop =
zeek::id::find_val<zeek::BoolVal>("Cluster::Backend::ZeroMQ::connect_xpub_nodrop")->AsBool() ? 1 : 0;
listen_log_endpoint =
zeek::id::find_val<zeek::StringVal>("Cluster::Backend::ZeroMQ::listen_log_endpoint")->ToStdString();
linger_ms = static_cast<int>(zeek::id::find_val<zeek::IntVal>("Cluster::Backend::ZeroMQ::linger_ms")->AsInt());
poll_max_messages = zeek::id::find_val<zeek::CountVal>("Cluster::Backend::ZeroMQ::poll_max_messages")->Get();
debug_flags = zeek::id::find_val<zeek::CountVal>("Cluster::Backend::ZeroMQ::debug_flags")->Get();
proxy_io_threads =
static_cast<int>(zeek::id::find_val<zeek::CountVal>("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");
}
void ZeroMQBackend::DoTerminate() {
ThreadedBackend::DoTerminate();
ZEROMQ_DEBUG("Shutting down ctx");
ctx.shutdown();
ZEROMQ_DEBUG("Joining self_thread");
@ -131,16 +137,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<int>(zeek::id::find_val<zeek::IntVal>("Cluster::Backend::ZeroMQ::linger_ms")->AsInt());
int xpub_nodrop = zeek::id::find_val<zeek::BoolVal>("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 {
@ -239,7 +242,8 @@ bool ZeroMQBackend::DoInit() {
}
bool ZeroMQBackend::SpawnZmqProxyThread() {
proxy_thread = std::make_unique<ProxyThread>(listen_xpub_endpoint, listen_xsub_endpoint, listen_xpub_nodrop);
proxy_thread =
std::make_unique<ProxyThread>(listen_xpub_endpoint, listen_xsub_endpoint, listen_xpub_nodrop, proxy_io_threads);
return proxy_thread->Start();
}
@ -268,7 +272,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 ( 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.
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;
@ -281,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()});
@ -303,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;
}
@ -329,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
@ -368,9 +392,6 @@ void ZeroMQBackend::Run() {
using MultipartMessage = std::vector<zmq::message_t>;
auto HandleLogMessages = [this](const std::vector<MultipartMessage>& msgs) {
QueueMessages qmsgs;
qmsgs.reserve(msgs.size());
for ( const auto& msg : msgs ) {
// sender, format, type, payload
if ( msg.size() != 4 ) {
@ -379,22 +400,21 @@ void ZeroMQBackend::Run() {
}
detail::byte_buffer payload{msg[3].data<std::byte>(), msg[3].data<std::byte>() + msg[3].size()};
qmsgs.emplace_back(LogMessage{.format = std::string(msg[2].data<const char>(), msg[2].size()),
.payload = std::move(payload)});
}
LogMessage lm{.format = std::string(msg[2].data<const char>(), msg[2].size()),
.payload = std::move(payload)};
QueueForProcessing(std::move(qmsgs));
QueueForProcessing(std::move(lm));
}
};
auto HandleInprocMessages = [this](std::vector<MultipartMessage>& msgs) {
// 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() )
@ -432,13 +452,13 @@ void ZeroMQBackend::Run() {
} while ( ! result );
}
}
else {
ZEROMQ_THREAD_PRINTF("inproc: error: expected 1 or 4 parts, have %zu!\n", msg.size());
}
}
};
auto HandleXPubMessages = [this](const std::vector<MultipartMessage>& 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());
@ -464,17 +484,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<MultipartMessage>& 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());
@ -487,12 +502,12 @@ void ZeroMQBackend::Run() {
continue;
detail::byte_buffer payload{msg[3].data<std::byte>(), msg[3].data<std::byte>() + msg[3].size()};
qmsgs.emplace_back(EventMessage{.topic = std::string(msg[0].data<const char>(), msg[0].size()),
.format = std::string(msg[2].data<const char>(), msg[2].size()),
.payload = std::move(payload)});
}
EventMessage em{.topic = std::string(msg[0].data<const char>(), msg[0].size()),
.format = std::string(msg[2].data<const char>(), msg[2].size()),
.payload = std::move(payload)};
QueueForProcessing(std::move(qmsgs));
QueueForProcessing(std::move(em));
}
};
// Helper class running at destruction.

View file

@ -62,18 +62,18 @@ 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.
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;
@ -99,6 +99,7 @@ private:
std::thread self_thread;
int proxy_io_threads = 2;
std::unique_ptr<ProxyThread> proxy_thread;
// Tracking the subscriptions on the local XPUB socket.

View file

@ -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<zeek::RecordType>("Cluster::WebSocketServerOptions");
const auto& tls_options_type = zeek::id::find_type<zeek::RecordType>("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::RecordVal>{zeek::NewRef{}, options->AsRecordVal()};
auto tls_options_rec = options_rec->GetFieldOrDefault<zeek::RecordVal>("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<zeek::StringVal>("cert_file")->ToStdString()} : std::nullopt,
have_key ? std::optional{tls_options_rec->GetField<zeek::StringVal>("key_file")->ToStdString()} : std::nullopt,
tls_options_rec->GetFieldOrDefault<zeek::BoolVal>("enable_peer_verification")->Get(),
tls_options_rec->GetFieldOrDefault<zeek::StringVal>("ca_file")->ToStdString(),
tls_options_rec->GetFieldOrDefault<zeek::StringVal>("ciphers")->ToStdString(),
};
struct ServerOptions server_options {
options_rec->GetField<zeek::StringVal>("listen_host")->ToStdString(),
static_cast<uint16_t>(options_rec->GetField<zeek::PortVal>("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);
%}

View file

@ -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<broker::zeek::Event> to_broker_event(const detail::Event& ev) {
std::optional<broker::zeek::Event> detail::to_broker_event(const detail::Event& ev) {
broker::vector xs;
xs.reserve(ev.args.size());
@ -51,15 +43,7 @@ std::optional<broker::zeek::Event> 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<detail::Event> to_zeek_event(const broker::zeek::Event& ev) {
std::optional<detail::Event> detail::to_zeek_event(const broker::zeek::Event& ev) {
auto&& name = ev.name();
auto&& args = ev.args();
@ -117,8 +101,6 @@ std::optional<detail::Event> 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 )

View file

@ -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<detail::Event> 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<broker::zeek::Event> 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 {

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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 <memory>
#include <stdexcept>
#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<ix::ConnectionState> cs, std::shared_ptr<ix::WebSocket> 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<ix::ConnectionState> cs;
std::shared_ptr<ix::WebSocket> ws;
};
/**
* Implementation of WebSocketServer using the IXWebsocket library.
*/
class IXWebSocketServer : public WebSocketServer {
public:
IXWebSocketServer(std::unique_ptr<WebSocketEventDispatcher> dispatcher, std::unique_ptr<ix::WebSocketServer> server)
: WebSocketServer(std::move(dispatcher)), server(std::move(server)) {}
private:
void DoTerminate() override {
// Stop the server.
server->stop();
}
std::unique_ptr<ix::WebSocketServer> server;
};
std::unique_ptr<WebSocketServer> StartServer(std::unique_ptr<WebSocketEventDispatcher> dispatcher,
const ServerOptions& options) {
auto server =
std::make_unique<ix::WebSocketServer>(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<ix::WebSocket> websocket,
std::shared_ptr<ix::ConnectionState> cs) -> void {
// Hold a shared_ptr to the WebSocket object until we see the close.
std::shared_ptr<ix::WebSocket> 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<IxWebSocketClient>(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<IXWebSocketServer>(std::move(dispatcher), std::move(server));
}
} // namespace zeek::cluster::websocket::detail::ixwebsocket
using namespace zeek::cluster::websocket::detail;
std::unique_ptr<WebSocketServer> zeek::cluster::websocket::detail::StartServer(
std::unique_ptr<WebSocketEventDispatcher> dispatcher, const ServerOptions& options) {
// Just delegate to the above IXWebSocket specific implementation.
return ixwebsocket::StartServer(std::move(dispatcher), options);
}

View file

@ -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 <memory>
#include <string_view>
#include <variant>
#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<WebSocketClient> 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<WebSocketClient> 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<std::string>& 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<std::string> WebSocketClient::GetSubscriptions() const {
std::vector<std::string> 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<WebSocketEventDispatcher, WebSocketEvent>(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<zeek::EnumVal>("Cluster::event_serializer");
auto event_serializer = cluster::manager->InstantiateEventSerializer(event_serializer_val);
const auto& cluster_backend_val = id::find_val<zeek::EnumVal>("Cluster::backend");
auto event_handling_strategy = std::make_unique<WebSocketEventHandlingStrategy>(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<std::string> 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);
}
}

View file

@ -0,0 +1,321 @@
// See the file "COPYING" in the main distribution directory for copyright.
#pragma once
#include <list>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <variant>
#include <vector>
namespace zeek {
namespace detail {
template<class Proc, class Work>
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<std::string>& topic_prefixes);
/**
* @return The client's subscriptions.
*/
const std::vector<std::string> 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<std::string, bool> 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<WebSocketClient> 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<WebSocketOpen, WebSocketSubscribeFinished, WebSocketClose, WebSocketMessage>;
struct WebSocketSendReply {
std::shared_ptr<WebSocketClient> wsc;
std::string msg;
};
struct WebSocketCloseReply {
std::shared_ptr<WebSocketClient> wsc;
uint16_t code = 1000;
std::string reason = "Normal closure";
};
using WebSocketReply = std::variant<WebSocketSendReply, WebSocketCloseReply>;
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<WebSocketClient> wsc;
std::shared_ptr<zeek::cluster::Backend> backend;
uint64_t msg_count = 0;
std::list<WebSocketMessage> 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<WebSocketEventDispatcher, WebSocketEvent>;
// Clients that this dispatcher is tracking.
std::map<std::string, WebSocketClientEntry> clients;
// Connector to the IO loop.
zeek::detail::OnLoopProcess<WebSocketEventDispatcher, WebSocketEvent>* 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<WebSocketEventDispatcher> 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<WebSocketEventDispatcher> dispatcher;
};
/**
* TLS configuration for a WebSocket server.
*/
struct TLSOptions {
std::optional<std::string> cert_file;
std::optional<std::string> 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<WebSocketServer> StartServer(std::unique_ptr<WebSocketEventDispatcher> dispatcher,
const ServerOptions& options);
} // namespace websocket::detail
} // namespace cluster
} // namespace zeek

View file

@ -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)

@ -0,0 +1 @@
Subproject commit 80e6c4fe48dcad816a0e684dbb269957f9073e79

View file

@ -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%);

View file

@ -74,6 +74,7 @@ static std::unordered_map<std::string, unsigned int> 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},

View file

@ -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();

View file

@ -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:<port> (Cluster::__listen_websocket(ws_opts_x))
error in <...>/main.zeek, line 654: Already listening on 127.0.0.1:<port> (Cluster::__listen_websocket(ws_opts_wss_port))
received termination signal

View file

@ -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))

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -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'}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -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'}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -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

View file

@ -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

View file

@ -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 <nodeid> (127.0.0.1:<port>) subscribed to [/topic/ws/1, /topic/ws/all]
manager WebSocket client <nodeid> (127.0.0.1:<port>) subscribed to [/topic/ws/2, /topic/ws/all]
manager WebSocket client <nodeid> (127.0.0.1:<port>) subscribed to [/topic/ws/3, /topic/ws/all]
manager WebSocket client <nodeid> (127.0.0.1:<port>) gone
manager WebSocket client <nodeid> (127.0.0.1:<port>) gone
manager WebSocket client <nodeid> (127.0.0.1:<port>) gone

View file

@ -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

View file

@ -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:<port> (Cluster::listen_websocket(ws_opts_x))
error in <...>/listen-idempotent.zeek, line 47: Already listening on 127.0.0.1:<port> (Cluster::listen_websocket(ws_opts_wss_port))
received termination signal

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -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}]

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -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}]

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -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}]

View file

@ -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

View file

@ -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

View file

@ -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)))

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -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}]

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.

View file

@ -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}]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 ()

View file

@ -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, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_BitTorrent.events.bif.zeek, <...>/Zeek_BitTorrent.events.bif.zeek) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek, <...>/Zeek_Cluster_Backend_ZeroMQ.cluster_backend_zeromq.bif.zeek) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_Cluster_WebSocket.events.bif.zeek, <...>/Zeek_Cluster_WebSocket.events.bif.zeek) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_ConfigReader.config.bif.zeek, <...>/Zeek_ConfigReader.config.bif.zeek) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_ConnSize.events.bif.zeek, <...>/Zeek_ConnSize.events.bif.zeek) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_ConnSize.functions.bif.zeek, <...>/Zeek_ConnSize.functions.bif.zeek) -> (-1, <no content>)
@ -841,6 +844,7 @@
0.000000 MetaHookPost LoadFileExtended(0, base/init-frameworks-and-bifs.zeek, <...>/init-frameworks-and-bifs.zeek) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, base/packet-protocols, <...>/packet-protocols) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, base<...>/CPP-load.bif, <...>/CPP-load.bif.zeek) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, base<...>/Zeek_Cluster_WebSocket.events.bif.zeek, <...>/Zeek_Cluster_WebSocket.events.bif.zeek) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, base<...>/Zeek_GTPv1.events.bif, <...>/Zeek_GTPv1.events.bif.zeek) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, base<...>/Zeek_GTPv1.functions.bif, <...>/Zeek_GTPv1.functions.bif.zeek) -> (-1, <no content>)
0.000000 MetaHookPost LoadFileExtended(0, base<...>/Zeek_KRB.types.bif, <...>/Zeek_KRB.types.bif.zeek) -> (-1, <no content>)
@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <nodeid> /g" | sed -r "s/:[0-9]+/:<port>/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

View file

@ -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:<port>/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();
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]);
}

View file

@ -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

View file

@ -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

View file

@ -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",