From 8ebd054abc5daaada1b44642f3e827e98141b799 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Sat, 13 Jan 2024 12:27:54 +0100 Subject: [PATCH 01/13] HTTP: Add mechanism to instantiate Upgrade analyzer When a HTTP upgrade request/reply is detected, lookup an analyzer tag from HTTP::upgrade_analyzers, or if nothing is found, attach PIA_TCP. --- scripts/base/init-bare.zeek | 11 +++++- src/analyzer/protocol/http/HTTP.cc | 62 +++++++++++++++++++++++------- src/analyzer/protocol/http/HTTP.h | 1 + 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/scripts/base/init-bare.zeek b/scripts/base/init-bare.zeek index 0d818cdf80..7ab94432d7 100644 --- a/scripts/base/init-bare.zeek +++ b/scripts/base/init-bare.zeek @@ -423,7 +423,6 @@ export { ## The full list of TCP Option fields parsed from a TCP header. type OptionList: vector of Option; } -module GLOBAL; module Tunnel; export { @@ -449,6 +448,16 @@ export { const max_changes_per_connection: count = 5 &redef; } # end export + +module HTTP; +export { + ## Lookup table for Upgrade analyzers. First, a case sensitive lookup + ## is done using the client's Upgrade header. If no match is found, + ## the all lower-case value is used. If there's still no match Zeek + ## uses dynamic protocol detection for the upgraded to protocol instead. + const upgrade_analyzers: table[string] of Analyzer::Tag &redef; +} + module GLOBAL; ## A type alias for a vector of encapsulating "connections", i.e. for when diff --git a/src/analyzer/protocol/http/HTTP.cc b/src/analyzer/protocol/http/HTTP.cc index 4a4eaca9d0..bd252e837d 100644 --- a/src/analyzer/protocol/http/HTTP.cc +++ b/src/analyzer/protocol/http/HTTP.cc @@ -12,6 +12,7 @@ #include "zeek/Event.h" #include "zeek/NetVar.h" +#include "zeek/analyzer/Manager.h" #include "zeek/analyzer/protocol/http/events.bif.h" #include "zeek/analyzer/protocol/mime/MIME.h" #include "zeek/file_analysis/Manager.h" @@ -775,12 +776,9 @@ void HTTP_Analyzer::DeliverStream(int len, const u_char* data, bool is_orig) { if ( TCP() && TCP()->IsPartial() ) return; - if ( upgraded ) - return; - - if ( pia ) { + if ( upgraded || pia ) { // There will be a PIA instance if this connection has been identified - // as a connect proxy. + // as a connect proxy, or a child analyzer if there was an upgrade. ForwardStream(len, data, is_orig); return; } @@ -1311,15 +1309,8 @@ void HTTP_Analyzer::ReplyMade(bool interrupted, const char* msg) { reply_reason_phrase = nullptr; // unanswered requests = 1 because there is no pop after 101. - if ( reply_code == 101 && unanswered_requests.size() == 1 && upgrade_connection && upgrade_protocol.size() ) { - // Upgraded connection that switches immediately - e.g. websocket. - upgraded = true; - RemoveSupportAnalyzer(content_line_orig); - RemoveSupportAnalyzer(content_line_resp); - - if ( http_connection_upgrade ) - EnqueueConnEvent(http_connection_upgrade, ConnVal(), make_intrusive(upgrade_protocol)); - } + if ( reply_code == 101 && unanswered_requests.size() == 1 && upgrade_connection && upgrade_protocol.size() ) + HTTP_Upgrade(); reply_code = 0; upgrade_connection = false; @@ -1331,6 +1322,49 @@ void HTTP_Analyzer::ReplyMade(bool interrupted, const char* msg) { reply_state = EXPECT_REPLY_LINE; } +void HTTP_Analyzer::HTTP_Upgrade() { + // Upgraded connection that switches immediately - e.g. websocket. + + // Lookup an analyzer tag in the HTTP::upgrade_analyzer table. + static const auto& upgrade_analyzers = id::find_val("HTTP::upgrade_analyzers"); + + auto upgrade_protocol_val = make_intrusive(upgrade_protocol); + auto v = upgrade_analyzers->Find(upgrade_protocol_val); + if ( ! v ) { + // If not found, try the all lower version, too. + auto lower_upgrade_protocol = util::strtolower(upgrade_protocol); + upgrade_protocol_val = make_intrusive(lower_upgrade_protocol); + v = upgrade_analyzers->Find(upgrade_protocol_val); + } + + if ( v ) { + auto analyzer_tag_val = cast_intrusive(v); + DBG_LOG(DBG_ANALYZER, "Found %s in HTTP::upgrade_analyzers for %s", + analyzer_tag_val->GetType()->Lookup(analyzer_tag_val->AsEnum()), + upgrade_protocol_val->CheckString()); + auto analyzer_tag = analyzer_mgr->GetComponentTag(analyzer_tag_val.get()); + auto* analyzer = analyzer_mgr->InstantiateAnalyzer(analyzer_tag, Conn()); + if ( analyzer ) + AddChildAnalyzer(analyzer); + } + else { + DBG_LOG(DBG_ANALYZER, "No mapping for %s in HTTP::upgrade_analyzers, using PIA instead", + upgrade_protocol.c_str()); + pia = new analyzer::pia::PIA_TCP(Conn()); + if ( AddChildAnalyzer(pia) ) { + pia->FirstPacket(true, nullptr); + pia->FirstPacket(false, nullptr); + } + } + + upgraded = true; + RemoveSupportAnalyzer(content_line_orig); + RemoveSupportAnalyzer(content_line_resp); + + if ( http_connection_upgrade ) + EnqueueConnEvent(http_connection_upgrade, ConnVal(), make_intrusive(upgrade_protocol)); +} + void HTTP_Analyzer::RequestClash(Val* /* clash_val */) { Weird("multiple_HTTP_request_elements"); diff --git a/src/analyzer/protocol/http/HTTP.h b/src/analyzer/protocol/http/HTTP.h index 016ba4f2e1..62f519201d 100644 --- a/src/analyzer/protocol/http/HTTP.h +++ b/src/analyzer/protocol/http/HTTP.h @@ -220,6 +220,7 @@ protected: void HTTP_Request(); void HTTP_Reply(); + void HTTP_Upgrade(); void RequestMade(bool interrupted, const char* msg); void ReplyMade(bool interrupted, const char* msg); From efc2681152da6ca7f2742ba46b90941f081d3b25 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Sat, 13 Jan 2024 16:56:52 +0100 Subject: [PATCH 02/13] WebSocket: Introduce new analyzer and log This adds a new WebSocket analyzer that is enabled with the HTTP upgrade mechanism introduced previously. It is a first implementation in BinPac with manual chunking of frame payload. Configuration of the analyzer is sketched via the new websocket_handshake() event and a configuration BiF called WebSocket::__configure_analyzer(). In short, script land collects WebSocket related HTTP headers and can forward these to the analyzer to change its parsing behavior at websocket_handshake() time. For now, however, there's no actual logic that would change behavior based on agreed upon extensions exchanged via HTTP headers (e.g. frame compression). WebSocket::Configure() simply attaches a PIA_TCP analyzer to the WebSocket analyzer for dynamic protocol detection (or a custom analyzer if set). The added pcaps show this in action for tunneled ssh, http and https using wstunnel. One test pcap is Broker's WebSocket traffic from our own test suite, the other is the Jupyter websocket traffic from the ticket/discussion. This commit further adds a basic websocket.log that aggregates the WebSocket specific headers (Sec-WebSocket-*) headers into a single log. Closes #3424 --- scripts/base/init-bare.zeek | 35 ++++ scripts/base/init-default.zeek | 1 + .../base/protocols/websocket/__load__.zeek | 2 + scripts/base/protocols/websocket/consts.zeek | 21 +++ scripts/base/protocols/websocket/main.zeek | 176 ++++++++++++++++++ src/analyzer/protocol/CMakeLists.txt | 1 + .../protocol/websocket/CMakeLists.txt | 15 ++ src/analyzer/protocol/websocket/Plugin.cc | 24 +++ src/analyzer/protocol/websocket/WebSocket.cc | 101 ++++++++++ src/analyzer/protocol/websocket/WebSocket.h | 38 ++++ src/analyzer/protocol/websocket/consts.bif | 1 + src/analyzer/protocol/websocket/events.bif | 71 +++++++ src/analyzer/protocol/websocket/functions.bif | 46 +++++ src/analyzer/protocol/websocket/types.bif | 4 + .../protocol/websocket/websocket-analyzer.pac | 126 +++++++++++++ .../protocol/websocket/websocket-protocol.pac | 149 +++++++++++++++ src/analyzer/protocol/websocket/websocket.pac | 24 +++ .../canonified_loaded_scripts.log | 4 + .../canonified_loaded_scripts.log | 7 + .../btest/Baseline/coverage.find-bro-logs/out | 1 + .../coverage.record-fields/out.default | 14 ++ testing/btest/Baseline/plugins.hooks/output | 24 +++ .../scripts.base.files.x509.files/files.log | 12 +- .../conn.log.cut | 3 + .../websocket.log | 11 ++ .../out | 91 +++++++++ .../conn.log.cut | 3 + .../websocket.log | 11 ++ .../conn.log.cut | 3 + .../http.log.cut | 4 + .../websocket.log | 11 ++ .../conn.log.cut | 3 + .../ssl.log.cut | 3 + .../websocket.log | 11 ++ .../conn.log.cut | 4 + .../websocket.log | 12 ++ .../conn.log.cut | 4 + .../ssh.log.cut | 4 + .../websocket.log | 12 ++ .../conn.log.cut | 4 + .../ssh.log.cut | 4 + .../websocket.log | 12 ++ .../Traces/websocket/broker-websocket.pcap | Bin 0 -> 5103 bytes .../Traces/websocket/jupyter-websocket.pcap | Bin 0 -> 10340 bytes .../btest/Traces/websocket/wstunnel-http.pcap | Bin 0 -> 2181 bytes .../Traces/websocket/wstunnel-https.pcap | Bin 0 -> 97914 bytes .../btest/Traces/websocket/wstunnel-ssh.pcap | Bin 0 -> 44410 bytes .../protocols/websocket/broker-websocket.zeek | 13 ++ .../base/protocols/websocket/events.zeek | 38 ++++ .../websocket/jupyter-websocket.zeek | 13 ++ .../protocols/websocket/wstunnel-http.zeek | 16 ++ .../protocols/websocket/wstunnel-https.zeek | 16 ++ .../wstunnel-ssh-configure-wrong.zeek | 21 +++ .../websocket/wstunnel-ssh-configure.zeek | 22 +++ .../protocols/websocket/wstunnel-ssh.zeek | 16 ++ 55 files changed, 1256 insertions(+), 6 deletions(-) create mode 100644 scripts/base/protocols/websocket/__load__.zeek create mode 100644 scripts/base/protocols/websocket/consts.zeek create mode 100644 scripts/base/protocols/websocket/main.zeek create mode 100644 src/analyzer/protocol/websocket/CMakeLists.txt create mode 100644 src/analyzer/protocol/websocket/Plugin.cc create mode 100644 src/analyzer/protocol/websocket/WebSocket.cc create mode 100644 src/analyzer/protocol/websocket/WebSocket.h create mode 100644 src/analyzer/protocol/websocket/consts.bif create mode 100644 src/analyzer/protocol/websocket/events.bif create mode 100644 src/analyzer/protocol/websocket/functions.bif create mode 100644 src/analyzer/protocol/websocket/types.bif create mode 100644 src/analyzer/protocol/websocket/websocket-analyzer.pac create mode 100644 src/analyzer/protocol/websocket/websocket-protocol.pac create mode 100644 src/analyzer/protocol/websocket/websocket.pac create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.broker-websocket/conn.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.broker-websocket/websocket.log create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.events/out create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.jupyter-websocket/conn.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.jupyter-websocket/websocket.log create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/conn.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/http.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/websocket.log create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/conn.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/ssl.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/websocket.log create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-wrong/conn.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-wrong/websocket.log create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/conn.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/ssh.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/websocket.log create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/conn.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/ssh.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/websocket.log create mode 100644 testing/btest/Traces/websocket/broker-websocket.pcap create mode 100644 testing/btest/Traces/websocket/jupyter-websocket.pcap create mode 100644 testing/btest/Traces/websocket/wstunnel-http.pcap create mode 100644 testing/btest/Traces/websocket/wstunnel-https.pcap create mode 100644 testing/btest/Traces/websocket/wstunnel-ssh.pcap create mode 100644 testing/btest/scripts/base/protocols/websocket/broker-websocket.zeek create mode 100644 testing/btest/scripts/base/protocols/websocket/events.zeek create mode 100644 testing/btest/scripts/base/protocols/websocket/jupyter-websocket.zeek create mode 100644 testing/btest/scripts/base/protocols/websocket/wstunnel-http.zeek create mode 100644 testing/btest/scripts/base/protocols/websocket/wstunnel-https.zeek create mode 100644 testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure-wrong.zeek create mode 100644 testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure.zeek create mode 100644 testing/btest/scripts/base/protocols/websocket/wstunnel-ssh.zeek diff --git a/scripts/base/init-bare.zeek b/scripts/base/init-bare.zeek index 7ab94432d7..3e153c204a 100644 --- a/scripts/base/init-bare.zeek +++ b/scripts/base/init-bare.zeek @@ -458,6 +458,41 @@ export { const upgrade_analyzers: table[string] of Analyzer::Tag &redef; } +module WebSocket; +export { + ## The WebSocket analyzer consumes and forwards + ## frame payload in chunks to keep memory usage + ## bounded. There should not be a reason to change + ## this value except for debugging and + ## testing reasons. + const payload_chunk_size = 8192 &redef; + + ## Whether to enable DPD on WebSocket frame payload by default. + const use_dpd_default = T &redef; + + ## Record type that is passed to :zeek:see:`WebSocket::__configure_analyzer`. + ## + ## This allows to configure the WebSocket analyzer given parameters + ## collected from HTTP headers. + type AnalyzerConfig: record { + ## The analyzer to attach for analysis of the WebSocket + ## frame payload. See *use_dpd* below for the behavior + ## when unset. + analyzer: Analyzer::Tag &optional; + + ## If *analyzer* is unset, determines whether to attach a + ## PIA_TCP analyzer for dynamic protocol detection with + ## WebSocket payload. + use_dpd: bool &default=use_dpd_default; + + ## The subprotocol as selected by the server, if any. + subprotocol: string &optional; + + ## The WebSocket extensions as selected by the server, if any. + server_extensions: vector of string &optional; + }; +} + module GLOBAL; ## A type alias for a vector of encapsulating "connections", i.e. for when diff --git a/scripts/base/init-default.zeek b/scripts/base/init-default.zeek index 95b1c64628..3a3efb6853 100644 --- a/scripts/base/init-default.zeek +++ b/scripts/base/init-default.zeek @@ -78,6 +78,7 @@ @load base/protocols/ssh @load base/protocols/ssl @load base/protocols/syslog +@load base/protocols/websocket @load base/protocols/tunnels @load base/protocols/xmpp diff --git a/scripts/base/protocols/websocket/__load__.zeek b/scripts/base/protocols/websocket/__load__.zeek new file mode 100644 index 0000000000..465d04deda --- /dev/null +++ b/scripts/base/protocols/websocket/__load__.zeek @@ -0,0 +1,2 @@ +@load ./consts.zeek +@load ./main.zeek diff --git a/scripts/base/protocols/websocket/consts.zeek b/scripts/base/protocols/websocket/consts.zeek new file mode 100644 index 0000000000..9bdffc2b7c --- /dev/null +++ b/scripts/base/protocols/websocket/consts.zeek @@ -0,0 +1,21 @@ +##! WebSocket constants. + +module WebSocket; + +export { + const OPCODE_CONTINUATION = 0x00; + const OPCODE_TEXT = 0x01; + const OPCODE_BINARY = 0x02; + const OPCODE_CLOSE = 0x08; + const OPCODE_PING = 0x09; + const OPCODE_PONG = 0x0a; + + const opcodes: table[count] of string = { + [OPCODE_CONTINUATION] = "continuation", + [OPCODE_TEXT] = "text", + [OPCODE_BINARY] = "binary", + [OPCODE_CLOSE] = "close", + [OPCODE_PING] = "ping", + [OPCODE_PONG] = "pong", + } &default=function(opcode: count): string { return fmt("unknown-%x", opcode); } &redef; +} diff --git a/scripts/base/protocols/websocket/main.zeek b/scripts/base/protocols/websocket/main.zeek new file mode 100644 index 0000000000..8d22c250db --- /dev/null +++ b/scripts/base/protocols/websocket/main.zeek @@ -0,0 +1,176 @@ +##! Implements base functionality for WebSocket analysis. +##! +##! Upon a websocket_handshake(), logs all gathered information into websocket.log +##! and then configures the WebSocket analyzer with the headers collected using +##! http events. + +@load base/protocols/http + +module WebSocket; + +# Register the WebSocket analyzer as HTTP upgrade analyzer. +redef HTTP::upgrade_analyzers += { + ["websocket"] = Analyzer::ANALYZER_WEBSOCKET, +}; + +export { + redef enum Log::ID += { LOG }; + + ## The record type for the WebSocket log. + type Info: record { + ## Timestamp + ts: time &log; + ## Unique ID for the connection. + uid: string &log; + ## The connection's 4-tuple of endpoint addresses/ports. + id: conn_id &log; + ## Same as in the HTTP log. + host: string &log &optional; + ## Same as in the HTTP log. + uri: string &log &optional; + ## Same as in the HTTP log. + user_agent: string &log &optional; + ## The WebSocket subprotocol as selected by the server. + subprotocol: string &log &optional; + ## The protocols requested by the client, if any. + client_protocols: vector of string &log &optional; + ## The extensions selected by the the server, if any. + server_extensions: vector of string &log &optional; + ## The extensions requested by the client, if any. + client_extensions: vector of string &log &optional; + }; + + ## Event that can be handled to access the WebSocket record as it is + ## sent on to the logging framework. + global log_websocket: event(rec: Info); + + ## Log policy hook. + global log_policy: Log::PolicyHook; + + ## Hook to allow interception of WebSocket analyzer configuration. + ## + ## c: The connection + ## + ## aid: The analyzer ID for the WebSocket analyzer. + ## + ## config: The configuration record, also containing information + ## about the subprotocol and extensions. + global configure_analyzer: hook(c: connection, aid: count, config: AnalyzerConfig); +} + +redef record connection += { + websocket: Info &optional; +}; + +function set_websocket(c: connection) + { + c$websocket = Info( + $ts=network_time(), + $uid=c$uid, + $id=c$id, + ); + } + +event http_header(c: connection, is_orig: bool, name: string, value: string) + { + if ( ! starts_with(name, "SEC-WEBSOCKET-") ) + return; + + if ( ! c?$websocket ) + set_websocket(c); + + local ws = c$websocket; + + if ( is_orig ) + { + if ( name == "SEC-WEBSOCKET-PROTOCOL" ) + { + if ( ! ws?$client_protocols ) + ws$client_protocols = vector(); + + ws$client_protocols += split_string(value, / *, */); + } + + else if ( name == "SEC-WEBSOCKET-EXTENSIONS" ) + { + if ( ! ws?$client_extensions ) + ws$client_extensions = vector(); + + ws$client_extensions += split_string(value, / *, */); + } + } + else + { + if ( name == "SEC-WEBSOCKET-PROTOCOL" ) + { + ws$subprotocol = value; + } + else if ( name == "SEC-WEBSOCKET-EXTENSIONS" ) + { + if ( ! ws?$server_extensions ) + ws$server_extensions = vector(); + + ws$server_extensions += split_string(value, / *, */); + } + } + } + +event http_request(c: connection, method: string, original_URI: string, + unescaped_URI: string, version: string) + { + # If we see a http_request and have websocket state, wipe it as + # we should've seen a websocket_handshake even on success and + # likely no more http events. + if ( ! c?$websocket ) + delete c$websocket; + } + +event websocket_handshake(c: connection, aid: count) &priority=5 + { + if ( ! c?$websocket ) + { + # This means we never saw a Sec-WebSocket-* header, weird. + Reporter::conn_weird("websocket_handshake_unexpected", c, "", "WebSocket"); + set_websocket(c); + } + + local ws = c$websocket; + + # Replicate some information from the HTTP.log + if ( c?$http ) + { + if ( c$http?$host ) + ws$host = c$http$host; + + if ( c$http?$uri ) + ws$uri = c$http$uri; + + if ( c$http?$user_agent ) + ws$user_agent = c$http$user_agent; + } + } + +event websocket_handshake(c: connection, aid: count) &priority=-5 + { + local ws = c$websocket; + + local config = AnalyzerConfig(); + if ( ws?$subprotocol ) + config$subprotocol = ws$subprotocol; + + if ( ws?$server_extensions ) + config$server_extensions = ws$server_extensions; + + # Give other scripts a chance to modify the analyzer configuration. + hook WebSocket::configure_analyzer(c, aid, config); + + WebSocket::__configure_analyzer(c, aid, config); + + ws$ts = network_time(); + Log::write(LOG, ws); + } + +event zeek_init() + { + Log::create_stream(LOG, [$columns=Info, $ev=log_websocket, $path="websocket", $policy=log_policy]); + } diff --git a/src/analyzer/protocol/CMakeLists.txt b/src/analyzer/protocol/CMakeLists.txt index 79e5b6cea0..896c53b4e6 100644 --- a/src/analyzer/protocol/CMakeLists.txt +++ b/src/analyzer/protocol/CMakeLists.txt @@ -42,5 +42,6 @@ add_subdirectory(ssh) add_subdirectory(ssl) add_subdirectory(syslog) add_subdirectory(tcp) +add_subdirectory(websocket) add_subdirectory(xmpp) add_subdirectory(zip) diff --git a/src/analyzer/protocol/websocket/CMakeLists.txt b/src/analyzer/protocol/websocket/CMakeLists.txt new file mode 100644 index 0000000000..d522c58821 --- /dev/null +++ b/src/analyzer/protocol/websocket/CMakeLists.txt @@ -0,0 +1,15 @@ +zeek_add_plugin( + Zeek + WebSocket + SOURCES + WebSocket.cc + Plugin.cc + BIFS + consts.bif + events.bif + functions.bif + types.bif + PAC + websocket.pac + websocket-analyzer.pac + websocket-protocol.pac) diff --git a/src/analyzer/protocol/websocket/Plugin.cc b/src/analyzer/protocol/websocket/Plugin.cc new file mode 100644 index 0000000000..c3ef84765a --- /dev/null +++ b/src/analyzer/protocol/websocket/Plugin.cc @@ -0,0 +1,24 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +#include "zeek/plugin/Plugin.h" + +#include "zeek/analyzer/Component.h" + +#include "analyzer/protocol/websocket/WebSocket.h" + +namespace zeek::plugin::detail::Zeek_WebSocket { + +class Plugin : public zeek::plugin::Plugin { +public: + zeek::plugin::Configuration Configure() override { + AddComponent( + new zeek::analyzer::Component("WebSocket", zeek::analyzer::websocket::WebSocket_Analyzer::Instantiate)); + + zeek::plugin::Configuration config; + config.name = "Zeek::WebSocket"; + config.description = "WebSocket analyzer"; + return config; + } +} plugin; + +} // namespace zeek::plugin::detail::Zeek_WebSocket diff --git a/src/analyzer/protocol/websocket/WebSocket.cc b/src/analyzer/protocol/websocket/WebSocket.cc new file mode 100644 index 0000000000..20e8a22086 --- /dev/null +++ b/src/analyzer/protocol/websocket/WebSocket.cc @@ -0,0 +1,101 @@ +// See the file in the main distribution directory for copyright. + +#include "zeek/analyzer/protocol/websocket/WebSocket.h" + +#include + +#include "zeek/analyzer/Manager.h" +#include "zeek/analyzer/protocol/pia/PIA.h" +#include "zeek/analyzer/protocol/websocket/events.bif.h" + +namespace zeek::analyzer::websocket { + +WebSocket_Analyzer::WebSocket_Analyzer(Connection* conn) : analyzer::tcp::TCP_ApplicationAnalyzer("WebSocket", conn) { + // TODO: Consider approaches dispatching to optionally use a + // Spicy analyzer here instead of the BinPac interpreter. + // + // E.g. we could instantiate a SPICY_WEBSOCKET analyzer and pass it the necessary + // information and call DeliverStream() directly on it. + interp = std::make_unique(this); +} + +void WebSocket_Analyzer::Init() { + tcp::TCP_ApplicationAnalyzer::Init(); + + // This event calls back via Configure() + zeek::BifEvent::enqueue_websocket_handshake(this, Conn(), GetID()); +} + +bool WebSocket_Analyzer::Configure(zeek::RecordValPtr config) { + // TODO: Check extensions and modify parsing if needed, e.g. WebSocket frame + // compression extension: https://www.rfc-editor.org/rfc/rfc7692.html + // + // interp->SetExtensions(...) + // + // TODO: The Sec-WebSocket-Protocol header might provide some information + // that we could leverage to instantiate a more specific analyzer. + // + // For now, we just attach a PIA analyzer as child analyzer. + + static const auto& config_type = id::find_type("WebSocket::AnalyzerConfig"); + static int analyzer_idx = config_type->FieldOffset("analyzer"); + static int use_dpd_idx = config_type->FieldOffset("use_dpd"); + + if ( config->HasField(analyzer_idx) ) { + const auto& analyzer_tag_val = config->GetField(analyzer_idx); + auto analyzer_tag = analyzer_mgr->GetComponentTag(analyzer_tag_val.get()); + + if ( analyzer_tag == zeek::Tag() ) { + reporter->InternalWarning("no component tag for enum '%s'", + analyzer_tag_val->GetType()->Lookup(analyzer_tag_val->AsEnum())); + return false; + } + + DBG_LOG(DBG_ANALYZER, "%s Configure() using analyzer %s", fmt_analyzer(this).c_str(), + analyzer_tag_val->GetType()->Lookup(analyzer_tag_val->AsEnum())); + + auto* analyzer = analyzer_mgr->InstantiateAnalyzer(analyzer_tag, Conn()); + if ( ! analyzer ) + return false; + + return AddChildAnalyzer(analyzer); + } + else if ( config->GetField(use_dpd_idx)->AsBool() ) { + DBG_LOG(DBG_ANALYZER, "%s Configure() enables DPD via PIA_TCP", fmt_analyzer(this).c_str()); + + auto* pia = new analyzer::pia::PIA_TCP(Conn()); + if ( AddChildAnalyzer(pia) ) { + pia->FirstPacket(true, nullptr); + pia->FirstPacket(false, nullptr); + return true; + } + + return false; + } + + // Neither analyzer nor dpd was enabled, success. + return true; +} + + +void WebSocket_Analyzer::DeliverStream(int len, const u_char* data, bool orig) { + analyzer::tcp::TCP_ApplicationAnalyzer::DeliverStream(len, data, orig); + if ( had_gap ) { + DBG_LOG(DBG_ANALYZER, "Skipping data after gap len=%d orig=%d", len, orig); + return; + } + + try { + interp->NewData(orig, data, data + len); + } catch ( const binpac::Exception& e ) { + AnalyzerViolation(e.c_msg(), reinterpret_cast(data), len); + } +} + +void WebSocket_Analyzer::Undelivered(uint64_t seq, int len, bool orig) { + analyzer::tcp::TCP_ApplicationAnalyzer::Undelivered(seq, len, orig); + interp->NewGap(orig, len); + had_gap = true; +} + +} // namespace zeek::analyzer::websocket diff --git a/src/analyzer/protocol/websocket/WebSocket.h b/src/analyzer/protocol/websocket/WebSocket.h new file mode 100644 index 0000000000..394eab194e --- /dev/null +++ b/src/analyzer/protocol/websocket/WebSocket.h @@ -0,0 +1,38 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +#pragma once + +#include + +#include "zeek/analyzer/protocol/tcp/TCP.h" +#include "zeek/analyzer/protocol/websocket/websocket_pac.h" + +namespace zeek::analyzer::websocket { + +/** + * A WebSocket analyzer to be used directly on top of HTTP. + */ +class WebSocket_Analyzer : public analyzer::tcp::TCP_ApplicationAnalyzer { +public: + WebSocket_Analyzer(zeek::Connection* conn); + ~WebSocket_Analyzer() = default; + + /** + * Allows script land to configure the WebSocket analyzer before analysis. + * + * @param config Zeek value of type WebSocket::AnalyzerConfig + */ + bool Configure(zeek::RecordValPtr config); + + void Init() override; + void DeliverStream(int len, const u_char* data, bool orig) override; + void Undelivered(uint64_t seq, int len, bool orig) override; + + static zeek::analyzer::Analyzer* Instantiate(Connection* conn) { return new WebSocket_Analyzer(conn); } + +private: + std::unique_ptr interp; + bool had_gap = false; +}; + +} // namespace zeek::analyzer::websocket diff --git a/src/analyzer/protocol/websocket/consts.bif b/src/analyzer/protocol/websocket/consts.bif new file mode 100644 index 0000000000..164181474c --- /dev/null +++ b/src/analyzer/protocol/websocket/consts.bif @@ -0,0 +1 @@ +const WebSocket::payload_chunk_size: count; diff --git a/src/analyzer/protocol/websocket/events.bif b/src/analyzer/protocol/websocket/events.bif new file mode 100644 index 0000000000..77f450c5fe --- /dev/null +++ b/src/analyzer/protocol/websocket/events.bif @@ -0,0 +1,71 @@ +## Generated when a WebSocket handshake completed. +## +## This is a bit artificial. It can be used to configure the WebSocket +## analyzer if the HTTP headers contained protocol and extension headers. +## +## c: The WebSocket connection. +## +## aid: The analyzer identifier of the WebSocket analyzer. +## +## .. zeek:see:: WebSocket::__configure_analyzer +## +## .. zeek:see:: WebSocket::configure_analyzer +## +event websocket_handshake%(c: connection, aid: count%); + +## Generated for every WebSocket frame. +## +## c: The WebSocket connection. +## +## is_orig: True if the frame is from the originator, else false. +## +## fin: True if the fin bit is set, else false. +## +## rsv: The value of the RSV1, RSV2 and RSV3 bits. +## +## opcode: The frame's opcode. +## +## payload_len: The frame's payload length. +## +event websocket_frame%(c: connection, is_orig: bool, fin: bool, rsv: count, opcode: count, payload_len: count%); + +## Generated for every chunk of WebSocket frame payload data. +## +## Do not use it to extract data from a WebSocket connection unless for testing +## or experimentation. Consider implementing a proper analyzer instead. +## +## c: The WebSocket connection. +## +## is_orig: True if the frame is from the originator, else false. +## +## data: One data chunk of frame payload. The length of is at most +## :zeek:see:`WebSocket::payload_chunk_size` bytes. A frame with +## a longer payload will result in multiple events events. +## +## .. zeek:see:: WebSocket::payload_chunk_size +event websocket_frame_data%(c: connection, is_orig: bool, data: string%); + + +## Generated for every completed WebSocket message. +## +## c: The WebSocket connection. +## +## is_orig: True if the frame is from the originator, else false. +## +## opcode: The first frame's opcode. +event websocket_message%(c: connection, is_orig: bool, opcode: count%); + +## Generated for WebSocket Close frames. +## +## c: The WebSocket connection. +## +## is_orig: True if the frame is from the originator, else false. +## +## status: If the CloseFrame had no payload, this is 0, otherwise the value +## of the first two bytes in the frame's payload. +## +## reason: Remaining payload after *status*. This is capped at +## 2 bytes less than :zeek:see:`WebSocket::payload_chunk_size`. +## +## .. zeek:see:: WebSocket::payload_chunk_size +event websocket_close%(c: connection, is_orig: bool, status: count, reason: string%); diff --git a/src/analyzer/protocol/websocket/functions.bif b/src/analyzer/protocol/websocket/functions.bif new file mode 100644 index 0000000000..bb06a3fd30 --- /dev/null +++ b/src/analyzer/protocol/websocket/functions.bif @@ -0,0 +1,46 @@ +%%{ +#include "zeek/analyzer/protocol/websocket/WebSocket.h" +%%} + +module WebSocket; + +## Configure the WebSocket analyzer. +## +## Called during :zeek:see:`websocket_handshake` to configure +## the WebSocket analyzer given the selected protocol and extension +## as chosen by the server. +## +## c: The WebSocket connection. +# +## aid: The identifier for the WebSocket analyzer as provided to :zeek:see:`websocket_handshake`. +## +## server_protocol: The protocol as found in the server's Sec-WebSocket-Protocol HTTP header, or empty. +## +## server_extensions: The extension as selected by the server via the Sec-WebSocket-Extensions HTTP Header. +## +## .. zeek:see:: websocket_handshake +function __configure_analyzer%(c: connection, aid: count, config: WebSocket::AnalyzerConfig%): bool + %{ + auto* analyzer = c->FindAnalyzer(aid); + auto* ws_analyzer = dynamic_cast(analyzer); + if ( ! ws_analyzer ) + { + reporter->Warning("WebSocket analyzer to configure not found"); + return zeek::val_mgr->False(); + } + + static const auto& config_type = zeek::id::find_type("WebSocket::AnalyzerConfig"); + + if ( config->GetType() != config_type ) + { + reporter->Warning("config has wrong type %s, expected %s", + config->GetType()->GetName().c_str(), + config_type->GetName().c_str()); + return zeek::val_mgr->False(); + } + + if ( ! ws_analyzer->Configure({zeek::NewRef{}, config->AsRecordVal()}) ) + return zeek::val_mgr->False(); + + return zeek::val_mgr->True(); + %} diff --git a/src/analyzer/protocol/websocket/types.bif b/src/analyzer/protocol/websocket/types.bif new file mode 100644 index 0000000000..90378cef1b --- /dev/null +++ b/src/analyzer/protocol/websocket/types.bif @@ -0,0 +1,4 @@ +module WebSocket; + +# A configuration record used for the WebSocket analyzer. +type AnalyzerConfig: record; diff --git a/src/analyzer/protocol/websocket/websocket-analyzer.pac b/src/analyzer/protocol/websocket/websocket-analyzer.pac new file mode 100644 index 0000000000..c57dd9a651 --- /dev/null +++ b/src/analyzer/protocol/websocket/websocket-analyzer.pac @@ -0,0 +1,126 @@ +# See the file "COPYING" in the main distribution directory for copyright. +# +# The WebSocket analyzer. +# + +refine flow WebSocket_Flow += { + + function process_message(msg: WebSocket_Message): bool + %{ + connection()->zeek_analyzer()->AnalyzerConfirmation(); + + if ( websocket_message ) + { + zeek::BifEvent::enqueue_websocket_message(connection()->zeek_analyzer(), + connection()->zeek_analyzer()->Conn(), + is_orig(), + ${msg.opcode}); + } + + return true; + %} + + function process_header(hdr: WebSocket_FrameHeader): bool + %{ + if ( websocket_frame ) + { + zeek::BifEvent::enqueue_websocket_frame(connection()->zeek_analyzer(), + connection()->zeek_analyzer()->Conn(), + is_orig(), + ${hdr.b.fin}, + ${hdr.b.reserved}, + ${hdr.b.opcode}, + ${hdr.payload_len}); + } + + return true; + %} + + function process_payload_unmask(chunk: WebSocket_FramePayloadUnmask): bool + %{ + auto& data = ${chunk.data}; + + // In-place unmasking if frame payload is masked. + if ( has_mask_ ) + { + auto *d = data.data(); + for ( int i = 0; i < data.length(); i++ ) + d[i] = d[i] ^ mask_[mask_idx_++ % mask_.size()]; + } + + return true; + %} + + function process_payload_close(close: WebSocket_FramePayloadClose): bool + %{ + if ( websocket_close ) + { + const auto& reason = ${close.reason}; + auto reason_val = zeek::make_intrusive(reason.length(), + reinterpret_cast(reason.data())); + zeek::BifEvent::enqueue_websocket_close(connection()->zeek_analyzer(), + connection()->zeek_analyzer()->Conn(), + is_orig(), + ${close.status}, + reason_val); + } + + return true; + %} + + function process_empty_close(hdr: WebSocket_FrameHeader): bool + %{ + if ( websocket_close ) + { + zeek::BifEvent::enqueue_websocket_close(connection()->zeek_analyzer(), + connection()->zeek_analyzer()->Conn(), + is_orig(), + 0, /* use placeholder status */ + zeek::val_mgr->EmptyString()); + } + + return true; + %} + + function process_payload_chunk(chunk: WebSocket_FramePayloadChunk): bool + %{ + auto& data = ${chunk.unmask.data}; + + if ( websocket_frame_data ) + { + auto data_val = zeek::make_intrusive(data.length(), reinterpret_cast(data.data())); + zeek::BifEvent::enqueue_websocket_frame_data(connection()->zeek_analyzer(), + connection()->zeek_analyzer()->Conn(), + is_orig(), + data_val); + } + + // Forward text and binary data to downstream analyzers. + if ( ${chunk.hdr.b.opcode} == OPCODE_TEXT|| ${chunk.hdr.b.opcode} == OPCODE_BINARY) + connection()->zeek_analyzer()->ForwardStream(data.length(), + data.data(), + is_orig()); + + return true; + %} +}; + +refine typeattr WebSocket_Message += &let { + proc_message = $context.flow.process_message(this); +}; + +refine typeattr WebSocket_FrameHeader += &let { + proc_header = $context.flow.process_header(this); +}; + +refine typeattr WebSocket_FramePayloadUnmask += &let { + proc_payload_unmask = $context.flow.process_payload_unmask(this); +}; + +refine typeattr WebSocket_FramePayloadClose += &let { + proc_payload_close = $context.flow.process_payload_close(this); +}; + +refine typeattr WebSocket_FramePayloadChunk += &let { + proc_payload_chunk = $context.flow.process_payload_chunk(this); +}; diff --git a/src/analyzer/protocol/websocket/websocket-protocol.pac b/src/analyzer/protocol/websocket/websocket-protocol.pac new file mode 100644 index 0000000000..01ab76ff9e --- /dev/null +++ b/src/analyzer/protocol/websocket/websocket-protocol.pac @@ -0,0 +1,149 @@ +# See the file "COPYING" in the main distribution directory for copyright. +# +# The WebSocket protocol. +# +# https://datatracker.ietf.org/doc/html/rfc6455 + +enum Opcodes { + OPCODE_CONTINUATION = 0x00, + OPCODE_TEXT = 0x01, + OPCODE_BINARY = 0x02, + OPCODE_CLOSE = 0x08, + OPCODE_PING = 0x09, + OPCODE_PONG = 0x0a, +} + +type WebSocket_FrameHeaderFixed(first_frame: bool) = record { + # First frame in message cannot be continuation, following + # frames are only expected to be continuations. + b: uint16 &enforce((first_frame && opcode != 0) || (!first_frame && opcode == 0)); +} &let { + fin: bool = (b & 0x8000) ? true : false; + reserved: uint8 = ((b & 0x7000) >> 12); + opcode: uint8 = (b & 0x0f00) >> 8; + has_mask: bool = (b & 0x0080) ? true : false; + payload_len1: uint8 = (b & 0x007f); + rest_header_len: uint64 = (has_mask ? 4 : 0) + (payload_len1 < 126 ? 0 : (payload_len1 == 126 ? 2 : 8)); +} &length=2; + +type WebSocket_FrameHeader(b: WebSocket_FrameHeaderFixed) = record { + maybe_more_len: case b.payload_len1 of { + 126 -> payload_len2: uint16; + 127 -> payload_len8: uint64; + default -> short_len: empty; + }; + + maybe_mask: case b.has_mask of { + true -> mask: bytestring &length=4; + false -> no_mask: empty; + }; +} &let { + payload_len: uint64 = b.payload_len1 < 126 ? b.payload_len1 : (b.payload_len1 == 126 ? payload_len2 : payload_len8); + new_frame_payload = $context.flow.new_frame_payload(this); +} &length=b.rest_header_len; + +type WebSocket_FramePayloadClose(hdr: WebSocket_FrameHeader) = record { + status: uint16; + reason: bytestring &restofdata; +} &byteorder=bigendian; + +type WebSocket_FramePayloadUnmask(hdr: WebSocket_FrameHeader) = record { + data: bytestring &restofdata; +}; + +type WebSocket_FramePayloadChunk(len: uint64, hdr: WebSocket_FrameHeader) = record { + unmask: WebSocket_FramePayloadUnmask(hdr); +} &let { + consumed_payload = $context.flow.consumed_chunk(len); + close_payload: WebSocket_FramePayloadClose(hdr) withinput unmask.data &length=len &if(hdr.b.opcode == OPCODE_CLOSE); +} &length=len; + +type WebSocket_Frame(first_frame: bool, msg: WebSocket_Message) = record { + b: WebSocket_FrameHeaderFixed(first_frame); + hdr: WebSocket_FrameHeader(b); + + # This is implementing frame payload chunking so that we do not + # attempt to buffer huge frames and forward data to downstream + # analyzers in chunks. + # + # I tried &chunked and it didn't do anything very useful. + chunks: WebSocket_FramePayloadChunk($context.flow.next_chunk_len(), hdr)[] + &until($context.flow.remaining_frame_payload_len() == 0) + &transient; +} &let { + # If we find a close frame without payload, raise the event here + # as the close won't have been parsed via chunks. + empty_close = $context.flow.process_empty_close(hdr) &if(b.opcode == OPCODE_CLOSE) && hdr.payload_len == 0; +}; + +type WebSocket_Message = record { + first_frame: WebSocket_Frame(true, this); + optional_more_frames: case first_frame.hdr.b.fin of { + true -> no_more_frames: empty; + false -> more_frames: WebSocket_Frame(false, this)[] &until($element.hdr.b.fin) &transient; + }; +} &let { + opcode = first_frame.hdr.b.opcode; +} &byteorder=bigendian; + +flow WebSocket_Flow(is_orig: bool) { + flowunit = WebSocket_Message withcontext(connection, this); + + %member{ + bool has_mask_; + uint64_t mask_idx_; + uint64_t frame_payload_len_; + std::array mask_; + %} + + %init{ + has_mask_ = false; + mask_idx_ = 0; + frame_payload_len_ = 0; + %} + + function new_frame_payload(hdr: WebSocket_FrameHeader): uint64 + %{ + if ( frame_payload_len_ > 0 ) + connection()->zeek_analyzer()->Weird("websocket_frame_not_consumed"); + + frame_payload_len_ = ${hdr.payload_len}; + has_mask_ = ${hdr.b.has_mask}; + mask_idx_ = 0; + if ( has_mask_ ) { + assert(${hdr.mask}.length() == static_cast(mask_.size())); + memcpy(mask_.data(), ${hdr.mask}.data(), mask_.size()); + } + return frame_payload_len_; + %} + + function remaining_frame_payload_len(): uint64 + %{ + return frame_payload_len_; + %} + + function consumed_chunk(len: uint64): uint64 + %{ + if ( len > frame_payload_len_ ) { + connection()->zeek_analyzer()->Weird("websocket_frame_consuming_too_much"); + len = frame_payload_len_; + } + + frame_payload_len_ -= len; + return len; + %} + + function next_chunk_len(): uint64 + %{ + uint64_t len = frame_payload_len_; + + // It would be somewhat nicer if we could just consume + // anything still left to consume from the current packet, + // but couldn't figure out if that information can be pulled + // flow buffer. + if ( len > zeek::BifConst::WebSocket::payload_chunk_size ) + len = zeek::BifConst::WebSocket::payload_chunk_size; + + return len; + %} +}; diff --git a/src/analyzer/protocol/websocket/websocket.pac b/src/analyzer/protocol/websocket/websocket.pac new file mode 100644 index 0000000000..4e710e2712 --- /dev/null +++ b/src/analyzer/protocol/websocket/websocket.pac @@ -0,0 +1,24 @@ +# See the file "COPYING" in the main distribution directory for copyright. + +%include binpac.pac +%include zeek.pac + +%extern{ +#include + +#include "zeek/analyzer/protocol/websocket/consts.bif.h" +#include "zeek/analyzer/protocol/websocket/events.bif.h" +%} + +analyzer WebSocket withcontext { + connection: WebSocket_Conn; + flow: WebSocket_Flow; +}; + +connection WebSocket_Conn(zeek_analyzer: ZeekAnalyzer) { + upflow = WebSocket_Flow(true); + downflow = WebSocket_Flow(false); +}; + +%include websocket-protocol.pac +%include websocket-analyzer.pac diff --git a/testing/btest/Baseline/coverage.bare-load-baseline/canonified_loaded_scripts.log b/testing/btest/Baseline/coverage.bare-load-baseline/canonified_loaded_scripts.log index 13a58ed17e..f7ca9ce013 100644 --- a/testing/btest/Baseline/coverage.bare-load-baseline/canonified_loaded_scripts.log +++ b/testing/btest/Baseline/coverage.bare-load-baseline/canonified_loaded_scripts.log @@ -244,6 +244,10 @@ scripts/base/init-frameworks-and-bifs.zeek build/scripts/base/bif/plugins/Zeek_TCP.events.bif.zeek build/scripts/base/bif/plugins/Zeek_TCP.types.bif.zeek build/scripts/base/bif/plugins/Zeek_TCP.functions.bif.zeek + build/scripts/base/bif/plugins/Zeek_WebSocket.consts.bif.zeek + build/scripts/base/bif/plugins/Zeek_WebSocket.events.bif.zeek + build/scripts/base/bif/plugins/Zeek_WebSocket.functions.bif.zeek + build/scripts/base/bif/plugins/Zeek_WebSocket.types.bif.zeek build/scripts/base/bif/plugins/Zeek_XMPP.events.bif.zeek build/scripts/base/bif/plugins/Zeek_ARP.events.bif.zeek build/scripts/base/bif/plugins/Zeek_UDP.events.bif.zeek diff --git a/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log b/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log index 43d87b3515..77e4e88706 100644 --- a/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log +++ b/testing/btest/Baseline/coverage.default-load-baseline/canonified_loaded_scripts.log @@ -244,6 +244,10 @@ scripts/base/init-frameworks-and-bifs.zeek build/scripts/base/bif/plugins/Zeek_TCP.events.bif.zeek build/scripts/base/bif/plugins/Zeek_TCP.types.bif.zeek build/scripts/base/bif/plugins/Zeek_TCP.functions.bif.zeek + build/scripts/base/bif/plugins/Zeek_WebSocket.consts.bif.zeek + build/scripts/base/bif/plugins/Zeek_WebSocket.events.bif.zeek + build/scripts/base/bif/plugins/Zeek_WebSocket.functions.bif.zeek + build/scripts/base/bif/plugins/Zeek_WebSocket.types.bif.zeek build/scripts/base/bif/plugins/Zeek_XMPP.events.bif.zeek build/scripts/base/bif/plugins/Zeek_ARP.events.bif.zeek build/scripts/base/bif/plugins/Zeek_UDP.events.bif.zeek @@ -465,6 +469,9 @@ scripts/base/init-default.zeek scripts/base/protocols/syslog/spicy-events.zeek scripts/base/protocols/syslog/consts.zeek scripts/base/protocols/syslog/main.zeek + scripts/base/protocols/websocket/__load__.zeek + scripts/base/protocols/websocket/consts.zeek + scripts/base/protocols/websocket/main.zeek scripts/base/protocols/tunnels/__load__.zeek scripts/base/protocols/xmpp/__load__.zeek scripts/base/protocols/xmpp/main.zeek diff --git a/testing/btest/Baseline/coverage.find-bro-logs/out b/testing/btest/Baseline/coverage.find-bro-logs/out index efe4d98b6d..ad46a67218 100644 --- a/testing/btest/Baseline/coverage.find-bro-logs/out +++ b/testing/btest/Baseline/coverage.find-bro-logs/out @@ -65,6 +65,7 @@ telemetry_histogram traceroute tunnel unknown_protocols +websocket weird weird_stats x509 diff --git a/testing/btest/Baseline/coverage.record-fields/out.default b/testing/btest/Baseline/coverage.record-fields/out.default index cf0204c6dd..527f4fead2 100644 --- a/testing/btest/Baseline/coverage.record-fields/out.default +++ b/testing/btest/Baseline/coverage.record-fields/out.default @@ -884,4 +884,18 @@ connection { } * uid: string, log=F, optional=F * vlan: int, log=F, optional=T + * websocket: record WebSocket::Info, log=F, optional=T + WebSocket::Info { + * client_extensions: vector of string, log=T, optional=T + * client_protocols: vector of string, log=T, optional=T + * host: string, log=T, optional=T + * id: record conn_id, log=T, optional=F + conn_id { ... } + * server_extensions: vector of string, log=T, optional=T + * subprotocol: string, log=T, optional=T + * ts: time, log=T, optional=F + * uid: string, log=T, optional=F + * uri: string, log=T, optional=T + * user_agent: string, log=T, optional=T + } } diff --git a/testing/btest/Baseline/plugins.hooks/output b/testing/btest/Baseline/plugins.hooks/output index 386e891e7a..db0b88a431 100644 --- a/testing/btest/Baseline/plugins.hooks/output +++ b/testing/btest/Baseline/plugins.hooks/output @@ -463,6 +463,10 @@ 0.000000 MetaHookPost LoadFile(0, ./Zeek_Teredo.functions.bif.zeek, <...>/Zeek_Teredo.functions.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, ./Zeek_UDP.events.bif.zeek, <...>/Zeek_UDP.events.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, ./Zeek_VXLAN.events.bif.zeek, <...>/Zeek_VXLAN.events.bif.zeek) -> -1 +0.000000 MetaHookPost LoadFile(0, ./Zeek_WebSocket.consts.bif.zeek, <...>/Zeek_WebSocket.consts.bif.zeek) -> -1 +0.000000 MetaHookPost LoadFile(0, ./Zeek_WebSocket.events.bif.zeek, <...>/Zeek_WebSocket.events.bif.zeek) -> -1 +0.000000 MetaHookPost LoadFile(0, ./Zeek_WebSocket.functions.bif.zeek, <...>/Zeek_WebSocket.functions.bif.zeek) -> -1 +0.000000 MetaHookPost LoadFile(0, ./Zeek_WebSocket.types.bif.zeek, <...>/Zeek_WebSocket.types.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, ./Zeek_X509.events.bif.zeek, <...>/Zeek_X509.events.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, ./Zeek_X509.functions.bif.zeek, <...>/Zeek_X509.functions.bif.zeek) -> -1 0.000000 MetaHookPost LoadFile(0, ./Zeek_X509.ocsp_events.bif.zeek, <...>/Zeek_X509.ocsp_events.bif.zeek) -> -1 @@ -752,6 +756,10 @@ 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_Teredo.functions.bif.zeek, <...>/Zeek_Teredo.functions.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_UDP.events.bif.zeek, <...>/Zeek_UDP.events.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_VXLAN.events.bif.zeek, <...>/Zeek_VXLAN.events.bif.zeek) -> (-1, ) +0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_WebSocket.consts.bif.zeek, <...>/Zeek_WebSocket.consts.bif.zeek) -> (-1, ) +0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_WebSocket.events.bif.zeek, <...>/Zeek_WebSocket.events.bif.zeek) -> (-1, ) +0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_WebSocket.functions.bif.zeek, <...>/Zeek_WebSocket.functions.bif.zeek) -> (-1, ) +0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_WebSocket.types.bif.zeek, <...>/Zeek_WebSocket.types.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_X509.events.bif.zeek, <...>/Zeek_X509.events.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_X509.functions.bif.zeek, <...>/Zeek_X509.functions.bif.zeek) -> (-1, ) 0.000000 MetaHookPost LoadFileExtended(0, ./Zeek_X509.ocsp_events.bif.zeek, <...>/Zeek_X509.ocsp_events.bif.zeek) -> (-1, ) @@ -1389,6 +1397,10 @@ 0.000000 MetaHookPre LoadFile(0, ./Zeek_Teredo.functions.bif.zeek, <...>/Zeek_Teredo.functions.bif.zeek) 0.000000 MetaHookPre LoadFile(0, ./Zeek_UDP.events.bif.zeek, <...>/Zeek_UDP.events.bif.zeek) 0.000000 MetaHookPre LoadFile(0, ./Zeek_VXLAN.events.bif.zeek, <...>/Zeek_VXLAN.events.bif.zeek) +0.000000 MetaHookPre LoadFile(0, ./Zeek_WebSocket.consts.bif.zeek, <...>/Zeek_WebSocket.consts.bif.zeek) +0.000000 MetaHookPre LoadFile(0, ./Zeek_WebSocket.events.bif.zeek, <...>/Zeek_WebSocket.events.bif.zeek) +0.000000 MetaHookPre LoadFile(0, ./Zeek_WebSocket.functions.bif.zeek, <...>/Zeek_WebSocket.functions.bif.zeek) +0.000000 MetaHookPre LoadFile(0, ./Zeek_WebSocket.types.bif.zeek, <...>/Zeek_WebSocket.types.bif.zeek) 0.000000 MetaHookPre LoadFile(0, ./Zeek_X509.events.bif.zeek, <...>/Zeek_X509.events.bif.zeek) 0.000000 MetaHookPre LoadFile(0, ./Zeek_X509.functions.bif.zeek, <...>/Zeek_X509.functions.bif.zeek) 0.000000 MetaHookPre LoadFile(0, ./Zeek_X509.ocsp_events.bif.zeek, <...>/Zeek_X509.ocsp_events.bif.zeek) @@ -1678,6 +1690,10 @@ 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_Teredo.functions.bif.zeek, <...>/Zeek_Teredo.functions.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_UDP.events.bif.zeek, <...>/Zeek_UDP.events.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_VXLAN.events.bif.zeek, <...>/Zeek_VXLAN.events.bif.zeek) +0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_WebSocket.consts.bif.zeek, <...>/Zeek_WebSocket.consts.bif.zeek) +0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_WebSocket.events.bif.zeek, <...>/Zeek_WebSocket.events.bif.zeek) +0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_WebSocket.functions.bif.zeek, <...>/Zeek_WebSocket.functions.bif.zeek) +0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_WebSocket.types.bif.zeek, <...>/Zeek_WebSocket.types.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_X509.events.bif.zeek, <...>/Zeek_X509.events.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_X509.functions.bif.zeek, <...>/Zeek_X509.functions.bif.zeek) 0.000000 MetaHookPre LoadFileExtended(0, ./Zeek_X509.ocsp_events.bif.zeek, <...>/Zeek_X509.ocsp_events.bif.zeek) @@ -2314,6 +2330,10 @@ 0.000000 | HookLoadFile ./Zeek_Teredo.functions.bif.zeek <...>/Zeek_Teredo.functions.bif.zeek 0.000000 | HookLoadFile ./Zeek_UDP.events.bif.zeek <...>/Zeek_UDP.events.bif.zeek 0.000000 | HookLoadFile ./Zeek_VXLAN.events.bif.zeek <...>/Zeek_VXLAN.events.bif.zeek +0.000000 | HookLoadFile ./Zeek_WebSocket.consts.bif.zeek <...>/Zeek_WebSocket.consts.bif.zeek +0.000000 | HookLoadFile ./Zeek_WebSocket.events.bif.zeek <...>/Zeek_WebSocket.events.bif.zeek +0.000000 | HookLoadFile ./Zeek_WebSocket.functions.bif.zeek <...>/Zeek_WebSocket.functions.bif.zeek +0.000000 | HookLoadFile ./Zeek_WebSocket.types.bif.zeek <...>/Zeek_WebSocket.types.bif.zeek 0.000000 | HookLoadFile ./Zeek_X509.events.bif.zeek <...>/Zeek_X509.events.bif.zeek 0.000000 | HookLoadFile ./Zeek_X509.functions.bif.zeek <...>/Zeek_X509.functions.bif.zeek 0.000000 | HookLoadFile ./Zeek_X509.ocsp_events.bif.zeek <...>/Zeek_X509.ocsp_events.bif.zeek @@ -2603,6 +2623,10 @@ 0.000000 | HookLoadFileExtended ./Zeek_Teredo.functions.bif.zeek <...>/Zeek_Teredo.functions.bif.zeek 0.000000 | HookLoadFileExtended ./Zeek_UDP.events.bif.zeek <...>/Zeek_UDP.events.bif.zeek 0.000000 | HookLoadFileExtended ./Zeek_VXLAN.events.bif.zeek <...>/Zeek_VXLAN.events.bif.zeek +0.000000 | HookLoadFileExtended ./Zeek_WebSocket.consts.bif.zeek <...>/Zeek_WebSocket.consts.bif.zeek +0.000000 | HookLoadFileExtended ./Zeek_WebSocket.events.bif.zeek <...>/Zeek_WebSocket.events.bif.zeek +0.000000 | HookLoadFileExtended ./Zeek_WebSocket.functions.bif.zeek <...>/Zeek_WebSocket.functions.bif.zeek +0.000000 | HookLoadFileExtended ./Zeek_WebSocket.types.bif.zeek <...>/Zeek_WebSocket.types.bif.zeek 0.000000 | HookLoadFileExtended ./Zeek_X509.events.bif.zeek <...>/Zeek_X509.events.bif.zeek 0.000000 | HookLoadFileExtended ./Zeek_X509.functions.bif.zeek <...>/Zeek_X509.functions.bif.zeek 0.000000 | HookLoadFileExtended ./Zeek_X509.ocsp_events.bif.zeek <...>/Zeek_X509.ocsp_events.bif.zeek diff --git a/testing/btest/Baseline/scripts.base.files.x509.files/files.log b/testing/btest/Baseline/scripts.base.files.x509.files/files.log index e64dfc52c0..ce19924fa1 100644 --- a/testing/btest/Baseline/scripts.base.files.x509.files/files.log +++ b/testing/btest/Baseline/scripts.base.files.x509.files/files.log @@ -7,10 +7,10 @@ #open XXXX-XX-XX-XX-XX-XX #fields ts fuid uid id.orig_h id.orig_p id.resp_h id.resp_p source depth analyzers mime_type filename duration local_orig is_orig seen_bytes total_bytes missing_bytes overflow_bytes timedout parent_fuid md5 sha1 sha256 #types time string string addr port addr port string count set[string] string string interval bool bool count count count count bool string string string string -XXXXXXXXXX.XXXXXX FgN3AE3of2TRIqaeQe CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-user-cert - 0.000000 F F 1859 - 0 0 F - 7af07aca6d5c6e8e87fe4bb34786edc0 548b9e03bc183d1cd39f93a37985cb3950f8f06f 6bacfa4536150ed996f2b0c05ab6e345a257225f449aeb9d2018ccd88f4ede43 -XXXXXXXXXX.XXXXXX Fv2Agc4z5boBOacQi6 CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 1032 - 0 0 F - 9e4ac96474245129d9766700412a1f89 d83c1a7f4d0446bb2081b81a1670f8183451ca24 a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d -XXXXXXXXXX.XXXXXX Ftmyeg2qgI2V38Dt3g CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 897 - 0 0 F - 2e7db2a31d0e3da4b25f49b9542a2e1a 7359755c6df9a0abc3060bce369564c8ec4542a3 3c35cc963eb004451323d3275d05b353235053490d9cd83729a2faf5e7ca1cc0 -XXXXXXXXXX.XXXXXX FUFNf84cduA0IJCp07 ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-user-cert - 0.000000 F F 1859 - 0 0 F - 7af07aca6d5c6e8e87fe4bb34786edc0 548b9e03bc183d1cd39f93a37985cb3950f8f06f 6bacfa4536150ed996f2b0c05ab6e345a257225f449aeb9d2018ccd88f4ede43 -XXXXXXXXXX.XXXXXX F1H4bd2OKGbLPEdHm4 ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 1032 - 0 0 F - 9e4ac96474245129d9766700412a1f89 d83c1a7f4d0446bb2081b81a1670f8183451ca24 a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d -XXXXXXXXXX.XXXXXX Fgsbci2jxFXYMOHOhi ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 SHA256,X509,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 897 - 0 0 F - 2e7db2a31d0e3da4b25f49b9542a2e1a 7359755c6df9a0abc3060bce369564c8ec4542a3 3c35cc963eb004451323d3275d05b353235053490d9cd83729a2faf5e7ca1cc0 +XXXXXXXXXX.XXXXXX FgN3AE3of2TRIqaeQe CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-user-cert - 0.000000 F F 1859 - 0 0 F - 7af07aca6d5c6e8e87fe4bb34786edc0 548b9e03bc183d1cd39f93a37985cb3950f8f06f 6bacfa4536150ed996f2b0c05ab6e345a257225f449aeb9d2018ccd88f4ede43 +XXXXXXXXXX.XXXXXX Fv2Agc4z5boBOacQi6 CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 1032 - 0 0 F - 9e4ac96474245129d9766700412a1f89 d83c1a7f4d0446bb2081b81a1670f8183451ca24 a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d +XXXXXXXXXX.XXXXXX Ftmyeg2qgI2V38Dt3g CHhAvVGS1DHFjwGM9 192.168.4.149 60623 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 897 - 0 0 F - 2e7db2a31d0e3da4b25f49b9542a2e1a 7359755c6df9a0abc3060bce369564c8ec4542a3 3c35cc963eb004451323d3275d05b353235053490d9cd83729a2faf5e7ca1cc0 +XXXXXXXXXX.XXXXXX FUFNf84cduA0IJCp07 ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-user-cert - 0.000000 F F 1859 - 0 0 F - 7af07aca6d5c6e8e87fe4bb34786edc0 548b9e03bc183d1cd39f93a37985cb3950f8f06f 6bacfa4536150ed996f2b0c05ab6e345a257225f449aeb9d2018ccd88f4ede43 +XXXXXXXXXX.XXXXXX F1H4bd2OKGbLPEdHm4 ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 1032 - 0 0 F - 9e4ac96474245129d9766700412a1f89 d83c1a7f4d0446bb2081b81a1670f8183451ca24 a047a37fa2d2e118a4f5095fe074d6cfe0e352425a7632bf8659c03919a6c81d +XXXXXXXXXX.XXXXXX Fgsbci2jxFXYMOHOhi ClEkJM2Vm5giqnMf4h 192.168.4.149 60624 74.125.239.129 443 SSL 0 X509,SHA256,SHA1,MD5 application/x-x509-ca-cert - 0.000000 F F 897 - 0 0 F - 2e7db2a31d0e3da4b25f49b9542a2e1a 7359755c6df9a0abc3060bce369564c8ec4542a3 3c35cc963eb004451323d3275d05b353235053490d9cd83729a2faf5e7ca1cc0 #close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.broker-websocket/conn.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.broker-websocket/conn.log.cut new file mode 100644 index 0000000000..f5bd2aa2ab --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.broker-websocket/conn.log.cut @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid history service +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 ShADadfF websocket,http diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.broker-websocket/websocket.log b/testing/btest/Baseline/scripts.base.protocols.websocket.broker-websocket/websocket.log new file mode 100644 index 0000000000..ba6652b2bb --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.broker-websocket/websocket.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path websocket +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p host uri user_agent subprotocol client_protocols server_extensions client_extensions +#types time string addr port addr port string string string string vector[string] vector[string] vector[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 38776 127.0.0.1 27599 localhost:27599 /v1/messages/json Python/3.10 websockets/12.0 - - - permessage-deflate; client_max_window_bits +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.events/out b/testing/btest/Baseline/scripts.base.protocols.websocket.events/out new file mode 100644 index 0000000000..6c6a139ac6 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.events/out @@ -0,0 +1,91 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +jupyter-websocket.pcap +websocket_handshake, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=40492/tcp, resp_h=127.0.0.1, resp_p=51185/tcp], host=192.168.122.182, uri=/user/christian/api/kernels/f8645ecd-0a76-4bb1-9e6e-cb464276bc69/channels?session_id=deeecee7-efc2-42a1-a7c1-e1c0569436e3, user_agent=Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0, subprotocol=v1.kernel.websocket.jupyter.org, client_protocols=[v1.kernel.websocket.jupyter.org], server_extensions=, client_extensions=[permessage-deflate]] +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, binary, payload_len, 262 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 262, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x00\x00\x00\x00\x06\x01\x00\x00\x00\x00\x00\x00shell{"date":"2023-09-29T23:25:05.568Z","msg_id":"5af8fd02-14a1- +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 539 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 539, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x14\x01\x00\x00\x00\x00\x00\x00\xfe\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x1b\x02\x00\x00\x00\x00\x00\x00iopub{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_7", "ms +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 527 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 527, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x14\x01\x00\x00\x00\x00\x00\x00\xf2\x01\x00\x00\x00\x00\x00\x00\xf4\x01\x00\x00\x00\x00\x00\x00\x0f\x02\x00\x00\x00\x00\x00\x00iopub{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_9", "ms +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 1647 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 1647, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00 \x01\x00\x00\x00\x00\x00\x00\xf1\x01\x00\x00\x00\x00\x00\x00\xf3\x01\x00\x00\x00\x00\x00\x00o\x06\x00\x00\x00\x00\x00\x00shell{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_14", "m +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 540 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 540, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x15\x01\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x1c\x02\x00\x00\x00\x00\x00\x00iopub{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_10", "m +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 540 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 540, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x15\x01\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x1c\x02\x00\x00\x00\x00\x00\x00iopub{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_12", "m +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 515 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 515, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x15\x01\x00\x00\x00\x00\x00\x00\xe6\x01\x00\x00\x00\x00\x00\x00\xe8\x01\x00\x00\x00\x00\x00\x00\x03\x02\x00\x00\x00\x00\x00\x00iopub{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_13", "m +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 515 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 515, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x15\x01\x00\x00\x00\x00\x00\x00\xe6\x01\x00\x00\x00\x00\x00\x00\xe8\x01\x00\x00\x00\x00\x00\x00\x03\x02\x00\x00\x00\x00\x00\x00iopub{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_15", "m +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 540 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 540, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x15\x01\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x1c\x02\x00\x00\x00\x00\x00\x00iopub{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_16", "m +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 540 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 540, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x15\x01\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x1c\x02\x00\x00\x00\x00\x00\x00iopub{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_18", "m +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 540 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 540, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x15\x01\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x1c\x02\x00\x00\x00\x00\x00\x00iopub{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_20", "m +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 540 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 540, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x15\x01\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\x00\x00\x00\x00\x01\x02\x00\x00\x00\x00\x00\x00\x1c\x02\x00\x00\x00\x00\x00\x00iopub{"msg_id": "5accb611-84744090d18c920147a97eb5_25702_21", "m +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 0 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 0, reason, +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, close +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 0 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 0, reason, +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, close +wstunnel-http.pcap +websocket_handshake, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=51102/tcp, resp_h=127.0.0.1, resp_p=8888/tcp], host=localhost:8888, uri=/v1/events, user_agent=, subprotocol=v1, client_protocols=[v1, authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGNmZWFiLWY5OWQtNzBmNy05NmFmLTBlOGJhNjk2YTFmNiIsInAiOiJUY3AiLCJyIjoiemVlay5vcmciLCJycCI6ODB9.FsquetBp_jsIDzBslWyyTPlS2hcMprVuWmbT2r57N0A], server_extensions=, client_extensions=] +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, binary, payload_len, 72 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 72, data, GET / HTTP/1.1\x0d\x0aHost: zeek.org\x0d\x0aUser-Agent: curl/7.81.0\x0d\x0aAccept: */*\x0d\x0a\x0d\x0a +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_len, 409 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 409, data, HTTP/1.1 301 Moved Permanently\x0d\x0aServer: nginx\x0d\x0aDate: Fri, 12 Jan 2024 17:15:32 GMT\x0d\x0aContent-Type: text/html\x0d\x0aContent-Len +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 2 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, close +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 2 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, close +broker-websocket.pcap +websocket_handshake, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=38776/tcp, resp_h=127.0.0.1, resp_p=27599/tcp], host=localhost:27599, uri=/v1/messages/json, user_agent=Python/3.10 websockets/12.0, subprotocol=, client_protocols=, server_extensions=, client_extensions=[permessage-deflate; client_max_window_bits]] +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, text, payload_len, 24 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 24, data, ["/zeek/event/my_topic"] +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, text +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, text, payload_len, 91 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 91, data, {"type": "ack", "endpoint": "cfc03c41-7983-5fe2-b22e-6894100e6305", "version": "2.8.0-dev"} +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, text +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, text, payload_len, 533 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 533, data, {"type": "data-message", "topic": "/zeek/event/my_topic", "@data-type": "vector", "data": [{"@data-type": "count", "data +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, text +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, text, payload_len, 361 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 361, data, {"type": "data-message", "topic": "/zeek/event/my_topic", "@data-type": "vector", "data": [{"@data-type": "count", "data +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, text +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, text, payload_len, 533 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 533, data, {"type": "data-message", "topic": "/zeek/event/my_topic", "@data-type": "vector", "data": [{"@data-type": "count", "data +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, text +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, text, payload_len, 361 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 361, data, {"type": "data-message", "topic": "/zeek/event/my_topic", "@data-type": "vector", "data": [{"@data-type": "count", "data +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, text +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, text, payload_len, 533 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 533, data, {"type": "data-message", "topic": "/zeek/event/my_topic", "@data-type": "vector", "data": [{"@data-type": "count", "data +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, text +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, text, payload_len, 361 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 361, data, {"type": "data-message", "topic": "/zeek/event/my_topic", "@data-type": "vector", "data": [{"@data-type": "count", "data +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, text +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 2 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, close diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.jupyter-websocket/conn.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.jupyter-websocket/conn.log.cut new file mode 100644 index 0000000000..f5bd2aa2ab --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.jupyter-websocket/conn.log.cut @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid history service +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 ShADadfF websocket,http diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.jupyter-websocket/websocket.log b/testing/btest/Baseline/scripts.base.protocols.websocket.jupyter-websocket/websocket.log new file mode 100644 index 0000000000..c9c00fc79f --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.jupyter-websocket/websocket.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path websocket +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p host uri user_agent subprotocol client_protocols server_extensions client_extensions +#types time string addr port addr port string string string string vector[string] vector[string] vector[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 40492 127.0.0.1 51185 192.168.122.182 /user/christian/api/kernels/f8645ecd-0a76-4bb1-9e6e-cb464276bc69/channels?session_id=deeecee7-efc2-42a1-a7c1-e1c0569436e3 Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0 v1.kernel.websocket.jupyter.org v1.kernel.websocket.jupyter.org - permessage-deflate +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/conn.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/conn.log.cut new file mode 100644 index 0000000000..bb892cdeb5 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/conn.log.cut @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid history service +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 ShADadFR websocket,http diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/http.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/http.log.cut new file mode 100644 index 0000000000..0aa3a4d7a5 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/http.log.cut @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid host uri status_code user_agent +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 localhost:8888 /v1/events 301 - +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 zeek.org / - curl/7.81.0 diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/websocket.log b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/websocket.log new file mode 100644 index 0000000000..e577952088 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-http/websocket.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path websocket +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p host uri user_agent subprotocol client_protocols server_extensions client_extensions +#types time string addr port addr port string string string string vector[string] vector[string] vector[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 51102 127.0.0.1 8888 localhost:8888 /v1/events - v1 v1,authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGNmZWFiLWY5OWQtNzBmNy05NmFmLTBlOGJhNjk2YTFmNiIsInAiOiJUY3AiLCJyIjoiemVlay5vcmciLCJycCI6ODB9.FsquetBp_jsIDzBslWyyTPlS2hcMprVuWmbT2r57N0A - - +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/conn.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/conn.log.cut new file mode 100644 index 0000000000..b61cd3bda1 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/conn.log.cut @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid history service +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 ShADadFR websocket,ssl,http diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/ssl.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/ssl.log.cut new file mode 100644 index 0000000000..e98e26c97c --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/ssl.log.cut @@ -0,0 +1,3 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid version server_name ssl_history +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 TLSv13 zeek.org CsiI diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/websocket.log b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/websocket.log new file mode 100644 index 0000000000..a814ca5986 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-https/websocket.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path websocket +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p host uri user_agent subprotocol client_protocols server_extensions client_extensions +#types time string addr port addr port string string string string vector[string] vector[string] vector[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 39992 127.0.0.1 8888 localhost:8888 /v1/events - v1 v1,authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGQwNTY1LTlmZTItNzFkZS1iNjRlLTU5MzhmZTI0ZmIyZCIsInAiOiJUY3AiLCJyIjoiMTkyLjAuNzguMTUwIiwicnAiOjQ0M30.xyDNRR4kK4fQSGfEyGzuUINn0xBVltxrFVBieMlqwEI - - +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-wrong/conn.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-wrong/conn.log.cut new file mode 100644 index 0000000000..e8d2c4ae9a --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-wrong/conn.log.cut @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid history service +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 ShADadR websocket,http +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h ShADadR websocket,http diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-wrong/websocket.log b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-wrong/websocket.log new file mode 100644 index 0000000000..e993d7b4cf --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-wrong/websocket.log @@ -0,0 +1,12 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path websocket +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p host uri user_agent subprotocol client_protocols server_extensions client_extensions +#types time string addr port addr port string string string string vector[string] vector[string] vector[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 42906 127.0.0.1 8888 localhost:8888 /v1/events - v1 v1,authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGQwNTVkLTQ5OTgtNzI5Zi04Yjg2LTMwZTBiZWEyZGE4ZiIsInAiOiJUY3AiLCJyIjoiMTk1LjIwMS4xNDguMjA5IiwicnAiOjIyfQ.jjTNJL12tQbAuhTB9p_geFXRkEHkxcvOS6zf76qDklQ - - +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 46796 127.0.0.1 8888 localhost:8888 /v1/events - v1 v1,authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGQwNTVkLTc4MWYtNzNiYi1hZDkwLTEzNjA5NzRjY2JmMyIsInAiOiJUY3AiLCJyIjoiMTk1LjIwMS4xNDguMjA5IiwicnAiOjIyfQ.2HQ4uC23p_OYIXnQWeSZCqdA3jc_lVVH7-T5xZDPrz4 - - +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/conn.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/conn.log.cut new file mode 100644 index 0000000000..72dcb1c5fa --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/conn.log.cut @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid history service +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 ShADadR websocket,ssh,http +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h ShADadR websocket,ssh,http diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/ssh.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/ssh.log.cut new file mode 100644 index 0000000000..0780213a89 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/ssh.log.cut @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid client server auth_success auth_attempts kex_alg host_key_alg +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5 F 4 curve25519-sha256 ssh-ed25519 +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5 T 5 curve25519-sha256 ssh-ed25519 diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/websocket.log b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/websocket.log new file mode 100644 index 0000000000..e993d7b4cf --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure/websocket.log @@ -0,0 +1,12 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path websocket +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p host uri user_agent subprotocol client_protocols server_extensions client_extensions +#types time string addr port addr port string string string string vector[string] vector[string] vector[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 42906 127.0.0.1 8888 localhost:8888 /v1/events - v1 v1,authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGQwNTVkLTQ5OTgtNzI5Zi04Yjg2LTMwZTBiZWEyZGE4ZiIsInAiOiJUY3AiLCJyIjoiMTk1LjIwMS4xNDguMjA5IiwicnAiOjIyfQ.jjTNJL12tQbAuhTB9p_geFXRkEHkxcvOS6zf76qDklQ - - +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 46796 127.0.0.1 8888 localhost:8888 /v1/events - v1 v1,authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGQwNTVkLTc4MWYtNzNiYi1hZDkwLTEzNjA5NzRjY2JmMyIsInAiOiJUY3AiLCJyIjoiMTk1LjIwMS4xNDguMjA5IiwicnAiOjIyfQ.2HQ4uC23p_OYIXnQWeSZCqdA3jc_lVVH7-T5xZDPrz4 - - +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/conn.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/conn.log.cut new file mode 100644 index 0000000000..72dcb1c5fa --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/conn.log.cut @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid history service +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 ShADadR websocket,ssh,http +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h ShADadR websocket,ssh,http diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/ssh.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/ssh.log.cut new file mode 100644 index 0000000000..0780213a89 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/ssh.log.cut @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid client server auth_success auth_attempts kex_alg host_key_alg +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5 F 4 curve25519-sha256 ssh-ed25519 +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.5 T 5 curve25519-sha256 ssh-ed25519 diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/websocket.log b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/websocket.log new file mode 100644 index 0000000000..e993d7b4cf --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh/websocket.log @@ -0,0 +1,12 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path websocket +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p host uri user_agent subprotocol client_protocols server_extensions client_extensions +#types time string addr port addr port string string string string vector[string] vector[string] vector[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 42906 127.0.0.1 8888 localhost:8888 /v1/events - v1 v1,authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGQwNTVkLTQ5OTgtNzI5Zi04Yjg2LTMwZTBiZWEyZGE4ZiIsInAiOiJUY3AiLCJyIjoiMTk1LjIwMS4xNDguMjA5IiwicnAiOjIyfQ.jjTNJL12tQbAuhTB9p_geFXRkEHkxcvOS6zf76qDklQ - - +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 46796 127.0.0.1 8888 localhost:8888 /v1/events - v1 v1,authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGQwNTVkLTc4MWYtNzNiYi1hZDkwLTEzNjA5NzRjY2JmMyIsInAiOiJUY3AiLCJyIjoiMTk1LjIwMS4xNDguMjA5IiwicnAiOjIyfQ.2HQ4uC23p_OYIXnQWeSZCqdA3jc_lVVH7-T5xZDPrz4 - - +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Traces/websocket/broker-websocket.pcap b/testing/btest/Traces/websocket/broker-websocket.pcap new file mode 100644 index 0000000000000000000000000000000000000000..4d7ea41199ae5519b846fca9490dec17f13cbc8e GIT binary patch literal 5103 zcmeI03v5%@8ON`09u0PE2g<{B+Qc!zWr`j9+Rn>~NlZu^C?;764>xsw9*My_L4}-(rwU5(F%;vLaR`90%fqZ+oVpKl!}ls4ONvk7|=rZoonC3 zc8sH1)uc&1=t#N8=ls6!{J-!1gY%~gzZt`^e2g1keu-f`csTX1W7NTiacm3xjArsE z!rCybDc0D6wG>W9_QL@*kAz%ToF%u{p`D}n-Ot{y!7x0(;AB2t#8)1>eG0Af^70-+ z$TUrIHbyBaZ`lBSb^VnVubuw+T)=}#zA`wvc%^?{&tE~?V@_NvwKueQ9}kjoL#U13tp8^#nRPNd31Rfx3=CUC`I+ z-$FvU;v)6Il^!foYy;cjG44w74!_fUV`f znI-mSBuaQUZ#Se8qd$rmes^`9r{|0v9h{=4BO7X6pecO8H;X;&tznGecfVZjj0Cq)V=p$th2-mv(p< zv_$ri8Xc)8RCNvYI#o>{rB(H4wUny1zCllDG*qolQ-jU~;@yvqXshe1HPGAv`7lcN zwX<0TZ(-$$a_J@nw}RjiB>3eI=hK~m9%2Qb1Y$Q1mI5<*AOUc5^mUe4$Pp6)L^uIn z`XGfk6OG`j79{AkGj>(t1x5E_Xs?4rw^D~G%B7|P@G`0ghHUY5*wDhB;1mpiqtHHd z5G}(wP&ai4Ne_au3mb@Gr7V{{OGolEK6t&7Ldm`8IYYc$SOY`zGG!nxzNIz8uBGEH zy<>5t#dC}@Ft6))oAr94WT0RT||vRUuz)1TXl8vVQbdkcOSqM1BU^YX@H8xir0C?gTvV)~tV;wSJr%tY_=9Q${d z)A?c7YO&F{!xr8r5;SOWX_Z?fF&bNKVHYMQUNEe2?6=xJqNxiq}4!&)6K7Ss)xVO>zBjKH}~E3FciSz5S0j5x(5s^?3sOtTrCv-UJU zhcnH&?0H1m=)#0yU5B+PQd$PFE7zvwQssJVb!{jq7h`z|JZFfP3kxqC^zn5tF}5iRxDv+wM{vYha`!SA@SX4I=Sz4M zO8Ch){sX@}mU7L-UiQ`eM+@)tVqaJ~?$SHHUjL~lQvGMorL1nz%DRP+Yqhxl+`W8b zMmPbjcJq6m&bQhuwAxkV7Ww;jDEC_`b}(hZ)s+^)?3IR0_9BIuEDK_lmWozpMXhK< zcQ7wfrg^feZ0%;JIBXiOjIXcn4!ZY9E6m>#E1$Jn=wgLX)8SqDxCqwUDX|{TiFPR( zS?2Cxfd+9pT%yL^id};ubWUL!d~9gfU@m)}u}dmi1D2s;MY(=A1+i^dS}sKu&kd~7 zCgl=*)&K#t>4)wPx^79m+%^taP}Lw_#e21?*GLtbg6E!=EJORv59qyVy@NV zZt(?tw-H}NZb5kFTWtoE*`0%}x!9C!$SxV~2Xg$3}D;XQwFM z$?y8M-hWSLIzPe`{BxvVaQyIwymRly*RQ{)s~(L~;(r|8TQ)XTwQcS-MmTYh+zRXb z@xxv?w`JAbD9eP;fWvam(EQSThY0>zY?~|X(E?BlCc=YJ3VFGHusWOIu5aq z{xmI@L)4?cekG8Ui>~YhJZFfP3oGy3?_Uu7_Kwa*LBt!YqF@YGd7kNiE^M39f|A3*Wfe62AN*UwplI+20ZQZ$N(V7vJbOBES78 zM~?fD_@cAq-Wl{?*$cN)nqv+TCqBwbe1j#HW+W~L;?2dxhyR?77<+=f{XUKuZ{T}p m;P#geAmYMbFwpJU9GkKdqrYHtlybzxtuMi?C)r!45&sK06Agm^ literal 0 HcmV?d00001 diff --git a/testing/btest/Traces/websocket/jupyter-websocket.pcap b/testing/btest/Traces/websocket/jupyter-websocket.pcap new file mode 100644 index 0000000000000000000000000000000000000000..6c2f292202fb98ea7d9e5480c122ce24a7514322 GIT binary patch literal 10340 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 literal 0 HcmV?d00001 diff --git a/testing/btest/Traces/websocket/wstunnel-http.pcap b/testing/btest/Traces/websocket/wstunnel-http.pcap new file mode 100644 index 0000000000000000000000000000000000000000..338fe623742963f51a6fc07850d12bf9216b7303 GIT binary patch literal 2181 zcma);U2NM_6vv&kz-r0B5JG(cT~$JYZHeQ2w4F(-#ch)&O_P+QpD&f;Tw=HW$ZR)l z`Y@(RXyrplle(h4OzQ(wtb9~-5^Xh!sYRLq0UP3-2L$2?Ql?hW2gbcl93`WzIMPvU z-*bOH_niN+^5f2D%TA)1u0+WMVHo0x% z?seDV7iC1>yz$IG3m^R2NXB%Vyo1ObdK30~vvT%xt5(j>+2m@@n~1)9@oC5Y59nYH zM6CK;!N0|86V!R5x9&>e21@03}{QIjz ziLH*kt+__xrG9HN>jtvl8pwGFr`?*Kr^sOg>aI{8jV%j=8C|A3+?|ZuLy@6s9Z%7x z?9;ZqK@vZq?qjbv>zi?Z;7^>}A6`4Ril*+YBTZeoh!n7PwU*m9Qozck3yy0w75I<} z2DKv0ffUpgUPbxHC5lW#9nCQd)V!q5QDpD`=}eENnrOQ#Q!1b~nsCmfMN^R}(q4!| zHE)V6VA{Rb!`f$O8p48$m!%@4$T<3BRX#?dX%-BYrtO;16glSN1r~VSqNsvN-qaLH znB)}$aO0^^I*n>4o33;bg^0ecHrDU=wY>rqzN@p{??IKN$RJLr8~;K+*Bvw^Q-5lL9Tb_PcA}LaT@AzULc!HLtDN;~v`w{2h45I4*A_@3 zjQWhd-YhA0a-Uk_t_^%u+*e$S+bPuUz0g{%-8-n=+Y8R7-!S#To5zmKnngWni>OMy zYEe%*_P$+iq++7*)^T|bJix^A*EX>t%k31BhjlJHi)4xOz Wm>T@frdIP@!jUQTzhm-ug7_D^$7s9& literal 0 HcmV?d00001 diff --git a/testing/btest/Traces/websocket/wstunnel-https.pcap b/testing/btest/Traces/websocket/wstunnel-https.pcap new file mode 100644 index 0000000000000000000000000000000000000000..ff653db7706e5f72212457857e8fec48938651de GIT binary patch literal 97914 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

N)GIq%d(h3{#c)sTRSmDmQ2Ql!{uA zYPvn2@dbFHIqW)hC6_mgSU)9a*_1gRZGb{obS>*Fmzw*Xx~dV~Y+a)SOu+{p$W+pX zURb7G%Oe6g&ji!i@hD-5D6`fJor|JiiZx=nI6A_YQJKWihk_*RX@;zFk z??n6vz%3hvC6@A;_g6Tm8N>rphnY`!FzYD?SCg6!wpEbviu-Dbh#WL;G(ix~xU7EZ zD8x{kBg;?5eHfQhy<)}dLX9FUy2A#O2g~|whc%g5Os@qC@ROr7Lf9Coyy9y|EQP{p zDev;gmz2~m#OrOM&lO=yM&)Wltz4*I?d82hV#{jLq&VeU$sHK(r(EO}Z5ZkhN+)*= z)KY>Fcxz$O1j=}&wAp3&o>==T$_KBoRVQrB(;oGH7$z0a774@8k|hOvQv7(2wvNr~ zy7O-S)4w-=`)~7W{nh;67XFxDn&PkKmzh#oTPD2RjQJ@;v;oUpBw}KRz!$?he}R0P zhfZY>DF0&ywOXSy`KW;`kPj9bLH>D59Th4bv6bKLRuIl6T!JM);Rggn`^jlx2k~JZ*QFt<}6V|UGnL<6d1gvRcJTNyR!O!u1wpC*mm%8OB;6}-cix22LIF6iaPE2iRVoq!byWw|FhM>J*I0fBQqe&cY zx18Ce4{3E(W@BNEV-^3J8jCKI{hYb*RfiaM2eL^iKxGY9k*YI?eS=!qslnKy3ywsz zEh0)Pq9$88@mt^8_r~nL&}DtVGmmMW&hGMZI-zn(mVfY0-%VnO(;%=D*2~JBXEB&$ zz1-J`j%!d6mM!-i^=Ro-JbmlMz52&ckuiCO;vPFbnsbeHKi1~VcF_NvOs>Lh&*bK?P8Xv=> z$yRXuI>UFdS%ad(6p*lB7?j3I=b4Sm@2ugP_Aw=oUG=fB-15zUyyunK2p$Ui`zV_r zfk1CvtKr>aTB?(U8la5KfuFjzDLM*Vvw{UoIb4~tCBm}Hm$&o;elG3cFgeyG!k{|_ zr=#PVtswiKup$jw$4^x*yh^C0LuW=`DWBt%2w%fo8%vG0U>ruk;v^!xI7Tq2n&)sp zion!VilOD+hyYTcyLff4?GA?9DP(1ee{~xMuJBeX%Qc{iy4&Ae6F)UUlhoDh?AK1_ z+l`-W=%opJ?_G($@X*(vAQF)EB>}TAOm9V2S{c*asG#$$3`Ry0#1k-8eHH;XctLBG zcO0;ifBxX9MR#>Cxl(e%*%%<50Hgj@VTZYMdkQfk7TmmqDUt(fXA4UZ~^396t-8b>*UO%ABTX+a@ei;%y*v@&x0d-c;rK|rkA?RGo@mJnWSZJ>lf^lVwd?krChr$$=4@>m~YMfkE#{r0;Isb#W?b20!uX&fe6BtauqpQb=vM z%#;00YwnY5zCF$vrTGj}dQ*Yct)hi2#*-rxXrCTKMwZocxsBrX6l|yiL`G6lBLJo!grAPD0H-o20Z@vgIY=IY}a) zE5U0n`lvz1jFZCxWC71d6Tz2)Nh`+}1&$gYUWvO9ypz$vV1Sup6tGIC6hq_|VL^aH zMKAj(QTaPsqJc@Y2kVl@*d+R9yZjzcHS><@GF#1GyXDd~T?xBxT6xTC@TR;t#Z46_ z%k;5aUX6^oMaMra z>c5)*!u4rws>k?+MjHrg(vSr98g@uXk1+o@ae&Uu@t2!;5ox7p6VXky+=P}+f-8?1 zP)SrKZzS#QYbVS_PzxkFk_yR`{6QiY(pa9Uo&!uKg|}HOwY(-FUT4qH62ten0i|o_ ziL2sBqJ&ox{!PSG)96+;atcT30ue$#)UELk<{&>qrOz9vaMVJ!hda2*?{?#V#V;6V z#1TB3VWP7Rpxo)lA$l3=jY)d6r zMqga;vV|GonLIi9R~<*DDiS>&7{?RS2^4ke|ytX7X6fGfO_?iOV+~t>;VN)-*Bo`Z2EkLUxY$Z?-XAcg^Yl_+7NHRSx)Nr_XZWlq>~>-@GepSgd&t55by)Vy(Js z#BVFrY3yfZgty9FG;=@?Yh2LAy#&6sr2^kKnoNTl3hT{vgl<-HVLv$6ZHL>sY+n>- z1Auh>GWN;WF;mMYT=dBiAk_lLfVsmd4fr0EhgsnwxG3b5pBSOp5@R*i74ljeH`URT zH;BTU&>0#Cwp2H^g&bvI0mi6OPJ7xC5!2MFj#;!?uz0mga^-U0(vTLkJmTgz8qIai zB2=2cyYD&^2Yg|H^p#U7gO}%T=`A^MG)TKCXthYbBjt45%Ool<`|{!u@!`Z$mxDr> z(Ih8NTBz*Ft3};e;E9g9VNFz~#}9XGoi8f})d5?medt&x);(zSQzOPplUcV7fH~s#bh~5edH25a1_xShnd!wqEJ?EVWp8DT) zd?UVXp`gEQ5C3xo#N}1gH#LO0u_RRrJD-AZG`mR_ablY zntyB1d;EL(-L4__+~4g2{71W{fA`c|yNv#41MW||BCB|LZ43GNZPN__;r5giJjmU3 zd=b|G#isiw=?ft2D_1=IHpFj6iZj@8xA$naz}l(g8do&@4pk)G44zfj5{+1;d%b0( zD-g=|{p->L$cyYQnS3ZdXh#tw9EQdC>TBodUX``^eaC98$zF ziQI6gwY_r|?4ToF$eAINUmnZN@6B9(Yv%u*v}m30&7=hR)69P?Hx*w0Y%w=^PudN& zzg#|-cVY!39MA{|(C_%qjss~!>v|`WyZ*1lw~jOWpS@7PTSB<_dxHeM4O3v8gxZxg zk$@TuDZ5pbhkz5vA+9aroNg~V1tE6@T^*y;OY3@Daib>GD&w%cJ(RpWX&8&8a1`K# zSU;F^WKu(c7$J-BNXn*lbTaoG#fW7c-?A_`w5vIeAc& z<^~d$&pSp@Zj}er^a(LW1X|W!aabFP zUEg!O(6dN&?~-uk&M}ZwzBqTsAul26Vhl5qXNr%eKDx8e!k%E4k#v_tabT5QF!_g4 zClZ$2WQgE_)m3nld%-EQ5G)&|ZjoM&HrKBO#;1GG>cH#jO$4ofE8SqpoAPOU8NaGR zs$rAx;%wx<+;E1;c71SPO3?n@Jz0CpOFVn=0I@yC&v@~&nkitu{cU?UTLKQJt3&KN znMWOXuTtYfLp+Gm%^Dut$Fp=^srdCcL7{upv9vmHwAdx7HB0;+pv%0d+5=M*E!q|8 z%&+nIwXBikCTwo}qanhk;?11=6*pY=U+qxuY>c-ha@bfyGsvk=#K=*h)C z_3XsmgAuaRSQbj7%ZrjeuoQ0-;Pke0xn&e2Q(cPsdGethhMR?0(z^VXypRqTZV!%y zM)3d{#kYik)E3k`j2#}R_|Bm-?w4QXyr^|dqBb^p`J$zJjO{5Xj5^AuJDPJQSqZ$@ zJMpl4%Y-zW@2Tw!2M6@_m5krVl-3U=tiv-%-rmt(yV(z`4_EQPm-;L4k-(wW$ z&uRZLMoYZj#%RR*7+q8U*J;F6wAo9F{02hJi4t+QGEpYjp;IbY4P@ zwX}Vt0$*c)WrF1qrl{Go58pc;inwbt;M6O3j%yQ!y9cCl3zKP%Xdie#byKfFt2nGUdts)Uu=C;+~KcD?*^IlihcqF zs^MN8evGy2Ix4K`$$L^wchIp04we*vEuT5;ibtfSkF<0%svE$f#_#CBXTT(w!L8Ki zV{bAk4^$VRwLJORQyobUuO z9Yt|or>3CS)dkjMPbP{o(~8aQX$=pO+p~|wE;dYHLP=?s#;Hp~{?XWFCX4C)>RL9# z4TX@T3zNv>E5F|~-R^1hZNz}tZwAhCcOYzkrAh(i^yyeA)^8{j6 z?^y6$mIP;TlKm{WNn|NS^z*Tcg-zDXWEa{V5wBXU_Fx<%3?PBaY{h2wxqv#x?U_vU z_5}zZ48(ioJ13%$Ez^lmDh5f14AhGOAR`mlpsTf?Kb9RzVyqhIvK3XFE55+b4 zswtJ#i>X0#GCaMRrL$YpFNE8{0w4sU=G2xSu;|6+;^#bCGjViJ;XWJ;Mt@=*}NYU2LDo9j-4wD)IV2etd>&U_^Xv zY!U6{v}N7<<#1pQ`Snl`dJIAsYP6Nk6Q{MS^3;12`qJr~Eb;=E`L&%=y{Ui?xBo3mq2* zWv{bw>o+^tEV*lT^KJ@K&qXjhqS!Dc>X{yUSxn}z;K@$}J9(k(SK4K(&eO{sx=CY+ zd#*LKcS1xqmiDhvVF2+Q*q{9#ofRK&TH3$<&>}i{RBl+mMi?j+Ve2z%pHOrH#x*~i zBxF-+C?A83{aGRGmh|Ad?g-A|8=XxeEq2prr!V=qaUo>PG6ZezuSv?W>>k!XkPThU zCelW>@I`?6Inb^C+BxF-QxBfHtxxNc^M%bogRb@s>AV*Hp|>sEVxUscNo&n;bOlnw z=!LSs<5Cxxd9Ng&1?LQs@y{1Mwe zO+7Skhnit8rCZY})uy3O*kY1r2{&sk3pntV5|LJpSys+~N z9TW(Rbo@aH_5#ViXZ12R?w}by?^RM@vNTT|O_z%-p+Wj&8jsCxg z-fu)*e{Z6hH!p*m)9AN@&wWEiASEa4DQ=sAk*24%$DsHk%LryWBBg=;B|U3w-J&UN z4zU}CFUgqVSmW5?{!3*#bxyDcG^s>ZF9+xavH@$Y#xFMOyrK?{&*nClcDG-gy7$G) z2~A__r8(p0e!l#;EsRFl8V}VCOlnTQ{}+WaPqb=f>CgoTwZRETyoJE_D*#%34Cu zVmwig7DG_hdgDRPkHLr&6KZKnM43?w5fIpqU;zWQjaP_|6FH6_HoLgNQz42kVPWJA@s_{YJrDhT4N z=_oo@1UifQI+V*>V-%D5a?Pb*kz5J)F zj#tDkGYP+?kf=w@wr}Xk1!YdhQJo?5dI=>`?BP|Fl@ED61axp~6-FsNv&I_H%uRGY zbsG$K!1coVJIJ<{P&j|j|JjwE`YTOR0+H>J1+}RffxzI1&AFq)u?zap;Itl$7BbYP z1E#>EpFPu*m?(TsrHFqL`5PClh7}Xrug1@TAedBfNubp%b0M+)b~}~_J+YitZj7`D zj36^Zth=%XJRWz-w3=)X#$}sS`N2X6=H8mtN-2d2%x5uJQc-yG8{bVW3 zoR^ay^36;pL+?64)e~hBtc}&`Sg}MgZ{mu22eES6<^ynHU))%9z?m{nPXI;FY?99j zx8W%Vs$&Kwe5UPybPR&Kn2dBD!LfnLIlUTile49LZ(0=8@QYVxb>H$K#X`*YGXEda>f-6&CMo4;n%tSYO0aC>94fUv#61@Z+Sx-GhcK^y-h9S?*{hf7%{^{ww z6Ua)&;y`qk7B+UMS)uGI9%mP`nhH-7dlG|dp2UuX&72e|F38ykn4DNX#sjS?19>p1 z|B^VQrWasl&*qKs5c~Bi<)d?5Hg)jy)=nT4(<{2YnD>;mQz_T?9pLU&|89QnVgEgA zD)Fz4u8Aya$FsYRpldQhF?O|jy%f1|cJm)A=U6Kp+I#{d;jUByhddTAX~tGXevTdq zjQ-Z``V@6Q(thy4EvS54p};!rMlsFh z-TdIbe{24}-{x2OtNH7(|Cpa-^RMQ=*RIq~t=Jw$jQjX0-c+%G?=qaA$GV;5!__v@ z&GH) zDsGtHlKnUMd_Ta#490%v-3j?)7Ws(7Sx`A2HL_uFv_UPe_?k@kui{W0Lxo>(g(l9q zwSGQe-+|&^ga~fS)(3~3+G&ol*Hnver5|^z-F6le_RGtHhD5_R!vwg(WWttWo&wiA zGEY=MMGTItObczE+DVWPb&#@^PH7p+C?~4Z#)|iUwo?vO9Qeu*2%$v>vsI%r{(_>h zufqV)h6~&o{zR#V`&EZA8=%>#P$H31p@BRS2`&P?A5l4$*>r>CGh8u3ffsg&)&*X( zRV6LGs=c7P?MS_+pjxblktzix0X{ti&fI1vVcG$xB~Sk`!K3xk{$Ylb)Qi)MK%8%^ zFupd1gh=xfMITG`b@*l1IZY79zOHDh-iK}xT*l;IZ6@iqnM|(}mrb^~Y?!GwF=j`S z->;0#oEXlw$N;%KE>2xxO&2|Nkzj288tFssXGw$=Q=Br^*`c)|--T9L$Bm zvT0-;0in$|F8JkDN`>H|i1dkq{YFG_XJ7&C_Z|=p+{8V3cEXG-vUWN%v@YA9$g)X0=;6PYv+v3nRkgZ&kj>y!S5#+ zfEaFlsuZOZIS>I$&kkH^FT^#hCKP|bWjDd~ft~_4x|BJWXzF7*h6(FR;9`JgPCstD z*mJ)jO!oJbTNWY73wO=y@jR{CvRQsuNwQwZl%q_Wed`nNC@d&rMvEmnjL(-kVy;o{ zI&rbx3_>w%jownZ`q&#rwwC&$K-hpiUwaGmI{t9k$$aW41gRvCn`zt4ue=}7U~sZK ze79eg%R}^V0ubKcjV9Fi{W({{a$Ld+)Paa@~eWS)>_eoL_>Mlw}^t zoF=~=etHMcl0E05k#S6KpS)6B{ZG15m!EaI&?H!R7O(NqVk+G@?BNM&(X^{i0b!lM zmi>VWkbtvQCFT}uo~0|)p21mAnd76AETjIG1_uQFQK@vf3=YEUWd~bTP;->1?vt-p zqdJvO`v@T>!7GhA}ha^ch2vnRCVksnCpVMk{JpML(M zLK}2eom;KI7K~wKk)DB1C`7C$_79St=hSEG^He1FcB#jeR?$NSEppq3Qd7m2pBe>m zk{H>rt8An)c!`Fr#C|2Dt!U(Mg#^~d}`t$#Itgr^$*qN9&%tH!}9 zdm|kgVuYmcQuL-qUos~BW6UJ>u>=InHZ%dW1jIE1xFjx0<~cZ5#C%`!x2&D|2sT<1 ztKLMvaMDzHcq2V;LFpTwY=S!-t0h1D@a_-j({NwxruV-f0iLrO%$7J@q*%&3H;55& zOv4diX*lT{1eH##4EP@QZYhegTx8Fr=9jlEbGC%w#BO@sQFqlMA(^aQA|aRpPlN=M z15q&~r-SV=)eZ(XKS7kcFGL1f@u)JmCVtge5Yqu&DHB8bWLa2)0m4?q+f{+MTvQxx z-G|^)Bxs-vgR&35#wSQ%70OTCQVo-yWa@DMov{+3(Uc~bpJ<7teVcbPv|bo{eTI3` z8%63XzS-V>`pLQ@Bc-z|WpC#bEm+Il7V4L*jsYXb#4gGjUcC~f(I6xRaKE_?7E+rp z8zP=z*i3h#mZ^OUpCd<^zhFLNfT%hNr6L(te22QDHI-EPage|jRZK4ENnfKK;7p$< z+d%S&iGphG-UG32)-Ge%>TcTYG7m%Lz(Fk}{i4KFG$err1qb{LGQPBxY$-y&i_nG- z-t%Hpq2D$VTqwDt<5Yi0dasww=dp;qsWPiCHDBJ|Gw$(P2v-q3PVrG$yF|wvuW}JK zIQx5;D<@c*NPNVsTqtJz(li1a>P`aKeE5x`7<@FCAx4WVWbct7Gn%Yx1qcN25%GM@ z)R=Al69T0X{OVHdFJ7)_iSaa^HT|EHUauck@cafNzf`D~`p6{huHxPJhoky9Wl;pb zw1X~qq3@bQ5SP0RPo@=c@xEv-&Gx;XO_{-~cGo{w%TIBUe?NR%bVi`k)QI=eE{FxsdL+ z|DXgrEJP3(QqIOQjbE1$nT@h&Bnm$lJ_y$PdPHgVQK4t1k~yPP$$^ovv^&T#DS~2s zeWU#Rbk+h=u_XvtNC{E(*h1o`vbA7pSuGsly){XYD5d19qd7IJ)z$|7XA+t(k7zw{ zV1vt?Vs^d7CRBt$ zbL6Hn0dh)wpqJmz=U{r_yqqG5Wh9(?F+~j_xkTQ>Yu+G=z929j^yg;16->(-C8GW` zcvg9lQoLh;kC5jzk;Dz+3i|vh47ra@l^7-!%hgkkNl;LPk!J{{-0uqsxWF$Y3IP>J zOuug=xs=DZUXvUNU@fZ?fKiD_V?`Zi;j(Rv`m!>T$+qBq+-+h(dE|VR8#7sqvF6SS zBjFzkgdh-J&))4|QWO2Vf+z`>10w1heB43fDmeWgs{GIB^(rFAfFgJu55%wxGm3s< zJ}e@mvZ^A?*a3H;-z;Amr-flAW()ffy$FzbP&JYCN zM+MTCPYgO}BRFk*kJw0yzZ7y;X@^b}zXz#o?Kf-jE8I$&qnKHOeiN)sn$EYwC7!7r zTD5hYm6d(kp0%YM&JKC8QA$!D$F2}GUo zlKSEpn4$5j#A2;tKA*OhF3e|(k24ZTY7zc1LnD7)oqc1Ul+hEdvN!-Nj%bUX^hG_R zTSkXUkE*L9SYXlaw10J%;d`)&L>J8EhwdCoe~J-@d2j&eB$^3yFpMGMaFJz-j6c+Y zEZy>8Ud^H{qaQnIX{wv>gtzUL-@Gw5*Ql_;U1im>P~=$w7c!w0ynAtH z_Yi6?B*8i*ZgF+Hdu^BqAMIkSCwpBs5LoP|Qj*WbocQ*`mRV+kDd9@j#8P&<_PReq z)vO9X#7cxSb)KUagzbcUmJb*GB*Z8d-kOTEAjWWbH03oxm%DcavhF3(CUxRLQ{#_Z z)~(||nH$%hcSWwK4!z-kWdguW$2EXJ;`pR+qccg?kRAv^BN`$QJt4w zJ^y3!BgI2{QtO9Ib@u|E>FR;8wdxkQ>=-WuU?jS^S(v`4> z9lm*BOm_bUp=?R3C*}@$@!*d?YXBeE^rP0$G$*4`#~4UmZ^86VE<@5fys`!@DET^S zP1LQxeedWGDmkAkBI0BxoWCwyx-{UUxt^;!!(b{141eKbbIqR?h8$_d6cKMX4q-5- zDfIh7`hA>V!JKL;?^%2d@&)c#@~ZcVhwv+wk@b;=2n0h;o0AJvQ5nb)Eq21!a$3z( z?8**y8%_@?-Gkk5-417dd!#erEh*BVQdZ67#bdTh5t1`jnI$x0P#QRwRA^o*D8Hx@ zv8+XE{hO;y$%33@5;f*1@`4CO5qQq;m>Y>2D+rztGPKsYSc9dkO__-&jnA{TPh?CI zK`8T^(!%}$t%)vO4`BL%p{t;lIH4-!9F_+IKvQ}KOCSTrUYD`*gsUX-(~mv%8nY;{ zv0*mYo94PRwd;rmVb~Dggh{xQ6 zqwyCw?JF)Xp81((KD4pI=%YoUaD zbSgUNTpAbQ;w5S42`rEA|(y<8BrWPBma_KPh!ldEH4?a^(@zug} zd#k{sSMPNm)x?T!=Gany6(nDFQqv0H=^_uXy!hz>^&|bo$k``XCp2Hcg%8VuIZ>rNvS^cBsLGCTTFv{xbJXIf~WT^GpHNZ0co_1v|omQ>GAA*+;`o-dO*S3kUpATXNaTLnhB+VMHE=DfMgPgZWzg-P`wD+%x;LkI`sKBOnb)5va75&y@x;Uv@y;1U@}aj`bnP zMhtZ7AgFMJ!whti8LE>gMm^I0C;|b*aGQWhbaF&O+AqcX5wy%X#fF?rkftolegZBJ z<)rf4--Nz0pw-1FhCVQp%K(3sl1RBWJKnGceH-842;v5LOl?(3@N)(<*5a*@0UYZ%pOb~Gfck;->eKE`gez+ShOwKA;F zJ%Z{$yz&Bn_|O`d<>xL!Ng(r*x$n{b>mXq^93;hk;VR9o1($L@x`;D7baL6)$mr{h zmY^y}=R=O_&&O26S|{Euxq@S;`{o$e#2~nAD_*cqt8%D}Su$%>J%IItkYhXOq^J+^ zsqf||`+M^b{x-kjU(Mf-{M-C*`>#7=e>MN`-0C=IT#(r-byzG53TD}-phN_>yxAl0 z4_nF`kzW*|>YrYp1eloV({g3#ld6wC&kmBD&nH^An*<-|NH6bE2rvxW7DRnHQ}>#> zovDQc0%^8}l8l8d*zx&zt=KJDE8T=WMk#AV_=`>cJ{e-V$da%<<$+YIJcIa^PMiV0% z*MYKv%AJ72Hxo)C z`7Ss@W{p1xJoh59zdj2&i=R*1dnbRAW$wmT79z7r0ap46CZ0v``g#>K5}q7`fN+YG zz})I?3~CM#;h{9s)N8?qo*gZ3v1Y)Sm8QY>OO5(suzmJ%Aw~#*TkL(ueMdRuRkdi& zG&#pmC&AIDE-bGQt-BH1=XVLQ)Um4`GjV0_6%jOsEJAvw7s@rBKM!5ajvbUzN?p7L zQ|!e2GMai-L}0*=DV{dvEaDm8##KqwUiuUvfFF4k*Jb3v6~Yt;TSh6We0IRPeIBj# zxICKazzRGM>`1FMro+fbF0X5+rKYSSZ;Iu`e%PH`tUors2xi@u675dR7l1T6{9!h^ z@foY-gU|zuO_??t-yxu!ZA@Cq98K}4dYxA?!#kKL0?U z*o-qQ@~7~21m{L7aY&{7`rDAa@;*F_O|t1VJ0#-qEK?79$?Z9pxhzU*_A98Y#n(`@ zl$@WVGs!!7w<4Z-@}hI`zqH*U1A(VF!~Mc*dO+t2L-L-c#--+nF8#DX#st*@$zAJs ztB|D$fF?8c#Nf(ywNLcw=%B>2z%=np9j#mi4ee)kxstQ+`FkTgg2M!rV|KQeMLs26 zl_@yv?~c)b@3UqpglTwvMqzeKE6(JO?`=2OIV)HsEkEw6pwANjz{8Gs958HOdhkHX zriSu4&7MK5yDwD4$i=}B7wU{KkpWL*+A)jO!UFT@bGYMy5lv*vf)Ig~ssnFZHXRuJ zrsjtzQB29JvCjMSA3qD)$kdGmK&7K3R{Xb%Z?bgNQ`Y?|Uvw-zo{P^y$u~ZMJG(Y) zu@-`sy^hCz3H5T6p>m|l$|tTN0FF0a7y*6tmNgMc%z~*MGYeCm~6$4wKrRy zkT>^Y_}K|Q?3#K{bt^(IWT^<-EMJ2bc|nvn8!N62hWSN2fX`Dt47K{M_QhKiq%+e+ z&$1Ljhe*KdCi`H93LIy)8MjwK0@a+7QCMhG$XZv<4Tfl=T~;>Wu4YQGXTx$b7y~_= z-}irON7?@sbm1+_;up@oO$?tjPMiRB1@Fr#N&vjwPl|2P69vHf7E5&xv%DR#2HT;O zs_8kLKbbzQt1^P*yMD}P$na8$xBSVu84tDz-c`x+`KC1~uU z<<#raZF5ONB$GnR5-zh6J-(6lez@4X`RU%w|Gr=Rf9x3-yqg~a?~nQaXV17E`+d)N z>h&+?_kJfb|2?trHxc*0iOpSa#LH35|0c5iJ+bIF5$C^&on~*uCZiR3hFOJUHszXu)}pR`cJ-b}Jjg4h(>{RWa}Aj&m~g(8hz=5!p$FaO4!TrAs^QSa5XQ&f4hh3 zx$EBX|M+^x;7q%%Ydf}W+qP}nPC9ndNyoO6j&0kvZ5tii`MP`8y`Oj2S9PCN*WdB$ zs$;A%*PL^;=aH3%+?w9;%K`+0b=Yq4k+0Sj&oeK3&%YtH6y&IME|bjN=E9;EjzrY- z8kLj_%@BG+AJUYyO8ZSztRqlRAX?pAVkCiKrcO3E0uj_Fu^JUb$WUark$s*X1URQ& z@MQi<6v^l3a^2*-J!|z{SDx!P5$r_OyjbmYbmK#)M{uQBaMLQY6)B_~C%+Ikqgd)@ zGnB z&RBQ^$e4%|NCIH06>4vCIrn*11UT&}7~wcKU=@OS0U`!poLR9MY?Dzi&Me0X!S8-% z^b9w$9TiKshqDzp19@q|qOegDx3ErA-Ns(+DE)n(l`WQDzTTtZrGzkqYPVT&QLLAsR@?y<`#QdQJk|7}PQubiT zn{mO*TFQpM2G4!)#aJea>#hmJg&d>r??G>zG%wM){HWz*)@Vh5!MNf}C#Y5g+!s{B znYQ(Y@X)fVhsdZaRcyQjd@PiXyI#|M&Hhw~^9{ZpKG((%y}F0fF>0wCVr->6#Lqm* z@%_h^ms?Itfj!WEoEsRAiav&w>Gxyc={*LYk?Jp1*~{gCq@4OhPrahtX?PWT4Saj% zei8=`EB#Y8@fsY~GVdaUxNxK8c1-H|Li<-m3cJa(-I2s07zLJrul?z@zt&2^v}T^V z&vmbw^ouo}CR|95mX?FM423isY>Sc%zWD>ztRWu%ch4`K9%#Eg#{~_F8?}_=X_%HoRV}BHh6#h~C4>M;Z|EXX_m;YDM z=Xbx7x4$XH@y z`)m_r169G$7T~Fn!lw0)m`Zky4ji|a3ze4=lp4KXH9h^g@7$3}j9~5a*i2g`5?A^b zTUF0zr{MK8hqA>C3s-Z3$*3hZ*E#GT(zqHYeLTJ98Q{29f^Gh2-qljHr;y|nPhy$g z96gTY(U0K%a6dJPXOB^)t#-}JjtJ=v7{=TO?t1DxBxKi&h zrUfk5;|Xu{(+(*7YM3cVHb4X9#`oc23!6g06qyB{5xd1y_Eh&2**06l+qF$9&f9sf z$v~g=eg4LHI`Wo4ch#7I)UiZd4}sgU-k9|_We;?`&7?Kp1vQ%ZN_1GG#6Wi9gIMj} zS88mT6_}h1rXtiZ5U~@-nB?oq0eBPS!RXi`4^#cOZ9kY=m}1Z7C4WWDeNP;>yDr&x zT}c#skqceL7^D%Y%zZb??M*!3WH@8Yex@99=|3<{ZyHstv4>(t){@WZk{bl}zo>kO zgh#|ekb8-KB)Bdr_>!lYM0ox3BH=WHl1T|x)*g_vnPEXEAfe0Y=C{NQ`8Xd(u=SNkNq;X1Os-rV*9dc?01+Wd9KW0aq8Mps zaTGTrxe)LbXAZ}9QO5K~J!cx&)SH*&t!oynp1MlO#9h!TlM34KaAhZ26q#%&l(HgP-4i364 zI*3De^VjS9h@G$2!7xgY3@i5AiiJx0`I*>T-^&$yMDK@N^$@j1yxmJwgF+ej2 zUtdwohwa(YSxU1{2wyh3X7l|0{+%~^6U|@Z1jXM(z@xdn`hG|y1}zuLGr8Hffn^_7 ztw-pRhhpl_wmBq{8Nc2Ijz~o|EO==Tmu@{pdIH-}OFn0~7xa$PGBTk4a?6&EV&kBV zB@82(OyEEi3f|_*Dq#Y9%}&iC#?tV?S@@QV+}kUDnGy?ufW{0jIUEjgEl{YwTh`4u z+)U(*1PK&tFj2w7;H|Li?)SQz=fj6GP~fb%OiL3`h9ae%#Z+Y)xYLoxaPs1V4BK?e zytm+zK<52=E6cJ2V~Wx!EXb;=@kPQ8y>zaN0KK)#uh|$~3nG3R+%c6=0wAUwP_Hp3 zF8Lmt%@9VdkK!xWK*{r^duhMFXy7URv#>Jx9!Ap68pgLs;H1(fdKS`E3w;Bq_y(vL zTlCQon0bPHhS0!g|A)b}jU<1FwFJ>&o6|A$QkWiV)fiH94%dc(Ub<|HMAPsJ<2KMX z(@k;EBk!*6a;mBvS0>3~@_K$uL|}3^eRj3+aF^I4gh_b=C4>&Upzm^Ig=ab9qQ4_c zeYPGn=+M?UjesGryuRtha#_twAN zA3{Eg29r*2M)yD(774t3%Pxm>uzb13*z}L}Ivye{Esy7)UYe zVBFesbVhv4>8(fCSV4Y7N4dVXs-(weE*Ypha1AIBqtWo}gy$8%--Hqli%U$>tR6X> z9C1W8h^ve_)LNf)D6?L~0n~mP&0!tG%x_{Uim5z9!20O@IOoI%BnK(iX%OnY|BsiX z{3d_X@9BX*zyJ69J3&x_GX9tR0sl+>#hZUl%cvXwOMYzPGCZNWob~Ku_N%-OmKrXN z8-mZLt+h1xycIWe!(Jm)5Z>}=?!9dCvOc5l_QCY3k=Gr^4YJAK%b-&WD|9e|m>jWo zRGd5fn;^{YJMhrb6;~)zKtKR-#?($P19NxdLg0zxTE8EnwS%OhZ&M1Z8Jgjty6Dfx zO`I91Jz9thmJ)8U(#m$MJ+j&Uz$#<{9)X3@*-T14U4&TEXvaD8m8{l$mPfbhoyRqI zfV>gEk@j^6fFanwRcsr^)o0LQaUDkQ)CVd3Mkj>ul-VhbUySjyQubn4y7h>qrjBAHbQY9Uw)oY%VY(^L7{-0aWFYh#yW>CbE0%e@W>lOYEtcVuFzacPp?&?AadS+Gj0*fJGp^6gqUanx# zch@6q*Uv;wKa*Pbwq=Pg6(NWgXI{yE)CX*+&L4nLOb*Wyxt%TMb~!N&qU~>+-;zSr z%`7o~y0;WC`e=gff0OS}Q844-;t)<9YPd+~j;-91gA<}g$BFH#wN2QKZN|i$tp$+% zr3_lWrCTp0wMZ|^n+>B$F(bx_iT7_;&-!FE=H6ZpGR>5G5$7Pv0FIB1IedbpGxakrDT;m zVIo1yk2*=B1sTgQ=_Z$}<|s{b=Car``?{}`?h0(CB7_WP?jngBcbOFT6m&dVQ0nM>l|;0osAvdCfK;d{#Bnqn_3uB^V8mcUlFiFLvUY>po(n zXADxRI75>Iu!kFd`3NJ72GWT-(awa3ot-3o9Y%p3MmPIeB_0iA(z2FuT*_vBggAb0 z`4%bBU*?ReLJtf#TwMb5Ybb;k!xu$-+eqB`P!lAS0wHQ8-3+MFy)5nI<=~- zhL{8|*83!urE)!k=1WJx)h)~S!P!6Z9rHxN^JVcQc>nX5;rp!YW$TNjt>VYEtUvKgf|1Rvy2O3a>Z9`J!m!`%ub zt|d=D2EixKc&kwmW zja=uVQ0@fZms5)C7BkdkD*pxeg_qInOhpN8h6pj+fewBvu`NcMYe2lU7GK{ppom|L z3fw0y=rD5+Ws767DuhFSJ{;)GmmD4=_A!l`tlCQ0tfT=jis(K6(rlg+#BBS{q9TG@ z*+|8{-bFGhYqJO0-XqWzGlw{%`yC}IpEu+miYNK~Yc;CkZz4eZhvNU22-Z!1CxVCa z+W(=r&mTps!2hhf{ImA*|NZ{@!T`#|Utci$NAW+2VBPc|UjY5@II#DhMBwvh<)vLV zicj$%ZsgPU>Vhm;3l-Ed0PZW5$vy*OHGSWEIDlZBZP+>ViZ=8RtbQEuVl9j83jSv* z2t0X{eLBPfG$C>M7$Vri{$p)wisLSaazd&D=0MQM`**E5%%w*nydH#mNgiGM5)g&B zvH>Nfn}DNmC0j$}+JnoQ$Ca}mfsDW8wB3X}XOL>QO7fVg$V@UkS zUs4MNmR9NCS=(3nfDwlDrNRWX!Y<~4v*)3a;K+5Yn>l} z$1L$(!rH8;^5lHCk#Du;+7wEGTBK1(b|d}%O8)T` zAP%y+#;mTiecWcZBr#yi)M*c8wib?Er(wF^T)J>ChR6a=aG58;VegIdDS# z_*u6>&`+>`NnWd`c8}Onvam}Hh>&2)oiE}I@e*kk_J_3Pcbx3$e37L?Mcx~5|9F$2 zdL$7Vtv2JoFy+N{F>WP92GipR`mtf+bn|@fgugp*jaV&K)Q(bTl2u7C3-M?LE1Znd z#3NI~H`0M;^g<)Wy0Sh5=O_DvN`Em68#7d%dr3oj^+>^>`Qewie~ki`t>kqrtw5Jb zJqp5{8uVVHavzIDHA{@rPs7%)yN*FZvN@k!9XwD>E|76m<3)U+z}vZZ$k;M*lgoE1 zUew~_OgR&rO3PbnLvHc=lb-QS8?~Yp^TlO31rSMUkHawt*b;GCS?!xH*A|TNuZcl= zNTzX#?A)Fs(6TpFY+KYfDV>4UU2cK3N@Nl*dd*& z4)((4FDyi~FrSmsONpu}voj4T3qnQFbDK35xnHplBsq!3wjPn&LW7glA!g48p@GDb zhP?FYqHQ<}HDIJ>zsJPr6PCrg)OGOvkQS5muA*@%-O!54XUqJ7Bhmmf(9b^CiYfOE z4NC_%Cz}d4i<*8ikkqM#Ewugu!JD(BL=N^|0z!+V;G-+gj4!-f`Cu(;?n!r1ZsTt0 zw7N@pfSl!_YS>|VpG;iE$h{D;#qtJa3lZ|P>1CXBno@~2QNbPBmmrO8r51(pwS4dM zQq0V=N0REpzpHPwnjnpVW`Z_ZbDs-uox*ETxFNcCW+BObl`4Ut5r`fxs19E_IHyrFlU=PegXE`ek)T@v;VVb?m2Zjjwkgh}j?xq*ww z>`4vZ3rpqvmK%EmIo0R8A8i-b5~PSb*Mp?uUoG#h*QcYW+CWqT01Kr%jWCS+?U_l$ z#0``#o*{{VyZ~@q_2Tc;o6>K>|H}e>|gz@c-`_ZXy5b8IylK z!}K4;|9HmE!#|$kF7{tV-+w$qmxr7GniB{}6?^{0o!aRtrd|9VRy#L+x;q3tL~3xB zcoK>QASk}6G+l8)C$IB}2F8>Hl3h?`+0NL(?~Vncbh6dtn)i9d3%pDFQEg)LfQ$~g zObpLTowt|;Qkt?h7hr50uCd2ffiSj4y4ajImd1LjD>;7+8rvpzxB|p-qLxB}RcUN^ zu#I^bsE@+q-L;WOdzOG0X7e%O(?}B5%qYrSSGs=I1l2ndYlTV(L~T@K2^@m3aY&&f zNVBgt{qYUty_ir@?D+fWRpo`GY{5_o)dyMj=i4mhk5q)~W4oIapN|{cAFVXDkw3e+ z1l8o#y98Id#8(-Hz#`pNKK29;?Us?x=)@0|TJgtkPH^pgIoX(9FIEWzG>E;EVA8!S zXysDzDEkiQ6TjgfVI-6V9QcJ-)1;=!V-3T`kQkC}s|PY%TiDaKvOiV1OKz1$9~w&f zdt$tkoOjXHqu8`ld9&GhEMT+-1PE)zo3c{Z;Q%k%k-f}F@gcil;8z_BBVldvz-oQe zBeH{cSOsKqOA2-;f)9ucliW0Jbemp@i>0%k{9wty!;Gl}f*Z;jvhlj6_l}S(?SLIQW z^K0O=Ir3evKtu>(b532=C zv50}+l=10q)T;k!K$ck6=zHRz#^mT5sR-2@yb4X-!**Oel^82_Q6{yk>(y2t^oQOk z2^D&UCm0H|1%Oj3cU^n=z*QRUm?E?+V0n5@FgWAvEg8=WTrkG26iB-3OV0ST_b033 zo@R(xX4|vl7m=yh?WfQCy`QTBTl1pP1@(4{qY3|%--oe17O66)OJyTitfkpP#KIWv4pFPg)dx_%4+9uKya~Kc{LD! zYF2=i@c1f+iCVnq@uO(?de5Fz`Y-w!<(TuAsz}%-vd%XVaa1FVdLoD!G=oKv46E#A z_2wSI)AEgCL{YQ`P7j2S^7#`-Mpd-8c7G7Kfb@>`Mr`(U+Q4+Tv`KBKYiqgzYm2oc zdDb7>7CL60NET$`KU$t9pTp*JD-r;)qA^3FvC_8Et0LaYPUXOb8 zRH3O)l=IWZYv_eZkP>+VwPdGlVw1#vyrs1z!pWAlXezZ>*IEg#2LRL4`hDhDF)HO> zX&(fB1)woUBOf=gA|W%F7QD}wp0FS$=^QxVNAF;LlQoh2xKa;Bpbv)c!B6wSl$VV7 zQOrxvclE~T>8EnN@~xNd!We`6{t#Hy{uvTPTE>U9^d@xPJEmVs05xHC+YWzF5~u*J zk+u;9bb+-lv$-LxmkvQ0)F;0fV3COqmx-*m_xs@+`T;H5QEqClf(_+#Bt0u(p9iYP zLR)5#i+mA;wc|~j4;zyo7l}LBUt7p`e32C^qUVgFWidb;{3+ibIy%s4tZ?Xfo#=5V zQ?JbJmc!BP6LV{DhIA3B(DIEL|IRfrp+z`Aw8)Lr-c^@UHIwuCA}w8!?%~^&fB3Mp zW2K;-4G<(5252Lr!wbo`nXgn2ywc7|-Q^WjPsprFF84^iXl zfC>6$q7EBujt$|^8wuUF+dPu{_2JFAsf8Fh$|4@jYCQ^Jy~N=wa8mhMvV3E_+j$wE7wn;e}8w_AKZpNu~I8Oji#jFO(ezxoMCyRE*K}Bjj-WHy11|FUL-A>^dEtmEHBJ~T^LqNV zYl9KXe;cZdM#!gpQ}~p`0xv*VT(I9C1{iU+l!lCt1zf< zQJ(w+^YRUx<$fl4!&DeP8mf;GncKda$#a+nyKrY7fK+Njzz|7dTuRY}sk=CDg@ydD zaJT%p?5cA)XK6dWG-NrO;m*WhN^&xDp-1P#1fXpqjRp(Ss=uHmi~}%A zo(u^~8B8t0G|fwmOb<#R1S)NNh&r&XvyQdUQQ_OOsk6#_sYubsxaehF)V0@FJ0(CO z?pr&RTzua)^8$e@pT76-IzY|72l+^$HKmYLO^20)M+&sZcE|7#g*dFI-KMt@_;m?o z;KXPLrizf$o>S0<34IHyZy2F-KyT2dJKlm&HnbV#;_J}i)T`{2M@WLaiZsKn+<2!y^e@B(mQ2Ca)TFUO z-h9Gr4rfQNA97h#xujeAZa%$5RXBDPeD$Pxe;xlEB)vtctq;3wC05TjO(ii4n9;%5 z5eP5rh2>$5-od?0KC0Cq;ii1E3mg2d={t-5d2p9jTu#-<%cWUgC6H^DAUw}fjKrZ& z4hduWNK=tXcW+AJWjK_%rlpf6ELg5i&6r#w8A2Yo$9RD!)x^{cos#CS12T-@3`oEvf&2sbSe}=;rDqeR2VnU_AbZy9%kKO|!Nqq6Q1`^nxIL^YHMy#I4d8AOp%4|r zc%h$V1IWhP(91ph=|-@A*vU39fV}|QE0z2C>e=$buWmdp)BC(^5lib%l!5m2_xx%> zILh#^x{YhST--fKlVwi~GwI{g-`b{Q&@`-Z=vdo$3eFz`L6a7Aj)is1 z9+zyc8dt&<7pm;be@6P$d+V^?m|C(I&RW#DOJ!VNkc_T(Ohl{tB4*M~D*)P80Pp|a zMHV~+nDBxyh-Tr`hE9cB0yD_>2uWh!8CcDi6(kPz2FP|mnzZC8l#&XtU=s6v3BEn_ zBLqbdl~v~_EO`8me%OjX2kS?PCjTra%u?s8&7Q1PLun28VNpo8 zvoS{f_X-sh!EN7edrEOM>$8mq(zZLTrFSb%Jy^MvEkmcfVQR3PuV+ECUpcpr-UIZs)qv$Ri4 zy?*Hxi4PETM>LH%W=UEcJ@bGG5ublj#r~jvl?fSau4w5&*p#ZP_qFGMYIAa0$T;N) z5Rn+b3Ni!!HeDaSwzYz9?o?{6)rXR8VuyHkKaV<vi5D*MjNYzI?HC>=p-Wrx{CLv@}?RJcUMWUJ!SZE8h(m{SsUKs({ zRIGtoIkALhR}10P5GDt|O||cfP_wyD`O8YesxGYBW=m+ZSs%IqF6pa{ll({gIC&cM z3W(Hufx0|9q?e@~EHmc$z>PK7cToHeui$_GGyc2LfDijaJ%7{o)KH(DyriI@Da= z0${!~Pa57e>Uhm6+Oc1h4pWiYSQ8aLxJX`Hwab8F6&5wldf28yrdn{f);ql&96OL$ zEL>hHwq(FI#|u4eh@Q?Qa}XpNnfQDkOfB$D<%4hvFoTF1tlkv5UlIxCx~jr%(hcOS zXC)(UjdU?P{x=WP2%iT04uQ8Sykly8F>xYkYaarI3+N zO<{J=WOr7GibV|nL;ycl7Asbb+peB!YJ&qHKAz8vA+u8M;$N%C*WF9jHeG2ES z8XkrbN5QLWWhL)7OY-ty<=8)t~0K|7L zBr9Qla6Fm06%PpXOhha2Ewg00%H479(I5`SulCZ{GT{E2h=6^0qduA0Y1_l8oe?Gu`c6+SG5s%kyzE;6DTU1&P!eGI_D z_)1mo&5c5XJ1Lj&3kjJII<{JZmN4cHh7frwTv0ta)l%X$#TiJ8I1*z`+5|WwQmi?a zLZ2oDW%XE?smg2GD9`L40dQ8DAIq&jA)JJaWk5h2?#^Qcks~gtQk$QegsvzfGZZf0 zq#?`ooYvHRBixz#i3m`y9qZU?r#bX>3c?#KM;qo?vH{~tr<;jBPdSXy(7}x*ZW5Z9&eKQAs;e^diM5 znW9v9)7A4^1{fq zwy6SHV?`mjJ&{NdmL6fTE7JYjsy>Qrk`E$!M)?b!h#=)KrevGDTrV^m{4z2l zSM4SL&~1DVB|xCFQwE`~Y3a@P5?a=&$rH%u9OuubP(e)6y@qkh&f+o*?kH&w11p7u zcj0q6oGO{N6SK||PrZ>HS5xB1A{?{lG+Q5O(IG}K22_RVbqK3uuaHUiO4zar0HbYk z1GppRPp_Ei#yhL0uMScgRo012)YOvJP^jC0ZJ;Yz6vQ)`P99>cP`syHRz+SOd-L_1 z6``%YYSXN%4~Qd;mlc`8Fg^IGD>aIR-?Mv4s>ciru+vk0X6{kDjaVOdH(QnZ^+8)b zSu^!go<{CMXGpiD2ZpPvmcK~t`UHG?%dTHyW=|ICU+ZL4^uz=yg2=7tCO;oMzsXoy z6Mc1JJ~gPuR}cVY2^8qR2K5$g(xJJl1uuha5Wj^lG~gNQgnt$`AuWJbyUlql3qr&g zWIc`aV(jOe5PLKPuS2k%uNkX!M?tC-L*y!nXKn(yJIBAK1t_iACSj=1w=nzj!X+os z@_C0NgW)<1u(r2Cz8=aBV|H_ZYW89_|G+kVVFAd`f@dfr=*1mU;&J3>k7$*H-DG}; zY}N$BK)Z9s!bMRxL<67CWUJ8q`mq4X4{N~=;PlsM%-WBq!Q{f|eTEr+E((+-j$A<$ zsT>qU+EdS+mU&d_41q`03_tXUXDbTd<`*d?Yib*rX@Rd(MRc=`e%$Uada{`cMNT}W z@o?J&x~9@Y=acP*9fOoHF<7+ZxmJA^WtX?%8v?i2fY@oEIv1T z&4ueHNs`XS!L}FUfen36sTJ&Zo{B`I zfk_2%x=&wGHAE&d30TamTBWR1K=gZ%YrOxM=j~jAq@F{_0R<*+oT!gV&QLDb=6PdTW*2yZapc zCAz<{-1HFHk}M;)ULG`VN_~p+H1dW1+%rbtg}>ndwXL1Xph_elyBc zLL>@3aVa_mV+eb|WoXGgvW6CR z(b|MtFQ;;y&gLDTQ{F5~l{CJj8qe?oIq@@jdT<7QoA5mW7y=ZuwR^uzer1yjgEYdS=W~&jSUR$9x)b9xQuHts1}s*0Ub(40VHp*6LP@5hAnUbChAOU5L6xzBdb4ox(i(^#Vz*d=g1##N~~X@Z53|K&<1p9 z_^hR_wXw-J$OkZEO28+v+N&*Vmwvy|Lj+KljfEI65a`&#WD;X!p~=YUx#Q0gWm&S?+d z;Y0004F0HNC4yUiR) z+F<^+sbTrjqL}9H#3qjSC01~NczJqHrOooO5M+Rgj}bL zKAd*GFQRt{FE&^09ElwiBJC@37d!xK7fvY3nyfK`k1C!7XkthOmo^@2^ zUnClvAP;l0%oMRiF7!bvsG_1;vRn=!zdA zc(w#lEA6wRBOwC@7vn|*a8VIP5wVD>vj9Ul4{4}%_3}&o>(X8+ki+k5nc|0_K_NN- z$9bRHgn7p-gs9p*u;p$zS)7&B_?Z{2Lk(lg7xP86nf`RbHC!3@9I(2)G8+ z7vGMgqfwtQa+O(UC-J@YhIW=|PifK*0hZNcb6DLT8?XeYd?Rh*jU!l-QDSXCWe^N} zzv!aa*4aU(i3PHOHYpUa(B)yKX@w{zi6=zA#&03OP17IZ z!KQq%0u7%HUow<0jhm3%)pO*EV%Nw!{g!MGmF!<0pIC z4MD9rZHieQTv=A8ZjFSvT^ZntADbf}yJp5u-3m#pxJMcUJ|LPzvu-ae&!{>Kmt8fU z*+j^NX!Stb&oqir!`y*i{1Y(`a>^T#+SXapSTQXD@A3Lir!Gzt?O47BOXdboo_ApR zvso5Lu&IUSoE|UY4Q1;~QQQ;bb{YCtH=}+r&JMwDi0MlN@m$!t3wvzZaf?jGGVQjd zW&4k50NCmse+67$87S)02*~Mntf}p!)093{0Pb9gBMJwR>Dj$CJxpFzM05%=tL5`z zK7N%iXs94C!upv7YRd$!cAx{x5_Dk!h!74~P!=94aGY>3_5;I)>U ziA?&Ca7ZD7#fV&#IF{89k(^W_C2?`;OS`%_tnTw5L&Y`M^uQS z4u4_j`$PBblwM5N^1>14JD{0D6-goSJwW(?wWZ>pMwRNz3O{@jR4FeP5w}?~$ah2h zy^hgAyOD?;Cr$T%-X7Gvz0WJB9G1VRz%BN6>~OFzvY8n%x4LeV!W2m<(k1^r*B{*> zj=&_8q1R#LH&nVm>AEQKG{d+!op-AsfcpUw6cB$Pz*(7P)0Ds4|Eg`&vitOE;{R?K zp(ggEx+AY}G4I*{AFSFuNs{0iKPK!O^JP>NQvYX10w#=LezFWWs>A z&w~&ilHAOiO*1R+<|5ETg*7W8QQ20e$lP>(W5;UM|Fmj@3?R$@z4iZ~4> z50Ynv_feau@3C|KUPp4CGi_Q5b}s`ZgSQ50Z^EFFGhXphHMsGsaBzGKyqO4E0yfOB z`}bFDwIr(}74BUQ&J!i(^;G)}WCUtW_^nC1(O*Zb5=Uk3RTAq_N!vk~>_mB3Efvy* z3Nd!?jEY3`E{n||+=n{dB5E~yizLJ!Rsr;W$pnm8-v+Bgv{&K= zsd-q#F*9IQgkv}DhRk?X717@b%?qV)QeY`~VsSJcb=d2fYLzL|`aDZ2IZJ+prTbM= ztjJafTv^rQ(A_Z#P{Mm?0uNbe79#CxjCLL$$Vuq;e^$?_A^O|4mjpFiD;)B-kr?KW3qVrYi%F&4*$+j!?aT?E!1-4QRPBijZ?CqrmI z)s-|3G--mXC){@i``okH=QB-}dS z({mr)(e)Txz|@Q*^S`Z)6j>P5GFvoRNd?O_15CzROpl+=py_jlYsP!sGK$byN+guX zRG7w2r_DUh`7+6SLgwq{j;SA71^)^+jaM0QsGC@?Wa4jS=ioIq;p%j2D5CGaVX;JF zt@pjj4e^2yXy(mdNYH;cENz*`-G{YF9AxdDSrv)IY!&DaOdhj>_n)r01cb*9X46JV z;?BMk>pby60d$&O3V)t=lCvcH-A1Ja8xac&)+2NM%s*CARyVR^-CsDcZHZ$~>FepT z{HmX~^g|LeOg9sT#z`TSNFrQ5Ul{*+SFO z$#2j{IHe5XgBN_fjLN1$&$#zh??k7FvOpbbz;lKv`}|rOq8VpLE@IJu ziYs)GQCCTn8^(K8nkd$OH+ddEDmaubDGX$wE;w{YTExh0L@_|S=YSrkG_7@f9+Bzu zJ?8fVj|jY}MT5ȴ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 T>fdozJ%3aF=}61>0t5RmsVd+` literal 0 HcmV?d00001 diff --git a/testing/btest/Traces/websocket/wstunnel-ssh.pcap b/testing/btest/Traces/websocket/wstunnel-ssh.pcap new file mode 100644 index 0000000000000000000000000000000000000000..29d1da9cd163a4304ef7d34d34aacbe48a95d8c9 GIT binary patch literal 44410 zcmeEu1yogC*Dei`N-15E(jAJV(jlGF-3=-!C8B@|(x7yQG$@U9cPiZtlHR?K!UylG zjB)R{_x^YM z@4I);KSLe`kc9wCV87TP zoY|XXA)M{bkbiUL0niCNuS4&BqyacW|Kbaxf?Vq);h;ujf*~Hr3ftS?6T3^=!@|GKDXvmH> z#&)^}hPNA`l!(eXc%1dg(Cd=)} zs>o_!U?iby=pyNAZO$y`eDB^r4|X!Nvxk7Ou>EtFjh(fFwZ63lFR2p?J*lpvgNe1B zshcinn^DhD*Ur$6(a=?b*-%x`RMu2NQkg~GR7zOFMDL+K__w%{n>dS%sRR#rkd3;q zIH$Oopo{E7d1o0#WpgP-c@9}cV+R>GaSnA;W_C3*V^%3eX=imsAyah~QCIbcqU`FX z;`ZWJ5O5_mHbD^FRou+lR9exTMaoRvSz3YJMMlKfQQAz9L)_HaR3ALnOx)E-p3%%q zQAR?Fh1Ee`PtegsQHaOpv9aL;)ko%{V&*RTPO=J|Zbn?3wj$;h@@U9t$lyqK03-c( z?O2h6Bi#{yHqyUUNzi;U(G6(lF!AN(kp>Y9d*Gnp0NsH7VuPr0&x!&>Y#;k0;w|Vy z%!zZv@1Ppb#6rr#%tET*Z0ewIVrpefD))Pk?EfN-KWXT)Ty*vI4Q&ARJ(4!NBc?7R z#bNeHQQkpB-k#M&TtQaCo>@ZvU#ht%4K!o}T?as<3XWFvq%7>D61rBTtjw(Jq|D5` ztem_YoTLw>70;sF3&iB_QErll%ut)NnEaZd6WzcJ9S5Skn-F*@Cf~s*v-ZJ3*#hAO z`^5%{vVPMe2%^9r5pO~7zL>v^=$(H1iGPPPP3JVOQpvPA%J&4|eJG*Q=g&AAJz4TN z+ZETuwji(AO8Iq+Ab~I^_$yqpx(yLd>=}XI!WnNqnS=s`lW^yZ05vKw%n+9*-oZmz z0=|I#VuQH!*oqQ@X!}P*KsZ0*&JnReL~mLJ1u+IzMrHHjniKh~rG8n^e%!VEY!j!#=o zjqQDk02RMopB=wlBmaKh9@S#wATZl0C>{wxBR-0#K0B^9b!gQ$F+QVl_ice^R$F%C zme;Xg@#taA=KGwM#fkl}z4z&bY4sR0?f#icead_9iH@?42djxUJKo>=zTjL(@Ysay8SA5Ux8?ayx9j7b488h7I- zV?KS|1hGRhs|zEtTMwcVJ@>=YYPP~MJb!p)4y{il_8&(l*Zm0p&1Ev7{@`8q*MjK8 zn(escx`MaGwM#y`(V+c|+C8tMs1L1s;foE2@oCkkv5PIMG5O;;t>C$vG1-4UH}cHy z!=OCRz1R=61tANKhe3-I%U;bDp3UiqFOeun)|S_|%5e5J3NRi{l&dRWVt0o0rD(G`u7xyLGW+ z*=wQg&~x`~_C)IN>SXfQ6~IP8NZw>&SmH!Z&9Uc?ko13^2jDNZ9LHT8qyGYXRQc7X zA&b$O6N$rHm*NK54*K?W`4Y#4hOMyFUwQ+549}~+6wB!Jw&P#=exFnG*K=e3b)Fx1 z-d5x#EXcOq_|!|hvOvCI{_zBJ%zj*Q$1jb6V|TW17szWjhCrU_P5gFZJ z_#mR&)riy(#HfEG&O%nd$dZ2})=$Gj837-$z<#lTQH4{rq5%{7&Rt^pp9tL|8T~-cG`hRT^ zurSrLw>M$5wlk(T)Hg7J9LQj0YVTkJ9{-O`Y~1Ys+{D4kLT_MdWMpc{U}9)tVX14y zU~FgYXv1LWqHm&WWo&q6`cDV3aDsa|SXloE!NPszUiRO}fTx0X_PUV27(lG^KR^!v z=NNzuAnmhb&b!^*dVX>N9Y%GZ;EtW(e61Qvc@;6YvC3 zISil&f8PrP{jYmj{oLN49Cja%8=d-n4%Zs z0(kt_9l*YSH878L@4-X40DHiGu|aaVS(6eZmvf$FonKe+vq(vB$^8~no{O}i5Dx&gIfs7#N_&EY6_3=b9PD8?QAA||S6$g%k# zkFS0RP=~PK!8W@*w+VwZjeDCfYKLAc4b#X7Yw7vV* zzeYvS=8+mf#ZJ|h6VZdl5C$ovp6czlBET^U|ArPgbSa4$HcD8OxQOp2} zQv|OG!p`ryPzL~Zv_QlN4MZphfHK%GHVAX0CKU*C?lZ*S%oWd=zXi1V>#iaMF~B=3 z+S!f!L(umR+qZ6+^|CM^S(dkGNb;e$i)D6rqN50GtG|yVHAph~CSIIT5X>9P0dmL! zI9ze1a|hy8_!)-_Uim@1^193cL~Q+t2xSAf1NMsz;+38iJp|GCkBGo{Q0Sc_MgegO z@TTuE4XH5r9)JP=$j9xDvxa(OX(gt+<~=F_>EAEPD*RX_S#WS7Jn zQmzz2y3s4V-d)CgyxdOdH_%~QzvMHD76d1>dy}GoY3gRO0DaR(k^}_kFdaNALx&@> zpG;2j?jW~kqDWUon*kD=w`bgbMf)};prI}>+M(s=-0A^t(IBD`?6s?+%?Lp>{39aZ zZ7BIU;tq)DO%L^A+AC1GW_E}-RIgq_mF~K4gUc62#g|4w)z8Tf_mM_C>fh6srKAUo z#1Vt`(UGn}eFkzc*e^Cn9?ol01MS8K$Ho;}&+jv$LQeLqyo&{5t>M%LKML)#Ud5PCW; zgPdpv87fy*G=rno5%zRw4_|b#XtQtC42=l$-)vi+u1m@ejqMb9k}F28DUp*3my}_C z=6qOj^on*?&%?x^NVIDh_IfVcN6L&ih48f>(%;k5o@tKnrJ80}pQ`be=&dscZZicR zSD|Bv35Rm2v6|!}KKBxM(Vd=0^b$|SzcsiO?zDpfU8XPErGmW0{e@+o&yUn)jmKP} z-(M+sD+eSGb0NO2PVgN-h~B)HJyJ4dWpJd;)47vxkxuY2?uDB`5cZIlLWPG?U9ABYo zo7GFZm-jF!*5F1UIGTLGXkL*Gm>_BjI~&d4`V?qBnU@0f`TF_!Xzl@{xeFq4*j%Yk zW(cDG9}xk4bdR1P(p(1-z3KfBvzcS6XccfHot2Y?+F&iPzEAbibD@qJ`N>K+`G#8m zJms=)pMlk@dj+vCVh`vO2GA$aFE&u0FnLYtpnbdLGyA{vIWGn1(+bq5)c14yH)zXg zu6v)tI3Uotz{aT~4lLbzZy#29{h(b_h=a^!D@zwi71m%-sx<@CwG!SaeM#0iFd8q> z`Wl8V%fuH`D6{HbShUId){U$NcFx*PNTu=Nw4cxc2F>n_<7z$o2W3u|jQ6f-3fVc2 z4is^dx}}Ib0-K9vd@W@JIirYg2dO1Lp!ryCklIdCYu>jI;+XSoK|*XZhwVW!d*I`| zk}R#UxT^i(_XCeCo@b@3%#Vc*6h5+q&o1^ev5E>qCCku9SC_agiCH1^c--{bzBqz= z&|{m)n3e+>vw<#TGc&U~sTYLVZ) zuapuxo%G$kAlHiZajJ(Cl(94kMKro^k&Hr|`}d@>^k@^N6Md+xfzNmMrcRu>Mh_xG zqlE9*-hR3Ac0bU7!MjIGQ#e-vox6WK!C4U_PQa}vorbTTu9^Zp8)4kKqO^B0u?_8Q z;Z(&9=Te>p_RrY~I4xgz0!PHvCBNM-Or@_bLxO>kk-_6d$LJjXQuD%M6p7>g0QXz{3;Da72xP4gqHq|KOFgi3Kv7^v=}$dwyCqpDca_)vV2Qu4Xzw z%{RR1@vIeZ7CSY^Yn2w?37mN03BKa_%8AIfw6Ki=h3| zkt?&k258@Y>CFDG*|hi-m`yRDeXRHA_U{1p>A-CNQZnnBr^d~d1;tTU=RivVuJ90x zmq>V7_IJ6PG<<0u*N~x!`QJCHS6f|!3I1U7L5rT>1Wibeg3*L0f?X_niZh1Or2gGT ziHv>hR|2o~DZg~Ps)v5JNAQl0R{WIbbsRa_=t-nHp&SI&gl{Q!bw6h@(}!+*5#vyw zxhD{)p4lRV>aeRVf>!8#fd7d5=^)-i>z5Os^x7XPTHHF=VkOrJKe?14=m z;Ms>xs$lAOgz3Ro3_^|iNONt6|AWa>raJ@HVN^usW^F9g6e#U%j{Jq2)(3QfP|oz( zFCI{Lr(um1Ll^PI{rKqrd08!xw^OAn1Ti!)hK^{|gpY1f0A8(dShX-%(|ukEqv!aam{5`VQ%di-vJ^iC9Nq~xb5iOKc`0T*yI zh1HO+x^`fNs7d#1G=Hlpruk%91RPCt;`7mz0!H(WH$6pY+#1I{Z9CPupKfi@qw562 z&tTpmykbriS=3I1UqTNM*v+}Oni$L~E(lp|xHOTW_<_6(_KOXYo0*$5A!;%`v;Rv? z%OZf9T0r|hZk^kg2JA}#YTEH3X58grj#8zRZ}zew&sSbv&k^)yke$0fp{})0muVM^ z_~@2>X*)y56#i{?S$XNjQGFMSj4%C_hn?Ho8sRt0ab|FB2TGW66LX&L;5psKhHsC< z?tJd7ki2XhMd-NqcEY6DA-hu^+am0NSVn(11>@~9%}1^dfr4D=#0E*QbR(Qi)wWd; zaPm^LWvlhi3Z1{SKDad#;AH#hXE>!l@#|ql#aTjj#MQx^FGf=3%$q=tK$0KZ|sjco_*@qTSzzoXOG; zpD&8s{n6HkDeYR*FbbD?`=Tk-iHYxnDXlDD`_b>OXt%x=F;%;o`z@!zenhh7kUl*p@V*99{!P@dpn|h*;(@NO0B1ahGQ>$tE4@QL%MagmJ#MS#cv+s%_pmnE76aX=V~$p)Fkaq-^f?|&aS=Z z=1ucLNB^U`D3$;pQ(iYLM!IeK&H(~FJsgwt78;aOCnxLZ7odGcJ(Mf1otm^j`;G=@ z_J65)RRB=)5AfQ_zvJ9~2w>k(2|(-~frAnQCI;9qHpnL_-X?7bqShY~f%$PC+ySXN z;k5t}13mXJi@QNw62B|&ZnHxo6?}&KJ5oP@NId|V>X)74zNancwNM2RYhGMg)AJa@ zRQ(L`x9<-iz5`5e5uG7EzyJ{62Y83YzEKEu1d*ozWFf$LuwQJD7_eJ$K)AjZ{4EY{_{P(!#WI;IN{DZR{fX)lVb+0q=EDFC+L9QbJ;@>N; zaVi_oW3;sV;d8N-T zt)d20UdfoMnX;Iui!KekX-7UN=2?HqQ%Umj@?u~P*0ipr~g5)jeo!d>SJh(H02C+Rt&CaCfM11?A5Gv)#Pj+u&S!sSy=zCS6*@PGIO6*UV$;$1!8i=49$VeP^_~k|H>-r$iNJR zWWIL%OEJ*|5y>9FUQv0KlM5mSPyZ1S$j}@QFC%)V^WD`ZF|o#Fy@qMU>LA5u3%nPA z#Wc1?CPaBA^N7w27vm1(6$R4-lLaJTFb;b~om4+5L^w`o1bz$W7!VE*aAA5#$#+h` z2N-6}|A{&&(74Ak>=kuVIeDP*fb*;3-|^&mt=K{ht|Wxo=f?d2<33=WRCBy(Nprk~ zWJeI9rihn<;9Y_W$Iqs=xY5<7C0}!pYbzE}eUCF#U(KvQ4;*DYj?P#jsL9IK^!||| zQlEIL;r(+Gej;-Pe{z0lkeka&7_QMENq_^UX}r zo{S^tsPcUz-Syc187U!QK@_Qb!A!dJ~PLt4OIms{y!!WnzAl5>lhms0W z`?6P%F2}S0xu;@Tq~;)pcBt$S5#aQ&FJ()#{zCz9P62A~uQ>nhn1A8{~jeVvQ5=@^Aay; zkS~~jqQD%JxP{dBOJm^Jo$XTv@>*pr$n$6Bzc@g)gJY7le0fa%g9YmKpYb@u)`$Z2 z{CDv%d37|+{#G%wf(e~s$luHUO z+=lA^wSs`F$bUM>7=GodwZESXvQQ?RG5)nswontt=?H>rRipknV}D?+@&gfJh2XBJ zYU(Ek5%r(^5fO+1d*?Y~EQt7@s;0(Jju7;Qt#`g+q+9{G+b2ML|9@4}|F3E~E6M`o z@ZYxh=fACEe=T16>yC?A_3S;Eb!?U4uBd9tDS%|&^QW zDN||E|C#1f7Qpg^NEvCpvjX^#{Ku*$+@hP`bk^NdTvXor*~GSgH^Z>{^1Y94dM&`` z!9ucDRu2oT1iYPh{{fK*dydvq9awB=pRYxkCjm(kyekZQh-ae%yk4qm$|N&OitKm9 z?#QvR+3gDIaSLS6$oN3B!N@K^>I&x)<;Kdy(9F_1LUOm(kontp{Xvg7Sp_b}RN_6xrT1RVH%%_(YL&RFSVm!14hOaefngHEaNlu~k)*{E}Y7N;0*fGoH4V zlCP}mIF=%F=WP~EL+nf$g;24+BPb>Zg497J1}w3c@82+IHlw){ltwDQI?4=-n|bYg zOz{mduET&<`2}sV!v+DUg!N;%E2^4u3L%R8ai)adigbRh*aZtJVM5?s32lH9s=Y&~ z28n5aew)4)@(RAE0qxvPzUp3Z|83BY@r8$sz&jdUuz=!%!4>?=`8WPbKL9&zAR=W7 z{1sJAIiDcRNB_Y*_>8$5&pC4t@rQSo-Ir=s4Ejul_;9Nl^{32PM@DS*jf9~?wKJXB zBhij=5^hi1eVhj0eETdUBFn|M1#-9#aQOQ@iGIj~PR2239DZGwK)m__@rvp)2M}>| z1^$Zf%KE7wakBg)A}}6#k!Og?z*DP$SM;}aC0Ep8t}nm-{sf!s?G%rUTt#?<5|`Q( zic0z0p?nFO;*VCjEFWL6V;6h`?T4+yUs2VRQv``>?V0`GdiVlFl?Bv;Ki;{06xwoS zWbgXc0NNP)@Nzj-A{MvpI2gyz9I=ik_n{M!MtZHYOn;_Rn$A=p(sygHS9ZmgKOcm} zM#Wa+`et;>w$%_Nt`PDGFDR{^9EwvM?kLBzg^Cup496Vd` z58MqAJ$hVZ?fXsK85?H?&l0r0dZA7kzaG&6c$U!R;HF) z`ruE@Cq6nz5k+yQNodJ0$xbp6>(pXKc2^*Ng)G7%aQuZxx>;DM*L?~7DP8dcTn*Wo zxKIU3W*%E+7s>#e>+k!d;d2BkA~#!}hq+0RSmfL=ptU@5)#s8BbV`9rp145)tH#cJ zyF08MUDT!7+sxn*kDpS~)5lf>zRPNIV#}~TVJ_EFZ4pMhv?;voDHK`yyc0y8l)jZk zRHS!PaimIn#(&4=%W{Eruf|g*yUxU~Y*yRz>jxvxpS!=8dXg+55je?-Dt)Tx2SX@+t9Gsfjb!vQ=1}9(M9iVS*p51AKfDf56U^T#hH^?kH3hx8tR_8Q zKuy7*eYYRy_5}g^0${}uwl}qna?|a&0eQ7ZZF|yuGy3ni&>|R4D_hd`-sf$YI0v zrcyM`+bC*Z;#&VBfmc4<6Eq3Vc+dM+_q((exNi=WWHj(xk3)ams!Z3EHLoNva{XrE zlf0O6r`4AZ6Z{3Vga<{pC*i$d zZ$qB9=Ll(nxk2!amzL*UGeYQ$hex1Fbkd}dyHN`Z8rrKH-ch5sdU3e#_qp@pM04fr z+58B?k^FqOA^%-S5TewUZ85BDY1Gmulyb%=xfqFAHj744k<{+7raP<&Ps(C{RADAc z(h?9N8S-OCSLC@L$GZpH7Ju{K6uyJ?O>VVpyBU99bp6M}m^oW`}}-Bc6G}J{cEMKF(JSyGwIE^-_Vz*$oX9Pa`G?QZr!{%#G(Y z?lw>b^HwpVeSQ*H&23y?wZykGgL+&yurZa?es7wQC8hg0#y*bV3O)B|N8e<3p3s2`Rk#76g9BHuu<7kk`aKyEnb+1#OTrGF><{jiqR@wuIi{}_W4!XHa3Ig z&4@R2aq4bQ7oR;q+WKHm%D5apL)%|$8}8XZV`U@#{&nv#0qK_|0WIoyp`!Uk6K0q@ zFpilHT4gKu&~=IwdQrg96c4>pO^_OR`6Kbl&VYSKKutSV5$y*F!^D$81VWt07-JmdBv{R9P0yv7 zS=TayclJm3b4r%^udyf}mkGYwkDl3>!Ki3nZ_oNFI7KkF_C4)XIQ1~d;7PP}lcZ&< z0Iy}*)>}vN;7p&~g#5NgrXnyE>@HEoWkuQrjR+F(Z9~`hq}W<8zDo#7+)CxZ@~;d< zL9BYR>awQ+mASX_WEmQvC&|PSR#fmPIlnf&EVft8Ylj>mwm4eK(^YhB?OOUsJ2BK! zOn~q=b9lwVn39)){D_RN8|k6KYg#!R`;%AHabE1zO1*WaUDMb8N`w{YJi{AZt{;uB zHNpSPHF(8xME0ihXCtV#uk~^FpC-nj)djwpWEBc}-YZSN>+9*bf{w=KzDAik?Al9+ zDc-IqjO7?Lub|Y=PNf9>#&w1eL-SLK!n*kO4lr_i->~$RKEkyg#Kl2(9LS4z!SW^ebP(9|IJ;{1W91rCjRB6a zS1YEN#^<@yyGZeye(j?Nw84GGid%|#4GuR>$i((>nS5)^gS!5umAl zF{0srVQ~L6;FU6t{Ha=g7Tt5A>TJG~)yB{uCV$2RbfuMN2L{bKa)`r+#CbDXMCCYq+-XHgWP0{V$i!2WA(0MTOr4Qc?mb^-gv2Hru2`I=J! zxpq1KotjICfJYzH&k!F^fr!Ah%ggO2qhfC1eI{Oq#KIGO_V@qf9DMK zXZ2p^Y;>}6Qk9lm&wPVG&K2B_ymrK;<9>~rscMd^6N48aK7T8*e!bME7k5Z@p7=GF zl9`EFp_9X%pFdvAklfOw$_%Qm)h0!jmPIcLXQlTPsNoFpB6n2q-h6l382z@>LB~@03-p4vvH~?AaG3{dM(rfacWhr;4{iL|R9VWV4 zN+VhHy^7q}6fC?WJk>Pvw#cZV)qu7L1I8MQ;+=c_yM#f*D| z^AD-i7Q!hzXTFchk@3JN+8arYPfSkXOZCYR*bmi?nY;u z2NKCpJP*moud3eoYz_WvcHQFNzxQfGBBv<=gn#3H?Z@;>%8s#aH2uGhB&yJxR^ zYpI;PjbUG@+<757q|h<~g{I`|7t`@0Uc>(jLus7swAa$!BHF>X&oTSr?suQP`=Rvs zsBf|gc*LmXjX4&Pb7yO>0vosbCwFT?a!Es9Ju=H3qB}R#zWSN`T$R;i%?VkCJ<8JS zbdA79`jpL=Bl;3|$7Ei2{ZH*J37Z*!s|03 zm=k!*0Ug0>w+^1XDS1w=WoQCxatdcz6uXI*_F-cK}7)lX766AUIA$D5U}_6y9qg!ka-G! zX7AUmGDidEDF--DE9*j5hhDv~o3kq!O%MYT|~bDn*yXUJv9VaS!|?SCcN}u&w#5 zKNc!gxUmN0QjUUgg?If7kZ4|bCJDR))Xf1z^C84Lx^phfwBkLEGJW>D zf$MEOq^B#4JbWr6Ql&3>O(SBU<2>Pe4K`SwKPeNx{*8RV+mvwupD;^xa)4${Q<9em zOY*_1(#WCj$p-hfk!M*NZ3O1|Jrf5$QPydpOOBo9FXiJ|x?)f%XKSnXG_)AM(m;nX zzg0G6;O60RQ<(V^oo3ZQ3MI6Hmy3Utf3T$~zu-sJI*WI2iTPK3`@HYqLZx({TDK3s zdZ(n|_qZN6(FPS74f8%4PPz{f3QUS$wIdAnpf}?#6oP@`=b!$l0$n&uNeUk_=6D!m z!tw45DpXEozzL8?KGE72gm0Zi$dxX0ZM1njlKr&j=X33~*DDN86g>BGxM!mZ$Wa14 z69se*-BU`!&6Yl-op{8%hflwLNCK$9B|9Z21m3;O;5ELD|}aW-$F9% zQAB&sib66f-0m2kJQ&;07=EpkbBxWBoKHjsj^|Y>8+HRJ8>3c6Bo zF!gMI%be#3|Gk9cD6O^$8~oP7Dlz#L`hmO$$Bv7d-EXH!UzOpkS`XPXTl+gjA5cp@ z;YxVZ!A9MzWN6#sTC#WO_|!gq?cfttedf~7b>)FzXw=(%pRDrUrkHwElomImbYr+Y z;$QrFTshUHq~g2d3TsI}MZ#m{cWny4@fnq7`kO8UC`}9Y!|OO?IQ%1w5>NItCBy={ zD<8WYxNWic^&#C5yR%^A;=}?ckx$+qukcXai_}PmbzHRIZrp}BgDbUPNPw6YS~hYp zW8;ZXRy1XUN(#LW`Mgn;6$!?>cv@alSf_h=p<8&=7SCC*eE1ayYQLpsY0M254h8!> z42KaJ*!jwOdR9UA+m+9eBT%yBH)5qiJ~3)iaeL| z8Ir|e&$80*EdKDdV!tbx=fbX?kNW-}@BqQduno*hG=R<>R((PlfafvVz((H+n5JOcQgpMzCKD zf7Qx$v=4_KR1z~x#d5_dA~rq9oRD!4$0C?872ByTaEbVQ@QqrH=?iZ4+K(9&)F<2B zE~c#|90Itu8+rriCyLc;Y-??*1%_%5ZHa>EPt&u`R6Ep1VI_5CT3Emsx}`kI#LbN`Z%gEvoX{>7tN5&A+3< z!<&{SH2lry0fm7TA)Qe{($C>e_2%yybEdDoAHNAh*d|Hrw|wy?vrK3+M3ZOQO~w1V zp`DVD^d{eQc~0KUbF|(eLTg*DMXc%VL~W>YC{hK14GGw4`xUC57?L|rSUbLivfd8s zM8@KzYN{?9z)ybpW^bG~xY?mCy*J}UCXdSIP%jKJ`@R*+$lTs4adz{<{&mD9LK!yI zZ!-~ZR^_~13vv-msT0WkY7g!=8{C~mMl2#!&Rs@2!HK;$YFL4ZD&!JgNw2tJpBXMI zz3I%8*sn*HYTf0^@UseUXcLW1@iyCC)W%rsjRyU${1W4?$?+V{dkA3PoDX=!-toeTODbaj%M{8F361P*zFjFL4_zem_ zn0N$?d=;4M>RQ@t8g!~n>i2T8ryg=g5EFZ|E~nG4@)@wxJ*b{D3988Y67GJREG#gq zJt{+jT#yG3t+9xl#)YYNO@%*P=>3pz-e>cNbT6xS39uv7I6P}>r<3H!KlZ%x7$n17 z_!jkgkmS>WVGSqrf(lzsU^4z&j^=B`p&h3f<|<Ju6p2p!Xb(=ZgECecA)kfG}n9a*JK?L<5^g=<@%aA zEAOP$sCPu#@RjSa9TXmpb2G~=eB`S&H?wBQ9eJ|JqhPbgIrqW9`&M2rS5caeY`xi} z_LOhBLcMDF*o_J7C7uOTchuRxNtW|Ap(l z`Atv8He0f6@G5x*?dXm zO-nCTyJ>OcI?f;_L>5WhuYvG#lLYy)^SaxMHLbNBaKcx+EoiUQy;-cq6ZM%x5+jIBx zHVU6{g@9R30?2ZI&zt>_XOfLW&+_K)yq!uMXFUq$&6iu3vRna(h!h9|l?NnDuwQJD z>-i7;kmoXu$^VE5VFuqzjc2p@IEB%QaxTm; z`^B?k*37nLH_UBgY#64`>u)2SJTZ0BD)M5d*`XXCpB~f?`~v&@Q_XTWU04d!^m0Ub z48bY?11r{YdYztd%YbNfx6MQWclYCp-4Tz8R5)gkKV~Kj6gn&vr2msYEL1-$nD-6p zF7qG2fkXu0&!&1Fkv<52cbKmvt2;`Gh!HhUk1QTo?&t^+(a$E~zAFA$O-rL6+Zy$# zkGix&K_^opTUPfcm#<``8^($U1!*v&lPQbM%-64I1>)F0RD>vBjBEHf-$~k?KM>wI z8KmPYeXr29~X)#^XHoz z)k`c_}wXsqeWg z6rf-kQIK@=Ct{7=!R*Wn)&U9S zvXd6vjQeY%Nrb1Hjovi#)Q56)nJSS47aU~qo+@x0zG?1f9o{B{+s$Em)9zgyLi}#a zMq|zerwB-OI}`;JzR_<;+Fyjj7sTFaMix45<`u>BF;hME6JSF465C zPxpzR>ppSl#j+IWh@Ac`&>-))mSCeWqj#+ZfkBF7SHk~}|H)7l)Ezk^snc2yIJ=+Z zBG7DPS)8SHbBMY97z_uunB~eSb)Q*=w3^t9Zg6*n#XhWzySbvK6|PpVEfytD4@0bO zu@F5eB6S2eXhY5Qb>ps2#(D<|k&z%-QvlbSZxc-io(hMW4h5sK0fF_$vbqeLI_Ph2rOk>i}ZEw>#{64Es6OnTG)` zN(|IE{<6&h-5YmaI7bC_!D}P$rij{w&u(K&^jCh4F4D40Me(V~qjRx*g#d#_vIa}# zUh$^Wtw7y((Wmg~9vS6CasA!f3AKyLWt@n$b4l=hJLK^E(=CXlR{W$+B6+cH&l;8L z@V?BmZyb2K&6zaKEbpMs-ZHpewKi@U>a}$}~yX>0F>#W*xi+uedyWDiw3g1?3mgQOEs z^%G5Sq*+BHj~5~6gqBP(Rbc4bZscUqB3o(P&{GCGRx3SWi%dn)yzB4Ku$KJFO6oqF z-kfa9T-Q*&{ngi<{3#}F(9sB4z=M(J`&nX0&v1^~UmH~}X`tS)f-`iI%x6}%D8iet z6OV+yPi5;;;vZm`PE7?rwsf4M&EsHRS%LP_-z%<*LeYpYu5j;x6z`+;{+Bn?cscKT z4+*6pvS;_dl%v}&yKB&C5@=kVT)y@`*PuX`Nd(tr{(3_t8rySSsiowvk`#FRAGq?j zIAJP339qf(Hmj)NbdnH5UXJk3UDG@%x`RSPpExV#{}l61LupsYWT9xND}(txz3_Fu zw{4>1kL4636O5)_TR#Y$hV_x%;${z?6?*7W8F&w!d3dEGjxfT`VI~}X#-YOV$o;+J zUDv`T&KaCSegZomW6W}=$5EbLHM4N!yX7`sOQrIGF?qpN&EGzLe?^Ehxi!E0^6}(} zSd{f-lLo>DT01-q=g4w3Q48AgmVUXO#9Jm>!+CfO%(?!piUF&=PEWjGfsNXh(uLI! z`WMJJh1$M2^3S}8&6V=vCAGXu{S67G`rpHSZ0trEZ;v!D?ac3(B6<>h-+^A0ANjUU z=k?iihbCsk6RwyPe3Q%3hB(e^^RAZiKopHR=uKH6!HU2B>QL z$X8e^k#g_eq8+%)ec0|^;#Qj+ll6B|L5$cs74_gCH4%IqAQe|965-vDg__=yhA@h; zQje#@r)n34PWd`FG}^-lYCj(N$YC*_ZxVuv&|MUAE}B(gA99!Du=g;Bd`?`hzN$((rxwf~ z!9UMt`+xGs*fTJH_@3XdG$aIO`#P9EVrSr>Vt~W}_KOX2z459a@&vCj+Zp1o{4w_I zEPrgB|Eh|i5pB64p|`4pq2zbGoy~C~s#kN>W?jy8p5?nwLXFcTK9rOx3H^+xt7Kw$ zkc7f{Z^Cv>#w|>2>&N&Ang1K1`xLD-Jq$#F{dTj4hzKvaSOhhsYHUAgE_!Jm<94G} zfLt2^u2*EwI*9kDXIw9MKL~pN{QNr!2vxVoOZ2aI1@wfMb zXWsKKoMlf#8^C)bZ;{m@d{n+$MR_Nv@Ts@d6is+Gk4nm|1gJPc1=;s=q z_hxBl+=#zZE=h^FW6-Mg_Pvx!ncoYI5BV{h!C2m)r$qp-D?F`-czSZi>pz~36G1!$ zT>15V4uqEth$zi)Wi4F;1o0pDiNTczKm@MKA_vbAT>(#Ryt6bXOJC}(yKD=UtP9Bn zN^$8*N4SZD8G=sd z`>D!C(qwXSd2pXV`>L*2{cdAiToSZPre6$+;99TDH3_P=po|6x!K?*i?|`$6lf zA58QRZH#VD{W4a2sW=jC+$y}A#*=DJNcSnqyA-EcMrP%CT@INMX=vVuZ}WOJlhbvr z>{#B^l(|-DcRb41M7?C5sS!IAsPNEb0Io?cDTV6hH;Z?7V~^#H*z{HJps~7$2l%=K zhKL<}UP*J9khKyXl+({eM?Y>=V0!wn#PnO&emmA`IsdEvadsS+X4W#dSO)h(sH9-PXDj5NQG*S|q0ch7*!kqt(!(A|x@`bn15l>L@qf`(M`(-e6`}a&dAEmwZJ<&~Aog zEg(5S|M|^g&l>#eR$&`WZZg%5nD^4UCH_P&?23TjRzh}v9o9xsIR7*CwGaLdP46Sj zx%ieR11H~ zSOmXfE)_b~m zb3%}qwGw$}JfF!*{{`*V>Fu?xV>*phZpkiJ=(qVym`LVX` zD2ZvmdH)RKV`VL3=gEz(r$R4vpVjQo!9C6q*7l>ciZ>lUGmtHcwr3 z?scl*tYW5p^81jV7_4Hse)6o;0IRzFG&w8HNU$lwX=hQ>`llz_)NSsA8zwzA65HR? z7!ISPM?Cw|b55uCW| z^Jt_&=UC(w{Sv3F7oxfip9*hF-Sq7q26L<+Mc3|;6r5W7?db~N+$yux;3A-o1s)sC zAwX(qczwxJ)F*|bfQ@K{ieyE;zlps=(*4%mYuv*MV*l)_LA+BWex4cUNlPn^z7=R)#ph2Sl-+mH&qcv9;FHV!wg9zRrTsK| zaS!Tc1DN>@fy{pjtcqa2*dSLq8#zso)sO#j=65WDWPV`vqd2d`G=yaSJ$IMflrPXb zVKliRh;SRT2htLuqML9klegz?KH!KFQDFDcgO7q1*j!6#!x0sHt*WYb^oA~Tz3@SU zEZ=Jw?Sat5FKvZ&<&mB7hp6|u*}uFrL9II^w)-D_op(G{|KrDz>``ebk?L9{CCbc* zD3!>nq(WArFeaKBiQ$tHeva(fVl#xZ@AZ7Y zp67kfeV=iUbH~!uHI3H&y~whQSMWRgCwlLC$rmPfo)1X4g`Yjg(P?J2xT8i~{`WJ^ zD|+dlQr3f;sARlzdbimsKeIeD7+3O%(I&3&X=~!=ON9{(tenH?LO`(Cu=t+m^E(o3 z-Tb?oqZwF>ri;1USgNs30#Bp0Wn7h&Ory(|6(c*ma5V6#*~l z7^OH-ZrF9qpGVv0w?~lBP+P_J;Ps8hG$U~f>C_V=>A!94Ya1b#mCix3h4Y^xFH?T= z;REjRZ%l<-IBqZo7oKPz0}>lB zQ?ovvgAY&2E-D zidr{xuTaHDx_IeJ@UfxqrTr z9m`d#A)`@vF4Jf)F!~6N)SEcd>ja+4})`Z`=(6 zS3ZogD$dK^rt7JZRnkF6wjFfwfw8n^0WU7O*L_ZI=E_Hti&Y0xAom;K{<}$jC9?Pp z=Q1sRjWcZ$LQRfO;!428Iw}X*c6o^spH7r=xW!exGt7ZTnbS5MzHl{7;dXE%Fvkzs;etaq2uDiJ3d*|G%>Bk7Q6wg-h114%%?72#mzV4 zxe?@^2V4@1Z&2qc`yO8%U~xaR=#)E&s|OQpZt##xd!F+Jj z!)OK?{4RMC1HbZb9rgzqFY%7&b*KeC+($K%U zr|BbSh8yI51Y8n}Rj6|x&SiFX1r{TrMMv{V+zOa@bvrMa#dj!?xXLvt5j3&aBogaE zlM0OH+GacFv9jH&j$H(m|M!hi(Swhz z^!M{d=LA7+mpUJr#cI^~%M^>Lz~WVC(E%*c##fE*!hqWc69>)t$Sl^N#PVqqL6Z<- zmEtZ26EWJG_5j_0T`h^y|4Hs%{q4moddS*0%g!W>oz0KqeVgm$Wpit6E%!&Zt&lw{ zkB@9FZbTM$<7}qItH5G8w7ATMXrm~@a2LZUXR3tF{#Nct&64$a!1ta*#&SOw?PBwr zn!*KTLjv25a-D3I>w#Qzz$KZBwW#waoXea!<-j8RACBMcnxM!LD#k$Vo%W*aj42nnA^{zQ+ zEwxkK8iQZ4zjolXEQCn0Tw?)i>opSwuTJDQyQcA9X;u|B6ri)Q>Bt$DD@2v zx^7}v|A3}(){)Hle^su$)uk0hKrT4GBD44&b?(NwOpBj^#UIe3O~53s228XFoyk%I z7ESOy@rBk-71g>h(Y^;yWH$MMHwhx{>GA}7cux&1m?d5~iNeqUDn z2Jpph+sIca`tkg01xm~gwoeOg>PBX*+->fYF*MB%c{8X?qy@;LTc;oSHF&%AAv0sK}(X^SC71P;Vu8?t@#-AW)0+w ze}+*@J(%bbK9lUYU=x}Fl6c~{x8`HGw_)PQ&r=_G)5A>9_Z07Rk-sB$zF^SbA!RAW zG^4vkNlwHvHld^=+R7};*(NpTkix3Klp2d`T3UgXN_+b}pMO}u>bFWYXWyT|-=W)C zdtTMLEPcGovfZZQoTBY;#2z@#JTOj@<{waV;&})YnxiYP*1<$`I8Ob8An{2P>1w+H zCEmiDPf7%HpO=Owde4N37%j6<)q0l27>o*U6?mj#s*dj6v@0l27mgv<_YKJep@48dE z!$LbW+3acfCbI^5lLN2coxQ`hR{F|PMv!|Ca7ipSqR#D;JNwOxGi~97^JjO|B(4Zd zRKLSXw)bj6iKWvff+nYb5Q*iWi3mnprk7(QVs(@2s8_J>-~PEpSx5ZWOFEC~8%jhx zTjp6a#Op}4)eFt^fox}PF0$_cePq!TXOCNK;RF^np+&F11RLC|;$4nWV$a$1?}$#Y z44i-O&T)&7rU#cSiDssjC6-K^2%0Q(!V`TsL6dhFP1Ez*){@|Dzixpvko81T30B zi{`D9xS}vIJBFKVF1Dh?|E5?RPXtY}dWb|N&_ooYNx8&rPC2}T{)?XRP)@bZ)!)ar zvMj@_Y42R>NN3YXHRI}+0f&wy+>mWuLuzpgvbYmxGcB3`i%HPpwi5)~0I-!XN>#|> zka`Oxvy+M?g5j@dv8}TkZ|3jUUj6mguv5sY6&CdUGBn7I^W`C%i*2ZL6V7E?Oad0s zz0BqU1osHw8oj$`@*-&TdWhSx%NUK#1y*Z^5H#@S4Z;67@<+t6Zj9nt0qVBf$f zzOGS~f~(R5_6m5X*h@y6`hMdP&6%|dvlO`4P+@UF$abf*Fyy`mT#`?82kK1xA8n?^ z+rVNPoQtM?lekY{qT3Pyvbora60hS;CMALmRxKrQP2cvD4=i2$? zZ|VUtn|awOvG4uH&Z`6urzaNV?M$H#E38*872eO2biO$WvgPVH$t-R|7N`7nspXN_ zMgQ5*;>mJ?T_D5oKaWwgly>=3WU79)1f}m*imtE`sh(N4DTCcR#cRbbPygTHk<0n4 zAlDsmN#RdOuv;S;h@iUx@hSW*i0+`s`Px|fh2_;^eHW4(5!H7ig93KAyjCL;R znTvzo`>gkCUtIRmFCARdo1(PHO;2#J#b@2Z@`2J=tAr}k!CR4#ZM%VVzqlP)G{o7= zx%e4aRD%}99^>o)@U$BLpBUxu`Luhvt_Q$i^|;am3m4xFwI8nwgiv*Rxvfk zy&|jyaxVieiN(*Ta}CZNpNsx#ai$+-D zLpB!;k;UzJn{kUDX9J6N&|+B|!QKtndKl$Ev!d+Xo4lrv*PNkWli&%6t$FF(yI!#3 zz?JI$AO3L~NqS!wL9QX-l346To!{YHrbRnoQ3+by7CVW%8zy!Y@RRKq`%q%ew27d} z1#LVr5S#@J*bU}l>OP5CZ=T;ZNc)qUP)NDLl5NNyk~&AI|JJfAnx{rjTKOI7J-V)T z4P*~zT@#LS{z za#Q4at2$HHaPY>My8>JLOUPZpNxEPBiaNi=xlD_3z@injXkbHhhTKgsaq&7qGK>8v zF?-rX(8LU!ZWuof3`~TH7|oNj^*AfPdO~^ZgNDZ{=e`WjnD24uj!jtP-`YeS`Uu^} zJH7ss7WTm&u-i6|Unhj;QzyRAk@lWyBV=(a&SqM)0v1c5MRjR{eFd-+G0O1@p17tU z4|c7ieg>TLMO97~Y+j>MKhj)u`$EjfuIn5HXJ@a6T=6aA|N8;X$B8e*Rm>kionPZz zro~cVF&tXlLYyZFgxo7Ial>}fD?@`Qamsmp|M5i7gi4$hrn^~~$vO|;mItIIO^6FZJqfIBa} zou5K(J>dSkN&Yuvu@dJpw|5V)n1h;#PvR!Q#CA>6Gi~2dV#c(Ipou7P^h*V2Ua3hK ztz0}%<43!I{ixH8IY=teu4VD{*#|3T?VoW!{z|1|x>Cp-A%hy_ zNH?0N%H6i^Hh%lxvCsLcroHXmQihn9E+rrpv+2;1S4Oj2%zd-p`Um4Av8Plm!|K)ZA>(4;Q z4pk;ylbIrm`co{v0v0zwiU_{e&K)mGH#3gP%XS*rh2Q# zhxI>OPm>ilO;is;Za(0WSp11PPuWuhYycL&LyNig_xsLvN5dW? z3Gny14T}E?K#BD+BHE+V7K|zZgXpiDy+ZEhfa7wu->nZ6Z%_ z=gBaF`7oNI4CjITz7(5pkL)Z572fjjaF!bQj%sh3AuPt0?ED9? zNi6O{ot-9kZWRG@;uf^HkyslAL-stFxTu75&AJ;UZk#p|w5fedv@wN=7=w7U%2>2ld9AesJ-*TuD7;t%so?~SysA1W3MmdzP?AgPXCQ8p2N9Ji?@J9ZD_GB zi{P3At|>=EdU}gU-I3fo zg>!htQsgeJglu1aezJAWUSt~mD>cYxZaZ)#Bv25}n|;K&hTvYn{(w=ET82wMoWsOi zKVMM{7dO7Y?!O_ya8}=az7jtR^W>cb53onbL+)|F{r8;7|BE``pWHc65Lmnp=ZuyH z(HU}k9YA8P;rKm7V3|MZHGUrvodqSPPMZjtDBr;oL%`8Na4$xi6<^ddM^?n@lk&Na zj5X1J*iX}(q=p-|%gHV~lXu32&B>rO&XC^323M9}0{1JMLLlPe?_qa9yUn#suc zA~r8gdj2A|(7(4@lNUTb>V8&E#pXcvk{5OUL!x7IyY4qaHdT?8Y%cCY7B}E*ro~!d z(H2^i^dQ)kfPE9ATvSrdzRn)?J5uwocW!66h=}yP2GgRir%KFzYAHu=UV7Eowgqyz zzK}f`gFXXwzJqg_7HxsWF*p}DfNd~-Tp98ka4TV=D+}40o6e3Blcr4sO?)Nr#L!1D z5u@e03+?gncP>;HNJtEK?d&zytX}D?@yV;&}&AlI4 zRL9v&i(|kddVcF*3c+rXVT3-yD8}?n_b;%Ah~J&#W3AzzF;c|3+-RVC?-}{?T3+XV z{`F}s>skxBvmD6Q-1M2Ka~aNMS_IF%Zd(p5s(}Noaf_jli?&Nu9NB7-&VdpWrcDGE zuczaQVXiO{qp`4R&Mvv2xK3cXqQQP&HtYPmfg*a;Uv`=TCv!(d*l*4cTcn`9?|1`j zQ~#cH&3yn_RKwZh7TcBsizRR_8Ws_3a6}pAicwUTewxJ^CgtS)B4Ln&w{cnG{ncH? z3VO?=*?HVIJ?OreRd^@>as}$i*4%VX)R}>EnHEcc#VydH3b7&ygWOP<==_*$eMRR& ziBp~q9XOr{nvAp)O~BK_!a^~cx^hipnzK8ld4u~*cSDItN%prTawWH~9z0&RrD~y6 zhk3lCv*Ph=JIG$RfOIuvjV!9-ZI~9f0E-`?#j{d48xsQTKj5w(7Dv0g9O>Oh=2c-W zpXY7P?q=8NOyAg|)~926-zVMnOIT?>zM}J>#1qpdf+oDOMB-K%1}22jHrVJWuyN$Qy)v7R7yDi0{&S$PP;IZ%@AHfr z&D1wknW)^Y1MB_hGLZdUjr97E4YH_$vzZp%#V%5Lp~Z{Dei7RT*jq75nwv*d{`JA` z2QSrD(?a*|4v4nDt7FEEove6Tr|Gnk-9eXM7;@LBldW{h|c1M9Z9eqQ#nA4A~|Eq~}3xkws;k&9s;dEUtqVzpxSP1u~3qaQDI5 zGNE5`LaEV3s+-n+IPp;I!7Tll+F3Cou?bP?#>=xl3@<2+gj{dHC7Fx-s57{4pM@2^ znHJXpi|80*9b*!A0ZiNxKz5alE`SnarcDG*a*3np@U@`H0&pHwzQ2m^_sl`+mj&z#NGuAX z&V@LaX>mWWi0*_@B=%zA;5V<~x-ijmDd~6gER=Y3+C*ScQxtC!Q4A9?+Lvf7$cD|Z zUG0vQ?cc#55iP|n#&%0KWd@c=`t>K|$)?L6a5z&a#|_yHPNd(_cF5v-oXxZd?u4Pn zz`3YToP&)3_moBygQLV*m9mu$3->(^Nq4CaZRkywzjvj%Yi57kGd9g5hP4vM*7XQN zZXe*1Se%VI7vNl`#h5tLc629<{7s@WOp!pq&>2@ z4sSDVu^rqALoI?98wUw?vJ4|C3>>?3W@NgVL^h?QrBdhJ7dVvE{ZQHH@1v-(&#EhY zsvB~TeE%2?xfXy+Vo?Zn&d0gT&PBi?x)Wv<@fnDM++>&-agFp7Jr^a0Pn!sujG5z2 zj!MBqj8?Ik+F#zUk>JTanqpQ_;rJ$Iqmu1@1-kR*t?n=0$Ap#^7A$P)`tTXDzyA>+ zdv=clWZM&W$TBU0J7K7e(4r+A6#9R_N7ZE*N2M^ALt&ox{S+=RO4_@W&mC9KJjGtp zXF1n7-mt3XZ@T`~m}r_%*YX14tYWT1Z#QMPpW@H5yxg$+&cX+4M@zVkcJpV{Q<~JJ$kJ&C1r#1NX|&oHc*da+f#7 zT_e~U=is;0%I-JPT7$KN)V7Uf|7zH6DnjVu`|WY7LUdIr)7 zwV84kEo>9S58EsNhw0<<2C@@uVB#Nb(ld~UP@+8EhB*_*6G0p5DI$>y6ERA`*3h8p z*Zp7fc~&ky-&>rNbK}C{V+Gk~MIwAXjN7yKzu!Dp7jk7blAQ&l3nPm;ICuQ>90Y!0 z-(HTI%$dYRZtPc(-AzPah!TURO$1Huj1h?=pa~VDm0U8Zb=;Zk<$JR#Cav{2hr~~D z(Io~&2KDkOUd1|ce6)m|A7Xd?Jz$#>Kho8rGqSh_XEQC9gYBXOKhNn_I6ILaut&i4 zUeygHzkU{LB_!?NVz0L7;o|whJ1U;msB;Id@bdYS{A-4i)oI8b16-25;v&>}%GKE* zC19}&TAW85twC-gKTO>7mh`(t1SL}OCd}^ejR#zNf{>?F>o zCI$hvJw}oG)M8uJ`d!D@?r1`Ebnkka_?r76-&w+~)){n~>L$GAFFLXXa@_%!#NuMq zdCI;g$U4rn1AVII5^H_L4T6b!%B0^dq9`!{Z^E=Vo(P&exjiQh^F2ItuO_dqMW5kbtYmqS(PM#(ZcOJfWUsB8LH3+gH)K%) zXEQA-0E_=Yi}GJ^_9^hR?4({yRwszte8pZzowiHoFN^L`;;PiP>tcV&m-4wP(Vpwr zonZftd?BqH%1X3{?-xV91K^X)NpaNu63%DZ1iwS?u!c5g5ocB*{}dZcRGh)iGR(q) z-rx`MLg#9`tKDIuJ#ih7*~l7i6!(>A1ojoDz!S1r3WkFIb5IU_e1_kt{6pCv6Y<0$ zmz!qERdd4cZ7JL&+>$%gu%P7XJC55AUHen6XQz9|OP?9>jt;97pA*k;k6W{B^EsL4 z1L4xC5BZja#Xk{fIC(ov`HqCFlGX#x&*HZiy{L>-di+SljM@<;zgD|Qw{=NJLf^Kg5M@t;D?DyGs*7mr!PZ^-v56~fkZG< zx{2@PQ_w5eW2(K9myAeUsMUTCCK?#4t;(Or>GpHIv!;RpbU$_At( zl|Um+!Mih^g5MyGkM#ILJn`8H8Ab_5OsJ8I(HfY05ag1K^e`Iflsg{LNC)87&nTIg z`|Lz8NSqxogCzp&8Q^>33+*F(=#prp#A=y2(gA#=b6WAldKDSQvlCcZ3V%A)dx=x= z;TlF#=scas%Qy8dRG0M0rLQh-9kDyGT=+>|>^s>xeG4+*)gMiKUT1hHKJ_X8k|p2R zhx1SI9!!H{k^*BQX?X;-oN`AZY#EHcSHeXnj;US+CaQo$15gjXC%#Zib-EO4nK*fS z1S{Yzd5JYIY+0`ojcpRJWz*|~$(rP2^+scza^D{ss~X%Q;l%2xUKJ+8#1|k@4-68% zC%({Fm8*SF;*|SPn2F%?_~U==QDQXPjBc3PNIFisG%_&d>Sr*xXSH)-=v**PUcxC% GRsBCjk12%! literal 0 HcmV?d00001 diff --git a/testing/btest/scripts/base/protocols/websocket/broker-websocket.zeek b/testing/btest/scripts/base/protocols/websocket/broker-websocket.zeek new file mode 100644 index 0000000000..bd66b7ce7e --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/broker-websocket.zeek @@ -0,0 +1,13 @@ +# @TEST-DOC: Test Broker WebSocket traffic. +# +# @TEST-EXEC: zeek -b -r $TRACES/websocket/broker-websocket.pcap %INPUT +# +# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut + +# @TEST-EXEC: btest-diff conn.log.cut +# @TEST-EXEC: btest-diff websocket.log +# @TEST-EXEC: test ! -f analyzer.log +# @TEST-EXEC: test ! -f weird.log + +@load base/protocols/conn +@load base/protocols/websocket diff --git a/testing/btest/scripts/base/protocols/websocket/events.zeek b/testing/btest/scripts/base/protocols/websocket/events.zeek new file mode 100644 index 0000000000..27dc66d9eb --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/events.zeek @@ -0,0 +1,38 @@ +# @TEST-DOC: Test WebSocket events. +# +# @TEST-EXEC: echo "jupyter-websocket.pcap" >>out +# @TEST-EXEC: zeek -b -r $TRACES/websocket/jupyter-websocket.pcap %INPUT >>out +# @TEST-EXEC: echo "wstunnel-http.pcap" >>out +# @TEST-EXEC: zeek -b -r $TRACES/websocket/wstunnel-http.pcap %INPUT >>out +# @TEST-EXEC: echo "broker-websocket.pcap" >>out +# @TEST-EXEC: zeek -b -r $TRACES//websocket/broker-websocket.pcap %INPUT >>out +# @TEST-EXEC: btest-diff out +# @TEST-EXEC: test ! -f analyzer.log +# @TEST-EXEC: test ! -f weird.log + +@load base/protocols/websocket + +event websocket_handshake(c: connection, aid: count) + { + print "websocket_handshake", c$uid, aid, c$websocket; + } + +event websocket_message(c: connection, is_orig: bool, opcode: count) + { + print "websocket_message", c$uid, is_orig, "opcode", WebSocket::opcodes[opcode]; + } + +event websocket_frame(c: connection, is_orig: bool, fin: bool, rsv: count, opcode: count, payload_len: count) + { + print "websocket_frame", c$uid, is_orig, "fin", fin, "rsv", rsv, "opcode", WebSocket::opcodes[opcode], "payload_len", payload_len; + } + +event websocket_frame_data(c: connection, is_orig: bool, data: string) + { + print "websocket_frame_data", c$uid, is_orig, "len", |data|, "data", data[:120]; + } + +event websocket_close(c: connection, is_orig: bool, status: count, reason: string) + { + print "websocket_close", c$uid, is_orig, "status", status, "reason", reason; + } diff --git a/testing/btest/scripts/base/protocols/websocket/jupyter-websocket.zeek b/testing/btest/scripts/base/protocols/websocket/jupyter-websocket.zeek new file mode 100644 index 0000000000..d7a62476ae --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/jupyter-websocket.zeek @@ -0,0 +1,13 @@ +# @TEST-DOC: Testing Jupyter WebSocket traffic. +# +# @TEST-EXEC: zeek -b -r $TRACES/websocket/jupyter-websocket.pcap %INPUT +# +# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut +# +# @TEST-EXEC: btest-diff conn.log.cut +# @TEST-EXEC: btest-diff websocket.log +# @TEST-EXEC: test ! -f analyzer.log +# @TEST-EXEC: test ! -f weird.log + +@load base/protocols/conn +@load base/protocols/websocket diff --git a/testing/btest/scripts/base/protocols/websocket/wstunnel-http.zeek b/testing/btest/scripts/base/protocols/websocket/wstunnel-http.zeek new file mode 100644 index 0000000000..5d676868a0 --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/wstunnel-http.zeek @@ -0,0 +1,16 @@ +# @TEST-DOC: Test HTTP connection tunneled within WebSocket using wstunnel. Seems something in the HTTP scripts gets confused :-/ +# +# @TEST-EXEC: zeek -b -r $TRACES/websocket/wstunnel-http.pcap %INPUT +# +# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut +# @TEST-EXEC: zeek-cut -m ts uid host uri status_code user_agent < http.log > http.log.cut + +# @TEST-EXEC: btest-diff conn.log.cut +# @TEST-EXEC: btest-diff http.log.cut +# @TEST-EXEC: btest-diff websocket.log +# @TEST-EXEC: test ! -f analyzer.log +# @TEST-EXEC: test ! -f weird.log + +@load base/protocols/conn +@load base/protocols/ssh +@load base/protocols/websocket diff --git a/testing/btest/scripts/base/protocols/websocket/wstunnel-https.zeek b/testing/btest/scripts/base/protocols/websocket/wstunnel-https.zeek new file mode 100644 index 0000000000..3212023b87 --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/wstunnel-https.zeek @@ -0,0 +1,16 @@ +# @TEST-DOC: Test SSH connection tunneled within WebSocket using wstunnel. +# +# @TEST-EXEC: zeek -b -r $TRACES/websocket/wstunnel-https.pcap %INPUT +# +# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut +# @TEST-EXEC: zeek-cut -m ts uid version server_name ssl_history < ssl.log > ssl.log.cut + +# @TEST-EXEC: btest-diff conn.log.cut +# @TEST-EXEC: btest-diff ssl.log.cut +# @TEST-EXEC: btest-diff websocket.log +# @TEST-EXEC: test ! -f analyzer.log +# @TEST-EXEC: test ! -f weird.log + +@load base/protocols/conn +@load base/protocols/ssl +@load base/protocols/websocket diff --git a/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure-wrong.zeek b/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure-wrong.zeek new file mode 100644 index 0000000000..bbcba7683a --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure-wrong.zeek @@ -0,0 +1,21 @@ +# @TEST-DOC: Test SSH connection tunneled within WebSocket using wstunnel, attaches HTTP analyzer instead of SSH. +# +# @TEST-EXEC: zeek -b -r $TRACES/websocket/wstunnel-ssh.pcap %INPUT +# +# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut + +# @TEST-EXEC: btest-diff conn.log.cut +# @TEST-EXEC: btest-diff websocket.log +# @TEST-EXEC: test ! -f ssh.log +# @TEST-EXEC: test ! -f analyzer.log + +@load base/protocols/conn +@load base/protocols/http +@load base/protocols/ssh +@load base/protocols/websocket + +hook WebSocket::configure_analyzer(c: connection, aid: count, config: WebSocket::AnalyzerConfig) + { + print "WebSocket::configure_analyzer", c$uid, aid; + config$analyzer = Analyzer::ANALYZER_HTTP; # this is obviously wrong :-) + } diff --git a/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure.zeek b/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure.zeek new file mode 100644 index 0000000000..560e9694a8 --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure.zeek @@ -0,0 +1,22 @@ +# @TEST-DOC: Test SSH connection tunneled within WebSocket using wstunnel, configure SSH analyzer via hook explicitly. +# +# @TEST-EXEC: zeek -b -r $TRACES/websocket/wstunnel-ssh.pcap %INPUT +# +# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut +# @TEST-EXEC: zeek-cut -m ts uid client server auth_success auth_attempts kex_alg host_key_alg < ssh.log > ssh.log.cut + +# @TEST-EXEC: btest-diff conn.log.cut +# @TEST-EXEC: btest-diff ssh.log.cut +# @TEST-EXEC: btest-diff websocket.log +# @TEST-EXEC: test ! -f analyzer.log +# @TEST-EXEC: test ! -f weird.log + +@load base/protocols/conn +@load base/protocols/ssh +@load base/protocols/websocket + +hook WebSocket::configure_analyzer(c: connection, aid: count, config: WebSocket::AnalyzerConfig) + { + print "WebSocket::configure_analyzer", c$uid, aid; + config$analyzer = Analyzer::ANALYZER_SSH; + } diff --git a/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh.zeek b/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh.zeek new file mode 100644 index 0000000000..02f445ed6c --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh.zeek @@ -0,0 +1,16 @@ +# @TEST-DOC: Test SSH connection tunneled within WebSocket using wstunnel. +# +# @TEST-EXEC: zeek -b -r $TRACES/websocket/wstunnel-ssh.pcap %INPUT +# +# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut +# @TEST-EXEC: zeek-cut -m ts uid client server auth_success auth_attempts kex_alg host_key_alg < ssh.log > ssh.log.cut + +# @TEST-EXEC: btest-diff conn.log.cut +# @TEST-EXEC: btest-diff ssh.log.cut +# @TEST-EXEC: btest-diff websocket.log +# @TEST-EXEC: test ! -f analyzer.log +# @TEST-EXEC: test ! -f weird.log + +@load base/protocols/conn +@load base/protocols/ssh +@load base/protocols/websocket From 37521f58e58a12f462049eb12f2bdd34cab4912a Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Sun, 14 Jan 2024 18:26:50 +0100 Subject: [PATCH 03/13] btest/http: Explain switching-protocols test change as comment DPD enables HTTP based on the content of the WebSocket frames. However, it's not HTTP, the protocol is x-kaazing-handshake and the server sends some form of status/acknowledge to the client first, so the HTTP and the HTTP analyzer receives that as the first bytes of the response and bails, oh well. --- .../.stdout | 1 + .../websocket.log | 11 +++++++++ .../http/101-switching-protocols.zeek | 24 ++++++++++++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 testing/btest/Baseline/scripts.base.protocols.http.101-switching-protocols/websocket.log diff --git a/testing/btest/Baseline/scripts.base.protocols.http.101-switching-protocols/.stdout b/testing/btest/Baseline/scripts.base.protocols.http.101-switching-protocols/.stdout index 3ad275db0c..59953c34b1 100644 --- a/testing/btest/Baseline/scripts.base.protocols.http.101-switching-protocols/.stdout +++ b/testing/btest/Baseline/scripts.base.protocols.http.101-switching-protocols/.stdout @@ -1,2 +1,3 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +WebSocket::configure_analyzer, CHhAvVGS1DHFjwGM9, 7, x-kaazing-handshake Connection upgraded to websocket diff --git a/testing/btest/Baseline/scripts.base.protocols.http.101-switching-protocols/websocket.log b/testing/btest/Baseline/scripts.base.protocols.http.101-switching-protocols/websocket.log new file mode 100644 index 0000000000..6e576ea12a --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.http.101-switching-protocols/websocket.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path websocket +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p host uri user_agent subprotocol client_protocols server_extensions client_extensions +#types time string addr port addr port string string string string vector[string] vector[string] vector[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 192.168.0.5 50798 54.148.114.85 80 sandbox.kaazing.net /echo?.kl=Y Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0 x-kaazing-handshake x-kaazing-handshake - permessage-deflate +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/scripts/base/protocols/http/101-switching-protocols.zeek b/testing/btest/scripts/base/protocols/http/101-switching-protocols.zeek index e8ec4ff491..5a7231ac30 100644 --- a/testing/btest/scripts/base/protocols/http/101-switching-protocols.zeek +++ b/testing/btest/scripts/base/protocols/http/101-switching-protocols.zeek @@ -1,13 +1,31 @@ -# This tests that the HTTP analyzer does not generate a dpd error as a -# result of seeing an upgraded connection. +# This tests that the HTTP analyzer upgrades to the WebSocket analyzer. +# +# Further, we implement a WebSocket::configure_analyzer() hook to prevent +# DPD on the inner connection. # # @TEST-EXEC: zeek -r $TRACES/http/websocket.pcap %INPUT -# @TEST-EXEC: test ! -f dpd.log # @TEST-EXEC: test ! -f weird.log +# @TEST-EXEC: test ! -f dpd.log # @TEST-EXEC: btest-diff http.log +# @TEST-EXEC: btest-diff websocket.log # @TEST-EXEC: btest-diff .stdout event http_connection_upgrade(c: connection, protocol: string) { print fmt("Connection upgraded to %s", protocol); } + +hook WebSocket::configure_analyzer(c: connection, aid: count, config: WebSocket::AnalyzerConfig) + { + if ( ! config?$subprotocol ) + return; + + print "WebSocket::configure_analyzer", c$uid, aid, config$subprotocol; + if ( config$subprotocol == "x-kaazing-handshake" ) + # The originator's WebSocket frames match HTTP, so DPD would + # enable HTTP for the frame's payload, but the responder's frames + # contain some ack/status junk just before HTTP response that + # trigger a violation. Disable DPD for to prevent a dpd.log + # entry. + config$use_dpd = F; + } From 7967ef993b98ad9cd6e7846b908f376a6de9149f Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Mon, 15 Jan 2024 15:39:51 +0100 Subject: [PATCH 04/13] HTTP: Drain event queue after instantiating upgrade analyzer With configurability through script-land comes the draw back that we actually need to execute event handlers in the middle of the parsing process: This might not be the best model, but the script-side configurability it enables is kind of nice. This explicit call only matters here when the HTTP reply is directly followed by some WebSocket message data within the same network packet, otherwise the queue is drained once the packet has been completely processed anyhow. --- src/analyzer/protocol/http/HTTP.cc | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/analyzer/protocol/http/HTTP.cc b/src/analyzer/protocol/http/HTTP.cc index bd252e837d..3635e020e4 100644 --- a/src/analyzer/protocol/http/HTTP.cc +++ b/src/analyzer/protocol/http/HTTP.cc @@ -1344,8 +1344,24 @@ void HTTP_Analyzer::HTTP_Upgrade() { upgrade_protocol_val->CheckString()); auto analyzer_tag = analyzer_mgr->GetComponentTag(analyzer_tag_val.get()); auto* analyzer = analyzer_mgr->InstantiateAnalyzer(analyzer_tag, Conn()); - if ( analyzer ) + if ( analyzer ) { AddChildAnalyzer(analyzer); + + // The analyzer's Init() may have scheduled an event for analyzer configuration. + // Drain the event queue now to process it. This further ensures that other + // events already in the event queue (http_reply, http_header, ...) are drained + // as well and accessible when the configuration runs. + // + // Don't just copy this code into a new analyzer, there might be better and more + // more general approaches. + // + // Alternative proposal from Robin: + // + // Collect all HTTP headers (pattern/names configurable by script land) + // and forward the collected headers to the analyzer via a custom + // configuration method or some in-band channel. + event_mgr.Drain(); + } } else { DBG_LOG(DBG_ANALYZER, "No mapping for %s in HTTP::upgrade_analyzers, using PIA instead", From 2b9776adca4a2f9dbec15d4d6589564fb4b132a0 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Thu, 18 Jan 2024 18:42:42 +0100 Subject: [PATCH 05/13] ContentLine: Add GetDeliverStreamRemainingLength() accessor Helper to get information from the ContentLine analyzer about bytes still pending to be delivered. In certain cases this can be a signal for weirdness. --- src/analyzer/protocol/tcp/ContentLine.cc | 3 +++ src/analyzer/protocol/tcp/ContentLine.h | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/analyzer/protocol/tcp/ContentLine.cc b/src/analyzer/protocol/tcp/ContentLine.cc index 1bbc730db8..5544338434 100644 --- a/src/analyzer/protocol/tcp/ContentLine.cc +++ b/src/analyzer/protocol/tcp/ContentLine.cc @@ -30,6 +30,7 @@ void ContentLine_Analyzer::InitState() { delivery_length = -1; is_plain = false; suppress_weirds = false; + deliver_stream_remaining_length = 0; InitBuffer(0); } @@ -149,6 +150,7 @@ void ContentLine_Analyzer::DoDeliver(int len, const u_char* data) { plain_delivery_length -= deliver_plain; is_plain = true; + deliver_stream_remaining_length = len - deliver_plain; ForwardStream(deliver_plain, data, IsOrig()); is_plain = false; @@ -207,6 +209,7 @@ int ContentLine_Analyzer::DoDeliverOnce(int len, const u_char* data) { int seq_len = data + 1 - data_start; \ seq_delivered_in_lines = seq + seq_len; \ last_char = c; \ + deliver_stream_remaining_length = len - 1; \ ForwardStream(offset, buf, IsOrig()); \ offset = 0; \ return seq_len; \ diff --git a/src/analyzer/protocol/tcp/ContentLine.h b/src/analyzer/protocol/tcp/ContentLine.h index 5572e76535..4d473a5f84 100644 --- a/src/analyzer/protocol/tcp/ContentLine.h +++ b/src/analyzer/protocol/tcp/ContentLine.h @@ -47,6 +47,11 @@ public: int64_t GetPlainDeliveryLength() const { return plain_delivery_length; } bool IsPlainDelivery() { return is_plain; } + // Helper to check how many bytes are still in-flight for the + // current DeliverStream() invocation. This can be called + // by the parent during its DeliverStream() invocation. + int GetDeliverStreamRemainingLength() const { return deliver_stream_remaining_length; } + // Skip bytes after this line. // Can be used to skip HTTP data for performance considerations. void SkipBytesAfterThisLine(int64_t length); @@ -107,6 +112,8 @@ protected: // Whether to skip partial conns. bool skip_partial; + + int deliver_stream_remaining_length; }; } // namespace zeek::analyzer::tcp From de836ab52878e3676f6775a870c031e0780bc084 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Thu, 18 Jan 2024 21:14:01 +0100 Subject: [PATCH 06/13] HTTP/Upgrade: Weird when more data is available After an HTTP upgrade to another protocol, create a weird if the packet that contains the HTTP reply *also* contains some additional data belonging to the upgraded to protocol already. --- src/analyzer/protocol/http/HTTP.cc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/analyzer/protocol/http/HTTP.cc b/src/analyzer/protocol/http/HTTP.cc index 3635e020e4..aee7fda3d8 100644 --- a/src/analyzer/protocol/http/HTTP.cc +++ b/src/analyzer/protocol/http/HTTP.cc @@ -1325,6 +1325,21 @@ void HTTP_Analyzer::ReplyMade(bool interrupted, const char* msg) { void HTTP_Analyzer::HTTP_Upgrade() { // Upgraded connection that switches immediately - e.g. websocket. + int remaining_in_content_line = content_line_resp->GetDeliverStreamRemainingLength(); + + if ( remaining_in_content_line > 0 ) { + // We've seen a complete HTTP response for an upgrade request and there's + // more data buffered in the ContentLine analyzer. This means the next + // protocol's data is in the same packet as the HTTP reply. Log a weird + // as this seems not very likely to happen in the wild. + const char* addl = zeek::util::fmt("%d", remaining_in_content_line); + Weird("protocol_data_with_HTTP_upgrade_reply", addl); + + // Switch the ContentLine analyzer to deliver anything remaining in + // plain mode so it can be forwarded to the upgrade analyzer. + content_line_resp->SetPlainDelivery(remaining_in_content_line); + } + // Lookup an analyzer tag in the HTTP::upgrade_analyzer table. static const auto& upgrade_analyzers = id::find_val("HTTP::upgrade_analyzers"); From 4d81389df0b94712704a485ae9e02da495fde267 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Thu, 18 Jan 2024 21:23:56 +0100 Subject: [PATCH 07/13] HTTP/CONNECT: Also weird on extra data in reply --- src/analyzer/protocol/http/HTTP.cc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/analyzer/protocol/http/HTTP.cc b/src/analyzer/protocol/http/HTTP.cc index aee7fda3d8..e9c0ff4a2d 100644 --- a/src/analyzer/protocol/http/HTTP.cc +++ b/src/analyzer/protocol/http/HTTP.cc @@ -949,6 +949,19 @@ void HTTP_Analyzer::DeliverStream(int len, const u_char* data, bool is_orig) { pia->FirstPacket(true, nullptr); pia->FirstPacket(false, nullptr); + int remaining_in_content_line = content_line_resp->GetDeliverStreamRemainingLength(); + if ( remaining_in_content_line > 0 ) { + // If there's immediately data following the empty line + // of a successful CONNECT reply, that's at least curious. + // Further, switch the responder's ContentLine analyzer + // into plain delivery mode so anything left is sent to + // PIA unaltered. + const char* addl = zeek::util::fmt("%d", remaining_in_content_line); + Weird("protocol_data_with_HTTP_CONNECT_reply", addl); + content_line_resp->SetPlainDelivery(remaining_in_content_line); + } + + // This connection has transitioned to no longer // being http and the content line support analyzers // need to be removed. From a6c1d12206d121891567c9f90f2b53b844279cec Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Thu, 18 Jan 2024 21:09:11 +0100 Subject: [PATCH 08/13] btest/websocket: Test for coalesced reply-ping Add a constructed PCAP where the HTTP/websocket server send a WebSocket ping message directly with the packet of the HTTP reply. Ensure this is interpreted the same as if the WebSocket message is in a separate packet following the HTTP reply. For the server side this should work, for the client side we'd need to synchronize suspend parsing the client side as we currently cannot quite know whether it's a pipelined HTTP request following, or upgraded protocol data and we don't have "suspend parsing" functionality here. --- .../out-coalesced | 16 +++++++++ .../out-separate | 16 +++++++++ .../weird.log | 11 ++++++ .../websocket/reply-ping-coalesced.pcap | Bin 0 -> 1860 bytes .../Traces/websocket/reply-ping-separate.pcap | Bin 0 -> 2024 bytes .../websocket/coalesced-reply-ping.zeek | 33 ++++++++++++++++++ 6 files changed, 76 insertions(+) create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/weird.log create mode 100644 testing/btest/Traces/websocket/reply-ping-coalesced.pcap create mode 100644 testing/btest/Traces/websocket/reply-ping-separate.pcap create mode 100644 testing/btest/scripts/base/protocols/websocket/coalesced-reply-ping.zeek diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced new file mode 100644 index 0000000000..024146cbf5 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced @@ -0,0 +1,16 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +websocket_handshake, CHhAvVGS1DHFjwGM9, 7 +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, ping, payload_len, 4 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 4, data, Zeek +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, pong, payload_len, 4 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 4, data, Zeek +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, text, payload_len, 11 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 11, data, Hello Zeek! +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, text, payload_len, 12 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 12, data, Hello there! +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 2 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 2 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate new file mode 100644 index 0000000000..024146cbf5 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate @@ -0,0 +1,16 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +websocket_handshake, CHhAvVGS1DHFjwGM9, 7 +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, ping, payload_len, 4 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 4, data, Zeek +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, pong, payload_len, 4 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 4, data, Zeek +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, text, payload_len, 11 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 11, data, Hello Zeek! +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, text, payload_len, 12 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 12, data, Hello there! +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 2 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 2 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/weird.log b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/weird.log new file mode 100644 index 0000000000..16031b4969 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/weird.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path weird +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p name addl notice peer source +#types time string addr port addr port string string bool string string +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 45838 127.0.0.1 8080 protocol_data_with_HTTP_upgrade_reply 6 F zeek HTTP +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Traces/websocket/reply-ping-coalesced.pcap b/testing/btest/Traces/websocket/reply-ping-coalesced.pcap new file mode 100644 index 0000000000000000000000000000000000000000..9fbb2ac70a8df63273ac8947da86b5f10d6f7aec GIT binary patch literal 1860 zcmaKtO-vhC5Xavd>QJ}Nms*JnVH-8MAY&UsiyWs4R0dN5nAi@1C?fXquvcF1y56k~ zrvmDMwrOLc9-_ulh?GNosZ?-~66KI6AD5gW5k-!AtD<};haLh_L4?jcywvL$k2TVI z_wDcf=RdQ%%l8(pnn*3tV{?-b4*pmV=V-?zGx-LNF-xP4cnEP9`j3<2W+C=H+<>{b zyD_t|(B;LI^QN_ba{m%ys{J5eYpSyxc|3d_&vP|3yO7*hl}red-M4gdV`g#p3+d<$ zp!dx0e>eL!ZzLmKC!YdxC+vh-8cg$%!T{5p)X7E7JwRWY+RLr-pMXYgOD|GUtNnK~ z83#)+O9P4A>4Fc5;l7H*dTuq=woS|-vD*{i?YuV-@Y$WsPOHVMsG5tH6d@!<@vz;| z?yy>diAXXuMqPZGhE+v~QO#=kT9IWcXrdy!_~Lb|#ZLv>5Doj;8e1=&bn(3-?f&uJ zrWUb(Ky32&iz8n~g~7zjI2#Vz5Q&4D7rAb>y1nzV3zDX^r$oAG|r{n($ zRlBpL8ES_gxZVb?@7%}id31fgO?SOGAxj$z-ErvS`hiyl*KCKA*J}ndgpz)Jx7AOprFdXytO`Bc?z1f< z2s8m6bp%pvsZ_9QLiPHFJiP;7gj173Yh0KNc0enQ_aqEXTP;7+)fe9y+O3MUznRwUMQE1|!Q#Gig>;{H1L z?byv8WH;Y86F)#POM|t@Q}8qP*)n#TsRefY_BJ~Yk6Lc}=WBny{pii|rO2bXKjgd5 zp5PXJu*J@XWPimi=3DhGw&uz`40AQh=889NfyDDb^nqH;(qN)Hvlkfg^NK{6E7u+) zar$E~l_Z7NqBKcAL;jryRo}vaiiXdEDUm5KWoDOwpP!xiaAWhFjF)G7|oF{-1C8e`Ob+Bk&47a533OiLFRy@ u6N^Kf0SBb1ecVdJ=KI8!s>JL?rqQkwi&G$h#yOmV2KcfSzY;iT`TqdzgX{wU literal 0 HcmV?d00001 diff --git a/testing/btest/Traces/websocket/reply-ping-separate.pcap b/testing/btest/Traces/websocket/reply-ping-separate.pcap new file mode 100644 index 0000000000000000000000000000000000000000..5fa723bc2cd1ef48d4498f8165f991cee1454d43 GIT binary patch literal 2024 zcmaKtPfQbO7{d8n~$tQrk9ah5D7Smjz@`RmYHVee$o?ndfPY`=WA;VxZD+PyVWO3vWJU{d^p~Nhc%To zm3C_=8BK+|sE5nYh$Qm+scg6I5Cwtqa!eFFT>iY>8l=3lokoJ}8fO#j_i&NWz}|+W z-^=f+>ibG^HE(Hcs!q4m)fq4To~9%Qb5|Kpd9yMV@TBD7k~Ec|k`#_oXBX{`hh_Q= z$H!w-kUJCMY-c7WbcvbHNKBTDlt4<9MP7`1xIP#(B+-G@=#bI%HGb9g{0^BNA5;90} z5ETEt=+!e|;Pj<2{kUl0ED(L*9JaQYfgVR6i=Vi1HA;t>(z|*t5?|f1k!FyHtu3bXTjePuzAZ>BGfltWswbX2Hc)!b&L#fo zM)ulYYy@@}>rdET+m436TN!&kstRAZEIfkA;_&Cp_?|o`4X2Xhb5Md z&XHk8H>peHjUEQ0W4QM(oz*Xa#N9v)fLd&AF+;XHvW!?;kOS_pD||E_ z7dg3yrlQamqccnnM#K~uU;>G)GaKP>FZLQ$gff;nLLRw}*V!06yA dc-#(sN?iGCQH=+f#+?R@qo6Uxc0oo@{12S!LTms4 literal 0 HcmV?d00001 diff --git a/testing/btest/scripts/base/protocols/websocket/coalesced-reply-ping.zeek b/testing/btest/scripts/base/protocols/websocket/coalesced-reply-ping.zeek new file mode 100644 index 0000000000..f425013c6b --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/coalesced-reply-ping.zeek @@ -0,0 +1,33 @@ +# @TEST-DOC: The reply-ping-coalesced pcap contains a WebSocket ping message right after the HTTP reply, in the same packet. + +# @TEST-EXEC: zeek -b -r $TRACES/websocket/reply-ping-separate.pcap %INPUT >>out-separate +# @TEST-EXEC: test ! -f weird.log +# +# @TEST-EXEC: zeek -b -r $TRACES/websocket/reply-ping-coalesced.pcap %INPUT >>out-coalesced +# @TEST-EXEC: btest-diff out-separate +# @TEST-EXEC: btest-diff out-coalesced +# @TEST-EXEC: btest-diff weird.log +# @TEST-EXEC: diff out-separate out-coalesced +# @TEST-EXEC: test ! -f analyzer.log + +@load base/protocols/websocket + +event websocket_handshake(c: connection, aid: count) + { + print "websocket_handshake", c$uid, aid; + } + +event websocket_frame(c: connection, is_orig: bool, fin: bool, rsv: count, opcode: count, payload_len: count) + { + print "websocket_frame", c$uid, is_orig, "fin", fin, "rsv", rsv, "opcode", WebSocket::opcodes[opcode], "payload_len", payload_len; + } + +event websocket_frame_data(c: connection, is_orig: bool, data: string) + { + print "websocket_frame_data", c$uid, is_orig, "len", |data|, "data", data[:120]; + } + +event websocket_close(c: connection, is_orig: bool, status: count, reason: string) + { + print "websocket_close", c$uid, is_orig, "status", status, "reason", reason; + } From e17655be61152545091802089aae53b96be7569a Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Fri, 19 Jan 2024 17:32:06 +0100 Subject: [PATCH 09/13] websocket: Verify Sec-WebSocket-Key/Accept headers and review feedback Don't log them, they are random and arbitrary in the normal case. Users can do the following to log them if wanted. redef += WebSocket::Info$client_key += { &log }; redef += WebSocket::Info$server_accept += { &log }; --- scripts/base/init-bare.zeek | 6 +- scripts/base/protocols/websocket/consts.zeek | 2 + scripts/base/protocols/websocket/main.zeek | 57 +++++++++++++++--- src/analyzer/protocol/websocket/WebSocket.cc | 2 +- src/analyzer/protocol/websocket/events.bif | 2 +- src/analyzer/protocol/websocket/functions.bif | 6 +- .../coverage.record-fields/out.default | 2 + .../out-coalesced | 2 +- .../out-separate | 2 +- .../out | 6 +- .../websocket.log | 11 ++++ .../weird.log | 11 ++++ .../Traces/websocket/wrong-accept-header.pcap | Bin 0 -> 1485 bytes .../websocket/coalesced-reply-ping.zeek | 4 +- .../base/protocols/websocket/events.zeek | 4 +- .../protocols/websocket/key-accept-wrong.zeek | 7 +++ 16 files changed, 100 insertions(+), 24 deletions(-) create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.key-accept-wrong/websocket.log create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.key-accept-wrong/weird.log create mode 100644 testing/btest/Traces/websocket/wrong-accept-header.pcap create mode 100644 testing/btest/scripts/base/protocols/websocket/key-accept-wrong.zeek diff --git a/scripts/base/init-bare.zeek b/scripts/base/init-bare.zeek index 3e153c204a..8345d71290 100644 --- a/scripts/base/init-bare.zeek +++ b/scripts/base/init-bare.zeek @@ -470,10 +470,10 @@ export { ## Whether to enable DPD on WebSocket frame payload by default. const use_dpd_default = T &redef; - ## Record type that is passed to :zeek:see:`WebSocket::__configure_analyzer`. + ## Record type that is passed to :zeek:see:`WebSocket::configure_analyzer`. ## - ## This allows to configure the WebSocket analyzer given parameters - ## collected from HTTP headers. + ## This record allows to configure the WebSocket analyzer given + ## parameters collected from HTTP headers. type AnalyzerConfig: record { ## The analyzer to attach for analysis of the WebSocket ## frame payload. See *use_dpd* below for the behavior diff --git a/scripts/base/protocols/websocket/consts.zeek b/scripts/base/protocols/websocket/consts.zeek index 9bdffc2b7c..c422710bb9 100644 --- a/scripts/base/protocols/websocket/consts.zeek +++ b/scripts/base/protocols/websocket/consts.zeek @@ -18,4 +18,6 @@ export { [OPCODE_PING] = "ping", [OPCODE_PONG] = "pong", } &default=function(opcode: count): string { return fmt("unknown-%x", opcode); } &redef; + + const HANDSHAKE_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; } diff --git a/scripts/base/protocols/websocket/main.zeek b/scripts/base/protocols/websocket/main.zeek index 8d22c250db..dfcfc04685 100644 --- a/scripts/base/protocols/websocket/main.zeek +++ b/scripts/base/protocols/websocket/main.zeek @@ -1,11 +1,13 @@ ##! Implements base functionality for WebSocket analysis. ##! -##! Upon a websocket_handshake(), logs all gathered information into websocket.log -##! and then configures the WebSocket analyzer with the headers collected using -##! http events. +##! Upon a websocket_established() event, logs all gathered information into +##! websocket.log and configures the WebSocket analyzer with the headers +##! collected via http events. @load base/protocols/http +@load ./consts + module WebSocket; # Register the WebSocket analyzer as HTTP upgrade analyzer. @@ -38,6 +40,10 @@ export { server_extensions: vector of string &log &optional; ## The extensions requested by the client, if any. client_extensions: vector of string &log &optional; + ## The Sec-WebSocket-Key header from the client. + client_key: string &optional; + ## The Sec-WebSocket-Accept header from the server. + server_accept: string &optional; }; ## Event that can be handled to access the WebSocket record as it is @@ -71,6 +77,10 @@ function set_websocket(c: connection) ); } +function expected_accept_for(key: string): string { + return encode_base64(hexstr_to_bytestring(sha1_hash(key + HANDSHAKE_GUID))); +} + event http_header(c: connection, is_orig: bool, name: string, value: string) { if ( ! starts_with(name, "SEC-WEBSOCKET-") ) @@ -98,11 +108,21 @@ event http_header(c: connection, is_orig: bool, name: string, value: string) ws$client_extensions += split_string(value, / *, */); } + else if ( name == "SEC-WEBSOCKET-KEY" ) + { + if ( ws?$client_key ) + Reporter::conn_weird("websocket_multiple_key_headers", c, "", "WebSocket"); + + ws$client_key = value; + } } else { if ( name == "SEC-WEBSOCKET-PROTOCOL" ) { + if ( ws?$subprotocol ) + Reporter::conn_weird("websocket_multiple_protocol_headers", c, "", "WebSocket"); + ws$subprotocol = value; } else if ( name == "SEC-WEBSOCKET-EXTENSIONS" ) @@ -112,6 +132,13 @@ event http_header(c: connection, is_orig: bool, name: string, value: string) ws$server_extensions += split_string(value, / *, */); } + else if ( name == "SEC-WEBSOCKET-ACCEPT" ) + { + if ( ws?$server_accept ) + Reporter::conn_weird("websocket_multiple_accept_headers", c, "", "WebSocket"); + + ws$server_accept = value; + } } } @@ -119,23 +146,39 @@ event http_request(c: connection, method: string, original_URI: string, unescaped_URI: string, version: string) { # If we see a http_request and have websocket state, wipe it as - # we should've seen a websocket_handshake even on success and + # we should've seen a websocket_established even on success and # likely no more http events. if ( ! c?$websocket ) delete c$websocket; } -event websocket_handshake(c: connection, aid: count) &priority=5 +event websocket_established(c: connection, aid: count) &priority=5 { if ( ! c?$websocket ) { # This means we never saw a Sec-WebSocket-* header, weird. - Reporter::conn_weird("websocket_handshake_unexpected", c, "", "WebSocket"); + Reporter::conn_weird("websocket_established_unexpected", c, "", "WebSocket"); set_websocket(c); } local ws = c$websocket; + if ( ! ws?$client_key ) + Reporter::conn_weird("websocket_missing_key_header", c, "", "WebSocket"); + + if ( ! ws?$server_accept ) + Reporter::conn_weird("websocket_missing_accept_header", c, "", "WebSocket"); + + # Verify the Sec-WebSocket-Accept header's value given the Sec-WebSocket-Key header's value. + if ( ws?$client_key && ws?$server_accept ) + { + local expected_accept = expected_accept_for(ws$client_key); + if ( ws$server_accept != expected_accept ) + Reporter::conn_weird("websocket_wrong_accept_header", c, + fmt("expected=%s, found=%s", expected_accept, ws$server_accept), + "WebSocket"); + } + # Replicate some information from the HTTP.log if ( c?$http ) { @@ -150,7 +193,7 @@ event websocket_handshake(c: connection, aid: count) &priority=5 } } -event websocket_handshake(c: connection, aid: count) &priority=-5 +event websocket_established(c: connection, aid: count) &priority=-5 { local ws = c$websocket; diff --git a/src/analyzer/protocol/websocket/WebSocket.cc b/src/analyzer/protocol/websocket/WebSocket.cc index 20e8a22086..4b15f4aa10 100644 --- a/src/analyzer/protocol/websocket/WebSocket.cc +++ b/src/analyzer/protocol/websocket/WebSocket.cc @@ -23,7 +23,7 @@ void WebSocket_Analyzer::Init() { tcp::TCP_ApplicationAnalyzer::Init(); // This event calls back via Configure() - zeek::BifEvent::enqueue_websocket_handshake(this, Conn(), GetID()); + zeek::BifEvent::enqueue_websocket_established(this, Conn(), GetID()); } bool WebSocket_Analyzer::Configure(zeek::RecordValPtr config) { diff --git a/src/analyzer/protocol/websocket/events.bif b/src/analyzer/protocol/websocket/events.bif index 77f450c5fe..e2ded2870c 100644 --- a/src/analyzer/protocol/websocket/events.bif +++ b/src/analyzer/protocol/websocket/events.bif @@ -11,7 +11,7 @@ ## ## .. zeek:see:: WebSocket::configure_analyzer ## -event websocket_handshake%(c: connection, aid: count%); +event websocket_established%(c: connection, aid: count%); ## Generated for every WebSocket frame. ## diff --git a/src/analyzer/protocol/websocket/functions.bif b/src/analyzer/protocol/websocket/functions.bif index bb06a3fd30..fc33a1a39a 100644 --- a/src/analyzer/protocol/websocket/functions.bif +++ b/src/analyzer/protocol/websocket/functions.bif @@ -6,19 +6,19 @@ module WebSocket; ## Configure the WebSocket analyzer. ## -## Called during :zeek:see:`websocket_handshake` to configure +## Called during :zeek:see:`websocket_established` to configure ## the WebSocket analyzer given the selected protocol and extension ## as chosen by the server. ## ## c: The WebSocket connection. # -## aid: The identifier for the WebSocket analyzer as provided to :zeek:see:`websocket_handshake`. +## aid: The identifier for the WebSocket analyzer as provided to :zeek:see:`websocket_established`. ## ## server_protocol: The protocol as found in the server's Sec-WebSocket-Protocol HTTP header, or empty. ## ## server_extensions: The extension as selected by the server via the Sec-WebSocket-Extensions HTTP Header. ## -## .. zeek:see:: websocket_handshake +## .. zeek:see:: websocket_established function __configure_analyzer%(c: connection, aid: count, config: WebSocket::AnalyzerConfig%): bool %{ auto* analyzer = c->FindAnalyzer(aid); diff --git a/testing/btest/Baseline/coverage.record-fields/out.default b/testing/btest/Baseline/coverage.record-fields/out.default index 527f4fead2..37d4a8083f 100644 --- a/testing/btest/Baseline/coverage.record-fields/out.default +++ b/testing/btest/Baseline/coverage.record-fields/out.default @@ -887,10 +887,12 @@ connection { * websocket: record WebSocket::Info, log=F, optional=T WebSocket::Info { * client_extensions: vector of string, log=T, optional=T + * client_key: string, log=F, optional=T * client_protocols: vector of string, log=T, optional=T * host: string, log=T, optional=T * id: record conn_id, log=T, optional=F conn_id { ... } + * server_accept: string, log=F, optional=T * server_extensions: vector of string, log=T, optional=T * subprotocol: string, log=T, optional=T * ts: time, log=T, optional=F diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced index 024146cbf5..8a28458914 100644 --- a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced @@ -1,5 +1,5 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. -websocket_handshake, CHhAvVGS1DHFjwGM9, 7 +websocket_established, CHhAvVGS1DHFjwGM9, 7 websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, ping, payload_len, 4 websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 4, data, Zeek websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, pong, payload_len, 4 diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate index 024146cbf5..8a28458914 100644 --- a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate @@ -1,5 +1,5 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. -websocket_handshake, CHhAvVGS1DHFjwGM9, 7 +websocket_established, CHhAvVGS1DHFjwGM9, 7 websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, ping, payload_len, 4 websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 4, data, Zeek websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, pong, payload_len, 4 diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.events/out b/testing/btest/Baseline/scripts.base.protocols.websocket.events/out index 6c6a139ac6..4bbd3de1cb 100644 --- a/testing/btest/Baseline/scripts.base.protocols.websocket.events/out +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.events/out @@ -1,6 +1,6 @@ ### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. jupyter-websocket.pcap -websocket_handshake, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=40492/tcp, resp_h=127.0.0.1, resp_p=51185/tcp], host=192.168.122.182, uri=/user/christian/api/kernels/f8645ecd-0a76-4bb1-9e6e-cb464276bc69/channels?session_id=deeecee7-efc2-42a1-a7c1-e1c0569436e3, user_agent=Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0, subprotocol=v1.kernel.websocket.jupyter.org, client_protocols=[v1.kernel.websocket.jupyter.org], server_extensions=, client_extensions=[permessage-deflate]] +websocket_established, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=40492/tcp, resp_h=127.0.0.1, resp_p=51185/tcp], host=192.168.122.182, uri=/user/christian/api/kernels/f8645ecd-0a76-4bb1-9e6e-cb464276bc69/channels?session_id=deeecee7-efc2-42a1-a7c1-e1c0569436e3, user_agent=Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/118.0, subprotocol=v1.kernel.websocket.jupyter.org, client_protocols=[v1.kernel.websocket.jupyter.org], server_extensions=, client_extensions=[permessage-deflate], client_key=7K5Qx7HwJUsja5KzBhGvfQ==, server_accept=USseDip1PofjB67M6I5CNkbYbp0=] websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, binary, payload_len, 262 websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 262, data, \x06\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00=\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x02\x01\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x00\x00\x00\x00\x06\x01\x00\x00\x00\x00\x00\x00shell{"date":"2023-09-29T23:25:05.568Z","msg_id":"5af8fd02-14a1- websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, binary @@ -44,7 +44,7 @@ websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_le websocket_close, CHhAvVGS1DHFjwGM9, F, status, 0, reason, websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, close wstunnel-http.pcap -websocket_handshake, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=51102/tcp, resp_h=127.0.0.1, resp_p=8888/tcp], host=localhost:8888, uri=/v1/events, user_agent=, subprotocol=v1, client_protocols=[v1, authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGNmZWFiLWY5OWQtNzBmNy05NmFmLTBlOGJhNjk2YTFmNiIsInAiOiJUY3AiLCJyIjoiemVlay5vcmciLCJycCI6ODB9.FsquetBp_jsIDzBslWyyTPlS2hcMprVuWmbT2r57N0A], server_extensions=, client_extensions=] +websocket_established, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=51102/tcp, resp_h=127.0.0.1, resp_p=8888/tcp], host=localhost:8888, uri=/v1/events, user_agent=, subprotocol=v1, client_protocols=[v1, authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGNmZWFiLWY5OWQtNzBmNy05NmFmLTBlOGJhNjk2YTFmNiIsInAiOiJUY3AiLCJyIjoiemVlay5vcmciLCJycCI6ODB9.FsquetBp_jsIDzBslWyyTPlS2hcMprVuWmbT2r57N0A], server_extensions=, client_extensions=, client_key=FdRecb4tsolqJgO+HrbUfg==, server_accept=PbXiEPoL5O2wxc6/MdNHnSOXy+c=] websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, binary, payload_len, 72 websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 72, data, GET / HTTP/1.1\x0d\x0aHost: zeek.org\x0d\x0aUser-Agent: curl/7.81.0\x0d\x0aAccept: */*\x0d\x0a\x0d\x0a websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, binary @@ -60,7 +60,7 @@ websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, close broker-websocket.pcap -websocket_handshake, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=38776/tcp, resp_h=127.0.0.1, resp_p=27599/tcp], host=localhost:27599, uri=/v1/messages/json, user_agent=Python/3.10 websockets/12.0, subprotocol=, client_protocols=, server_extensions=, client_extensions=[permessage-deflate; client_max_window_bits]] +websocket_established, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=38776/tcp, resp_h=127.0.0.1, resp_p=27599/tcp], host=localhost:27599, uri=/v1/messages/json, user_agent=Python/3.10 websockets/12.0, subprotocol=, client_protocols=, server_extensions=, client_extensions=[permessage-deflate; client_max_window_bits], client_key=E58pVwft35HPkD/MFCjtEA==, server_accept=HxOmr1a2nvOOc4Qiv7Ou3wrCsJc=] websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, text, payload_len, 24 websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 24, data, ["/zeek/event/my_topic"] websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, text diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.key-accept-wrong/websocket.log b/testing/btest/Baseline/scripts.base.protocols.websocket.key-accept-wrong/websocket.log new file mode 100644 index 0000000000..077a4e7b65 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.key-accept-wrong/websocket.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path websocket +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p host uri user_agent subprotocol client_protocols server_extensions client_extensions +#types time string addr port addr port string string string string vector[string] vector[string] vector[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 53654 127.0.0.1 8080 localhost:8080 / Python/3.10 websockets/12.0 v1 v1 - permessage-deflate; client_max_window_bits +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.key-accept-wrong/weird.log b/testing/btest/Baseline/scripts.base.protocols.websocket.key-accept-wrong/weird.log new file mode 100644 index 0000000000..7f15a7082c --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.key-accept-wrong/weird.log @@ -0,0 +1,11 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path weird +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p name addl notice peer source +#types time string addr port addr port string string bool string string +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 53654 127.0.0.1 8080 websocket_wrong_accept_header expected=N8ntNYkX6Qerw4tK3s/CYzpSZNc=, found=N8ntNYkX6Qerw4tK3s/CYzpSZNc=-wrong F zeek WebSocket +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/Traces/websocket/wrong-accept-header.pcap b/testing/btest/Traces/websocket/wrong-accept-header.pcap new file mode 100644 index 0000000000000000000000000000000000000000..80ba9b1d4ee74e8329e72b64fb9f5b62cf0a48e5 GIT binary patch literal 1485 zcmaKsPfXKL9LHbBRA^Eo9Do~*3KGr!IYLN<1Vv;pSf(4uNR({dV`~`g(snC&k*G0x z<3wWgfLG%Qz09i@E`S(`N4|ApF{pGDk z$mKrq*6pfgh0_~%$?r@}%_&0uQI(7kA$PyC$t?>Ti)YX6od^2%Qp4Vpy=5nv&~5T> zAYX%jB2_u4=8jT`YM!&nmga4sx6Yqob}u%7My9NnP>I&p&+E_>L_(@^5F#^P8YILA z@gs@N%x>_GlQ_^H=Y4!A9*_D0-T=pj6ipZSoFXQ3Su*YNclkMXG@ntEX)N#tPHBob zjdhOgQ)C&7x}?YgZ>@7|42z!II2EINJYhU1@M9wdTo@Y67CQZX{_OSe@TFp`Gt<*^ zc=H%mH3|y^4=?r4>R2X=n!x9=I)gPWnZcekp2{V4e1#Wt5|;JJndI!GA<1dQm`q8! zc9;@X6gRD&>Qd{IMjTB%U}o*@jeqF;q_KRMQ#I2x-#5C8YslN3dqhm0a!vK~oHk9Xe0s?ni`gPN}| zaW5a6h=@HNLsjGq$2w2u9eI~yyalF@kFhjFKL^7$UH%-Rjqo|FK#V)>!%ovA#xxK^ zkRPeaL1PrmbsFPOn`kv=g*FCa{E~7KpSmY7o+ezWy3#OJTC}+pO4q2;8%{1EYBE>N z`!FRIjwIGII~U;AD_;d46ZTc`SgA%dKTz*mHqr9F2Hulw2RB0C^{Oc0H4suD{h URHJSaE!R!ZpLOE8E#f5p10XzuhX4Qo literal 0 HcmV?d00001 diff --git a/testing/btest/scripts/base/protocols/websocket/coalesced-reply-ping.zeek b/testing/btest/scripts/base/protocols/websocket/coalesced-reply-ping.zeek index f425013c6b..509a44624e 100644 --- a/testing/btest/scripts/base/protocols/websocket/coalesced-reply-ping.zeek +++ b/testing/btest/scripts/base/protocols/websocket/coalesced-reply-ping.zeek @@ -12,9 +12,9 @@ @load base/protocols/websocket -event websocket_handshake(c: connection, aid: count) +event websocket_established(c: connection, aid: count) { - print "websocket_handshake", c$uid, aid; + print "websocket_established", c$uid, aid; } event websocket_frame(c: connection, is_orig: bool, fin: bool, rsv: count, opcode: count, payload_len: count) diff --git a/testing/btest/scripts/base/protocols/websocket/events.zeek b/testing/btest/scripts/base/protocols/websocket/events.zeek index 27dc66d9eb..2b6eae6cde 100644 --- a/testing/btest/scripts/base/protocols/websocket/events.zeek +++ b/testing/btest/scripts/base/protocols/websocket/events.zeek @@ -12,9 +12,9 @@ @load base/protocols/websocket -event websocket_handshake(c: connection, aid: count) +event websocket_established(c: connection, aid: count) { - print "websocket_handshake", c$uid, aid, c$websocket; + print "websocket_established", c$uid, aid, c$websocket; } event websocket_message(c: connection, is_orig: bool, opcode: count) diff --git a/testing/btest/scripts/base/protocols/websocket/key-accept-wrong.zeek b/testing/btest/scripts/base/protocols/websocket/key-accept-wrong.zeek new file mode 100644 index 0000000000..17aceb0ba7 --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/key-accept-wrong.zeek @@ -0,0 +1,7 @@ +# @TEST-DOC: Test weird generation when the Sec-WebSocket-Accept socket isn't as expected. +# +# @TEST-EXEC: zeek -b -r $TRACES/websocket/wrong-accept-header.pcap %INPUT +# @TEST-EXEC: btest-diff websocket.log +# @TEST-EXEC: btest-diff weird.log + +@load base/protocols/websocket From 5eb380d74addd169fd68c5015b663ede436dab66 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Fri, 19 Jan 2024 19:26:42 +0100 Subject: [PATCH 10/13] websocket: Fix crash for fragmented messages The &transient attribute does not work well with $element as that won't be available within &until anymore apparently. Found after a few seconds building out the fuzzer. --- .../protocol/websocket/websocket-protocol.pac | 2 +- .../out | 37 ++++++++++++++++++ .../websocket/message-too-big-status.pcap | Bin 0 -> 1684 bytes .../websocket/two-binary-fragments.pcap | Bin 0 -> 2026 bytes .../base/protocols/websocket/events.zeek | 4 ++ 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 testing/btest/Traces/websocket/message-too-big-status.pcap create mode 100644 testing/btest/Traces/websocket/two-binary-fragments.pcap diff --git a/src/analyzer/protocol/websocket/websocket-protocol.pac b/src/analyzer/protocol/websocket/websocket-protocol.pac index 01ab76ff9e..93cd4eb94a 100644 --- a/src/analyzer/protocol/websocket/websocket-protocol.pac +++ b/src/analyzer/protocol/websocket/websocket-protocol.pac @@ -80,7 +80,7 @@ type WebSocket_Message = record { first_frame: WebSocket_Frame(true, this); optional_more_frames: case first_frame.hdr.b.fin of { true -> no_more_frames: empty; - false -> more_frames: WebSocket_Frame(false, this)[] &until($element.hdr.b.fin) &transient; + false -> more_frames: WebSocket_Frame(false, this)[] &until($element.hdr.b.fin); }; } &let { opcode = first_frame.hdr.b.opcode; diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.events/out b/testing/btest/Baseline/scripts.base.protocols.websocket.events/out index 4bbd3de1cb..41bb69dcbe 100644 --- a/testing/btest/Baseline/scripts.base.protocols.websocket.events/out +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.events/out @@ -89,3 +89,40 @@ websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_le websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, close +message-too-big-status.pcap +websocket_established, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=60956/tcp, resp_h=127.0.0.1, resp_p=8080/tcp], host=localhost:8080, uri=/, user_agent=Python/3.10 websockets/12.0, subprotocol=v1, client_protocols=[v1], server_extensions=, client_extensions=[permessage-deflate; client_max_window_bits], client_key=iTel1Ova5Nhz/G7VlI2qKg==, server_accept=YsQYYLj7ZCpzTLsVLb+w/ydy79E=] +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, ping, payload_len, 4 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 4, data, Zeek +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, ping +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 31 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1009, reason, over size limit (4 > 2 bytes) +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 31, data, \x03\xf1over size limit (4 > 2 bytes) +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, close +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 2 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, close +two-binary-fragments.pcap +websocket_established, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=50198/tcp, resp_h=127.0.0.1, resp_p=8080/tcp], host=localhost:8080, uri=/, user_agent=Python/3.10 websockets/12.0, subprotocol=v1, client_protocols=[v1], server_extensions=, client_extensions=[permessage-deflate; client_max_window_bits], client_key=cQGA5Z1nvyUJ9XOVIaLaQA==, server_accept=zWaHVUKxEGPDs+xJeKtzkE1bm54=] +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, ping, payload_len, 4 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 4, data, Zeek +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, ping +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, pong, payload_len, 4 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 4, data, Zeek +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, pong +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, binary, payload_len, 11 +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 11, data, Hello Zeek! +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, F, rsv, 0, opcode, binary, payload_len, 5 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 5, data, Hello +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, continuation, payload_len, 7 +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 7, data, there! +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary +websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 2 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, +websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 +websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, close +websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 2 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, +websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 +websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, close diff --git a/testing/btest/Traces/websocket/message-too-big-status.pcap b/testing/btest/Traces/websocket/message-too-big-status.pcap new file mode 100644 index 0000000000000000000000000000000000000000..7f51e3f583fbfddd98f314b42d0046f12086e021 GIT binary patch literal 1684 zcmaKrOH30%7{_;)R+=VFM5C#Mgf&th(b5OSwp&1yR6vMgC}07D?RIDjWw&-0N(qGe ziVzkbDMKLMacd*+$v0=KB&kt$7^MhmyO=jmz(#Mlz)rQIXbLdNt7rEkP*{L?ka! zrHJSZ??}{>8&PHqkX6PlL%)Y|&|JctK?72+#T=A@u1mJIp$5Q#2U!1pq_G zfnVU+ZXoKkRXoQ5Rt)nTL#Nku+D5<{TY$e2?=jYa1Ve{CAY!@D=PNxQ8ZcGcn zt98Nh^32W6ASPg#r6hC7-7f+TEeZ@B1+g9=2)-aN20&-TCjuAEM#6v-J9>Qm9q}+1 z;NuscTg-nu6igOtG3;HZMXuk2 zuK$0IwqfMDrb==>eM5#s*RdHm$IbR>rR#{e3W=+5fGFjGZ^pbWiHNz6cO>e`&6c0r z#2~sE(gbOX*+MtQ!y+3Bb3t0_8JfY$=GB{;r%s#xPz+XpGu)oD(Z{kN3O(`)7roy4 zE_-WLbih+DG}rr&$4!Yq!tQj#Q7X?w6;7uN{F2o`>Lh*$ccpey58>F%9Ji7W4P0;6 zDB2Ao1HVCH4Rj8rJg|X`?HV45OCqL5vXYNS0u9_-s3eZ6S^?-r-0#_y+Og6*5?7{_ z#!4wA7ZJw~sEHtyh*BO{sh4tKBG{3rC)XdfDv2YLuX5EbuQ$35F7_IV7KX0G=ebbD z+apGH!f5~#VDQkQcmkDu0?VF-^-#(Kmvz{#V}r`1vT1{8XwXs;8ub2CMp-K7E22Jw z)GROI2Bx0MpcY|j9=X}}K}khKRem-i<;3=kO2o9`OYmwKl6mBM+sA*2x~xRoO{}p( SBBnbxK#j}jwYJSGiGKiM8Lou@ literal 0 HcmV?d00001 diff --git a/testing/btest/Traces/websocket/two-binary-fragments.pcap b/testing/btest/Traces/websocket/two-binary-fragments.pcap new file mode 100644 index 0000000000000000000000000000000000000000..043c46e61ff6a04f04d5a0529b0ca74a5cb51c56 GIT binary patch literal 2026 zcmaKtPi)&%9LJyCjz&gdY=S8Vrg;p3CX}XTn^ep;t+1@28Ctvkag_>Hj{BNMP3#bx zwH*SYK}BF3c0gzt)@~JtLUWp?9wwn3m^5)11!)pDDh&|ZfdOhdq(!CtelKxnn>0V^ zCpoeGdEfW_zW03V+sl`D(oCFMUnhivAE|G?rnlY_NEcjVyDLDVgd9{dN63*!etYd* zcmezRj$P-`QP+6RyNz>5AsTCAnF?5}3R@%^Iv35Tw^inOX&}1)rCLE_zvZ(cR zb{t4Z`l+e@t``SS47{oyS9>}R9r8asK=USpN&Eb_qLUWY@un$@IhxN>(^OM5JVHk^ zszqNCwM?4o)^Jvx9G*z)BgVvVGHscDN_XC{49&>M;whMDze)4qjugBni``S!n4w4Z zwMuOdXJ|&G7g}NNK>=M~1J^gt(LRr^&+TMr9AwX<* zv6b>-2b3N@fr2hbX=8o72 zGu`S&m?_OpZ{@+j)uFg|x@h2SAjZHsY~@INNVf7U5-@Ok(o6iXIYH?- za({2EG{=D_F)TbDA8&^sE>Csz8 w2hNuo5fzI$;QLz6EP(?%3){HWMEU>3FB=l=S*G!6pT?b_@dsQ7iF;n+e_Aa|%m4rY literal 0 HcmV?d00001 diff --git a/testing/btest/scripts/base/protocols/websocket/events.zeek b/testing/btest/scripts/base/protocols/websocket/events.zeek index 2b6eae6cde..5c4c1fd2cf 100644 --- a/testing/btest/scripts/base/protocols/websocket/events.zeek +++ b/testing/btest/scripts/base/protocols/websocket/events.zeek @@ -6,6 +6,10 @@ # @TEST-EXEC: zeek -b -r $TRACES/websocket/wstunnel-http.pcap %INPUT >>out # @TEST-EXEC: echo "broker-websocket.pcap" >>out # @TEST-EXEC: zeek -b -r $TRACES//websocket/broker-websocket.pcap %INPUT >>out +# @TEST-EXEC: echo "message-too-big-status.pcap" >>out +# @TEST-EXEC: zeek -b -r $TRACES//websocket/message-too-big-status.pcap %INPUT >>out +# @TEST-EXEC: echo "two-binary-fragments.pcap" >>out +# @TEST-EXEC: zeek -b -r $TRACES//websocket/two-binary-fragments.pcap %INPUT >>out # @TEST-EXEC: btest-diff out # @TEST-EXEC: test ! -f analyzer.log # @TEST-EXEC: test ! -f weird.log From 1775b01b58515c4e526b4a79659a6a1ca45abc80 Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Fri, 19 Jan 2024 20:17:57 +0100 Subject: [PATCH 11/13] fuzzers: Add WebSocket fuzzer It immediately found an issue with &transient, but fairly stable thereafter. This is a separate fuzzer implementation as there's a custom Configure() call for the analyzer as well as disabling all other analyzers so we don't fuzz unrelated protocols. --- src/fuzzers/CMakeLists.txt | 1 + src/fuzzers/corpora/websocket-corpus.zip | Bin 0 -> 62884 bytes src/fuzzers/websocket-fuzzer.cc | 107 +++++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/fuzzers/corpora/websocket-corpus.zip create mode 100644 src/fuzzers/websocket-fuzzer.cc diff --git a/src/fuzzers/CMakeLists.txt b/src/fuzzers/CMakeLists.txt index ef9012b9c1..41a46dc64a 100644 --- a/src/fuzzers/CMakeLists.txt +++ b/src/fuzzers/CMakeLists.txt @@ -94,6 +94,7 @@ target_link_libraries(zeek_fuzzer_shared PUBLIC ${zeek_fuzzer_shared_deps} add_fuzz_target(packet) add_fuzz_target(dns) +add_fuzz_target(websocket) add_generic_analyzer_fuzz_target(ftp) add_generic_analyzer_fuzz_target(http) diff --git a/src/fuzzers/corpora/websocket-corpus.zip b/src/fuzzers/corpora/websocket-corpus.zip new file mode 100644 index 0000000000000000000000000000000000000000..d2f3b23363dc10a97c8c9f33ebdcc986e0f5e8ba GIT binary patch literal 62884 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 literal 0 HcmV?d00001 diff --git a/src/fuzzers/websocket-fuzzer.cc b/src/fuzzers/websocket-fuzzer.cc new file mode 100644 index 0000000000..1d1b47d39e --- /dev/null +++ b/src/fuzzers/websocket-fuzzer.cc @@ -0,0 +1,107 @@ +#include + +#include "zeek/Conn.h" +#include "zeek/ID.h" +#include "zeek/RunState.h" +#include "zeek/analyzer/Analyzer.h" +#include "zeek/analyzer/Manager.h" +#include "zeek/analyzer/protocol/pia/PIA.h" +#include "zeek/analyzer/protocol/tcp/TCP.h" +#include "zeek/analyzer/protocol/websocket/WebSocket.h" +#include "zeek/fuzzers/FuzzBuffer.h" +#include "zeek/fuzzers/fuzzer-setup.h" +#include "zeek/packet_analysis/protocol/tcp/TCPSessionAdapter.h" +#include "zeek/session/Manager.h" + +static constexpr auto FUZZ_ANALYZER_NAME = "websocket"; + +static zeek::Connection* add_connection() { + static constexpr double network_time_start = 1439471031; + zeek::run_state::detail::update_network_time(network_time_start); + + zeek::Packet p; + zeek::ConnTuple conn_id; + conn_id.src_addr = zeek::IPAddr("1.2.3.4"); + conn_id.dst_addr = zeek::IPAddr("5.6.7.8"); + conn_id.src_port = htons(23132); + conn_id.dst_port = htons(80); + conn_id.is_one_way = false; + conn_id.proto = TRANSPORT_TCP; + zeek::detail::ConnKey key(conn_id); + zeek::Connection* conn = new zeek::Connection(key, network_time_start, &conn_id, 1, &p); + conn->SetTransport(TRANSPORT_TCP); + zeek::session_mgr->Insert(conn); + return conn; +} + +static std::tuple add_analyzer( + zeek::Connection* conn, const zeek::Tag& analyzer_tag) { + auto* tcp = new zeek::packet_analysis::TCP::TCPSessionAdapter(conn); + auto* pia = new zeek::analyzer::pia::PIA_TCP(conn); + auto a = zeek::analyzer_mgr->InstantiateAnalyzer(analyzer_tag, conn); + tcp->AddChildAnalyzer(a); + tcp->AddChildAnalyzer(pia->AsAnalyzer()); + conn->SetSessionAdapter(tcp, pia); + + return {a, tcp}; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + static auto analyzer_tag = zeek::analyzer_mgr->GetComponentTag(FUZZ_ANALYZER_NAME); + if ( ! analyzer_tag ) { + std::fprintf(stderr, "Unable to find component tag for '%s'", FUZZ_ANALYZER_NAME); + abort(); + } + + // Would be nice to have that in LLVMFuzzerInitialize, oh well... + static bool one_time_setup = false; + if ( ! one_time_setup ) { + zeek::analyzer_mgr->DisableAllAnalyzers(); + zeek::analyzer_mgr->EnableAnalyzer(analyzer_tag); + const auto& pia_tcp_tag = zeek::analyzer_mgr->GetComponentTag("PIA_TCP"); + zeek::analyzer_mgr->EnableAnalyzer(pia_tcp_tag); + one_time_setup = true; + } + + zeek::detail::FuzzBuffer fb{data, size}; + + if ( ! fb.Valid() ) + return 0; + + auto conn = add_connection(); + if ( new_connection ) + conn->Event(new_connection, nullptr); + + auto [a, adapter] = add_analyzer(conn, analyzer_tag); + + // WebSocket specific initialization. May also want to fuzz + // this in the future. + static const auto& config_type = zeek::id::find_type("WebSocket::AnalyzerConfig"); + static const auto& config_rec = zeek::make_intrusive(config_type); + auto wsa = static_cast(a); + wsa->Configure(config_rec); + + for ( ;; ) { + auto chunk = fb.Next(); + + if ( ! chunk ) + break; + + try { + a->NextStream(chunk->size, chunk->data.get(), chunk->is_orig); + } catch ( const binpac::Exception& e ) { + } + + chunk = {}; // Release buffer before draining events. + zeek::event_mgr.Drain(); + + // Has the analyzer been disabled during event processing? + if ( ! adapter->HasChildAnalyzer(analyzer_tag) ) + break; + } + + zeek::event_mgr.Drain(); + zeek::detail::fuzzer_cleanup_one_input(); + + return 0; +} From 015a7c5fbcb1efcc9904f855798d41afec12d0ed Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Mon, 22 Jan 2024 15:01:35 +0100 Subject: [PATCH 12/13] websocket: Address review feedback for BinPac code * Rename mask_ to masking_key_ * Fold FrameHeaderFixed into FrameHeader directly * Drop WebSocket_FramePayloadUnmask type Thanks a bunch @ckreibich! --- .../protocol/websocket/websocket-analyzer.pac | 41 +++------- .../protocol/websocket/websocket-protocol.pac | 79 +++++++++---------- .../out-coalesced | 4 +- .../out-separate | 4 +- .../out | 14 ++-- 5 files changed, 60 insertions(+), 82 deletions(-) diff --git a/src/analyzer/protocol/websocket/websocket-analyzer.pac b/src/analyzer/protocol/websocket/websocket-analyzer.pac index c57dd9a651..fc60538161 100644 --- a/src/analyzer/protocol/websocket/websocket-analyzer.pac +++ b/src/analyzer/protocol/websocket/websocket-analyzer.pac @@ -27,30 +27,15 @@ refine flow WebSocket_Flow += { zeek::BifEvent::enqueue_websocket_frame(connection()->zeek_analyzer(), connection()->zeek_analyzer()->Conn(), is_orig(), - ${hdr.b.fin}, - ${hdr.b.reserved}, - ${hdr.b.opcode}, + ${hdr.fin}, + ${hdr.reserved}, + ${hdr.opcode}, ${hdr.payload_len}); } return true; %} - function process_payload_unmask(chunk: WebSocket_FramePayloadUnmask): bool - %{ - auto& data = ${chunk.data}; - - // In-place unmasking if frame payload is masked. - if ( has_mask_ ) - { - auto *d = data.data(); - for ( int i = 0; i < data.length(); i++ ) - d[i] = d[i] ^ mask_[mask_idx_++ % mask_.size()]; - } - - return true; - %} - function process_payload_close(close: WebSocket_FramePayloadClose): bool %{ if ( websocket_close ) @@ -84,7 +69,15 @@ refine flow WebSocket_Flow += { function process_payload_chunk(chunk: WebSocket_FramePayloadChunk): bool %{ - auto& data = ${chunk.unmask.data}; + auto& data = ${chunk.data}; + + // In-place unmasking if frame payload is masked. + if ( has_mask_ ) + { + auto *d = data.data(); + for ( int i = 0; i < data.length(); i++ ) + d[i] = d[i] ^ masking_key_[masking_key_idx_++ % masking_key_.size()]; + } if ( websocket_frame_data ) { @@ -96,7 +89,7 @@ refine flow WebSocket_Flow += { } // Forward text and binary data to downstream analyzers. - if ( ${chunk.hdr.b.opcode} == OPCODE_TEXT|| ${chunk.hdr.b.opcode} == OPCODE_BINARY) + if ( ${chunk.hdr.opcode} == OPCODE_TEXT|| ${chunk.hdr.opcode} == OPCODE_BINARY) connection()->zeek_analyzer()->ForwardStream(data.length(), data.data(), is_orig()); @@ -113,14 +106,6 @@ refine typeattr WebSocket_FrameHeader += &let { proc_header = $context.flow.process_header(this); }; -refine typeattr WebSocket_FramePayloadUnmask += &let { - proc_payload_unmask = $context.flow.process_payload_unmask(this); -}; - refine typeattr WebSocket_FramePayloadClose += &let { proc_payload_close = $context.flow.process_payload_close(this); }; - -refine typeattr WebSocket_FramePayloadChunk += &let { - proc_payload_chunk = $context.flow.process_payload_chunk(this); -}; diff --git a/src/analyzer/protocol/websocket/websocket-protocol.pac b/src/analyzer/protocol/websocket/websocket-protocol.pac index 93cd4eb94a..1abd317859 100644 --- a/src/analyzer/protocol/websocket/websocket-protocol.pac +++ b/src/analyzer/protocol/websocket/websocket-protocol.pac @@ -13,54 +13,47 @@ enum Opcodes { OPCODE_PONG = 0x0a, } -type WebSocket_FrameHeaderFixed(first_frame: bool) = record { - # First frame in message cannot be continuation, following - # frames are only expected to be continuations. - b: uint16 &enforce((first_frame && opcode != 0) || (!first_frame && opcode == 0)); -} &let { - fin: bool = (b & 0x8000) ? true : false; - reserved: uint8 = ((b & 0x7000) >> 12); - opcode: uint8 = (b & 0x0f00) >> 8; - has_mask: bool = (b & 0x0080) ? true : false; - payload_len1: uint8 = (b & 0x007f); - rest_header_len: uint64 = (has_mask ? 4 : 0) + (payload_len1 < 126 ? 0 : (payload_len1 == 126 ? 2 : 8)); -} &length=2; - -type WebSocket_FrameHeader(b: WebSocket_FrameHeaderFixed) = record { - maybe_more_len: case b.payload_len1 of { +type WebSocket_FrameHeader(first_frame: bool) = record { + first2: uint16 &enforce((first_frame && opcode != 0) || (!first_frame && opcode == 0)); + maybe_more_len: case payload_len1 of { 126 -> payload_len2: uint16; 127 -> payload_len8: uint64; default -> short_len: empty; }; - maybe_mask: case b.has_mask of { - true -> mask: bytestring &length=4; - false -> no_mask: empty; + maybe_masking_key: case has_mask of { + true -> masking_key: bytestring &length=4; + false -> no_masking_key: empty; }; } &let { - payload_len: uint64 = b.payload_len1 < 126 ? b.payload_len1 : (b.payload_len1 == 126 ? payload_len2 : payload_len8); - new_frame_payload = $context.flow.new_frame_payload(this); -} &length=b.rest_header_len; + fin: bool = (first2 & 0x8000) ? true : false; + reserved: uint8 = ((first2 & 0x7000) >> 12); + opcode: uint8 = (first2 & 0x0f00) >> 8; + payload_len1: uint8 = (first2 & 0x007f); + has_mask: bool = (first2 & 0x0080) ? true : false; -type WebSocket_FramePayloadClose(hdr: WebSocket_FrameHeader) = record { + # Derived fields. + rest_header_len: uint64 = (has_mask ? 4 : 0) + (payload_len1 < 126 ? 0 : (payload_len1 == 126 ? 2 : 8)); + payload_len: uint64 = payload_len1 < 126 ? payload_len1 : (payload_len1 == 126 ? payload_len2 : payload_len8); + + new_frame_payload = $context.flow.new_frame_payload(this); +} &length=2+rest_header_len; + +type WebSocket_FramePayloadClose = record { status: uint16; reason: bytestring &restofdata; } &byteorder=bigendian; -type WebSocket_FramePayloadUnmask(hdr: WebSocket_FrameHeader) = record { - data: bytestring &restofdata; -}; - type WebSocket_FramePayloadChunk(len: uint64, hdr: WebSocket_FrameHeader) = record { - unmask: WebSocket_FramePayloadUnmask(hdr); + data: bytestring &restofdata; } &let { - consumed_payload = $context.flow.consumed_chunk(len); - close_payload: WebSocket_FramePayloadClose(hdr) withinput unmask.data &length=len &if(hdr.b.opcode == OPCODE_CLOSE); + consumed_payload = $context.flow.consumed_chunk_len(len); + payload_chunk = $context.flow.process_payload_chunk(this); # unmasks if needed + close_payload: WebSocket_FramePayloadClose withinput data &length=len &if(hdr.opcode == OPCODE_CLOSE); } &length=len; type WebSocket_Frame(first_frame: bool, msg: WebSocket_Message) = record { - b: WebSocket_FrameHeaderFixed(first_frame); - hdr: WebSocket_FrameHeader(b); + hdr: WebSocket_FrameHeader(first_frame); # This is implementing frame payload chunking so that we do not # attempt to buffer huge frames and forward data to downstream @@ -73,17 +66,17 @@ type WebSocket_Frame(first_frame: bool, msg: WebSocket_Message) = record { } &let { # If we find a close frame without payload, raise the event here # as the close won't have been parsed via chunks. - empty_close = $context.flow.process_empty_close(hdr) &if(b.opcode == OPCODE_CLOSE) && hdr.payload_len == 0; + empty_close = $context.flow.process_empty_close(hdr) &if(hdr.opcode == OPCODE_CLOSE) && hdr.payload_len == 0; }; type WebSocket_Message = record { first_frame: WebSocket_Frame(true, this); - optional_more_frames: case first_frame.hdr.b.fin of { + optional_more_frames: case first_frame.hdr.fin of { true -> no_more_frames: empty; - false -> more_frames: WebSocket_Frame(false, this)[] &until($element.hdr.b.fin); + false -> more_frames: WebSocket_Frame(false, this)[] &until($element.hdr.fin); }; } &let { - opcode = first_frame.hdr.b.opcode; + opcode = first_frame.hdr.opcode; } &byteorder=bigendian; flow WebSocket_Flow(is_orig: bool) { @@ -91,14 +84,14 @@ flow WebSocket_Flow(is_orig: bool) { %member{ bool has_mask_; - uint64_t mask_idx_; + uint64_t masking_key_idx_; uint64_t frame_payload_len_; - std::array mask_; + std::array masking_key_; %} %init{ has_mask_ = false; - mask_idx_ = 0; + masking_key_idx_ = 0; frame_payload_len_ = 0; %} @@ -108,11 +101,11 @@ flow WebSocket_Flow(is_orig: bool) { connection()->zeek_analyzer()->Weird("websocket_frame_not_consumed"); frame_payload_len_ = ${hdr.payload_len}; - has_mask_ = ${hdr.b.has_mask}; - mask_idx_ = 0; + has_mask_ = ${hdr.has_mask}; + masking_key_idx_ = 0; if ( has_mask_ ) { - assert(${hdr.mask}.length() == static_cast(mask_.size())); - memcpy(mask_.data(), ${hdr.mask}.data(), mask_.size()); + assert(${hdr.masking_key}.length() == static_cast(masking_key_.size())); + memcpy(masking_key_.data(), ${hdr.masking_key}.data(), masking_key_.size()); } return frame_payload_len_; %} @@ -122,7 +115,7 @@ flow WebSocket_Flow(is_orig: bool) { return frame_payload_len_; %} - function consumed_chunk(len: uint64): uint64 + function consumed_chunk_len(len: uint64): uint64 %{ if ( len > frame_payload_len_ ) { connection()->zeek_analyzer()->Weird("websocket_frame_consuming_too_much"); diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced index 8a28458914..bb559170db 100644 --- a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-coalesced @@ -9,8 +9,8 @@ websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 11, data, Hello Zeek! websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, text, payload_len, 12 websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 12, data, Hello there! websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 2 -websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 2 -websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate index 8a28458914..bb559170db 100644 --- a/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.coalesced-reply-ping/out-separate @@ -9,8 +9,8 @@ websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 11, data, Hello Zeek! websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, text, payload_len, 12 websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 12, data, Hello there! websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 2 -websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 2 -websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.events/out b/testing/btest/Baseline/scripts.base.protocols.websocket.events/out index 41bb69dcbe..af9e938aaf 100644 --- a/testing/btest/Baseline/scripts.base.protocols.websocket.events/out +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.events/out @@ -52,12 +52,12 @@ websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, binary, payload_l websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 409, data, HTTP/1.1 301 Moved Permanently\x0d\x0aServer: nginx\x0d\x0aDate: Fri, 12 Jan 2024 17:15:32 GMT\x0d\x0aContent-Type: text/html\x0d\x0aContent-Len websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 2 -websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, close websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 2 -websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, close broker-websocket.pcap websocket_established, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=38776/tcp, resp_h=127.0.0.1, resp_p=27599/tcp], host=localhost:27599, uri=/v1/messages/json, user_agent=Python/3.10 websockets/12.0, subprotocol=, client_protocols=, server_extensions=, client_extensions=[permessage-deflate; client_max_window_bits], client_key=E58pVwft35HPkD/MFCjtEA==, server_accept=HxOmr1a2nvOOc4Qiv7Ou3wrCsJc=] @@ -86,8 +86,8 @@ websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, text, payload_len websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 361, data, {"type": "data-message", "topic": "/zeek/event/my_topic", "@data-type": "vector", "data": [{"@data-type": "count", "data websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, text websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 2 -websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, close message-too-big-status.pcap websocket_established, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=60956/tcp, resp_h=127.0.0.1, resp_p=8080/tcp], host=localhost:8080, uri=/, user_agent=Python/3.10 websockets/12.0, subprotocol=v1, client_protocols=[v1], server_extensions=, client_extensions=[permessage-deflate; client_max_window_bits], client_key=iTel1Ova5Nhz/G7VlI2qKg==, server_accept=YsQYYLj7ZCpzTLsVLb+w/ydy79E=] @@ -95,12 +95,12 @@ websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, ping, payload_len websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 4, data, Zeek websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, ping websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 31 -websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1009, reason, over size limit (4 > 2 bytes) websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 31, data, \x03\xf1over size limit (4 > 2 bytes) +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1009, reason, over size limit (4 > 2 bytes) websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, close websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 2 -websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, close two-binary-fragments.pcap websocket_established, CHhAvVGS1DHFjwGM9, 7, [ts=XXXXXXXXXX.XXXXXX, uid=CHhAvVGS1DHFjwGM9, id=[orig_h=127.0.0.1, orig_p=50198/tcp, resp_h=127.0.0.1, resp_p=8080/tcp], host=localhost:8080, uri=/, user_agent=Python/3.10 websockets/12.0, subprotocol=v1, client_protocols=[v1], server_extensions=, client_extensions=[permessage-deflate; client_max_window_bits], client_key=cQGA5Z1nvyUJ9XOVIaLaQA==, server_accept=zWaHVUKxEGPDs+xJeKtzkE1bm54=] @@ -119,10 +119,10 @@ websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, continuation, pay websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 7, data, there! websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, binary websocket_frame, CHhAvVGS1DHFjwGM9, T, fin, T, rsv, 0, opcode, close, payload_len, 2 -websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, T, len, 2, data, \x03\xe8 +websocket_close, CHhAvVGS1DHFjwGM9, T, status, 1000, reason, websocket_message, CHhAvVGS1DHFjwGM9, T, opcode, close websocket_frame, CHhAvVGS1DHFjwGM9, F, fin, T, rsv, 0, opcode, close, payload_len, 2 -websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, websocket_frame_data, CHhAvVGS1DHFjwGM9, F, len, 2, data, \x03\xe8 +websocket_close, CHhAvVGS1DHFjwGM9, F, status, 1000, reason, websocket_message, CHhAvVGS1DHFjwGM9, F, opcode, close From 96542260755960153f8be1076c2e9f226b38cc4a Mon Sep 17 00:00:00 2001 From: Arne Welzel Date: Mon, 22 Jan 2024 14:51:18 +0100 Subject: [PATCH 13/13] websocket: Handle breaking from WebSocket::configure_analyzer() ...and various nits from the review. --- scripts/base/protocols/websocket/main.zeek | 25 ++++++++++---- src/analyzer/protocol/websocket/WebSocket.cc | 4 ++- src/analyzer/protocol/websocket/events.bif | 8 +---- src/analyzer/protocol/websocket/functions.bif | 2 +- .../conn.log.cut | 4 +++ .../out | 5 +++ .../websocket.log | 12 +++++++ .../wstunnel-ssh-configure-break.zeek | 33 +++++++++++++++++++ 8 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/conn.log.cut create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/out create mode 100644 testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/websocket.log create mode 100644 testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure-break.zeek diff --git a/scripts/base/protocols/websocket/main.zeek b/scripts/base/protocols/websocket/main.zeek index dfcfc04685..ab11a46a08 100644 --- a/scripts/base/protocols/websocket/main.zeek +++ b/scripts/base/protocols/websocket/main.zeek @@ -53,7 +53,15 @@ export { ## Log policy hook. global log_policy: Log::PolicyHook; - ## Hook to allow interception of WebSocket analyzer configuration. + ## Experimental: Hook to intercept WebSocket analyzer configuration. + ## + ## Breaking from this hook disables the WebSocket analyzer immediately. + ## To modify the configuration of the analyzer, use the + ## :zeek:see:`WebSocket::AnalyzerConfig` type. + ## + ## While this API allows quite some flexibility currently, should be + ## considered experimental and may change in the future with or + ## without a deprecation phase. ## ## c: The connection ## @@ -77,9 +85,10 @@ function set_websocket(c: connection) ); } -function expected_accept_for(key: string): string { +function expected_accept_for(key: string): string + { return encode_base64(hexstr_to_bytestring(sha1_hash(key + HANDSHAKE_GUID))); -} + } event http_header(c: connection, is_orig: bool, name: string, value: string) { @@ -205,9 +214,13 @@ event websocket_established(c: connection, aid: count) &priority=-5 config$server_extensions = ws$server_extensions; # Give other scripts a chance to modify the analyzer configuration. - hook WebSocket::configure_analyzer(c, aid, config); - - WebSocket::__configure_analyzer(c, aid, config); + # + # Breaking from this hook disables the new WebSocket analyzer + # completely instead of configuring it. + if ( hook WebSocket::configure_analyzer(c, aid, config) ) + WebSocket::__configure_analyzer(c, aid, config); + else + disable_analyzer(c$id, aid); ws$ts = network_time(); Log::write(LOG, ws); diff --git a/src/analyzer/protocol/websocket/WebSocket.cc b/src/analyzer/protocol/websocket/WebSocket.cc index 4b15f4aa10..de098e472c 100644 --- a/src/analyzer/protocol/websocket/WebSocket.cc +++ b/src/analyzer/protocol/websocket/WebSocket.cc @@ -22,7 +22,9 @@ WebSocket_Analyzer::WebSocket_Analyzer(Connection* conn) : analyzer::tcp::TCP_Ap void WebSocket_Analyzer::Init() { tcp::TCP_ApplicationAnalyzer::Init(); - // This event calls back via Configure() + // This event gives scriptland a chance to log and configure the analyzer. + // The WebSocket analyzer ships with a handler that calls back into + // Configure(), via WebSocket::__configure_analyzer(). zeek::BifEvent::enqueue_websocket_established(this, Conn(), GetID()); } diff --git a/src/analyzer/protocol/websocket/events.bif b/src/analyzer/protocol/websocket/events.bif index e2ded2870c..04ea73484f 100644 --- a/src/analyzer/protocol/websocket/events.bif +++ b/src/analyzer/protocol/websocket/events.bif @@ -1,16 +1,10 @@ ## Generated when a WebSocket handshake completed. ## -## This is a bit artificial. It can be used to configure the WebSocket -## analyzer if the HTTP headers contained protocol and extension headers. -## ## c: The WebSocket connection. ## ## aid: The analyzer identifier of the WebSocket analyzer. ## -## .. zeek:see:: WebSocket::__configure_analyzer -## -## .. zeek:see:: WebSocket::configure_analyzer -## +## .. zeek:see:: WebSocket::__configure_analyzer WebSocket::configure_analyzer event websocket_established%(c: connection, aid: count%); ## Generated for every WebSocket frame. diff --git a/src/analyzer/protocol/websocket/functions.bif b/src/analyzer/protocol/websocket/functions.bif index fc33a1a39a..dfa33f631d 100644 --- a/src/analyzer/protocol/websocket/functions.bif +++ b/src/analyzer/protocol/websocket/functions.bif @@ -31,7 +31,7 @@ function __configure_analyzer%(c: connection, aid: count, config: WebSocket::Ana static const auto& config_type = zeek::id::find_type("WebSocket::AnalyzerConfig"); - if ( config->GetType() != config_type ) + if ( config->GetType() != config_type ) { reporter->Warning("config has wrong type %s, expected %s", config->GetType()->GetName().c_str(), diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/conn.log.cut b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/conn.log.cut new file mode 100644 index 0000000000..43dce97d68 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/conn.log.cut @@ -0,0 +1,4 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +ts uid history service +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 ShADadR http +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h ShADadR http diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/out b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/out new file mode 100644 index 0000000000..d4a94d5da7 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/out @@ -0,0 +1,5 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +WebSocket::configure_analyzer, CHhAvVGS1DHFjwGM9, 7 +disabling_analyzer, CHhAvVGS1DHFjwGM9, Analyzer::ANALYZER_WEBSOCKET, 7 +WebSocket::configure_analyzer, ClEkJM2Vm5giqnMf4h, 14 +disabling_analyzer, ClEkJM2Vm5giqnMf4h, Analyzer::ANALYZER_WEBSOCKET, 14 diff --git a/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/websocket.log b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/websocket.log new file mode 100644 index 0000000000..e993d7b4cf --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.websocket.wstunnel-ssh-configure-break/websocket.log @@ -0,0 +1,12 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path websocket +#open XXXX-XX-XX-XX-XX-XX +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p host uri user_agent subprotocol client_protocols server_extensions client_extensions +#types time string addr port addr port string string string string vector[string] vector[string] vector[string] +XXXXXXXXXX.XXXXXX CHhAvVGS1DHFjwGM9 127.0.0.1 42906 127.0.0.1 8888 localhost:8888 /v1/events - v1 v1,authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGQwNTVkLTQ5OTgtNzI5Zi04Yjg2LTMwZTBiZWEyZGE4ZiIsInAiOiJUY3AiLCJyIjoiMTk1LjIwMS4xNDguMjA5IiwicnAiOjIyfQ.jjTNJL12tQbAuhTB9p_geFXRkEHkxcvOS6zf76qDklQ - - +XXXXXXXXXX.XXXXXX ClEkJM2Vm5giqnMf4h 127.0.0.1 46796 127.0.0.1 8888 localhost:8888 /v1/events - v1 v1,authorization.bearer.eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjAxOGQwNTVkLTc4MWYtNzNiYi1hZDkwLTEzNjA5NzRjY2JmMyIsInAiOiJUY3AiLCJyIjoiMTk1LjIwMS4xNDguMjA5IiwicnAiOjIyfQ.2HQ4uC23p_OYIXnQWeSZCqdA3jc_lVVH7-T5xZDPrz4 - - +#close XXXX-XX-XX-XX-XX-XX diff --git a/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure-break.zeek b/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure-break.zeek new file mode 100644 index 0000000000..9a9354f01d --- /dev/null +++ b/testing/btest/scripts/base/protocols/websocket/wstunnel-ssh-configure-break.zeek @@ -0,0 +1,33 @@ +# @TEST-DOC: Test that breaking from configure_analyzer() removes the attached analyzer. +# +# @TEST-EXEC: zeek -b -r $TRACES/websocket/wstunnel-ssh.pcap %INPUT >out 2>&1 +# +# @TEST-EXEC: zeek-cut -m ts uid history service < conn.log > conn.log.cut + +# @TEST-EXEC: btest-diff out +# @TEST-EXEC: btest-diff conn.log.cut +# @TEST-EXEC: btest-diff websocket.log +# @TEST-EXEC: test ! -f ssh.log +# @TEST-EXEC: test ! -f analyzer.log + +@load base/protocols/conn +@load base/protocols/http +@load base/protocols/ssh +@load base/protocols/websocket + +hook WebSocket::configure_analyzer(c: connection, aid: count, config: WebSocket::AnalyzerConfig) + { + print "WebSocket::configure_analyzer", c$uid, aid; + break; + } + +# These should never be raised +event websocket_message(c: connection, is_orig: bool, opcode: count) + { + print "ERROR: websocket_message", c$uid, is_orig, "opcode", WebSocket::opcodes[opcode]; + } + +hook Analyzer::disabling_analyzer(c: connection, atype: AllAnalyzers::Tag, aid: count) + { + print "disabling_analyzer", c$uid, atype, aid; + }