diff --git a/.gitignore b/.gitignore
index 09256f7..710e12f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,8 @@ compile
depcomp
missing
src/dispatcher
+src/rzb-dev
+src/rzb-worker
aclocal.m4
libtool
ltmain.sh
@@ -47,6 +49,17 @@ tests/test_string_list
tests/test_uuids
tests/test_config_file
tests/test_timer
+tests/test_config_next
+tests/test_health
+tests/test_message_body
+tests/test_messages_next
+tests/test_runtime_next
+tests/test_schema_conformance
+tests/test_fileserver
+tests/smoke_inspector_next
+tests/smoke_operational_next
+tests/smoke_source_next
+tests/.smoke-operation.json
tests/test_json_buffer_primitives
tests/test_json_buffer_structs
tests/test_json_buffer_invalid
@@ -55,4 +68,6 @@ tests/*.log
tests/*.trs
test-driver
razorback.pc
+pacp
+txt
/\.idea
diff --git a/.gitmodules b/.gitmodules
index 4855b4b..d254b5c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -17,3 +17,6 @@
[submodule "common_m4"]
path = common_m4
url = ../razorback-common-m4.git
+[submodule "schemas/razorback"]
+ path = schemas/razorback
+ url = ../razorback-schemas.git
diff --git a/Dockerfile b/Dockerfile
index 701acb6..e70941b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -51,3 +51,4 @@ RUN cp /razorback/etc/razorback/magic.sample /razorback/etc/razorback/magic \
&& printf '/razorback/lib\n' > /etc/ld.so.conf.d/razorback.conf \
&& ldconfig
WORKDIR /razorback
+USER razorback
diff --git a/Makefile.am b/Makefile.am
index f383ce3..89e3d85 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -20,3 +20,8 @@ EXTRA_DIST = LICENSE README.md SECURITY.md
pkgconfigdir = $(libdir)/pkgconfig
pkgconfig_DATA = razorback.pc
+
+.PHONY: smoke-dispatcher-next
+
+smoke-dispatcher-next:
+ $(MAKE) -C tests smoke-dispatcher-next
diff --git a/README.md b/README.md
index cb8bdcf..e5cf6b0 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,32 @@ int main(void)
}
```
+## Tooling
+
+The API build now installs two helper binaries for nugget development and
+operation.
+
+`rzb-worker` loads a single nugget module through the existing `initNug()` /
+`shutdownNug()` interface and keeps it running with the normal Razorback
+runtime:
+
+```bash
+src/rzb-worker --debug --health-port=8080 /path/to/nugget.so
+```
+
+`rzb-dev` also loads a nugget through `initNug()` / `shutdownNug()`, but it
+enables an internal local-only dev mode first so the created context does not
+require broker connections or other external Razorback services. It runs one
+file through the nugget's inspection hook and prints the resulting judgments
+and submissions:
+
+```bash
+src/rzb-dev --type=image/svg+xml /path/to/nugget.so /path/to/input.file
+```
+
+If `--type` is omitted, `rzb-dev` will infer the datatype only when the nugget
+registers exactly one inspection datatype.
+
## Health Checks
Razorback provides a small process-wide health subsystem for library consumers
diff --git a/api.vcxproj b/api.vcxproj
index b73e347..7999a1a 100644
--- a/api.vcxproj
+++ b/api.vcxproj
@@ -219,9 +219,10 @@
-
-
-
+
+
+
+
diff --git a/api.vcxproj.filters b/api.vcxproj.filters
index cbe1eab..e68fc36 100644
--- a/api.vcxproj.filters
+++ b/api.vcxproj.filters
@@ -120,12 +120,15 @@
Source Files
-
- Source Files
-
-
- Source Files
-
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
Source Files
diff --git a/common_m4 b/common_m4
index 80fed7f..589fafe 160000
--- a/common_m4
+++ b/common_m4
@@ -1 +1 @@
-Subproject commit 80fed7f391bd756075a3326d08337915a452fd03
+Subproject commit 589fafee91c31749a69c5a265df2273c00b78315
diff --git a/configure.ac b/configure.ac
index 9ff6f8f..5d2783f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -53,6 +53,23 @@ m4_include([common_m4/platform_flags.m4])
# Pull in the dependancies on on the API (use the nugget way).
m4_include([common_m4/api_deps.m4])
+AC_CHECK_LIB([z], [compress2], [], [ZLIB="no"])
+AS_IF([test "x$ZLIB" = "xno"],
+ [AC_MSG_ERROR("zlib library not found")]
+ )
+AC_CHECK_HEADERS([zlib.h], [], [ZLIB_HEADER="no"])
+AS_IF([test "x$ZLIB_HEADER" = "xno"],
+ [AC_MSG_ERROR("zlib header file not found")]
+ )
+AC_CHECK_LIB([yaml], [yaml_parser_initialize], [], [YAML="no"])
+AS_IF([test "x$YAML" = "xno"],
+ [AC_MSG_ERROR("libyaml library not found")]
+ )
+AC_CHECK_HEADERS([yaml.h], [], [YAML_HEADER="no"])
+AS_IF([test "x$YAML_HEADER" = "xno"],
+ [AC_MSG_ERROR("libyaml header file not found")]
+ )
+
# Pull in the standard header and function checks.
m4_include([common_m4/standard_headers_functions.m4])
diff --git a/debian/control b/debian/control
index d35ddaf..016326a 100644
--- a/debian/control
+++ b/debian/control
@@ -1,7 +1,7 @@
Source: librazorback
Priority: extra
Maintainer: Tom Judge
-Build-Depends: debhelper (>= 7.0.50~), autotools-dev, uuid-dev, libmagic-dev, libssl-dev, libconfig8-dev, libpcre3-dev, libmysqlclient-dev, libcurl4-openssl-dev, cdbs
+Build-Depends: debhelper (>= 7.0.50~), autotools-dev, uuid-dev, libmagic-dev, libssl-dev, zlib1g-dev, libyaml-dev, libconfig8-dev, libpcre3-dev, libmysqlclient-dev, libcurl4-openssl-dev, cdbs
Standards-Version: 3.9.1
Section: libs
Homepage: http://razorbacktm.sourceforge.net/
diff --git a/docker-bake.hcl b/docker-bake.hcl
index 8bcc98a..79a78eb 100644
--- a/docker-bake.hcl
+++ b/docker-bake.hcl
@@ -25,7 +25,7 @@ variable "BASE_IMAGE" {
}
variable "REPO" {
- default = "harbor.vrt.sourcefire.com/pinkhat/"
+ default = "ghcr.io/cisco-talos/razorback/"
}
variable "VERSION" {
diff --git a/etc/api.conf.sample b/etc/api.conf.sample
index dbc7996..fd61167 100644
--- a/etc/api.conf.sample
+++ b/etc/api.conf.sample
@@ -5,8 +5,6 @@ Global: {
HelloTime=2;
DeadTime=10;
MessageFormat="json";
- TransferPassword="razorback";
- TransferMode="traditional";
};
Locality: {
diff --git a/include/Makefile.am b/include/Makefile.am
index ce1c42b..b0c2ac2 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -21,12 +21,14 @@ razorback/api.h \
razorback/block.h \
razorback/block_id.h \
razorback/block_pool.h \
+razorback/config_next.h \
razorback/config_file.h \
razorback/connected_entity.h \
razorback/daemon.h \
razorback/debug.h \
razorback/event.h \
razorback/file.h \
+razorback/fileserver.h \
razorback.h \
razorback/hash.h \
razorback/health.h \
@@ -37,6 +39,8 @@ razorback/list.h \
razorback/lock.h \
razorback/log.h \
razorback/message_formats.h \
+razorback/message_body.h \
+razorback/messages_next.h \
razorback/messages.h \
razorback/metadata.h \
razorback/ntlv.h \
@@ -44,6 +48,7 @@ razorback/nugget.h \
razorback/queue.h \
razorback/queue_list.h \
razorback/response_queue.h \
+razorback/runtime_next.h \
razorback/socket.h \
razorback/string_list.h \
razorback/submission.h \
@@ -51,7 +56,6 @@ razorback/telemetry.h \
razorback/thread.h \
razorback/thread_pool.h \
razorback/timer.h \
-razorback/transfer.h \
razorback/types.h \
razorback/uuids.h \
razorback/visibility.h
diff --git a/include/razorback.h b/include/razorback.h
index a0a972b..53d6bc6 100644
--- a/include/razorback.h
+++ b/include/razorback.h
@@ -27,16 +27,20 @@
#include
#include
#include
+#include
#include
#include
#include
#include
#include
#include
+#include
+#include
#include
#include
#include
#include
+#include
#include
#include
#include
diff --git a/include/razorback/api.h b/include/razorback/api.h
index f57890d..ff6c5cf 100644
--- a/include/razorback/api.h
+++ b/include/razorback/api.h
@@ -105,6 +105,7 @@ struct RazorbackCommandAndControlHooks
#define CONTEXT_FLAG_STAND_ALONE 0x00000001
+#define CONTEXT_FLAG_DEV_TOOL 0x00000002
/** API Context
*/
@@ -251,12 +252,6 @@ SO_PUBLIC extern bool Razorback_Output_Launch(
struct RazorbackOutputHooks *hooks
);
-/**
- * Get the transfer server protocol.
- * @return The password.
- */
-SO_PUBLIC extern char * Razorback_Get_Transfer_Password(void);
-
/* Make APIs standardized while keeping function naming convention */
#define RZB_Register_Collector Razorback_Init_Collection_Context
#define RZB_DataBlock_Create BlockPool_CreateItem
diff --git a/src/transfer/core.h b/include/razorback/config_next.h
similarity index 58%
rename from src/transfer/core.h
rename to include/razorback/config_next.h
index 8a03263..a8b1588 100644
--- a/src/transfer/core.h
+++ b/include/razorback/config_next.h
@@ -16,32 +16,41 @@
* MA 02110-1301, USA.
*/
-#ifndef TRANSFER_CORE_H
-#define TRANSFER_CORE_H
-#include
-#include
-#include
+/** @file config_next.h
+ * Dispatcher-next YAML/env configuration helper.
+ */
+#ifndef RAZORBACK_CONFIG_NEXT_H
+#define RAZORBACK_CONFIG_NEXT_H
+#include
+#include
#ifdef __cplusplus
extern "C" {
#endif
+enum RzbNextConfigType
+{
+ RZB_NEXT_CONFIG_STRING = 0,
+ RZB_NEXT_CONFIG_INT = 1,
+ RZB_NEXT_CONFIG_BOOL = 2
+};
+
+struct RzbNextConfigKey
+{
+ const char *key;
+ enum RzbNextConfigType type;
+ void *dest;
+};
+
+SO_PUBLIC extern bool RzbNextConfig_Load(
+ const char *baseFile,
+ const char *localFile,
+ const char *envPrefix,
+ const struct RzbNextConfigKey *keys
+);
-
-char * Transfer_generateFilename (struct Block *block);
-
-bool Transport_IsSupported(uint8_t protocol);
-enum TransferStatus Transfer_Store(struct BlockPoolItem *item, struct ConnectedEntity *dispatcher);
-enum TransferStatus Transfer_Fetch(struct Block *block, struct ConnectedEntity *dispatcher);
-void Transfer_Free(struct Block *block, struct ConnectedEntity *dispatcher);
-
-
-// Init functions
-bool File_Init(void);
-bool SSH_Init(void);
-bool HTTP_Init(void);
#ifdef __cplusplus
}
#endif
-#endif
+#endif /* RAZORBACK_CONFIG_NEXT_H */
diff --git a/include/razorback/fileserver.h b/include/razorback/fileserver.h
new file mode 100644
index 0000000..77ae263
--- /dev/null
+++ b/include/razorback/fileserver.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+/** @file fileserver.h
+ * Dispatcher-next HTTP(S) fileserver block transport.
+ */
+#ifndef RAZORBACK_FILESERVER_H
+#define RAZORBACK_FILESERVER_H
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct RzbNextFileserverClient RzbNextFileserverClient_t;
+
+enum RzbNextFileserverStatus
+{
+ RZB_NEXT_FILESERVER_OK = 0,
+ RZB_NEXT_FILESERVER_LOCAL_ERROR = 1,
+ RZB_NEXT_FILESERVER_NOT_FOUND = 2,
+ RZB_NEXT_FILESERVER_RETRYABLE = 3,
+ RZB_NEXT_FILESERVER_HTTP_ERROR = 4,
+ RZB_NEXT_FILESERVER_PAYLOAD_MISMATCH = 5
+};
+
+SO_PUBLIC extern RzbNextFileserverClient_t * RzbNextFileserverClient_Create(
+ const char *baseUrl,
+ uint64_t fetchTimeoutSeconds,
+ uint64_t uploadTimeoutSeconds
+);
+SO_PUBLIC extern void RzbNextFileserverClient_Destroy(
+ RzbNextFileserverClient_t *client
+);
+SO_PUBLIC extern enum RzbNextFileserverStatus RzbNextFileserver_BuildUrl(
+ const RzbNextFileserverClient_t *client,
+ const struct BlockId *blockId,
+ char **url
+);
+SO_PUBLIC extern enum RzbNextFileserverStatus RzbNextFileserver_StoreFile(
+ RzbNextFileserverClient_t *client,
+ const struct BlockId *blockId,
+ const char *fileName
+);
+SO_PUBLIC extern enum RzbNextFileserverStatus RzbNextFileserver_StoreBytes(
+ RzbNextFileserverClient_t *client,
+ const struct BlockId *blockId,
+ const uint8_t *data,
+ size_t length
+);
+SO_PUBLIC extern enum RzbNextFileserverStatus RzbNextFileserver_StoreBlockPoolItem(
+ RzbNextFileserverClient_t *client,
+ struct BlockPoolItem *item
+);
+SO_PUBLIC extern enum RzbNextFileserverStatus RzbNextFileserver_FetchToFile(
+ RzbNextFileserverClient_t *client,
+ const struct BlockId *blockId,
+ char **fileName
+);
+SO_PUBLIC extern enum RzbNextFileserverStatus RzbNextFileserver_FetchBlock(
+ RzbNextFileserverClient_t *client,
+ struct Block *block
+);
+SO_PUBLIC extern bool RzbNextFileserver_AttachFileToBlock(
+ struct Block *block,
+ char *fileName,
+ bool tempFile
+);
+SO_PUBLIC extern void RzbNextFileserver_FreeBlockData(struct Block *block);
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* RAZORBACK_FILESERVER_H */
diff --git a/include/razorback/message_body.h b/include/razorback/message_body.h
new file mode 100644
index 0000000..abcf14f
--- /dev/null
+++ b/include/razorback/message_body.h
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+/** @file message_body.h
+ * Dispatcher-next message body encoding helpers.
+ */
+#ifndef RAZORBACK_MESSAGE_BODY_H
+#define RAZORBACK_MESSAGE_BODY_H
+
+#include
+#include
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define MESSAGE_BODY_DEFAULT_MAX_INLINE_BYTES 921600U
+#define MESSAGE_BODY_DEFAULT_MAX_EXPANDED_BYTES 268435456U
+#define MESSAGE_BODY_CONTENT_ENCODING_ZLIB "zlib"
+#define MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_NAME "razorback.messages.claim_check_reference"
+#define MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_VERSION 1U
+
+enum MessageBodyMode
+{
+ MESSAGE_BODY_MODE_INLINE = 0,
+ MESSAGE_BODY_MODE_ZLIB = 1,
+ MESSAGE_BODY_MODE_CLAIM_CHECK = 2
+};
+
+struct MessageBodyPolicy
+{
+ size_t maxInlineBytes;
+ size_t maxExpandedBytes;
+};
+
+struct ClaimCheckReference
+{
+ char *referenceSchemaName;
+ uint32_t referenceSchemaVersion;
+ char *signedUrl;
+ char *expiresAt;
+ char *bucket;
+ char *objectKey;
+ char *sha256;
+ uint64_t uncompressedSize;
+ uint64_t storedCompressedSize;
+ char *contentType;
+ char *schemaName;
+ uint32_t schemaVersion;
+ char *contentEncoding;
+};
+
+struct EncodedMessageBody
+{
+ enum MessageBodyMode mode;
+ uint8_t *transportBody;
+ size_t transportBodySize;
+ char *contentEncoding;
+ uint8_t *claimCheckBody;
+ size_t claimCheckBodySize;
+ struct ClaimCheckReference *claimCheckReference;
+};
+
+SO_PUBLIC extern struct MessageBodyPolicy MessageBodyPolicy_Default(void);
+SO_PUBLIC extern const char * MessageBodyMode_ToString(enum MessageBodyMode mode);
+
+SO_PUBLIC extern struct ClaimCheckReference * ClaimCheckReference_Create(
+ const char *signedUrl,
+ const char *expiresAt,
+ const char *bucket,
+ const char *objectKey,
+ const char *contentType,
+ const char *schemaName,
+ uint32_t schemaVersion
+);
+SO_PUBLIC extern struct ClaimCheckReference * ClaimCheckReference_Clone(
+ const struct ClaimCheckReference *source
+);
+SO_PUBLIC extern void ClaimCheckReference_Destroy(struct ClaimCheckReference *reference);
+SO_PUBLIC extern char * ClaimCheckReference_ToJson(
+ const struct ClaimCheckReference *reference
+);
+SO_PUBLIC extern struct ClaimCheckReference * ClaimCheckReference_FromJson(
+ const char *json
+);
+
+SO_PUBLIC extern bool MessageBody_Encode(
+ const struct MessageBodyPolicy *policy,
+ const uint8_t *body,
+ size_t bodySize,
+ const struct ClaimCheckReference *claimCheckTemplate,
+ struct EncodedMessageBody **encoded
+);
+SO_PUBLIC extern bool MessageBody_DecodeInline(
+ const uint8_t *body,
+ size_t bodySize,
+ const char *contentEncoding,
+ uint8_t **decoded,
+ size_t *decodedSize
+);
+SO_PUBLIC extern bool MessageBody_DecodeInlineWithPolicy(
+ const struct MessageBodyPolicy *policy,
+ const uint8_t *body,
+ size_t bodySize,
+ const char *contentEncoding,
+ uint8_t **decoded,
+ size_t *decodedSize
+);
+SO_PUBLIC extern bool MessageBody_DecodeClaimCheck(
+ const uint8_t *compressedBody,
+ size_t compressedBodySize,
+ const struct ClaimCheckReference *reference,
+ uint8_t **decoded,
+ size_t *decodedSize
+);
+SO_PUBLIC extern bool MessageBody_DecodeClaimCheckWithPolicy(
+ const struct MessageBodyPolicy *policy,
+ const uint8_t *compressedBody,
+ size_t compressedBodySize,
+ const struct ClaimCheckReference *reference,
+ uint8_t **decoded,
+ size_t *decodedSize
+);
+SO_PUBLIC extern void EncodedMessageBody_Destroy(struct EncodedMessageBody *encoded);
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* RAZORBACK_MESSAGE_BODY_H */
diff --git a/include/razorback/messages_next.h b/include/razorback/messages_next.h
new file mode 100644
index 0000000..9392d29
--- /dev/null
+++ b/include/razorback/messages_next.h
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+/** @file messages_next.h
+ * Dispatcher-next message identity and routing helpers.
+ */
+#ifndef RAZORBACK_MESSAGES_NEXT_H
+#define RAZORBACK_MESSAGES_NEXT_H
+
+#include
+#include
+#include
+
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define RZB_NEXT_SCHEMA_VERSION 1U
+
+#define RZB_NEXT_METADATA_DEFAULT_MAX_VALUE_BYTES (5U * 1024U * 1024U)
+#define RZB_NEXT_METADATA_MAX_VALUE_BYTES_ENV "RZB_METADATA__MAX_VALUE_BYTES"
+
+#define RZB_NEXT_SCHEMA_CLAIM_CHECK_REFERENCE "razorback.messages.claim_check_reference"
+#define RZB_NEXT_SCHEMA_CNC_REGISTRATION_REQUEST "razorback.cnc.registration_request"
+#define RZB_NEXT_SCHEMA_CNC_REGISTRATION_ACCEPTED "razorback.cnc.registration_accepted"
+#define RZB_NEXT_SCHEMA_CNC_REGISTRATION_REJECTED "razorback.cnc.registration_rejected"
+#define RZB_NEXT_SCHEMA_CNC_LIVENESS "razorback.cnc.liveness"
+#define RZB_NEXT_SCHEMA_CNC_BYE "razorback.cnc.bye"
+#define RZB_NEXT_SCHEMA_CNC_DIRECTED_COMMAND "razorback.cnc.directed_command"
+#define RZB_NEXT_SCHEMA_CNC_DISPATCHER_HELLO "razorback.cnc.dispatcher_hello"
+#define RZB_NEXT_SCHEMA_BLOCK_SUBMISSION "razorback.messages.block_submission"
+#define RZB_NEXT_SCHEMA_BLOCK_UPDATE "razorback.messages.block_update"
+#define RZB_NEXT_SCHEMA_INSPECTION_WORK "razorback.messages.inspection_work"
+#define RZB_NEXT_SCHEMA_ANALYSIS_RESULT "razorback.analysis_result.envelope"
+#define RZB_NEXT_SCHEMA_CACHE_REQUEST "razorback.cache.request"
+#define RZB_NEXT_SCHEMA_CACHE_RESPONSE "razorback.cache.response"
+#define RZB_NEXT_SCHEMA_CATALOG_INVALIDATION "razorback.catalog_invalidation.event"
+#define RZB_NEXT_SCHEMA_FILE_REMOVE_REQUEST "razorback.file_remove.request"
+#define RZB_NEXT_SCHEMA_FILE_REMOVE_RESULT "razorback.file_remove.result"
+#define RZB_NEXT_SCHEMA_SEARCH_EXPORT "razorback.search_export.record"
+
+#define RZB_NEXT_QUEUE_COMMAND "COMMAND"
+#define RZB_NEXT_QUEUE_DIRECTED_COMMAND_PREFIX "COMMAND"
+#define RZB_NEXT_EXCHANGE_DISPATCHER_HELLO "DISPATCHER.HELLO"
+#define RZB_NEXT_QUEUE_INPUT "INPUT"
+#define RZB_NEXT_QUEUE_BLOCK_UPDATE "BLOCK_UPDATE"
+#define RZB_NEXT_QUEUE_INSPECTOR_PREFIX "INSPECTOR"
+#define RZB_NEXT_QUEUE_ANALYSIS_RESULT "ANALYSIS_RESULT"
+#define RZB_NEXT_QUEUE_CACHE_REQUEST "REQUEST"
+#define RZB_NEXT_QUEUE_CACHE_RESPONSE_PREFIX "CACHE_RESPONSE"
+#define RZB_NEXT_EXCHANGE_CATALOG_INVALIDATION "CATALOG.INVALIDATION"
+#define RZB_NEXT_EXCHANGE_FILE_REMOVE "FILE_REMOVE"
+#define RZB_NEXT_QUEUE_FILE_REMOVE_FILE_STORE "FILE_REMOVE.FILE_STORE"
+#define RZB_NEXT_EXCHANGE_FILE_REMOVE_VARNISH "FILE_REMOVE.VARNISH"
+#define RZB_NEXT_QUEUE_FILE_REMOVE_VARNISH "FILE_REMOVE.VARNISH.INVALIDATE"
+#define RZB_NEXT_ROUTING_KEY_FILE_REMOVE_VARNISH "file_store.removed"
+#define RZB_NEXT_QUEUE_FILE_REMOVE_RESULT "FILE_REMOVE.RESULT"
+
+#define RZB_NEXT_HEADER_SCHEMA_NAME "rzb-schema-name"
+#define RZB_NEXT_HEADER_SCHEMA_VERSION "rzb-schema-version"
+#define RZB_NEXT_HEADER_BODY_MODE "rzb-body-mode"
+#define RZB_NEXT_HEADER_CONTENT_ENCODING "content-encoding"
+
+enum RzbNextTransport
+{
+ RZB_NEXT_TRANSPORT_RABBITMQ = 0,
+ RZB_NEXT_TRANSPORT_KAFKA = 1
+};
+
+enum RzbNextCacheSubmitDecision
+{
+ RZB_NEXT_CACHE_SUBMIT_NEW = 0,
+ RZB_NEXT_CACHE_SKIP_KNOWN = 1,
+ RZB_NEXT_CACHE_SUBMIT_FOR_REINSPECTION = 2,
+ RZB_NEXT_CACHE_RESTORE_AND_SUBMIT_FOR_REINSPECTION = 3,
+ RZB_NEXT_CACHE_TIMEOUT = 4,
+ RZB_NEXT_CACHE_BACKEND_UNAVAILABLE = 5,
+ RZB_NEXT_CACHE_INVALID_REQUEST = 6
+};
+
+struct RzbNextRoute
+{
+ enum RzbNextTransport transport;
+ char *exchange;
+ char *routingKey;
+};
+
+struct RzbNextMessageHeader
+{
+ char *name;
+ char *value;
+};
+
+struct RzbNextPreparedRabbitMqMessage
+{
+ struct RzbNextRoute *route;
+ struct RzbNextMessageHeader *headers;
+ size_t headerCount;
+ uint8_t *body;
+ size_t bodySize;
+ char *contentType;
+ char *contentEncoding;
+ uint8_t *claimCheckBody;
+ size_t claimCheckBodySize;
+ struct ClaimCheckReference *claimCheckReference;
+};
+
+struct RzbNextDecodedRabbitMqMessage
+{
+ char *jsonMessage;
+ struct ClaimCheckReference *claimCheckReference;
+};
+
+SO_PUBLIC extern const char * RzbNextTransport_ToString(
+ enum RzbNextTransport transport
+);
+SO_PUBLIC extern size_t RzbNextMetadata_MaxValueBytes(void);
+SO_PUBLIC extern bool RzbNextMessage_IsKnownSchema(const char *schemaName);
+SO_PUBLIC extern bool RzbNextMessage_ValidateIdentity(const char *jsonMessage);
+SO_PUBLIC extern bool RzbNextMessage_Validate(const char *jsonMessage);
+SO_PUBLIC extern bool RzbNextMessage_Route(
+ const char *jsonMessage,
+ const char *cacheRequestorUuid,
+ struct RzbNextRoute **route
+);
+SO_PUBLIC extern enum RzbNextCacheSubmitDecision RzbNextCache_SubmitDecision(
+ const char *jsonMessage
+);
+SO_PUBLIC extern bool RzbNextRabbitMq_PrepareMessage(
+ const char *jsonMessage,
+ const char *cacheRequestorUuid,
+ const struct MessageBodyPolicy *policy,
+ const struct ClaimCheckReference *claimCheckTemplate,
+ struct RzbNextPreparedRabbitMqMessage **prepared
+);
+SO_PUBLIC extern bool RzbNextRabbitMq_DecodeMessage(
+ const uint8_t *body,
+ size_t bodySize,
+ const struct RzbNextMessageHeader *headers,
+ size_t headerCount,
+ const uint8_t *claimCheckBody,
+ size_t claimCheckBodySize,
+ struct RzbNextDecodedRabbitMqMessage **decoded
+);
+SO_PUBLIC extern bool RzbNextRabbitMq_DecodeMessageWithPolicy(
+ const struct MessageBodyPolicy *policy,
+ const uint8_t *body,
+ size_t bodySize,
+ const struct RzbNextMessageHeader *headers,
+ size_t headerCount,
+ const uint8_t *claimCheckBody,
+ size_t claimCheckBodySize,
+ struct RzbNextDecodedRabbitMqMessage **decoded
+);
+SO_PUBLIC extern bool RzbNextCnc_IsReadyDispatcherHello(const char *jsonMessage);
+SO_PUBLIC extern bool RzbNextCnc_RegistrationAcceptedTiming(
+ const char *jsonMessage,
+ uint64_t *livenessInterval,
+ uint64_t *livenessFreshnessWindow,
+ uint64_t *livenessClockSkewTolerance
+);
+SO_PUBLIC extern char * RzbNextCnc_DirectedCommandQueue(const char *nuggetUuid);
+SO_PUBLIC extern char * RzbNextAnalysisResult_BuildCompleted(
+ const char *inspectionWorkJson,
+ const char *inspectorUuid,
+ const char *createdAt,
+ const char *blockMetadataUpdatesJson,
+ const char *metadataJson,
+ const char *tagMutationsJson,
+ const char *alertsJson
+);
+SO_PUBLIC extern char * RzbNextAnalysisResult_BuildError(
+ const char *inspectionWorkJson,
+ const char *inspectorUuid,
+ const char *category,
+ const char *code,
+ const char *message,
+ const char *detailsJson,
+ const char *createdAt
+);
+SO_PUBLIC extern char * RzbNextAnalysisResult_BuildDeferred(
+ const char *inspectionWorkJson,
+ const char *inspectorUuid,
+ const char *reasonCode,
+ const char *message,
+ const char *pollAfter,
+ const char *detailsJson,
+ const char *createdAt
+);
+SO_PUBLIC extern void RzbNext_FreeString(char *value);
+SO_PUBLIC extern void RzbNextRoute_Destroy(struct RzbNextRoute *route);
+SO_PUBLIC extern void RzbNextPreparedRabbitMqMessage_Destroy(
+ struct RzbNextPreparedRabbitMqMessage *prepared
+);
+SO_PUBLIC extern void RzbNextDecodedRabbitMqMessage_Destroy(
+ struct RzbNextDecodedRabbitMqMessage *decoded
+);
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* RAZORBACK_MESSAGES_NEXT_H */
diff --git a/include/razorback/runtime_next.h b/include/razorback/runtime_next.h
new file mode 100644
index 0000000..488b147
--- /dev/null
+++ b/include/razorback/runtime_next.h
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+/** @file runtime_next.h
+ * Dispatcher-next cluster-join runtime helpers.
+ */
+#ifndef RAZORBACK_RUNTIME_NEXT_H
+#define RAZORBACK_RUNTIME_NEXT_H
+
+#include
+#include
+#include
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct RzbNextRuntime RzbNextRuntime_t;
+
+enum RzbNextRuntimeState
+{
+ RZB_NEXT_RUNTIME_STARTING = 0,
+ RZB_NEXT_RUNTIME_WAITING_FOR_DISPATCHER = 1,
+ RZB_NEXT_RUNTIME_REGISTERING = 2,
+ RZB_NEXT_RUNTIME_READY = 3,
+ RZB_NEXT_RUNTIME_PAUSED = 4,
+ RZB_NEXT_RUNTIME_DRAINING = 5,
+ RZB_NEXT_RUNTIME_STOPPED = 6,
+ RZB_NEXT_RUNTIME_FAILED = 7
+};
+
+enum RzbNextRuntimeTransitionKind
+{
+ RZB_NEXT_RUNTIME_TRANSITION_NONE = 0,
+ RZB_NEXT_RUNTIME_TRANSITION_REGISTERED = 1,
+ RZB_NEXT_RUNTIME_TRANSITION_REJECTED = 2
+};
+
+enum RzbNextRuntimeDirectedEffect
+{
+ RZB_NEXT_RUNTIME_DIRECTED_IGNORED_STALE = 0,
+ RZB_NEXT_RUNTIME_DIRECTED_PAUSE = 1,
+ RZB_NEXT_RUNTIME_DIRECTED_RESUME = 2,
+ RZB_NEXT_RUNTIME_DIRECTED_SHUTDOWN = 3,
+ RZB_NEXT_RUNTIME_DIRECTED_REREGISTER = 4,
+ RZB_NEXT_RUNTIME_DIRECTED_CACHE_INVALIDATE = 5
+};
+
+enum RzbNextRuntimeRetryAction
+{
+ RZB_NEXT_RUNTIME_RETRY_START_NOW = 0,
+ RZB_NEXT_RUNTIME_RETRY_ALREADY_IN_FLIGHT = 1,
+ RZB_NEXT_RUNTIME_RETRY_AFTER = 2,
+ RZB_NEXT_RUNTIME_RETRY_STOP = 3
+};
+
+struct RzbNextRuntimeHealth
+{
+ bool startupz;
+ bool healthz;
+ bool readyz;
+};
+
+struct RzbNextRuntimeTransition
+{
+ enum RzbNextRuntimeTransitionKind kind;
+ bool ready;
+ bool retryable;
+ uint64_t retryAfter;
+ char generation[37];
+ char reasonCode[128];
+};
+
+struct RzbNextRuntimeDirectedResult
+{
+ enum RzbNextRuntimeDirectedEffect effect;
+ bool staleGeneration;
+ char invalidationId[37];
+};
+
+typedef bool (*RzbNextRuntimeCallbackFn)(
+ RzbNextRuntime_t *runtime,
+ void *userData
+);
+typedef bool (*RzbNextRuntimeTransitionCallbackFn)(
+ RzbNextRuntime_t *runtime,
+ const struct RzbNextRuntimeTransition *transition,
+ void *userData
+);
+typedef bool (*RzbNextRuntimeCacheInvalidateCallbackFn)(
+ RzbNextRuntime_t *runtime,
+ const char *invalidationId,
+ void *userData
+);
+typedef bool (*RzbNextRuntimeErrorCallbackFn)(
+ RzbNextRuntime_t *runtime,
+ const char *reasonCode,
+ bool retryable,
+ void *userData
+);
+
+struct RzbNextRuntimeCallbacks
+{
+ RzbNextRuntimeTransitionCallbackFn onRegistered;
+ RzbNextRuntimeCallbackFn onReady;
+ RzbNextRuntimeCallbackFn onPause;
+ RzbNextRuntimeCallbackFn onResume;
+ RzbNextRuntimeCacheInvalidateCallbackFn onCacheInvalidate;
+ RzbNextRuntimeCallbackFn onShutdown;
+ RzbNextRuntimeErrorCallbackFn onError;
+ void *userData;
+};
+
+struct RzbNextRuntimeLivenessPlan
+{
+ char *message;
+ uint64_t interval;
+ uint64_t freshnessWindow;
+ uint64_t clockSkewTolerance;
+ uint64_t messageExpiration;
+};
+
+struct RzbNextRuntimeRetryPolicy
+{
+ uint64_t initialBackoff;
+ uint64_t maxBackoff;
+ uint8_t jitterPercent;
+};
+
+struct RzbNextRuntimeRetryState
+{
+ struct RzbNextRuntimeRetryPolicy policy;
+ bool inFlight;
+ bool dirtyRerun;
+ uint32_t consecutiveFailureCount;
+};
+
+struct RzbNextRuntimeRetryDecision
+{
+ enum RzbNextRuntimeRetryAction action;
+ uint64_t delay;
+};
+
+SO_PUBLIC extern const char * RzbNextRuntime_StateString(
+ enum RzbNextRuntimeState state
+);
+SO_PUBLIC extern RzbNextRuntime_t * RzbNextRuntime_Create(
+ const char *nuggetUuid,
+ const char *processUuid
+);
+SO_PUBLIC extern RzbNextRuntime_t * RzbNextRuntime_CreateGenerated(
+ const char *nuggetUuid
+);
+SO_PUBLIC extern void RzbNextRuntime_Destroy(RzbNextRuntime_t *runtime);
+SO_PUBLIC extern bool RzbNextRuntime_SetCallbacks(
+ RzbNextRuntime_t *runtime,
+ const struct RzbNextRuntimeCallbacks *callbacks
+);
+SO_PUBLIC extern uint32_t RzbNextRuntime_CallbackFailureCount(
+ const RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern const char * RzbNextRuntime_LastCallbackFailure(
+ const RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern enum RzbNextRuntimeState RzbNextRuntime_State(
+ const RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern const char * RzbNextRuntime_ProcessUuid(
+ const RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern const char * RzbNextRuntime_RegistrationGeneration(
+ const RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern struct RzbNextRuntimeHealth RzbNextRuntime_Health(
+ const RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern bool RzbNextRuntime_HealthStartupCheck(void *userData);
+SO_PUBLIC extern bool RzbNextRuntime_HealthLiveCheck(void *userData);
+SO_PUBLIC extern bool RzbNextRuntime_HealthReadyCheck(void *userData);
+SO_PUBLIC extern void RzbNextRuntime_Initialize(RzbNextRuntime_t *runtime);
+SO_PUBLIC extern bool RzbNextRuntime_ObserveDispatcherHello(
+ RzbNextRuntime_t *runtime,
+ const char *jsonMessage
+);
+SO_PUBLIC extern bool RzbNextRuntime_BeginRegistration(
+ RzbNextRuntime_t *runtime,
+ const char *requestId
+);
+SO_PUBLIC extern bool RzbNextRuntime_RegistrationAccepted(
+ RzbNextRuntime_t *runtime,
+ const char *jsonMessage,
+ struct RzbNextRuntimeTransition *transition
+);
+SO_PUBLIC extern bool RzbNextRuntime_RegistrationRejected(
+ RzbNextRuntime_t *runtime,
+ const char *jsonMessage,
+ struct RzbNextRuntimeTransition *transition
+);
+SO_PUBLIC extern bool RzbNextRuntime_ApplyDirectedCommand(
+ RzbNextRuntime_t *runtime,
+ const char *jsonMessage,
+ struct RzbNextRuntimeDirectedResult *result
+);
+SO_PUBLIC extern bool RzbNextRuntime_LivenessPlan(
+ const RzbNextRuntime_t *runtime,
+ const char *createdAt,
+ struct RzbNextRuntimeLivenessPlan *plan
+);
+SO_PUBLIC extern char * RzbNextRuntime_BuildBye(
+ const RzbNextRuntime_t *runtime,
+ const char *reason,
+ const char *createdAt
+);
+SO_PUBLIC extern void RzbNextRuntime_LivenessPlanClear(
+ struct RzbNextRuntimeLivenessPlan *plan
+);
+SO_PUBLIC extern bool RzbNextRuntime_AcceptsWork(
+ const RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern bool RzbNextRuntime_StartWhenReady(
+ RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern void RzbNextRuntime_CompleteWork(RzbNextRuntime_t *runtime);
+SO_PUBLIC extern uint32_t RzbNextRuntime_InFlightWork(
+ const RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern void RzbNextRuntime_PauseNewWork(RzbNextRuntime_t *runtime);
+SO_PUBLIC extern bool RzbNextRuntime_ResumeWhenReady(RzbNextRuntime_t *runtime);
+SO_PUBLIC extern bool RzbNextRuntime_Drain(RzbNextRuntime_t *runtime);
+SO_PUBLIC extern void RzbNextRuntime_BeginDraining(RzbNextRuntime_t *runtime);
+SO_PUBLIC extern bool RzbNextRuntime_DependencyUnavailable(
+ RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern bool RzbNextRuntime_DependencyRecovered(
+ RzbNextRuntime_t *runtime
+);
+SO_PUBLIC extern void RzbNextRuntime_MarkStopped(RzbNextRuntime_t *runtime);
+SO_PUBLIC extern void RzbNextRuntime_MarkFailed(RzbNextRuntime_t *runtime);
+SO_PUBLIC extern void RzbNextRuntimeRetry_Init(
+ struct RzbNextRuntimeRetryState *state,
+ struct RzbNextRuntimeRetryPolicy policy
+);
+SO_PUBLIC extern struct RzbNextRuntimeRetryDecision
+RzbNextRuntimeRetry_RequestRegistration(struct RzbNextRuntimeRetryState *state);
+SO_PUBLIC extern void RzbNextRuntimeRetry_Accepted(
+ struct RzbNextRuntimeRetryState *state
+);
+SO_PUBLIC extern struct RzbNextRuntimeRetryDecision
+RzbNextRuntimeRetry_RetryableFailure(
+ struct RzbNextRuntimeRetryState *state,
+ uint64_t retryAfter,
+ uint8_t jitterPercentile
+);
+SO_PUBLIC extern struct RzbNextRuntimeRetryDecision
+RzbNextRuntimeRetry_TerminalFailure(struct RzbNextRuntimeRetryState *state);
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* RAZORBACK_RUNTIME_NEXT_H */
diff --git a/include/razorback/telemetry.h b/include/razorback/telemetry.h
index 5f8c325..0c3235c 100644
--- a/include/razorback/telemetry.h
+++ b/include/razorback/telemetry.h
@@ -29,6 +29,13 @@
extern "C" {
#endif
+#define RAZORBACK_RUNTIME_DEPENDENCY_STATE_METRIC "razorback.runtime.dependency.state"
+#define RAZORBACK_RUNTIME_WORKFLOW_STATE_METRIC "razorback.runtime.workflow.state"
+#define RAZORBACK_RUNTIME_READINESS_STATE_METRIC "razorback.runtime.readiness.state"
+#define RAZORBACK_RUNTIME_STARTUP_DURATION_METRIC "razorback.runtime.startup.duration"
+#define RAZORBACK_RUNTIME_SHUTDOWN_DRAIN_DURATION_METRIC "razorback.runtime.shutdown.drain.duration"
+#define RAZORBACK_RUNTIME_TELEMETRY_FLUSH_OUTCOME_METRIC "razorback.runtime.telemetry.flush.outcome"
+
typedef enum TelemetrySpanKind
{
TELEMETRY_SPAN_KIND_INTERNAL = 0,
@@ -422,6 +429,43 @@ SO_PUBLIC extern void Telemetry_ObservableObserveDouble(
size_t attributeCount
);
+SO_PUBLIC extern void Telemetry_RecordRuntimeDependencyState(
+ const char *dependency,
+ const char *status,
+ const char *reasonCode
+);
+
+SO_PUBLIC extern void Telemetry_RecordRuntimeWorkflowState(
+ const char *workflow,
+ const char *state,
+ const char *reasonCode
+);
+
+SO_PUBLIC extern void Telemetry_RecordRuntimeReadinessState(
+ const char *state,
+ const char *outcome,
+ const char *reasonCode
+);
+
+SO_PUBLIC extern void Telemetry_RecordRuntimeStartupDuration(
+ double durationSeconds,
+ const char *outcome,
+ const char *reasonCode
+);
+
+SO_PUBLIC extern void Telemetry_RecordRuntimeShutdownDrainDuration(
+ double durationSeconds,
+ const char *service,
+ const char *outcome,
+ const char *reasonCode
+);
+
+SO_PUBLIC extern void Telemetry_RecordRuntimeTelemetryFlushOutcome(
+ const char *service,
+ const char *outcome,
+ const char *reasonCode
+);
+
#ifdef __cplusplus
}
#endif
diff --git a/include/razorback/transfer.h b/include/razorback/transfer.h
deleted file mode 100644
index 250d1c7..0000000
--- a/include/razorback/transfer.h
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (c) 2011-2026 Cisco Systems, Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-//
-// Created by amishhammer on 3/2/24.
-//
-
-#ifndef RAZORBACK_TRANSFER_H
-#define RAZORBACK_TRANSFER_H
-#include
-#include
-#include
-
-#define TRANSFER_MODE_FILE 0
-#define TRANSFER_MODE_SSH 1
-#define TRANSFER_MODE_HTTP 2
-#define TRANSFER_MODE_HTTPS 3
-#define TRANSFER_MODE_CUSTOM 255
-
-enum TransferStatus {
- TRANSFER_OK, ///< Block was transfered successfully
- TRANSFER_FAIL_DISPATCHER, ///< An error was encountered talking to the dispatcher (dispatcher will be marked as bad)
- TRANSFER_FAIL_LOCAL, ///< A local error occured (Dispatcher status will be unchanged)
-};
-
-/**
- * Transfer implementation descriptor.
- */
-struct TransportDescriptor {
- uint8_t id;
- const char *name;
- const char *description;
- enum TransferStatus (*store)(struct BlockPoolItem *item, struct ConnectedEntity *dispatcher);
- enum TransferStatus (*fetch)(struct Block *block, struct ConnectedEntity *dispatcher);
-};
-
-#define DECL_TRANSFER_INIT bool transferInit(void)
-
-#define DECL_TRANSFER_STORE enum TransferStatus Plugin_Store(struct BlockPoolItem *item, struct ConnectedEntity *dispatcher)
-#define DECL_TRANSFER_FETCH enum TransferStatus Plugin_Fetch(struct Block *block, struct ConnectedEntity *dispatcher)
-
-/**
- * Register a transport descriptor.
- * @param desc Transport descriptor to register.
- * @return true on success, false on failure.
- */
-SO_PUBLIC extern bool Transport_Register(struct TransportDescriptor *desc);
-
-/**
- * Store a block using the file transport.
- * @param item Block pool item to store.
- * @param dispatcher Dispatcher to use for the transfer.
- * @return Transfer status result.
- */
-SO_PUBLIC extern enum TransferStatus Transfer_File_Store(
- struct BlockPoolItem *item,
- struct ConnectedEntity *dispatcher
-);
-
-/**
- * Fetch a block using the file transport.
- * @param block Block to fetch.
- * @param dispatcher Dispatcher to use for the transfer.
- * @return Transfer status result.
- */
-SO_PUBLIC extern enum TransferStatus Transfer_File_Fetch(
- struct Block *block,
- struct ConnectedEntity *dispatcher
-);
-
-/**
- * Store a block using the SSH transport.
- * @param item Block pool item to store.
- * @param dispatcher Dispatcher to use for the transfer.
- * @return Transfer status result.
- */
-SO_PUBLIC extern enum TransferStatus Transfer_SSH_Store(
- struct BlockPoolItem *item,
- struct ConnectedEntity *dispatcher
-);
-
-/**
- * Fetch a block using the SSH transport.
- * @param block Block to fetch.
- * @param dispatcher Dispatcher to use for the transfer.
- * @return Transfer status result.
- */
-SO_PUBLIC extern enum TransferStatus Transfer_SSH_Fetch(
- struct Block *block,
- struct ConnectedEntity *dispatcher
-);
-
-/**
- * Store a block using the HTTP transport.
- * @param item Block pool item to store.
- * @param dispatcher Dispatcher to use for the transfer.
- * @return Transfer status result.
- */
-SO_PUBLIC extern enum TransferStatus Transfer_HTTP_Store(
- struct BlockPoolItem *item,
- struct ConnectedEntity *dispatcher
-);
-
-/**
- * Fetch a block using the HTTP transport.
- * @param block Block to fetch.
- * @param dispatcher Dispatcher to use for the transfer.
- * @return Transfer status result.
- */
-SO_PUBLIC extern enum TransferStatus Transfer_HTTP_Fetch(
- struct Block *block,
- struct ConnectedEntity *dispatcher
-);
-
-/**
- * Prepare a file for block transfer.
- * @param block Block to operate on.
- * @param file Destination file path.
- * @param temp Set to true when the prepared file should be treated as temporary.
- * @return true on success, false on failure.
- */
-SO_PUBLIC extern bool Transfer_Prepare_File(struct Block *block, char *file, bool temp);
-#endif //RAZORBACK_TRANSFER_H
diff --git a/include/razorback/types.h b/include/razorback/types.h
index a9bec49..db2a5e0 100644
--- a/include/razorback/types.h
+++ b/include/razorback/types.h
@@ -248,14 +248,6 @@ struct DeferredList
#define JUDGMENT_REASON_PENDING 4 ///< Inspection result current pending (@note This is never sent in a message, it is the state the dispatcher stores for an inspection record.)
/// @}
-/** Transfer protocols
- * @{
- */
-#define TRANSFER_METHOD_FILE 0 ///< Transfer to block store shared file system
-#define TRANSFER_METHOD_SSH 1 ///< Transfer via SSH+SFTP
-#define TRANSFER_METHOD_HTTP 2 ///< Transfer via HTTP
-/// @}
-
/** Submission types.
* @{
*/
diff --git a/schemas/README.md b/schemas/README.md
new file mode 100644
index 0000000..e6dc17c
--- /dev/null
+++ b/schemas/README.md
@@ -0,0 +1,13 @@
+# C SDK Schemas
+
+`schemas/razorback` must point at the pinned `razorback-schemas` repository for
+dispatcher-next conformance fixtures.
+
+Run the pinned schema and fixture conformance check with:
+
+```bash
+python3 tools/schema_conformance.py
+```
+
+The same check is wired into the C SDK `make check` workflow through
+`tests/test_schema_conformance.c`.
diff --git a/schemas/razorback b/schemas/razorback
new file mode 160000
index 0000000..0e54bc9
--- /dev/null
+++ b/schemas/razorback
@@ -0,0 +1 @@
+Subproject commit 0e54bc90c2147ca7ed24c80985a12ee1c8d2913f
diff --git a/src/Makefile.am b/src/Makefile.am
index bc19aee..9f4cb7f 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -15,24 +15,26 @@
# MA 02110-1301, USA.
lib_LTLIBRARIES = librazorback_api.la
+bin_PROGRAMS = rzb-worker rzb-dev
noinst_HEADERS = \
api_internal.h \
block_pool_private.h \
command_and_control.h \
connected_entity_private.h \
+dev_mode.h \
fantasia.h \
health_internal.h \
init.h \
inspection.h \
judgment_private.h \
local_cache.h \
+nugget_tool.h \
messages/core.h \
messages/cnc/core.h \
runtime_config.h \
submission_private.h \
-telemetry.h \
-transfer/core.h
+telemetry.h
librazorback_api_la_SOURCES = \
api.c \
@@ -40,12 +42,15 @@ block.c \
block_pool.c \
block_id.c \
command_and_control.c \
+config_next.c \
config_file.c \
connected_entity.c \
crypto.c \
daemon.c \
+dev_mode.c \
event.c \
fantasia.c \
+fileserver.c \
hash.c \
health.c \
init.c \
@@ -57,6 +62,8 @@ lock.c \
log.c \
judgment.c \
json_buffer.c \
+message_body.c \
+messages_next.c \
metadata.c \
messages/alert_child.c \
messages/alert_primary.c \
@@ -87,6 +94,7 @@ nugget.c \
queue.c \
queue_list.c \
response_queue.c \
+runtime_next.c \
runtime_config.c \
socket.c \
string_list.c \
@@ -94,20 +102,25 @@ submission.c \
thread.c \
thread_pool.c \
timer.c \
-transfer/core.c \
-transfer/file.c \
-transfer/http.c \
-transfer/ssh.c \
uuids.c \
telemetry_otel.cc
-#librazorback_api_la_CFLAGS = -DBUILDING_SO -I$(top_srcdir)/libssh/include -I$(top_srcdir)/include
-#librazorback_api_la_LDFLAGS = -version-info @APIVERSION@ @XCCFLAGS@ ../libssh/src/libssh.la
+rzb_worker_SOURCES = \
+rzb_worker.c \
+nugget_tool.c
+
+rzb_worker_LDADD = librazorback_api.la @OPENTELEMETRY_CPP_LIBS@
+
+rzb_dev_SOURCES = \
+rzb_dev.c \
+nugget_tool.c
+
+rzb_dev_LDADD = librazorback_api.la @OPENTELEMETRY_CPP_LIBS@
+
librazorback_api_la_CFLAGS = -DBUILDING_SO -I$(top_srcdir)/include
librazorback_api_la_LDFLAGS = -version-info @APIVERSION@ @XCCFLAGS@ -Wl,--disable-new-dtags -Wl,-rpath,'$$ORIGIN'
librazorback_api_la_LIBADD = @OPENTELEMETRY_CPP_LIBS@
librazorback_api_la_CPPFLAGS = $(AM_CPPFLAGS) @OPENTELEMETRY_CPP_CFLAGS@
-#AM_CPPFLAGS = -I$(srcdir) -I$(top_srcdir)/libssh/include -I$(top_srcdir)/include @extra_incl@
AM_CPPFLAGS = -I$(srcdir) -I$(top_srcdir)/include @extra_incl@
diff --git a/src/api.c b/src/api.c
index 7a72bff..be17e8d 100644
--- a/src/api.c
+++ b/src/api.c
@@ -41,6 +41,7 @@
#endif //_MSC_VER
#include "api_internal.h"
#include "command_and_control.h"
+#include "dev_mode.h"
#include "submission_private.h"
#include "judgment_private.h"
#include "runtime_config.h"
@@ -76,6 +77,8 @@ void Razorback_Destroy_Context(struct RazorbackContext *context) {
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Context is NULL", __func__);
return;
}
+ if ((context->iFlags & CONTEXT_FLAG_DEV_TOOL) == CONTEXT_FLAG_DEV_TOOL)
+ Razorback_DevMode_UnregisterContext(context);
if (context->inspector.dataTypeList != NULL)
free(context->inspector.dataTypeList);
if (context->regSem != NULL)
@@ -152,7 +155,7 @@ Razorback_Init_Inspection_Context (uuid_t nuggetId,
uuid_copy (context->uuidNuggetId, nuggetId);
uuid_copy (context->uuidNuggetType, uuidInspector);
uuid_copy (context->uuidApplicationType, applicationType);
- context->iFlags = 0;
+ context->iFlags = Razorback_DevMode_IsEnabled() ? CONTEXT_FLAG_DEV_TOOL : 0;
context->inspector.dataTypeCount = dataTypeCount;
context->inspector.dataTypeList = dataTypeList;
context->inspector.dataTypeList = calloc(dataTypeCount, sizeof(uuid_t));
@@ -171,6 +174,18 @@ Razorback_Init_Inspection_Context (uuid_t nuggetId,
return NULL;
}
+ if ((context->iFlags & CONTEXT_FLAG_DEV_TOOL) == CONTEXT_FLAG_DEV_TOOL) {
+ if (!Razorback_DevMode_RegisterContext(context)) {
+ rzb_log(LOG_ERR, LOG_C_CORE,
+ "%s: Failed to initialize dev-mode capture state",
+ __func__);
+ Razorback_Remove_Context(context);
+ Razorback_Destroy_Context(context);
+ return NULL;
+ }
+ return context;
+ }
+
if ((context->inspector.judgmentQueue = Queue_Create(JUDGMENT_QUEUE, false, QUEUE_FLAG_SEND)) == NULL) {
rzb_log (LOG_ERR, LOG_C_CORE, "%s: Failed to create judgment queue", __func__);
Razorback_Remove_Context(context);
@@ -208,7 +223,7 @@ Razorback_Init_Output_Context (uuid_t nuggetId,
uuid_copy (context->uuidNuggetId, nuggetId);
uuid_copy (context->uuidNuggetType, uuidOutput);
uuid_copy (context->uuidApplicationType, applicationType);
- context->iFlags = 0;
+ context->iFlags = Razorback_DevMode_IsEnabled() ? CONTEXT_FLAG_DEV_TOOL : 0;
context->pCommandHooks = NULL;
context->inspector.hooks = NULL;
context->output.threads = List_Create(LIST_MODE_GENERIC,
@@ -244,6 +259,8 @@ Razorback_Init_Collection_Context (uuid_t nuggetId,
uuid_copy (context->uuidNuggetType, uuidCollection);
uuid_copy (context->uuidApplicationType, applicationType);
context->iFlags = CONTEXT_FLAG_STAND_ALONE;
+ if (Razorback_DevMode_IsEnabled())
+ context->iFlags |= CONTEXT_FLAG_DEV_TOOL;
context->inspector.dataTypeCount = 0;
context->inspector.dataTypeList = NULL;
context->pCommandHooks = NULL;
@@ -268,15 +285,24 @@ Kill_Output_Thread(void *ut, void *ud) {
SO_PUBLIC void
Razorback_Shutdown_Context (struct RazorbackContext *context) {
- CommandAndControl_SendBye(context);
+ bool useCommandAndControl;
- CommandAndControl_Pause();
- List_Remove(sg_ContextList, context);
- CommandAndControl_Unpause();
+ useCommandAndControl =
+ ((context->iFlags & CONTEXT_FLAG_DEV_TOOL) != CONTEXT_FLAG_DEV_TOOL);
+
+ if (useCommandAndControl) {
+ CommandAndControl_SendBye(context);
+ CommandAndControl_Pause();
+ List_Remove(sg_ContextList, context);
+ CommandAndControl_Unpause();
+ } else {
+ List_Remove(sg_ContextList, context);
+ }
Inspection_Shutdown(context);
Submission_Shutdown(context);
- if ((context->iFlags & CONTEXT_FLAG_STAND_ALONE) ==
+ if (useCommandAndControl &&
+ (context->iFlags & CONTEXT_FLAG_STAND_ALONE) ==
CONTEXT_FLAG_STAND_ALONE) {
CommandAndControl_Shutdown();
}
@@ -344,6 +370,18 @@ Razorback_Render_Verdict (struct Judgment *judgment) {
}
context = Thread_GetCurrentContext();
+ if (context == NULL) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to resolve current context", __func__);
+ return false;
+ }
+
+ if ((context->iFlags & CONTEXT_FLAG_DEV_TOOL) == CONTEXT_FLAG_DEV_TOOL) {
+ if (!Razorback_DevMode_CaptureVerdict(context, judgment)) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to capture local dev verdict", __func__);
+ return false;
+ }
+ return true;
+ }
if ((message = MessageJudgmentSubmission_Initialize (JUDGMENT_REASON_ALERT, judgment)) == NULL) {
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to create message", __func__);
diff --git a/src/command_and_control.c b/src/command_and_control.c
index a547d49..c285130 100644
--- a/src/command_and_control.c
+++ b/src/command_and_control.c
@@ -96,12 +96,14 @@ static struct RazorbackCommandAndControlHooks sg_DefaultHooks = {
void
CommandAndControl_Pause(void) {
- Mutex_Lock(processLock);
+ if (processLock != NULL)
+ Mutex_Lock(processLock);
}
void
CommandAndControl_Unpause(void) {
- Mutex_Unlock(processLock);
+ if (processLock != NULL)
+ Mutex_Unlock(processLock);
}
bool
@@ -117,6 +119,17 @@ CommandAndControl_Start (struct RazorbackContext *p_pContext) {
if (p_pContext->pCommandHooks == NULL)
p_pContext->pCommandHooks = &sg_DefaultHooks;
+ if ((p_pContext->iFlags & CONTEXT_FLAG_DEV_TOOL) == CONTEXT_FLAG_DEV_TOOL) {
+ p_pContext->regOk = true;
+ Semaphore_Post(p_pContext->regSem);
+ return true;
+ }
+
+ rzb_log(LOG_ERR, LOG_C_CNC,
+ "%s: Legacy dispatcher C&C runtime was removed for dispatcher-next; "
+ "use RzbNextRuntime instead", __func__);
+ return false;
+
if ((p_pContext->iFlags & CONTEXT_FLAG_STAND_ALONE) ==
CONTEXT_FLAG_STAND_ALONE) {
if (!sg_bQueueInitialized) {
@@ -169,6 +182,11 @@ CommandAndControl_Start (struct RazorbackContext *p_pContext) {
void
CommandAndControl_Shutdown(void) {
+ rzb_log(LOG_DEBUG, LOG_C_CNC,
+ "%s: Legacy dispatcher C&C runtime is disabled for dispatcher-next",
+ __func__);
+ return;
+
// Shut down state tracking timer.
ConnectedEntityList_Stop();
@@ -483,6 +501,11 @@ CommandAndControl_SendBye (struct RazorbackContext *context) {
return false;
}
+ rzb_log(LOG_ERR, LOG_C_CNC,
+ "%s: Legacy dispatcher C&C runtime was removed for dispatcher-next; "
+ "use RzbNextRuntime shutdown instead", __func__);
+ return false;
+
if ((bye = MessageBye_Initialize(
context->uuidNuggetId)) == NULL) {
rzb_log(LOG_ERR, LOG_C_CNC, "%s: Failed to create bye message", __func__);
@@ -500,7 +523,7 @@ CommandAndControl_SendBye (struct RazorbackContext *context) {
static bool
CommandAndControl_Register (struct RazorbackContext *p_pContext,
- const char *phase) {
+ const char *stage) {
struct Message *regReq;
struct ConnectedEntity *dispatcher = NULL;
double waitStartedAt;
@@ -521,7 +544,7 @@ CommandAndControl_Register (struct RazorbackContext *p_pContext,
}
Telemetry_RecordDispatcherWait(Telemetry_GetMonotonicTimeSeconds() - waitStartedAt,
"available",
- phase,
+ stage,
p_pContext);
if ((regReq = MessageRegistrationRequest_Initialize(
diff --git a/src/config_next.c b/src/config_next.c
new file mode 100644
index 0000000..db34298
--- /dev/null
+++ b/src/config_next.c
@@ -0,0 +1,465 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+extern char **environ;
+
+struct ConfigBinding
+{
+ const struct RzbNextConfigKey *key;
+ char *ownedString;
+};
+
+static bool
+RzbNextConfig_Strndup(const char *value, size_t length, char **copy)
+{
+ char *allocated;
+
+ if (value == NULL || copy == NULL)
+ return false;
+ allocated = malloc(length + 1);
+ if (allocated == NULL)
+ return false;
+ memcpy(allocated, value, length);
+ allocated[length] = '\0';
+ *copy = allocated;
+ return true;
+}
+
+static bool
+RzbNextConfig_KeySegmentValid(const char *value)
+{
+ size_t index;
+
+ if (value == NULL || value[0] == '\0')
+ return false;
+ if (!islower((unsigned char)value[0]) && !isdigit((unsigned char)value[0]))
+ return false;
+ for (index = 1; value[index] != '\0'; index++) {
+ if (!islower((unsigned char)value[index]) &&
+ !isdigit((unsigned char)value[index]) &&
+ value[index] != '_') {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool
+RzbNextConfig_KeyValid(const char *key)
+{
+ char *segment;
+ char *cursor;
+ char *copy;
+ bool valid = true;
+
+ if (key == NULL || key[0] == '\0')
+ return false;
+ copy = strdup(key);
+ if (copy == NULL)
+ return false;
+ segment = copy;
+ for (cursor = copy; ; cursor++) {
+ if (*cursor == '.' || *cursor == '\0') {
+ char saved = *cursor;
+
+ *cursor = '\0';
+ if (!RzbNextConfig_KeySegmentValid(segment))
+ valid = false;
+ *cursor = saved;
+ if (saved == '\0')
+ break;
+ segment = cursor + 1;
+ }
+ }
+ free(copy);
+ return valid;
+}
+
+static bool
+RzbNextConfig_FileExists(const char *path)
+{
+ struct stat statbuf;
+
+ return path != NULL && path[0] != '\0' && stat(path, &statbuf) == 0 &&
+ S_ISREG(statbuf.st_mode);
+}
+
+static size_t
+RzbNextConfig_KeyCount(const struct RzbNextConfigKey *keys)
+{
+ size_t count = 0;
+
+ if (keys == NULL)
+ return 0;
+ while (keys[count].key != NULL)
+ count++;
+ return count;
+}
+
+static bool
+RzbNextConfig_ValidateBindings(struct ConfigBinding *bindings, size_t count)
+{
+ size_t index;
+ size_t other;
+
+ if (bindings == NULL || count == 0)
+ return false;
+ for (index = 0; index < count; index++) {
+ if (bindings[index].key == NULL ||
+ bindings[index].key->dest == NULL ||
+ !RzbNextConfig_KeyValid(bindings[index].key->key)) {
+ return false;
+ }
+ for (other = index + 1; other < count; other++) {
+ if (strcmp(bindings[index].key->key, bindings[other].key->key) == 0)
+ return false;
+ }
+ }
+ return true;
+}
+
+static struct ConfigBinding *
+RzbNextConfig_FindBinding(struct ConfigBinding *bindings, size_t count,
+ const char *key)
+{
+ size_t index;
+
+ for (index = 0; index < count; index++) {
+ if (strcmp(bindings[index].key->key, key) == 0)
+ return &bindings[index];
+ }
+ return NULL;
+}
+
+static bool
+RzbNextConfig_HasPrefixBinding(struct ConfigBinding *bindings, size_t count,
+ const char *prefix)
+{
+ size_t index;
+ size_t length;
+
+ length = strlen(prefix);
+ for (index = 0; index < count; index++) {
+ if (strncmp(bindings[index].key->key, prefix, length) == 0 &&
+ bindings[index].key->key[length] == '.') {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool
+RzbNextConfig_IsOwnedSection(struct ConfigBinding *bindings, size_t count,
+ const char *key)
+{
+ size_t index;
+ size_t sectionLength;
+ const char *dot;
+
+ dot = strchr(key, '.');
+ sectionLength = dot == NULL ? strlen(key) : (size_t)(dot - key);
+ for (index = 0; index < count; index++) {
+ if (strncmp(bindings[index].key->key, key, sectionLength) == 0 &&
+ (bindings[index].key->key[sectionLength] == '.' ||
+ bindings[index].key->key[sectionLength] == '\0')) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool
+RzbNextConfig_ParseBool(const char *value, bool *parsed)
+{
+ if (value == NULL || parsed == NULL)
+ return false;
+ if (strcasecmp(value, "true") == 0 || strcmp(value, "1") == 0 ||
+ strcasecmp(value, "yes") == 0 || strcasecmp(value, "on") == 0) {
+ *parsed = true;
+ return true;
+ }
+ if (strcasecmp(value, "false") == 0 || strcmp(value, "0") == 0 ||
+ strcasecmp(value, "no") == 0 || strcasecmp(value, "off") == 0) {
+ *parsed = false;
+ return true;
+ }
+ return false;
+}
+
+static bool
+RzbNextConfig_ApplyValue(struct ConfigBinding *binding, const char *value)
+{
+ long parsedInt;
+ char *end = NULL;
+ char *copy;
+ bool parsedBool;
+
+ if (binding == NULL || value == NULL)
+ return false;
+ switch (binding->key->type) {
+ case RZB_NEXT_CONFIG_STRING:
+ copy = strdup(value);
+ if (copy == NULL)
+ return false;
+ free(binding->ownedString);
+ binding->ownedString = copy;
+ *(char **)binding->key->dest = copy;
+ return true;
+
+ case RZB_NEXT_CONFIG_INT:
+ errno = 0;
+ parsedInt = strtol(value, &end, 10);
+ while (end != NULL && isspace((unsigned char)*end))
+ end++;
+ if (errno != 0 || end == value || (end != NULL && *end != '\0') ||
+ parsedInt < INT_MIN || parsedInt > INT_MAX) {
+ return false;
+ }
+ *(int *)binding->key->dest = (int)parsedInt;
+ return true;
+
+ case RZB_NEXT_CONFIG_BOOL:
+ if (!RzbNextConfig_ParseBool(value, &parsedBool))
+ return false;
+ *(bool *)binding->key->dest = parsedBool;
+ return true;
+ }
+ return false;
+}
+
+static char *
+RzbNextConfig_JoinKey(const char *prefix, const char *name)
+{
+ char *joined;
+ size_t length;
+
+ if (name == NULL || name[0] == '\0')
+ return NULL;
+ if (prefix == NULL || prefix[0] == '\0')
+ return strdup(name);
+ length = strlen(prefix) + 1 + strlen(name) + 1;
+ joined = malloc(length);
+ if (joined == NULL)
+ return NULL;
+ snprintf(joined, length, "%s.%s", prefix, name);
+ return joined;
+}
+
+static bool
+RzbNextConfig_LoadMapping(yaml_document_t *document, yaml_node_t *node,
+ const char *prefix,
+ struct ConfigBinding *bindings, size_t count)
+{
+ yaml_node_pair_t *pair;
+
+ if (node == NULL || node->type != YAML_MAPPING_NODE)
+ return false;
+ for (pair = node->data.mapping.pairs.start;
+ pair < node->data.mapping.pairs.top; pair++) {
+ yaml_node_t *keyNode = yaml_document_get_node(document, pair->key);
+ yaml_node_t *valueNode = yaml_document_get_node(document, pair->value);
+ const char *keyText;
+ char *fullKey;
+ bool result = false;
+
+ if (keyNode == NULL || keyNode->type != YAML_SCALAR_NODE ||
+ valueNode == NULL) {
+ return false;
+ }
+ keyText = (const char *)keyNode->data.scalar.value;
+ if (!RzbNextConfig_KeySegmentValid(keyText))
+ return false;
+ fullKey = RzbNextConfig_JoinKey(prefix, keyText);
+ if (fullKey == NULL)
+ return false;
+
+ if (valueNode->type == YAML_MAPPING_NODE) {
+ if (RzbNextConfig_HasPrefixBinding(bindings, count, fullKey)) {
+ result = RzbNextConfig_LoadMapping(document, valueNode, fullKey,
+ bindings, count);
+ }
+ } else if (valueNode->type == YAML_SCALAR_NODE) {
+ struct ConfigBinding *binding;
+
+ binding = RzbNextConfig_FindBinding(bindings, count, fullKey);
+ if (binding != NULL) {
+ result = RzbNextConfig_ApplyValue(
+ binding,
+ (const char *)valueNode->data.scalar.value
+ );
+ }
+ }
+ free(fullKey);
+ if (!result)
+ return false;
+ }
+ return true;
+}
+
+static bool
+RzbNextConfig_LoadYamlFile(const char *path, bool required,
+ struct ConfigBinding *bindings, size_t count)
+{
+ FILE *file;
+ yaml_parser_t parser;
+ yaml_document_t document;
+ yaml_node_t *root;
+ bool loaded = false;
+ bool result = false;
+
+ if (!RzbNextConfig_FileExists(path))
+ return !required;
+ file = fopen(path, "rb");
+ if (file == NULL)
+ return false;
+ if (!yaml_parser_initialize(&parser)) {
+ fclose(file);
+ return false;
+ }
+ yaml_parser_set_input_file(&parser, file);
+ loaded = yaml_parser_load(&parser, &document) != 0;
+ yaml_parser_delete(&parser);
+ fclose(file);
+ if (!loaded)
+ return false;
+
+ root = yaml_document_get_root_node(&document);
+ if (root != NULL) {
+ result = RzbNextConfig_LoadMapping(&document, root, "", bindings,
+ count);
+ }
+ yaml_document_delete(&document);
+ return result;
+}
+
+static char *
+RzbNextConfig_EnvToKey(const char *envName, const char *prefix)
+{
+ char *key;
+ size_t prefixLength;
+ size_t input;
+ size_t output = 0;
+
+ prefixLength = strlen(prefix);
+ if (strncmp(envName, prefix, prefixLength) != 0 ||
+ envName[prefixLength] != '_') {
+ return NULL;
+ }
+ envName += prefixLength + 1;
+ if (envName[0] == '\0')
+ return NULL;
+ key = calloc(strlen(envName) + 1, sizeof(char));
+ if (key == NULL)
+ return NULL;
+
+ for (input = 0; envName[input] != '\0'; input++) {
+ if (envName[input] == '_' && envName[input + 1] == '_') {
+ key[output++] = '.';
+ input++;
+ } else {
+ key[output++] = (char)tolower((unsigned char)envName[input]);
+ }
+ }
+ key[output] = '\0';
+ return key;
+}
+
+static bool
+RzbNextConfig_ApplyEnv(const char *envPrefix, struct ConfigBinding *bindings,
+ size_t count)
+{
+ char **env;
+
+ for (env = environ; env != NULL && *env != NULL; env++) {
+ char *equals = strchr(*env, '=');
+ char *envName;
+ char *key;
+ struct ConfigBinding *binding;
+ bool applied;
+
+ if (equals == NULL)
+ continue;
+ if (!RzbNextConfig_Strndup(*env, (size_t)(equals - *env), &envName))
+ return false;
+ key = RzbNextConfig_EnvToKey(envName, envPrefix);
+ free(envName);
+ if (key == NULL)
+ continue;
+ if (!RzbNextConfig_KeyValid(key)) {
+ free(key);
+ return false;
+ }
+ binding = RzbNextConfig_FindBinding(bindings, count, key);
+ if (binding == NULL) {
+ bool owned = RzbNextConfig_IsOwnedSection(bindings, count, key);
+
+ free(key);
+ if (owned)
+ return false;
+ continue;
+ }
+ applied = RzbNextConfig_ApplyValue(binding, equals + 1);
+ free(key);
+ if (!applied)
+ return false;
+ }
+ return true;
+}
+
+SO_PUBLIC bool
+RzbNextConfig_Load(const char *baseFile, const char *localFile,
+ const char *envPrefix,
+ const struct RzbNextConfigKey *keys)
+{
+ struct ConfigBinding *bindings;
+ size_t count;
+ size_t index;
+ bool result;
+
+ count = RzbNextConfig_KeyCount(keys);
+ if (count == 0)
+ return false;
+ bindings = calloc(count, sizeof(*bindings));
+ if (bindings == NULL)
+ return false;
+ for (index = 0; index < count; index++)
+ bindings[index].key = &keys[index];
+
+ result = RzbNextConfig_ValidateBindings(bindings, count) &&
+ RzbNextConfig_LoadYamlFile(baseFile, true, bindings, count) &&
+ RzbNextConfig_LoadYamlFile(localFile, false, bindings, count) &&
+ RzbNextConfig_ApplyEnv(envPrefix == NULL ? "RZB" : envPrefix,
+ bindings, count);
+
+ free(bindings);
+ return result;
+}
diff --git a/src/connected_entity.c b/src/connected_entity.c
index a058447..ab6a6a5 100644
--- a/src/connected_entity.c
+++ b/src/connected_entity.c
@@ -31,7 +31,6 @@
#include "connected_entity_private.h"
#include "runtime_config.h"
#include "telemetry.h"
-#include "transfer/core.h"
#define SEARCH_KEY_NUGGET_ID (1 << 0)
#define SEARCH_KEY_APP_TYPE (1 << 1)
#define SEARCH_KEY_NUGGET_TYPE (1 << 2)
@@ -181,7 +180,7 @@ ConnectedEntityList_GetEntity (struct Message *message)
ret->dispatcher->flags = hello->flags;
ret->dispatcher->port = hello->port;
ret->dispatcher->protocol = hello->protocol;
- ret->dispatcher->usable = Transport_IsSupported(hello->protocol);
+ ret->dispatcher->usable = true;
if ((ret->dispatcher->addressList = List_Clone(hello->addressList)) == NULL) {
free(ret->dispatcher);
free(ret);
diff --git a/src/dev_mode.c b/src/dev_mode.c
new file mode 100644
index 0000000..62c6cfa
--- /dev/null
+++ b/src/dev_mode.c
@@ -0,0 +1,331 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include "dev_mode.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+struct RazorbackDevContextCapture
+{
+ struct RazorbackContext *context;
+ List_t *judgments;
+ List_t *submissions;
+};
+
+static atomic_bool sg_devModeEnabled;
+static List_t *sg_devCaptures;
+static Mutex_t *sg_devCaptureLock;
+
+static int
+DevCapture_Cmp(void *a, void *b)
+{
+ const struct RazorbackDevContextCapture *left = a;
+ const struct RazorbackDevContextCapture *right = b;
+
+ if (left == right)
+ return 0;
+ if (left == NULL || right == NULL)
+ return -1;
+ return (left->context == right->context) ? 0 : -1;
+}
+
+static int
+DevCapture_KeyCmp(void *a, const void *id)
+{
+ const struct RazorbackDevContextCapture *capture = a;
+ const struct RazorbackContext *context = id;
+
+ if (capture == NULL || context == NULL)
+ return -1;
+ return (capture->context == context) ? 0 : -1;
+}
+
+static void
+DevCapture_Destroy(void *item)
+{
+ struct RazorbackDevContextCapture *capture = item;
+
+ if (capture == NULL)
+ return;
+
+ if (capture->judgments != NULL)
+ List_Destroy(capture->judgments);
+ if (capture->submissions != NULL)
+ List_Destroy(capture->submissions);
+ free(capture);
+}
+
+static void
+DevCapture_DestroySubmission(void *item)
+{
+ if (item == NULL)
+ return;
+
+ (void)BlockPool_DestroyItem(item);
+}
+
+static struct Judgment *
+DevMode_CloneJudgment(const struct Judgment *source)
+{
+ struct Judgment *copy;
+
+ if (source == NULL)
+ return NULL;
+
+ copy = calloc(1, sizeof(*copy));
+ if (copy == NULL)
+ return NULL;
+
+ uuid_copy(copy->uuidNuggetId, source->uuidNuggetId);
+ copy->iSeconds = source->iSeconds;
+ copy->iNanoSecs = source->iNanoSecs;
+ copy->iPriority = source->iPriority;
+ copy->iGID = source->iGID;
+ copy->iSID = source->iSID;
+ copy->Set_SfFlags = source->Set_SfFlags;
+ copy->Set_EntFlags = source->Set_EntFlags;
+ copy->Unset_SfFlags = source->Unset_SfFlags;
+ copy->Unset_EntFlags = source->Unset_EntFlags;
+
+ if (source->pEventId != NULL) {
+ copy->pEventId = EventId_Clone(source->pEventId);
+ if (copy->pEventId == NULL)
+ goto error;
+ }
+
+ if (source->pBlockId != NULL) {
+ copy->pBlockId = BlockId_Clone(source->pBlockId);
+ if (copy->pBlockId == NULL)
+ goto error;
+ }
+
+ if (source->pMetaDataList != NULL) {
+ copy->pMetaDataList = List_Clone(source->pMetaDataList);
+ if (copy->pMetaDataList == NULL)
+ goto error;
+ }
+
+ if (source->sMessage != NULL) {
+ copy->sMessage = (uint8_t *)strdup((const char *)source->sMessage);
+ if (copy->sMessage == NULL)
+ goto error;
+ }
+
+ return copy;
+
+error:
+ Judgment_Destroy(copy);
+ return NULL;
+}
+
+static struct RazorbackDevContextCapture *
+DevMode_GetCaptureLocked(struct RazorbackContext *context)
+{
+ if (sg_devCaptures == NULL || context == NULL)
+ return NULL;
+
+ return List_Find(sg_devCaptures, context);
+}
+
+bool
+Razorback_DevMode_Initialize(void)
+{
+ if (sg_devCaptures != NULL && sg_devCaptureLock != NULL)
+ return true;
+
+ sg_devCaptures = List_Create(LIST_MODE_GENERIC,
+ DevCapture_Cmp,
+ DevCapture_KeyCmp,
+ DevCapture_Destroy,
+ NULL,
+ NULL,
+ NULL);
+ sg_devCaptureLock = Mutex_Create(MUTEX_MODE_NORMAL);
+ atomic_init(&sg_devModeEnabled, false);
+
+ if (sg_devCaptures == NULL || sg_devCaptureLock == NULL) {
+ if (sg_devCaptures != NULL) {
+ List_Destroy(sg_devCaptures);
+ sg_devCaptures = NULL;
+ }
+ if (sg_devCaptureLock != NULL) {
+ Mutex_Destroy(sg_devCaptureLock);
+ sg_devCaptureLock = NULL;
+ }
+ return false;
+ }
+
+ return true;
+}
+
+void
+Razorback_DevMode_SetEnabled(bool enabled)
+{
+ atomic_store(&sg_devModeEnabled, enabled);
+}
+
+bool
+Razorback_DevMode_IsEnabled(void)
+{
+ return atomic_load(&sg_devModeEnabled);
+}
+
+bool
+Razorback_DevMode_RegisterContext(struct RazorbackContext *context)
+{
+ struct RazorbackDevContextCapture *capture;
+
+ if (context == NULL)
+ return false;
+
+ if (sg_devCaptures == NULL || sg_devCaptureLock == NULL)
+ return false;
+
+ Mutex_Lock(sg_devCaptureLock);
+ if (DevMode_GetCaptureLocked(context) != NULL) {
+ Mutex_Unlock(sg_devCaptureLock);
+ return true;
+ }
+
+ capture = calloc(1, sizeof(*capture));
+ if (capture == NULL)
+ goto error;
+
+ capture->context = context;
+ capture->judgments = List_Create(LIST_MODE_GENERIC,
+ NULL,
+ NULL,
+ (void (*)(void *))Judgment_Destroy,
+ NULL,
+ NULL,
+ NULL);
+ capture->submissions = List_Create(LIST_MODE_GENERIC,
+ NULL,
+ NULL,
+ DevCapture_DestroySubmission,
+ NULL,
+ NULL,
+ NULL);
+ if (capture->judgments == NULL || capture->submissions == NULL)
+ goto error;
+
+ if (!List_Push(sg_devCaptures, capture))
+ goto error;
+
+ Mutex_Unlock(sg_devCaptureLock);
+ return true;
+
+error:
+ DevCapture_Destroy(capture);
+ Mutex_Unlock(sg_devCaptureLock);
+ return false;
+}
+
+void
+Razorback_DevMode_UnregisterContext(struct RazorbackContext *context)
+{
+ if (context == NULL || sg_devCaptures == NULL || sg_devCaptureLock == NULL)
+ return;
+
+ Mutex_Lock(sg_devCaptureLock);
+ (void)List_Remove(sg_devCaptures, context);
+ Mutex_Unlock(sg_devCaptureLock);
+}
+
+bool
+Razorback_DevMode_CaptureVerdict(struct RazorbackContext *context,
+ const struct Judgment *judgment)
+{
+ struct RazorbackDevContextCapture *capture;
+ struct Judgment *copy;
+ bool pushed;
+
+ if (context == NULL || judgment == NULL)
+ return false;
+
+ copy = DevMode_CloneJudgment(judgment);
+ if (copy == NULL)
+ return false;
+
+ Mutex_Lock(sg_devCaptureLock);
+ capture = DevMode_GetCaptureLocked(context);
+ pushed = (capture != NULL) && List_Push(capture->judgments, copy);
+ Mutex_Unlock(sg_devCaptureLock);
+
+ if (!pushed)
+ Judgment_Destroy(copy);
+
+ return pushed;
+}
+
+bool
+Razorback_DevMode_CaptureSubmission(struct RazorbackContext *context,
+ struct BlockPoolItem *item)
+{
+ struct RazorbackDevContextCapture *capture;
+ bool pushed;
+
+ if (context == NULL || item == NULL)
+ return false;
+
+ Mutex_Lock(sg_devCaptureLock);
+ capture = DevMode_GetCaptureLocked(context);
+ pushed = (capture != NULL) && List_Push(capture->submissions, item);
+ Mutex_Unlock(sg_devCaptureLock);
+ return pushed;
+}
+
+List_t *
+Razorback_DevMode_GetJudgments(struct RazorbackContext *context)
+{
+ struct RazorbackDevContextCapture *capture;
+
+ if (context == NULL || sg_devCaptureLock == NULL)
+ return NULL;
+
+ Mutex_Lock(sg_devCaptureLock);
+ capture = DevMode_GetCaptureLocked(context);
+ Mutex_Unlock(sg_devCaptureLock);
+ return (capture != NULL) ? capture->judgments : NULL;
+}
+
+List_t *
+Razorback_DevMode_GetSubmissions(struct RazorbackContext *context)
+{
+ struct RazorbackDevContextCapture *capture;
+
+ if (context == NULL || sg_devCaptureLock == NULL)
+ return NULL;
+
+ Mutex_Lock(sg_devCaptureLock);
+ capture = DevMode_GetCaptureLocked(context);
+ Mutex_Unlock(sg_devCaptureLock);
+ return (capture != NULL) ? capture->submissions : NULL;
+}
diff --git a/src/dev_mode.h b/src/dev_mode.h
new file mode 100644
index 0000000..1afd3eb
--- /dev/null
+++ b/src/dev_mode.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#ifndef RAZORBACK_DEV_MODE_H
+#define RAZORBACK_DEV_MODE_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+bool Razorback_DevMode_Initialize(void);
+void Razorback_DevMode_SetEnabled(bool enabled);
+bool Razorback_DevMode_IsEnabled(void);
+
+bool Razorback_DevMode_RegisterContext(struct RazorbackContext *context);
+void Razorback_DevMode_UnregisterContext(struct RazorbackContext *context);
+
+bool Razorback_DevMode_CaptureVerdict(struct RazorbackContext *context,
+ const struct Judgment *judgment);
+bool Razorback_DevMode_CaptureSubmission(struct RazorbackContext *context,
+ struct BlockPoolItem *item);
+
+List_t *Razorback_DevMode_GetJudgments(struct RazorbackContext *context);
+List_t *Razorback_DevMode_GetSubmissions(struct RazorbackContext *context);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/fileserver.c b/src/fileserver.c
new file mode 100644
index 0000000..f035b47
--- /dev/null
+++ b/src/fileserver.c
@@ -0,0 +1,894 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#ifndef _POSIX_C_SOURCE
+#define _POSIX_C_SOURCE 200809L
+#endif
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifndef P_tmpdir
+#define P_tmpdir "/tmp"
+#endif
+
+#define DEFAULT_FILESERVER_URL "http://file-server:8080"
+#define DEFAULT_FILESERVER_TIMEOUT 30U
+#define MAX_FILESERVER_ATTEMPTS 10
+
+struct RzbNextFileserverClient
+{
+ char *baseUrl;
+ uint64_t fetchTimeoutSeconds;
+ uint64_t uploadTimeoutSeconds;
+};
+
+struct ResponseBuffer
+{
+ char *memory;
+ size_t size;
+};
+
+struct BlockPoolUpload
+{
+ struct BlockPoolItem *item;
+ struct BlockPoolData *dataItem;
+ size_t bytesRead;
+ size_t bytesTransferred;
+};
+
+static const char *Fileserver_DefaultBaseUrl(void);
+static uint64_t Fileserver_DefaultTimeout(const char *primaryName,
+ const char *legacyName);
+static char *Fileserver_CopyBaseUrl(const char *baseUrl);
+static enum RzbNextFileserverStatus Fileserver_BlockFilename(
+ const struct BlockId *blockId,
+ char **filename
+);
+static bool Fileserver_IsSha256Block(const struct BlockId *blockId);
+static bool Fileserver_IsSuccess(long httpCode);
+static bool Fileserver_IsRetryableHttp(long httpCode);
+static enum RzbNextFileserverStatus Fileserver_StatusForHttp(long httpCode,
+ const char *body);
+static size_t Fileserver_WriteMemory(void *contents, size_t size, size_t nmemb,
+ void *userp);
+static size_t Fileserver_WriteFile(void *contents, size_t size, size_t nmemb,
+ void *userp);
+static size_t Fileserver_ReadBlockPool(char *buffer, size_t size, size_t nitems,
+ void *userdata);
+static enum RzbNextFileserverStatus Fileserver_StoreMime(
+ RzbNextFileserverClient_t *client,
+ const struct BlockId *blockId,
+ bool (*configurePart)(curl_mimepart *part, void *userData),
+ void *userData
+);
+static enum RzbNextFileserverStatus Fileserver_PerformWithRetry(
+ enum RzbNextFileserverStatus (*operation)(void *userData),
+ void *userData
+);
+static enum RzbNextFileserverStatus Fileserver_VerifyFetchedFile(
+ const struct BlockId *blockId,
+ FILE *file
+);
+static enum RzbNextFileserverStatus Fileserver_VerifyBytes(
+ const struct BlockId *blockId,
+ const uint8_t *data,
+ size_t length
+);
+static enum RzbNextFileserverStatus Fileserver_VerifyFile(
+ const struct BlockId *blockId,
+ const char *fileName
+);
+static enum RzbNextFileserverStatus Fileserver_VerifyBlockPoolItem(
+ const struct BlockPoolItem *item
+);
+static bool Fileserver_UpdateHashBytes(struct Hash *hash, const uint8_t *data,
+ size_t length);
+static bool Fileserver_ConfigFilePart(curl_mimepart *part, void *userData);
+static bool Fileserver_ConfigBytesPart(curl_mimepart *part, void *userData);
+static bool Fileserver_ConfigBlockPoolPart(curl_mimepart *part, void *userData);
+static void Fileserver_RewindBlockPoolData(struct BlockPoolData *dataItem);
+
+struct BytesUpload
+{
+ const uint8_t *data;
+ size_t length;
+};
+
+struct StoreMimeRequest
+{
+ RzbNextFileserverClient_t *client;
+ const struct BlockId *blockId;
+ bool (*configurePart)(curl_mimepart *part, void *userData);
+ void *userData;
+};
+
+struct FetchRequest
+{
+ RzbNextFileserverClient_t *client;
+ const struct BlockId *blockId;
+ FILE *file;
+ char *fileName;
+ size_t bytesTransferred;
+};
+
+static const char *
+Fileserver_DefaultBaseUrl(void)
+{
+ const char *value;
+
+ value = getenv("RZB_FILESERVER__URL");
+ if (value != NULL && value[0] != '\0')
+ return value;
+ value = getenv("RZB_FILESERVER_URL");
+ if (value != NULL && value[0] != '\0')
+ return value;
+ return DEFAULT_FILESERVER_URL;
+}
+
+static uint64_t
+Fileserver_DefaultTimeout(const char *primaryName, const char *legacyName)
+{
+ const char *value;
+ char *end = NULL;
+ unsigned long long parsed;
+
+ value = getenv(primaryName);
+ if ((value == NULL || value[0] == '\0') && legacyName != NULL)
+ value = getenv(legacyName);
+ if (value == NULL || value[0] == '\0')
+ return DEFAULT_FILESERVER_TIMEOUT;
+ errno = 0;
+ parsed = strtoull(value, &end, 10);
+ if (errno != 0 || end == value || *end != '\0' || parsed == 0)
+ return DEFAULT_FILESERVER_TIMEOUT;
+ return (uint64_t)parsed;
+}
+
+static char *
+Fileserver_CopyBaseUrl(const char *baseUrl)
+{
+ char *copy;
+ size_t length;
+
+ if (baseUrl == NULL || baseUrl[0] == '\0')
+ baseUrl = Fileserver_DefaultBaseUrl();
+ copy = strdup(baseUrl);
+ if (copy == NULL)
+ return NULL;
+ length = strlen(copy);
+ while (length > 0 && copy[length - 1] == '/') {
+ copy[length - 1] = '\0';
+ length--;
+ }
+ return copy;
+}
+
+SO_PUBLIC RzbNextFileserverClient_t *
+RzbNextFileserverClient_Create(const char *baseUrl,
+ uint64_t fetchTimeoutSeconds,
+ uint64_t uploadTimeoutSeconds)
+{
+ RzbNextFileserverClient_t *client;
+
+ client = calloc(1, sizeof(*client));
+ if (client == NULL)
+ return NULL;
+ client->baseUrl = Fileserver_CopyBaseUrl(baseUrl);
+ if (client->baseUrl == NULL) {
+ free(client);
+ return NULL;
+ }
+ client->fetchTimeoutSeconds = fetchTimeoutSeconds == 0 ?
+ Fileserver_DefaultTimeout("RZB_FILESERVER__FETCH_TIMEOUT",
+ "RZB_FILESERVER_FETCH_TIMEOUT") :
+ fetchTimeoutSeconds;
+ client->uploadTimeoutSeconds = uploadTimeoutSeconds == 0 ?
+ Fileserver_DefaultTimeout("RZB_FILESERVER__UPLOAD_TIMEOUT",
+ "RZB_FILESERVER_UPLOAD_TIMEOUT") :
+ uploadTimeoutSeconds;
+ return client;
+}
+
+SO_PUBLIC void
+RzbNextFileserverClient_Destroy(RzbNextFileserverClient_t *client)
+{
+ if (client == NULL)
+ return;
+ free(client->baseUrl);
+ free(client);
+}
+
+static bool
+Fileserver_IsSha256Block(const struct BlockId *blockId)
+{
+ return blockId != NULL &&
+ blockId->pHash != NULL &&
+ blockId->pHash->iType == HASH_TYPE_SHA256 &&
+ blockId->pHash->iSize == 32 &&
+ (blockId->pHash->iFlags & HASH_FLAG_FINAL) != 0 &&
+ blockId->iLength > 0;
+}
+
+static enum RzbNextFileserverStatus
+Fileserver_BlockFilename(const struct BlockId *blockId, char **filename)
+{
+ char *hashText;
+
+ if (filename == NULL || !Fileserver_IsSha256Block(blockId))
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ hashText = Hash_ToText(blockId->pHash);
+ if (hashText == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ if (asprintf(filename, "%s.%ju", hashText, (uintmax_t)blockId->iLength) == -1) {
+ free(hashText);
+ *filename = NULL;
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ }
+ free(hashText);
+ return RZB_NEXT_FILESERVER_OK;
+}
+
+SO_PUBLIC enum RzbNextFileserverStatus
+RzbNextFileserver_BuildUrl(const RzbNextFileserverClient_t *client,
+ const struct BlockId *blockId,
+ char **url)
+{
+ char *filename = NULL;
+ enum RzbNextFileserverStatus status;
+
+ if (client == NULL || client->baseUrl == NULL || url == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ status = Fileserver_BlockFilename(blockId, &filename);
+ if (status != RZB_NEXT_FILESERVER_OK)
+ return status;
+ if (asprintf(url, "%s/%c/%c/%c/%c/%s",
+ client->baseUrl,
+ filename[0],
+ filename[1],
+ filename[2],
+ filename[3],
+ filename) == -1) {
+ free(filename);
+ *url = NULL;
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ }
+ free(filename);
+ return RZB_NEXT_FILESERVER_OK;
+}
+
+static bool
+Fileserver_IsSuccess(long httpCode)
+{
+ return httpCode >= 200 && httpCode < 300;
+}
+
+static bool
+Fileserver_IsRetryableHttp(long httpCode)
+{
+ return httpCode == 408 || httpCode == 429 || httpCode >= 500;
+}
+
+static enum RzbNextFileserverStatus
+Fileserver_StatusForHttp(long httpCode, const char *body)
+{
+ if (Fileserver_IsSuccess(httpCode))
+ return RZB_NEXT_FILESERVER_OK;
+ if (httpCode == 404)
+ return RZB_NEXT_FILESERVER_NOT_FOUND;
+ if (httpCode == 400 && body != NULL && strstr(body, "already exists") != NULL)
+ return RZB_NEXT_FILESERVER_OK;
+ if (Fileserver_IsRetryableHttp(httpCode))
+ return RZB_NEXT_FILESERVER_RETRYABLE;
+ return RZB_NEXT_FILESERVER_HTTP_ERROR;
+}
+
+static size_t
+Fileserver_WriteMemory(void *contents, size_t size, size_t nmemb, void *userp)
+{
+ size_t realSize = size * nmemb;
+ struct ResponseBuffer *buffer = userp;
+ char *next;
+
+ next = realloc(buffer->memory, buffer->size + realSize + 1);
+ if (next == NULL)
+ return 0;
+ buffer->memory = next;
+ memcpy(&(buffer->memory[buffer->size]), contents, realSize);
+ buffer->size += realSize;
+ buffer->memory[buffer->size] = '\0';
+ return realSize;
+}
+
+static size_t
+Fileserver_WriteFile(void *contents, size_t size, size_t nmemb, void *userp)
+{
+ struct FetchRequest *request = userp;
+ size_t written;
+
+ written = fwrite(contents, size, nmemb, request->file);
+ request->bytesTransferred += size * written;
+ return written;
+}
+
+static size_t
+Fileserver_ReadBlockPool(char *buffer, size_t size, size_t nitems, void *userdata)
+{
+ struct BlockPoolUpload *upload = userdata;
+ size_t want = size * nitems;
+ size_t read = 0;
+
+ while (read < want && upload->dataItem != NULL) {
+ size_t available;
+ size_t remaining;
+ size_t toRead;
+
+ if (upload->dataItem->iFlags == BLOCK_POOL_DATA_FLAG_FILE) {
+ available = upload->dataItem->iLength - upload->bytesRead;
+ remaining = want - read;
+ toRead = available < remaining ? available : remaining;
+ if (toRead == 0) {
+ upload->dataItem = upload->dataItem->pNext;
+ upload->bytesRead = 0;
+ continue;
+ }
+ toRead = fread(buffer + read, 1, toRead, upload->dataItem->data.file);
+ } else {
+ available = upload->dataItem->iLength - upload->bytesRead;
+ remaining = want - read;
+ toRead = available < remaining ? available : remaining;
+ if (toRead > 0) {
+ memcpy(buffer + read,
+ upload->dataItem->data.pointer + upload->bytesRead,
+ toRead);
+ }
+ }
+ read += toRead;
+ upload->bytesRead += toRead;
+ upload->bytesTransferred += toRead;
+ if (upload->bytesRead >= upload->dataItem->iLength) {
+ upload->dataItem = upload->dataItem->pNext;
+ upload->bytesRead = 0;
+ }
+ if (toRead == 0)
+ break;
+ }
+ return read;
+}
+
+static enum RzbNextFileserverStatus
+Fileserver_PerformWithRetry(enum RzbNextFileserverStatus (*operation)(void *userData),
+ void *userData)
+{
+ enum RzbNextFileserverStatus status = RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ unsigned int delay = 1;
+ int attempt;
+
+ for (attempt = 0; attempt < MAX_FILESERVER_ATTEMPTS; attempt++) {
+ status = operation(userData);
+ if (status != RZB_NEXT_FILESERVER_RETRYABLE)
+ return status;
+ if (attempt + 1 < MAX_FILESERVER_ATTEMPTS) {
+ sleep(delay);
+ if (delay < 8U)
+ delay *= 2U;
+ }
+ }
+ return status;
+}
+
+static enum RzbNextFileserverStatus
+Fileserver_StoreMimeAttempt(void *userData)
+{
+ struct StoreMimeRequest *request = userData;
+ struct ResponseBuffer response = { NULL, 0 };
+ CURL *curl = NULL;
+ curl_mime *mime = NULL;
+ curl_mimepart *part = NULL;
+ char *url = NULL;
+ CURLcode code;
+ long httpCode = 0;
+ enum RzbNextFileserverStatus status = RZB_NEXT_FILESERVER_LOCAL_ERROR;
+
+ response.memory = calloc(1, sizeof(char));
+ if (response.memory == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ status = RzbNextFileserver_BuildUrl(request->client, request->blockId, &url);
+ if (status != RZB_NEXT_FILESERVER_OK)
+ goto cleanup;
+ curl = curl_easy_init();
+ if (curl == NULL)
+ goto cleanup;
+ mime = curl_mime_init(curl);
+ if (mime == NULL)
+ goto cleanup;
+ part = curl_mime_addpart(mime);
+ if (part == NULL)
+ goto cleanup;
+ if (!request->configurePart(part, request->userData))
+ goto cleanup;
+ if (curl_easy_setopt(curl, CURLOPT_URL, url) != CURLE_OK ||
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT,
+ (long)request->client->uploadTimeoutSeconds) != CURLE_OK ||
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Fileserver_WriteMemory) != CURLE_OK ||
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response) != CURLE_OK ||
+ curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime) != CURLE_OK) {
+ goto cleanup;
+ }
+ code = curl_easy_perform(curl);
+ if (code != CURLE_OK) {
+ status = RZB_NEXT_FILESERVER_RETRYABLE;
+ goto cleanup;
+ }
+ if (curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode) != CURLE_OK)
+ goto cleanup;
+ status = Fileserver_StatusForHttp(httpCode, response.memory);
+
+cleanup:
+ if (mime != NULL)
+ curl_mime_free(mime);
+ if (curl != NULL)
+ curl_easy_cleanup(curl);
+ free(response.memory);
+ free(url);
+ return status;
+}
+
+static enum RzbNextFileserverStatus
+Fileserver_StoreMime(RzbNextFileserverClient_t *client,
+ const struct BlockId *blockId,
+ bool (*configurePart)(curl_mimepart *part, void *userData),
+ void *userData)
+{
+ struct StoreMimeRequest request = {
+ client,
+ blockId,
+ configurePart,
+ userData
+ };
+
+ if (client == NULL || blockId == NULL || configurePart == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ return Fileserver_PerformWithRetry(Fileserver_StoreMimeAttempt, &request);
+}
+
+static bool
+Fileserver_ConfigFilePart(curl_mimepart *part, void *userData)
+{
+ const char *fileName = userData;
+
+ return curl_mime_name(part, "file") == CURLE_OK &&
+ curl_mime_filename(part, "file") == CURLE_OK &&
+ curl_mime_type(part, "application/octet-stream") == CURLE_OK &&
+ curl_mime_filedata(part, fileName) == CURLE_OK;
+}
+
+static bool
+Fileserver_ConfigBytesPart(curl_mimepart *part, void *userData)
+{
+ const struct BytesUpload *upload = userData;
+
+ return curl_mime_name(part, "file") == CURLE_OK &&
+ curl_mime_filename(part, "file") == CURLE_OK &&
+ curl_mime_type(part, "application/octet-stream") == CURLE_OK &&
+ curl_mime_data(part, (const char *)upload->data,
+ upload->length) == CURLE_OK;
+}
+
+static bool
+Fileserver_ConfigBlockPoolPart(curl_mimepart *part, void *userData)
+{
+ struct BlockPoolUpload *upload = userData;
+ curl_off_t length;
+
+ if (upload == NULL || upload->item == NULL || upload->item->pEvent == NULL ||
+ upload->item->pEvent->pBlock == NULL ||
+ upload->item->pEvent->pBlock->pId == NULL) {
+ return false;
+ }
+ upload->dataItem = upload->item->pDataHead;
+ upload->bytesRead = 0;
+ upload->bytesTransferred = 0;
+ Fileserver_RewindBlockPoolData(upload->dataItem);
+ length = (curl_off_t)upload->item->pEvent->pBlock->pId->iLength;
+ return curl_mime_name(part, "file") == CURLE_OK &&
+ curl_mime_filename(part, "file") == CURLE_OK &&
+ curl_mime_type(part, "application/octet-stream") == CURLE_OK &&
+ curl_mime_data_cb(part, length, Fileserver_ReadBlockPool,
+ NULL, NULL, upload) == CURLE_OK;
+}
+
+static void
+Fileserver_RewindBlockPoolData(struct BlockPoolData *dataItem)
+{
+ while (dataItem != NULL) {
+ if (dataItem->iFlags == BLOCK_POOL_DATA_FLAG_FILE &&
+ dataItem->data.file != NULL) {
+ rewind(dataItem->data.file);
+ }
+ dataItem = dataItem->pNext;
+ }
+}
+
+SO_PUBLIC enum RzbNextFileserverStatus
+RzbNextFileserver_StoreFile(RzbNextFileserverClient_t *client,
+ const struct BlockId *blockId,
+ const char *fileName)
+{
+ enum RzbNextFileserverStatus status;
+
+ if (fileName == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ status = Fileserver_VerifyFile(blockId, fileName);
+ if (status != RZB_NEXT_FILESERVER_OK)
+ return status;
+ return Fileserver_StoreMime(client, blockId, Fileserver_ConfigFilePart,
+ (void *)fileName);
+}
+
+SO_PUBLIC enum RzbNextFileserverStatus
+RzbNextFileserver_StoreBytes(RzbNextFileserverClient_t *client,
+ const struct BlockId *blockId,
+ const uint8_t *data,
+ size_t length)
+{
+ struct BytesUpload upload = { data, length };
+ enum RzbNextFileserverStatus status;
+
+ if (data == NULL || length == 0)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ status = Fileserver_VerifyBytes(blockId, data, length);
+ if (status != RZB_NEXT_FILESERVER_OK)
+ return status;
+ return Fileserver_StoreMime(client, blockId, Fileserver_ConfigBytesPart,
+ &upload);
+}
+
+SO_PUBLIC enum RzbNextFileserverStatus
+RzbNextFileserver_StoreBlockPoolItem(RzbNextFileserverClient_t *client,
+ struct BlockPoolItem *item)
+{
+ struct BlockPoolUpload upload = { item, NULL, 0, 0 };
+ enum RzbNextFileserverStatus status;
+
+ if (item == NULL || item->pEvent == NULL || item->pEvent->pBlock == NULL ||
+ item->pEvent->pBlock->pId == NULL || item->pDataHead == NULL) {
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ }
+ status = Fileserver_VerifyBlockPoolItem(item);
+ if (status != RZB_NEXT_FILESERVER_OK)
+ return status;
+ status = Fileserver_StoreMime(client, item->pEvent->pBlock->pId,
+ Fileserver_ConfigBlockPoolPart, &upload);
+ Fileserver_RewindBlockPoolData(item->pDataHead);
+ return status;
+}
+
+static enum RzbNextFileserverStatus
+Fileserver_FetchAttempt(void *userData)
+{
+ struct FetchRequest *request = userData;
+ CURL *curl = NULL;
+ char *url = NULL;
+ CURLcode code;
+ long httpCode = 0;
+ enum RzbNextFileserverStatus status = RZB_NEXT_FILESERVER_LOCAL_ERROR;
+
+ request->bytesTransferred = 0;
+ rewind(request->file);
+ if (ftruncate(fileno(request->file), 0) != 0)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ status = RzbNextFileserver_BuildUrl(request->client, request->blockId, &url);
+ if (status != RZB_NEXT_FILESERVER_OK)
+ return status;
+ curl = curl_easy_init();
+ if (curl == NULL)
+ goto cleanup;
+ if (curl_easy_setopt(curl, CURLOPT_URL, url) != CURLE_OK ||
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT,
+ (long)request->client->fetchTimeoutSeconds) != CURLE_OK ||
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Fileserver_WriteFile) != CURLE_OK ||
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, request) != CURLE_OK) {
+ goto cleanup;
+ }
+ code = curl_easy_perform(curl);
+ if (code != CURLE_OK) {
+ status = RZB_NEXT_FILESERVER_RETRYABLE;
+ goto cleanup;
+ }
+ if (curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode) != CURLE_OK)
+ goto cleanup;
+ status = Fileserver_StatusForHttp(httpCode, NULL);
+ if (status == RZB_NEXT_FILESERVER_OK)
+ status = Fileserver_VerifyFetchedFile(request->blockId, request->file);
+
+cleanup:
+ if (curl != NULL)
+ curl_easy_cleanup(curl);
+ free(url);
+ return status;
+}
+
+static bool
+Fileserver_UpdateHashBytes(struct Hash *hash, const uint8_t *data,
+ size_t length)
+{
+ size_t offset = 0;
+
+ while (offset < length) {
+ size_t chunk = length - offset;
+ if (chunk > UINT32_MAX)
+ chunk = UINT32_MAX;
+ if (!Hash_Update(hash, (uint8_t *)(data + offset), (uint32_t)chunk))
+ return false;
+ offset += chunk;
+ }
+ return true;
+}
+
+static enum RzbNextFileserverStatus
+Fileserver_VerifyBytes(const struct BlockId *blockId,
+ const uint8_t *data,
+ size_t length)
+{
+ struct Hash *hash;
+ bool success;
+
+ if (!Fileserver_IsSha256Block(blockId) || data == NULL || length == 0)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ if ((uint64_t)length != blockId->iLength)
+ return RZB_NEXT_FILESERVER_PAYLOAD_MISMATCH;
+ hash = Hash_Create_Type(HASH_TYPE_SHA256);
+ if (hash == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ success = Fileserver_UpdateHashBytes(hash, data, length) &&
+ Hash_Finalize(hash) &&
+ Hash_IsEqual(hash, blockId->pHash);
+ Hash_Destroy(hash);
+ return success ? RZB_NEXT_FILESERVER_OK :
+ RZB_NEXT_FILESERVER_PAYLOAD_MISMATCH;
+}
+
+static enum RzbNextFileserverStatus
+Fileserver_VerifyFile(const struct BlockId *blockId,
+ const char *fileName)
+{
+ FILE *file;
+ enum RzbNextFileserverStatus status;
+
+ if (!Fileserver_IsSha256Block(blockId) || fileName == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ file = fopen(fileName, "rb");
+ if (file == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ status = Fileserver_VerifyFetchedFile(blockId, file);
+ fclose(file);
+ return status;
+}
+
+static enum RzbNextFileserverStatus
+Fileserver_VerifyBlockPoolItem(const struct BlockPoolItem *item)
+{
+ const struct BlockId *blockId;
+ const struct BlockPoolData *dataItem;
+ struct Hash *hash;
+ uint64_t total = 0;
+ bool success = true;
+ uint8_t buffer[8192];
+
+ if (item == NULL || item->pEvent == NULL || item->pEvent->pBlock == NULL ||
+ item->pEvent->pBlock->pId == NULL || item->pDataHead == NULL) {
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ }
+ blockId = item->pEvent->pBlock->pId;
+ if (!Fileserver_IsSha256Block(blockId))
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ hash = Hash_Create_Type(HASH_TYPE_SHA256);
+ if (hash == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ for (dataItem = item->pDataHead; dataItem != NULL && success;
+ dataItem = dataItem->pNext) {
+ total += dataItem->iLength;
+ if (dataItem->iFlags == BLOCK_POOL_DATA_FLAG_FILE) {
+ size_t readBytes;
+
+ if (dataItem->data.file == NULL) {
+ success = false;
+ break;
+ }
+ rewind(dataItem->data.file);
+ while ((readBytes = fread(buffer, 1, sizeof(buffer),
+ dataItem->data.file)) > 0) {
+ if (!Fileserver_UpdateHashBytes(hash, buffer, readBytes)) {
+ success = false;
+ break;
+ }
+ }
+ if (ferror(dataItem->data.file))
+ success = false;
+ rewind(dataItem->data.file);
+ } else {
+ if (dataItem->data.pointer == NULL ||
+ !Fileserver_UpdateHashBytes(hash, dataItem->data.pointer,
+ dataItem->iLength)) {
+ success = false;
+ }
+ }
+ }
+ success = success && total == blockId->iLength && Hash_Finalize(hash) &&
+ Hash_IsEqual(hash, blockId->pHash);
+ Hash_Destroy(hash);
+ Fileserver_RewindBlockPoolData(item->pDataHead);
+ return success ? RZB_NEXT_FILESERVER_OK :
+ RZB_NEXT_FILESERVER_PAYLOAD_MISMATCH;
+}
+
+SO_PUBLIC enum RzbNextFileserverStatus
+RzbNextFileserver_FetchToFile(RzbNextFileserverClient_t *client,
+ const struct BlockId *blockId,
+ char **fileName)
+{
+ struct FetchRequest request = { client, blockId, NULL, NULL, 0 };
+ const char *tempDir;
+ int fd;
+ enum RzbNextFileserverStatus status;
+
+ if (client == NULL || blockId == NULL || fileName == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ tempDir = getenv("TMPDIR");
+ if (tempDir == NULL || tempDir[0] == '\0')
+ tempDir = P_tmpdir;
+ if (asprintf(&request.fileName, "%s/rzb-XXXXXX", tempDir) == -1)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ fd = mkstemp(request.fileName);
+ if (fd == -1) {
+ free(request.fileName);
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ }
+ request.file = fdopen(fd, "w+b");
+ if (request.file == NULL) {
+ close(fd);
+ unlink(request.fileName);
+ free(request.fileName);
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ }
+ status = Fileserver_PerformWithRetry(Fileserver_FetchAttempt, &request);
+ fclose(request.file);
+ if (status != RZB_NEXT_FILESERVER_OK) {
+ unlink(request.fileName);
+ free(request.fileName);
+ return status;
+ }
+ *fileName = request.fileName;
+ return RZB_NEXT_FILESERVER_OK;
+}
+
+static enum RzbNextFileserverStatus
+Fileserver_VerifyFetchedFile(const struct BlockId *blockId, FILE *file)
+{
+ struct Hash *hash = NULL;
+ bool success;
+ off_t length;
+
+ if (blockId == NULL || file == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ if (fflush(file) != 0)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ if (fseeko(file, 0, SEEK_END) != 0)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ length = ftello(file);
+ if (length < 0)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ if ((uint64_t)length != blockId->iLength)
+ return RZB_NEXT_FILESERVER_PAYLOAD_MISMATCH;
+ if (fseeko(file, 0, SEEK_SET) != 0)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ hash = Hash_Create_Type(HASH_TYPE_SHA256);
+ if (hash == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ success = Hash_Update_File(hash, file) && Hash_Finalize(hash) &&
+ Hash_IsEqual(hash, blockId->pHash);
+ Hash_Destroy(hash);
+ if (fseeko(file, 0, SEEK_SET) != 0)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ return success ? RZB_NEXT_FILESERVER_OK :
+ RZB_NEXT_FILESERVER_PAYLOAD_MISMATCH;
+}
+
+SO_PUBLIC bool
+RzbNextFileserver_AttachFileToBlock(struct Block *block, char *fileName,
+ bool tempFile)
+{
+ ASSERT(block != NULL);
+ ASSERT(fileName != NULL);
+ if (block == NULL || fileName == NULL)
+ return false;
+ block->data.file = fopen(fileName, "rb");
+ if (block->data.file == NULL) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: failed to open block file %s: %s",
+ __func__, fileName, strerror(errno));
+ return false;
+ }
+ block->data.fileName = fileName;
+ block->data.tempFile = tempFile;
+ block->data.pointer = mmap(NULL, block->pId->iLength, PROT_READ, MAP_PRIVATE,
+ fileno(block->data.file), 0);
+ if (block->data.pointer == MAP_FAILED) {
+ block->data.pointer = NULL;
+ fclose(block->data.file);
+ block->data.file = NULL;
+ return false;
+ }
+ return true;
+}
+
+SO_PUBLIC void
+RzbNextFileserver_FreeBlockData(struct Block *block)
+{
+ if (block == NULL)
+ return;
+ if (block->data.pointer != NULL) {
+ munmap(block->data.pointer, block->pId->iLength);
+ block->data.pointer = NULL;
+ }
+ if (block->data.file != NULL) {
+ fclose(block->data.file);
+ block->data.file = NULL;
+ }
+ if (block->data.tempFile && block->data.fileName != NULL)
+ unlink(block->data.fileName);
+ free(block->data.fileName);
+ block->data.fileName = NULL;
+ block->data.tempFile = false;
+}
+
+SO_PUBLIC enum RzbNextFileserverStatus
+RzbNextFileserver_FetchBlock(RzbNextFileserverClient_t *client,
+ struct Block *block)
+{
+ char *fileName = NULL;
+ enum RzbNextFileserverStatus status;
+
+ if (block == NULL || block->pId == NULL)
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ status = RzbNextFileserver_FetchToFile(client, block->pId, &fileName);
+ if (status != RZB_NEXT_FILESERVER_OK)
+ return status;
+ if (!RzbNextFileserver_AttachFileToBlock(block, fileName, true)) {
+ unlink(fileName);
+ free(fileName);
+ return RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ }
+ return RZB_NEXT_FILESERVER_OK;
+}
diff --git a/src/health.c b/src/health.c
index 4f90f79..cf458f7 100644
--- a/src/health.c
+++ b/src/health.c
@@ -35,16 +35,23 @@
#include
#include
#include
+#include
+#include
#define HEALTH_DEFAULT_BIND_ADDRESS "127.0.0.1"
#define HEALTH_CHECK_RELEASE_WAIT_MS 1U
#define HEALTH_REQUEST_LINE_TIMEOUT_MS 250U
+#define HEALTH_ENVELOPE_BODY_SIZE 2048U
#define HEALTH_HTTP_OK "HTTP/1.1 200 OK"
#define HEALTH_HTTP_BAD_REQUEST "HTTP/1.1 400 Bad Request"
#define HEALTH_HTTP_METHOD_NOT_ALLOWED "HTTP/1.1 405 Method Not Allowed"
#define HEALTH_HTTP_NOT_FOUND "HTTP/1.1 404 Not Found"
#define HEALTH_HTTP_UNHEALTHY "HTTP/1.1 503 Service Unavailable"
+#ifndef PACKAGE_VERSION
+#define PACKAGE_VERSION "0.0.0"
+#endif
+
struct RazorbackHealthCheck
{
RazorbackHealthCheckId_t id;
@@ -141,6 +148,13 @@ static bool Health_SendProbeResponse(const struct Socket *socket,
RazorbackHealthCheckKind_t kind);
static bool Health_SendAggregateResponse(const struct Socket *socket,
const struct RazorbackHealthHttpRequest *request);
+static bool Health_RenderEnvelope(char *body,
+ size_t bodySize,
+ bool live,
+ bool ready,
+ bool startup);
+static void Health_ObservedAt(char *buffer, size_t bufferSize);
+static void Health_InstanceId(char *buffer, size_t bufferSize);
static int
Health_Check_Cmp(void *a, void *b)
@@ -708,11 +722,18 @@ Health_SendProbeResponse(const struct Socket *socket,
RazorbackHealthCheckKind_t kind)
{
bool healthy = Health_EvaluateInternal(kind);
+ bool live = Health_EvaluateInternal(RAZORBACK_HEALTH_LIVE);
+ bool ready = Health_EvaluateInternal(RAZORBACK_HEALTH_READY);
+ bool startup = Health_EvaluateInternal(RAZORBACK_HEALTH_STARTUP);
+ char body[HEALTH_ENVELOPE_BODY_SIZE];
+
+ if (!Health_RenderEnvelope(body, sizeof(body), live, ready, startup))
+ return false;
return Health_SendResponse(socket,
healthy ? HEALTH_HTTP_OK : HEALTH_HTTP_UNHEALTHY,
- "text/plain; charset=utf-8",
- healthy ? "ok\n" : "unhealthy\n",
+ "application/json",
+ body,
!request->headOnly);
}
@@ -720,19 +741,13 @@ static bool
Health_SendAggregateResponse(const struct Socket *socket,
const struct RazorbackHealthHttpRequest *request)
{
- char body[128];
+ char body[HEALTH_ENVELOPE_BODY_SIZE];
bool live = Health_EvaluateInternal(RAZORBACK_HEALTH_LIVE);
bool ready = Health_EvaluateInternal(RAZORBACK_HEALTH_READY);
bool startup = Health_EvaluateInternal(RAZORBACK_HEALTH_STARTUP);
bool healthy = live && ready && startup;
- int length;
- length = snprintf(body, sizeof(body),
- "{\"live\":%s,\"ready\":%s,\"startup\":%s}\n",
- live ? "true" : "false",
- ready ? "true" : "false",
- startup ? "true" : "false");
- if (length < 0 || (size_t)length >= sizeof(body))
+ if (!Health_RenderEnvelope(body, sizeof(body), live, ready, startup))
return false;
return Health_SendResponse(socket,
@@ -742,6 +757,125 @@ Health_SendAggregateResponse(const struct Socket *socket,
!request->headOnly);
}
+static bool
+Health_RenderEnvelope(char *body,
+ size_t bodySize,
+ bool live,
+ bool ready,
+ bool startup)
+{
+ char observedAt[128];
+ char instanceId[128];
+ const bool aggregateHealthy = live && ready && startup;
+ const char *workflowState;
+ char reasonCodes[96];
+ char workflowReason[128];
+ int length;
+
+ ASSERT(body != NULL);
+ if (body == NULL || bodySize == 0U)
+ return false;
+
+ if (!live)
+ workflowState = "failed";
+ else if (!startup)
+ workflowState = "starting";
+ else if (!ready)
+ workflowState = "paused_registration";
+ else
+ workflowState = "running";
+
+ if (strcmp(workflowState, "running") == 0) {
+ snprintf(reasonCodes, sizeof(reasonCodes), "[]");
+ workflowReason[0] = '\0';
+ } else {
+ static const char *prefix = "c_sdk_process_";
+ char reason[64];
+
+ snprintf(reason, sizeof(reason), "%s%s", prefix, workflowState);
+ snprintf(reasonCodes, sizeof(reasonCodes), "[\"%s\"]", reason);
+ snprintf(workflowReason, sizeof(workflowReason),
+ ",\"reason_code\":\"%s\"", reason);
+ }
+
+ Health_ObservedAt(observedAt, sizeof(observedAt));
+ Health_InstanceId(instanceId, sizeof(instanceId));
+
+ length = snprintf(
+ body,
+ bodySize,
+ "{\"service\":{\"name\":\"razorback-c-sdk-component\","
+ "\"version\":\"%s\",\"instance_id\":\"%s\"},"
+ "\"observed_at\":\"%s\","
+ "\"status\":\"%s\","
+ "\"reason_codes\":%s,"
+ "\"startup_complete\":%s,"
+ "\"startup_ready\":%s,"
+ "\"ready\":%s,"
+ "\"dependencies\":{},"
+ "\"workflows\":{\"c_sdk_process\":{\"state\":\"%s\","
+ "\"enabled\":true,\"dependencies\":[],\"observed_at\":\"%s\"%s}},"
+ "\"schema_functional_level\":{\"status\":\"unknown\"},"
+ "\"diagnostics\":{},"
+ "\"live\":%s,\"startup\":%s}\n",
+ PACKAGE_VERSION,
+ instanceId,
+ observedAt,
+ aggregateHealthy ? "ok" : "not_ready",
+ reasonCodes,
+ startup ? "true" : "false",
+ startup ? "true" : "false",
+ ready ? "true" : "false",
+ workflowState,
+ observedAt,
+ workflowReason,
+ live ? "true" : "false",
+ startup ? "true" : "false");
+
+ return length >= 0 && (size_t)length < bodySize;
+}
+
+static void
+Health_ObservedAt(char *buffer, size_t bufferSize)
+{
+ struct timespec now;
+ struct tm utc;
+
+ ASSERT(buffer != NULL);
+ if (buffer == NULL || bufferSize == 0U)
+ return;
+
+ if (clock_gettime(CLOCK_REALTIME, &now) != 0 ||
+ gmtime_r(&now.tv_sec, &utc) == NULL) {
+ snprintf(buffer, bufferSize, "1970-01-01T00:00:00.000Z");
+ return;
+ }
+
+ snprintf(buffer, bufferSize,
+ "%04d-%02d-%02dT%02d:%02d:%02d.%03ldZ",
+ utc.tm_year + 1900,
+ utc.tm_mon + 1,
+ utc.tm_mday,
+ utc.tm_hour,
+ utc.tm_min,
+ utc.tm_sec,
+ now.tv_nsec / 1000000L);
+}
+
+static void
+Health_InstanceId(char *buffer, size_t bufferSize)
+{
+ ASSERT(buffer != NULL);
+ if (buffer == NULL || bufferSize == 0U)
+ return;
+
+ if (gethostname(buffer, bufferSize) != 0) {
+ snprintf(buffer, bufferSize, "unknown");
+ return;
+ }
+ buffer[bufferSize - 1U] = '\0';
+}
+
static bool
Health_HandleClient(const struct Socket *socket)
{
diff --git a/src/init.c b/src/init.c
index 568efa7..f2912e9 100644
--- a/src/init.c
+++ b/src/init.c
@@ -20,6 +20,7 @@
#include "init.h"
#include "block_pool_private.h"
+#include "dev_mode.h"
#include "health_internal.h"
#include "submission_private.h"
#include "telemetry.h"
@@ -76,7 +77,15 @@ RZB_Init_API(void)
Magic_Init();
initcache();
initUuids();
+ if (!Thread_Initialize()) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to initialize thread subsystem", __func__);
+ exit(1);
+ }
initApi();
+ if (!Razorback_DevMode_Initialize()) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to initialize local dev-mode support",
+ __func__);
+ }
if (!Health_Initialize()) {
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to initialize health subsystem, continuing without health",
__func__);
@@ -93,7 +102,4 @@ RZB_Init_API(void)
}
atexit(Submission_Shutdown_Global);
Message_Init();
- if (!Transfer_Init()) {
- exit(1);
- }
}
diff --git a/src/init.h b/src/init.h
index 3e453bd..6be2a15 100644
--- a/src/init.h
+++ b/src/init.h
@@ -37,6 +37,9 @@ bool readApiConfig (void);
// api.c
void initApi (void);
+// thread.c
+bool Thread_Initialize(void);
+
bool Crypto_Initialize(void);
bool Socket_TLS_InitializeSharedState(void);
@@ -46,9 +49,6 @@ bool Magic_Init(void);
//messages/core.c
bool Message_Init(void);
-//transfer/core.c
-bool Transfer_Init(void);
-
#ifdef __cplusplus
}
#endif
diff --git a/src/inspection.c b/src/inspection.c
index 9fac3cc..060fb5c 100644
--- a/src/inspection.c
+++ b/src/inspection.c
@@ -30,13 +30,12 @@
#include
#include
#include
+#include
#include
#include
#include
#include "judgment_private.h"
#include "command_and_control.h"
-#include "transfer/core.h"
-#include "connected_entity_private.h"
#include "runtime_config.h"
#include "telemetry.h"
#include
@@ -515,8 +514,8 @@ Inspection_Process_Message(Thread_t *p_pThread,
struct EventId *l_pEventId = NULL;
uint8_t l_iResult = JUDGMENT_REASON_ERROR;
struct Judgment *judgment = NULL;
- enum TransferStatus transfered = TRANSFER_FAIL_LOCAL;
- int transferTries = 0;
+ RzbNextFileserverClient_t *fileserver = NULL;
+ enum RzbNextFileserverStatus fetchStatus = RZB_NEXT_FILESERVER_LOCAL_ERROR;
struct TelemetrySpan *inspectSpan = NULL;
struct TelemetrySpan *runSpan = NULL;
bool processSuccess = false;
@@ -525,7 +524,7 @@ Inspection_Process_Message(Thread_t *p_pThread,
const char *processError = NULL;
const char *runError = NULL;
const char *resultReason = "error";
- const char *errorPhase = NULL;
+ const char *errorStage = NULL;
const char *errorClass = NULL;
bool hasAlerts = false;
double inspectionStartedAt = Telemetry_GetMonotonicTimeSeconds();
@@ -537,7 +536,7 @@ Inspection_Process_Message(Thread_t *p_pThread,
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed dispatch message due to wrong type %u",
__func__, message->type);
processError = "unexpected inspection message type";
- errorPhase = "validation";
+ errorStage = "validation";
errorClass = "invalid_message";
goto cleanup;
}
@@ -547,7 +546,7 @@ Inspection_Process_Message(Thread_t *p_pThread,
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed dispatch message due to NULL payload",
__func__);
processError = "inspection message missing payload";
- errorPhase = "validation";
+ errorStage = "validation";
errorClass = "invalid_message";
goto cleanup;
}
@@ -555,7 +554,7 @@ Inspection_Process_Message(Thread_t *p_pThread,
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed dispatch message due to NULL block",
__func__);
processError = "inspection message missing block";
- errorPhase = "validation";
+ errorStage = "validation";
errorClass = "invalid_message";
goto cleanup;
}
@@ -563,7 +562,7 @@ Inspection_Process_Message(Thread_t *p_pThread,
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed dispatch message due to NULL Hash",
__func__);
processError = "inspection message missing block hash";
- errorPhase = "validation";
+ errorStage = "validation";
errorClass = "invalid_message";
goto cleanup;
}
@@ -573,31 +572,17 @@ Inspection_Process_Message(Thread_t *p_pThread,
TELEMETRY_SPAN_KIND_INTERNAL);
Telemetry_AddBlockAttributes(inspectSpan, l_pBlock);
- while (transferTries < 20) {
- struct ConnectedEntity *dispatcher = ConnectedEntityList_GetDispatcher();
-
- if (dispatcher == NULL) {
- rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to find usable dispatcher", __func__);
- transferTries++;
- break;
- }
-
- transfered = Transfer_Fetch(l_pBlock, dispatcher);
- if (transfered == TRANSFER_FAIL_DISPATCHER) {
- rzb_log(LOG_ERR, LOG_C_CORE, "%s: Marking dispatcher unusable", __func__);
- ConnectedEntityList_MarkDispatcherUnusable(dispatcher->uuidNuggetId);
- }
- ConnectedEntity_Destroy(dispatcher);
- if (transfered == TRANSFER_OK)
- break;
-
- transferTries++;
- }
-
- if (transfered != TRANSFER_OK) {
- rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to transfer block giving up", __func__);
- processError = "failed to fetch block from dispatcher";
- errorPhase = "transfer";
+ fileserver = RzbNextFileserverClient_Create(NULL, 0, 0);
+ if (fileserver != NULL)
+ fetchStatus = RzbNextFileserver_FetchBlock(fileserver, l_pBlock);
+ RzbNextFileserverClient_Destroy(fileserver);
+ fileserver = NULL;
+ if (fetchStatus != RZB_NEXT_FILESERVER_OK) {
+ rzb_log(LOG_ERR, LOG_C_CORE,
+ "%s: Failed to fetch block from fileserver, status=%d",
+ __func__, fetchStatus);
+ processError = "failed to fetch block from fileserver";
+ errorStage = "transfer";
errorClass = "transfer_failed";
goto cleanup;
}
@@ -606,21 +591,21 @@ Inspection_Process_Message(Thread_t *p_pThread,
if (l_pBlock->data.pointer == NULL || l_pBlock->data.fileName == NULL) {
rzb_log(LOG_ERR, LOG_C_CORE, "%s: No data block", __func__);
processError = "inspection block has no local data";
- errorPhase = "transfer";
+ errorStage = "transfer";
errorClass = "missing_block_data";
goto cleanup;
}
if ((l_pEventId = EventId_Clone(l_misMessage->eventId)) == NULL) {
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed create new event id", __func__);
processError = "failed to clone event id";
- errorPhase = "inspection";
+ errorStage = "inspection";
errorClass = "clone_failed";
goto cleanup;
}
if ((l_pClonedBlock = Block_Clone(l_pBlock)) == NULL) {
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed create new block", __func__);
processError = "failed to clone inspection block";
- errorPhase = "inspection";
+ errorStage = "inspection";
errorClass = "clone_failed";
goto cleanup;
}
@@ -660,7 +645,7 @@ Inspection_Process_Message(Thread_t *p_pThread,
hasAlerts = (l_iResult == JUDGMENT_REASON_ALERT);
if (l_iResult == JUDGMENT_REASON_ERROR || !runSuccess)
{
- errorPhase = "inspection";
+ errorStage = "inspection";
errorClass = (l_iResult == JUDGMENT_REASON_ERROR) ? "judgment_error"
: "invalid_judgment";
}
@@ -670,7 +655,7 @@ Inspection_Process_Message(Thread_t *p_pThread,
(l_iResult != JUDGMENT_REASON_DEFERRED)) {
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Bad return from inspection", __func__);
processError = "inspector returned invalid judgment";
- errorPhase = "inspection";
+ errorStage = "inspection";
errorClass = "invalid_judgment";
goto cleanup;
}
@@ -678,12 +663,12 @@ Inspection_Process_Message(Thread_t *p_pThread,
judgment = Judgment_Create(l_pEventId, l_pClonedBlock->pId);
if (judgment == NULL) {
processError = "failed to create judgment";
- errorPhase = "submission";
+ errorStage = "submission";
errorClass = "judgment_create_failed";
goto cleanup;
}
- Transfer_Free(l_pClonedBlock, NULL);
+ RzbNextFileserver_FreeBlockData(l_pClonedBlock);
l_pClonedBlock->data.pointer = NULL;
l_pClonedBlock->data.file = NULL;
l_pClonedBlock->data.fileName = NULL;
@@ -693,7 +678,7 @@ Inspection_Process_Message(Thread_t *p_pThread,
if ((l_mjsMessage = MessageJudgmentSubmission_Initialize(l_iResult, judgment)) == NULL) {
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to create message", __func__);
processError = "failed to create judgment submission";
- errorPhase = "submission";
+ errorStage = "submission";
errorClass = "message_create_failed";
judgment = NULL;
goto cleanup;
@@ -703,7 +688,7 @@ Inspection_Process_Message(Thread_t *p_pThread,
if (!Queue_Put(p_pContext->inspector.judgmentQueue, l_mjsMessage)) {
rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to send judgment submission", __func__);
processError = "failed to send judgment submission";
- errorPhase = "submission";
+ errorStage = "submission";
errorClass = "send_failed";
goto cleanup;
}
@@ -717,14 +702,14 @@ Inspection_Process_Message(Thread_t *p_pThread,
if (judgment != NULL)
Judgment_Destroy(judgment);
if (l_pClonedBlock != NULL) {
- Transfer_Free(l_pClonedBlock, NULL);
+ RzbNextFileserver_FreeBlockData(l_pClonedBlock);
l_pClonedBlock->data.pointer = NULL;
l_pClonedBlock->data.file = NULL;
l_pClonedBlock->data.fileName = NULL;
Block_Destroy(l_pClonedBlock);
}
if (destroyOriginalBlock && l_pBlock != NULL) {
- Transfer_Free(l_pBlock, NULL);
+ RzbNextFileserver_FreeBlockData(l_pBlock);
l_misMessage->pBlock = NULL;
Block_Destroy(l_pBlock);
}
@@ -732,8 +717,8 @@ Inspection_Process_Message(Thread_t *p_pThread,
EventId_Destroy(l_pEventId);
if (runSpan != NULL)
Telemetry_EndSpan(runSpan, false, (runError != NULL) ? runError : processError);
- if (errorPhase != NULL)
- Telemetry_RecordInspectionError(errorPhase, errorClass, p_pContext);
+ if (errorStage != NULL)
+ Telemetry_RecordInspectionError(errorStage, errorClass, p_pContext);
Telemetry_RecordInspectionResult(resultReason, hasAlerts, p_pContext);
Telemetry_RecordInspectionDuration(Telemetry_GetMonotonicTimeSeconds() - inspectionStartedAt,
resultReason,
diff --git a/src/message_body.c b/src/message_body.c
new file mode 100644
index 0000000..cd3ab81
--- /dev/null
+++ b/src/message_body.c
@@ -0,0 +1,838 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+static char *
+MessageBody_Strdup(const char *value)
+{
+ char *copy;
+ size_t length;
+
+ if (value == NULL)
+ return NULL;
+
+ length = strlen(value) + 1;
+ copy = malloc(length);
+ if (copy == NULL)
+ return NULL;
+ memcpy(copy, value, length);
+ return copy;
+}
+
+static bool
+MessageBody_StringIsEmpty(const char *value)
+{
+ return value == NULL || value[0] == '\0';
+}
+
+static struct MessageBodyPolicy
+MessageBody_EffectivePolicy(const struct MessageBodyPolicy *policy)
+{
+ struct MessageBodyPolicy effectivePolicy;
+
+ effectivePolicy.maxInlineBytes = MESSAGE_BODY_DEFAULT_MAX_INLINE_BYTES;
+ effectivePolicy.maxExpandedBytes = MESSAGE_BODY_DEFAULT_MAX_EXPANDED_BYTES;
+ if (policy == NULL)
+ return effectivePolicy;
+ if (policy->maxInlineBytes > 0)
+ effectivePolicy.maxInlineBytes = policy->maxInlineBytes;
+ if (policy->maxExpandedBytes > 0)
+ effectivePolicy.maxExpandedBytes = policy->maxExpandedBytes;
+ return effectivePolicy;
+}
+
+static bool
+MessageBody_IsSha256Hex(const char *value)
+{
+ size_t index;
+
+ if (value == NULL || strlen(value) != 64)
+ return false;
+ for (index = 0; index < 64; index++) {
+ if (!isdigit((unsigned char)value[index]) &&
+ (value[index] < 'a' || value[index] > 'f')) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool
+MessageBody_SchemaNameIsValid(const char *value)
+{
+ const char prefix[] = "razorback.";
+ size_t index;
+
+ if (value == NULL || strncmp(value, prefix, sizeof(prefix) - 1) != 0)
+ return false;
+ for (index = sizeof(prefix) - 1; value[index] != '\0'; index++) {
+ if (!islower((unsigned char)value[index]) &&
+ !isdigit((unsigned char)value[index]) &&
+ value[index] != '_' &&
+ value[index] != '.' &&
+ value[index] != '-') {
+ return false;
+ }
+ }
+ return value[sizeof(prefix) - 1] != '\0';
+}
+
+static char *
+MessageBody_Sha256Hex(const uint8_t *body, size_t bodySize)
+{
+ EVP_MD *md;
+ EVP_MD_CTX *ctx;
+ unsigned char digest[EVP_MAX_MD_SIZE];
+ unsigned int digestLength = 0;
+ char *hex;
+ unsigned int index;
+
+ md = EVP_MD_fetch(NULL, "SHA256", NULL);
+ if (md == NULL)
+ return NULL;
+ ctx = EVP_MD_CTX_new();
+ if (ctx == NULL) {
+ EVP_MD_free(md);
+ return NULL;
+ }
+ if (EVP_DigestInit_ex(ctx, md, NULL) != 1 ||
+ EVP_DigestUpdate(ctx, body, bodySize) != 1 ||
+ EVP_DigestFinal_ex(ctx, digest, &digestLength) != 1) {
+ EVP_MD_CTX_free(ctx);
+ EVP_MD_free(md);
+ return NULL;
+ }
+ EVP_MD_CTX_free(ctx);
+ EVP_MD_free(md);
+
+ if (digestLength != 32)
+ return NULL;
+ hex = calloc(65, sizeof(char));
+ if (hex == NULL)
+ return NULL;
+ for (index = 0; index < digestLength; index++)
+ snprintf(hex + (index * 2), 3, "%02x", digest[index]);
+ return hex;
+}
+
+static bool
+MessageBody_ZlibCompress(const uint8_t *body, size_t bodySize,
+ uint8_t **compressed, size_t *compressedSize)
+{
+ uLongf destinationSize;
+ uint8_t *destination;
+ int result;
+
+ if (bodySize > ULONG_MAX)
+ return false;
+
+ destinationSize = compressBound((uLong)bodySize);
+ destination = malloc(destinationSize == 0 ? 1 : destinationSize);
+ if (destination == NULL)
+ return false;
+
+ result = compress2(destination, &destinationSize, body, (uLong)bodySize,
+ Z_DEFAULT_COMPRESSION);
+ if (result != Z_OK) {
+ free(destination);
+ return false;
+ }
+
+ *compressed = destination;
+ *compressedSize = (size_t)destinationSize;
+ return true;
+}
+
+static bool
+MessageBody_ZlibDecompressUnknown(const uint8_t *body, size_t bodySize,
+ size_t maxExpandedSize,
+ uint8_t **decoded, size_t *decodedSize)
+{
+ z_stream stream;
+ uint8_t *output;
+ size_t outputSize;
+ int result;
+
+ if (bodySize > UINT_MAX)
+ return false;
+
+ memset(&stream, 0, sizeof(stream));
+ if (inflateInit(&stream) != Z_OK)
+ return false;
+
+ if (maxExpandedSize == 0)
+ return false;
+
+ outputSize = (bodySize * 4) + 1024;
+ if (outputSize < 1024)
+ outputSize = 1024;
+ if (outputSize > maxExpandedSize)
+ outputSize = maxExpandedSize;
+ output = malloc(outputSize);
+ if (output == NULL) {
+ inflateEnd(&stream);
+ return false;
+ }
+
+ stream.next_in = (Bytef *)body;
+ stream.avail_in = (uInt)bodySize;
+
+ for (;;) {
+ if (stream.total_out == outputSize) {
+ uint8_t *grown;
+ size_t grownSize = outputSize * 2;
+
+ if (outputSize >= maxExpandedSize || grownSize <= outputSize) {
+ free(output);
+ inflateEnd(&stream);
+ return false;
+ }
+ if (grownSize > maxExpandedSize)
+ grownSize = maxExpandedSize;
+ grown = realloc(output, grownSize);
+ if (grown == NULL) {
+ free(output);
+ inflateEnd(&stream);
+ return false;
+ }
+ output = grown;
+ outputSize = grownSize;
+ }
+
+ stream.next_out = output + stream.total_out;
+ stream.avail_out = (uInt)(outputSize - stream.total_out);
+ result = inflate(&stream, Z_NO_FLUSH);
+ if (stream.total_out > maxExpandedSize) {
+ free(output);
+ inflateEnd(&stream);
+ return false;
+ }
+ if (result == Z_STREAM_END)
+ break;
+ if (result != Z_OK) {
+ free(output);
+ inflateEnd(&stream);
+ return false;
+ }
+ }
+
+ *decodedSize = (size_t)stream.total_out;
+ *decoded = output;
+ inflateEnd(&stream);
+ return true;
+}
+
+static bool
+MessageBody_ZlibDecompressExpected(const uint8_t *body, size_t bodySize,
+ uint64_t expectedSize,
+ size_t maxExpandedSize,
+ uint8_t **decoded,
+ size_t *decodedSize)
+{
+ uint8_t *output;
+ uLongf outputSize;
+ int result;
+
+ if (bodySize > ULONG_MAX || expectedSize > ULONG_MAX ||
+ expectedSize > SIZE_MAX)
+ return false;
+ if (expectedSize > maxExpandedSize)
+ return false;
+
+ outputSize = (uLongf)expectedSize;
+ output = malloc(outputSize == 0 ? 1 : (size_t)outputSize);
+ if (output == NULL)
+ return false;
+
+ result = uncompress(output, &outputSize, body, (uLong)bodySize);
+ if (result != Z_OK || outputSize != expectedSize) {
+ free(output);
+ return false;
+ }
+
+ *decoded = output;
+ *decodedSize = (size_t)outputSize;
+ return true;
+}
+
+static bool
+ClaimCheckReference_AssignString(char **field, const char *value)
+{
+ char *copy;
+
+ copy = MessageBody_Strdup(value);
+ if (copy == NULL)
+ return false;
+ free(*field);
+ *field = copy;
+ return true;
+}
+
+SO_PUBLIC struct MessageBodyPolicy
+MessageBodyPolicy_Default(void)
+{
+ struct MessageBodyPolicy policy;
+
+ policy.maxInlineBytes = MESSAGE_BODY_DEFAULT_MAX_INLINE_BYTES;
+ policy.maxExpandedBytes = MESSAGE_BODY_DEFAULT_MAX_EXPANDED_BYTES;
+ return policy;
+}
+
+SO_PUBLIC const char *
+MessageBodyMode_ToString(enum MessageBodyMode mode)
+{
+ switch (mode) {
+ case MESSAGE_BODY_MODE_INLINE:
+ return "inline";
+ case MESSAGE_BODY_MODE_ZLIB:
+ return "zlib";
+ case MESSAGE_BODY_MODE_CLAIM_CHECK:
+ return "claim_check";
+ default:
+ return NULL;
+ }
+}
+
+SO_PUBLIC struct ClaimCheckReference *
+ClaimCheckReference_Create(const char *signedUrl, const char *expiresAt,
+ const char *bucket, const char *objectKey,
+ const char *contentType, const char *schemaName,
+ uint32_t schemaVersion)
+{
+ struct ClaimCheckReference *reference;
+
+ if (MessageBody_StringIsEmpty(signedUrl) ||
+ MessageBody_StringIsEmpty(expiresAt) ||
+ MessageBody_StringIsEmpty(bucket) ||
+ MessageBody_StringIsEmpty(objectKey) ||
+ MessageBody_StringIsEmpty(contentType) ||
+ MessageBody_StringIsEmpty(schemaName) ||
+ schemaVersion == 0) {
+ return NULL;
+ }
+
+ reference = calloc(1, sizeof(*reference));
+ if (reference == NULL)
+ return NULL;
+
+ reference->referenceSchemaName =
+ MessageBody_Strdup(MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_NAME);
+ reference->referenceSchemaVersion =
+ MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_VERSION;
+ reference->signedUrl = MessageBody_Strdup(signedUrl);
+ reference->expiresAt = MessageBody_Strdup(expiresAt);
+ reference->bucket = MessageBody_Strdup(bucket);
+ reference->objectKey = MessageBody_Strdup(objectKey);
+ reference->sha256 = MessageBody_Strdup("");
+ reference->contentType = MessageBody_Strdup(contentType);
+ reference->schemaName = MessageBody_Strdup(schemaName);
+ reference->schemaVersion = schemaVersion;
+ reference->contentEncoding =
+ MessageBody_Strdup(MESSAGE_BODY_CONTENT_ENCODING_ZLIB);
+
+ if (reference->referenceSchemaName == NULL || reference->signedUrl == NULL ||
+ reference->expiresAt == NULL || reference->bucket == NULL ||
+ reference->objectKey == NULL || reference->sha256 == NULL ||
+ reference->contentType == NULL || reference->schemaName == NULL ||
+ reference->contentEncoding == NULL) {
+ ClaimCheckReference_Destroy(reference);
+ return NULL;
+ }
+ return reference;
+}
+
+SO_PUBLIC struct ClaimCheckReference *
+ClaimCheckReference_Clone(const struct ClaimCheckReference *source)
+{
+ struct ClaimCheckReference *copy;
+
+ if (source == NULL)
+ return NULL;
+
+ copy = ClaimCheckReference_Create(source->signedUrl, source->expiresAt,
+ source->bucket, source->objectKey,
+ source->contentType, source->schemaName,
+ source->schemaVersion);
+ if (copy == NULL)
+ return NULL;
+ copy->referenceSchemaVersion = source->referenceSchemaVersion;
+ copy->uncompressedSize = source->uncompressedSize;
+ copy->storedCompressedSize = source->storedCompressedSize;
+
+ if (!ClaimCheckReference_AssignString(©->referenceSchemaName,
+ source->referenceSchemaName) ||
+ !ClaimCheckReference_AssignString(©->sha256, source->sha256) ||
+ !ClaimCheckReference_AssignString(©->contentEncoding,
+ source->contentEncoding)) {
+ ClaimCheckReference_Destroy(copy);
+ return NULL;
+ }
+ return copy;
+}
+
+SO_PUBLIC void
+ClaimCheckReference_Destroy(struct ClaimCheckReference *reference)
+{
+ if (reference == NULL)
+ return;
+ free(reference->referenceSchemaName);
+ free(reference->signedUrl);
+ free(reference->expiresAt);
+ free(reference->bucket);
+ free(reference->objectKey);
+ free(reference->sha256);
+ free(reference->contentType);
+ free(reference->schemaName);
+ free(reference->contentEncoding);
+ free(reference);
+}
+
+static bool
+ClaimCheckReference_Validate(const struct ClaimCheckReference *reference)
+{
+ if (reference == NULL)
+ return false;
+ if (reference->referenceSchemaName == NULL ||
+ reference->contentEncoding == NULL ||
+ reference->sha256 == NULL ||
+ reference->schemaName == NULL)
+ return false;
+ if (strcmp(reference->referenceSchemaName,
+ MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_NAME) != 0)
+ return false;
+ if (reference->referenceSchemaVersion !=
+ MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_VERSION)
+ return false;
+ if (strcmp(reference->contentEncoding, MESSAGE_BODY_CONTENT_ENCODING_ZLIB) != 0)
+ return false;
+ if (MessageBody_StringIsEmpty(reference->signedUrl) ||
+ MessageBody_StringIsEmpty(reference->expiresAt) ||
+ MessageBody_StringIsEmpty(reference->bucket) ||
+ MessageBody_StringIsEmpty(reference->objectKey) ||
+ MessageBody_StringIsEmpty(reference->contentType))
+ return false;
+ if (!MessageBody_SchemaNameIsValid(reference->schemaName))
+ return false;
+ if (!MessageBody_IsSha256Hex(reference->sha256))
+ return false;
+ return reference->schemaVersion > 0 && reference->uncompressedSize > 0 &&
+ reference->storedCompressedSize > 0;
+}
+
+static bool
+ClaimCheckReference_JsonAddString(json_object *object, const char *name,
+ const char *value)
+{
+ json_object *stringObject;
+
+ if (value == NULL)
+ return false;
+ stringObject = json_object_new_string(value);
+ if (stringObject == NULL)
+ return false;
+ json_object_object_add(object, name, stringObject);
+ return true;
+}
+
+static bool
+ClaimCheckReference_JsonAddUint64(json_object *object, const char *name,
+ uint64_t value)
+{
+ json_object *intObject;
+
+ if (value > INT64_MAX)
+ return false;
+ intObject = json_object_new_int64((int64_t)value);
+ if (intObject == NULL)
+ return false;
+ json_object_object_add(object, name, intObject);
+ return true;
+}
+
+SO_PUBLIC char *
+ClaimCheckReference_ToJson(const struct ClaimCheckReference *reference)
+{
+ json_object *object;
+ const char *json;
+ char *copy;
+
+ if (reference == NULL)
+ return NULL;
+
+ object = json_object_new_object();
+ if (object == NULL)
+ return NULL;
+
+ if (!ClaimCheckReference_JsonAddString(object, "reference_schema_name",
+ reference->referenceSchemaName) ||
+ !ClaimCheckReference_JsonAddUint64(object, "reference_schema_version",
+ reference->referenceSchemaVersion) ||
+ !ClaimCheckReference_JsonAddString(object, "signed_url",
+ reference->signedUrl) ||
+ !ClaimCheckReference_JsonAddString(object, "expires_at",
+ reference->expiresAt) ||
+ !ClaimCheckReference_JsonAddString(object, "bucket",
+ reference->bucket) ||
+ !ClaimCheckReference_JsonAddString(object, "object_key",
+ reference->objectKey) ||
+ !ClaimCheckReference_JsonAddString(object, "sha256",
+ reference->sha256) ||
+ !ClaimCheckReference_JsonAddUint64(object, "uncompressed_size",
+ reference->uncompressedSize) ||
+ !ClaimCheckReference_JsonAddUint64(object, "stored_compressed_size",
+ reference->storedCompressedSize) ||
+ !ClaimCheckReference_JsonAddString(object, "content_type",
+ reference->contentType) ||
+ !ClaimCheckReference_JsonAddString(object, "schema_name",
+ reference->schemaName) ||
+ !ClaimCheckReference_JsonAddUint64(object, "schema_version",
+ reference->schemaVersion) ||
+ !ClaimCheckReference_JsonAddString(object, "content_encoding",
+ reference->contentEncoding)) {
+ json_object_put(object);
+ return NULL;
+ }
+
+ json = json_object_to_json_string_ext(object, JSON_C_TO_STRING_PLAIN);
+ copy = MessageBody_Strdup(json);
+ json_object_put(object);
+ return copy;
+}
+
+static bool
+ClaimCheckReference_ReadString(json_object *object, const char *name,
+ char **value)
+{
+ json_object *field = NULL;
+
+ if (!json_object_object_get_ex(object, name, &field) ||
+ json_object_get_type(field) != json_type_string) {
+ return false;
+ }
+ *value = MessageBody_Strdup(json_object_get_string(field));
+ return *value != NULL;
+}
+
+static bool
+ClaimCheckReference_ReadUint64(json_object *object, const char *name,
+ uint64_t *value)
+{
+ json_object *field = NULL;
+ int64_t parsed;
+
+ if (!json_object_object_get_ex(object, name, &field) ||
+ json_object_get_type(field) != json_type_int) {
+ return false;
+ }
+ parsed = json_object_get_int64(field);
+ if (parsed < 0)
+ return false;
+ *value = (uint64_t)parsed;
+ return true;
+}
+
+SO_PUBLIC struct ClaimCheckReference *
+ClaimCheckReference_FromJson(const char *json)
+{
+ json_object *object;
+ struct ClaimCheckReference *reference;
+ uint64_t referenceVersion;
+ uint64_t schemaVersion;
+
+ if (json == NULL)
+ return NULL;
+
+ object = json_tokener_parse(json);
+ if (object == NULL || json_object_get_type(object) != json_type_object) {
+ if (object != NULL)
+ json_object_put(object);
+ return NULL;
+ }
+
+ reference = calloc(1, sizeof(*reference));
+ if (reference == NULL) {
+ json_object_put(object);
+ return NULL;
+ }
+
+ if (!ClaimCheckReference_ReadString(object, "reference_schema_name",
+ &reference->referenceSchemaName) ||
+ !ClaimCheckReference_ReadUint64(object, "reference_schema_version",
+ &referenceVersion) ||
+ referenceVersion > UINT32_MAX ||
+ !ClaimCheckReference_ReadString(object, "signed_url",
+ &reference->signedUrl) ||
+ !ClaimCheckReference_ReadString(object, "expires_at",
+ &reference->expiresAt) ||
+ !ClaimCheckReference_ReadString(object, "bucket", &reference->bucket) ||
+ !ClaimCheckReference_ReadString(object, "object_key",
+ &reference->objectKey) ||
+ !ClaimCheckReference_ReadString(object, "sha256", &reference->sha256) ||
+ !ClaimCheckReference_ReadUint64(object, "uncompressed_size",
+ &reference->uncompressedSize) ||
+ !ClaimCheckReference_ReadUint64(object, "stored_compressed_size",
+ &reference->storedCompressedSize) ||
+ !ClaimCheckReference_ReadString(object, "content_type",
+ &reference->contentType) ||
+ !ClaimCheckReference_ReadString(object, "schema_name",
+ &reference->schemaName) ||
+ !ClaimCheckReference_ReadUint64(object, "schema_version",
+ &schemaVersion) ||
+ schemaVersion > UINT32_MAX ||
+ !ClaimCheckReference_ReadString(object, "content_encoding",
+ &reference->contentEncoding)) {
+ json_object_put(object);
+ ClaimCheckReference_Destroy(reference);
+ return NULL;
+ }
+
+ reference->referenceSchemaVersion = (uint32_t)referenceVersion;
+ reference->schemaVersion = (uint32_t)schemaVersion;
+
+ json_object_put(object);
+ if (!ClaimCheckReference_Validate(reference)) {
+ ClaimCheckReference_Destroy(reference);
+ return NULL;
+ }
+ return reference;
+}
+
+static bool
+EncodedMessageBody_SetTransport(struct EncodedMessageBody *encoded,
+ const uint8_t *body, size_t bodySize)
+{
+ encoded->transportBody = malloc(bodySize == 0 ? 1 : bodySize);
+ if (encoded->transportBody == NULL)
+ return false;
+ if (bodySize > 0)
+ memcpy(encoded->transportBody, body, bodySize);
+ encoded->transportBodySize = bodySize;
+ return true;
+}
+
+SO_PUBLIC bool
+MessageBody_Encode(const struct MessageBodyPolicy *policy, const uint8_t *body,
+ size_t bodySize,
+ const struct ClaimCheckReference *claimCheckTemplate,
+ struct EncodedMessageBody **encoded)
+{
+ struct MessageBodyPolicy effectivePolicy;
+ struct EncodedMessageBody *result;
+ uint8_t *compressed = NULL;
+ size_t compressedSize = 0;
+ char *referenceJson;
+
+ if (encoded == NULL || (body == NULL && bodySize > 0))
+ return false;
+ *encoded = NULL;
+
+ effectivePolicy = MessageBody_EffectivePolicy(policy);
+ result = calloc(1, sizeof(*result));
+ if (result == NULL)
+ return false;
+
+ if (bodySize <= effectivePolicy.maxInlineBytes) {
+ result->mode = MESSAGE_BODY_MODE_INLINE;
+ if (!EncodedMessageBody_SetTransport(result, body, bodySize)) {
+ EncodedMessageBody_Destroy(result);
+ return false;
+ }
+ *encoded = result;
+ return true;
+ }
+
+ if (!MessageBody_ZlibCompress(body, bodySize, &compressed, &compressedSize)) {
+ EncodedMessageBody_Destroy(result);
+ return false;
+ }
+
+ if (compressedSize <= effectivePolicy.maxInlineBytes) {
+ result->mode = MESSAGE_BODY_MODE_ZLIB;
+ result->transportBody = compressed;
+ result->transportBodySize = compressedSize;
+ result->contentEncoding =
+ MessageBody_Strdup(MESSAGE_BODY_CONTENT_ENCODING_ZLIB);
+ if (result->contentEncoding == NULL) {
+ EncodedMessageBody_Destroy(result);
+ return false;
+ }
+ *encoded = result;
+ return true;
+ }
+
+ result->mode = MESSAGE_BODY_MODE_CLAIM_CHECK;
+ result->claimCheckReference = ClaimCheckReference_Clone(claimCheckTemplate);
+ if (result->claimCheckReference == NULL) {
+ free(compressed);
+ EncodedMessageBody_Destroy(result);
+ return false;
+ }
+
+ result->claimCheckBody = compressed;
+ result->claimCheckBodySize = compressedSize;
+ result->claimCheckReference->referenceSchemaVersion =
+ MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_VERSION;
+ result->claimCheckReference->uncompressedSize = bodySize;
+ result->claimCheckReference->storedCompressedSize = compressedSize;
+ if (!ClaimCheckReference_AssignString(
+ &result->claimCheckReference->referenceSchemaName,
+ MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_NAME) ||
+ !ClaimCheckReference_AssignString(
+ &result->claimCheckReference->contentEncoding,
+ MESSAGE_BODY_CONTENT_ENCODING_ZLIB)) {
+ EncodedMessageBody_Destroy(result);
+ return false;
+ }
+
+ free(result->claimCheckReference->sha256);
+ result->claimCheckReference->sha256 =
+ MessageBody_Sha256Hex(result->claimCheckBody, result->claimCheckBodySize);
+ if (result->claimCheckReference->sha256 == NULL) {
+ EncodedMessageBody_Destroy(result);
+ return false;
+ }
+
+ referenceJson = ClaimCheckReference_ToJson(result->claimCheckReference);
+ if (referenceJson == NULL) {
+ EncodedMessageBody_Destroy(result);
+ return false;
+ }
+ result->transportBodySize = strlen(referenceJson);
+ result->transportBody = (uint8_t *)referenceJson;
+ *encoded = result;
+ return true;
+}
+
+SO_PUBLIC bool
+MessageBody_DecodeInlineWithPolicy(const struct MessageBodyPolicy *policy,
+ const uint8_t *body, size_t bodySize,
+ const char *contentEncoding,
+ uint8_t **decoded, size_t *decodedSize)
+{
+ struct MessageBodyPolicy effectivePolicy;
+
+ if (decoded == NULL || decodedSize == NULL || (body == NULL && bodySize > 0))
+ return false;
+ *decoded = NULL;
+ *decodedSize = 0;
+ effectivePolicy = MessageBody_EffectivePolicy(policy);
+
+ if (contentEncoding == NULL || contentEncoding[0] == '\0') {
+ if (bodySize > effectivePolicy.maxExpandedBytes)
+ return false;
+ *decoded = malloc(bodySize == 0 ? 1 : bodySize);
+ if (*decoded == NULL)
+ return false;
+ if (bodySize > 0)
+ memcpy(*decoded, body, bodySize);
+ *decodedSize = bodySize;
+ return true;
+ }
+
+ if (strcmp(contentEncoding, MESSAGE_BODY_CONTENT_ENCODING_ZLIB) != 0)
+ return false;
+ return MessageBody_ZlibDecompressUnknown(body, bodySize,
+ effectivePolicy.maxExpandedBytes,
+ decoded, decodedSize);
+}
+
+SO_PUBLIC bool
+MessageBody_DecodeInline(const uint8_t *body, size_t bodySize,
+ const char *contentEncoding, uint8_t **decoded,
+ size_t *decodedSize)
+{
+ return MessageBody_DecodeInlineWithPolicy(NULL, body, bodySize,
+ contentEncoding, decoded,
+ decodedSize);
+}
+
+SO_PUBLIC bool
+MessageBody_DecodeClaimCheckWithPolicy(const struct MessageBodyPolicy *policy,
+ const uint8_t *compressedBody,
+ size_t compressedBodySize,
+ const struct ClaimCheckReference *reference,
+ uint8_t **decoded,
+ size_t *decodedSize)
+{
+ struct MessageBodyPolicy effectivePolicy;
+ char *sha256;
+ bool ok;
+
+ if (decoded == NULL || decodedSize == NULL ||
+ (compressedBody == NULL && compressedBodySize > 0))
+ return false;
+ *decoded = NULL;
+ *decodedSize = 0;
+ effectivePolicy = MessageBody_EffectivePolicy(policy);
+
+ if (!ClaimCheckReference_Validate(reference) ||
+ reference->storedCompressedSize != compressedBodySize)
+ return false;
+ if (reference->uncompressedSize > effectivePolicy.maxExpandedBytes)
+ return false;
+
+ sha256 = MessageBody_Sha256Hex(compressedBody, compressedBodySize);
+ if (sha256 == NULL)
+ return false;
+ ok = strcmp(sha256, reference->sha256) == 0;
+ free(sha256);
+ if (!ok)
+ return false;
+
+ return MessageBody_ZlibDecompressExpected(compressedBody, compressedBodySize,
+ reference->uncompressedSize,
+ effectivePolicy.maxExpandedBytes,
+ decoded, decodedSize);
+}
+
+SO_PUBLIC bool
+MessageBody_DecodeClaimCheck(const uint8_t *compressedBody,
+ size_t compressedBodySize,
+ const struct ClaimCheckReference *reference,
+ uint8_t **decoded, size_t *decodedSize)
+{
+ return MessageBody_DecodeClaimCheckWithPolicy(NULL, compressedBody,
+ compressedBodySize, reference,
+ decoded, decodedSize);
+}
+
+SO_PUBLIC void
+EncodedMessageBody_Destroy(struct EncodedMessageBody *encoded)
+{
+ if (encoded == NULL)
+ return;
+ free(encoded->transportBody);
+ free(encoded->contentEncoding);
+ free(encoded->claimCheckBody);
+ ClaimCheckReference_Destroy(encoded->claimCheckReference);
+ free(encoded);
+}
diff --git a/src/messages_next.c b/src/messages_next.c
new file mode 100644
index 0000000..199f4da
--- /dev/null
+++ b/src/messages_next.c
@@ -0,0 +1,2770 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+struct KnownSchema
+{
+ const char *name;
+ uint32_t version;
+};
+
+static const struct KnownSchema KnownSchemas[] = {
+ { RZB_NEXT_SCHEMA_CLAIM_CHECK_REFERENCE, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_CNC_REGISTRATION_REQUEST, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_CNC_REGISTRATION_ACCEPTED, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_CNC_REGISTRATION_REJECTED, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_CNC_LIVENESS, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_CNC_BYE, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_CNC_DIRECTED_COMMAND, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_CNC_DISPATCHER_HELLO, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_BLOCK_SUBMISSION, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_BLOCK_UPDATE, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_INSPECTION_WORK, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_ANALYSIS_RESULT, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_CACHE_REQUEST, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_CACHE_RESPONSE, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_CATALOG_INVALIDATION, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_FILE_REMOVE_REQUEST, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_FILE_REMOVE_RESULT, RZB_NEXT_SCHEMA_VERSION },
+ { RZB_NEXT_SCHEMA_SEARCH_EXPORT, RZB_NEXT_SCHEMA_VERSION }
+};
+
+static const char * RzbNext_GetString(json_object *object, const char *field);
+static uint8_t * RzbNext_CopyBytes(const uint8_t *value, size_t size);
+static bool RzbNextPrepared_AddHeader(
+ struct RzbNextPreparedRabbitMqMessage *prepared,
+ const char *name,
+ const char *value
+);
+static const char * RzbNextHeader_Value(
+ const struct RzbNextMessageHeader *headers,
+ size_t headerCount,
+ const char *name
+);
+static bool RzbNextDecoded_FromBytes(
+ const uint8_t *bytes,
+ size_t size,
+ const char *schemaName,
+ const char *schemaVersion,
+ struct ClaimCheckReference *claimCheckReference,
+ struct RzbNextDecodedRabbitMqMessage **decoded
+);
+static bool RzbNext_StringArrayContains(json_object *array, const char *needle);
+static bool RzbNext_MetadataValueWithinLimit(json_object *value);
+
+static char *
+RzbNext_Strdup(const char *value)
+{
+ char *copy;
+ size_t length;
+
+ if (value == NULL)
+ return NULL;
+
+ length = strlen(value) + 1;
+ copy = malloc(length);
+ if (copy == NULL)
+ return NULL;
+ memcpy(copy, value, length);
+ return copy;
+}
+
+static char *
+RzbNext_JsonToOwnedString(json_object *object)
+{
+ const char *jsonText;
+
+ if (object == NULL)
+ return NULL;
+ jsonText = json_object_to_json_string_ext(object, JSON_C_TO_STRING_PLAIN);
+ return RzbNext_Strdup(jsonText);
+}
+
+static char *
+RzbNext_Format2(const char *prefix, const char *value)
+{
+ char *formatted;
+ size_t length;
+
+ if (prefix == NULL || value == NULL)
+ return NULL;
+ length = strlen(prefix) + 1 + strlen(value) + 1;
+ formatted = malloc(length);
+ if (formatted == NULL)
+ return NULL;
+ snprintf(formatted, length, "%s.%s", prefix, value);
+ return formatted;
+}
+
+static char *
+RzbNext_Format3(const char *prefix, const char *middle, const char *suffix)
+{
+ char *formatted;
+ size_t length;
+
+ if (prefix == NULL || middle == NULL || suffix == NULL)
+ return NULL;
+ length = strlen(prefix) + 1 + strlen(middle) + 1 + strlen(suffix) + 1;
+ formatted = malloc(length);
+ if (formatted == NULL)
+ return NULL;
+ snprintf(formatted, length, "%s.%s.%s", prefix, middle, suffix);
+ return formatted;
+}
+
+static uint8_t *
+RzbNext_CopyBytes(const uint8_t *value, size_t size)
+{
+ uint8_t *copy;
+
+ if (value == NULL && size > 0)
+ return NULL;
+ copy = malloc(size == 0 ? 1 : size);
+ if (copy == NULL)
+ return NULL;
+ if (size > 0)
+ memcpy(copy, value, size);
+ return copy;
+}
+
+static bool
+RzbNextPrepared_AddHeader(struct RzbNextPreparedRabbitMqMessage *prepared,
+ const char *name, const char *value)
+{
+ struct RzbNextMessageHeader *headers;
+ char *nameCopy;
+ char *valueCopy;
+
+ if (prepared == NULL || name == NULL || value == NULL)
+ return false;
+ nameCopy = RzbNext_Strdup(name);
+ valueCopy = RzbNext_Strdup(value);
+ if (nameCopy == NULL || valueCopy == NULL) {
+ free(nameCopy);
+ free(valueCopy);
+ return false;
+ }
+ headers = realloc(prepared->headers,
+ sizeof(*prepared->headers) * (prepared->headerCount + 1U));
+ if (headers == NULL) {
+ free(nameCopy);
+ free(valueCopy);
+ return false;
+ }
+ prepared->headers = headers;
+ prepared->headers[prepared->headerCount].name = nameCopy;
+ prepared->headers[prepared->headerCount].value = valueCopy;
+ prepared->headerCount++;
+ return true;
+}
+
+static const char *
+RzbNextHeader_Value(const struct RzbNextMessageHeader *headers,
+ size_t headerCount, const char *name)
+{
+ size_t index;
+
+ if (headers == NULL || name == NULL)
+ return NULL;
+ for (index = 0; index < headerCount; index++) {
+ if (headers[index].name != NULL &&
+ strcmp(headers[index].name, name) == 0) {
+ return headers[index].value;
+ }
+ }
+ return NULL;
+}
+
+static bool
+RzbNext_IsLowerHex(char value)
+{
+ return (value >= '0' && value <= '9') || (value >= 'a' && value <= 'f');
+}
+
+static bool
+RzbNext_IsUuid(const char *value)
+{
+ size_t index;
+
+ if (value == NULL || strlen(value) != 36)
+ return false;
+ for (index = 0; index < 36; index++) {
+ if (index == 8 || index == 13 || index == 18 || index == 23) {
+ if (value[index] != '-')
+ return false;
+ } else if (!RzbNext_IsLowerHex(value[index])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool
+RzbNext_IsSafeKey(const char *value)
+{
+ size_t index;
+
+ if (value == NULL || value[0] == '\0' || strlen(value) > 128)
+ return false;
+ if (!islower((unsigned char)value[0]) && !isdigit((unsigned char)value[0]))
+ return false;
+ for (index = 1; value[index] != '\0'; index++) {
+ if (!islower((unsigned char)value[index]) &&
+ !isdigit((unsigned char)value[index]) &&
+ value[index] != '_' &&
+ value[index] != '-') {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool
+RzbNext_IsDataTypeName(const char *value)
+{
+ size_t index;
+
+ if (value == NULL)
+ return false;
+ if (strcmp(value, "ANY_DATA") == 0)
+ return true;
+ if (value[0] == '\0' || strlen(value) > 128)
+ return false;
+ if (!islower((unsigned char)value[0]) && !isdigit((unsigned char)value[0]))
+ return false;
+ for (index = 1; value[index] != '\0'; index++) {
+ if (!islower((unsigned char)value[index]) &&
+ !isdigit((unsigned char)value[index]) &&
+ value[index] != '_' &&
+ value[index] != '.' &&
+ value[index] != '/' &&
+ value[index] != '+' &&
+ value[index] != '-') {
+ return false;
+ }
+ }
+ return strchr(value, '~') == NULL;
+}
+
+static bool
+RzbNext_IsSha256(const char *value)
+{
+ size_t index;
+
+ if (value == NULL || strlen(value) != 64)
+ return false;
+ for (index = 0; index < 64; index++) {
+ if (!RzbNext_IsLowerHex(value[index]))
+ return false;
+ }
+ return true;
+}
+
+static bool
+RzbNext_IsTimestamp(const char *value)
+{
+ size_t index;
+
+ if (value == NULL || strlen(value) != 24)
+ return false;
+ for (index = 0; index < 24; index++) {
+ switch (index) {
+ case 4:
+ case 7:
+ if (value[index] != '-')
+ return false;
+ break;
+ case 10:
+ if (value[index] != 'T')
+ return false;
+ break;
+ case 13:
+ case 16:
+ if (value[index] != ':')
+ return false;
+ break;
+ case 19:
+ if (value[index] != '.')
+ return false;
+ break;
+ case 23:
+ if (value[index] != 'Z')
+ return false;
+ break;
+ default:
+ if (!isdigit((unsigned char)value[index]))
+ return false;
+ break;
+ }
+ }
+ return true;
+}
+
+static bool
+RzbNext_IsSchemaName(const char *value)
+{
+ size_t index;
+
+ if (value == NULL || strncmp(value, "razorback.", 10) != 0 ||
+ value[10] == '\0') {
+ return false;
+ }
+ for (index = 0; value[index] != '\0'; index++) {
+ if (!islower((unsigned char)value[index]) &&
+ !isdigit((unsigned char)value[index]) &&
+ value[index] != '.' &&
+ value[index] != '_' &&
+ value[index] != '-') {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool
+RzbNext_IsPrintableId(const char *value)
+{
+ size_t index;
+ size_t length;
+
+ if (value == NULL)
+ return false;
+ length = strlen(value);
+ if (length == 0 || length > 512)
+ return false;
+ for (index = 0; index < length; index++) {
+ if ((unsigned char)value[index] < 33 ||
+ (unsigned char)value[index] > 126) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool
+RzbNext_StringIn(const char *value, const char * const *allowed, size_t count)
+{
+ size_t index;
+
+ if (value == NULL)
+ return false;
+ for (index = 0; index < count; index++) {
+ if (strcmp(value, allowed[index]) == 0)
+ return true;
+ }
+ return false;
+}
+
+static bool
+RzbNext_TextBounded(const char *value, size_t maxLength, bool trimCheck)
+{
+ size_t length;
+
+ if (value == NULL)
+ return false;
+ length = strlen(value);
+ if (length == 0 || length > maxLength)
+ return false;
+ if (trimCheck &&
+ (isspace((unsigned char)value[0]) ||
+ isspace((unsigned char)value[length - 1]))) {
+ return false;
+ }
+ return true;
+}
+
+static bool
+RzbNext_ObjectHasField(json_object *object, const char *field)
+{
+ json_object *value;
+
+ return json_object_object_get_ex(object, field, &value);
+}
+
+static size_t
+RzbNext_ObjectFieldCount(json_object *object)
+{
+ size_t count = 0;
+
+ if (object == NULL || json_object_get_type(object) != json_type_object)
+ return 0;
+ json_object_object_foreach(object, key, value) {
+ (void)key;
+ (void)value;
+ count++;
+ }
+ return count;
+}
+
+static bool
+RzbNext_ObjectOnlyHasFields(json_object *object,
+ const char * const *allowed,
+ size_t count)
+{
+ json_object_object_foreach(object, key, value) {
+ (void)value;
+ if (!RzbNext_StringIn(key, allowed, count))
+ return false;
+ }
+ return true;
+}
+
+static json_object *
+RzbNext_GetTyped(json_object *object, const char *field, enum json_type type)
+{
+ json_object *value;
+
+ if (!json_object_object_get_ex(object, field, &value))
+ return NULL;
+ if (json_object_get_type(value) != type)
+ return NULL;
+ return value;
+}
+
+static bool
+RzbNext_RequireString(json_object *object, const char *field,
+ bool (*validator)(const char *))
+{
+ const char *value = RzbNext_GetString(object, field);
+
+ return value != NULL && (validator == NULL || validator(value));
+}
+
+static bool
+RzbNext_RequireEnum(json_object *object, const char *field,
+ const char * const *allowed, size_t count)
+{
+ return RzbNext_StringIn(RzbNext_GetString(object, field), allowed, count);
+}
+
+static bool
+RzbNext_RequireBool(json_object *object, const char *field)
+{
+ return RzbNext_GetTyped(object, field, json_type_boolean) != NULL;
+}
+
+static bool
+RzbNext_RequireIntRange(json_object *object, const char *field,
+ int64_t minimum, int64_t maximum)
+{
+ json_object *value = RzbNext_GetTyped(object, field, json_type_int);
+ int64_t integer;
+
+ if (value == NULL)
+ return false;
+ integer = json_object_get_int64(value);
+ return integer >= minimum && integer <= maximum;
+}
+
+static bool
+RzbNext_RequirePositiveInt(json_object *object, const char *field)
+{
+ return RzbNext_RequireIntRange(object, field, 1, INT64_MAX);
+}
+
+static bool
+RzbNext_OptionalTimestamp(json_object *object, const char *field)
+{
+ return !RzbNext_ObjectHasField(object, field) ||
+ RzbNext_RequireString(object, field, RzbNext_IsTimestamp);
+}
+
+static bool
+RzbNext_StringArrayValid(json_object *array, bool (*validator)(const char *),
+ size_t minimum, bool unique)
+{
+ size_t count;
+ size_t index;
+ size_t other;
+
+ if (array == NULL || json_object_get_type(array) != json_type_array)
+ return false;
+ count = json_object_array_length(array);
+ if (count < minimum)
+ return false;
+ for (index = 0; index < count; index++) {
+ json_object *item = json_object_array_get_idx(array, index);
+ const char *value;
+
+ if (item == NULL || json_object_get_type(item) != json_type_string)
+ return false;
+ value = json_object_get_string(item);
+ if (validator != NULL && !validator(value))
+ return false;
+ if (!unique)
+ continue;
+ for (other = index + 1; other < count; other++) {
+ json_object *otherItem = json_object_array_get_idx(array, other);
+
+ if (otherItem != NULL &&
+ json_object_get_type(otherItem) == json_type_string &&
+ strcmp(value, json_object_get_string(otherItem)) == 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+static bool
+RzbNext_StringArrayContains(json_object *array, const char *needle)
+{
+ size_t count;
+ size_t index;
+
+ if (array == NULL || needle == NULL ||
+ !json_object_is_type(array, json_type_array))
+ return false;
+ count = json_object_array_length(array);
+ for (index = 0; index < count; index++) {
+ json_object *item = json_object_array_get_idx(array, index);
+ const char *value;
+
+ if (item == NULL || !json_object_is_type(item, json_type_string))
+ continue;
+ value = json_object_get_string(item);
+ if (value != NULL && strcmp(value, needle) == 0)
+ return true;
+ }
+ return false;
+}
+
+static bool
+RzbNext_StringArrayFieldValid(json_object *object, const char *field,
+ bool (*validator)(const char *), size_t minimum,
+ bool unique)
+{
+ return RzbNext_StringArrayValid(RzbNext_GetTyped(object, field,
+ json_type_array),
+ validator, minimum, unique);
+}
+
+static bool
+RzbNext_IsSystemTag(const char *value)
+{
+ static const char * const tags[] = {
+ "GOOD", "BAD", "SUSPICIOUS", "ALLOW_LIST", "BLOCK_LIST",
+ "DIRTY", "NOT_STORED", "PROCESSING", "REMOVED"
+ };
+
+ return RzbNext_StringIn(value, tags, sizeof(tags) / sizeof(tags[0]));
+}
+
+static bool
+RzbNext_ValidateBlock(json_object *object, bool storageOnly)
+{
+ static const char * const blockFields[] = {
+ "sha256", "size", "data_type"
+ };
+ static const char * const storageFields[] = {
+ "sha256", "size"
+ };
+
+ if (object == NULL || json_object_get_type(object) != json_type_object)
+ return false;
+ if (storageOnly) {
+ if (!RzbNext_ObjectOnlyHasFields(
+ object, storageFields,
+ sizeof(storageFields) / sizeof(storageFields[0]))) {
+ return false;
+ }
+ } else if (!RzbNext_ObjectOnlyHasFields(
+ object, blockFields,
+ sizeof(blockFields) / sizeof(blockFields[0]))) {
+ return false;
+ }
+
+ if (!RzbNext_RequireString(object, "sha256", RzbNext_IsSha256) ||
+ !RzbNext_RequirePositiveInt(object, "size")) {
+ return false;
+ }
+ return storageOnly ||
+ RzbNext_RequireString(object, "data_type", RzbNext_IsDataTypeName);
+}
+
+static bool
+RzbNext_ValidateMetadataArray(json_object *array, size_t minimum, bool updates)
+{
+ static const char * const recordFields[] = {
+ "name", "type", "value"
+ };
+ static const char * const updateFields[] = {
+ "name", "type", "created_at", "value"
+ };
+ size_t count;
+ size_t index;
+
+ if (array == NULL || json_object_get_type(array) != json_type_array)
+ return false;
+ count = json_object_array_length(array);
+ if (count < minimum)
+ return false;
+ for (index = 0; index < count; index++) {
+ json_object *record = json_object_array_get_idx(array, index);
+ json_object *value;
+
+ if (record == NULL ||
+ json_object_get_type(record) != json_type_object) {
+ return false;
+ }
+ if (updates) {
+ if (!RzbNext_ObjectOnlyHasFields(
+ record, updateFields,
+ sizeof(updateFields) / sizeof(updateFields[0])) ||
+ !RzbNext_RequireString(record, "created_at",
+ RzbNext_IsTimestamp)) {
+ return false;
+ }
+ } else if (!RzbNext_ObjectOnlyHasFields(
+ record, recordFields,
+ sizeof(recordFields) / sizeof(recordFields[0]))) {
+ return false;
+ }
+ if (!RzbNext_RequireString(record, "name", RzbNext_IsSafeKey) ||
+ !RzbNext_RequireString(record, "type", RzbNext_IsSafeKey) ||
+ !json_object_object_get_ex(record, "value", &value) ||
+ json_object_get_type(value) == json_type_null ||
+ !RzbNext_MetadataValueWithinLimit(value)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool
+RzbNext_MetadataValueWithinLimit(json_object *value)
+{
+ const char *jsonText;
+
+ jsonText = json_object_to_json_string_ext(value, JSON_C_TO_STRING_PLAIN);
+ return jsonText != NULL &&
+ strlen(jsonText) <= RzbNextMetadata_MaxValueBytes();
+}
+
+static bool
+RzbNext_MetadataArrayHasOversizedValue(json_object *array)
+{
+ size_t count;
+ size_t index;
+
+ if (array == NULL || json_object_get_type(array) != json_type_array)
+ return false;
+ count = json_object_array_length(array);
+ for (index = 0; index < count; index++) {
+ json_object *record = json_object_array_get_idx(array, index);
+ json_object *value;
+
+ if (record == NULL ||
+ json_object_get_type(record) != json_type_object ||
+ !json_object_object_get_ex(record, "value", &value)) {
+ continue;
+ }
+ if (!RzbNext_MetadataValueWithinLimit(value))
+ return true;
+ }
+ return false;
+}
+
+static bool
+RzbNext_AlertsHaveOversizedMetadata(json_object *alerts)
+{
+ size_t count;
+ size_t index;
+
+ if (alerts == NULL || json_object_get_type(alerts) != json_type_array)
+ return false;
+ count = json_object_array_length(alerts);
+ for (index = 0; index < count; index++) {
+ json_object *alert = json_object_array_get_idx(alerts, index);
+ json_object *metadata;
+
+ if (alert == NULL || json_object_get_type(alert) != json_type_object)
+ continue;
+ if (json_object_object_get_ex(alert, "metadata", &metadata) &&
+ RzbNext_MetadataArrayHasOversizedValue(metadata)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool
+RzbNext_AnalysisResultHasOversizedMetadata(json_object *result)
+{
+ json_object *value;
+
+ if (result == NULL || json_object_get_type(result) != json_type_object)
+ return false;
+ if (json_object_object_get_ex(result, "block_metadata_updates", &value) &&
+ RzbNext_MetadataArrayHasOversizedValue(value)) {
+ return true;
+ }
+ if (json_object_object_get_ex(result, "metadata", &value) &&
+ RzbNext_MetadataArrayHasOversizedValue(value)) {
+ return true;
+ }
+ return json_object_object_get_ex(result, "alerts", &value) &&
+ RzbNext_AlertsHaveOversizedMetadata(value);
+}
+
+static bool
+RzbNext_OptionalMetadataArray(json_object *object, const char *field,
+ size_t minimum, bool updates)
+{
+ if (!RzbNext_ObjectHasField(object, field))
+ return true;
+ return RzbNext_ValidateMetadataArray(
+ RzbNext_GetTyped(object, field, json_type_array), minimum, updates);
+}
+
+static bool
+RzbNext_ValidateTagMutations(json_object *object)
+{
+ static const char * const fields[] = {
+ "set_system_tags", "unset_system_tags",
+ "set_enterprise_tags", "unset_enterprise_tags"
+ };
+ bool seen = false;
+
+ if (object == NULL || json_object_get_type(object) != json_type_object)
+ return false;
+ if (!RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0]))) {
+ return false;
+ }
+ if (RzbNext_ObjectHasField(object, "set_system_tags")) {
+ seen = true;
+ if (!RzbNext_StringArrayFieldValid(object, "set_system_tags",
+ RzbNext_IsSystemTag, 1, true))
+ return false;
+ }
+ if (RzbNext_ObjectHasField(object, "unset_system_tags")) {
+ seen = true;
+ if (!RzbNext_StringArrayFieldValid(object, "unset_system_tags",
+ RzbNext_IsSystemTag, 1, true))
+ return false;
+ }
+ if (RzbNext_ObjectHasField(object, "set_enterprise_tags")) {
+ seen = true;
+ if (!RzbNext_StringArrayFieldValid(object, "set_enterprise_tags",
+ RzbNext_IsSafeKey, 1, true))
+ return false;
+ }
+ if (RzbNext_ObjectHasField(object, "unset_enterprise_tags")) {
+ seen = true;
+ if (!RzbNext_StringArrayFieldValid(object, "unset_enterprise_tags",
+ RzbNext_IsSafeKey, 1, true))
+ return false;
+ }
+ return seen;
+}
+
+static bool
+RzbNext_OptionalTagMutations(json_object *object, const char *field)
+{
+ if (!RzbNext_ObjectHasField(object, field))
+ return true;
+ return RzbNext_ValidateTagMutations(RzbNext_GetTyped(object, field,
+ json_type_object));
+}
+
+static bool
+RzbNext_ValidateTextMessage(json_object *object, const char *field,
+ size_t maxLength, bool trimCheck)
+{
+ return RzbNext_TextBounded(RzbNext_GetString(object, field), maxLength,
+ trimCheck);
+}
+
+static bool
+RzbNext_ValidateClaimCheck(json_object *object)
+{
+ static const char * const fields[] = {
+ "reference_schema_name", "reference_schema_version", "signed_url",
+ "expires_at", "bucket", "object_key", "sha256", "uncompressed_size",
+ "stored_compressed_size", "content_type", "schema_name",
+ "schema_version", "content_encoding"
+ };
+
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "reference_schema_name", NULL) &&
+ strcmp(RzbNext_GetString(object, "reference_schema_name"),
+ RZB_NEXT_SCHEMA_CLAIM_CHECK_REFERENCE) == 0 &&
+ RzbNext_RequireIntRange(object, "reference_schema_version", 1, 1) &&
+ RzbNext_ValidateTextMessage(object, "signed_url", 4096, false) &&
+ strstr(RzbNext_GetString(object, "signed_url"), "://") != NULL &&
+ RzbNext_RequireString(object, "expires_at", RzbNext_IsTimestamp) &&
+ RzbNext_ValidateTextMessage(object, "bucket", 1024, false) &&
+ RzbNext_ValidateTextMessage(object, "object_key", 4096, false) &&
+ RzbNext_RequireString(object, "sha256", RzbNext_IsSha256) &&
+ RzbNext_RequirePositiveInt(object, "uncompressed_size") &&
+ RzbNext_RequirePositiveInt(object, "stored_compressed_size") &&
+ RzbNext_ValidateTextMessage(object, "content_type", 256, false) &&
+ RzbNext_RequireString(object, "schema_name", RzbNext_IsSchemaName) &&
+ RzbNext_RequirePositiveInt(object, "schema_version") &&
+ RzbNext_RequireString(object, "content_encoding", NULL) &&
+ strcmp(RzbNext_GetString(object, "content_encoding"), "zlib") == 0;
+}
+
+static bool
+RzbNext_ValidateCapabilities(json_object *object)
+{
+ static const char * const fields[] = {
+ "component_version", "sdk_name", "sdk_version",
+ "supported_message_body_modes", "supports_deferred_results"
+ };
+ static const char * const modes[] = {
+ "inline", "zlib", "claim_check"
+ };
+ json_object *array;
+ size_t count;
+ size_t index;
+ bool hasInline = false;
+
+ if (object == NULL || json_object_get_type(object) != json_type_object)
+ return false;
+ if (!RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) ||
+ !RzbNext_ValidateTextMessage(object, "component_version", 128, true) ||
+ !RzbNext_ValidateTextMessage(object, "sdk_name", 128, true) ||
+ !RzbNext_ValidateTextMessage(object, "sdk_version", 128, true) ||
+ !RzbNext_RequireBool(object, "supports_deferred_results")) {
+ return false;
+ }
+
+ array = RzbNext_GetTyped(object, "supported_message_body_modes",
+ json_type_array);
+ if (!RzbNext_StringArrayValid(array, NULL, 1, true))
+ return false;
+ count = json_object_array_length(array);
+ for (index = 0; index < count; index++) {
+ const char *mode =
+ json_object_get_string(json_object_array_get_idx(array, index));
+
+ if (!RzbNext_StringIn(mode, modes, sizeof(modes) / sizeof(modes[0])))
+ return false;
+ if (strcmp(mode, "inline") == 0)
+ hasInline = true;
+ }
+ return hasInline;
+}
+
+static bool
+RzbNext_ValidateDataTypes(json_object *object)
+{
+ json_object *array;
+ size_t count;
+ size_t index;
+ bool hasAnyData = false;
+
+ if (!RzbNext_ObjectHasField(object, "data_types"))
+ return true;
+ array = RzbNext_GetTyped(object, "data_types", json_type_array);
+ if (!RzbNext_StringArrayValid(array, RzbNext_IsDataTypeName, 1, true))
+ return false;
+ count = json_object_array_length(array);
+ for (index = 0; index < count; index++) {
+ const char *dataType =
+ json_object_get_string(json_object_array_get_idx(array, index));
+
+ if (strcmp(dataType, "ANY_DATA") == 0)
+ hasAnyData = true;
+ }
+ return !hasAnyData || count == 1;
+}
+
+static bool
+RzbNext_ValidateCncRegistrationRequest(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "request_id", "nugget_uuid",
+ "process_uuid", "nugget_type", "app_type", "data_types",
+ "capabilities", "desired_runtime_policy", "created_at"
+ };
+ static const char * const runtimePolicies[] = { "running", "paused" };
+
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "request_id", RzbNext_IsUuid) &&
+ RzbNext_RequireString(object, "nugget_uuid", RzbNext_IsUuid) &&
+ RzbNext_RequireString(object, "process_uuid", RzbNext_IsUuid) &&
+ RzbNext_RequireString(object, "nugget_type", RzbNext_IsSafeKey) &&
+ RzbNext_RequireString(object, "app_type", RzbNext_IsSafeKey) &&
+ RzbNext_ValidateDataTypes(object) &&
+ RzbNext_ValidateCapabilities(RzbNext_GetTyped(object, "capabilities",
+ json_type_object)) &&
+ RzbNext_RequireEnum(object, "desired_runtime_policy",
+ runtimePolicies,
+ sizeof(runtimePolicies) /
+ sizeof(runtimePolicies[0])) &&
+ RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp);
+}
+
+static bool
+RzbNext_ValidateCncRegistrationAccepted(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "request_id", "nugget_uuid",
+ "registration_generation", "effective_runtime_policy",
+ "liveness_interval", "liveness_freshness_window",
+ "liveness_clock_skew_tolerance", "created_at"
+ };
+ static const char * const runtimePolicies[] = { "running", "paused" };
+
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "request_id", RzbNext_IsUuid) &&
+ RzbNext_RequireString(object, "nugget_uuid", RzbNext_IsUuid) &&
+ RzbNext_RequireString(object, "registration_generation",
+ RzbNext_IsUuid) &&
+ RzbNext_RequireEnum(object, "effective_runtime_policy",
+ runtimePolicies,
+ sizeof(runtimePolicies) /
+ sizeof(runtimePolicies[0])) &&
+ RzbNext_RequirePositiveInt(object, "liveness_interval") &&
+ RzbNext_RequirePositiveInt(object, "liveness_freshness_window") &&
+ RzbNext_RequirePositiveInt(object,
+ "liveness_clock_skew_tolerance") &&
+ RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp);
+}
+
+static bool
+RzbNext_ValidateCncRegistrationRejected(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "request_id", "nugget_uuid",
+ "reason_code", "message", "retryable", "retry_after", "created_at"
+ };
+ static const char * const reasons[] = {
+ "invalid_schema", "unsupported_schema_version", "invalid_uuid",
+ "invalid_catalog_reference", "retired_catalog_reference",
+ "unauthorized", "auto_provision_disabled",
+ "auto_provision_not_allowed", "topology_error", "duplicate_identity",
+ "dependency_unavailable", "rate_limited", "internal_error"
+ };
+ json_object *retryable = RzbNext_GetTyped(object, "retryable",
+ json_type_boolean);
+
+ if (!RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) ||
+ !RzbNext_RequireString(object, "request_id", RzbNext_IsUuid) ||
+ !RzbNext_RequireString(object, "nugget_uuid", RzbNext_IsUuid) ||
+ !RzbNext_RequireEnum(object, "reason_code", reasons,
+ sizeof(reasons) / sizeof(reasons[0])) ||
+ !RzbNext_ValidateTextMessage(object, "message", 4096, true) ||
+ retryable == NULL ||
+ !RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp)) {
+ return false;
+ }
+ if (!RzbNext_ObjectHasField(object, "retry_after"))
+ return true;
+ return json_object_get_boolean(retryable) &&
+ RzbNext_RequirePositiveInt(object, "retry_after");
+}
+
+static bool
+RzbNext_ValidateCncLiveness(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "nugget_uuid",
+ "registration_generation", "runtime_policy", "availability",
+ "created_at"
+ };
+ static const char * const runtimePolicies[] = { "running", "paused" };
+ static const char * const availability[] = {
+ "ready", "registration_gated", "dependency_paused",
+ "draining", "failed"
+ };
+
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "nugget_uuid", RzbNext_IsUuid) &&
+ RzbNext_RequireString(object, "registration_generation",
+ RzbNext_IsUuid) &&
+ RzbNext_RequireEnum(object, "runtime_policy", runtimePolicies,
+ sizeof(runtimePolicies) /
+ sizeof(runtimePolicies[0])) &&
+ RzbNext_RequireEnum(object, "availability", availability,
+ sizeof(availability) / sizeof(availability[0])) &&
+ RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp);
+}
+
+static bool
+RzbNext_ValidateCncBye(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "nugget_uuid",
+ "registration_generation", "reason", "created_at"
+ };
+ static const char * const reasons[] = {
+ "shutdown", "restart", "operator_requested", "terminate_command",
+ "fatal_error", "dependency_failure"
+ };
+
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "nugget_uuid", RzbNext_IsUuid) &&
+ RzbNext_RequireString(object, "registration_generation",
+ RzbNext_IsUuid) &&
+ RzbNext_RequireEnum(object, "reason", reasons,
+ sizeof(reasons) / sizeof(reasons[0])) &&
+ RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp);
+}
+
+static bool
+RzbNext_DirectedReasonAllowed(const char *command, const char *reason)
+{
+ static const char * const pauseReasons[] = {
+ "operator_requested", "policy_change", "internal_recovery"
+ };
+ static const char * const goReasons[] = {
+ "operator_requested", "policy_change", "dependency_recovery",
+ "internal_recovery"
+ };
+ static const char * const terminateReasons[] = {
+ "operator_requested", "shutdown", "internal_recovery"
+ };
+ static const char * const reregisterReasons[] = {
+ "stale_presence", "generation_mismatch", "catalog_invalidation",
+ "dependency_recovery", "internal_recovery"
+ };
+ static const char * const cacheReasons[] = {
+ "operator_requested", "policy_change", "catalog_invalidation",
+ "internal_recovery"
+ };
+
+ if (strcmp(command, "pause") == 0)
+ return RzbNext_StringIn(reason, pauseReasons,
+ sizeof(pauseReasons) /
+ sizeof(pauseReasons[0]));
+ if (strcmp(command, "go") == 0)
+ return RzbNext_StringIn(reason, goReasons,
+ sizeof(goReasons) / sizeof(goReasons[0]));
+ if (strcmp(command, "terminate") == 0)
+ return RzbNext_StringIn(reason, terminateReasons,
+ sizeof(terminateReasons) /
+ sizeof(terminateReasons[0]));
+ if (strcmp(command, "re_register") == 0)
+ return RzbNext_StringIn(reason, reregisterReasons,
+ sizeof(reregisterReasons) /
+ sizeof(reregisterReasons[0]));
+ if (strcmp(command, "cache_invalidate") == 0)
+ return RzbNext_StringIn(reason, cacheReasons,
+ sizeof(cacheReasons) /
+ sizeof(cacheReasons[0]));
+ return false;
+}
+
+static bool
+RzbNext_ValidateCncDirectedCommand(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "command_id", "target_nugget_uuid",
+ "registration_generation", "command", "reason_code", "invalidation_id",
+ "created_at"
+ };
+ const char *command;
+ const char *reason;
+ bool hasInvalidation;
+
+ if (!RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) ||
+ !RzbNext_RequireString(object, "command_id", RzbNext_IsUuid) ||
+ !RzbNext_RequireString(object, "target_nugget_uuid", RzbNext_IsUuid) ||
+ !RzbNext_RequireString(object, "registration_generation",
+ RzbNext_IsUuid) ||
+ !RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp)) {
+ return false;
+ }
+ command = RzbNext_GetString(object, "command");
+ reason = RzbNext_GetString(object, "reason_code");
+ hasInvalidation = RzbNext_ObjectHasField(object, "invalidation_id");
+ if (command == NULL || reason == NULL ||
+ !RzbNext_DirectedReasonAllowed(command, reason)) {
+ return false;
+ }
+ if (strcmp(command, "cache_invalidate") == 0) {
+ return hasInvalidation &&
+ RzbNext_RequireString(object, "invalidation_id",
+ RzbNext_IsUuid);
+ }
+ return !hasInvalidation;
+}
+
+static bool
+RzbNext_ValidateDispatcherHelloReasons(json_object *object, bool ready,
+ const char *availability)
+{
+ static const char * const reasonValues[] = {
+ "starting", "draining", "rabbitmq_unavailable", "mysql_unavailable",
+ "etcd_unavailable", "cassandra_unavailable", "kafka_unavailable",
+ "claim_check_unavailable", "catalog_cache_stale", "internal_error"
+ };
+ json_object *array = RzbNext_GetTyped(object, "dependency_reason_codes",
+ json_type_array);
+ size_t count;
+ size_t index;
+
+ if (!RzbNext_StringArrayValid(array, NULL, 0, true))
+ return false;
+ count = json_object_array_length(array);
+ for (index = 0; index < count; index++) {
+ const char *reason =
+ json_object_get_string(json_object_array_get_idx(array, index));
+
+ if (!RzbNext_StringIn(reason, reasonValues,
+ sizeof(reasonValues) /
+ sizeof(reasonValues[0]))) {
+ return false;
+ }
+ }
+
+ if (ready && strcmp(availability, "ready") == 0)
+ return count == 0;
+ if (ready && strcmp(availability, "degraded") == 0)
+ return count > 0;
+ if (!ready && strcmp(availability, "starting") == 0)
+ return count == 1 &&
+ strcmp(json_object_get_string(
+ json_object_array_get_idx(array, 0)),
+ "starting") == 0;
+ if (!ready && strcmp(availability, "draining") == 0)
+ return count == 1 &&
+ strcmp(json_object_get_string(
+ json_object_array_get_idx(array, 0)),
+ "draining") == 0;
+ if (!ready && strcmp(availability, "failed") == 0)
+ return count > 0;
+ return false;
+}
+
+static bool
+RzbNext_ValidateCncDispatcherHello(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "dispatcher_id", "created_at",
+ "started_at", "ready", "availability", "dependency_reason_codes"
+ };
+ static const char * const availabilityValues[] = {
+ "starting", "ready", "degraded", "draining", "failed"
+ };
+ json_object *ready;
+ const char *availability;
+
+ if (!RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) ||
+ !RzbNext_RequireString(object, "dispatcher_id", RzbNext_IsUuid) ||
+ !RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp) ||
+ !RzbNext_RequireString(object, "started_at", RzbNext_IsTimestamp) ||
+ !RzbNext_RequireEnum(object, "availability", availabilityValues,
+ sizeof(availabilityValues) /
+ sizeof(availabilityValues[0]))) {
+ return false;
+ }
+ ready = RzbNext_GetTyped(object, "ready", json_type_boolean);
+ availability = RzbNext_GetString(object, "availability");
+ return ready != NULL &&
+ RzbNext_ValidateDispatcherHelloReasons(
+ object, json_object_get_boolean(ready), availability);
+}
+
+static bool
+RzbNext_ValidateBlockSubmission(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "event_id", "source_nugget_uuid",
+ "block", "stored", "event_metadata", "parent_event_id",
+ "parent_block", "created_at"
+ };
+ bool hasParentEvent;
+ bool hasParentBlock;
+
+ if (!RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) ||
+ !RzbNext_RequireString(object, "event_id", RzbNext_IsUuid) ||
+ !RzbNext_RequireString(object, "source_nugget_uuid",
+ RzbNext_IsUuid) ||
+ !RzbNext_ValidateBlock(RzbNext_GetTyped(object, "block",
+ json_type_object), false) ||
+ !RzbNext_RequireBool(object, "stored") ||
+ !RzbNext_OptionalMetadataArray(object, "event_metadata", 1, false) ||
+ !RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp)) {
+ return false;
+ }
+ hasParentEvent = RzbNext_ObjectHasField(object, "parent_event_id");
+ hasParentBlock = RzbNext_ObjectHasField(object, "parent_block");
+ if (hasParentEvent != hasParentBlock)
+ return false;
+ if (hasParentEvent &&
+ (!RzbNext_RequireString(object, "parent_event_id", RzbNext_IsUuid) ||
+ !RzbNext_ValidateBlock(RzbNext_GetTyped(object, "parent_block",
+ json_type_object), false))) {
+ return false;
+ }
+ return true;
+}
+
+static bool
+RzbNext_ValidateReinspectionRequest(json_object *object)
+{
+ static const char * const fields[] = { "reason_code" };
+ static const char * const reasons[] = {
+ "operator_requested", "dirty_block", "dependency_recovery",
+ "state_reconciliation"
+ };
+
+ return object != NULL &&
+ json_object_get_type(object) == json_type_object &&
+ RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireEnum(object, "reason_code", reasons,
+ sizeof(reasons) / sizeof(reasons[0]));
+}
+
+static bool
+RzbNext_OptionalReinspectionRequest(json_object *object)
+{
+ if (!RzbNext_ObjectHasField(object, "reinspection_request"))
+ return true;
+ return RzbNext_ValidateReinspectionRequest(
+ RzbNext_GetTyped(object, "reinspection_request", json_type_object));
+}
+
+static bool
+RzbNext_ValidateBlockUpdate(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "update_id", "source_nugget_uuid",
+ "block", "metadata_updates", "tag_mutations",
+ "reinspection_request", "created_at"
+ };
+ bool hasMutation;
+
+ if (!RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) ||
+ !RzbNext_RequireString(object, "update_id", RzbNext_IsUuid) ||
+ !RzbNext_RequireString(object, "source_nugget_uuid",
+ RzbNext_IsUuid) ||
+ !RzbNext_ValidateBlock(RzbNext_GetTyped(object, "block",
+ json_type_object), false) ||
+ !RzbNext_OptionalMetadataArray(object, "metadata_updates", 1, true) ||
+ !RzbNext_OptionalTagMutations(object, "tag_mutations") ||
+ !RzbNext_OptionalReinspectionRequest(object) ||
+ !RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp)) {
+ return false;
+ }
+ hasMutation = RzbNext_ObjectHasField(object, "metadata_updates") ||
+ RzbNext_ObjectHasField(object, "tag_mutations") ||
+ RzbNext_ObjectHasField(object, "reinspection_request");
+ return hasMutation;
+}
+
+static bool
+RzbNext_ValidateInspectionEvent(json_object *object)
+{
+ static const char * const fields[] = {
+ "event_id", "event_kind", "created_at", "source_nugget_uuid",
+ "stored", "parent_event_id", "parent_block", "update_id", "metadata"
+ };
+ const char *eventKind;
+ bool hasParentEvent;
+ bool hasParentBlock;
+
+ if (object == NULL || json_object_get_type(object) != json_type_object ||
+ !RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) ||
+ !RzbNext_RequireString(object, "event_id", RzbNext_IsUuid) ||
+ !RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp) ||
+ !RzbNext_RequireString(object, "source_nugget_uuid",
+ RzbNext_IsUuid) ||
+ !RzbNext_OptionalMetadataArray(object, "metadata", 0, false)) {
+ return false;
+ }
+
+ eventKind = RzbNext_GetString(object, "event_kind");
+ if (eventKind == NULL)
+ return false;
+ if (strcmp(eventKind, "source_submission") == 0) {
+ if (!RzbNext_RequireBool(object, "stored") ||
+ !RzbNext_ObjectHasField(object, "metadata") ||
+ RzbNext_ObjectHasField(object, "update_id")) {
+ return false;
+ }
+ hasParentEvent = RzbNext_ObjectHasField(object, "parent_event_id");
+ hasParentBlock = RzbNext_ObjectHasField(object, "parent_block");
+ if (hasParentEvent != hasParentBlock)
+ return false;
+ if (hasParentEvent &&
+ (!RzbNext_RequireString(object, "parent_event_id",
+ RzbNext_IsUuid) ||
+ !RzbNext_ValidateBlock(RzbNext_GetTyped(object, "parent_block",
+ json_type_object),
+ false))) {
+ return false;
+ }
+ return true;
+ }
+ if (strcmp(eventKind, "unsolicited_block_update") == 0) {
+ return RzbNext_RequireString(object, "update_id", RzbNext_IsUuid) &&
+ RzbNext_ObjectHasField(object, "metadata") &&
+ !RzbNext_ObjectHasField(object, "stored") &&
+ !RzbNext_ObjectHasField(object, "parent_event_id") &&
+ !RzbNext_ObjectHasField(object, "parent_block");
+ }
+ return false;
+}
+
+static bool
+RzbNext_ValidateInspectionWork(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "inspection_id", "block",
+ "app_type", "work_kind", "event", "created_at"
+ };
+ static const char * const kinds[] = { "inspect", "deferred_poll" };
+
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "inspection_id", RzbNext_IsUuid) &&
+ RzbNext_ValidateBlock(RzbNext_GetTyped(object, "block",
+ json_type_object), false) &&
+ RzbNext_RequireString(object, "app_type", RzbNext_IsSafeKey) &&
+ RzbNext_RequireEnum(object, "work_kind", kinds,
+ sizeof(kinds) / sizeof(kinds[0])) &&
+ RzbNext_ValidateInspectionEvent(RzbNext_GetTyped(object, "event",
+ json_type_object)) &&
+ RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp);
+}
+
+static bool
+RzbNext_ValidateAnalysisError(json_object *object)
+{
+ static const char * const fields[] = {
+ "category", "code", "message", "details"
+ };
+ static const char * const categories[] = {
+ "input_malformed", "input_unsupported", "dependency_unavailable",
+ "timeout", "resource_exhausted", "policy_rejected",
+ "inspector_failed", "internal_error"
+ };
+
+ return object != NULL &&
+ json_object_get_type(object) == json_type_object &&
+ RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireEnum(object, "category", categories,
+ sizeof(categories) / sizeof(categories[0])) &&
+ RzbNext_RequireString(object, "code", RzbNext_IsSafeKey) &&
+ RzbNext_ValidateTextMessage(object, "message", 4096, false) &&
+ (!RzbNext_ObjectHasField(object, "details") ||
+ RzbNext_GetTyped(object, "details", json_type_object) != NULL);
+}
+
+static bool
+RzbNext_ValidateAnalysisDeferred(json_object *object)
+{
+ static const char * const fields[] = {
+ "reason_code", "message", "poll_after", "details"
+ };
+
+ return object != NULL &&
+ json_object_get_type(object) == json_type_object &&
+ RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "reason_code", RzbNext_IsSafeKey) &&
+ RzbNext_ValidateTextMessage(object, "message", 4096, false) &&
+ RzbNext_OptionalTimestamp(object, "poll_after") &&
+ (!RzbNext_ObjectHasField(object, "details") ||
+ RzbNext_GetTyped(object, "details", json_type_object) != NULL);
+}
+
+static bool
+RzbNext_ValidateAlerts(json_object *array)
+{
+ static const char * const fields[] = {
+ "alert_id", "created_at", "priority", "title", "message", "metadata"
+ };
+ size_t count;
+ size_t index;
+
+ if (array == NULL || json_object_get_type(array) != json_type_array)
+ return false;
+ count = json_object_array_length(array);
+ for (index = 0; index < count; index++) {
+ json_object *alert = json_object_array_get_idx(array, index);
+
+ if (alert == NULL ||
+ json_object_get_type(alert) != json_type_object ||
+ !RzbNext_ObjectOnlyHasFields(alert, fields,
+ sizeof(fields) / sizeof(fields[0])) ||
+ !RzbNext_RequireString(alert, "alert_id", RzbNext_IsUuid) ||
+ !RzbNext_RequireString(alert, "created_at",
+ RzbNext_IsTimestamp) ||
+ !RzbNext_RequireIntRange(alert, "priority", 0, 255) ||
+ !RzbNext_ValidateTextMessage(alert, "title", 256, false) ||
+ !RzbNext_ValidateTextMessage(alert, "message", 4096, false) ||
+ !RzbNext_OptionalMetadataArray(alert, "metadata", 0, false) ||
+ !RzbNext_ObjectHasField(alert, "metadata")) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool
+RzbNext_ValidateAnalysisResult(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "inspection_id", "event_id",
+ "block", "inspector_id", "app_type", "result_status",
+ "block_metadata_updates", "metadata", "tag_mutations", "alerts",
+ "error", "deferred", "created_at"
+ };
+ const char *status;
+
+ if (!RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) ||
+ !RzbNext_RequireString(object, "inspection_id", RzbNext_IsUuid) ||
+ !RzbNext_RequireString(object, "event_id", RzbNext_IsUuid) ||
+ !RzbNext_ValidateBlock(RzbNext_GetTyped(object, "block",
+ json_type_object), false) ||
+ !RzbNext_RequireString(object, "inspector_id", RzbNext_IsUuid) ||
+ !RzbNext_RequireString(object, "app_type", RzbNext_IsSafeKey) ||
+ !RzbNext_OptionalMetadataArray(object, "block_metadata_updates", 1,
+ true) ||
+ !RzbNext_OptionalMetadataArray(object, "metadata", 0, false) ||
+ !RzbNext_OptionalTagMutations(object, "tag_mutations") ||
+ !RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp)) {
+ return false;
+ }
+ if (RzbNext_ObjectHasField(object, "alerts") &&
+ !RzbNext_ValidateAlerts(RzbNext_GetTyped(object, "alerts",
+ json_type_array))) {
+ return false;
+ }
+ status = RzbNext_GetString(object, "result_status");
+ if (status == NULL)
+ return false;
+ if (strcmp(status, "completed") == 0)
+ return !RzbNext_ObjectHasField(object, "error") &&
+ !RzbNext_ObjectHasField(object, "deferred");
+ if (strcmp(status, "error") == 0)
+ return RzbNext_ValidateAnalysisError(
+ RzbNext_GetTyped(object, "error", json_type_object)) &&
+ !RzbNext_ObjectHasField(object, "deferred") &&
+ !RzbNext_ObjectHasField(object, "block_metadata_updates") &&
+ !RzbNext_ObjectHasField(object, "metadata") &&
+ !RzbNext_ObjectHasField(object, "tag_mutations") &&
+ !RzbNext_ObjectHasField(object, "alerts");
+ if (strcmp(status, "deferred") == 0)
+ return RzbNext_ValidateAnalysisDeferred(
+ RzbNext_GetTyped(object, "deferred", json_type_object)) &&
+ !RzbNext_ObjectHasField(object, "error") &&
+ !RzbNext_ObjectHasField(object, "block_metadata_updates") &&
+ !RzbNext_ObjectHasField(object, "metadata") &&
+ !RzbNext_ObjectHasField(object, "tag_mutations") &&
+ !RzbNext_ObjectHasField(object, "alerts");
+ return false;
+}
+
+static bool
+RzbNextAnalysisResult_AddString(json_object *object, const char *field,
+ const char *value)
+{
+ json_object *stringValue;
+
+ if (object == NULL || field == NULL || value == NULL)
+ return false;
+ stringValue = json_object_new_string(value);
+ if (stringValue == NULL)
+ return false;
+ json_object_object_add(object, field, stringValue);
+ return true;
+}
+
+static bool
+RzbNextAnalysisResult_AddOptionalJson(json_object *object, const char *field,
+ const char *jsonText,
+ enum json_type expectedType)
+{
+ json_object *value;
+
+ if (jsonText == NULL)
+ return true;
+ value = json_tokener_parse(jsonText);
+ if (value == NULL || json_object_get_type(value) != expectedType) {
+ json_object_put(value);
+ return false;
+ }
+ json_object_object_add(object, field, value);
+ return true;
+}
+
+static json_object *
+RzbNextAnalysisResult_ParseInspectionWork(const char *inspectionWorkJson)
+{
+ json_object *work;
+ json_object *version;
+ const char *schemaName;
+
+ if (inspectionWorkJson == NULL)
+ return NULL;
+ work = json_tokener_parse(inspectionWorkJson);
+ if (work == NULL || json_object_get_type(work) != json_type_object)
+ goto error;
+ schemaName = RzbNext_GetString(work, "schema_name");
+ version = RzbNext_GetTyped(work, "schema_version", json_type_int);
+ if (schemaName == NULL ||
+ strcmp(schemaName, RZB_NEXT_SCHEMA_INSPECTION_WORK) != 0 ||
+ version == NULL ||
+ json_object_get_int64(version) != RZB_NEXT_SCHEMA_VERSION ||
+ !RzbNext_ValidateInspectionWork(work)) {
+ goto error;
+ }
+ return work;
+
+error:
+ if (work != NULL)
+ json_object_put(work);
+ return NULL;
+}
+
+static json_object *
+RzbNextAnalysisResult_Common(json_object *work, const char *inspectorUuid,
+ const char *status, const char *createdAt)
+{
+ json_object *result;
+ json_object *schemaVersion;
+ json_object *block;
+ json_object *event;
+ const char *inspectionId;
+ const char *eventId;
+ const char *appType;
+
+ if (work == NULL || !RzbNext_IsUuid(inspectorUuid) ||
+ !RzbNext_IsTimestamp(createdAt)) {
+ return NULL;
+ }
+
+ inspectionId = RzbNext_GetString(work, "inspection_id");
+ appType = RzbNext_GetString(work, "app_type");
+ block = RzbNext_GetTyped(work, "block", json_type_object);
+ event = RzbNext_GetTyped(work, "event", json_type_object);
+ eventId = RzbNext_GetString(event, "event_id");
+ if (inspectionId == NULL || appType == NULL || block == NULL ||
+ eventId == NULL) {
+ return NULL;
+ }
+
+ result = json_object_new_object();
+ schemaVersion = json_object_new_int((int)RZB_NEXT_SCHEMA_VERSION);
+ if (result == NULL || schemaVersion == NULL) {
+ if (result != NULL)
+ json_object_put(result);
+ if (schemaVersion != NULL)
+ json_object_put(schemaVersion);
+ return NULL;
+ }
+
+ json_object_object_add(result, "schema_version", schemaVersion);
+ json_object_object_add(result, "block", json_object_get(block));
+ if (!RzbNextAnalysisResult_AddString(result, "schema_name",
+ RZB_NEXT_SCHEMA_ANALYSIS_RESULT) ||
+ !RzbNextAnalysisResult_AddString(result, "inspection_id",
+ inspectionId) ||
+ !RzbNextAnalysisResult_AddString(result, "event_id", eventId) ||
+ !RzbNextAnalysisResult_AddString(result, "inspector_id",
+ inspectorUuid) ||
+ !RzbNextAnalysisResult_AddString(result, "app_type", appType) ||
+ !RzbNextAnalysisResult_AddString(result, "result_status", status) ||
+ !RzbNextAnalysisResult_AddString(result, "created_at", createdAt)) {
+ json_object_put(result);
+ return NULL;
+ }
+ return result;
+}
+
+static char *
+RzbNextAnalysisResult_MetadataLimitDetails(void)
+{
+ json_object *details;
+ char *jsonText;
+
+ details = json_object_new_object();
+ if (details == NULL)
+ return NULL;
+ json_object_object_add(details, "reason_code",
+ json_object_new_string("metadata_value_too_large"));
+ json_object_object_add(
+ details, "max_value_bytes",
+ json_object_new_int64((int64_t)RzbNextMetadata_MaxValueBytes()));
+ jsonText = RzbNext_JsonToOwnedString(details);
+ json_object_put(details);
+ return jsonText;
+}
+
+static char *
+RzbNextAnalysisResult_BuildMetadataLimitError(const char *inspectionWorkJson,
+ const char *inspectorUuid,
+ const char *createdAt)
+{
+ char *detailsJson;
+ char *jsonText;
+
+ detailsJson = RzbNextAnalysisResult_MetadataLimitDetails();
+ jsonText = RzbNextAnalysisResult_BuildError(
+ inspectionWorkJson, inspectorUuid, "inspector_failed",
+ "metadata_value_too_large",
+ "inspector produced metadata that exceeds the per-item metadata value limit",
+ detailsJson, createdAt);
+ free(detailsJson);
+ return jsonText;
+}
+
+char *
+RzbNextAnalysisResult_BuildCompleted(const char *inspectionWorkJson,
+ const char *inspectorUuid,
+ const char *createdAt,
+ const char *blockMetadataUpdatesJson,
+ const char *metadataJson,
+ const char *tagMutationsJson,
+ const char *alertsJson)
+{
+ json_object *work;
+ json_object *result;
+ char *jsonText = NULL;
+ bool metadataTooLarge = false;
+
+ work = RzbNextAnalysisResult_ParseInspectionWork(inspectionWorkJson);
+ result = RzbNextAnalysisResult_Common(work, inspectorUuid, "completed",
+ createdAt);
+ if (result == NULL)
+ goto cleanup;
+ if (!RzbNextAnalysisResult_AddOptionalJson(
+ result, "block_metadata_updates", blockMetadataUpdatesJson,
+ json_type_array) ||
+ !RzbNextAnalysisResult_AddOptionalJson(result, "metadata",
+ metadataJson,
+ json_type_array) ||
+ !RzbNextAnalysisResult_AddOptionalJson(result, "tag_mutations",
+ tagMutationsJson,
+ json_type_object) ||
+ !RzbNextAnalysisResult_AddOptionalJson(result, "alerts", alertsJson,
+ json_type_array)) {
+ goto cleanup;
+ }
+ metadataTooLarge = RzbNext_AnalysisResultHasOversizedMetadata(result);
+ if (!RzbNext_ValidateAnalysisResult(result)) {
+ if (metadataTooLarge) {
+ jsonText = RzbNextAnalysisResult_BuildMetadataLimitError(
+ inspectionWorkJson, inspectorUuid, createdAt);
+ }
+ goto cleanup;
+ }
+ jsonText = RzbNext_JsonToOwnedString(result);
+
+cleanup:
+ if (result != NULL)
+ json_object_put(result);
+ if (work != NULL)
+ json_object_put(work);
+ return jsonText;
+}
+
+char *
+RzbNextAnalysisResult_BuildError(const char *inspectionWorkJson,
+ const char *inspectorUuid,
+ const char *category, const char *code,
+ const char *message,
+ const char *detailsJson,
+ const char *createdAt)
+{
+ json_object *work;
+ json_object *result;
+ json_object *error;
+ char *jsonText = NULL;
+
+ work = RzbNextAnalysisResult_ParseInspectionWork(inspectionWorkJson);
+ result = RzbNextAnalysisResult_Common(work, inspectorUuid, "error",
+ createdAt);
+ error = json_object_new_object();
+ if (result == NULL || error == NULL)
+ goto cleanup;
+ if (!RzbNextAnalysisResult_AddString(error, "category", category) ||
+ !RzbNextAnalysisResult_AddString(error, "code", code) ||
+ !RzbNextAnalysisResult_AddString(error, "message", message) ||
+ !RzbNextAnalysisResult_AddOptionalJson(error, "details",
+ detailsJson,
+ json_type_object)) {
+ goto cleanup;
+ }
+ json_object_object_add(result, "error", error);
+ error = NULL;
+ if (!RzbNext_ValidateAnalysisResult(result))
+ goto cleanup;
+ jsonText = RzbNext_JsonToOwnedString(result);
+
+cleanup:
+ if (error != NULL)
+ json_object_put(error);
+ if (result != NULL)
+ json_object_put(result);
+ if (work != NULL)
+ json_object_put(work);
+ return jsonText;
+}
+
+char *
+RzbNextAnalysisResult_BuildDeferred(const char *inspectionWorkJson,
+ const char *inspectorUuid,
+ const char *reasonCode,
+ const char *message,
+ const char *pollAfter,
+ const char *detailsJson,
+ const char *createdAt)
+{
+ json_object *work;
+ json_object *result;
+ json_object *deferred;
+ char *jsonText = NULL;
+
+ work = RzbNextAnalysisResult_ParseInspectionWork(inspectionWorkJson);
+ result = RzbNextAnalysisResult_Common(work, inspectorUuid, "deferred",
+ createdAt);
+ deferred = json_object_new_object();
+ if (result == NULL || deferred == NULL)
+ goto cleanup;
+ if (!RzbNextAnalysisResult_AddString(deferred, "reason_code",
+ reasonCode) ||
+ !RzbNextAnalysisResult_AddString(deferred, "message", message) ||
+ !RzbNextAnalysisResult_AddOptionalJson(deferred, "details",
+ detailsJson,
+ json_type_object)) {
+ goto cleanup;
+ }
+ if (pollAfter != NULL &&
+ !RzbNextAnalysisResult_AddString(deferred, "poll_after",
+ pollAfter)) {
+ goto cleanup;
+ }
+ json_object_object_add(result, "deferred", deferred);
+ deferred = NULL;
+ if (!RzbNext_ValidateAnalysisResult(result))
+ goto cleanup;
+ jsonText = RzbNext_JsonToOwnedString(result);
+
+cleanup:
+ if (deferred != NULL)
+ json_object_put(deferred);
+ if (result != NULL)
+ json_object_put(result);
+ if (work != NULL)
+ json_object_put(work);
+ return jsonText;
+}
+
+static bool
+RzbNext_ValidateCacheRequest(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "request_id", "requestor_uuid",
+ "block", "created_at"
+ };
+
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "request_id", RzbNext_IsUuid) &&
+ RzbNext_RequireString(object, "requestor_uuid", RzbNext_IsUuid) &&
+ RzbNext_ValidateBlock(RzbNext_GetTyped(object, "block",
+ json_type_object), false) &&
+ RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp);
+}
+
+static bool
+RzbNext_ValidateCacheResponse(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "request_id", "block", "found",
+ "system_tags", "enterprise_tags", "created_at"
+ };
+
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "request_id", RzbNext_IsUuid) &&
+ RzbNext_ValidateBlock(RzbNext_GetTyped(object, "block",
+ json_type_object), false) &&
+ RzbNext_RequireBool(object, "found") &&
+ RzbNext_StringArrayFieldValid(object, "system_tags",
+ RzbNext_IsSystemTag, 0, true) &&
+ RzbNext_StringArrayFieldValid(object, "enterprise_tags",
+ RzbNext_IsSafeKey, 0, true) &&
+ RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp);
+}
+
+static bool
+RzbNext_ValidateCatalogInvalidation(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "mutation_id", "resource_type",
+ "resource_key", "mutation_action", "invalidation_scope", "created_at"
+ };
+ static const char * const resourceTypes[] = {
+ "nugget_type", "app_type", "data_type", "data_type_alias",
+ "metadata_name", "metadata_type", "locality", "nugget",
+ "system_tag", "enterprise_tag"
+ };
+ static const char * const actions[] = {
+ "created", "updated", "retired", "deleted"
+ };
+ static const char * const scopes[] = { "global", "locality", "nugget" };
+
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "mutation_id", RzbNext_IsUuid) &&
+ RzbNext_RequireEnum(object, "resource_type", resourceTypes,
+ sizeof(resourceTypes) /
+ sizeof(resourceTypes[0])) &&
+ RzbNext_ValidateTextMessage(object, "resource_key", 512, false) &&
+ RzbNext_RequireEnum(object, "mutation_action", actions,
+ sizeof(actions) / sizeof(actions[0])) &&
+ RzbNext_RequireEnum(object, "invalidation_scope", scopes,
+ sizeof(scopes) / sizeof(scopes[0])) &&
+ RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp);
+}
+
+static bool
+RzbNext_ValidateFileRemoveRequest(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "request_id", "block",
+ "source_workflow", "source_transition_id", "reason_code", "created_at"
+ };
+ static const char * const sourceWorkflows[] = {
+ "block_update", "state_reconciliation", "operator_replay",
+ "test_fixture"
+ };
+ static const char * const reasons[] = {
+ "administrative_removal", "retention_expired",
+ "state_reconciliation", "test_fixture"
+ };
+
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "request_id", RzbNext_IsUuid) &&
+ RzbNext_ValidateBlock(RzbNext_GetTyped(object, "block",
+ json_type_object), true) &&
+ RzbNext_RequireEnum(object, "source_workflow", sourceWorkflows,
+ sizeof(sourceWorkflows) /
+ sizeof(sourceWorkflows[0])) &&
+ RzbNext_RequireString(object, "source_transition_id",
+ RzbNext_IsPrintableId) &&
+ RzbNext_RequireEnum(object, "reason_code", reasons,
+ sizeof(reasons) / sizeof(reasons[0])) &&
+ RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp);
+}
+
+static bool
+RzbNext_ValidateFileRemoveError(json_object *object)
+{
+ static const char * const fields[] = {
+ "category", "code", "message", "details"
+ };
+ static const char * const categories[] = {
+ "invalid_request", "unsafe_target", "dependency_unavailable",
+ "timeout", "permission_denied", "internal_error"
+ };
+
+ return object != NULL &&
+ json_object_get_type(object) == json_type_object &&
+ RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireEnum(object, "category", categories,
+ sizeof(categories) / sizeof(categories[0])) &&
+ RzbNext_RequireString(object, "code", RzbNext_IsSafeKey) &&
+ RzbNext_ValidateTextMessage(object, "message", 4096, false) &&
+ (!RzbNext_ObjectHasField(object, "details") ||
+ RzbNext_GetTyped(object, "details", json_type_object) != NULL);
+}
+
+static bool
+RzbNext_ValidateFileRemoveResult(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "request_id", "service", "status",
+ "error", "created_at"
+ };
+ static const char * const services[] = { "file_store", "varnish" };
+ static const char * const statuses[] = { "success", "failed" };
+ const char *status;
+
+ if (!RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) ||
+ !RzbNext_RequireString(object, "request_id", RzbNext_IsUuid) ||
+ !RzbNext_RequireEnum(object, "service", services,
+ sizeof(services) / sizeof(services[0])) ||
+ !RzbNext_RequireEnum(object, "status", statuses,
+ sizeof(statuses) / sizeof(statuses[0])) ||
+ !RzbNext_RequireString(object, "created_at", RzbNext_IsTimestamp)) {
+ return false;
+ }
+ status = RzbNext_GetString(object, "status");
+ if (strcmp(status, "success") == 0)
+ return !RzbNext_ObjectHasField(object, "error");
+ return RzbNext_ValidateFileRemoveError(
+ RzbNext_GetTyped(object, "error", json_type_object));
+}
+
+static bool
+RzbNext_ValidateSearchExport(json_object *object)
+{
+ static const char * const fields[] = {
+ "schema_name", "schema_version", "export_event_id",
+ "aggregate_type", "aggregate_id", "event_type", "occurred_at",
+ "target_index", "document_id", "document"
+ };
+ static const char * const aggregateTypes[] = {
+ "block", "event", "analysis_result", "alert", "audit"
+ };
+ static const char * const eventTypes[] = {
+ "event.upserted", "block.seen", "block.updated",
+ "analysis_result.completed", "analysis_result.error",
+ "alert.created", "block.finalized", "audit.recorded"
+ };
+ static const char * const targetIndexes[] = {
+ "razorback-blocks", "razorback-events",
+ "razorback-analysis-results", "razorback-alerts",
+ "razorback-admin-audit"
+ };
+ json_object *document;
+
+ document = RzbNext_GetTyped(object, "document", json_type_object);
+ return RzbNext_ObjectOnlyHasFields(object, fields,
+ sizeof(fields) / sizeof(fields[0])) &&
+ RzbNext_RequireString(object, "export_event_id",
+ RzbNext_IsPrintableId) &&
+ RzbNext_RequireEnum(object, "aggregate_type", aggregateTypes,
+ sizeof(aggregateTypes) /
+ sizeof(aggregateTypes[0])) &&
+ RzbNext_RequireString(object, "aggregate_id",
+ RzbNext_IsPrintableId) &&
+ RzbNext_RequireEnum(object, "event_type", eventTypes,
+ sizeof(eventTypes) / sizeof(eventTypes[0])) &&
+ RzbNext_RequireString(object, "occurred_at", RzbNext_IsTimestamp) &&
+ RzbNext_RequireEnum(object, "target_index", targetIndexes,
+ sizeof(targetIndexes) /
+ sizeof(targetIndexes[0])) &&
+ RzbNext_RequireString(object, "document_id",
+ RzbNext_IsPrintableId) &&
+ document != NULL &&
+ RzbNext_ObjectFieldCount(document) > 0;
+}
+
+static bool
+RzbNext_ValidateObjectForSchema(json_object *object, const char *schemaName)
+{
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CLAIM_CHECK_REFERENCE) == 0)
+ return RzbNext_ValidateClaimCheck(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_REGISTRATION_REQUEST) == 0)
+ return RzbNext_ValidateCncRegistrationRequest(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_REGISTRATION_ACCEPTED) == 0)
+ return RzbNext_ValidateCncRegistrationAccepted(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_REGISTRATION_REJECTED) == 0)
+ return RzbNext_ValidateCncRegistrationRejected(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_LIVENESS) == 0)
+ return RzbNext_ValidateCncLiveness(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_BYE) == 0)
+ return RzbNext_ValidateCncBye(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_DIRECTED_COMMAND) == 0)
+ return RzbNext_ValidateCncDirectedCommand(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_DISPATCHER_HELLO) == 0)
+ return RzbNext_ValidateCncDispatcherHello(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_BLOCK_SUBMISSION) == 0)
+ return RzbNext_ValidateBlockSubmission(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_BLOCK_UPDATE) == 0)
+ return RzbNext_ValidateBlockUpdate(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_INSPECTION_WORK) == 0)
+ return RzbNext_ValidateInspectionWork(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_ANALYSIS_RESULT) == 0)
+ return RzbNext_ValidateAnalysisResult(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CACHE_REQUEST) == 0)
+ return RzbNext_ValidateCacheRequest(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CACHE_RESPONSE) == 0)
+ return RzbNext_ValidateCacheResponse(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CATALOG_INVALIDATION) == 0)
+ return RzbNext_ValidateCatalogInvalidation(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_FILE_REMOVE_REQUEST) == 0)
+ return RzbNext_ValidateFileRemoveRequest(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_FILE_REMOVE_RESULT) == 0)
+ return RzbNext_ValidateFileRemoveResult(object);
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_SEARCH_EXPORT) == 0)
+ return RzbNext_ValidateSearchExport(object);
+ return false;
+}
+
+static json_object *
+RzbNext_ParseJsonObject(const char *jsonMessage)
+{
+ json_tokener *tokener;
+ json_object *object;
+ enum json_tokener_error error;
+ int parseEnd;
+ size_t length;
+
+ if (jsonMessage == NULL)
+ return NULL;
+ length = strlen(jsonMessage);
+ tokener = json_tokener_new();
+ if (tokener == NULL)
+ return NULL;
+ object = json_tokener_parse_ex(tokener, jsonMessage, (int)length);
+ error = json_tokener_get_error(tokener);
+ parseEnd = json_tokener_get_parse_end(tokener);
+ json_tokener_free(tokener);
+ if (object == NULL || error != json_tokener_success) {
+ if (object != NULL)
+ json_object_put(object);
+ return NULL;
+ }
+ while (parseEnd < (int)length &&
+ isspace((unsigned char)jsonMessage[parseEnd])) {
+ parseEnd++;
+ }
+ if (parseEnd != (int)length ||
+ json_object_get_type(object) != json_type_object) {
+ json_object_put(object);
+ return NULL;
+ }
+ return object;
+}
+
+static const char *
+RzbNext_GetString(json_object *object, const char *field)
+{
+ json_object *value;
+
+ if (!json_object_object_get_ex(object, field, &value))
+ return NULL;
+ if (json_object_get_type(value) != json_type_string)
+ return NULL;
+ return json_object_get_string(value);
+}
+
+static bool
+RzbNext_GetVersion(json_object *object, const char *field, uint32_t *version)
+{
+ json_object *value;
+
+ if (!json_object_object_get_ex(object, field, &value))
+ return false;
+ if (json_object_get_type(value) != json_type_int)
+ return false;
+ if (json_object_get_int64(value) < 0 ||
+ json_object_get_int64(value) > UINT32_MAX) {
+ return false;
+ }
+ *version = (uint32_t)json_object_get_int64(value);
+ return true;
+}
+
+static bool
+RzbNext_GetUint64(json_object *object, const char *field, uint64_t *number)
+{
+ json_object *value;
+ int64_t parsed;
+
+ if (number == NULL)
+ return false;
+ if (!json_object_object_get_ex(object, field, &value))
+ return false;
+ if (json_object_get_type(value) != json_type_int)
+ return false;
+ parsed = json_object_get_int64(value);
+ if (parsed < 0)
+ return false;
+ *number = (uint64_t)parsed;
+ return true;
+}
+
+static bool
+RzbNext_GetBool(json_object *object, const char *field, bool *boolean)
+{
+ json_object *value;
+
+ if (boolean == NULL)
+ return false;
+ if (!json_object_object_get_ex(object, field, &value))
+ return false;
+ if (json_object_get_type(value) != json_type_boolean)
+ return false;
+ *boolean = json_object_get_boolean(value) ? true : false;
+ return true;
+}
+
+static bool
+RzbNext_GetIdentity(json_object *object, const char **schemaName,
+ uint32_t *schemaVersion)
+{
+ const char *referenceSchemaName;
+
+ referenceSchemaName = RzbNext_GetString(object, "reference_schema_name");
+ if (referenceSchemaName != NULL) {
+ *schemaName = referenceSchemaName;
+ return RzbNext_GetVersion(object, "reference_schema_version",
+ schemaVersion);
+ }
+ *schemaName = RzbNext_GetString(object, "schema_name");
+ if (*schemaName == NULL)
+ return false;
+ return RzbNext_GetVersion(object, "schema_version", schemaVersion);
+}
+
+static bool
+RzbNext_IsKnownSchemaVersion(const char *schemaName, uint32_t schemaVersion)
+{
+ size_t index;
+
+ if (schemaName == NULL)
+ return false;
+ for (index = 0; index < sizeof(KnownSchemas) / sizeof(KnownSchemas[0]);
+ index++) {
+ if (strcmp(schemaName, KnownSchemas[index].name) == 0)
+ return schemaVersion == KnownSchemas[index].version;
+ }
+ return false;
+}
+
+static struct RzbNextRoute *
+RzbNextRoute_Create(enum RzbNextTransport transport, const char *exchange,
+ char *routingKey)
+{
+ struct RzbNextRoute *route;
+
+ if (routingKey == NULL)
+ return NULL;
+ route = calloc(1, sizeof(*route));
+ if (route == NULL) {
+ free(routingKey);
+ return NULL;
+ }
+ route->transport = transport;
+ route->exchange = RzbNext_Strdup(exchange == NULL ? "" : exchange);
+ route->routingKey = routingKey;
+ if (route->exchange == NULL) {
+ RzbNextRoute_Destroy(route);
+ return NULL;
+ }
+ return route;
+}
+
+SO_PUBLIC const char *
+RzbNextTransport_ToString(enum RzbNextTransport transport)
+{
+ switch (transport) {
+ case RZB_NEXT_TRANSPORT_RABBITMQ:
+ return "rabbitmq";
+ case RZB_NEXT_TRANSPORT_KAFKA:
+ return "kafka";
+ default:
+ return "unknown";
+ }
+}
+
+SO_PUBLIC size_t
+RzbNextMetadata_MaxValueBytes(void)
+{
+ const char *configured;
+ char *end = NULL;
+ unsigned long long parsed;
+
+ configured = getenv(RZB_NEXT_METADATA_MAX_VALUE_BYTES_ENV);
+ if (configured == NULL || *configured == '\0')
+ return RZB_NEXT_METADATA_DEFAULT_MAX_VALUE_BYTES;
+
+ errno = 0;
+ parsed = strtoull(configured, &end, 10);
+ if (errno != 0 || end == configured || *end != '\0' ||
+ parsed == 0 || parsed > SIZE_MAX) {
+ return RZB_NEXT_METADATA_DEFAULT_MAX_VALUE_BYTES;
+ }
+ return (size_t)parsed;
+}
+
+SO_PUBLIC bool
+RzbNextMessage_IsKnownSchema(const char *schemaName)
+{
+ return RzbNext_IsKnownSchemaVersion(schemaName, RZB_NEXT_SCHEMA_VERSION);
+}
+
+SO_PUBLIC bool
+RzbNextMessage_ValidateIdentity(const char *jsonMessage)
+{
+ json_object *object;
+ const char *schemaName;
+ uint32_t schemaVersion;
+ bool valid;
+
+ object = RzbNext_ParseJsonObject(jsonMessage);
+ if (object == NULL)
+ return false;
+ valid = RzbNext_GetIdentity(object, &schemaName, &schemaVersion) &&
+ RzbNext_IsKnownSchemaVersion(schemaName, schemaVersion);
+ json_object_put(object);
+ return valid;
+}
+
+SO_PUBLIC bool
+RzbNextMessage_Validate(const char *jsonMessage)
+{
+ json_object *object;
+ const char *schemaName;
+ uint32_t schemaVersion;
+ bool valid;
+
+ object = RzbNext_ParseJsonObject(jsonMessage);
+ if (object == NULL)
+ return false;
+ valid = RzbNext_GetIdentity(object, &schemaName, &schemaVersion) &&
+ RzbNext_IsKnownSchemaVersion(schemaName, schemaVersion) &&
+ RzbNext_ValidateObjectForSchema(object, schemaName);
+ json_object_put(object);
+ return valid;
+}
+
+SO_PUBLIC enum RzbNextCacheSubmitDecision
+RzbNextCache_SubmitDecision(const char *jsonMessage)
+{
+ json_object *object;
+ const char *schemaName;
+ uint32_t schemaVersion;
+ json_object *foundObject;
+ json_object *systemTags;
+ bool found;
+ bool dirty;
+ bool notStored;
+
+ object = RzbNext_ParseJsonObject(jsonMessage);
+ if (object == NULL)
+ return RZB_NEXT_CACHE_INVALID_REQUEST;
+ if (!RzbNext_GetIdentity(object, &schemaName, &schemaVersion) ||
+ strcmp(schemaName, RZB_NEXT_SCHEMA_CACHE_RESPONSE) != 0 ||
+ !RzbNext_IsKnownSchemaVersion(schemaName, schemaVersion) ||
+ !RzbNext_ValidateObjectForSchema(object, schemaName)) {
+ json_object_put(object);
+ return RZB_NEXT_CACHE_INVALID_REQUEST;
+ }
+
+ foundObject = RzbNext_GetTyped(object, "found", json_type_boolean);
+ found = json_object_get_boolean(foundObject) ? true : false;
+ if (!found) {
+ json_object_put(object);
+ return RZB_NEXT_CACHE_SUBMIT_NEW;
+ }
+
+ systemTags = RzbNext_GetTyped(object, "system_tags", json_type_array);
+ dirty = RzbNext_StringArrayContains(systemTags, "DIRTY");
+ notStored = RzbNext_StringArrayContains(systemTags, "NOT_STORED");
+ json_object_put(object);
+
+ if (dirty && notStored)
+ return RZB_NEXT_CACHE_RESTORE_AND_SUBMIT_FOR_REINSPECTION;
+ if (dirty)
+ return RZB_NEXT_CACHE_SUBMIT_FOR_REINSPECTION;
+ return RZB_NEXT_CACHE_SKIP_KNOWN;
+}
+
+SO_PUBLIC bool
+RzbNextMessage_Route(const char *jsonMessage, const char *cacheRequestorUuid,
+ struct RzbNextRoute **route)
+{
+ json_object *object;
+ const char *schemaName;
+ uint32_t schemaVersion;
+ char *routingKey = NULL;
+ enum RzbNextTransport transport = RZB_NEXT_TRANSPORT_RABBITMQ;
+ const char *exchange = "";
+ const char *field;
+
+ if (route == NULL)
+ return false;
+ *route = NULL;
+ object = RzbNext_ParseJsonObject(jsonMessage);
+ if (object == NULL)
+ return false;
+ if (!RzbNext_GetIdentity(object, &schemaName, &schemaVersion) ||
+ !RzbNext_IsKnownSchemaVersion(schemaName, schemaVersion) ||
+ !RzbNext_ValidateObjectForSchema(object, schemaName)) {
+ json_object_put(object);
+ return false;
+ }
+
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_REGISTRATION_REQUEST) == 0 ||
+ strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_LIVENESS) == 0 ||
+ strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_BYE) == 0) {
+ routingKey = RzbNext_Strdup(RZB_NEXT_QUEUE_COMMAND);
+ } else if (strcmp(schemaName,
+ RZB_NEXT_SCHEMA_CNC_REGISTRATION_ACCEPTED) == 0 ||
+ strcmp(schemaName,
+ RZB_NEXT_SCHEMA_CNC_REGISTRATION_REJECTED) == 0) {
+ field = RzbNext_GetString(object, "nugget_uuid");
+ if (RzbNext_IsUuid(field))
+ routingKey = RzbNext_Format2(RZB_NEXT_QUEUE_DIRECTED_COMMAND_PREFIX,
+ field);
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_DIRECTED_COMMAND) == 0) {
+ field = RzbNext_GetString(object, "target_nugget_uuid");
+ if (RzbNext_IsUuid(field))
+ routingKey = RzbNext_Format2(RZB_NEXT_QUEUE_DIRECTED_COMMAND_PREFIX,
+ field);
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_DISPATCHER_HELLO) == 0) {
+ exchange = RZB_NEXT_EXCHANGE_DISPATCHER_HELLO;
+ routingKey = RzbNext_Strdup("");
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_BLOCK_SUBMISSION) == 0) {
+ routingKey = RzbNext_Strdup(RZB_NEXT_QUEUE_INPUT);
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_BLOCK_UPDATE) == 0) {
+ routingKey = RzbNext_Strdup(RZB_NEXT_QUEUE_BLOCK_UPDATE);
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_INSPECTION_WORK) == 0) {
+ field = RzbNext_GetString(object, "app_type");
+ if (RzbNext_IsSafeKey(field))
+ routingKey = RzbNext_Format2(RZB_NEXT_QUEUE_INSPECTOR_PREFIX, field);
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_ANALYSIS_RESULT) == 0) {
+ routingKey = RzbNext_Strdup(RZB_NEXT_QUEUE_ANALYSIS_RESULT);
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_CACHE_REQUEST) == 0) {
+ routingKey = RzbNext_Strdup(RZB_NEXT_QUEUE_CACHE_REQUEST);
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_CACHE_RESPONSE) == 0) {
+ if (RzbNext_IsUuid(cacheRequestorUuid))
+ routingKey = RzbNext_Format2(RZB_NEXT_QUEUE_CACHE_RESPONSE_PREFIX,
+ cacheRequestorUuid);
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_CATALOG_INVALIDATION) == 0) {
+ const char *resourceType = RzbNext_GetString(object, "resource_type");
+ const char *mutationAction = RzbNext_GetString(object, "mutation_action");
+
+ if (RzbNext_IsSafeKey(resourceType) &&
+ RzbNext_IsSafeKey(mutationAction)) {
+ exchange = RZB_NEXT_EXCHANGE_CATALOG_INVALIDATION;
+ routingKey = RzbNext_Format3("catalog", resourceType,
+ mutationAction);
+ }
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_FILE_REMOVE_REQUEST) == 0) {
+ exchange = RZB_NEXT_EXCHANGE_FILE_REMOVE;
+ routingKey = RzbNext_Strdup(RZB_NEXT_QUEUE_FILE_REMOVE_FILE_STORE);
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_FILE_REMOVE_RESULT) == 0) {
+ routingKey = RzbNext_Strdup(RZB_NEXT_QUEUE_FILE_REMOVE_RESULT);
+ } else if (strcmp(schemaName, RZB_NEXT_SCHEMA_SEARCH_EXPORT) == 0) {
+ field = RzbNext_GetString(object, "aggregate_id");
+ if (field != NULL && field[0] != '\0') {
+ transport = RZB_NEXT_TRANSPORT_KAFKA;
+ routingKey = RzbNext_Strdup(field);
+ }
+ }
+
+ json_object_put(object);
+ if (routingKey == NULL)
+ return false;
+ *route = RzbNextRoute_Create(transport, exchange, routingKey);
+ return *route != NULL;
+}
+
+SO_PUBLIC bool
+RzbNextRabbitMq_PrepareMessage(
+ const char *jsonMessage,
+ const char *cacheRequestorUuid,
+ const struct MessageBodyPolicy *policy,
+ const struct ClaimCheckReference *claimCheckTemplate,
+ struct RzbNextPreparedRabbitMqMessage **prepared)
+{
+ json_object *object;
+ const char *schemaName;
+ uint32_t schemaVersion;
+ char schemaVersionText[16];
+ struct RzbNextRoute *route = NULL;
+ struct EncodedMessageBody *encoded = NULL;
+ struct RzbNextPreparedRabbitMqMessage *result = NULL;
+ bool ok = false;
+
+ if (jsonMessage == NULL || prepared == NULL)
+ return false;
+ *prepared = NULL;
+ if (!RzbNextMessage_Route(jsonMessage, cacheRequestorUuid, &route))
+ return false;
+ if (route->transport != RZB_NEXT_TRANSPORT_RABBITMQ)
+ goto cleanup;
+
+ object = RzbNext_ParseJsonObject(jsonMessage);
+ if (object == NULL)
+ goto cleanup;
+ ok = RzbNext_GetIdentity(object, &schemaName, &schemaVersion) &&
+ RzbNext_IsKnownSchemaVersion(schemaName, schemaVersion) &&
+ RzbNext_ValidateObjectForSchema(object, schemaName);
+ json_object_put(object);
+ if (!ok)
+ goto cleanup;
+ ok = false;
+
+ if (!MessageBody_Encode(policy, (const uint8_t *)jsonMessage,
+ strlen(jsonMessage), claimCheckTemplate, &encoded)) {
+ goto cleanup;
+ }
+
+ result = calloc(1, sizeof(*result));
+ if (result == NULL)
+ goto cleanup;
+ result->route = route;
+ route = NULL;
+ result->body = RzbNext_CopyBytes(encoded->transportBody,
+ encoded->transportBodySize);
+ result->bodySize = encoded->transportBodySize;
+ result->contentType = RzbNext_Strdup("application/json");
+ result->contentEncoding = RzbNext_Strdup(encoded->contentEncoding);
+ if (encoded->claimCheckBody != NULL) {
+ result->claimCheckBody = RzbNext_CopyBytes(encoded->claimCheckBody,
+ encoded->claimCheckBodySize);
+ result->claimCheckBodySize = encoded->claimCheckBodySize;
+ }
+ if (encoded->claimCheckReference != NULL) {
+ result->claimCheckReference =
+ ClaimCheckReference_Clone(encoded->claimCheckReference);
+ }
+ snprintf(schemaVersionText, sizeof(schemaVersionText), "%u", schemaVersion);
+ if (result->body == NULL || result->contentType == NULL ||
+ (encoded->contentEncoding != NULL && result->contentEncoding == NULL) ||
+ (encoded->claimCheckBody != NULL && result->claimCheckBody == NULL) ||
+ (encoded->claimCheckReference != NULL &&
+ result->claimCheckReference == NULL) ||
+ !RzbNextPrepared_AddHeader(result, RZB_NEXT_HEADER_SCHEMA_NAME,
+ schemaName) ||
+ !RzbNextPrepared_AddHeader(result, RZB_NEXT_HEADER_SCHEMA_VERSION,
+ schemaVersionText) ||
+ !RzbNextPrepared_AddHeader(result, RZB_NEXT_HEADER_BODY_MODE,
+ MessageBodyMode_ToString(encoded->mode))) {
+ goto cleanup;
+ }
+ if (encoded->contentEncoding != NULL &&
+ !RzbNextPrepared_AddHeader(result, RZB_NEXT_HEADER_CONTENT_ENCODING,
+ encoded->contentEncoding)) {
+ goto cleanup;
+ }
+ *prepared = result;
+ result = NULL;
+ ok = true;
+
+cleanup:
+ RzbNextRoute_Destroy(route);
+ EncodedMessageBody_Destroy(encoded);
+ RzbNextPreparedRabbitMqMessage_Destroy(result);
+ return ok;
+}
+
+SO_PUBLIC bool
+RzbNextRabbitMq_DecodeMessageWithPolicy(
+ const struct MessageBodyPolicy *policy,
+ const uint8_t *body,
+ size_t bodySize,
+ const struct RzbNextMessageHeader *headers,
+ size_t headerCount,
+ const uint8_t *claimCheckBody,
+ size_t claimCheckBodySize,
+ struct RzbNextDecodedRabbitMqMessage **decoded)
+{
+ const char *schemaName;
+ const char *schemaVersion;
+ const char *bodyMode;
+ const char *contentEncoding;
+ uint8_t *decodedBytes = NULL;
+ size_t decodedSize = 0;
+ struct ClaimCheckReference *reference = NULL;
+ bool ok = false;
+
+ if (decoded == NULL || (body == NULL && bodySize > 0))
+ return false;
+ *decoded = NULL;
+ schemaName = RzbNextHeader_Value(headers, headerCount,
+ RZB_NEXT_HEADER_SCHEMA_NAME);
+ schemaVersion = RzbNextHeader_Value(headers, headerCount,
+ RZB_NEXT_HEADER_SCHEMA_VERSION);
+ bodyMode = RzbNextHeader_Value(headers, headerCount,
+ RZB_NEXT_HEADER_BODY_MODE);
+ contentEncoding = RzbNextHeader_Value(headers, headerCount,
+ RZB_NEXT_HEADER_CONTENT_ENCODING);
+ if (schemaName == NULL || schemaVersion == NULL || bodyMode == NULL)
+ return false;
+
+ if (strcmp(bodyMode, "inline") == 0) {
+ if (contentEncoding != NULL && contentEncoding[0] != '\0')
+ return false;
+ ok = MessageBody_DecodeInlineWithPolicy(policy, body, bodySize, NULL,
+ &decodedBytes, &decodedSize);
+ } else if (strcmp(bodyMode, "zlib") == 0) {
+ if (contentEncoding == NULL ||
+ strcmp(contentEncoding, MESSAGE_BODY_CONTENT_ENCODING_ZLIB) != 0) {
+ return false;
+ }
+ ok = MessageBody_DecodeInlineWithPolicy(policy, body, bodySize,
+ contentEncoding, &decodedBytes,
+ &decodedSize);
+ } else if (strcmp(bodyMode, "claim_check") == 0) {
+ char *referenceJson;
+
+ if (contentEncoding != NULL && contentEncoding[0] != '\0')
+ return false;
+ referenceJson = calloc(bodySize + 1U, sizeof(char));
+ if (referenceJson == NULL)
+ return false;
+ if (bodySize > 0)
+ memcpy(referenceJson, body, bodySize);
+ reference = ClaimCheckReference_FromJson(referenceJson);
+ free(referenceJson);
+ if (reference == NULL || claimCheckBody == NULL)
+ goto cleanup;
+ ok = MessageBody_DecodeClaimCheckWithPolicy(policy, claimCheckBody,
+ claimCheckBodySize,
+ reference, &decodedBytes,
+ &decodedSize);
+ } else {
+ return false;
+ }
+
+ if (!ok)
+ goto cleanup;
+ ok = RzbNextDecoded_FromBytes(decodedBytes, decodedSize, schemaName,
+ schemaVersion, reference, decoded);
+ if (ok)
+ reference = NULL;
+
+cleanup:
+ free(decodedBytes);
+ ClaimCheckReference_Destroy(reference);
+ return ok;
+}
+
+SO_PUBLIC bool
+RzbNextRabbitMq_DecodeMessage(
+ const uint8_t *body,
+ size_t bodySize,
+ const struct RzbNextMessageHeader *headers,
+ size_t headerCount,
+ const uint8_t *claimCheckBody,
+ size_t claimCheckBodySize,
+ struct RzbNextDecodedRabbitMqMessage **decoded)
+{
+ return RzbNextRabbitMq_DecodeMessageWithPolicy(NULL, body, bodySize,
+ headers, headerCount,
+ claimCheckBody,
+ claimCheckBodySize,
+ decoded);
+}
+
+SO_PUBLIC bool
+RzbNextCnc_IsReadyDispatcherHello(const char *jsonMessage)
+{
+ json_object *object;
+ json_object *reasons;
+ const char *schemaName;
+ const char *availability;
+ uint32_t schemaVersion;
+ bool ready;
+ bool valid;
+
+ object = RzbNext_ParseJsonObject(jsonMessage);
+ if (object == NULL)
+ return false;
+ valid = RzbNext_GetIdentity(object, &schemaName, &schemaVersion) &&
+ strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_DISPATCHER_HELLO) == 0 &&
+ RzbNext_IsKnownSchemaVersion(schemaName, schemaVersion) &&
+ RzbNext_ValidateObjectForSchema(object, schemaName) &&
+ RzbNext_GetBool(object, "ready", &ready) &&
+ ready &&
+ (availability = RzbNext_GetString(object, "availability")) != NULL &&
+ strcmp(availability, "ready") == 0 &&
+ json_object_object_get_ex(object, "dependency_reason_codes",
+ &reasons) &&
+ json_object_get_type(reasons) == json_type_array &&
+ json_object_array_length(reasons) == 0;
+ json_object_put(object);
+ return valid;
+}
+
+SO_PUBLIC bool
+RzbNextCnc_RegistrationAcceptedTiming(
+ const char *jsonMessage,
+ uint64_t *livenessInterval,
+ uint64_t *livenessFreshnessWindow,
+ uint64_t *livenessClockSkewTolerance)
+{
+ json_object *object;
+ const char *schemaName;
+ uint32_t schemaVersion;
+ uint64_t interval;
+ uint64_t freshness;
+ uint64_t skew;
+ bool valid;
+
+ if (livenessInterval == NULL || livenessFreshnessWindow == NULL ||
+ livenessClockSkewTolerance == NULL) {
+ return false;
+ }
+ object = RzbNext_ParseJsonObject(jsonMessage);
+ if (object == NULL)
+ return false;
+ valid = RzbNext_GetIdentity(object, &schemaName, &schemaVersion) &&
+ strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_REGISTRATION_ACCEPTED) == 0 &&
+ RzbNext_IsKnownSchemaVersion(schemaName, schemaVersion) &&
+ RzbNext_ValidateObjectForSchema(object, schemaName) &&
+ RzbNext_GetUint64(object, "liveness_interval", &interval) &&
+ RzbNext_GetUint64(object, "liveness_freshness_window", &freshness) &&
+ RzbNext_GetUint64(object, "liveness_clock_skew_tolerance", &skew);
+ json_object_put(object);
+ if (!valid)
+ return false;
+ *livenessInterval = interval;
+ *livenessFreshnessWindow = freshness;
+ *livenessClockSkewTolerance = skew;
+ return true;
+}
+
+SO_PUBLIC char *
+RzbNextCnc_DirectedCommandQueue(const char *nuggetUuid)
+{
+ if (!RzbNext_IsUuid(nuggetUuid))
+ return NULL;
+ return RzbNext_Format2(RZB_NEXT_QUEUE_DIRECTED_COMMAND_PREFIX, nuggetUuid);
+}
+
+SO_PUBLIC void
+RzbNext_FreeString(char *value)
+{
+ free(value);
+}
+
+static bool
+RzbNextDecoded_FromBytes(
+ const uint8_t *bytes,
+ size_t size,
+ const char *schemaName,
+ const char *schemaVersion,
+ struct ClaimCheckReference *claimCheckReference,
+ struct RzbNextDecodedRabbitMqMessage **decoded)
+{
+ char *jsonMessage;
+ json_object *object;
+ const char *decodedSchemaName;
+ uint32_t decodedSchemaVersion;
+ char decodedSchemaVersionText[16];
+ struct RzbNextDecodedRabbitMqMessage *result;
+ bool valid;
+
+ if (decoded == NULL || schemaName == NULL || schemaVersion == NULL ||
+ (bytes == NULL && size > 0)) {
+ return false;
+ }
+ *decoded = NULL;
+ jsonMessage = calloc(size + 1U, sizeof(char));
+ if (jsonMessage == NULL)
+ return false;
+ if (size > 0)
+ memcpy(jsonMessage, bytes, size);
+
+ object = RzbNext_ParseJsonObject(jsonMessage);
+ if (object == NULL) {
+ free(jsonMessage);
+ return false;
+ }
+ valid = RzbNext_GetIdentity(object, &decodedSchemaName,
+ &decodedSchemaVersion) &&
+ RzbNext_IsKnownSchemaVersion(decodedSchemaName,
+ decodedSchemaVersion) &&
+ RzbNext_ValidateObjectForSchema(object, decodedSchemaName);
+ if (!valid) {
+ json_object_put(object);
+ free(jsonMessage);
+ return false;
+ }
+ snprintf(decodedSchemaVersionText, sizeof(decodedSchemaVersionText), "%u",
+ decodedSchemaVersion);
+ if (strcmp(schemaName, decodedSchemaName) != 0 ||
+ strcmp(schemaVersion, decodedSchemaVersionText) != 0) {
+ json_object_put(object);
+ free(jsonMessage);
+ return false;
+ }
+ json_object_put(object);
+
+ result = calloc(1, sizeof(*result));
+ if (result == NULL) {
+ free(jsonMessage);
+ return false;
+ }
+ result->jsonMessage = jsonMessage;
+ result->claimCheckReference = claimCheckReference;
+ *decoded = result;
+ return true;
+}
+
+SO_PUBLIC void
+RzbNextRoute_Destroy(struct RzbNextRoute *route)
+{
+ if (route == NULL)
+ return;
+ free(route->exchange);
+ free(route->routingKey);
+ free(route);
+}
+
+SO_PUBLIC void
+RzbNextPreparedRabbitMqMessage_Destroy(
+ struct RzbNextPreparedRabbitMqMessage *prepared)
+{
+ size_t index;
+
+ if (prepared == NULL)
+ return;
+ RzbNextRoute_Destroy(prepared->route);
+ for (index = 0; index < prepared->headerCount; index++) {
+ free(prepared->headers[index].name);
+ free(prepared->headers[index].value);
+ }
+ free(prepared->headers);
+ free(prepared->body);
+ free(prepared->contentType);
+ free(prepared->contentEncoding);
+ free(prepared->claimCheckBody);
+ ClaimCheckReference_Destroy(prepared->claimCheckReference);
+ free(prepared);
+}
+
+SO_PUBLIC void
+RzbNextDecodedRabbitMqMessage_Destroy(
+ struct RzbNextDecodedRabbitMqMessage *decoded)
+{
+ if (decoded == NULL)
+ return;
+ free(decoded->jsonMessage);
+ ClaimCheckReference_Destroy(decoded->claimCheckReference);
+ free(decoded);
+}
diff --git a/src/nugget_tool.c b/src/nugget_tool.c
new file mode 100644
index 0000000..9c4cf78
--- /dev/null
+++ b/src/nugget_tool.c
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include "nugget_tool.h"
+
+#include
+
+#include
+#include
+
+#ifdef _MSC_VER
+#include
+#else
+#include
+#endif
+
+bool
+NuggetTool_LoadModule(const char *path, struct NuggetToolModule *module)
+{
+ if (path == NULL || module == NULL)
+ return false;
+
+ memset(module, 0, sizeof(*module));
+ module->path = strdup(path);
+ if (module->path == NULL)
+ return false;
+
+#ifdef _MSC_VER
+ module->handle = LoadLibraryA(path);
+ if (module->handle == NULL) {
+ rzb_log(LOG_ERR, LOG_C_NUGGET,
+ "%s: Failed to load nugget module '%s' (%lu)",
+ __func__, path, GetLastError());
+ goto error;
+ }
+
+ *(void **)&module->initNug = GetProcAddress(module->handle, "initNug");
+ *(void **)&module->shutdownNug = GetProcAddress(module->handle, "shutdownNug");
+#else
+ module->handle = dlopen(path, RTLD_LOCAL | RTLD_NOW);
+ if (module->handle == NULL) {
+ rzb_log(LOG_ERR, LOG_C_NUGGET,
+ "%s: Failed to load nugget module '%s': %s",
+ __func__, path, dlerror());
+ goto error;
+ }
+
+ *(void **)&module->initNug = dlsym(module->handle, "initNug");
+ *(void **)&module->shutdownNug = dlsym(module->handle, "shutdownNug");
+#endif
+
+ if (module->initNug == NULL || module->shutdownNug == NULL) {
+ rzb_log(LOG_ERR, LOG_C_NUGGET,
+ "%s: Nugget module '%s' is missing initNug/shutdownNug",
+ __func__, path);
+ goto error;
+ }
+
+ return true;
+
+error:
+ NuggetTool_UnloadModule(module);
+ return false;
+}
+
+void
+NuggetTool_UnloadModule(struct NuggetToolModule *module)
+{
+ if (module == NULL)
+ return;
+
+#ifdef _MSC_VER
+ if (module->handle != NULL)
+ FreeLibrary(module->handle);
+#else
+ if (module->handle != NULL)
+ dlclose(module->handle);
+#endif
+
+ free(module->path);
+ memset(module, 0, sizeof(*module));
+}
diff --git a/src/nugget_tool.h b/src/nugget_tool.h
new file mode 100644
index 0000000..227e7fc
--- /dev/null
+++ b/src/nugget_tool.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#ifndef RAZORBACK_NUGGET_TOOL_H
+#define RAZORBACK_NUGGET_TOOL_H
+
+#include
+
+#ifdef _MSC_VER
+#include
+#endif
+
+struct NuggetToolModule
+{
+ char *path;
+#ifdef _MSC_VER
+ HMODULE handle;
+#else
+ void *handle;
+#endif
+ bool (*initNug)(void);
+ void (*shutdownNug)(void);
+};
+
+bool NuggetTool_LoadModule(const char *path, struct NuggetToolModule *module);
+void NuggetTool_UnloadModule(struct NuggetToolModule *module);
+
+#endif
diff --git a/src/queue.c b/src/queue.c
index da3b57c..f5cf8e4 100644
--- a/src/queue.c
+++ b/src/queue.c
@@ -601,7 +601,11 @@ Queue_BeginReading (struct Queue *p_pQ)
{
amqp_bytes_t decQueuename;
const char *exchange;
+ int durable;
+ int exclusive;
int autoDelete;
+ amqp_table_entry_t quorumEntry;
+ amqp_table_t queueArgs;
ASSERT (p_pQ != NULL);
if (p_pQ == NULL) {
@@ -610,6 +614,8 @@ Queue_BeginReading (struct Queue *p_pQ)
}
exchange = p_pQ->bTopic ? "amq.topic" : "amq.direct";
+ durable = p_pQ->bTopic ? 0 : 1;
+ exclusive = p_pQ->bTopic ? 1 : 0;
autoDelete = p_pQ->bTopic ? 1 : 0;
if (p_pQ->bTopic) {
@@ -617,15 +623,25 @@ Queue_BeginReading (struct Queue *p_pQ)
} else {
decQueuename = amqp_cstring_bytes(p_pQ->sName);
}
+
+ queueArgs = amqp_empty_table;
+ if (!p_pQ->bTopic) {
+ quorumEntry.key = amqp_cstring_bytes("x-queue-type");
+ quorumEntry.value.kind = AMQP_FIELD_KIND_UTF8;
+ quorumEntry.value.value.bytes = amqp_cstring_bytes("quorum");
+ queueArgs.num_entries = 1;
+ queueArgs.entries = &quorumEntry;
+ }
+
amqp_queue_declare_ok_t *r = amqp_queue_declare(
p_pQ->pReadSocket->pConn,
AMQP_CHAN_ID,
decQueuename,
0,
- 1,
- 0,
+ durable,
+ exclusive,
autoDelete,
- amqp_empty_table
+ queueArgs
);
if (AMQP_error(amqp_get_rpc_reply(p_pQ->pReadSocket->pConn), __func__)) {
rzb_log(LOG_ERR, LOG_C_QUEUE, "%s: Failed to declare queue", __func__);
diff --git a/src/runtime_config.c b/src/runtime_config.c
index e526632..8c05d31 100644
--- a/src/runtime_config.c
+++ b/src/runtime_config.c
@@ -40,9 +40,6 @@ static conf_int_t sg_iMaxBlockSize;
static conf_int_t sg_iHelloTime;
static conf_int_t sg_iDeadTime;
-static char *sg_sTransferPassword;
-static char *sg_sTransferMode;
-
// Message Queue Stuff
static char *sg_sMqHost;
static conf_int_t sg_iMqPort;
@@ -294,18 +291,6 @@ Config_getLogFile(void)
return log_file;
}
-char *
-Config_getTransferMode(void)
-{
- return sg_sTransferMode;
-}
-
-SO_PUBLIC char *
-Razorback_Get_Transfer_Password()
-{
- return sg_sTransferPassword;
-}
-
static RZBConfCallBack hashCallback = {
&parseHashType
};
@@ -351,9 +336,6 @@ static RZBConfKey_t global_config[] = {
&sg_iHashType, &hashCallback},
{"Global.HelloTime", RZB_CONF_KEY_TYPE_INT, &sg_iHelloTime, NULL},
{"Global.DeadTime", RZB_CONF_KEY_TYPE_INT, &sg_iDeadTime, NULL},
- {"Global.TransferPassword", RZB_CONF_KEY_TYPE_STRING, &sg_sTransferPassword, NULL},
- {"Global.TransferMode", RZB_CONF_KEY_TYPE_STRING, &sg_sTransferMode, NULL},
-
// Local Cache Items
{"Cache.GoodLimit", RZB_CONF_KEY_TYPE_INT, &sg_iCacheGoodLimit, NULL},
{"Cache.BadLimit", RZB_CONF_KEY_TYPE_INT, &sg_iCacheBadLimit, NULL},
diff --git a/src/runtime_config.h b/src/runtime_config.h
index a955660..d4a7bf9 100644
--- a/src/runtime_config.h
+++ b/src/runtime_config.h
@@ -63,10 +63,6 @@ extern uint32_t Config_getSubTransferThreadsMax (void);
extern uint32_t Config_getBlockPoolMaxItems (void);
extern uint32_t Config_getBlockPoolMaxSize (void);
extern uint32_t Config_getBlockPoolMaxItemSize (void);
-extern char * Config_getTransferMode(void);
-
-
-
void Config_setLogDest (RZB_LOG_DEST_t);
void Config_setLogLevel(int);
diff --git a/src/runtime_next.c b/src/runtime_next.c
new file mode 100644
index 0000000..ad06f1e
--- /dev/null
+++ b/src/runtime_next.c
@@ -0,0 +1,1219 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#define RZB_NEXT_RUNTIME_SERVICE_NAME "razorback-c-sdk-component"
+#define RZB_NEXT_RUNTIME_WORKFLOW_NAME "component_runtime"
+#define RZB_NEXT_RUNTIME_DEPENDENCY_NAME "component_dependencies"
+
+struct RzbNextRuntime
+{
+ char *nuggetUuid;
+ char *processUuid;
+ double startedAt;
+ bool startupMetricRecorded;
+ char *inFlightRequestId;
+ char *acceptedRequestId;
+ char *registrationGeneration;
+ char *runtimePolicy;
+ uint64_t livenessInterval;
+ uint64_t livenessFreshnessWindow;
+ uint64_t livenessClockSkewTolerance;
+ uint32_t inFlightWork;
+ struct RzbNextRuntimeCallbacks callbacks;
+ uint32_t callbackFailureCount;
+ char lastCallbackFailure[64];
+ bool dependencyPaused;
+ enum RzbNextRuntimeState state;
+};
+
+static bool sg_runtimeActive = false;
+
+static bool RzbNextRuntime_IsUuid(const char *value);
+static char *RzbNextRuntime_Strdup(const char *value);
+static void RzbNextRuntime_Free(char **value);
+static void RzbNextRuntime_CopyFixed(char *dest, size_t destSize,
+ const char *source);
+static json_object *RzbNextRuntime_ParseValidated(const char *jsonMessage,
+ const char *schemaName);
+static const char *RzbNextRuntime_GetString(json_object *object,
+ const char *field);
+static uint64_t RzbNextRuntime_GetOptionalUint64(json_object *object,
+ const char *field);
+static bool RzbNextRuntime_RequireNugget(const RzbNextRuntime_t *runtime,
+ const char *actual);
+static bool RzbNextRuntime_RequireRequest(const RzbNextRuntime_t *runtime,
+ const char *actual);
+static const char *RzbNextRuntime_Availability(
+ const RzbNextRuntime_t *runtime
+);
+static const char *RzbNextRuntime_WorkflowState(
+ const RzbNextRuntime_t *runtime
+);
+static const char *RzbNextRuntime_WorkflowReasonCode(
+ const RzbNextRuntime_t *runtime
+);
+static const char *RzbNextRuntime_ReadinessReasonCode(
+ const RzbNextRuntime_t *runtime
+);
+static void RzbNextRuntime_RecordStateMetrics(
+ const RzbNextRuntime_t *runtime
+);
+static void RzbNextRuntime_RecordStartupDurationOnce(
+ RzbNextRuntime_t *runtime,
+ const char *outcome,
+ const char *reasonCode
+);
+static char *RzbNextRuntime_RenderJson(json_object *object);
+static void RzbNextRuntime_RecordCallbackFailure(
+ RzbNextRuntime_t *runtime,
+ const char *callback
+);
+static void RzbNextRuntime_InvokeCallback(
+ RzbNextRuntime_t *runtime,
+ const char *callbackName,
+ RzbNextRuntimeCallbackFn callback
+);
+static void RzbNextRuntime_InvokeRegisteredCallback(
+ RzbNextRuntime_t *runtime,
+ const struct RzbNextRuntimeTransition *transition
+);
+static void RzbNextRuntime_InvokeCacheInvalidateCallback(
+ RzbNextRuntime_t *runtime,
+ const char *invalidationId
+);
+static uint64_t RzbNextRuntime_DelayForFailure(
+ struct RzbNextRuntimeRetryPolicy policy,
+ uint32_t consecutiveFailureCount,
+ uint64_t retryAfter,
+ uint8_t jitterPercentile
+);
+
+static bool
+RzbNextRuntime_IsUuid(const char *value)
+{
+ size_t index;
+
+ if (value == NULL || strlen(value) != 36)
+ return false;
+ for (index = 0; index < 36; index++) {
+ if (index == 8 || index == 13 || index == 18 || index == 23) {
+ if (value[index] != '-')
+ return false;
+ } else if (!((value[index] >= '0' && value[index] <= '9') ||
+ (value[index] >= 'a' && value[index] <= 'f'))) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static char *
+RzbNextRuntime_Strdup(const char *value)
+{
+ char *copy;
+ size_t length;
+
+ if (value == NULL)
+ return NULL;
+ length = strlen(value) + 1U;
+ copy = malloc(length);
+ if (copy == NULL)
+ return NULL;
+ memcpy(copy, value, length);
+ return copy;
+}
+
+static void
+RzbNextRuntime_Free(char **value)
+{
+ if (value == NULL || *value == NULL)
+ return;
+ free(*value);
+ *value = NULL;
+}
+
+static void
+RzbNextRuntime_CopyFixed(char *dest, size_t destSize, const char *source)
+{
+ if (dest == NULL || destSize == 0U)
+ return;
+ dest[0] = '\0';
+ if (source == NULL)
+ return;
+ snprintf(dest, destSize, "%s", source);
+}
+
+static json_object *
+RzbNextRuntime_ParseValidated(const char *jsonMessage, const char *schemaName)
+{
+ json_object *object;
+ const char *actualSchemaName;
+
+ if (jsonMessage == NULL || schemaName == NULL)
+ return NULL;
+ if (!RzbNextMessage_Validate(jsonMessage))
+ return NULL;
+ object = json_tokener_parse(jsonMessage);
+ if (object == NULL)
+ return NULL;
+ if (!json_object_is_type(object, json_type_object)) {
+ json_object_put(object);
+ return NULL;
+ }
+ actualSchemaName = RzbNextRuntime_GetString(object, "schema_name");
+ if (actualSchemaName == NULL || strcmp(actualSchemaName, schemaName) != 0) {
+ json_object_put(object);
+ return NULL;
+ }
+ return object;
+}
+
+static const char *
+RzbNextRuntime_GetString(json_object *object, const char *field)
+{
+ json_object *value;
+
+ if (object == NULL || field == NULL)
+ return NULL;
+ if (!json_object_object_get_ex(object, field, &value) ||
+ !json_object_is_type(value, json_type_string)) {
+ return NULL;
+ }
+ return json_object_get_string(value);
+}
+
+static uint64_t
+RzbNextRuntime_GetOptionalUint64(json_object *object, const char *field)
+{
+ json_object *value;
+
+ if (object == NULL || field == NULL)
+ return 0U;
+ if (!json_object_object_get_ex(object, field, &value) ||
+ !json_object_is_type(value, json_type_int)) {
+ return 0U;
+ }
+ return (uint64_t)json_object_get_int64(value);
+}
+
+static bool
+RzbNextRuntime_RequireNugget(const RzbNextRuntime_t *runtime, const char *actual)
+{
+ return runtime != NULL && actual != NULL &&
+ strcmp(runtime->nuggetUuid, actual) == 0;
+}
+
+static bool
+RzbNextRuntime_RequireRequest(const RzbNextRuntime_t *runtime, const char *actual)
+{
+ return runtime != NULL && actual != NULL &&
+ runtime->inFlightRequestId != NULL &&
+ strcmp(runtime->inFlightRequestId, actual) == 0;
+}
+
+static const char *
+RzbNextRuntime_Availability(const RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL)
+ return "failed";
+ switch (runtime->state) {
+ case RZB_NEXT_RUNTIME_READY:
+ case RZB_NEXT_RUNTIME_PAUSED:
+ if (runtime->dependencyPaused)
+ return "dependency_paused";
+ return "ready";
+ case RZB_NEXT_RUNTIME_DRAINING:
+ return "draining";
+ case RZB_NEXT_RUNTIME_STOPPED:
+ case RZB_NEXT_RUNTIME_FAILED:
+ return "failed";
+ default:
+ return "registration_gated";
+ }
+}
+
+static const char *
+RzbNextRuntime_WorkflowState(const RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL)
+ return "failed";
+ if (runtime->dependencyPaused &&
+ (runtime->state == RZB_NEXT_RUNTIME_READY ||
+ runtime->state == RZB_NEXT_RUNTIME_PAUSED)) {
+ return "paused_dependency";
+ }
+ switch (runtime->state) {
+ case RZB_NEXT_RUNTIME_READY:
+ return "running";
+ case RZB_NEXT_RUNTIME_WAITING_FOR_DISPATCHER:
+ case RZB_NEXT_RUNTIME_REGISTERING:
+ case RZB_NEXT_RUNTIME_PAUSED:
+ return "paused_registration";
+ case RZB_NEXT_RUNTIME_DRAINING:
+ return "draining";
+ case RZB_NEXT_RUNTIME_STOPPED:
+ return "stopped";
+ case RZB_NEXT_RUNTIME_FAILED:
+ return "failed";
+ case RZB_NEXT_RUNTIME_STARTING:
+ default:
+ return "starting";
+ }
+}
+
+static const char *
+RzbNextRuntime_WorkflowReasonCode(const RzbNextRuntime_t *runtime)
+{
+ const char *workflowState = RzbNextRuntime_WorkflowState(runtime);
+
+ if (strcmp(workflowState, "running") == 0)
+ return NULL;
+ if (strcmp(workflowState, "paused_dependency") == 0)
+ return "component_runtime_paused_dependency";
+ if (strcmp(workflowState, "paused_registration") == 0)
+ return "component_runtime_paused_registration";
+ if (strcmp(workflowState, "draining") == 0)
+ return "component_runtime_draining";
+ if (strcmp(workflowState, "stopped") == 0)
+ return "component_runtime_stopped";
+ if (strcmp(workflowState, "failed") == 0)
+ return "component_runtime_failed";
+ return "component_runtime_starting";
+}
+
+static const char *
+RzbNextRuntime_ReadinessReasonCode(const RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL)
+ return "component_runtime_failed";
+ if (RzbNextRuntime_Health(runtime).readyz)
+ return NULL;
+ if (runtime->dependencyPaused &&
+ (runtime->state == RZB_NEXT_RUNTIME_READY ||
+ runtime->state == RZB_NEXT_RUNTIME_PAUSED)) {
+ return "dependency_unavailable";
+ }
+ return RzbNextRuntime_WorkflowReasonCode(runtime);
+}
+
+static void
+RzbNextRuntime_RecordStateMetrics(const RzbNextRuntime_t *runtime)
+{
+ struct RzbNextRuntimeHealth health;
+ const char *reasonCode;
+
+ if (runtime == NULL)
+ return;
+ health = RzbNextRuntime_Health(runtime);
+ reasonCode = RzbNextRuntime_ReadinessReasonCode(runtime);
+ Telemetry_RecordRuntimeWorkflowState(
+ RZB_NEXT_RUNTIME_WORKFLOW_NAME,
+ RzbNextRuntime_WorkflowState(runtime),
+ RzbNextRuntime_WorkflowReasonCode(runtime)
+ );
+ Telemetry_RecordRuntimeReadinessState(
+ health.readyz ? "ready" : "not_ready",
+ health.readyz ? "succeeded" : "failed",
+ reasonCode
+ );
+}
+
+static void
+RzbNextRuntime_RecordStartupDurationOnce(RzbNextRuntime_t *runtime,
+ const char *outcome,
+ const char *reasonCode)
+{
+ if (runtime == NULL || runtime->startupMetricRecorded)
+ return;
+ runtime->startupMetricRecorded = true;
+ Telemetry_RecordRuntimeStartupDuration(
+ Telemetry_GetMonotonicTimeSeconds() - runtime->startedAt,
+ outcome,
+ reasonCode
+ );
+}
+
+static char *
+RzbNextRuntime_RenderJson(json_object *object)
+{
+ const char *jsonText;
+ char *copy;
+
+ if (object == NULL)
+ return NULL;
+ jsonText = json_object_to_json_string_ext(object, JSON_C_TO_STRING_PLAIN);
+ copy = RzbNextRuntime_Strdup(jsonText);
+ json_object_put(object);
+ return copy;
+}
+
+static void
+RzbNextRuntime_RecordCallbackFailure(RzbNextRuntime_t *runtime,
+ const char *callback)
+{
+ if (runtime == NULL || callback == NULL)
+ return;
+ runtime->callbackFailureCount++;
+ RzbNextRuntime_CopyFixed(runtime->lastCallbackFailure,
+ sizeof(runtime->lastCallbackFailure), callback);
+ if (runtime->callbacks.onError != NULL && strcmp(callback, "onError") != 0) {
+ if (!runtime->callbacks.onError(runtime, "callback_failed", false,
+ runtime->callbacks.userData)) {
+ runtime->callbackFailureCount++;
+ RzbNextRuntime_CopyFixed(runtime->lastCallbackFailure,
+ sizeof(runtime->lastCallbackFailure),
+ "onError");
+ }
+ }
+}
+
+static void
+RzbNextRuntime_InvokeCallback(RzbNextRuntime_t *runtime,
+ const char *callbackName,
+ RzbNextRuntimeCallbackFn callback)
+{
+ if (runtime == NULL || callbackName == NULL || callback == NULL)
+ return;
+ if (!callback(runtime, runtime->callbacks.userData))
+ RzbNextRuntime_RecordCallbackFailure(runtime, callbackName);
+}
+
+static void
+RzbNextRuntime_InvokeRegisteredCallback(
+ RzbNextRuntime_t *runtime,
+ const struct RzbNextRuntimeTransition *transition
+)
+{
+ if (runtime == NULL || transition == NULL ||
+ runtime->callbacks.onRegistered == NULL) {
+ return;
+ }
+ if (!runtime->callbacks.onRegistered(runtime, transition,
+ runtime->callbacks.userData)) {
+ RzbNextRuntime_RecordCallbackFailure(runtime, "onRegistered");
+ }
+}
+
+static void
+RzbNextRuntime_InvokeCacheInvalidateCallback(RzbNextRuntime_t *runtime,
+ const char *invalidationId)
+{
+ if (runtime == NULL || invalidationId == NULL ||
+ runtime->callbacks.onCacheInvalidate == NULL) {
+ return;
+ }
+ if (!runtime->callbacks.onCacheInvalidate(runtime, invalidationId,
+ runtime->callbacks.userData)) {
+ RzbNextRuntime_RecordCallbackFailure(runtime, "onCacheInvalidate");
+ }
+}
+
+const char *
+RzbNextRuntime_StateString(enum RzbNextRuntimeState state)
+{
+ switch (state) {
+ case RZB_NEXT_RUNTIME_STARTING:
+ return "starting";
+ case RZB_NEXT_RUNTIME_WAITING_FOR_DISPATCHER:
+ return "waiting_for_dispatcher";
+ case RZB_NEXT_RUNTIME_REGISTERING:
+ return "registering";
+ case RZB_NEXT_RUNTIME_READY:
+ return "ready";
+ case RZB_NEXT_RUNTIME_PAUSED:
+ return "paused";
+ case RZB_NEXT_RUNTIME_DRAINING:
+ return "draining";
+ case RZB_NEXT_RUNTIME_STOPPED:
+ return "stopped";
+ case RZB_NEXT_RUNTIME_FAILED:
+ return "failed";
+ default:
+ return "unknown";
+ }
+}
+
+RzbNextRuntime_t *
+RzbNextRuntime_Create(const char *nuggetUuid, const char *processUuid)
+{
+ RzbNextRuntime_t *runtime;
+
+ if (sg_runtimeActive || !RzbNextRuntime_IsUuid(nuggetUuid) ||
+ !RzbNextRuntime_IsUuid(processUuid)) {
+ return NULL;
+ }
+ runtime = calloc(1U, sizeof(*runtime));
+ if (runtime == NULL)
+ return NULL;
+ runtime->nuggetUuid = RzbNextRuntime_Strdup(nuggetUuid);
+ runtime->processUuid = RzbNextRuntime_Strdup(processUuid);
+ runtime->runtimePolicy = RzbNextRuntime_Strdup("running");
+ runtime->startedAt = Telemetry_GetMonotonicTimeSeconds();
+ runtime->state = RZB_NEXT_RUNTIME_STARTING;
+ if (runtime->nuggetUuid == NULL || runtime->processUuid == NULL ||
+ runtime->runtimePolicy == NULL) {
+ RzbNextRuntime_Destroy(runtime);
+ return NULL;
+ }
+ sg_runtimeActive = true;
+ return runtime;
+}
+
+RzbNextRuntime_t *
+RzbNextRuntime_CreateGenerated(const char *nuggetUuid)
+{
+ uuid_t generated;
+ char processUuid[37];
+
+ uuid_generate_random(generated);
+ uuid_unparse_lower(generated, processUuid);
+ return RzbNextRuntime_Create(nuggetUuid, processUuid);
+}
+
+void
+RzbNextRuntime_Destroy(RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL)
+ return;
+ RzbNextRuntime_Free(&runtime->nuggetUuid);
+ RzbNextRuntime_Free(&runtime->processUuid);
+ RzbNextRuntime_Free(&runtime->inFlightRequestId);
+ RzbNextRuntime_Free(&runtime->acceptedRequestId);
+ RzbNextRuntime_Free(&runtime->registrationGeneration);
+ RzbNextRuntime_Free(&runtime->runtimePolicy);
+ free(runtime);
+ sg_runtimeActive = false;
+}
+
+bool
+RzbNextRuntime_SetCallbacks(RzbNextRuntime_t *runtime,
+ const struct RzbNextRuntimeCallbacks *callbacks)
+{
+ if (runtime == NULL)
+ return false;
+ if (callbacks == NULL) {
+ memset(&runtime->callbacks, 0, sizeof(runtime->callbacks));
+ } else {
+ runtime->callbacks = *callbacks;
+ }
+ return true;
+}
+
+uint32_t
+RzbNextRuntime_CallbackFailureCount(const RzbNextRuntime_t *runtime)
+{
+ return runtime == NULL ? 0U : runtime->callbackFailureCount;
+}
+
+const char *
+RzbNextRuntime_LastCallbackFailure(const RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL || runtime->callbackFailureCount == 0U)
+ return NULL;
+ return runtime->lastCallbackFailure;
+}
+
+enum RzbNextRuntimeState
+RzbNextRuntime_State(const RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL)
+ return RZB_NEXT_RUNTIME_FAILED;
+ return runtime->state;
+}
+
+const char *
+RzbNextRuntime_ProcessUuid(const RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL)
+ return NULL;
+ return runtime->processUuid;
+}
+
+const char *
+RzbNextRuntime_RegistrationGeneration(const RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL)
+ return NULL;
+ return runtime->registrationGeneration;
+}
+
+struct RzbNextRuntimeHealth
+RzbNextRuntime_Health(const RzbNextRuntime_t *runtime)
+{
+ struct RzbNextRuntimeHealth health = { true, false, false };
+
+ if (runtime == NULL)
+ return health;
+ switch (runtime->state) {
+ case RZB_NEXT_RUNTIME_STARTING:
+ case RZB_NEXT_RUNTIME_WAITING_FOR_DISPATCHER:
+ case RZB_NEXT_RUNTIME_REGISTERING:
+ case RZB_NEXT_RUNTIME_READY:
+ case RZB_NEXT_RUNTIME_PAUSED:
+ case RZB_NEXT_RUNTIME_DRAINING:
+ health.healthz = true;
+ break;
+ default:
+ health.healthz = false;
+ break;
+ }
+ health.readyz = runtime->state == RZB_NEXT_RUNTIME_READY &&
+ !runtime->dependencyPaused;
+ return health;
+}
+
+bool
+RzbNextRuntime_HealthStartupCheck(void *userData)
+{
+ return RzbNextRuntime_Health(userData).startupz;
+}
+
+bool
+RzbNextRuntime_HealthLiveCheck(void *userData)
+{
+ return RzbNextRuntime_Health(userData).healthz;
+}
+
+bool
+RzbNextRuntime_HealthReadyCheck(void *userData)
+{
+ return RzbNextRuntime_Health(userData).readyz;
+}
+
+void
+RzbNextRuntime_Initialize(RzbNextRuntime_t *runtime)
+{
+ if (runtime != NULL && runtime->state == RZB_NEXT_RUNTIME_STARTING) {
+ runtime->state = RZB_NEXT_RUNTIME_WAITING_FOR_DISPATCHER;
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ }
+}
+
+bool
+RzbNextRuntime_ObserveDispatcherHello(RzbNextRuntime_t *runtime,
+ const char *jsonMessage)
+{
+ if (runtime == NULL || !RzbNextCnc_IsReadyDispatcherHello(jsonMessage))
+ return false;
+ if (runtime->state == RZB_NEXT_RUNTIME_STARTING ||
+ runtime->state == RZB_NEXT_RUNTIME_WAITING_FOR_DISPATCHER) {
+ runtime->state = RZB_NEXT_RUNTIME_REGISTERING;
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ }
+ return true;
+}
+
+bool
+RzbNextRuntime_BeginRegistration(RzbNextRuntime_t *runtime, const char *requestId)
+{
+ char *copy;
+
+ if (runtime == NULL || !RzbNextRuntime_IsUuid(requestId))
+ return false;
+ copy = RzbNextRuntime_Strdup(requestId);
+ if (copy == NULL)
+ return false;
+ RzbNextRuntime_Free(&runtime->inFlightRequestId);
+ runtime->inFlightRequestId = copy;
+ runtime->state = RZB_NEXT_RUNTIME_REGISTERING;
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ return true;
+}
+
+bool
+RzbNextRuntime_RegistrationAccepted(RzbNextRuntime_t *runtime,
+ const char *jsonMessage,
+ struct RzbNextRuntimeTransition *transition)
+{
+ json_object *object;
+ const char *requestId;
+ const char *nuggetUuid;
+ const char *generation;
+ const char *policy;
+ uint64_t interval = 0U;
+ uint64_t freshness = 0U;
+ uint64_t skew = 0U;
+ bool success;
+ struct RzbNextRuntimeTransition appliedTransition;
+
+ if (transition != NULL)
+ memset(transition, 0, sizeof(*transition));
+ memset(&appliedTransition, 0, sizeof(appliedTransition));
+ if (runtime == NULL)
+ return false;
+ object = RzbNextRuntime_ParseValidated(
+ jsonMessage, RZB_NEXT_SCHEMA_CNC_REGISTRATION_ACCEPTED);
+ if (object == NULL)
+ return false;
+ requestId = RzbNextRuntime_GetString(object, "request_id");
+ nuggetUuid = RzbNextRuntime_GetString(object, "nugget_uuid");
+ generation = RzbNextRuntime_GetString(object, "registration_generation");
+ policy = RzbNextRuntime_GetString(object, "effective_runtime_policy");
+ if (!RzbNextRuntime_RequireNugget(runtime, nuggetUuid) ||
+ !RzbNextRuntime_RequireRequest(runtime, requestId) ||
+ !RzbNextCnc_RegistrationAcceptedTiming(jsonMessage, &interval,
+ &freshness, &skew)) {
+ json_object_put(object);
+ return false;
+ }
+
+ RzbNextRuntime_Free(&runtime->acceptedRequestId);
+ RzbNextRuntime_Free(&runtime->registrationGeneration);
+ RzbNextRuntime_Free(&runtime->runtimePolicy);
+ runtime->acceptedRequestId = RzbNextRuntime_Strdup(requestId);
+ runtime->registrationGeneration = RzbNextRuntime_Strdup(generation);
+ runtime->runtimePolicy = RzbNextRuntime_Strdup(policy);
+ RzbNextRuntime_Free(&runtime->inFlightRequestId);
+ runtime->livenessInterval = interval;
+ runtime->livenessFreshnessWindow = freshness;
+ runtime->livenessClockSkewTolerance = skew;
+ runtime->state = strcmp(policy, "running") == 0
+ ? RZB_NEXT_RUNTIME_READY
+ : RZB_NEXT_RUNTIME_PAUSED;
+ RzbNextRuntime_RecordStartupDurationOnce(runtime, "succeeded", NULL);
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ appliedTransition.kind = RZB_NEXT_RUNTIME_TRANSITION_REGISTERED;
+ appliedTransition.ready = runtime->state == RZB_NEXT_RUNTIME_READY;
+ RzbNextRuntime_CopyFixed(appliedTransition.generation,
+ sizeof(appliedTransition.generation), generation);
+ if (transition != NULL)
+ *transition = appliedTransition;
+ success = runtime->acceptedRequestId != NULL &&
+ runtime->registrationGeneration != NULL &&
+ runtime->runtimePolicy != NULL;
+ if (success) {
+ RzbNextRuntime_InvokeRegisteredCallback(runtime, &appliedTransition);
+ if (appliedTransition.ready) {
+ RzbNextRuntime_InvokeCallback(runtime, "onReady",
+ runtime->callbacks.onReady);
+ }
+ }
+ json_object_put(object);
+ return success;
+}
+
+bool
+RzbNextRuntime_RegistrationRejected(RzbNextRuntime_t *runtime,
+ const char *jsonMessage,
+ struct RzbNextRuntimeTransition *transition)
+{
+ json_object *object;
+ const char *requestId;
+ const char *nuggetUuid;
+ const char *reasonCode;
+ json_object *retryableObject = NULL;
+ bool retryable;
+
+ if (transition != NULL)
+ memset(transition, 0, sizeof(*transition));
+ if (runtime == NULL)
+ return false;
+ object = RzbNextRuntime_ParseValidated(
+ jsonMessage, RZB_NEXT_SCHEMA_CNC_REGISTRATION_REJECTED);
+ if (object == NULL)
+ return false;
+ requestId = RzbNextRuntime_GetString(object, "request_id");
+ nuggetUuid = RzbNextRuntime_GetString(object, "nugget_uuid");
+ reasonCode = RzbNextRuntime_GetString(object, "reason_code");
+ if (!RzbNextRuntime_RequireNugget(runtime, nuggetUuid) ||
+ !RzbNextRuntime_RequireRequest(runtime, requestId) ||
+ !json_object_object_get_ex(object, "retryable", &retryableObject)) {
+ json_object_put(object);
+ return false;
+ }
+ retryable = json_object_get_boolean(retryableObject);
+ RzbNextRuntime_Free(&runtime->inFlightRequestId);
+ if (retryable) {
+ runtime->state = RZB_NEXT_RUNTIME_REGISTERING;
+ } else {
+ RzbNextRuntime_Free(&runtime->acceptedRequestId);
+ RzbNextRuntime_Free(&runtime->registrationGeneration);
+ runtime->state = RZB_NEXT_RUNTIME_FAILED;
+ RzbNextRuntime_RecordStartupDurationOnce(runtime, "failed", reasonCode);
+ }
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ if (transition != NULL) {
+ transition->kind = RZB_NEXT_RUNTIME_TRANSITION_REJECTED;
+ transition->retryable = retryable;
+ transition->retryAfter =
+ RzbNextRuntime_GetOptionalUint64(object, "retry_after");
+ RzbNextRuntime_CopyFixed(transition->reasonCode,
+ sizeof(transition->reasonCode), reasonCode);
+ }
+ if (!retryable && runtime->callbacks.onError != NULL &&
+ !runtime->callbacks.onError(runtime, reasonCode, false,
+ runtime->callbacks.userData)) {
+ RzbNextRuntime_RecordCallbackFailure(runtime, "onError");
+ }
+ json_object_put(object);
+ return true;
+}
+
+bool
+RzbNextRuntime_ApplyDirectedCommand(RzbNextRuntime_t *runtime,
+ const char *jsonMessage,
+ struct RzbNextRuntimeDirectedResult *result)
+{
+ json_object *object;
+ const char *target;
+ const char *generation;
+ const char *command;
+ const char *invalidationId;
+ bool hadAcceptedGeneration;
+
+ if (result != NULL)
+ memset(result, 0, sizeof(*result));
+ if (runtime == NULL)
+ return false;
+ object = RzbNextRuntime_ParseValidated(
+ jsonMessage, RZB_NEXT_SCHEMA_CNC_DIRECTED_COMMAND);
+ if (object == NULL)
+ return false;
+ target = RzbNextRuntime_GetString(object, "target_nugget_uuid");
+ generation = RzbNextRuntime_GetString(object, "registration_generation");
+ command = RzbNextRuntime_GetString(object, "command");
+ if (!RzbNextRuntime_RequireNugget(runtime, target)) {
+ json_object_put(object);
+ return false;
+ }
+ hadAcceptedGeneration = runtime->registrationGeneration != NULL;
+ if (!hadAcceptedGeneration ||
+ strcmp(runtime->registrationGeneration, generation) != 0) {
+ RzbNextRuntime_Free(&runtime->acceptedRequestId);
+ RzbNextRuntime_Free(&runtime->registrationGeneration);
+ runtime->state = RZB_NEXT_RUNTIME_REGISTERING;
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ if (result != NULL) {
+ result->effect = hadAcceptedGeneration
+ ? RZB_NEXT_RUNTIME_DIRECTED_REREGISTER
+ : RZB_NEXT_RUNTIME_DIRECTED_IGNORED_STALE;
+ result->staleGeneration = true;
+ }
+ json_object_put(object);
+ return true;
+ }
+
+ if (strcmp(command, "pause") == 0) {
+ RzbNextRuntime_PauseNewWork(runtime);
+ if (result != NULL)
+ result->effect = RZB_NEXT_RUNTIME_DIRECTED_PAUSE;
+ RzbNextRuntime_InvokeCallback(runtime, "onPause",
+ runtime->callbacks.onPause);
+ } else if (strcmp(command, "go") == 0) {
+ if (!RzbNextRuntime_ResumeWhenReady(runtime)) {
+ json_object_put(object);
+ return false;
+ }
+ if (result != NULL)
+ result->effect = RZB_NEXT_RUNTIME_DIRECTED_RESUME;
+ RzbNextRuntime_InvokeCallback(runtime, "onResume",
+ runtime->callbacks.onResume);
+ } else if (strcmp(command, "terminate") == 0) {
+ RzbNextRuntime_BeginDraining(runtime);
+ if (result != NULL)
+ result->effect = RZB_NEXT_RUNTIME_DIRECTED_SHUTDOWN;
+ RzbNextRuntime_InvokeCallback(runtime, "onShutdown",
+ runtime->callbacks.onShutdown);
+ } else if (strcmp(command, "re_register") == 0) {
+ RzbNextRuntime_Free(&runtime->acceptedRequestId);
+ RzbNextRuntime_Free(&runtime->registrationGeneration);
+ runtime->state = RZB_NEXT_RUNTIME_REGISTERING;
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ if (result != NULL)
+ result->effect = RZB_NEXT_RUNTIME_DIRECTED_REREGISTER;
+ } else if (strcmp(command, "cache_invalidate") == 0) {
+ if (result != NULL) {
+ result->effect = RZB_NEXT_RUNTIME_DIRECTED_CACHE_INVALIDATE;
+ invalidationId = RzbNextRuntime_GetString(object, "invalidation_id");
+ RzbNextRuntime_CopyFixed(result->invalidationId,
+ sizeof(result->invalidationId),
+ invalidationId);
+ }
+ RzbNextRuntime_InvokeCacheInvalidateCallback(
+ runtime,
+ RzbNextRuntime_GetString(object, "invalidation_id")
+ );
+ } else {
+ json_object_put(object);
+ return false;
+ }
+ json_object_put(object);
+ return true;
+}
+
+bool
+RzbNextRuntime_LivenessPlan(const RzbNextRuntime_t *runtime,
+ const char *createdAt,
+ struct RzbNextRuntimeLivenessPlan *plan)
+{
+ json_object *object;
+
+ if (plan != NULL)
+ memset(plan, 0, sizeof(*plan));
+ if (runtime == NULL || plan == NULL ||
+ runtime->registrationGeneration == NULL || runtime->runtimePolicy == NULL ||
+ createdAt == NULL) {
+ return false;
+ }
+ object = json_object_new_object();
+ if (object == NULL)
+ return false;
+ json_object_object_add(object, "schema_name",
+ json_object_new_string(RZB_NEXT_SCHEMA_CNC_LIVENESS));
+ json_object_object_add(object, "schema_version",
+ json_object_new_int(RZB_NEXT_SCHEMA_VERSION));
+ json_object_object_add(object, "nugget_uuid",
+ json_object_new_string(runtime->nuggetUuid));
+ json_object_object_add(object, "registration_generation",
+ json_object_new_string(runtime->registrationGeneration));
+ json_object_object_add(object, "runtime_policy",
+ json_object_new_string(runtime->runtimePolicy));
+ json_object_object_add(object, "availability",
+ json_object_new_string(RzbNextRuntime_Availability(runtime)));
+ json_object_object_add(object, "created_at", json_object_new_string(createdAt));
+ plan->message = RzbNextRuntime_RenderJson(object);
+ if (plan->message == NULL || !RzbNextMessage_Validate(plan->message)) {
+ RzbNextRuntime_LivenessPlanClear(plan);
+ return false;
+ }
+ plan->interval = runtime->livenessInterval;
+ plan->freshnessWindow = runtime->livenessFreshnessWindow;
+ plan->clockSkewTolerance = runtime->livenessClockSkewTolerance;
+ plan->messageExpiration = runtime->livenessFreshnessWindow;
+ return true;
+}
+
+char *
+RzbNextRuntime_BuildBye(const RzbNextRuntime_t *runtime,
+ const char *reason,
+ const char *createdAt)
+{
+ json_object *object;
+ char *message;
+
+ if (runtime == NULL || runtime->registrationGeneration == NULL ||
+ reason == NULL || createdAt == NULL) {
+ return NULL;
+ }
+ object = json_object_new_object();
+ if (object == NULL)
+ return NULL;
+ json_object_object_add(object, "schema_name",
+ json_object_new_string(RZB_NEXT_SCHEMA_CNC_BYE));
+ json_object_object_add(object, "schema_version",
+ json_object_new_int(RZB_NEXT_SCHEMA_VERSION));
+ json_object_object_add(object, "nugget_uuid",
+ json_object_new_string(runtime->nuggetUuid));
+ json_object_object_add(object, "registration_generation",
+ json_object_new_string(runtime->registrationGeneration));
+ json_object_object_add(object, "reason", json_object_new_string(reason));
+ json_object_object_add(object, "created_at", json_object_new_string(createdAt));
+ message = RzbNextRuntime_RenderJson(object);
+ if (message == NULL || !RzbNextMessage_Validate(message)) {
+ free(message);
+ return NULL;
+ }
+ return message;
+}
+
+void
+RzbNextRuntime_LivenessPlanClear(struct RzbNextRuntimeLivenessPlan *plan)
+{
+ if (plan == NULL)
+ return;
+ free(plan->message);
+ memset(plan, 0, sizeof(*plan));
+}
+
+bool
+RzbNextRuntime_AcceptsWork(const RzbNextRuntime_t *runtime)
+{
+ return runtime != NULL && runtime->state == RZB_NEXT_RUNTIME_READY &&
+ !runtime->dependencyPaused;
+}
+
+bool
+RzbNextRuntime_StartWhenReady(RzbNextRuntime_t *runtime)
+{
+ if (!RzbNextRuntime_AcceptsWork(runtime))
+ return false;
+ runtime->inFlightWork++;
+ return true;
+}
+
+void
+RzbNextRuntime_CompleteWork(RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL || runtime->inFlightWork == 0U)
+ return;
+ runtime->inFlightWork--;
+}
+
+uint32_t
+RzbNextRuntime_InFlightWork(const RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL)
+ return 0U;
+ return runtime->inFlightWork;
+}
+
+void
+RzbNextRuntime_PauseNewWork(RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL ||
+ runtime->state == RZB_NEXT_RUNTIME_DRAINING ||
+ runtime->state == RZB_NEXT_RUNTIME_STOPPED ||
+ runtime->state == RZB_NEXT_RUNTIME_FAILED) {
+ return;
+ }
+ RzbNextRuntime_Free(&runtime->runtimePolicy);
+ runtime->runtimePolicy = RzbNextRuntime_Strdup("paused");
+ runtime->state = RZB_NEXT_RUNTIME_PAUSED;
+ RzbNextRuntime_RecordStateMetrics(runtime);
+}
+
+bool
+RzbNextRuntime_ResumeWhenReady(RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL || runtime->registrationGeneration == NULL ||
+ runtime->state == RZB_NEXT_RUNTIME_DRAINING ||
+ runtime->state == RZB_NEXT_RUNTIME_STOPPED ||
+ runtime->state == RZB_NEXT_RUNTIME_FAILED) {
+ return false;
+ }
+ RzbNextRuntime_Free(&runtime->runtimePolicy);
+ runtime->runtimePolicy = RzbNextRuntime_Strdup("running");
+ if (runtime->runtimePolicy == NULL)
+ return false;
+ runtime->state = RZB_NEXT_RUNTIME_READY;
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ return true;
+}
+
+bool
+RzbNextRuntime_Drain(RzbNextRuntime_t *runtime)
+{
+ bool drained;
+
+ if (runtime == NULL)
+ return false;
+ RzbNextRuntime_BeginDraining(runtime);
+ drained = runtime->inFlightWork == 0U;
+ Telemetry_RecordRuntimeShutdownDrainDuration(
+ 0.0,
+ RZB_NEXT_RUNTIME_SERVICE_NAME,
+ drained ? "succeeded" : "failed",
+ drained ? NULL : "drain_incomplete"
+ );
+ return drained;
+}
+
+bool
+RzbNextRuntime_DependencyUnavailable(RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL)
+ return false;
+ runtime->dependencyPaused = true;
+ Telemetry_RecordRuntimeDependencyState(
+ RZB_NEXT_RUNTIME_DEPENDENCY_NAME,
+ "unavailable",
+ "dependency_unavailable"
+ );
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ return true;
+}
+
+bool
+RzbNextRuntime_DependencyRecovered(RzbNextRuntime_t *runtime)
+{
+ if (runtime == NULL)
+ return false;
+ runtime->dependencyPaused = false;
+ Telemetry_RecordRuntimeDependencyState(
+ RZB_NEXT_RUNTIME_DEPENDENCY_NAME,
+ "available",
+ NULL
+ );
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ return true;
+}
+
+void
+RzbNextRuntime_BeginDraining(RzbNextRuntime_t *runtime)
+{
+ if (runtime != NULL) {
+ runtime->state = RZB_NEXT_RUNTIME_DRAINING;
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ }
+}
+
+void
+RzbNextRuntime_MarkStopped(RzbNextRuntime_t *runtime)
+{
+ if (runtime != NULL) {
+ runtime->state = RZB_NEXT_RUNTIME_STOPPED;
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ }
+}
+
+void
+RzbNextRuntime_MarkFailed(RzbNextRuntime_t *runtime)
+{
+ if (runtime != NULL) {
+ runtime->state = RZB_NEXT_RUNTIME_FAILED;
+ RzbNextRuntime_RecordStartupDurationOnce(runtime, "failed", "failed");
+ RzbNextRuntime_RecordStateMetrics(runtime);
+ }
+}
+
+void
+RzbNextRuntimeRetry_Init(struct RzbNextRuntimeRetryState *state,
+ struct RzbNextRuntimeRetryPolicy policy)
+{
+ if (state == NULL)
+ return;
+ memset(state, 0, sizeof(*state));
+ state->policy = policy;
+}
+
+struct RzbNextRuntimeRetryDecision
+RzbNextRuntimeRetry_RequestRegistration(struct RzbNextRuntimeRetryState *state)
+{
+ struct RzbNextRuntimeRetryDecision decision = {
+ RZB_NEXT_RUNTIME_RETRY_STOP, 0U
+ };
+
+ if (state == NULL)
+ return decision;
+ if (state->inFlight) {
+ state->dirtyRerun = true;
+ decision.action = RZB_NEXT_RUNTIME_RETRY_ALREADY_IN_FLIGHT;
+ return decision;
+ }
+ state->inFlight = true;
+ decision.action = RZB_NEXT_RUNTIME_RETRY_START_NOW;
+ return decision;
+}
+
+void
+RzbNextRuntimeRetry_Accepted(struct RzbNextRuntimeRetryState *state)
+{
+ if (state == NULL)
+ return;
+ state->inFlight = false;
+ state->dirtyRerun = false;
+ state->consecutiveFailureCount = 0U;
+}
+
+struct RzbNextRuntimeRetryDecision
+RzbNextRuntimeRetry_RetryableFailure(struct RzbNextRuntimeRetryState *state,
+ uint64_t retryAfter,
+ uint8_t jitterPercentile)
+{
+ struct RzbNextRuntimeRetryDecision decision = {
+ RZB_NEXT_RUNTIME_RETRY_STOP, 0U
+ };
+
+ if (state == NULL)
+ return decision;
+ state->inFlight = false;
+ if (state->dirtyRerun) {
+ state->dirtyRerun = false;
+ state->consecutiveFailureCount = 0U;
+ state->inFlight = true;
+ decision.action = RZB_NEXT_RUNTIME_RETRY_START_NOW;
+ return decision;
+ }
+ decision.action = RZB_NEXT_RUNTIME_RETRY_AFTER;
+ decision.delay = RzbNextRuntime_DelayForFailure(
+ state->policy, state->consecutiveFailureCount, retryAfter,
+ jitterPercentile);
+ state->consecutiveFailureCount++;
+ return decision;
+}
+
+struct RzbNextRuntimeRetryDecision
+RzbNextRuntimeRetry_TerminalFailure(struct RzbNextRuntimeRetryState *state)
+{
+ struct RzbNextRuntimeRetryDecision decision = {
+ RZB_NEXT_RUNTIME_RETRY_STOP, 0U
+ };
+
+ if (state != NULL) {
+ state->inFlight = false;
+ state->dirtyRerun = false;
+ }
+ return decision;
+}
+
+static uint64_t
+RzbNextRuntime_DelayForFailure(struct RzbNextRuntimeRetryPolicy policy,
+ uint32_t consecutiveFailureCount,
+ uint64_t retryAfter,
+ uint8_t jitterPercentile)
+{
+ uint64_t delay = policy.initialBackoff;
+ uint64_t lower;
+ uint64_t upper;
+ uint64_t span;
+ uint8_t jitter;
+ uint8_t percentile;
+ uint32_t index;
+
+ if (retryAfter > 0U)
+ return retryAfter;
+ if (policy.maxBackoff == 0U)
+ policy.maxBackoff = delay;
+ for (index = 0U; index < consecutiveFailureCount; index++) {
+ if (delay > policy.maxBackoff / 2U) {
+ delay = policy.maxBackoff;
+ break;
+ }
+ delay *= 2U;
+ if (delay >= policy.maxBackoff) {
+ delay = policy.maxBackoff;
+ break;
+ }
+ }
+ jitter = policy.jitterPercent > 100U ? 100U : policy.jitterPercent;
+ if (jitter == 0U)
+ return delay;
+ percentile = jitterPercentile > 100U ? 100U : jitterPercentile;
+ lower = (delay * (uint64_t)(100U - jitter)) / 100U;
+ upper = (delay * (uint64_t)(100U + jitter)) / 100U;
+ if (lower < policy.initialBackoff)
+ lower = policy.initialBackoff;
+ if (upper > policy.maxBackoff)
+ upper = policy.maxBackoff;
+ if (upper <= lower)
+ return lower;
+ span = upper - lower;
+ return lower + ((span * percentile) / 100U);
+}
diff --git a/src/rzb_dev.c b/src/rzb_dev.c
new file mode 100644
index 0000000..bfc5914
--- /dev/null
+++ b/src/rzb_dev.c
@@ -0,0 +1,578 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include "api_internal.h"
+#include "dev_mode.h"
+#include "nugget_tool.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+struct DevContextLookup
+{
+ struct RazorbackContext *context;
+ size_t count;
+};
+
+struct DevInspectRun
+{
+ struct RazorbackContext *context;
+ struct BlockPoolItem *inputItem;
+ List_t *eventMetadata;
+ uint8_t result;
+};
+
+static void
+RzbDev_Usage(const char *name)
+{
+ fprintf(stderr, "Usage: %s [--debug] [--type=DATA_TYPE] \n",
+ name);
+}
+
+static bool
+RzbDev_DisableTelemetryExporters(void)
+{
+#ifdef _MSC_VER
+ return _putenv_s("OTEL_SDK_DISABLED", "true") == 0;
+#else
+ return setenv("OTEL_SDK_DISABLED", "true", 1) == 0;
+#endif
+}
+
+static bool
+RzbDev_SetDefaultLogMask(void)
+{
+ if (getenv("RZB_LOG_MASK") != NULL)
+ return true;
+
+#ifdef _MSC_VER
+ return _putenv_s("RZB_LOG_MASK", "FFFFFFFFFFFFFEDF") == 0;
+#else
+ return setenv("RZB_LOG_MASK", "FFFFFFFFFFFFFEDF", 0) == 0;
+#endif
+}
+
+static void
+RzbDev_ClearFailedPreparedFile(struct Block *block)
+{
+ if (block == NULL)
+ return;
+
+ free(block->data.fileName);
+ memset(&block->data, 0, sizeof(block->data));
+}
+
+static const char *
+RzbDev_ResultLabel(uint8_t result)
+{
+ switch (result) {
+ case JUDGMENT_REASON_DONE:
+ return "done";
+ case JUDGMENT_REASON_ALERT:
+ return "alert";
+ case JUDGMENT_REASON_DEFERRED:
+ return "deferred";
+ case JUDGMENT_REASON_ERROR:
+ default:
+ return "error";
+ }
+}
+
+static const char *
+RzbDev_BlockStatusLabel(uint32_t status)
+{
+ switch (status & BLOCK_POOL_STATUS_MASK) {
+ case BLOCK_POOL_STATUS_COLLECTING:
+ return "collecting";
+ case BLOCK_POOL_STATUS_FINALIZED:
+ return "finalized";
+ case BLOCK_POOL_STATUS_CHECK_GLOBAL_CACHE:
+ return "check_global_cache";
+ case BLOCK_POOL_STATUS_CHECKING_GLOBAL_CACHE:
+ return "checking_global_cache";
+ case BLOCK_POOL_STATUS_SUBMIT_DATA:
+ return "submit_data";
+ case BLOCK_POOL_STATUS_PAGED:
+ return "paged";
+ case BLOCK_POOL_STATUS_DESTROY:
+ return "destroy";
+ case BLOCK_POOL_STATUS_ERROR:
+ return "error";
+ case BLOCK_POOL_STATUS_NO_TYPE:
+ return "no_type";
+ default:
+ return "unknown";
+ }
+}
+
+static int
+RzbDev_SelectContext(struct RazorbackContext *context, void *userData)
+{
+ struct DevContextLookup *lookup = userData;
+
+ if (lookup == NULL || context == NULL)
+ return LIST_EACH_ERROR;
+
+ lookup->context = context;
+ lookup->count++;
+ return LIST_EACH_OK;
+}
+
+static struct RazorbackContext *
+RzbDev_GetOnlyContext(void)
+{
+ struct DevContextLookup lookup;
+
+ lookup.context = NULL;
+ lookup.count = 0;
+ if (!Razorback_ForEach_Context(RzbDev_SelectContext, &lookup))
+ return NULL;
+ if (lookup.count != 1)
+ return NULL;
+ return lookup.context;
+}
+
+static char *
+RzbDev_HashToString(const struct Hash *hash)
+{
+ char *value;
+ uint32_t i;
+
+ if (hash == NULL || hash->pData == NULL || hash->iSize == 0)
+ return NULL;
+
+ value = calloc((size_t)hash->iSize * 2U + 1U, sizeof(char));
+ if (value == NULL)
+ return NULL;
+
+ for (i = 0; i < hash->iSize; ++i)
+ snprintf(value + (i * 2U), 3U, "%02x", hash->pData[i]);
+
+ return value;
+}
+
+static int
+RzbDev_PrintJudgment(void *item, void *userData)
+{
+ struct Judgment *judgment = item;
+ size_t *index = userData;
+ char *message;
+ char *hashValue;
+
+ if (judgment == NULL || index == NULL)
+ return LIST_EACH_ERROR;
+
+ (*index)++;
+ hashValue = (judgment->pBlockId != NULL) ? RzbDev_HashToString(judgment->pBlockId->pHash) : NULL;
+ message = (judgment->sMessage != NULL) ? strdup((const char *)judgment->sMessage) : NULL;
+
+ printf("judgment[%zu]: gid=%u sid=%u priority=%u sf_set=0x%08x ent_set=0x%08x\n",
+ *index, judgment->iGID, judgment->iSID, judgment->iPriority,
+ judgment->Set_SfFlags, judgment->Set_EntFlags);
+ if (message != NULL)
+ printf(" message: %s\n", message);
+ if (hashValue != NULL)
+ printf(" block_hash: %s\n", hashValue);
+
+ free(message);
+ free(hashValue);
+ return LIST_EACH_OK;
+}
+
+struct DevMetadataPrintState
+{
+ size_t index;
+};
+
+static int
+RzbDev_PrintMetadataItem(void *item, void *userData)
+{
+ struct NTLVItem *metadata = item;
+ struct DevMetadataPrintState *state = userData;
+ char *name = NULL;
+ char *type = NULL;
+ char addressBuffer[INET6_ADDRSTRLEN];
+ const char *value = NULL;
+ char numberBuffer[32];
+ uint16_t portValue;
+
+ if (metadata == NULL || state == NULL)
+ return LIST_EACH_ERROR;
+
+ state->index++;
+ name = UUID_Get_NameByUUID(metadata->uuidName, UUID_TYPE_NTLV_NAME);
+ type = UUID_Get_NameByUUID(metadata->uuidType, UUID_TYPE_NTLV_TYPE);
+
+ if (type != NULL && strcmp(type, NTLV_TYPE_STRING) == 0) {
+ value = (const char *)metadata->pData;
+ } else if (type != NULL && strcmp(type, NTLV_TYPE_JSON) == 0) {
+ value = (const char *)metadata->pData;
+ } else if (type != NULL && strcmp(type, NTLV_TYPE_IPv4_ADDR) == 0 &&
+ metadata->iLength == 4 &&
+ inet_ntop(AF_INET, metadata->pData, addressBuffer, sizeof(addressBuffer)) != NULL) {
+ value = addressBuffer;
+ } else if (type != NULL && strcmp(type, NTLV_TYPE_IPv6_ADDR) == 0 &&
+ metadata->iLength == 16 &&
+ inet_ntop(AF_INET6, metadata->pData, addressBuffer, sizeof(addressBuffer)) != NULL) {
+ value = addressBuffer;
+ } else if (type != NULL && strcmp(type, NTLV_TYPE_PORT) == 0 &&
+ metadata->iLength == sizeof(uint16_t)) {
+ memcpy(&portValue, metadata->pData, sizeof(portValue));
+ snprintf(numberBuffer, sizeof(numberBuffer), "%u", (unsigned int)portValue);
+ value = numberBuffer;
+ }
+
+ printf(" metadata[%zu]: name=%s type=%s length=%u",
+ state->index,
+ (name != NULL) ? name : "unknown",
+ (type != NULL) ? type : "unknown",
+ metadata->iLength);
+ if (value != NULL)
+ printf(" value=%s", value);
+ printf("\n");
+
+ free(name);
+ free(type);
+ return LIST_EACH_OK;
+}
+
+static void
+RzbDev_PrintMetadataList(const char *label, List_t *metadataList)
+{
+ struct DevMetadataPrintState state;
+
+ if (metadataList == NULL || List_Length(metadataList) == 0)
+ return;
+
+ printf(" %s_count: %zu\n", label, List_Length(metadataList));
+ state.index = 0;
+ (void)List_ForEach(metadataList, RzbDev_PrintMetadataItem, &state);
+}
+
+static int
+RzbDev_PrintSubmission(void *item, void *userData)
+{
+ struct BlockPoolItem *poolItem = item;
+ size_t *index = userData;
+ char *typeName;
+ char *hashValue;
+
+ if (poolItem == NULL || index == NULL)
+ return LIST_EACH_ERROR;
+
+ (*index)++;
+ typeName = UUID_Get_NameByUUID(poolItem->pEvent->pBlock->pId->uuidDataType,
+ UUID_TYPE_DATA_TYPE);
+ hashValue = RzbDev_HashToString(poolItem->pEvent->pBlock->pId->pHash);
+
+ printf("submission[%zu]: status=%s type=%s length=%llu\n",
+ *index,
+ RzbDev_BlockStatusLabel(poolItem->iStatus),
+ (typeName != NULL) ? typeName : "unknown",
+ (unsigned long long)poolItem->pEvent->pBlock->pId->iLength);
+ if (hashValue != NULL)
+ printf(" hash: %s\n", hashValue);
+ if (poolItem->pEvent != NULL) {
+ RzbDev_PrintMetadataList("event_metadata", poolItem->pEvent->pMetaDataList);
+ if (poolItem->pEvent->pBlock != NULL)
+ RzbDev_PrintMetadataList("block_metadata", poolItem->pEvent->pBlock->pMetaDataList);
+ }
+
+ free(typeName);
+ free(hashValue);
+ return LIST_EACH_OK;
+}
+
+static void
+RzbDev_InspectThread(Thread_t *thread)
+{
+ struct DevInspectRun *run;
+ struct RazorbackContext *context;
+ struct Block *block = NULL;
+ char *fileName = NULL;
+ void *threadData = NULL;
+ bool threadInitialized = false;
+ bool filePrepared = false;
+
+ run = Thread_GetUserData(thread);
+ context = Thread_GetContext(thread);
+ run->result = JUDGMENT_REASON_ERROR;
+
+ if (run == NULL || context == NULL || run->inputItem == NULL)
+ return;
+
+ if (context->inspector.hooks->initThread != NULL) {
+ if (!context->inspector.hooks->initThread(&threadData)) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Nugget initThread failed", __func__);
+ goto cleanup;
+ }
+ threadInitialized = true;
+ } else {
+ threadInitialized = true;
+ }
+
+ block = Block_Clone(run->inputItem->pEvent->pBlock);
+ if (block == NULL) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to clone input block", __func__);
+ goto cleanup;
+ }
+
+ if (run->inputItem->pDataHead == NULL ||
+ run->inputItem->pDataHead->data.fileName == NULL)
+ {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Local input block is missing file data", __func__);
+ goto cleanup;
+ }
+
+ fileName = strdup(run->inputItem->pDataHead->data.fileName);
+ if (fileName == NULL)
+ goto cleanup;
+
+ if (!RzbNextFileserver_AttachFileToBlock(block, fileName, false)) {
+ fileName = NULL;
+ RzbDev_ClearFailedPreparedFile(block);
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to prepare file-backed block", __func__);
+ goto cleanup;
+ }
+ fileName = NULL;
+ filePrepared = true;
+
+ run->result = context->inspector.hooks->processBlock(block,
+ run->inputItem->pEvent->pId,
+ run->eventMetadata,
+ threadData);
+
+cleanup:
+ free(fileName);
+ if (block != NULL) {
+ if (filePrepared)
+ RzbNextFileserver_FreeBlockData(block);
+ Block_Destroy(block);
+ }
+ if (threadInitialized &&
+ context != NULL &&
+ context->inspector.hooks->cleanupThread != NULL)
+ {
+ context->inspector.hooks->cleanupThread(threadData);
+ }
+}
+
+int
+main(int argc, char **argv)
+{
+ static const struct option long_options[] = {
+ {"debug", no_argument, NULL, 'd'},
+ {"help", no_argument, NULL, 'h'},
+ {"type", required_argument, NULL, 't'},
+ {NULL, 0, NULL, 0}
+ };
+ struct NuggetToolModule module;
+ struct RazorbackContext *context;
+ struct BlockPoolItem *inputItem = NULL;
+ Thread_t *inspectThread = NULL;
+ struct DevInspectRun run;
+ List_t *judgments;
+ List_t *submissions;
+ const char *typeNameArg = NULL;
+ const char *modulePath;
+ const char *filePath;
+ char *autoTypeName = NULL;
+ size_t index;
+ int opt;
+ int exitCode = 1;
+ bool debug = false;
+ bool moduleInitialized = false;
+
+ memset(&run, 0, sizeof(run));
+
+ while ((opt = getopt_long(argc, argv, "dht:", long_options, NULL)) != -1) {
+ switch (opt) {
+ case 'd':
+ debug = true;
+ break;
+ case 'h':
+ RzbDev_Usage(argv[0]);
+ return 0;
+ case 't':
+ typeNameArg = optarg;
+ break;
+ default:
+ RzbDev_Usage(argv[0]);
+ return 1;
+ }
+ }
+
+ if ((argc - optind) != 2) {
+ RzbDev_Usage(argv[0]);
+ return 1;
+ }
+
+ modulePath = argv[optind];
+ filePath = argv[optind + 1];
+
+ if (!RzbDev_DisableTelemetryExporters()) {
+ fprintf(stderr, "%s: failed to disable OpenTelemetry SDK\n", argv[0]);
+ return 1;
+ }
+ if (!RzbDev_SetDefaultLogMask()) {
+ fprintf(stderr, "%s: failed to set default RZB_LOG_MASK\n", argv[0]);
+ return 1;
+ }
+
+ RZB_Init_API();
+ if (debug)
+ rzb_debug_logging();
+ Razorback_DevMode_SetEnabled(true);
+
+ if (!NuggetTool_LoadModule(modulePath, &module))
+ return 1;
+
+ if (!module.initNug()) {
+ rzb_log(LOG_ERR, LOG_C_NUGGET, "%s: Nugget initialization failed for %s",
+ __func__, modulePath);
+ goto cleanup;
+ }
+ moduleInitialized = true;
+
+ context = RzbDev_GetOnlyContext();
+ if (context == NULL || context->inspector.hooks == NULL ||
+ context->inspector.hooks->processBlock == NULL)
+ {
+ rzb_log(LOG_ERR, LOG_C_CORE,
+ "%s: Expected exactly one inspection context after initNug()",
+ __func__);
+ goto cleanup;
+ }
+
+ inputItem = BlockPool_CreateItem(context);
+ if (inputItem == NULL) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to create local input item", __func__);
+ goto cleanup;
+ }
+
+ if (typeNameArg != NULL) {
+ if (!BlockPool_SetItemDataType(inputItem, (char *)typeNameArg)) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to set input data type '%s'",
+ __func__, typeNameArg);
+ goto cleanup;
+ }
+ } else if (context->inspector.dataTypeCount == 1) {
+ autoTypeName = UUID_Get_NameByUUID(context->inspector.dataTypeList[0],
+ UUID_TYPE_DATA_TYPE);
+ if (autoTypeName != NULL &&
+ !BlockPool_SetItemDataType(inputItem, autoTypeName))
+ {
+ rzb_log(LOG_ERR, LOG_C_CORE,
+ "%s: Failed to set inferred input data type '%s'",
+ __func__, autoTypeName);
+ goto cleanup;
+ }
+ }
+
+ if (!BlockPool_AddData_FromFile(inputItem, (char *)filePath, false)) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to attach input file '%s'",
+ __func__, filePath);
+ goto cleanup;
+ }
+
+ (void)Block_MetaData_Add_FileName(inputItem->pEvent->pBlock, filePath);
+
+ if (!BlockPool_FinalizeItem(inputItem)) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to finalize local input item", __func__);
+ goto cleanup;
+ }
+
+ if (uuid_is_null(inputItem->pEvent->pBlock->pId->uuidDataType) == 1) {
+ rzb_log(LOG_ERR, LOG_C_CORE,
+ "%s: Input file type was not resolved; pass --type for this nugget",
+ __func__);
+ goto cleanup;
+ }
+
+ run.context = context;
+ run.inputItem = inputItem;
+ run.eventMetadata = NTLVList_Create();
+ run.result = JUDGMENT_REASON_ERROR;
+ if (run.eventMetadata == NULL) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to allocate event metadata list", __func__);
+ goto cleanup;
+ }
+ (void)Metadata_Add_Filename(run.eventMetadata, filePath);
+
+ inspectThread = Thread_Launch(RzbDev_InspectThread, &run, "Local Inspect", context);
+ if (inspectThread == NULL) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to launch local inspection thread",
+ __func__);
+ goto cleanup;
+ }
+
+ Thread_Join(inspectThread);
+ Thread_Destroy(inspectThread);
+ inspectThread = NULL;
+
+ printf("inspection_result: %s\n", RzbDev_ResultLabel(run.result));
+
+ judgments = Razorback_DevMode_GetJudgments(context);
+ submissions = Razorback_DevMode_GetSubmissions(context);
+
+ printf("judgment_count: %zu\n", (judgments != NULL) ? List_Length(judgments) : 0U);
+ index = 0;
+ if (judgments != NULL)
+ (void)List_ForEach(judgments, RzbDev_PrintJudgment, &index);
+
+ printf("submission_count: %zu\n", (submissions != NULL) ? List_Length(submissions) : 0U);
+ index = 0;
+ if (submissions != NULL)
+ (void)List_ForEach(submissions, RzbDev_PrintSubmission, &index);
+
+ exitCode = 0;
+
+cleanup:
+ if (inspectThread != NULL) {
+ Thread_Join(inspectThread);
+ Thread_Destroy(inspectThread);
+ }
+ if (inputItem != NULL)
+ BlockPool_DestroyItem(inputItem);
+ if (run.eventMetadata != NULL)
+ List_Destroy(run.eventMetadata);
+ free(autoTypeName);
+ if (moduleInitialized && module.shutdownNug != NULL)
+ module.shutdownNug();
+ NuggetTool_UnloadModule(&module);
+ Razorback_DevMode_SetEnabled(false);
+ return exitCode;
+}
diff --git a/src/rzb_worker.c b/src/rzb_worker.c
new file mode 100644
index 0000000..2eba2ec
--- /dev/null
+++ b/src/rzb_worker.c
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include "nugget_tool.h"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+static volatile sig_atomic_t sg_stopRequested = 0;
+
+static void
+RzbWorker_Terminate(int sig)
+{
+ (void)sig;
+ sg_stopRequested = 1;
+}
+
+static void
+RzbWorker_Usage(const char *name)
+{
+ fprintf(stderr,
+ "Usage: %s [--debug] [--health-bind=ADDR] [--health-port=PORT] \n",
+ name);
+}
+
+static bool
+RzbWorker_ParseHealthPort(const char *value, unsigned long *port)
+{
+ char *end = NULL;
+ unsigned long parsed;
+
+ if (value == NULL || port == NULL)
+ return false;
+
+ errno = 0;
+ parsed = strtoul(value, &end, 10);
+ if (errno != 0 || end == value || *end != '\0' || parsed > UINT16_MAX)
+ return false;
+
+ *port = parsed;
+ return true;
+}
+
+int
+main(int argc, char **argv)
+{
+ static const struct option long_options[] = {
+ {"debug", no_argument, NULL, 'd'},
+ {"help", no_argument, NULL, 'h'},
+ {"health-bind", required_argument, NULL, 'b'},
+ {"health-port", required_argument, NULL, 'p'},
+ {NULL, 0, NULL, 0}
+ };
+ struct NuggetToolModule module;
+ RazorbackHealthServerConfig_t healthConfig;
+ const char *healthBind = "127.0.0.1";
+ const char *modulePath;
+ unsigned long healthPort = 0;
+ int opt;
+ bool debug = false;
+ bool healthStarted = false;
+
+ while ((opt = getopt_long(argc, argv, "dhb:p:", long_options, NULL)) != -1) {
+ switch (opt) {
+ case 'd':
+ debug = true;
+ break;
+ case 'h':
+ RzbWorker_Usage(argv[0]);
+ return 0;
+ case 'b':
+ healthBind = optarg;
+ break;
+ case 'p':
+ if (!RzbWorker_ParseHealthPort(optarg, &healthPort)) {
+ fprintf(stderr, "%s: invalid health port: %s\n", argv[0], optarg);
+ return 1;
+ }
+ break;
+ default:
+ RzbWorker_Usage(argv[0]);
+ return 1;
+ }
+ }
+
+ if (optind >= argc) {
+ RzbWorker_Usage(argv[0]);
+ return 1;
+ }
+ modulePath = argv[optind];
+
+ signal(SIGINT, RzbWorker_Terminate);
+ signal(SIGTERM, RzbWorker_Terminate);
+
+ RZB_Init_API();
+ if (debug)
+ rzb_debug_logging();
+
+ if (!NuggetTool_LoadModule(modulePath, &module))
+ return 1;
+
+ if (healthPort > 0) {
+ healthConfig.bindAddress = healthBind;
+ healthConfig.port = (uint16_t)healthPort;
+ healthConfig.requireContextsForReady = true;
+ if (!Razorback_Health_Start(&healthConfig)) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to start health listener", __func__);
+ NuggetTool_UnloadModule(&module);
+ return 1;
+ }
+ healthStarted = true;
+ }
+
+ if (!module.initNug()) {
+ rzb_log(LOG_ERR, LOG_C_NUGGET, "%s: Nugget initialization failed for %s",
+ __func__, modulePath);
+ if (healthStarted)
+ Razorback_Health_Stop();
+ NuggetTool_UnloadModule(&module);
+ return 1;
+ }
+
+ Razorback_Health_SetStartupComplete(true);
+ while (!sg_stopRequested)
+ Thread_Sleep(250);
+
+ Razorback_Health_SetStartupComplete(false);
+ module.shutdownNug();
+ if (healthStarted)
+ Razorback_Health_Stop();
+ NuggetTool_UnloadModule(&module);
+ return 0;
+}
diff --git a/src/submission.c b/src/submission.c
index a54903f..bea145e 100644
--- a/src/submission.c
+++ b/src/submission.c
@@ -25,16 +25,16 @@
#include
#include
#include
+#include
#include
#include
#include
#include
#include "block_pool_private.h"
+#include "dev_mode.h"
#include "submission_private.h"
#include "local_cache.h"
-#include "connected_entity_private.h"
-#include "transfer/core.h"
#include "runtime_config.h"
#include "telemetry.h"
#include
@@ -78,6 +78,10 @@ static int Submission_CacheLookupTiming_Cmp(void *a, void *b);
static void Submission_CacheLookupTiming_Destroy(void *item);
static void Submission_ClearCacheLookupTiming(struct BlockPoolItem *item);
static void Submission_RecordCacheLookupStart(struct BlockPoolItem *item);
+static int Submission_Submit_DevMode(struct BlockPoolItem *p_pItem,
+ int p_iFlags,
+ uint32_t *p_pSf_Flags,
+ uint32_t *p_pEnt_Flags);
static void Submission_RecordCacheLookupWait(struct BlockPoolItem *item, const char *result);
static const char *Submission_Reason_Label(uint32_t reason);
static void Submission_DestroySharedResources(void);
@@ -373,7 +377,13 @@ Submission_Shutdown(struct RazorbackContext *p_pContext)
double drainDeadline;
size_t cleanedCount;
- if (p_pContext == NULL || p_pContext->submission.responseThreadPool == NULL)
+ if (p_pContext == NULL)
+ return;
+
+ if ((p_pContext->iFlags & CONTEXT_FLAG_DEV_TOOL) == CONTEXT_FLAG_DEV_TOOL)
+ return;
+
+ if (p_pContext->submission.responseThreadPool == NULL)
return;
drainDeadline = Telemetry_GetMonotonicTimeSeconds() +
@@ -404,6 +414,9 @@ Submission_Init(struct RazorbackContext *p_pContext)
if (p_pContext == NULL)
return false;
+ if ((p_pContext->iFlags & CONTEXT_FLAG_DEV_TOOL) == CONTEXT_FLAG_DEV_TOOL)
+ return true;
+
if (p_pContext->submission.responseThreadPool != NULL)
return true;
@@ -471,6 +484,59 @@ Submission_GetContextSubmitQueueDepth(const struct RazorbackContext *p_pContext)
return depth.count;
}
+static int
+Submission_Submit_DevMode(struct BlockPoolItem *p_pItem,
+ int p_iFlags,
+ uint32_t *p_pSf_Flags,
+ uint32_t *p_pEnt_Flags)
+{
+ int ret = RZB_SUBMISSION_OK;
+
+ if (p_pSf_Flags != NULL)
+ *p_pSf_Flags = 0;
+ if (p_pEnt_Flags != NULL)
+ *p_pEnt_Flags = 0;
+
+ if ((p_pItem->pEvent->pBlock->pParentId != NULL) &&
+ BlockId_IsEqual(p_pItem->pEvent->pBlock->pId,
+ p_pItem->pEvent->pBlock->pParentId)) {
+ rzb_log(LOG_ERR, LOG_C_CORE,
+ "%s: Block submission listing its self as parent dropped.",
+ __func__);
+ BlockPool_SetStatus(p_pItem, BLOCK_POOL_STATUS_ERROR);
+ ret = RZB_SUBMISSION_ERROR;
+ } else if (p_pSf_Flags == NULL || p_pEnt_Flags == NULL) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: NULL pointer arguments to function", __func__);
+ BlockPool_SetStatus(p_pItem, BLOCK_POOL_STATUS_ERROR);
+ ret = RZB_SUBMISSION_ERROR;
+ } else if (uuid_is_null(p_pItem->pEvent->pBlock->pId->uuidDataType) == 1) {
+ rzb_log(LOG_INFO, LOG_C_CORE, "%s: Submission with null data type dropped.",
+ __func__);
+ BlockPool_SetStatus(p_pItem, BLOCK_POOL_STATUS_NO_TYPE);
+ ret = RZB_SUBMISSION_NO_TYPE;
+ } else {
+ BlockPool_SetFlags(p_pItem, p_iFlags);
+ BlockPool_SetStatus(p_pItem, BLOCK_POOL_STATUS_FINALIZED);
+ }
+
+ if (!Razorback_DevMode_CaptureSubmission(p_pItem->context, p_pItem)) {
+ rzb_log(LOG_ERR, LOG_C_CORE, "%s: Failed to capture local dev submission",
+ __func__);
+ BlockPool_SetStatus(p_pItem, BLOCK_POOL_STATUS_ERROR);
+ ret = RZB_SUBMISSION_ERROR;
+ if (p_pItem->submittedCallback != NULL)
+ p_pItem->submittedCallback(p_pItem);
+ BlockPool_Item_Unlock(p_pItem);
+ BlockPool_DestroyItem(p_pItem);
+ return ret;
+ }
+
+ if (p_pItem->submittedCallback != NULL)
+ p_pItem->submittedCallback(p_pItem);
+ BlockPool_Item_Unlock(p_pItem);
+ return ret;
+}
+
SO_PUBLIC int
Submission_Submit(struct BlockPoolItem *p_pItem, int p_iFlags, uint32_t *p_pSf_Flags, uint32_t *p_pEnt_Flags)
{
@@ -480,6 +546,9 @@ Submission_Submit(struct BlockPoolItem *p_pItem, int p_iFlags, uint32_t *p_pSf_F
BlockPool_Item_Lock(p_pItem);
Telemetry_UpdateContext(&p_pItem->telemetryContext);
+ if ((p_pItem->context->iFlags & CONTEXT_FLAG_DEV_TOOL) == CONTEXT_FLAG_DEV_TOOL)
+ return Submission_Submit_DevMode(p_pItem, p_iFlags, p_pSf_Flags, p_pEnt_Flags);
+
if ( (p_pItem->pEvent->pBlock->pParentId != NULL ) &&
BlockId_IsEqual(p_pItem->pEvent->pBlock->pId, p_pItem->pEvent->pBlock->pParentId) )
{
@@ -835,9 +904,8 @@ Submission_SubmitThread(Thread_t *p_pThread)
struct Message *message;
struct RazorbackContext *itemContext = NULL;
uint8_t storedLocality = 0;
- struct ConnectedEntity *dispatcher = NULL;
- enum TransferStatus transfered = TRANSFER_FAIL_LOCAL;
- int transferTries = 0;
+ RzbNextFileserverClient_t *fileserver = NULL;
+ enum RzbNextFileserverStatus storeStatus = RZB_NEXT_FILESERVER_LOCAL_ERROR;
uint32_t reason = 0;
struct BlockPoolItem *item = NULL;
struct Queue *queue = NULL;
@@ -859,9 +927,9 @@ Submission_SubmitThread(Thread_t *p_pThread)
while (true)
{
- transfered = TRANSFER_FAIL_LOCAL;
- transferTries = 0;
item = List_Pop_Ex(submitQueue, SUBMISSION_QUEUE_POP_TIMEOUT_MS);
+ storeStatus = RZB_NEXT_FILESERVER_LOCAL_ERROR;
+ fileserver = NULL;
if (item == NULL)
{
if (Thread_IsStopped(p_pThread) &&
@@ -906,33 +974,17 @@ Submission_SubmitThread(Thread_t *p_pThread)
}
else
{
- while (transferTries < 20)
- {
- dispatcher = ConnectedEntityList_GetDispatcher();
- rzb_log(LOG_ERR,LOG_C_CORE, "%s: %z", __func__, dispatcher);
- if (dispatcher == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_CORE, "%s: Failed to find usable dispatcher", __func__);
- transfered = TRANSFER_FAIL_LOCAL;
- itemError = "failed to find usable dispatcher";
- transferTries++;
- break;
- }
- transfered = Transfer_Store(item, dispatcher);
- if (transfered == TRANSFER_FAIL_DISPATCHER)
- {
- rzb_log(LOG_ERR,LOG_C_CORE, "%s: Marking dispatcher unusable", __func__);
- ConnectedEntityList_MarkDispatcherUnusable(dispatcher->uuidNuggetId);
- }
- if (transfered == TRANSFER_OK)
- break;
- else
- transferTries++;
- }
- if (transfered != TRANSFER_OK)
+ fileserver = RzbNextFileserverClient_Create(NULL, 0, 0);
+ if (fileserver != NULL)
+ storeStatus = RzbNextFileserver_StoreBlockPoolItem(fileserver, item);
+ RzbNextFileserverClient_Destroy(fileserver);
+ fileserver = NULL;
+ if (storeStatus != RZB_NEXT_FILESERVER_OK)
{
- rzb_log(LOG_ERR,LOG_C_CORE, "%s: Failed to transfer block giving up", __func__);
- itemError = "failed to transfer block";
+ rzb_log(LOG_ERR, LOG_C_CORE,
+ "%s: Failed to store block in fileserver, status=%d",
+ __func__, storeStatus);
+ itemError = "failed to store block in fileserver";
reasonLabel = Submission_Reason_Label(SUBMISSION_REASON_REQUESTED);
submitOutcome = "store_failed";
BlockPool_SetStatus(item, BLOCK_POOL_STATUS_ERROR);
@@ -949,11 +1001,9 @@ Submission_SubmitThread(Thread_t *p_pThread)
BlockPool_DestroyItem(item);
continue;
}
- storedLocality = dispatcher->locality;
+ storedLocality = itemContext != NULL ? itemContext->locality : 0;
reason = SUBMISSION_REASON_REQUESTED;
reasonLabel = Submission_Reason_Label(reason);
- rzb_log(LOG_ERR,LOG_C_CORE, "%s: %z", __func__, dispatcher);
- ConnectedEntity_Destroy(dispatcher);
}
if ((message = MessageBlockSubmission_Initialize( item->pEvent, reason, storedLocality)) == NULL)
diff --git a/src/telemetry.h b/src/telemetry.h
index b996de0..ef449b0 100644
--- a/src/telemetry.h
+++ b/src/telemetry.h
@@ -54,7 +54,7 @@ void Telemetry_LogMessage(unsigned level, uint64_t component, const char *messag
void Telemetry_RecordDispatcherWait(double durationSeconds,
const char *outcome,
- const char *phase,
+ const char *stage,
const struct RazorbackContext *context);
void Telemetry_RecordDispatcherSelection(const char *path,
const char *selectedLocality,
@@ -83,7 +83,7 @@ void Telemetry_RecordInspectionDuration(double durationSeconds,
void Telemetry_RecordInspectionResult(const char *reason,
bool hasAlerts,
const struct RazorbackContext *context);
-void Telemetry_RecordInspectionError(const char *phase,
+void Telemetry_RecordInspectionError(const char *stage,
const char *errorClass,
const struct RazorbackContext *context);
void Telemetry_RecordShutdownRequeuedInspection(const struct RazorbackContext *context);
diff --git a/src/telemetry_otel.cc b/src/telemetry_otel.cc
index 8836757..90c52ee 100644
--- a/src/telemetry_otel.cc
+++ b/src/telemetry_otel.cc
@@ -365,6 +365,12 @@ struct RazorbackStandardMetrics
TelemetryMetric_t *blockPoolSize = nullptr;
TelemetryMetric_t *dispatcherAvailable = nullptr;
TelemetryMetric_t *dispatcherUsable = nullptr;
+ TelemetryMetric_t *runtimeDependencyState = nullptr;
+ TelemetryMetric_t *runtimeWorkflowState = nullptr;
+ TelemetryMetric_t *runtimeReadinessState = nullptr;
+ TelemetryMetric_t *runtimeStartupDuration = nullptr;
+ TelemetryMetric_t *runtimeShutdownDrainDuration = nullptr;
+ TelemetryMetric_t *runtimeTelemetryFlushOutcome = nullptr;
};
TelemetryState &
@@ -403,6 +409,15 @@ MetricLabelOrNone(const char *value) noexcept
return (value != nullptr && value[0] != '\0') ? value : "none";
}
+const char *
+MetricBoundedLabelOrNone(const char *value) noexcept
+{
+ if (value == nullptr || value[0] == '\0')
+ return "none";
+
+ return strlen(value) > 128 ? "other" : value;
+}
+
static void
LowercaseString(char *value) noexcept
{
@@ -672,6 +687,12 @@ DestroyRazorbackStandardMetrics() noexcept
&metrics.blockPoolSize,
&metrics.dispatcherAvailable,
&metrics.dispatcherUsable,
+ &metrics.runtimeDependencyState,
+ &metrics.runtimeWorkflowState,
+ &metrics.runtimeReadinessState,
+ &metrics.runtimeStartupDuration,
+ &metrics.runtimeShutdownDrainDuration,
+ &metrics.runtimeTelemetryFlushOutcome,
};
for (auto *metric_ptr : all_metrics)
@@ -726,7 +747,7 @@ InitializeRazorbackStandardMetrics() noexcept
"");
metrics.inspectionErrors = Telemetry_CreateUInt64Counter(
"rzb.inspection.errors.total",
- "Inspection errors grouped by phase.",
+ "Inspection errors grouped by stage.",
"");
metrics.shutdownRequeuedInspections = Telemetry_CreateUInt64Counter(
"rzb.shutdown.requeued.inspections.total",
@@ -796,6 +817,30 @@ InitializeRazorbackStandardMetrics() noexcept
"",
ObserveDispatcherUsable,
nullptr);
+ metrics.runtimeDependencyState = Telemetry_CreateUInt64Counter(
+ RAZORBACK_RUNTIME_DEPENDENCY_STATE_METRIC,
+ "Phase 14 runtime dependency state transitions.",
+ "");
+ metrics.runtimeWorkflowState = Telemetry_CreateUInt64Counter(
+ RAZORBACK_RUNTIME_WORKFLOW_STATE_METRIC,
+ "Phase 14 runtime workflow state transitions.",
+ "");
+ metrics.runtimeReadinessState = Telemetry_CreateUInt64Counter(
+ RAZORBACK_RUNTIME_READINESS_STATE_METRIC,
+ "Phase 14 runtime readiness transitions.",
+ "");
+ metrics.runtimeStartupDuration = Telemetry_CreateDoubleHistogram(
+ RAZORBACK_RUNTIME_STARTUP_DURATION_METRIC,
+ "Phase 14 runtime startup duration.",
+ "ms");
+ metrics.runtimeShutdownDrainDuration = Telemetry_CreateDoubleHistogram(
+ RAZORBACK_RUNTIME_SHUTDOWN_DRAIN_DURATION_METRIC,
+ "Phase 14 runtime shutdown drain duration.",
+ "ms");
+ metrics.runtimeTelemetryFlushOutcome = Telemetry_CreateUInt64Counter(
+ RAZORBACK_RUNTIME_TELEMETRY_FLUSH_OUTCOME_METRIC,
+ "Phase 14 runtime telemetry flush outcomes.",
+ "");
}
bool
@@ -1516,6 +1561,8 @@ Telemetry_Shutdown(void)
if (!state.initialized)
return;
+ Telemetry_RecordRuntimeTelemetryFlushOutcome(GetServiceName(), "attempted", nullptr);
+
DestroyRazorbackStandardMetrics();
if (state.sdk_provider)
@@ -1564,10 +1611,119 @@ Telemetry_GetMonotonicTimeSeconds(void)
return GetMonotonicTimeSecondsInternal();
}
+extern "C" void
+Telemetry_RecordRuntimeDependencyState(const char *dependency,
+ const char *status,
+ const char *reasonCode)
+{
+ RazorbackStandardMetrics &metrics = GetRazorbackStandardMetrics();
+ TelemetryMetricAttribute_t attributes[3] = {
+ {"dependency", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(dependency), 0, 0.0, false},
+ {"status", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(status), 0, 0.0, false},
+ {"reason_code", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(reasonCode), 0, 0.0, false},
+ };
+
+ Telemetry_CounterAddUInt64(metrics.runtimeDependencyState, 1, attributes, 3);
+}
+
+extern "C" void
+Telemetry_RecordRuntimeWorkflowState(const char *workflow,
+ const char *state,
+ const char *reasonCode)
+{
+ RazorbackStandardMetrics &metrics = GetRazorbackStandardMetrics();
+ TelemetryMetricAttribute_t attributes[3] = {
+ {"workflow", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(workflow), 0, 0.0, false},
+ {"state", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(state), 0, 0.0, false},
+ {"reason_code", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(reasonCode), 0, 0.0, false},
+ };
+
+ Telemetry_CounterAddUInt64(metrics.runtimeWorkflowState, 1, attributes, 3);
+}
+
+extern "C" void
+Telemetry_RecordRuntimeReadinessState(const char *state,
+ const char *outcome,
+ const char *reasonCode)
+{
+ RazorbackStandardMetrics &metrics = GetRazorbackStandardMetrics();
+ TelemetryMetricAttribute_t attributes[3] = {
+ {"state", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(state), 0, 0.0, false},
+ {"outcome", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(outcome), 0, 0.0, false},
+ {"reason_code", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(reasonCode), 0, 0.0, false},
+ };
+
+ Telemetry_CounterAddUInt64(metrics.runtimeReadinessState, 1, attributes, 3);
+}
+
+extern "C" void
+Telemetry_RecordRuntimeStartupDuration(double durationSeconds,
+ const char *outcome,
+ const char *reasonCode)
+{
+ RazorbackStandardMetrics &metrics = GetRazorbackStandardMetrics();
+ TelemetryMetricAttribute_t attributes[2] = {
+ {"outcome", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(outcome), 0, 0.0, false},
+ {"reason_code", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(reasonCode), 0, 0.0, false},
+ };
+
+ Telemetry_HistogramRecordDouble(metrics.runtimeStartupDuration,
+ durationSeconds * 1000.0, attributes, 2);
+}
+
+extern "C" void
+Telemetry_RecordRuntimeShutdownDrainDuration(double durationSeconds,
+ const char *service,
+ const char *outcome,
+ const char *reasonCode)
+{
+ RazorbackStandardMetrics &metrics = GetRazorbackStandardMetrics();
+ TelemetryMetricAttribute_t attributes[3] = {
+ {"service", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(service), 0, 0.0, false},
+ {"outcome", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(outcome), 0, 0.0, false},
+ {"reason_code", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(reasonCode), 0, 0.0, false},
+ };
+
+ Telemetry_HistogramRecordDouble(metrics.runtimeShutdownDrainDuration,
+ durationSeconds * 1000.0, attributes, 3);
+}
+
+extern "C" void
+Telemetry_RecordRuntimeTelemetryFlushOutcome(const char *service,
+ const char *outcome,
+ const char *reasonCode)
+{
+ RazorbackStandardMetrics &metrics = GetRazorbackStandardMetrics();
+ TelemetryMetricAttribute_t attributes[3] = {
+ {"service", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(service), 0, 0.0, false},
+ {"outcome", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(outcome), 0, 0.0, false},
+ {"reason_code", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricBoundedLabelOrNone(reasonCode), 0, 0.0, false},
+ };
+
+ Telemetry_CounterAddUInt64(metrics.runtimeTelemetryFlushOutcome, 1, attributes, 3);
+}
+
extern "C" void
Telemetry_RecordDispatcherWait(double durationSeconds,
const char *outcome,
- const char *phase,
+ const char *stage,
const struct RazorbackContext *context)
{
RazorbackStandardMetrics &metrics = GetRazorbackStandardMetrics();
@@ -1577,8 +1733,8 @@ Telemetry_RecordDispatcherWait(double durationSeconds,
attributes[attributeCount++] = {"outcome", TELEMETRY_METRIC_ATTRIBUTE_STRING,
MetricLabelOrUnknown(outcome), 0, 0.0, false};
- attributes[attributeCount++] = {"phase", TELEMETRY_METRIC_ATTRIBUTE_STRING,
- MetricLabelOrUnknown(phase), 0, 0.0, false};
+ attributes[attributeCount++] = {"stage", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricLabelOrUnknown(stage), 0, 0.0, false};
attributeCount = AppendContextMetricAttributes(attributes, attributeCount, context, false,
nullptr, &nuggetTypeName);
@@ -1753,7 +1909,7 @@ Telemetry_RecordInspectionResult(const char *reason,
}
extern "C" void
-Telemetry_RecordInspectionError(const char *phase,
+Telemetry_RecordInspectionError(const char *stage,
const char *errorClass,
const struct RazorbackContext *context)
{
@@ -1762,8 +1918,8 @@ Telemetry_RecordInspectionError(const char *phase,
char *nuggetTypeName = nullptr;
size_t attributeCount = 0;
- attributes[attributeCount++] = {"phase", TELEMETRY_METRIC_ATTRIBUTE_STRING,
- MetricLabelOrUnknown(phase), 0, 0.0, false};
+ attributes[attributeCount++] = {"stage", TELEMETRY_METRIC_ATTRIBUTE_STRING,
+ MetricLabelOrUnknown(stage), 0, 0.0, false};
attributes[attributeCount++] = {"error_class", TELEMETRY_METRIC_ATTRIBUTE_STRING,
MetricLabelOrUnknown(errorClass), 0, 0.0, false};
attributeCount = AppendContextMetricAttributes(attributes, attributeCount, context, false,
diff --git a/src/thread.c b/src/thread.c
index d01df29..12e5192 100644
--- a/src/thread.c
+++ b/src/thread.c
@@ -97,6 +97,19 @@ initThreading (void)
#endif
}
+bool
+Thread_Initialize(void)
+{
+#ifdef _MSC_VER
+ if (initialized == 0)
+ initThreading();
+#else
+ pthread_once(&g_once_control, initThreading);
+#endif
+
+ return (sg_threadList != NULL);
+}
+
static void
Thread_LogLaunchFailure(const char *threadName, const char *reason)
{
@@ -442,6 +455,10 @@ Thread_GetCurrent(void)
{
Thread_t *l_pRet = NULL;
rzb_thread_t l_tCurrent = Thread_GetCurrentId();
+
+ if (sg_threadList == NULL)
+ return NULL;
+
l_pRet = (Thread_t *)List_Find(sg_threadList, &l_tCurrent);
if (l_pRet == NULL)
return NULL;
diff --git a/src/transfer/core.c b/src/transfer/core.c
deleted file mode 100644
index 96ca3d0..0000000
--- a/src/transfer/core.c
+++ /dev/null
@@ -1,487 +0,0 @@
-/*
- * Copyright (c) 2011-2026 Cisco Systems, Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-#include "config.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include "../telemetry.h"
-#include "transfer/core.h"
-#include "runtime_config.h"
-#ifdef _MSC_VER
-#include "bobins.h"
-#else //_MSC_VER
-#include
-#include
-#include
-
-#endif //_MSC_VER
-
-#define RETRIES 5
-
-List_t *sg_transportList = NULL;
-static int Transport_Cmp(void *a, void *b);
-static int Transport_KeyCmp(void *a, const void *key);
-static bool sg_bTraditionalMode = true;
-
-static void
-Transfer_AddFileAttribute(TelemetrySpan_t *span,
- const char *file_name)
-{
- if (span == NULL || file_name == NULL || file_name[0] == '\0')
- return;
-
- Telemetry_AddStringAttribute(span, "rzb.file.name", file_name);
-}
-
-static void
-Transfer_AddItemFileAttribute(TelemetrySpan_t *span,
- const struct BlockPoolItem *item)
-{
- const struct BlockPoolData *data_item;
-
- if (span == NULL || item == NULL)
- return;
-
- if (item->pEvent != NULL && item->pEvent->pBlock != NULL &&
- item->pEvent->pBlock->data.fileName != NULL) {
- Transfer_AddFileAttribute(span, item->pEvent->pBlock->data.fileName);
- return;
- }
-
- data_item = item->pDataHead;
- while (data_item != NULL) {
- if ((data_item->iFlags & BLOCK_POOL_DATA_FLAG_FILE) != 0 &&
- data_item->data.fileName != NULL &&
- data_item->data.fileName[0] != '\0') {
- Transfer_AddFileAttribute(span, data_item->data.fileName);
- return;
- }
- data_item = data_item->pNext;
- }
-}
-
-static const char *
-Transfer_DispatcherLocalityLabel(const struct RazorbackContext *context,
- const struct ConnectedEntity *dispatcher)
-{
- if (context == NULL || dispatcher == NULL)
- return "unknown";
-
- return (dispatcher->locality == context->locality) ? "local" : "remote";
-}
-
-static const char *
-Transfer_StreamKind(const struct BlockPoolItem *item)
-{
- const struct BlockPoolData *dataItem;
-
- if (item == NULL)
- return "memory";
-
- if (item->pEvent != NULL &&
- item->pEvent->pBlock != NULL &&
- item->pEvent->pBlock->data.fileName != NULL)
- return "file";
-
- dataItem = item->pDataHead;
- while (dataItem != NULL) {
- if ((dataItem->iFlags & BLOCK_POOL_DATA_FLAG_FILE) == BLOCK_POOL_DATA_FLAG_FILE)
- return "file";
- dataItem = dataItem->pNext;
- }
-
- return "memory";
-}
-
-char *
-Transfer_generateFilename (struct Block *block)
-{
- char *hash;
- char *filename;
- if ((hash = Hash_ToText (block->pId->pHash)) == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Could not convert hash to text", __func__);
- return NULL;
- }
-#ifdef _MSC_VER
-#define FILENAME_FMT "%s.%u"
-#else
-#define FILENAME_FMT "%s.%ju"
-#endif
- if (asprintf(&filename, FILENAME_FMT, hash, block->pId->iLength) == -1)
- {
- free(hash);
- return NULL;
- }
- free(hash);
- return filename;
-}
-
-void * pluginDlHandle;
-bool (*initPlugin)(void);
-
-bool
-Transfer_Init(void)
-{
- char * mode = Config_getTransferMode();
- if ((mode == NULL) || (strcmp(mode, "") ==0) ){
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Global.TransferMode not set in API config file.", __func__);
- return false;
- }
- sg_transportList = List_Create(LIST_MODE_GENERIC,
- Transport_Cmp,
- Transport_KeyCmp,
- NULL,
- NULL, // Clone
- NULL, // Lock
- NULL); // Unlock
- if (sg_transportList == NULL)
- return false;
-
- if (!File_Init())
- return false;
- if (!SSH_Init())
- return false;
- if (!HTTP_Init())
- return false;
-
- if (strcmp(mode, "traditional") == 0) {
- rzb_log(LOG_INFO,LOG_C_TRANSFER, "%s: File transfers taking place in traditional mode", __func__);
- sg_bTraditionalMode = true;
- } else {
- rzb_log(LOG_INFO,LOG_C_TRANSFER, "%s: File transfers taking place using plugin: %s", __func__, mode);
- sg_bTraditionalMode = false;
- char * soVersion;
- int l_iIt, l_iLen;
- if ((l_iLen = asprintf(&soVersion, "%s", NUGGET_SO_VERSION))== -1)
- return false;
-
- // This is ugly there must be a better way
- for (l_iIt =0; l_iIt < l_iLen; l_iIt++)
- if (soVersion[l_iIt] == ':')
- soVersion[l_iIt] = '.';
-
- char * soFile;
- if (asprintf(&soFile, "%s/razorback_transfer_%s.so.%s", LIB_DIR, mode, soVersion) == -1) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: Failed to allocate plugin path", __func__);
- free(soVersion);
- return false;
- }
- free(soVersion);
- soVersion = NULL;
-
- if ((pluginDlHandle = dlopen(soFile, RTLD_LOCAL | RTLD_NOW)) == NULL) {
- char * errstr = dlerror();
-
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to open %s, %s", __func__, soFile, errstr);
- free(soFile);
- return false;
- }
- free(soFile);
- soFile = NULL;
-
- *(void **)&initPlugin = dlsym(pluginDlHandle, "transferInit");
- if (initPlugin == NULL) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed find plugin init function", __func__);
- return false;
- }
-
- if (!initPlugin()) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to initialize transfer plugin", __func__);
- return false;
- }
- }
- return true;
-}
-
-bool
-Transport_Register(struct TransportDescriptor *desc)
-{
- return List_Push(sg_transportList, desc);
-}
-
-bool
-Transport_IsSupported(uint8_t protocol)
-{
- struct TransportDescriptor *trans= NULL;
- trans = List_Find(sg_transportList, &protocol);
- return !(trans == NULL);
-}
-
-enum TransferStatus
-Transfer_Store(struct BlockPoolItem *item, struct ConnectedEntity *dispatcher)
-{
- struct TransportDescriptor *trans = NULL;
- TelemetrySpan_t *transferSpan = NULL;
- int i;
- int attempt_count = 0;
- enum TransferStatus status;
- const char *transferError = NULL;
- const char *protocol = "none";
- const char *dispatcherLocality = "unknown";
- const char *streamKind = Transfer_StreamKind(item);
- struct RazorbackContext *context = NULL;
- double transferStartedAt = Telemetry_GetMonotonicTimeSeconds();
-
- transferSpan = Telemetry_StartSpan("store block", NULL);
- if (item != NULL && item->pEvent != NULL)
- Telemetry_AddBlockAttributes(transferSpan, item->pEvent->pBlock);
- Transfer_AddItemFileAttribute(transferSpan, item);
- context = (item != NULL && item->context != NULL) ? item->context
- : Thread_GetCurrentContext();
-
- if (dispatcher == NULL || dispatcher->dispatcher == NULL) {
- transferError = "missing dispatcher for store";
- Telemetry_RecordTransferStoreDuration(Telemetry_GetMonotonicTimeSeconds() - transferStartedAt,
- "failure",
- protocol,
- dispatcherLocality,
- streamKind,
- context);
- Telemetry_RecordTransferFailure("store", protocol, "missing_dispatcher",
- dispatcherLocality, context);
- Telemetry_EndSpan(transferSpan, false, transferError);
- return TRANSFER_FAIL_LOCAL;
- }
-
- if ((trans = List_Find(sg_transportList, &dispatcher->dispatcher->protocol)) == NULL) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "Failed to find transport descriptor for protocol: %u", dispatcher->dispatcher->protocol);
- transferError = "failed to find transport descriptor for store";
- Telemetry_RecordTransferStoreDuration(Telemetry_GetMonotonicTimeSeconds() - transferStartedAt,
- "failure",
- protocol,
- dispatcherLocality,
- streamKind,
- context);
- Telemetry_RecordTransferFailure("store", protocol, "transport_missing",
- dispatcherLocality, context);
- Telemetry_EndSpan(transferSpan, false, transferError);
- return TRANSFER_FAIL_LOCAL;
- }
- protocol = trans->name;
- dispatcherLocality = Transfer_DispatcherLocalityLabel(context, dispatcher);
- rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: locality: %u, protocol: %u", __func__, dispatcher->locality, dispatcher->dispatcher->protocol);
- rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Transport: %s", __func__, trans->name);
-
- for (i =0; i < RETRIES; i++)
- {
- attempt_count++;
- status = trans->store(item, dispatcher);
- if (status == TRANSFER_OK)
- break;
- }
- Telemetry_AddIntAttribute(transferSpan, "rzb.transfer.attempts",
- (int64_t)attempt_count);
- if (status != TRANSFER_OK)
- transferError = "failed to store block";
- Telemetry_RecordTransferStoreDuration(Telemetry_GetMonotonicTimeSeconds() - transferStartedAt,
- (status == TRANSFER_OK) ? "success" : "failure",
- protocol,
- dispatcherLocality,
- streamKind,
- context);
- if (status != TRANSFER_OK)
- Telemetry_RecordTransferFailure("store", protocol, "request_failed",
- dispatcherLocality, context);
- Telemetry_EndSpan(transferSpan, status == TRANSFER_OK, transferError);
- return status;
-}
-
-enum TransferStatus
-Transfer_Fetch(struct Block *block, struct ConnectedEntity *dispatcher)
-{
- struct TransportDescriptor *trans = NULL;
- TelemetrySpan_t *transferSpan = NULL;
- int i;
- enum TransferStatus status;
- const char *transferError = NULL;
- const char *protocol = "none";
- const char *dispatcherLocality = "unknown";
- struct RazorbackContext *context = Thread_GetCurrentContext();
- double transferStartedAt = Telemetry_GetMonotonicTimeSeconds();
-
- transferSpan = Telemetry_StartSpan("fetch block", NULL);
- Telemetry_AddBlockAttributes(transferSpan, block);
-
- if (dispatcher == NULL || dispatcher->dispatcher == NULL) {
- transferError = "missing dispatcher for fetch";
- Telemetry_RecordTransferFetchDuration(Telemetry_GetMonotonicTimeSeconds() - transferStartedAt,
- "failure",
- protocol,
- dispatcherLocality,
- context);
- Telemetry_RecordTransferFailure("fetch", protocol, "missing_dispatcher",
- dispatcherLocality, context);
- Telemetry_EndSpan(transferSpan, false, transferError);
- return TRANSFER_FAIL_LOCAL;
- }
-
- if ((trans = List_Find(sg_transportList, &dispatcher->dispatcher->protocol)) == NULL) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to find transport descriptor", __func__);
- transferError = "failed to find transport descriptor for fetch";
- Telemetry_RecordTransferFetchDuration(Telemetry_GetMonotonicTimeSeconds() - transferStartedAt,
- "failure",
- protocol,
- dispatcherLocality,
- context);
- Telemetry_RecordTransferFailure("fetch", protocol, "transport_missing",
- dispatcherLocality, context);
- Telemetry_EndSpan(transferSpan, false, transferError);
- return TRANSFER_FAIL_LOCAL;
- }
- protocol = trans->name;
- dispatcherLocality = Transfer_DispatcherLocalityLabel(context, dispatcher);
- rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: locality: %u, protocol: %u", __func__, dispatcher->locality, dispatcher->dispatcher->protocol);
- rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Transport: %s", __func__, trans->name);
-
-
- for (i = 0; i < RETRIES; i++)
- {
- status = trans->fetch(block, dispatcher);
- if (status == TRANSFER_OK)
- break;
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Retrying transfer", __func__);
- }
- if (status == TRANSFER_OK)
- Transfer_AddFileAttribute(transferSpan, block->data.fileName);
- if (status != TRANSFER_OK)
- transferError = "failed to fetch block";
- Telemetry_RecordTransferFetchDuration(Telemetry_GetMonotonicTimeSeconds() - transferStartedAt,
- (status == TRANSFER_OK) ? "success" : "failure",
- protocol,
- dispatcherLocality,
- context);
- if (status != TRANSFER_OK)
- Telemetry_RecordTransferFailure("fetch", protocol, "request_failed",
- dispatcherLocality, context);
- Telemetry_EndSpan(transferSpan, status == TRANSFER_OK, transferError);
- return status;
-}
-
-SO_PUBLIC bool
-Transfer_Prepare_File(struct Block *block, char *file, bool temp)
-{
- ASSERT(file != NULL);
- if (file == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: File is null", __func__);
- return false;
- }
- if ((block->data.file=fopen(file, "r")) == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to open file handle: %s, File: %s", __func__, strerror(errno), file);
- return false;
- }
- block->data.fileName = file;
- block->data.tempFile = temp;
-
-
-#ifdef _MSC_VER
-again:
- block->data.mfileHandle = CreateFileA(block->data.fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
- if (block->data.mfileHandle == NULL || block->data.mfileHandle == INVALID_HANDLE_VALUE)
- {
- if (GetLastError() == ERROR_SHARING_VIOLATION)
- {
- //rzb_log(LOG_ERR, "Sharing volation retry");
- goto again;
- }
-
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to create file handle: File: %s Error: %d", __func__, block->data.fileName, GetLastError());
- return false;
- }
- block->data.mapHandle = CreateFileMapping(block->data.mfileHandle, NULL, PAGE_READONLY, 0,0, NULL);
- if (block->data.mapHandle == NULL)
- {
- CloseHandle(block->data.mfileHandle);
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to create file mapping: %d", __func__, GetLastError());
- return false;
- }
- block->data.pointer = MapViewOfFile(block->data.mapHandle, FILE_MAP_READ, 0,0,0);
- if (block->data.pointer == NULL)
- {
- CloseHandle(block->data.mfileHandle);
- CloseHandle(block->data.mapHandle);
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to create map view", __func__);
- return false;
- }
-#else //_MSC_VER
- block->data.pointer = mmap (NULL, block->pId->iLength, PROT_READ, MAP_PRIVATE, fileno(block->data.file), 0);
- if (block->data.pointer == MAP_FAILED)
- {
- rzb_perror(LOG_C_TRANSFER,"%s");
- block->data.pointer = NULL;
- fclose(block->data.file);
- return false;
- }
-#endif //_MSC_VER
- return true;
-}
-
-void
-Transfer_Free(struct Block *block, struct ConnectedEntity *dispatcher)
-{
- //rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Called for %s", __func__, block->data.fileName);
- if (block->data.pointer != NULL)
- {
-#ifdef _MSC_VER
- UnmapViewOfFile(block->data.pointer);
- if (CloseHandle(block->data.mapHandle) == 0)
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Thread ID: %d Failed to close map handle: %d, File: %s", __func__, Thread_GetCurrent()->iThread, GetLastError(), block->data.fileName);
- if (CloseHandle(block->data.mfileHandle) == 0)
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Thread ID: %d Failed to close file handle: %d, File: %s", __func__, Thread_GetCurrent()->iThread, GetLastError(), block->data.fileName);
-
-#else //_MSC_VER
- munmap(block->data.pointer, block->pId->iLength);
-#endif
- }
-
- if (block->data.file != NULL)
- fclose(block->data.file);
-
- // If it was a temp file delete it
- if (block->data.tempFile)
- unlink(block->data.fileName);
-
- if (block->data.fileName != NULL)
- free(block->data.fileName);
-}
-
-static int
-Transport_Cmp(void *a, void *b)
-{
- struct TransportDescriptor *dA = a;
- struct TransportDescriptor *dB = b;
- if (dA == dB)
- return 0;
- return (dA->id - dB->id);
-}
-static int
-Transport_KeyCmp(void *a, const void *key)
-{
- struct TransportDescriptor *dA = a;
- const uint8_t *id = key;
- return (dA->id - *id);
-}
diff --git a/src/transfer/file.c b/src/transfer/file.c
deleted file mode 100644
index 27f8a48..0000000
--- a/src/transfer/file.c
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Copyright (c) 2011-2026 Cisco Systems, Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-#include "config.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#ifdef _MSC_VER
-#include
-#include
-#include "bobins.h"
-#else
-#include
-#endif
-#include
-
-#include "transfer/core.h"
-#include "runtime_config.h"
-
-static struct TransportDescriptor descriptor =
-{
- TRANSFER_MODE_FILE,
- "File",
- "Transfer file via shared file system",
- Transfer_File_Store,
- Transfer_File_Fetch
-};
-
-bool
-File_Init(void)
-{
- return Transport_Register(&descriptor);
-}
-
-static char * File_mkdir(const char *fmt, ...)
-{
- char *dir = NULL;
- va_list argp;
- va_start (argp, fmt);
- if (vasprintf (&dir, fmt, argp) == -1)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Could not allocate directory string",
- __func__);
- return NULL;
- }
-
- if (access (dir, F_OK) == -1)
- {
-#ifdef _MSC_VER
- if (_mkdir (dir) != 0)
-#else
- if (mkdir (dir, S_IRUSR | S_IWUSR | S_IXUSR |
- S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) == -1)
-#endif
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Error creating directory %s", __func__,
- dir);
- free (dir);
- return NULL;
- }
- }
- return dir;
-}
-static char *
-createDirectory(struct Block *block, const char *basepath)
-{
- char *hash;
- char *dir;
-
- hash = Transfer_generateFilename(block);
- if ((dir = File_mkdir("%s/%c", basepath, hash[0])) == NULL)
- goto createDone;
- else
- free(dir);
-
- if ((dir = File_mkdir("%s/%c/%c", basepath, hash[0], hash[1])) == NULL)
- goto createDone;
- else
- free(dir);
-
- if ((dir = File_mkdir("%s/%c/%c/%c", basepath, hash[0], hash[1], hash[2])) == NULL)
- goto createDone;
- else
- free(dir);
-
- dir = File_mkdir("%s/%c/%c/%c/%c", basepath, hash[0], hash[1], hash[2],
- hash[3]);
-
-createDone:
- free(hash);
-
- return dir;
-}
-
-static uint32_t
-writeWrap (int fd, uint8_t * data, uint64_t length)
-{
-
- int SizeDword;
- int totalbytes = 0;
- int bytessofar;
-
- SizeDword = (int) length;
-
- while (totalbytes < SizeDword)
- {
- bytessofar = write (fd, data + totalbytes, SizeDword - totalbytes);
- if (bytessofar == -1)
- {
- rzb_perror (LOG_C_TRANSFER,"writeWrap: Could not write data to file: %s");
- return 0;
- }
- totalbytes += bytessofar;
- }
-
- return 1;
-}
-
-SO_PUBLIC enum TransferStatus
-Transfer_File_Store(struct BlockPoolItem *item, struct ConnectedEntity *dispatcher)
-{
- int fd;
- char *filename =NULL;
- char *path = NULL;
- char *dir = NULL;
- struct BlockPoolData *dataItem = NULL;
- uint8_t data[4096];
- size_t len;
-
- ASSERT (item != NULL);
- if (item == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: item is NULL", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
-
- if ((filename = Transfer_generateFilename (item->pEvent->pBlock)) == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file name", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
- if ((dir = createDirectory(item->pEvent->pBlock, Config_getLocalityBlockStore())) == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to create storage dir", __func__);
- free(filename);
- return TRANSFER_FAIL_LOCAL;
- }
- if (asprintf(&path, "%s/%s", dir, filename) == -1)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file path", __func__);
- free(filename);
- free(dir);
- return TRANSFER_FAIL_LOCAL;
- }
- free(filename);
- free(dir);
- filename = NULL;
- dir = NULL;
-
-
- if ((fd = open(path, O_RDONLY, 0)) != -1)
- {
- close(fd);
- free(path);
- return TRANSFER_OK;
- }
- fd = open (path, O_RDWR | O_CREAT | O_TRUNC,
- S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
- if (fd == -1)
- {
- rzb_perror (LOG_C_TRANSFER,"StoreDataAsFile: Could not open file for writing: %s");
- free (path);
- return TRANSFER_FAIL_LOCAL;
- }
- free (path);
- path = NULL;
-
- dataItem = item->pDataHead;
- while (dataItem != NULL)
- {
- if (dataItem->iFlags == BLOCK_POOL_DATA_FLAG_FILE)
- {
- while((len = fread(data,1,4096, dataItem->data.file)) > 0)
- {
- if (writeWrap(fd,data,len) == 0)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Write failed.", __func__);
- close (fd);
- return TRANSFER_FAIL_LOCAL;
- }
- }
- rewind(dataItem->data.file);
-
- }
- else
- {
- if ((writeWrap (fd, dataItem->data.pointer, dataItem->iLength)) == 0)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Write failed.", __func__);
- close (fd);
- return TRANSFER_FAIL_LOCAL;
- }
- }
- dataItem = dataItem->pNext;
- }
-
- close (fd);
-
- return TRANSFER_OK;
-
-}
-
-SO_PUBLIC enum TransferStatus
-Transfer_File_Fetch(struct Block *block, struct ConnectedEntity *dispatcher)
-{
- int fd;
- char *filename = NULL;
- char *path = NULL;
- struct stat fs;
-#ifdef _MSC_VER
- char *tmp = NULL;
-#endif
-
- ASSERT (block != NULL);
- if (block == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: block is NULL", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
-
- if ((filename = Transfer_generateFilename (block)) == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file name", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
- if (asprintf(&path, "%s/%c/%c/%c/%c/%s", Config_getLocalityBlockStore(),
- filename[0], filename[1], filename[2], filename[3], filename) == -1)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file path", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
- free (filename); filename = NULL;
-
-#ifdef _MSC_VER
- while ((tmp = strchr( ((tmp == NULL) ? path : tmp), '/')) != NULL)
- *tmp = '\\';
-#endif
-
- fd = open (path, O_RDONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
- if (fd == -1)
- {
- rzb_perror
- (LOG_C_TRANSFER,"RetrieveDataAsFile: Could not open file for reading: %s");
- return TRANSFER_FAIL_LOCAL;
- }
-
- if (fstat (fd, &fs) == -1)
- {
- rzb_perror (LOG_C_TRANSFER,"RetrieveDataAsFile: Could not stat file: %s");
- close (fd);
- return TRANSFER_FAIL_LOCAL;
- }
- close(fd);
-
- return Transfer_Prepare_File(block, path, false) ? TRANSFER_OK : TRANSFER_FAIL_LOCAL;
-
-}
-
-bool
-File_Delete(struct Block *block)
-{
- char *filename = NULL;
- char *path = NULL;
-#ifdef _MSC_VER
- char *tmp = NULL;
-#endif
-
- ASSERT (block != NULL);
- if (block == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: block is NULL", __func__);
- return false;
- }
-
- if ((filename = Transfer_generateFilename (block)) == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file name", __func__);
- return false;
- }
- if (asprintf(&path, "%s/%c/%c/%c/%c/%s", Config_getLocalityBlockStore(),
- filename[0], filename[1], filename[2], filename[3], filename) == -1)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file path", __func__);
- return false;
- }
- free (filename); filename = NULL;
-
-#ifdef _MSC_VER
- while ((tmp = strchr( ((tmp == NULL) ? path : tmp), '/')) != NULL)
- *tmp = '\\';
-#endif
-
- if (remove(path) != 0)
- rzb_perror(LOG_C_TRANSFER,"File_Remove: failed to delete file: %s");
-
-
- return true;
-}
diff --git a/src/transfer/http.c b/src/transfer/http.c
deleted file mode 100644
index e913390..0000000
--- a/src/transfer/http.c
+++ /dev/null
@@ -1,873 +0,0 @@
-/*
- * Copyright (c) 2011-2026 Cisco Systems, Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-#include "config.h"
-#include
-#include
-#include
-
-#include
-#include
-#include
-#include
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "transfer/core.h"
-#include "runtime_config.h"
-#include "../telemetry.h"
-
-#ifndef P_tmpdir
-#define P_tmpdir "/tmp"
-#endif
-
-static bool sg_bSkipStore = false;
-
-static struct TransportDescriptor descriptorHttp = {
- TRANSFER_MODE_HTTP,
- "HTTP",
- "Transfer file via HTTP",
- Transfer_HTTP_Store,
- Transfer_HTTP_Fetch
-};
-
-static struct TransportDescriptor descriptorHttps = {
- TRANSFER_MODE_HTTPS,
- "HTTPS",
- "Transfer file via HTTPS",
- Transfer_HTTP_Store,
- Transfer_HTTP_Fetch
-};
-
-bool HTTP_Init(void)
-{
- const char *skip = getenv("RZB_SKIP_HTTP_STORE");
- if (skip != NULL)
- {
- if (strcmp(skip, "1") == 0 || strcasecmp(skip, "true") == 0)
- {
- sg_bSkipStore = true;
- rzb_log(LOG_INFO,LOG_C_TRANSFER, "%s: HTTP Store disabled via RZB_SKIP_HTTP_STORE", __func__);
- }
- }
- return Transport_Register(&descriptorHttp) && Transport_Register(&descriptorHttps);
-}
-
-struct StoreContext
-{
- struct BlockPoolItem *item;
- struct BlockPoolData *dataItem;
- size_t bytesRead;
- size_t bytesTransferred;
- char * filename;
- uint8_t protocol;
- uint16_t port;
- enum TransferStatus status;
- char * memory;
- size_t size;
-};
-
-static const char *
-HTTP_GetTempDirectory(void)
-{
- const char *path = Config_getLocalityBlockStore();
-
- if (path != NULL && path[0] != '\0' && !Config_isBlockStoreRemote()) {
- return path;
- }
-
- path = getenv("TMPDIR");
- if (path != NULL && path[0] != '\0') {
- return path;
- }
-
- return P_tmpdir;
-}
-
-static bool
-HTTP_GetProtocolSettings(uint8_t protocol, const char **scheme, bool *secure,
- const char *caller)
-{
- ASSERT(scheme != NULL);
- ASSERT(secure != NULL);
- if (scheme == NULL || secure == NULL) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: invalid protocol settings output", caller);
- return false;
- }
-
- switch (protocol) {
- case TRANSFER_MODE_HTTP:
- *scheme = "http";
- *secure = false;
- return true;
- case TRANSFER_MODE_HTTPS:
- *scheme = "https";
- *secure = true;
- return true;
- default:
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: unsupported HTTP transport protocol: %u",
- caller, protocol);
- return false;
- }
-}
-
-static bool
-HTTP_BuildURL(char **url, uint8_t protocol, const char *address, uint16_t port,
- const char *filename, const char *caller)
-{
- size_t filenameLength;
- const char *scheme;
- bool secure;
-
- ASSERT(url != NULL);
- ASSERT(address != NULL);
- ASSERT(filename != NULL);
- if (url == NULL || address == NULL || filename == NULL) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: invalid URL parameters", caller);
- return false;
- }
- if (!HTTP_GetProtocolSettings(protocol, &scheme, &secure, caller)) {
- return false;
- }
- (void)secure;
-
- filenameLength = strlen(filename);
- if (filenameLength < 4) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: invalid transfer filename", caller);
- return false;
- }
-
- if (asprintf(url, "%s://%s:%u/%c/%c/%c/%c/%s",
- scheme,
- address,
- port,
- filename[0],
- filename[1],
- filename[2],
- filename[3],
- filename) == -1) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: Failed to generate URL", caller);
- return false;
- }
-
- return true;
-}
-
-static bool
-HTTP_ConfigureCurl(CURL *curl, uint8_t protocol, const char *url)
-{
- bool secure;
- const char *scheme;
-
- ASSERT(curl != NULL);
- ASSERT(url != NULL);
- if (curl == NULL || url == NULL) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: invalid CURL parameters", __func__);
- return false;
- }
- if (!HTTP_GetProtocolSettings(protocol, &scheme, &secure, __func__)) {
- return false;
- }
-
- if (curl_easy_setopt(curl, CURLOPT_URL, url) != CURLE_OK) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: Failed to configure %s connection",
- __func__, scheme);
- return false;
- }
- if (secure &&
- ((curl_easy_setopt(curl, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2) != CURLE_OK) ||
- (curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L) != CURLE_OK) ||
- (curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L) != CURLE_OK))) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: Failed to configure %s connection",
- __func__, scheme);
- return false;
- }
-
- return true;
-}
-
-static void
-HTTP_AddCommonTelemetryAttributes(TelemetrySpan_t *span,
- const struct Block *block,
- uint8_t protocol,
- const char *address,
- uint16_t port,
- const char *url,
- const char *method)
-{
- const char *scheme = NULL;
- bool secure = false;
-
- if (span == NULL)
- return;
-
- if (block != NULL)
- Telemetry_AddBlockAttributes(span, block);
-
- if (method != NULL && method[0] != '\0')
- Telemetry_AddStringAttribute(span, "http.request.method", method);
-
- if (address != NULL && address[0] != '\0')
- Telemetry_AddStringAttribute(span, "server.address", address);
-
- Telemetry_AddIntAttribute(span, "server.port", (int64_t)port);
-
- if (url != NULL && url[0] != '\0')
- Telemetry_AddStringAttribute(span, "url.full", url);
-
- if (HTTP_GetProtocolSettings(protocol, &scheme, &secure, __func__))
- Telemetry_AddStringAttribute(span, "url.scheme", scheme);
-}
-
-static void
-HTTP_AddErrorTypeAttribute(TelemetrySpan_t *span,
- const char *errorType)
-{
- if (span == NULL || errorType == NULL || errorType[0] == '\0')
- return;
-
- Telemetry_AddStringAttribute(span, "error.type", errorType);
-}
-
-static void
-HTTP_AddStatusCodeErrorType(TelemetrySpan_t *span,
- long httpCode)
-{
- char errorType[32];
-
- if (span == NULL || httpCode <= 0)
- return;
-
- if (snprintf(errorType, sizeof(errorType), "%ld", httpCode) < 0)
- return;
-
- Telemetry_AddStringAttribute(span, "error.type", errorType);
-}
-
-static void
-HTTP_FreeRequestHeaders(struct curl_slist **requestHeaders)
-{
- if (requestHeaders == NULL || *requestHeaders == NULL)
- return;
-
- curl_slist_free_all(*requestHeaders);
- *requestHeaders = NULL;
-}
-
-static bool
-HTTP_ApplyTelemetryHeaders(CURL *curl,
- struct curl_slist **requestHeaders)
-{
- struct TelemetryInjectedHeaders injectedHeaders = { 0, NULL };
- size_t i;
-
- if (curl == NULL || requestHeaders == NULL)
- return false;
-
- if (!Telemetry_InjectCurrentContext(&injectedHeaders))
- return false;
-
- for (i = 0; i < injectedHeaders.count; ++i) {
- char *headerLine = NULL;
- struct curl_slist *nextHeaders;
-
- if (injectedHeaders.entries[i].name == NULL ||
- injectedHeaders.entries[i].value == NULL) {
- continue;
- }
-
- if (asprintf(&headerLine, "%s: %s",
- injectedHeaders.entries[i].name,
- injectedHeaders.entries[i].value) == -1) {
- Telemetry_FreeInjectedHeaders(&injectedHeaders);
- HTTP_FreeRequestHeaders(requestHeaders);
- return false;
- }
-
- nextHeaders = curl_slist_append(*requestHeaders, headerLine);
- free(headerLine);
- if (nextHeaders == NULL) {
- Telemetry_FreeInjectedHeaders(&injectedHeaders);
- HTTP_FreeRequestHeaders(requestHeaders);
- return false;
- }
-
- *requestHeaders = nextHeaders;
- }
-
- Telemetry_FreeInjectedHeaders(&injectedHeaders);
-
- if (*requestHeaders == NULL)
- return true;
-
- if (curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *requestHeaders) != CURLE_OK) {
- HTTP_FreeRequestHeaders(requestHeaders);
- return false;
- }
-
- return true;
-}
-
-static size_t
-read_callback(char *buffer, size_t size, size_t nitems, void *userdata) {
- struct StoreContext *context = (struct StoreContext *)userdata;
- //rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Reading %zu blocks of size %zu", __func__, nitems, size);
- if (context->dataItem == NULL) {
- // rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: No more data to read", __func__);
- return 0;
- }
- if (context->dataItem->iFlags == BLOCK_POOL_DATA_FLAG_FILE) {
- // If the data is a file, read from the file handle
- if (context->bytesRead + size * nitems > context->dataItem->iLength) {
- // If we are trying to read more data than is available in the file
- nitems = (context->dataItem->iLength - context->bytesRead);
- //rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Read too much data from file, read truncated: %zu", __func__, nitems);
- }
- {
- size_t itemsRead = fread(buffer, size, nitems,
- context->dataItem->data.file);
- size_t fileBytesRead = size * itemsRead;
-
- context->bytesRead += fileBytesRead;
- context->bytesTransferred += fileBytesRead;
- return itemsRead;
- }
- } else {
-
- size_t want = size * nitems;
- size_t read = 0;
- while (read < want ) {
- //rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Want %zu bytes, read %zu bytes", __func__, want, read);
- // If we have run out of buffers just return the requested size as to not read random data
- if (context->dataItem == NULL) {
- // rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: No more data to read", __func__);
- return read;
- }
- // How much data is left to read in the current buffer
- //rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Current buffer length %zu, bytes read %zu", __func__, context->dataItem->iLength, context->bytesRead);
- size_t avail = context->dataItem->iLength - context->bytesRead;
- size_t remain = want - read;
- // If more data is wanted that in the current buffer then only
- // copy the available data in the current buffer
- size_t to_read = avail < want ? avail : want;
- to_read = remain < to_read ? remain : to_read;
- //rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Reading %zu bytes from buffer - avail %zu - want %zu", __func__, to_read, avail, want);
- memcpy(buffer+read, context->dataItem->data.pointer + context->bytesRead, to_read);
- read += to_read;
- context->bytesRead += to_read;
- context->bytesTransferred += to_read;
- if (context->bytesRead == context->dataItem->iLength) {
- // If we have read all the data in the current buffer then
- // move to the next buffer
- context->dataItem = context->dataItem->pNext;
- context->bytesRead = 0;
- }
- }
- return read;
- }
-}
-
-
-static size_t
-WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp)
-{
- size_t realsize = size * nmemb;
- struct StoreContext *mem = (struct StoreContext *)userp;
-
- char *ptr = realloc(mem->memory, mem->size + realsize + 1);
- if (!ptr) {
- /* out of memory! */
- rzb_log(LOG_ERR,LOG_C_TRANSFER,"%s: not enough memory (realloc returned NULL)", __func__ );
- return 0;
- }
-
- mem->memory = ptr;
- memcpy(&(mem->memory[mem->size]), contents, realsize);
- mem->size += realsize;
- mem->memory[mem->size] = 0;
-
- return realsize;
-}
-
-static int
-HTTP_Try_Store(void *i, void*ud)
-{
- struct StoreContext *status = ud;
- char *address = i;
- char *url = NULL;
- CURL *curl = NULL;
- curl_mime *mime = NULL;
- curl_mimepart *part = NULL;
- struct curl_slist *requestHeaders = NULL;
- TelemetrySpan_t *requestSpan = NULL;
- CURLcode res;
- long http_code = 0;
- const char *requestError = NULL;
- bool spanSuccess = false;
-
- // Reset all the context states incase this is a retry
- status->dataItem = status->item->pDataHead;
- status->bytesRead = 0;
- status->bytesTransferred = 0;
- status->status = TRANSFER_FAIL_DISPATCHER;
- status->size = 0;
- if (status->memory != NULL) {
- status->memory[0] = '\0';
- }
- if (status->dataItem->iFlags == BLOCK_POOL_DATA_FLAG_FILE) {
- rewind(status->dataItem->data.file);
- }
-
- if (!HTTP_BuildURL(&url, status->protocol, address, status->port, status->filename,
- __func__)) {
- status->status = TRANSFER_FAIL_LOCAL;
- return LIST_EACH_OK;
- }
- requestSpan = Telemetry_StartSpanWithKind("POST",
- NULL,
- TELEMETRY_SPAN_KIND_CLIENT);
- HTTP_AddCommonTelemetryAttributes(
- requestSpan,
- (status->item != NULL && status->item->pEvent != NULL) ?
- status->item->pEvent->pBlock : NULL,
- status->protocol,
- address,
- status->port,
- url,
- "POST");
- Telemetry_AddIntAttribute(requestSpan, "rzb.transfer.bytes_expected",
- (int64_t)status->item->pEvent->pBlock->pId->iLength);
- rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Attempting to store %s at %s", __func__, status->filename, url);
- curl = curl_easy_init();
- if (curl == NULL) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to initialize curl", __func__);
- HTTP_AddErrorTypeAttribute(requestSpan, "curl.init");
- requestError = "failed to initialize curl";
- status->status = TRANSFER_FAIL_LOCAL;
- goto cleanup;
- }
- if (!HTTP_ApplyTelemetryHeaders(curl, &requestHeaders)) {
- rzb_log(LOG_WARNING, LOG_C_TRANSFER,
- "%s: Failed to apply telemetry HTTP headers", __func__);
- }
- mime = curl_mime_init(curl);
- if (mime == NULL) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to initialize curl MIME data", __func__);
- HTTP_AddErrorTypeAttribute(requestSpan, "curl.mime_init");
- requestError = "failed to initialize curl MIME data";
- status->status = TRANSFER_FAIL_LOCAL;
- goto cleanup;
- }
- part = curl_mime_addpart(mime);
- if (part == NULL) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to create curl MIME part", __func__);
- HTTP_AddErrorTypeAttribute(requestSpan, "curl.mime_addpart");
- requestError = "failed to create curl MIME part";
- status->status = TRANSFER_FAIL_LOCAL;
- goto cleanup;
- }
-
- if (!HTTP_ConfigureCurl(curl, status->protocol, url) ||
- (curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback) != CURLE_OK) ||
- (curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback) != CURLE_OK) ||
- (curl_easy_setopt(curl, CURLOPT_WRITEDATA, status) != CURLE_OK) ||
- (curl_mime_name(part, "file") != CURLE_OK) ||
- (curl_mime_filename(part, "file") != CURLE_OK) ||
- (curl_mime_type(part, "application/octet-stream") != CURLE_OK) ||
- (curl_mime_data_cb(part,
- status->item->pEvent->pBlock->pId->iLength,
- read_callback,
- NULL,
- NULL,
- status) != CURLE_OK) ||
- (curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime) != CURLE_OK)
- )
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to configure HTTP transport request",
- __func__);
- HTTP_AddErrorTypeAttribute(requestSpan, "curl.request_config");
- requestError = "failed to configure HTTP transport request";
- status->status = TRANSFER_FAIL_LOCAL;
- goto cleanup;
- }
-
- res = curl_easy_perform(curl);
- if(res != CURLE_OK) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: curl_easy_perform() failed: %s", __func__,
- curl_easy_strerror(res));
- HTTP_AddErrorTypeAttribute(requestSpan, "curl.perform");
- requestError = curl_easy_strerror(res);
- status->status = TRANSFER_FAIL_DISPATCHER;
- }
- if (curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &http_code) != CURLE_OK) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to get response code", __func__);
- HTTP_AddErrorTypeAttribute(requestSpan, "curl.response_code");
- requestError = "failed to get HTTP response code";
- status->status = TRANSFER_FAIL_LOCAL;
- }
-
-cleanup:
- if (http_code > 0)
- Telemetry_AddIntAttribute(requestSpan, "http.response.status_code",
- (int64_t)http_code);
- Telemetry_AddIntAttribute(requestSpan, "rzb.transfer.bytes_transferred",
- (int64_t)status->bytesTransferred);
- if (mime != NULL) {
- curl_mime_free(mime);
- }
- if (curl != NULL) {
- curl_easy_cleanup(curl);
- }
- HTTP_FreeRequestHeaders(&requestHeaders);
- free(url);
- // Rewind the filehandle after the request
- if (status->dataItem != NULL && status->dataItem->iFlags == BLOCK_POOL_DATA_FLAG_FILE)
- {
- rewind(status->dataItem->data.file);
- }
- if (status->status != TRANSFER_OK && status->status != TRANSFER_FAIL_DISPATCHER &&
- status->status != TRANSFER_FAIL_LOCAL) {
- status->status = TRANSFER_FAIL_LOCAL;
- }
- if (status->status == TRANSFER_FAIL_LOCAL) {
- if (requestError == NULL)
- requestError = "HTTP store request failed locally";
- Telemetry_EndSpan(requestSpan, false, requestError);
- return LIST_EACH_OK;
- }
- if (http_code != 200) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to store file: %zi", __func__, http_code);
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to store file: %s", __func__, status->memory);
- status->status = TRANSFER_FAIL_DISPATCHER;
- spanSuccess = (http_code >= 100 && http_code < 400);
- if (!spanSuccess)
- HTTP_AddStatusCodeErrorType(requestSpan, http_code);
- Telemetry_EndSpan(requestSpan, spanSuccess, spanSuccess ? NULL : requestError);
- return LIST_EACH_OK;
- }
-
- rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Successfully stored file", __func__);
- status->status = TRANSFER_OK;
- Telemetry_EndSpan(requestSpan, true, NULL);
- return LIST_EACH_END;
-}
-
-static int
-HTTP_Store(void *i, void*ud) {
- int try = 0;
- int max_tries = 10;
- struct StoreContext *status = ud;
- int res;
- for (try = 0; try < max_tries; try++) {
- res = HTTP_Try_Store(i, ud);
- if (status->status == TRANSFER_OK) {
- return res;
- }
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to store file, retrying %d/%d", __func__, try+1, max_tries);
- }
- return res;
-}
-
-SO_PUBLIC enum TransferStatus
-Transfer_HTTP_Store(struct BlockPoolItem *item, struct ConnectedEntity *dispatcher)
-{
- ASSERT(item != NULL);
- ASSERT(dispatcher != NULL);
- if (item == NULL || dispatcher == NULL) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: item or dispatcher is NULL", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
-
- struct StoreContext context = {
- .item = item,
- .dataItem = item->pDataHead,
- .bytesRead = 0,
- .bytesTransferred = 0,
- .filename = NULL,
- .protocol = dispatcher->dispatcher->protocol,
- .port = dispatcher->dispatcher->port,
- .status = TRANSFER_FAIL_LOCAL,
- .memory = NULL,
- .size = 0,
- };
- if (sg_bSkipStore) {
- return TRANSFER_OK;
- }
- if ((context.memory = calloc(1, sizeof(char))) == NULL) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: failed to allocate response buffer", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
-
- if ((context.filename = Transfer_generateFilename (item->pEvent->pBlock)) == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file name", __func__);
- free(context.memory);
- return TRANSFER_FAIL_LOCAL;
- }
- List_ForEach(dispatcher->dispatcher->addressList, HTTP_Store, &context);
- free(context.memory);
- free(context.filename);
- return context.status;
-}
-
-static const char * tempFileTemplate = "rzb-XXXXXX";
-struct FetchContext {
- struct Block *block;
- char * filename;
- char * tmpFileName;
- FILE * fd;
- uint8_t protocol;
- uint16_t port;
- enum TransferStatus status;
- size_t size;
- size_t expectedSize;
-};
-
-static bool
-HTTP_OpenFetchFile(struct FetchContext *context)
-{
- const char *tempDir;
- int fd;
-
- ASSERT(context != NULL);
- if (context == NULL) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: fetch context is NULL", __func__);
- return false;
- }
-
- tempDir = HTTP_GetTempDirectory();
- if (asprintf(&context->tmpFileName, "%s/%s", tempDir, tempFileTemplate) == -1) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: failed to allocate temp file template",
- __func__);
- context->tmpFileName = NULL;
- return false;
- }
-
- if ((fd = mkstemp(context->tmpFileName)) == -1) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: failed to create temp file: %s",
- __func__, strerror(errno));
- free(context->tmpFileName);
- context->tmpFileName = NULL;
- return false;
- }
-
- if ((context->fd = fdopen(fd, "w+b")) == NULL) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: failed to open temp file stream: %s",
- __func__, strerror(errno));
- close(fd);
- remove(context->tmpFileName);
- free(context->tmpFileName);
- context->tmpFileName = NULL;
- return false;
- }
-
- return true;
-}
-
-
-static size_t
-WriteFileCallback(void *contents, size_t size, size_t nmemb, void *userp)
-{
- struct FetchContext *req = (struct FetchContext *)userp;
- size_t written;
-
-
- written = fwrite(contents, size, nmemb, req->fd);
-
-
- req->size += written;
-
- return written;
-}
-
-static int
-HTTP_Try_Fetch(void *i, void*ud)
-{
- struct FetchContext *status = ud;
- char *address = i;
- char *url = NULL;
- CURL *curl = NULL;
- struct curl_slist *requestHeaders = NULL;
- TelemetrySpan_t *requestSpan = NULL;
- long http_code = 0;
- CURLcode res;
- const char *requestError = NULL;
- bool spanSuccess = false;
-
-
- rewind(status->fd);
- if (ftruncate(fileno(status->fd), 0) != 0) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER, "%s: Failed to truncate fetch file", __func__);
- status->status = TRANSFER_FAIL_LOCAL;
- return LIST_EACH_OK;
- }
- status->size = 0;
- status->status = TRANSFER_FAIL_DISPATCHER;
- if (!HTTP_BuildURL(&url, status->protocol, address, status->port, status->filename,
- __func__)) {
- status->status = TRANSFER_FAIL_LOCAL;
- return LIST_EACH_OK;
- }
- requestSpan = Telemetry_StartSpanWithKind("GET",
- NULL,
- TELEMETRY_SPAN_KIND_CLIENT);
- HTTP_AddCommonTelemetryAttributes(requestSpan,
- status->block,
- status->protocol,
- address,
- status->port,
- url,
- "GET");
- Telemetry_AddIntAttribute(requestSpan, "rzb.transfer.bytes_expected",
- (int64_t)status->expectedSize);
- rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Attempting to fetch %s from %s", __func__, status->filename, url);
- curl = curl_easy_init();
- if (curl == NULL) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to initialize curl", __func__);
- HTTP_AddErrorTypeAttribute(requestSpan, "curl.init");
- requestError = "failed to initialize curl";
- status->status = TRANSFER_FAIL_LOCAL;
- goto cleanup;
- }
- if (!HTTP_ApplyTelemetryHeaders(curl, &requestHeaders)) {
- rzb_log(LOG_WARNING, LOG_C_TRANSFER,
- "%s: Failed to apply telemetry HTTP headers", __func__);
- }
- if (!HTTP_ConfigureCurl(curl, status->protocol, url) ||
- (curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteFileCallback) != CURLE_OK) ||
- (curl_easy_setopt(curl, CURLOPT_WRITEDATA, status) != CURLE_OK)
- )
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to configure HTTP transport request",
- __func__);
- HTTP_AddErrorTypeAttribute(requestSpan, "curl.request_config");
- requestError = "failed to configure HTTP transport request";
- status->status = TRANSFER_FAIL_LOCAL;
- goto cleanup;
- }
-
- res = curl_easy_perform(curl);
- /* Check for errors */
- if(res != CURLE_OK) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: curl_easy_perform() failed: %s", __func__,
- curl_easy_strerror(res));
- HTTP_AddErrorTypeAttribute(requestSpan, "curl.perform");
- requestError = curl_easy_strerror(res);
- status->status = TRANSFER_FAIL_DISPATCHER;
- }
- if (curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &http_code) != CURLE_OK) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to get response code", __func__);
- HTTP_AddErrorTypeAttribute(requestSpan, "curl.response_code");
- requestError = "failed to get HTTP response code";
- status->status = TRANSFER_FAIL_LOCAL;
- }
-cleanup:
- if (http_code > 0)
- Telemetry_AddIntAttribute(requestSpan, "http.response.status_code",
- (int64_t)http_code);
- Telemetry_AddIntAttribute(requestSpan, "rzb.transfer.bytes_transferred",
- (int64_t)status->size);
- if (curl != NULL) {
- curl_easy_cleanup(curl);
- }
- HTTP_FreeRequestHeaders(&requestHeaders);
- free(url);
- if (status->status == TRANSFER_FAIL_LOCAL) {
- if (requestError == NULL)
- requestError = "HTTP fetch request failed locally";
- Telemetry_EndSpan(requestSpan, false, requestError);
- return LIST_EACH_OK;
- }
- if (http_code != 200) {
- status->status = TRANSFER_FAIL_DISPATCHER;
- spanSuccess = (http_code >= 100 && http_code < 400);
- if (!spanSuccess)
- HTTP_AddStatusCodeErrorType(requestSpan, http_code);
- Telemetry_EndSpan(requestSpan, spanSuccess, spanSuccess ? NULL : requestError);
- return LIST_EACH_OK;
- }
- if (status->size != status->expectedSize) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: File size mismatch, got %zu expected %zu", __func__, status->size, status->expectedSize);
- status->status = TRANSFER_FAIL_DISPATCHER;
- HTTP_AddErrorTypeAttribute(requestSpan, "http.response.body.size_mismatch");
- requestError = "fetched file size did not match expected size";
- Telemetry_EndSpan(requestSpan, false, requestError);
- return LIST_EACH_OK;
- }
- status->status = TRANSFER_OK;
- Telemetry_EndSpan(requestSpan, true, NULL);
- return LIST_EACH_END;
-}
-
-static int
-HTTP_Fetch(void *i, void*ud) {
- int try = 0;
- int max_tries = 10;
- struct FetchContext *status = ud;
- int res;
- for (try = 0; try < max_tries; try++) {
- res = HTTP_Try_Fetch(i, ud);
- if (status->status == TRANSFER_OK) {
- return res;
- }
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to fetch file, retrying %d/%d", __func__, try+1, max_tries);
- }
- return res;
-}
-
-SO_PUBLIC enum TransferStatus
-Transfer_HTTP_Fetch(struct Block *block, struct ConnectedEntity *dispatcher)
-{
- struct FetchContext context = {
- .block = block,
- .filename = NULL,
- .tmpFileName = NULL,
- .fd = NULL,
- .protocol = dispatcher->dispatcher->protocol,
- .port = dispatcher->dispatcher->port,
- .status = TRANSFER_FAIL_LOCAL,
- .size = 0,
- .expectedSize = block->pId->iLength,
- };
- if ((context.filename = Transfer_generateFilename (block)) == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file name", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
- if (!HTTP_OpenFetchFile(&context)) {
- free(context.filename);
- return TRANSFER_FAIL_LOCAL;
- }
- rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Storing file in: %s", __func__ , context.tmpFileName);
- List_ForEach(dispatcher->dispatcher->addressList, HTTP_Fetch, &context);
- fclose(context.fd);
- free(context.filename);
- if (context.status != TRANSFER_OK) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to fetch file", __func__);
- remove(context.tmpFileName);
- free(context.tmpFileName);
- return context.status;
- }
- return Transfer_Prepare_File(block, context.tmpFileName, true) ? TRANSFER_OK : TRANSFER_FAIL_LOCAL;
-}
diff --git a/src/transfer/ssh.c b/src/transfer/ssh.c
deleted file mode 100644
index 01871a4..0000000
--- a/src/transfer/ssh.c
+++ /dev/null
@@ -1,711 +0,0 @@
-/*
- * Copyright (c) 2011-2026 Cisco Systems, Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
- * MA 02110-1301, USA.
- */
-
-#include "config.h"
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-#include
-
-#include
-#include
-#ifdef _MSC_VER
-#include
-#include
-#include
-#include
-
-#include "bobins.h"
-#pragma comment(lib, "Shlwapi.lib")
-#define CREATE_MODE 755
-#else //_MSC_VER
-#include
-#include
-#define CREATE_MODE (S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)
-#define KNOWN_DISPATCHERS ETC_DIR "/known_dispatchers"
-#endif //_MSC_VER
-
-#ifndef MAX_PATH
-#define MAX_PATH 1024
-#endif
-
-#ifndef P_tmpdir
-#define P_tmpdir "/tmp"
-#endif
-
-#include
-#include
-
-#include "transfer/core.h"
-#include "runtime_config.h"
-
-
-
-static List_t *sessionList = NULL;
-
-static int SSH_Session_KeyCmp(void *a, const void *id);
-static int SSH_Session_Cmp(void *a, void *b);
-static struct SSH_Session * SSH_Get_Session(uuid_t nuggetId, struct ConnectedEntity *dispatcher);
-static bool SSH_Check_Session(struct SSH_Session *session);
-static bool SSH_Verify_Dispatcher(ssh_session session);
-static char * SSH_GetKnownDispatchers(void);
-
-static struct TransportDescriptor descriptor =
-{
- TRANSFER_MODE_SSH,
- "SSH",
- "Transfer file via SSH (sftp)",
- Transfer_SSH_Store,
- Transfer_SSH_Fetch
-};
-
-struct SSH_Session_Key
-{
- uuid_t nuggetId;
- uuid_t dispatcherId;
- rzb_thread_t threadId;
-};
-
-struct SSH_Session
-{
- struct SSH_Session_Key key;
- struct ConnectedEntity *dispatcher;
- ssh_session ssh;
- sftp_session sftp;
- char *hostname;
-};
-
-bool
-SSH_Init(void)
-{
- sessionList = List_Create(LIST_MODE_GENERIC,
- SSH_Session_Cmp, //Cmp
- SSH_Session_KeyCmp, //KeyCmp
- NULL, //Destroy
- NULL, //Clone,
- NULL, //Lock,
- NULL); //Unlock
- if (sessionList == NULL)
- return false;
-
- return Transport_Register(&descriptor);
-}
-
-static char *
-SSH_mkdir(struct SSH_Session *session, const char *fmt, ...)
-{
- char *dir = NULL;
- va_list argp;
- sftp_dir sdir;
- va_start (argp, fmt);
- if (vasprintf (&dir, fmt, argp) == -1)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Could not allocate directory string",
- __func__);
- return NULL;
- }
-
- if ((sdir = sftp_opendir(session->sftp, dir)) == NULL)
- {
- if (sftp_mkdir (session->sftp, dir, CREATE_MODE) == -1)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Error creating directory %s", __func__,
- dir);
- free (dir);
- return NULL;
- }
- }
- else
- sftp_closedir(sdir);
-
- return dir;
-}
-
-static char *
-createDirectory(struct Block *block, struct SSH_Session *session)
-{
- char *hash;
- char *dir, *cdir;
- cdir = sftp_canonicalize_path(session->sftp, ".");
- hash = Transfer_generateFilename(block);
-
- if ((dir =SSH_mkdir(session, "%s/%c", cdir, hash[0])) == NULL)
- goto createDone;
- else
- free(dir);
-
- if ((dir =SSH_mkdir(session, "%s/%c/%c", cdir, hash[0], hash[1])) == NULL)
- goto createDone;
- else
- free(dir);
-
- if ((dir =SSH_mkdir(session, "%s/%c/%c/%c", cdir, hash[0], hash[1], hash[2])) == NULL)
- goto createDone;
- else
- free(dir);
-
- dir =SSH_mkdir(session, "%s/%c/%c/%c/%c", cdir, hash[0], hash[1], hash[2],hash[3]);
-
-createDone:
- free(hash);
- free(cdir);
- return dir;
-}
-
-static uint32_t
-writeWrap (sftp_file fd, uint8_t * data, uint64_t length)
-{
-
- ssize_t size;
- ssize_t totalbytes = 0;
- ssize_t bytessofar;
-
- size = length;
-
- while (totalbytes < size)
- {
- bytessofar = sftp_write (fd, data + totalbytes, size - totalbytes);
- if (bytessofar < 0)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Could not write data to file", __func__);
- return 0;
- }
- totalbytes += bytessofar;
- }
-
- return 1;
-}
-
-SO_PUBLIC enum TransferStatus
-Transfer_SSH_Store(struct BlockPoolItem *item, struct ConnectedEntity *dispatcher)
-{
- struct RazorbackContext *ctx;
- struct SSH_Session *session;
- char *filename =NULL;
- char *path = NULL;
- char *fullpath = NULL;
- sftp_file fd;
- struct BlockPoolData *dataItem = NULL;
- uint8_t data[4096];
- size_t len;
-
- ASSERT (item != NULL);
- if (item == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: item is NULL", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
-
- ctx = Thread_GetContext(Thread_GetCurrent());
- if (ctx == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to lookup thread context", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
- session = SSH_Get_Session(ctx->uuidNuggetId, dispatcher);
- if (session == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to lookup context protocol session", __func__);
- return TRANSFER_FAIL_DISPATCHER;
- }
- if (!SSH_Check_Session(session))
- return TRANSFER_FAIL_DISPATCHER;
-
-
- if ((filename = Transfer_generateFilename (item->pEvent->pBlock)) == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file name", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
- if (( path = createDirectory(item->pEvent->pBlock, session)) == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to create storage dir", __func__);
- free (filename);
- return TRANSFER_FAIL_DISPATCHER;
- }
- if (asprintf(&fullpath, "%s/%s", path, filename) == -1)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file path", __func__);
- free(path);
- free (filename);
- return TRANSFER_FAIL_LOCAL;
- }
- // Check if its there already.
- //
- fd = sftp_open (session->sftp, fullpath, O_RDONLY,0);
- if (fd != NULL)
- {
- sftp_close(fd);
- free(fullpath);
- free(path);
- free(filename);
- return TRANSFER_OK;
- }
-
- fd = sftp_open (session->sftp, fullpath, O_RDWR | O_CREAT | O_TRUNC,
- S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
- if (fd == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Could not open file for writing: %s", __func__, ssh_get_error(session->ssh));
- free (filename);
- return TRANSFER_FAIL_DISPATCHER;
- }
- dataItem = item->pDataHead;
- while (dataItem != NULL)
- {
- if (dataItem->iFlags == BLOCK_POOL_DATA_FLAG_FILE)
- {
- while((len = fread(data,1,4096, dataItem->data.file)) > 0)
- {
- if (writeWrap(fd,data,len) == 0)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Write failed.", __func__);
- free(fullpath);
- free(path);
- free (filename);
- sftp_close (fd);
- return TRANSFER_FAIL_DISPATCHER;
- }
- }
- rewind(dataItem->data.file);
- }
- else
- {
- if ((writeWrap (fd, dataItem->data.pointer, dataItem->iLength)) == 0)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: Write failed.", __func__);
- sftp_close (fd);
- free(fullpath);
- free(path);
- free (filename);
- return TRANSFER_FAIL_DISPATCHER;
- }
- }
- dataItem = dataItem->pNext;
- }
-
- sftp_close (fd);
-
- free (filename);
- free (path);
- free(fullpath);
-
- return TRANSFER_OK;
-}
-
-SO_PUBLIC enum TransferStatus
-Transfer_SSH_Fetch(struct Block *block, struct ConnectedEntity *dispatcher)
-{
- struct RazorbackContext *ctx;
- struct SSH_Session *session;
- sftp_file fd;
- char *filename = NULL;
- char *path = NULL;
- char *fullpath = NULL;
- char * tmp_string; // Temporary string to use for path to tmpfile
-#ifdef _MSC_VER
- char lpTempPathBuffer[MAX_PATH];
-#else
- const char *tmpDir;
- int tmp_fd;
-#endif
- FILE *out_file; // Output stream to create a temporary file on tmpfs
- ssize_t read =0;
- ssize_t got =0;
- char buf[1024];
-
- ASSERT (block != NULL);
- if (block == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: block is NULL", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
-
- ctx = Thread_GetContext(Thread_GetCurrent());
- if (ctx == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to lookup thread context", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
- session = SSH_Get_Session(ctx->uuidNuggetId, dispatcher);
- if (session == NULL) {
- rzb_log(LOG_ERR, LOG_C_TRANSFER,"%s: Failed to lookup context protocol session", __func__);
- return TRANSFER_FAIL_DISPATCHER;
- }
- if (!SSH_Check_Session(session))
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Check Session failed", __func__);
- return TRANSFER_FAIL_DISPATCHER;
- }
-
- if ((filename = Transfer_generateFilename (block)) == NULL)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file name", __func__);
- return TRANSFER_FAIL_LOCAL;
- }
-
- if ((path = sftp_canonicalize_path(session->sftp, ".")) == NULL)
- {
- free(filename);
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to canonicalize path on server", __func__);
- return TRANSFER_FAIL_DISPATCHER;
- }
- if (asprintf(&fullpath, "%s/%c/%c/%c/%c/%s", path,
- filename[0], filename[1], filename[2], filename[3], filename) == -1)
- {
- rzb_log (LOG_ERR,LOG_C_TRANSFER, "%s: failed to generate file path", __func__);
- free(path);
- free(filename);
- return TRANSFER_FAIL_LOCAL;
- }
-
- fd = sftp_open (session->sftp, fullpath, O_RDONLY, 0);
-
- free (filename); filename = NULL;
- free (path); path = NULL;
- free (fullpath); fullpath = NULL;
-
- if (fd == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Could not open file for reading: %s", __func__, ssh_get_error(session->ssh));
- return TRANSFER_FAIL_DISPATCHER;
- }
-
-#ifdef _MSC_VER
- if ((tmp_string = calloc(1,MAX_PATH)) == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to allocate path", __func__);
- sftp_close(fd);
- return TRANSFER_FAIL_LOCAL;
- }
- tmp_string[0] = 0;
- GetTempPathA(MAX_PATH, lpTempPathBuffer);
-
- if (GetTempFileNameA(lpTempPathBuffer, "block", 0, tmp_string) == 0)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Cannot create temporary file name: %s, error: %s", __func__, tmp_string, strerror(errno));
- sftp_close(fd);
- free(tmp_string);
- return TRANSFER_FAIL_LOCAL;
- }
-#else
- tmpDir = getenv("TMPDIR");
- if (tmpDir == NULL || tmpDir[0] == '\0')
- {
- tmpDir = P_tmpdir;
- }
-
- if (asprintf(&tmp_string, "%s/%s", tmpDir, "razorback-ssh-XXXXXX") == -1)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to allocate temporary file path", __func__);
- sftp_close(fd);
- return TRANSFER_FAIL_LOCAL;
- }
- if ((tmp_fd = mkstemp(tmp_string)) == -1)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Cannot create temporary file name: %s, error: %s", __func__, tmp_string, strerror(errno));
- sftp_close(fd);
- free(tmp_string);
- return TRANSFER_FAIL_LOCAL;
- }
- if ((out_file = fdopen(tmp_fd, "wb")) == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Cannot create temporary file: %s, error: %s", __func__, tmp_string, strerror(errno));
- close(tmp_fd);
- sftp_close(fd);
- remove(tmp_string);
- free(tmp_string);
- return TRANSFER_FAIL_LOCAL;
- }
-#endif
- //rzb_log(LOG_DEBUG,LOG_C_TRANSFER, "%s: Thread ID: %d FileName: %s:", __func__, Thread_GetCurrent()->iThread, tmp_string);
- // Create tmpfile
-#ifdef _MSC_VER
- if ((out_file = fopen (tmp_string, "wb")) == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Cannot create temporary file: %s, error: %s", __func__, tmp_string, strerror(errno));
- sftp_close(fd);
- free(tmp_string);
- return TRANSFER_FAIL_LOCAL;
- }
-#endif
-
- while ((uint64_t)read < block->pId->iLength)
- {
- got = sftp_read(fd, buf, 1024);
- if (got < 0)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to read: %s", __func__, ssh_get_error(session->ssh));
- sftp_close(fd);
- fclose(out_file);
- remove(tmp_string);
- free(tmp_string);
- return TRANSFER_FAIL_DISPATCHER;
- }
- if (got == 0)
- break;
- fwrite(buf, 1, got, out_file);
- read += got;
- }
- if ((uint64_t)read != block->pId->iLength)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to read full data block", __func__);
- sftp_close (fd);
- fclose(out_file);
- remove(tmp_string);
- free(tmp_string);
- return TRANSFER_FAIL_DISPATCHER;
- }
- sftp_close (fd);
- fflush(out_file);
- fclose(out_file);
- // SSH only creates temp files.
- return Transfer_Prepare_File(block, tmp_string, true) ? TRANSFER_OK : TRANSFER_FAIL_LOCAL;
-}
-
-
-static int SSH_Session_KeyCmp(void *a, const void *id)
-{
- struct SSH_Session *ses = a;
- const struct SSH_Session_Key *key = id;
- if ((uuid_compare(ses->key.nuggetId, key->nuggetId) == 0) &&
- (uuid_compare(ses->key.dispatcherId, key->dispatcherId) == 0) &&
- (ses->key.threadId == key->threadId))
- return 0;
- return -1;
-}
-
-static int SSH_Session_Cmp(void *a, void *b)
-{
- struct SSH_Session *sesA = a;
- struct SSH_Session *sesB = b;
- if (a==b)
- return 0;
-
- if ((uuid_compare(sesA->key.nuggetId, sesB->key.nuggetId) == 0) &&
- (uuid_compare(sesA->key.dispatcherId, sesB->key.dispatcherId) == 0) &&
- (sesA->key.threadId == sesB->key.threadId))
- return 0;
- return -1;
-}
-
-static struct SSH_Session *
-SSH_Get_Session(uuid_t nuggetId, struct ConnectedEntity *dispatcher)
-{
- struct SSH_Session *session;
- struct SSH_Session_Key key;
- uuid_copy(key.nuggetId, nuggetId);
- uuid_copy(key.dispatcherId, dispatcher->uuidNuggetId);
- key.threadId = Thread_GetCurrentId();
-
- session = List_Find(sessionList, &key);
- if (session != NULL)
- return session;
-
- if ((session = calloc(1,sizeof(struct SSH_Session))) == NULL)
- return NULL;
-
- uuid_copy(session->key.nuggetId, nuggetId);
- uuid_copy(session->key.dispatcherId, dispatcher->uuidNuggetId);
- session->key.threadId = key.threadId;
- session->dispatcher=dispatcher;
- //session.context
- List_Push(sessionList, session);
- return session;
-}
-
-static bool
-SSH_Verify_Dispatcher(ssh_session session)
-{
- int state, key_error;
- size_t hlen;
- unsigned char *hash = NULL;
- char *hexa;
-
-
- state = ssh_session_is_known_server(session);
- ssh_key key;
- ssh_get_server_publickey(session,&key);
- key_error = ssh_get_publickey_hash(key, SSH_PUBLICKEY_HASH_SHA1, &hash, &hlen);
-
- ssh_key_free(key);
- if (key_error < 0)
- return false;
-
- switch (state)
- {
- case SSH_SERVER_KNOWN_OK:
- break; /* ok */
-
- case SSH_SERVER_KNOWN_CHANGED:
- hexa = ssh_get_hexa(hash, hlen);
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Host key for server changed. For security reasons, connection will be stopped. New key: %s", __func__, hexa);
- free(hexa);
- free(hash);
- return false;
-
- case SSH_SERVER_FOUND_OTHER:
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: The host key for this server was not found but an other"
- "type of key exists. An attacker might change the default server key to"
- "confuse your client into thinking the key does not exist", __func__);
- free(hash);
- return false;
-
- case SSH_SERVER_FILE_NOT_FOUND:
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Could not find known host file, it will be automatically created.", __func__);
- /* fallback to SSH_SERVER_NOT_KNOWN behavior */
- break;
- case SSH_SERVER_NOT_KNOWN:
- hexa = ssh_get_hexa(hash, hlen);
- rzb_log(LOG_ERR,LOG_C_TRANSFER,"%s The server is unknown. Adding the key: %s", __func__, hexa);
- free(hexa);
- if (ssh_session_update_known_hosts(session) < 0)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: %s", __func__, strerror(errno));
- free(hash);
- return false;
- }
- break;
-
- case SSH_SERVER_ERROR:
- fprintf(stderr, "Error %s", ssh_get_error(session));
- free(hash);
- return false;
- }
-
- free(hash);
- return true;
-}
-struct ConnectionStatus {
- struct SSH_Session * session;
- bool connected;
-};
-static int
-SSH_Connect_Address(void *i, void*ud)
-{
- struct ConnectionStatus *status = ud;
- char *address = i;
- ssh_options_set(status->session->ssh, SSH_OPTIONS_HOST, address);
- if (ssh_connect(status->session->ssh) != SSH_OK) {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to connect session (%s)", __func__, address);
- } else {
- status->connected = true;
- return LIST_EACH_END;
- }
- return LIST_EACH_OK;
-}
-static bool
-SSH_Check_Session(struct SSH_Session *session)
-{
- char user[UUID_STRING_LENGTH];
- ASSERT(session != NULL);
- if (session == NULL)
- return false;
- struct ConnectionStatus status = {session, false};
-
- // New connection
- if (session->ssh == NULL)
- {
- if ((session->ssh = ssh_new()) == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to allocate ssh session", __func__);
- return false;
- }
- uuid_unparse(session->key.nuggetId, user);
- ssh_options_set(session->ssh, SSH_OPTIONS_PORT, &session->dispatcher->dispatcher->port);
- ssh_options_set(session->ssh, SSH_OPTIONS_USER, user);
- ssh_options_set(session->ssh, SSH_OPTIONS_KNOWNHOSTS,SSH_GetKnownDispatchers());
- List_ForEach(session->dispatcher->dispatcher->addressList, SSH_Connect_Address, &status);
-
- if (!status.connected)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to connected to dispatcher", __func__);
- ssh_disconnect(session->ssh);
- ssh_free(session->ssh);
- session->ssh = NULL;
- return false;
- }
-
- if (!SSH_Verify_Dispatcher(session->ssh))
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to verify dispatcher", __func__);
- ssh_disconnect(session->ssh);
- ssh_free(session->ssh);
- session->ssh = NULL;
- return false;
- }
- if (ssh_userauth_password(session->ssh, NULL, Razorback_Get_Transfer_Password()) != SSH_AUTH_SUCCESS)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to authenticate: %s", __func__, ssh_get_error(session->ssh));
- ssh_disconnect(session->ssh);
- ssh_free(session->ssh);
- session->ssh = NULL;
- return false;
- }
- if ((session->sftp = sftp_new(session->ssh)) == NULL)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to create sftp session: %s", __func__, ssh_get_error(session->ssh));
- ssh_disconnect(session->ssh);
- ssh_free(session->ssh);
- session->ssh = NULL;
- return false;
- }
- if (sftp_init(session->sftp) != 0)
- {
- rzb_log(LOG_ERR,LOG_C_TRANSFER, "%s: Failed to init sftp session: %s", __func__, ssh_get_error(session->ssh));
- sftp_free(session->sftp);
- ssh_disconnect(session->ssh);
- ssh_free(session->ssh);
-
- session->sftp = NULL;
- session->ssh = NULL;
- return false;
- }
- }
- return true;
-}
-
-#ifdef _MSC_VER
-static char * SSH_GetKnownDispatchers(void)
-{
- char *path;
- if ((path = calloc(MAX_PATH, sizeof(char)))== NULL)
- return NULL;
-
- SHGetFolderPathA(NULL,
- CSIDL_COMMON_APPDATA|CSIDL_FLAG_CREATE, NULL, 0, path);
- PathAppendA(path, "Sourcefire Inc");
- CreateDirectoryA(path, NULL);
- PathAppendA(path, "Razorback");
- CreateDirectoryA(path, NULL);
- PathAppendA(path, "known_dispatchers");
- //rzb_log(LOG_ERR, LOG_C_TRANSFER,"PATH: %s",path);
- return path;
-}
-#else //_MSC_VER
-static char * SSH_GetKnownDispatchers(void)
-{
- return (char *)KNOWN_DISPATCHERS;
-}
-#endif //_MSC_VER
diff --git a/src/uuids.c b/src/uuids.c
index 695517c..3ce6f4c 100644
--- a/src/uuids.c
+++ b/src/uuids.c
@@ -24,6 +24,9 @@
#include
#include
+#ifdef BUILDING_SO
+#include "dev_mode.h"
+#endif
#include "init.h"
#include
@@ -64,6 +67,16 @@ static List_t *sg_NtlvNameList;
static List_t *sg_NuggetList;
static List_t *sg_NuggetTypeList;
+static bool
+UUID_IsDevModeEnabled(void)
+{
+#ifdef BUILDING_SO
+ return Razorback_DevMode_IsEnabled();
+#else
+ return false;
+#endif
+}
+
SO_PUBLIC bool
UUID_Add_List_Entry (List_t *list, uuid_t p_uuid,
const char *p_sName, const char *p_sDescr)
@@ -141,6 +154,35 @@ UUID_getNodeByName (const char *p_sName, int p_iType)
return (struct UUIDListNode *)List_Find(list, &key);
}
+static struct UUIDListNode *
+UUID_getOrCreateDevNodeByName(const char *p_sName, int p_iType)
+{
+ struct UUIDListNode *node;
+ List_t *list;
+ uuid_t generated;
+
+ node = UUID_getNodeByName(p_sName, p_iType);
+ if (node != NULL)
+ return node;
+
+ if (!UUID_IsDevModeEnabled())
+ return NULL;
+
+ list = UUID_Get_List(p_iType);
+ if (list == NULL)
+ return NULL;
+
+ uuid_generate_random(generated);
+ if (!UUID_Add_List_Entry(list, generated, p_sName, NULL))
+ return NULL;
+
+ rzb_log(LOG_DEBUG, LOG_C_CORE,
+ "%s: Registered dev-mode UUID mapping for type %d name '%s'",
+ __func__, p_iType, p_sName);
+
+ return UUID_getNodeByName(p_sName, p_iType);
+}
+
static struct UUIDListNode *
UUID_getNodeByUUID (uuid_t p_uuid, int p_iType)
@@ -189,7 +231,7 @@ UUID_Get_UUID (const char *p_sName, int p_iType, uuid_t r_uuid)
{
struct UUIDListNode *l_pListNode;
- if ((l_pListNode = UUID_getNodeByName (p_sName, p_iType)) == NULL)
+ if ((l_pListNode = UUID_getOrCreateDevNodeByName(p_sName, p_iType)) == NULL)
{
return false;
}
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 677e7c7..42d97b7 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -15,23 +15,65 @@
# MA 02110-1301, USA.
#noinst_PROGRAMS= configTest logTest daemonTest
-noinst_PROGRAMS= logTest daemonTest configTest
+noinst_PROGRAMS= logTest daemonTest configTest smoke_source_next smoke_inspector_next \
+ smoke_operational_next
configTest_SOURCES = configTest.c
-configTest_LDADD = -L../src -lrazorback_api @LIBS@
+configTest_LDADD = -L../src -lrazorback_api @OPENTELEMETRY_CPP_LIBS@ @LIBS@
logTest_SOURCES = logTest.c
-logTest_LDADD = -L../src -lrazorback_api @LIBS@
+logTest_LDADD = -L../src -lrazorback_api @OPENTELEMETRY_CPP_LIBS@ @LIBS@
#mqTest_SOURCES = mqTest.c
#mqTest_LDADD = -L../src -lrazorback_api
daemonTest_SOURCES = daemonTest.c
-daemonTest_LDADD = -L../src -lrazorback_api @LIBS@
+daemonTest_LDADD = -L../src -lrazorback_api @OPENTELEMETRY_CPP_LIBS@ @LIBS@
+
+SMOKE_NEXT_CPPFLAGS = $(AM_CPPFLAGS) \
+ -DRAZORBACK_SCHEMA_FIXTURE_ROOT=\"$(abs_top_srcdir)/schemas/razorback/fixtures\"
+
+smoke_source_next_SOURCES = smoke_source_next.c test_telemetry_stubs.c \
+ ../src/runtime_next.c \
+ ../src/messages_next.c ../src/message_body.c
+smoke_source_next_CPPFLAGS = $(SMOKE_NEXT_CPPFLAGS)
+smoke_source_next_LDADD = @LIBS@
+
+smoke_inspector_next_SOURCES = smoke_inspector_next.c test_telemetry_stubs.c \
+ ../src/runtime_next.c \
+ ../src/messages_next.c ../src/message_body.c
+smoke_inspector_next_CPPFLAGS = $(SMOKE_NEXT_CPPFLAGS)
+smoke_inspector_next_LDADD = @LIBS@
+
+smoke_operational_next_SOURCES = smoke_operational_next.c test_telemetry_stubs.c \
+ ../src/runtime_next.c \
+ ../src/messages_next.c ../src/message_body.c
+smoke_operational_next_CPPFLAGS = $(SMOKE_NEXT_CPPFLAGS)
+smoke_operational_next_LDADD = @LIBS@
+
+.PHONY: smoke-dispatcher-next
+
+PYTHON ?= python3
+RZB_SMOKE_OPERATION_REPORT ?= .smoke-operation.json
+SMOKE_COMPOSE ?= docker compose --env-file ../../deployment/.env -f ../../deployment/compose/core/compose.yaml -f ../../deployment/compose/razorback/compose.yaml -f ../../deployment/compose/razorback/smoke.yaml
+SMOKE_OPERATION_REPORT_CONTAINER ?= /tmp/razorback-smoke-operation.json
+
+smoke-dispatcher-next: smoke_source_next smoke_inspector_next smoke_operational_next
+ if test "$$RZB_SMOKE_LIVE" = "1"; then \
+ RZB_SMOKE_OPERATION_REPORT="$(RZB_SMOKE_OPERATION_REPORT)" ./smoke_operational_next || exit $$?; \
+ if test "$$RZB_SMOKE_ASSERT_KAFKA" != "0"; then \
+ $(SMOKE_COMPOSE) run --rm --no-deps --build --entrypoint python -v "$(abspath $(RZB_SMOKE_OPERATION_REPORT)):$(SMOKE_OPERATION_REPORT_CONTAINER):ro" smoke /smoke/search_export_probe.py --report "$(SMOKE_OPERATION_REPORT_CONTAINER)"; \
+ fi; \
+ else \
+ ./smoke_source_next; \
+ ./smoke_inspector_next; \
+ fi
if HAVE_CHECK
check_PROGRAMS = test_hash test_block_id test_list test_string_list \
- test_uuids test_config_file test_timer test_health
+ test_uuids test_config_file test_timer test_health test_message_body \
+ test_messages_next test_config_next test_schema_conformance test_runtime_next \
+ test_fileserver
TESTS = $(check_PROGRAMS)
TESTS_DIR_CPPFLAGS = $(AM_CPPFLAGS) \
@@ -41,6 +83,11 @@ test_hash_SOURCES = test_hash.c test_support.c ../src/hash.c
test_hash_CFLAGS = $(CHECK_CFLAGS)
test_hash_LDADD = $(CHECK_LIBS) @LIBS@
+test_fileserver_SOURCES = test_fileserver.c test_support.c ../src/hash.c \
+ ../src/fileserver.c
+test_fileserver_CFLAGS = $(CHECK_CFLAGS)
+test_fileserver_LDADD = $(CHECK_LIBS) @LIBS@
+
test_block_id_SOURCES = test_block_id.c test_support.c ../src/hash.c ../src/block_id.c
test_block_id_CFLAGS = $(CHECK_CFLAGS)
test_block_id_LDADD = $(CHECK_LIBS) @LIBS@
@@ -71,6 +118,42 @@ test_health_SOURCES = test_health.c test_runtime_support.c \
test_health_CFLAGS = $(CHECK_CFLAGS)
test_health_LDADD = $(CHECK_LIBS) @LIBS@
+MESSAGE_BODY_TEST_CPPFLAGS = $(AM_CPPFLAGS) \
+ -DRAZORBACK_SCHEMA_FIXTURE_DIR=\"$(abs_top_srcdir)/schemas/razorback/fixtures/messages\"
+
+test_message_body_SOURCES = test_message_body.c ../src/message_body.c
+test_message_body_CFLAGS = $(CHECK_CFLAGS)
+test_message_body_CPPFLAGS = $(MESSAGE_BODY_TEST_CPPFLAGS)
+test_message_body_LDADD = $(CHECK_LIBS) @LIBS@
+
+test_config_next_SOURCES = test_config_next.c ../src/config_next.c
+test_config_next_CFLAGS = $(CHECK_CFLAGS)
+test_config_next_LDADD = $(CHECK_LIBS) @LIBS@
+
+SCHEMA_CONFORMANCE_TEST_CPPFLAGS = $(AM_CPPFLAGS) \
+ -DRAZORBACK_REPO_ROOT=\"$(abs_top_srcdir)\"
+
+test_schema_conformance_SOURCES = test_schema_conformance.c
+test_schema_conformance_CFLAGS = $(CHECK_CFLAGS)
+test_schema_conformance_CPPFLAGS = $(SCHEMA_CONFORMANCE_TEST_CPPFLAGS)
+test_schema_conformance_LDADD = $(CHECK_LIBS) @LIBS@
+
+MESSAGES_NEXT_TEST_CPPFLAGS = $(AM_CPPFLAGS) \
+ -DRAZORBACK_SCHEMA_FIXTURE_ROOT=\"$(abs_top_srcdir)/schemas/razorback/fixtures\"
+
+test_messages_next_SOURCES = test_messages_next.c ../src/messages_next.c \
+ ../src/message_body.c
+test_messages_next_CFLAGS = $(CHECK_CFLAGS)
+test_messages_next_CPPFLAGS = $(MESSAGES_NEXT_TEST_CPPFLAGS)
+test_messages_next_LDADD = $(CHECK_LIBS) @LIBS@
+
+test_runtime_next_SOURCES = test_runtime_next.c test_telemetry_stubs.c \
+ ../src/runtime_next.c \
+ ../src/messages_next.c ../src/message_body.c
+test_runtime_next_CFLAGS = $(CHECK_CFLAGS)
+test_runtime_next_CPPFLAGS = $(MESSAGES_NEXT_TEST_CPPFLAGS)
+test_runtime_next_LDADD = $(CHECK_LIBS) @LIBS@
+
if HAVE_JV
check_PROGRAMS += test_json_buffer_primitives test_json_buffer_structs \
test_json_buffer_invalid test_messages_roundtrip
diff --git a/tests/smoke_inspector_next.c b/tests/smoke_inspector_next.c
new file mode 100644
index 0000000..1977881
--- /dev/null
+++ b/tests/smoke_inspector_next.c
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include "smoke_live_next.h"
+
+#define INSPECTOR_NUGGET_UUID "10000000-0000-4000-8000-000000000002"
+#define REQUEST_ID "10000000-0000-4000-8000-000000000102"
+#define GENERATION "10000000-0000-4000-8000-000000000202"
+#define INVALIDATION_ID "10000000-0000-4000-8000-000000000302"
+#define CREATED_AT "2026-06-25T00:00:00.000Z"
+
+static const char *DISPATCHER_HELLO =
+ "{"
+ "\"schema_name\":\"razorback.cnc.dispatcher_hello\","
+ "\"schema_version\":1,"
+ "\"dispatcher_id\":\"77777777-7777-4777-8777-777777777777\","
+ "\"created_at\":\"" CREATED_AT "\","
+ "\"started_at\":\"" CREATED_AT "\","
+ "\"ready\":true,"
+ "\"availability\":\"ready\","
+ "\"dependency_reason_codes\":[]"
+ "}";
+
+static const char *REGISTRATION_ACCEPTED =
+ "{"
+ "\"schema_name\":\"razorback.cnc.registration_accepted\","
+ "\"schema_version\":1,"
+ "\"request_id\":\"" REQUEST_ID "\","
+ "\"nugget_uuid\":\"" INSPECTOR_NUGGET_UUID "\","
+ "\"registration_generation\":\"" GENERATION "\","
+ "\"effective_runtime_policy\":\"running\","
+ "\"liveness_interval\":10,"
+ "\"liveness_freshness_window\":30,"
+ "\"liveness_clock_skew_tolerance\":5,"
+ "\"created_at\":\"" CREATED_AT "\""
+ "}";
+
+static void
+directed(char *buffer, size_t bufferSize, const char *command,
+ const char *invalidationId)
+{
+ snprintf(buffer, bufferSize,
+ "{"
+ "\"schema_name\":\"razorback.cnc.directed_command\","
+ "\"schema_version\":1,"
+ "\"command_id\":\"10000000-0000-4000-8000-000000000303\","
+ "\"target_nugget_uuid\":\"" INSPECTOR_NUGGET_UUID "\","
+ "\"registration_generation\":\"" GENERATION "\","
+ "\"command\":\"%s\","
+ "\"reason_code\":\"%s\","
+ "%s"
+ "\"created_at\":\"" CREATED_AT "\""
+ "}",
+ command,
+ strcmp(command, "cache_invalidate") == 0
+ ? "catalog_invalidation"
+ : "operator_requested",
+ invalidationId == NULL ? "" :
+ "\"invalidation_id\":\"" INVALIDATION_ID "\",");
+}
+
+int
+main(void)
+{
+ RzbNextRuntime_t *runtime;
+ struct RzbNextRuntimeTransition transition;
+ struct RzbNextRuntimeDirectedResult result;
+ struct RzbNextRuntimeHealth health;
+ char command[1024];
+ char *bye;
+
+ if (RzbSmoke_LiveEnabled())
+ return RzbSmoke_RunLive(RZB_SMOKE_ROLE_INSPECTOR) ? 0 : 1;
+
+ runtime = RzbNextRuntime_CreateGenerated(INSPECTOR_NUGGET_UUID);
+ assert(runtime != NULL);
+
+ RzbNextRuntime_Initialize(runtime);
+ assert(RzbNextRuntime_ObserveDispatcherHello(runtime, DISPATCHER_HELLO));
+ assert(RzbNextRuntime_BeginRegistration(runtime, REQUEST_ID));
+ assert(RzbNextRuntime_RegistrationAccepted(runtime, REGISTRATION_ACCEPTED,
+ &transition));
+ assert(RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_READY);
+
+ assert(RzbNextRuntime_DependencyUnavailable(runtime));
+ health = RzbNextRuntime_Health(runtime);
+ assert(health.healthz);
+ assert(!health.readyz);
+ assert(RzbNextRuntime_DependencyRecovered(runtime));
+ assert(RzbNextRuntime_Health(runtime).readyz);
+
+ directed(command, sizeof(command), "cache_invalidate", INVALIDATION_ID);
+ assert(RzbNextRuntime_ApplyDirectedCommand(runtime, command, &result));
+ assert(result.effect == RZB_NEXT_RUNTIME_DIRECTED_CACHE_INVALIDATE);
+ assert(strcmp(result.invalidationId, INVALIDATION_ID) == 0);
+ assert(RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_READY);
+
+ directed(command, sizeof(command), "terminate", NULL);
+ assert(RzbNextRuntime_ApplyDirectedCommand(runtime, command, &result));
+ assert(result.effect == RZB_NEXT_RUNTIME_DIRECTED_SHUTDOWN);
+ assert(RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_DRAINING);
+ bye = RzbNextRuntime_BuildBye(runtime, "terminate_command", CREATED_AT);
+ assert(bye != NULL);
+ free(bye);
+
+ RzbNextRuntime_Destroy(runtime);
+ puts("dispatcher-next C inspector smoke client passed");
+ return 0;
+}
diff --git a/tests/smoke_live_next.h b/tests/smoke_live_next.h
new file mode 100644
index 0000000..758f7f1
--- /dev/null
+++ b/tests/smoke_live_next.h
@@ -0,0 +1,2014 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#ifndef RAZORBACK_TESTS_SMOKE_LIVE_NEXT_H
+#define RAZORBACK_TESTS_SMOKE_LIVE_NEXT_H
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define RZB_SMOKE_DEFAULT_RABBITMQ_URL \
+ "amqp://razorback:razorback-rabbitmq@localhost:5672/razorback"
+#define RZB_SMOKE_DEFAULT_API_URL "http://localhost:5111"
+#define RZB_SMOKE_DEFAULT_KEYCLOAK_URL "http://localhost:8080"
+#define RZB_SMOKE_DEFAULT_KEYCLOAK_REALM "razorback-dev"
+#define RZB_SMOKE_DEFAULT_KEYCLOAK_CLIENT_ID "smoke"
+#define RZB_SMOKE_DEFAULT_KEYCLOAK_USERNAME "smoke"
+#define RZB_SMOKE_DEFAULT_KEYCLOAK_PASSWORD "razorback-smoke"
+#define RZB_SMOKE_DEFAULT_LOCALITY "default"
+#define RZB_SMOKE_DEFAULT_SECONDS 25
+#define RZB_SMOKE_OPERATIONAL_BLOCK_SIZE 4096ULL
+#define RZB_SMOKE_OPERATIONAL_BLOCK_DATA_TYPE "application/pdf"
+
+#if defined(__GNUC__)
+#define RZB_SMOKE_UNUSED __attribute__((unused))
+#else
+#define RZB_SMOKE_UNUSED
+#endif
+
+enum RzbSmokeRole
+{
+ RZB_SMOKE_ROLE_SOURCE = 0,
+ RZB_SMOKE_ROLE_INSPECTOR = 1
+};
+
+struct RzbSmokeConfig
+{
+ enum RzbSmokeRole role;
+ const char *label;
+ const char *envPrefix;
+ const char *nuggetUuid;
+ const char *nuggetType;
+ const char *appType;
+ const char *dataType;
+ const char *rabbitmqUrl;
+ const char *apiUrl;
+ const char *locality;
+};
+
+struct RzbSmokeHttpResponse
+{
+ long status;
+ char *body;
+ size_t bodySize;
+};
+
+struct RzbSmokeBroker
+{
+ amqp_connection_state_t connection;
+ amqp_channel_t channel;
+ char *helloQueue;
+ char *directedQueue;
+ char *workQueue;
+};
+
+struct RzbSmokeReport
+{
+ size_t deliveries;
+ bool registrationPublished;
+ bool livenessPublished;
+ bool byePublished;
+ bool liveStateSeen;
+ bool blockSubmissionPublished;
+ bool blockUpdatePublished;
+ bool inspectionWorkReceived;
+ bool analysisResultPublished;
+ bool activeInspectionSeen;
+ bool blockSeen;
+};
+
+struct RzbSmokeOperation
+{
+ char *eventId;
+ char *updateId;
+ char *inspectionId;
+ char *sha256;
+ const char *dataType;
+ unsigned long long size;
+ const char *inspectorAppType;
+};
+
+static bool RzbSmoke_LiveEnabled(void);
+static bool RZB_SMOKE_UNUSED RzbSmoke_RunLive(enum RzbSmokeRole role);
+static bool RZB_SMOKE_UNUSED RzbSmoke_RunLiveOperational(void);
+
+static const char *
+RzbSmoke_Env(const char *name, const char *fallback)
+{
+ const char *value = getenv(name);
+
+ if (value == NULL || value[0] == '\0')
+ return fallback;
+ return value;
+}
+
+static bool
+RzbSmoke_LiveEnabled(void)
+{
+ const char *value = getenv("RZB_SMOKE_LIVE");
+
+ return value != NULL &&
+ (strcmp(value, "1") == 0 || strcmp(value, "true") == 0 ||
+ strcmp(value, "TRUE") == 0 || strcmp(value, "yes") == 0 ||
+ strcmp(value, "YES") == 0 || strcmp(value, "on") == 0 ||
+ strcmp(value, "ON") == 0);
+}
+
+static bool
+RzbSmoke_EnvFalse(const char *name)
+{
+ const char *value = getenv(name);
+
+ return value != NULL &&
+ (strcmp(value, "0") == 0 || strcmp(value, "false") == 0 ||
+ strcmp(value, "FALSE") == 0 || strcmp(value, "no") == 0 ||
+ strcmp(value, "NO") == 0 || strcmp(value, "off") == 0 ||
+ strcmp(value, "OFF") == 0);
+}
+
+static unsigned long
+RzbSmoke_EnvSeconds(const char *name, unsigned long fallback)
+{
+ const char *value = getenv(name);
+ char *end = NULL;
+ unsigned long parsed;
+
+ if (value == NULL || value[0] == '\0')
+ return fallback;
+ parsed = strtoul(value, &end, 10);
+ if (end == value || *end != '\0')
+ return fallback;
+ return parsed;
+}
+
+static char *
+RzbSmoke_Strdup(const char *value)
+{
+ char *copy;
+ size_t length;
+
+ if (value == NULL)
+ return NULL;
+ length = strlen(value) + 1U;
+ copy = malloc(length);
+ if (copy != NULL)
+ memcpy(copy, value, length);
+ return copy;
+}
+
+static char *
+RzbSmoke_Format(const char *format, ...)
+{
+ va_list ap;
+ va_list copy;
+ int needed;
+ char *buffer;
+
+ va_start(ap, format);
+ va_copy(copy, ap);
+ needed = vsnprintf(NULL, 0, format, copy);
+ va_end(copy);
+ if (needed < 0) {
+ va_end(ap);
+ return NULL;
+ }
+ buffer = calloc((size_t)needed + 1U, sizeof(char));
+ if (buffer != NULL)
+ vsnprintf(buffer, (size_t)needed + 1U, format, ap);
+ va_end(ap);
+ return buffer;
+}
+
+static char *
+RzbSmoke_Uuid(void)
+{
+ uuid_t uuid;
+ char *text = calloc(37U, sizeof(char));
+
+ if (text == NULL)
+ return NULL;
+ uuid_generate_random(uuid);
+ uuid_unparse_lower(uuid, text);
+ return text;
+}
+
+static char *
+RzbSmoke_RandomSha256(void)
+{
+ char *first = RzbSmoke_Uuid();
+ char *second = RzbSmoke_Uuid();
+ char *sha = calloc(65U, sizeof(char));
+ size_t out = 0;
+ const char *parts[2];
+ size_t part;
+
+ if (first == NULL || second == NULL || sha == NULL)
+ goto cleanup;
+ parts[0] = first;
+ parts[1] = second;
+ for (part = 0; part < 2U; part++) {
+ const char *cursor;
+
+ for (cursor = parts[part]; *cursor != '\0' && out < 64U; cursor++) {
+ if (*cursor != '-')
+ sha[out++] = *cursor;
+ }
+ }
+ if (out != 64U) {
+ free(sha);
+ sha = NULL;
+ }
+
+cleanup:
+ free(first);
+ free(second);
+ return sha;
+}
+
+static char *
+RzbSmoke_Timestamp(void)
+{
+ time_t now = time(NULL);
+ struct tm tmValue;
+ char *text = calloc(25U, sizeof(char));
+
+ if (text == NULL)
+ return NULL;
+ gmtime_r(&now, &tmValue);
+ strftime(text, 25U, "%Y-%m-%dT%H:%M:%S", &tmValue);
+ strcat(text, ".000Z");
+ return text;
+}
+
+static bool
+RzbSmoke_OperationInit(const struct RzbSmokeConfig *inspector,
+ struct RzbSmokeOperation *operation)
+{
+ memset(operation, 0, sizeof(*operation));
+ operation->eventId = RzbSmoke_Uuid();
+ operation->updateId = RzbSmoke_Uuid();
+ operation->sha256 = RzbSmoke_RandomSha256();
+ operation->dataType = RZB_SMOKE_OPERATIONAL_BLOCK_DATA_TYPE;
+ operation->size = RZB_SMOKE_OPERATIONAL_BLOCK_SIZE;
+ operation->inspectorAppType = inspector->appType;
+ return operation->eventId != NULL && operation->updateId != NULL &&
+ operation->sha256 != NULL;
+}
+
+static void
+RzbSmoke_OperationClear(struct RzbSmokeOperation *operation)
+{
+ if (operation == NULL)
+ return;
+ free(operation->eventId);
+ free(operation->updateId);
+ free(operation->inspectionId);
+ free(operation->sha256);
+ memset(operation, 0, sizeof(*operation));
+}
+
+static bool
+RzbSmoke_WriteOperationReport(const struct RzbSmokeOperation *operation,
+ const char *sdkName)
+{
+ const char *path = getenv("RZB_SMOKE_OPERATION_REPORT");
+ FILE *file;
+
+ if (path == NULL || path[0] == '\0')
+ return true;
+ file = fopen(path, "w");
+ if (file == NULL)
+ return false;
+ fprintf(file,
+ "{\n"
+ " \"block_key\": \"%s:%llu\",\n"
+ " \"data_type\": \"%s\",\n"
+ " \"event_id\": \"%s\",\n"
+ " \"inspector_app_type\": \"%s\",\n",
+ operation->sha256, operation->size, operation->dataType,
+ operation->eventId, operation->inspectorAppType);
+ if (operation->inspectionId != NULL) {
+ fprintf(file, " \"inspection_id\": \"%s\",\n",
+ operation->inspectionId);
+ }
+ fprintf(file,
+ " \"sdk\": \"%s\",\n"
+ " \"sha256\": \"%s\",\n"
+ " \"size\": %llu,\n"
+ " \"update_id\": \"%s\"\n"
+ "}\n",
+ sdkName, operation->sha256, operation->size,
+ operation->updateId);
+ if (fclose(file) != 0)
+ return false;
+ return true;
+}
+
+static void
+RzbSmoke_DefaultConfig(enum RzbSmokeRole role, struct RzbSmokeConfig *config)
+{
+ memset(config, 0, sizeof(*config));
+ config->role = role;
+ config->rabbitmqUrl = RzbSmoke_Env("RZB_SMOKE_RABBITMQ_URL",
+ RzbSmoke_Env("RZB_RABBITMQ_URL",
+ RZB_SMOKE_DEFAULT_RABBITMQ_URL));
+ config->apiUrl = RzbSmoke_Env("RZB_SMOKE_API_URL",
+ RZB_SMOKE_DEFAULT_API_URL);
+ config->locality = RzbSmoke_Env("RZB_SMOKE_LOCALITY",
+ RZB_SMOKE_DEFAULT_LOCALITY);
+ if (role == RZB_SMOKE_ROLE_SOURCE) {
+ config->label = "source";
+ config->envPrefix = "SOURCE";
+ config->nuggetUuid = "10000000-0000-4000-8000-000000000001";
+ config->nuggetType = "collection";
+ config->appType = "smoke_source";
+ config->dataType = NULL;
+ } else {
+ config->label = "inspector";
+ config->envPrefix = "INSPECTOR";
+ config->nuggetUuid = "10000000-0000-4000-8000-000000000002";
+ config->nuggetType = "inspector";
+ config->appType = "pdf_inspector";
+ config->dataType = "application/pdf";
+ }
+}
+
+static char *
+RzbSmoke_QueryEncode(const char *value, bool keepSlash)
+{
+ static const char hex[] = "0123456789ABCDEF";
+ size_t length = 0;
+ char *encoded;
+ char *out;
+ const unsigned char *in;
+
+ for (in = (const unsigned char *)value; *in != '\0'; in++) {
+ if (isalnum(*in) || *in == '-' || *in == '_' || *in == '.' ||
+ *in == '~' || (keepSlash && *in == '/')) {
+ length++;
+ } else {
+ length += 3U;
+ }
+ }
+ encoded = calloc(length + 1U, sizeof(char));
+ if (encoded == NULL)
+ return NULL;
+ out = encoded;
+ for (in = (const unsigned char *)value; *in != '\0'; in++) {
+ if (isalnum(*in) || *in == '-' || *in == '_' || *in == '.' ||
+ *in == '~' || (keepSlash && *in == '/')) {
+ *out++ = (char)*in;
+ } else {
+ *out++ = '%';
+ *out++ = hex[*in >> 4U];
+ *out++ = hex[*in & 0x0FU];
+ }
+ }
+ return encoded;
+}
+
+static size_t
+RzbSmoke_CurlWrite(void *contents, size_t size, size_t nmemb, void *userp)
+{
+ size_t bytes = size * nmemb;
+ struct RzbSmokeHttpResponse *response = userp;
+ char *next = realloc(response->body, response->bodySize + bytes + 1U);
+
+ if (next == NULL)
+ return 0;
+ response->body = next;
+ memcpy(response->body + response->bodySize, contents, bytes);
+ response->bodySize += bytes;
+ response->body[response->bodySize] = '\0';
+ return bytes;
+}
+
+static void
+RzbSmoke_HttpResponseClear(struct RzbSmokeHttpResponse *response)
+{
+ if (response == NULL)
+ return;
+ free(response->body);
+ response->body = NULL;
+ response->bodySize = 0;
+ response->status = 0;
+}
+
+static bool
+RzbSmoke_HttpRequest(const char *method, const char *url, const char *bearer,
+ const char *contentType, const char *body,
+ struct RzbSmokeHttpResponse *response)
+{
+ CURL *curl;
+ CURLcode result;
+ struct curl_slist *headers = NULL;
+ char *authHeader = NULL;
+
+ memset(response, 0, sizeof(*response));
+ curl = curl_easy_init();
+ if (curl == NULL)
+ return false;
+ headers = curl_slist_append(headers, "Accept: application/json");
+ if (bearer != NULL) {
+ authHeader = RzbSmoke_Format("Authorization: Bearer %s", bearer);
+ headers = curl_slist_append(headers, authHeader);
+ }
+ if (contentType != NULL) {
+ char *contentTypeHeader = RzbSmoke_Format("Content-Type: %s",
+ contentType);
+ headers = curl_slist_append(headers, contentTypeHeader);
+ free(contentTypeHeader);
+ }
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, RzbSmoke_CurlWrite);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, response);
+ if (body != NULL)
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
+ result = curl_easy_perform(curl);
+ if (result == CURLE_OK)
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response->status);
+ curl_slist_free_all(headers);
+ curl_easy_cleanup(curl);
+ free(authHeader);
+ return result == CURLE_OK;
+}
+
+static json_object *
+RzbSmoke_HttpJson(const struct RzbSmokeHttpResponse *response)
+{
+ if (response->body == NULL)
+ return NULL;
+ return json_tokener_parse(response->body);
+}
+
+static bool
+RzbSmoke_JsonString(json_object *object, const char *field, const char **value)
+{
+ json_object *child;
+
+ if (!json_object_object_get_ex(object, field, &child) ||
+ json_object_get_type(child) != json_type_string) {
+ return false;
+ }
+ *value = json_object_get_string(child);
+ return *value != NULL;
+}
+
+static json_object *
+RzbSmoke_ResponseData(const struct RzbSmokeHttpResponse *response)
+{
+ json_object *root = RzbSmoke_HttpJson(response);
+ json_object *data;
+
+ if (root == NULL)
+ return NULL;
+ if (!json_object_object_get_ex(root, "data", &data)) {
+ json_object_put(root);
+ return NULL;
+ }
+ json_object_get(data);
+ json_object_put(root);
+ return data;
+}
+
+static json_object *
+RzbSmoke_FindNamed(const struct RzbSmokeConfig *config, const char *bearer,
+ const char *collection, const char *name)
+{
+ char *encoded = RzbSmoke_QueryEncode(name, false);
+ char *url = RzbSmoke_Format(
+ "%s%s?status=active&search=%s&search_field=name&limit=100",
+ config->apiUrl, collection, encoded);
+ struct RzbSmokeHttpResponse response;
+ json_object *root;
+ json_object *data;
+ size_t index;
+ size_t count;
+
+ free(encoded);
+ if (url == NULL)
+ return NULL;
+ if (!RzbSmoke_HttpRequest("GET", url, bearer, NULL, NULL, &response)) {
+ free(url);
+ return NULL;
+ }
+ free(url);
+ if (response.status != 200) {
+ RzbSmoke_HttpResponseClear(&response);
+ return NULL;
+ }
+ root = RzbSmoke_HttpJson(&response);
+ RzbSmoke_HttpResponseClear(&response);
+ if (root == NULL)
+ return NULL;
+ if (!json_object_object_get_ex(root, "data", &data) ||
+ json_object_get_type(data) != json_type_array) {
+ json_object_put(root);
+ return NULL;
+ }
+ count = json_object_array_length(data);
+ for (index = 0; index < count; index++) {
+ json_object *row = json_object_array_get_idx(data, index);
+ const char *actual = NULL;
+
+ if (row != NULL && RzbSmoke_JsonString(row, "name", &actual) &&
+ strcmp(actual, name) == 0) {
+ json_object_get(row);
+ json_object_put(root);
+ return row;
+ }
+ }
+ json_object_put(root);
+ return NULL;
+}
+
+static char *
+RzbSmoke_BearerToken(void)
+{
+ const char *token;
+ const char *keycloak;
+ const char *realm;
+ const char *clientId;
+ const char *username;
+ const char *password;
+ char *url;
+ char *body;
+ struct RzbSmokeHttpResponse response;
+ json_object *root;
+ const char *accessToken;
+ char *copy = NULL;
+
+ if (RzbSmoke_EnvFalse("RZB_SMOKE_API_AUTH"))
+ return NULL;
+ token = getenv("RZB_SMOKE_API_BEARER_TOKEN");
+ if (token == NULL || token[0] == '\0')
+ token = getenv("RZB_SMOKE_BEARER_TOKEN");
+ if (token != NULL && token[0] != '\0')
+ return RzbSmoke_Strdup(token);
+
+ keycloak = RzbSmoke_Env("RZB_SMOKE_KEYCLOAK_URL",
+ RZB_SMOKE_DEFAULT_KEYCLOAK_URL);
+ realm = RzbSmoke_Env("RZB_SMOKE_KEYCLOAK_REALM",
+ RZB_SMOKE_DEFAULT_KEYCLOAK_REALM);
+ clientId = RzbSmoke_Env("RZB_SMOKE_KEYCLOAK_CLIENT_ID",
+ RZB_SMOKE_DEFAULT_KEYCLOAK_CLIENT_ID);
+ username = RzbSmoke_Env("RZB_SMOKE_KEYCLOAK_USERNAME",
+ RZB_SMOKE_DEFAULT_KEYCLOAK_USERNAME);
+ password = RzbSmoke_Env("RZB_SMOKE_KEYCLOAK_PASSWORD",
+ RZB_SMOKE_DEFAULT_KEYCLOAK_PASSWORD);
+ url = RzbSmoke_Format("%s/realms/%s/protocol/openid-connect/token",
+ keycloak, realm);
+ body = RzbSmoke_Format(
+ "grant_type=password&client_id=%s&username=%s&password=%s"
+ "&scope=openid%%20profile%%20email",
+ clientId, username, password);
+ if (url == NULL || body == NULL)
+ goto cleanup;
+ if (!RzbSmoke_HttpRequest("POST", url, NULL,
+ "application/x-www-form-urlencoded", body,
+ &response)) {
+ goto cleanup;
+ }
+ if (response.status != 200) {
+ RzbSmoke_HttpResponseClear(&response);
+ goto cleanup;
+ }
+ root = RzbSmoke_HttpJson(&response);
+ RzbSmoke_HttpResponseClear(&response);
+ if (root == NULL)
+ goto cleanup;
+ if (RzbSmoke_JsonString(root, "access_token", &accessToken))
+ copy = RzbSmoke_Strdup(accessToken);
+ json_object_put(root);
+
+cleanup:
+ free(url);
+ free(body);
+ return copy;
+}
+
+static char *
+RzbSmoke_EnsureNuggetType(const struct RzbSmokeConfig *config,
+ const char *bearer)
+{
+ json_object *row = RzbSmoke_FindNamed(config, bearer,
+ "/api/v1/catalog/nugget-types",
+ config->nuggetType);
+ const char *uuidText = NULL;
+ char *copy = NULL;
+ char *body;
+ char *url;
+ struct RzbSmokeHttpResponse response;
+ json_object *data;
+
+ if (row != NULL) {
+ if (RzbSmoke_JsonString(row, "nugget_type_uuid", &uuidText))
+ copy = RzbSmoke_Strdup(uuidText);
+ json_object_put(row);
+ return copy;
+ }
+ body = RzbSmoke_Format(
+ "{\"name\":\"%s\",\"display_name\":\"%s\","
+ "\"description\":\"Dispatcher-next SDK live smoke nugget type.\"}",
+ config->nuggetType, config->nuggetType);
+ url = RzbSmoke_Format("%s/api/v1/catalog/nugget-types", config->apiUrl);
+ if (body == NULL || url == NULL)
+ goto cleanup;
+ if (!RzbSmoke_HttpRequest("POST", url, bearer, "application/json", body,
+ &response)) {
+ goto cleanup;
+ }
+ if (response.status == 409) {
+ RzbSmoke_HttpResponseClear(&response);
+ row = RzbSmoke_FindNamed(config, bearer,
+ "/api/v1/catalog/nugget-types",
+ config->nuggetType);
+ if (row != NULL && RzbSmoke_JsonString(row, "nugget_type_uuid",
+ &uuidText)) {
+ copy = RzbSmoke_Strdup(uuidText);
+ }
+ if (row != NULL)
+ json_object_put(row);
+ goto cleanup;
+ }
+ if (response.status == 201) {
+ data = RzbSmoke_ResponseData(&response);
+ if (data != NULL) {
+ if (RzbSmoke_JsonString(data, "nugget_type_uuid", &uuidText))
+ copy = RzbSmoke_Strdup(uuidText);
+ json_object_put(data);
+ }
+ }
+ RzbSmoke_HttpResponseClear(&response);
+
+cleanup:
+ free(body);
+ free(url);
+ return copy;
+}
+
+static bool
+RzbSmoke_EnsureAppType(const struct RzbSmokeConfig *config, const char *bearer,
+ const char *nuggetTypeUuid)
+{
+ json_object *row = RzbSmoke_FindNamed(config, bearer,
+ "/api/v1/catalog/app-types",
+ config->appType);
+ const char *actual = NULL;
+ char *body = NULL;
+ char *url = NULL;
+ struct RzbSmokeHttpResponse response;
+ bool ok = false;
+
+ if (row != NULL) {
+ ok = RzbSmoke_JsonString(row, "nugget_type_uuid", &actual) &&
+ strcmp(actual, nuggetTypeUuid) == 0;
+ json_object_put(row);
+ return ok;
+ }
+ body = RzbSmoke_Format(
+ "{\"name\":\"%s\",\"display_name\":\"%s\","
+ "\"description\":\"Dispatcher-next SDK live smoke app type.\","
+ "\"nugget_type_uuid\":\"%s\"}",
+ config->appType, config->appType, nuggetTypeUuid);
+ url = RzbSmoke_Format("%s/api/v1/catalog/app-types", config->apiUrl);
+ if (body == NULL || url == NULL)
+ goto cleanup;
+ if (!RzbSmoke_HttpRequest("POST", url, bearer, "application/json", body,
+ &response)) {
+ goto cleanup;
+ }
+ ok = response.status == 201 || response.status == 409;
+ RzbSmoke_HttpResponseClear(&response);
+
+cleanup:
+ free(body);
+ free(url);
+ return ok;
+}
+
+static bool
+RzbSmoke_EnsureNugget(const struct RzbSmokeConfig *config, const char *bearer,
+ const char *nuggetTypeUuid)
+{
+ char *pathUuid = RzbSmoke_QueryEncode(config->nuggetUuid, true);
+ char *url = RzbSmoke_Format("%s/api/v1/catalog/nuggets/%s",
+ config->apiUrl, pathUuid);
+ struct RzbSmokeHttpResponse response;
+ char *body = NULL;
+ bool ok = false;
+
+ free(pathUuid);
+ if (url == NULL)
+ return false;
+ if (!RzbSmoke_HttpRequest("GET", url, bearer, NULL, NULL, &response))
+ goto cleanup;
+ if (response.status == 200) {
+ ok = true;
+ RzbSmoke_HttpResponseClear(&response);
+ goto cleanup;
+ }
+ if (response.status != 404) {
+ RzbSmoke_HttpResponseClear(&response);
+ goto cleanup;
+ }
+ RzbSmoke_HttpResponseClear(&response);
+ free(url);
+ url = RzbSmoke_Format("%s/api/v1/catalog/nuggets", config->apiUrl);
+ body = RzbSmoke_Format(
+ "{\"nugget_uuid\":\"%s\",\"nugget_type_uuid\":\"%s\","
+ "\"locality\":\"%s\",\"name\":\"C SDK smoke %s\","
+ "\"provisioning_source\":\"manual\"}",
+ config->nuggetUuid, nuggetTypeUuid, config->locality, config->label);
+ if (url == NULL || body == NULL)
+ goto cleanup;
+ if (!RzbSmoke_HttpRequest("POST", url, bearer, "application/json", body,
+ &response)) {
+ goto cleanup;
+ }
+ ok = response.status == 201 || response.status == 409;
+ RzbSmoke_HttpResponseClear(&response);
+
+cleanup:
+ free(url);
+ free(body);
+ return ok;
+}
+
+static bool
+RzbSmoke_RequireDataType(const struct RzbSmokeConfig *config, const char *bearer)
+{
+ char *dataType;
+ char *url;
+ struct RzbSmokeHttpResponse response;
+ bool ok;
+
+ if (config->dataType == NULL)
+ return true;
+ dataType = RzbSmoke_QueryEncode(config->dataType, true);
+ url = RzbSmoke_Format("%s/api/v1/catalog/data-types/%s", config->apiUrl,
+ dataType);
+ free(dataType);
+ if (url == NULL)
+ return false;
+ ok = RzbSmoke_HttpRequest("GET", url, bearer, NULL, NULL, &response) &&
+ response.status == 200;
+ RzbSmoke_HttpResponseClear(&response);
+ free(url);
+ return ok;
+}
+
+static bool
+RzbSmoke_EnsureCatalog(const struct RzbSmokeConfig *config, const char *bearer)
+{
+ char *nuggetTypeUuid = RzbSmoke_EnsureNuggetType(config, bearer);
+ bool ok;
+
+ if (nuggetTypeUuid == NULL)
+ return false;
+ ok = RzbSmoke_EnsureAppType(config, bearer, nuggetTypeUuid) &&
+ RzbSmoke_EnsureNugget(config, bearer, nuggetTypeUuid) &&
+ RzbSmoke_RequireDataType(config, bearer);
+ free(nuggetTypeUuid);
+ return ok;
+}
+
+static bool
+RzbSmoke_OnlineNugget(const struct RzbSmokeConfig *config, const char *bearer,
+ bool *present)
+{
+ char *uuid = RzbSmoke_QueryEncode(config->nuggetUuid, false);
+ char *url = RzbSmoke_Format(
+ "%s/api/v1/admin/nuggets/online?nugget_uuid=%s&limit=100",
+ config->apiUrl, uuid);
+ struct RzbSmokeHttpResponse response;
+ json_object *root;
+ json_object *data;
+ size_t index;
+ size_t count;
+ bool ok = false;
+
+ *present = false;
+ free(uuid);
+ if (url == NULL)
+ return false;
+ if (!RzbSmoke_HttpRequest("GET", url, bearer, NULL, NULL, &response)) {
+ free(url);
+ return false;
+ }
+ free(url);
+ if (response.status != 200)
+ goto cleanup;
+ root = RzbSmoke_HttpJson(&response);
+ if (root == NULL)
+ goto cleanup;
+ if (!json_object_object_get_ex(root, "data", &data) ||
+ json_object_get_type(data) != json_type_array) {
+ json_object_put(root);
+ goto cleanup;
+ }
+ count = json_object_array_length(data);
+ for (index = 0; index < count; index++) {
+ json_object *row = json_object_array_get_idx(data, index);
+ const char *uuidText = NULL;
+ const char *policy = NULL;
+ const char *availability = NULL;
+ const char *queue = NULL;
+ char *expectedQueue = RzbNextCnc_DirectedCommandQueue(config->nuggetUuid);
+
+ if (row != NULL &&
+ RzbSmoke_JsonString(row, "nugget_uuid", &uuidText) &&
+ strcmp(uuidText, config->nuggetUuid) == 0) {
+ *present = true;
+ ok = RzbSmoke_JsonString(row, "runtime_policy", &policy) &&
+ strcmp(policy, "running") == 0 &&
+ RzbSmoke_JsonString(row, "availability", &availability) &&
+ strcmp(availability, "ready") == 0 &&
+ RzbSmoke_JsonString(row, "command_queue", &queue) &&
+ expectedQueue != NULL && strcmp(queue, expectedQueue) == 0;
+ free(expectedQueue);
+ json_object_put(root);
+ goto cleanup;
+ }
+ free(expectedQueue);
+ }
+ ok = true;
+ json_object_put(root);
+
+cleanup:
+ RzbSmoke_HttpResponseClear(&response);
+ return ok;
+}
+
+static bool
+RzbSmoke_RoutePresent(const struct RzbSmokeConfig *config, const char *bearer,
+ bool *present)
+{
+ char *uuid;
+ char *dataType;
+ char *appType;
+ char *url;
+ struct RzbSmokeHttpResponse response;
+ json_object *root;
+ json_object *data;
+ size_t index;
+ size_t count;
+ bool ok = false;
+
+ *present = false;
+ if (config->dataType == NULL)
+ return true;
+ uuid = RzbSmoke_QueryEncode(config->nuggetUuid, false);
+ dataType = RzbSmoke_QueryEncode(config->dataType, false);
+ appType = RzbSmoke_QueryEncode(config->appType, false);
+ url = RzbSmoke_Format(
+ "%s/api/v1/admin/routes?nugget_uuid=%s&data_type=%s"
+ "&app_type=%s&eligible=true&limit=100",
+ config->apiUrl, uuid, dataType, appType);
+ free(uuid);
+ free(dataType);
+ free(appType);
+ if (url == NULL)
+ return false;
+ if (!RzbSmoke_HttpRequest("GET", url, bearer, NULL, NULL, &response)) {
+ free(url);
+ return false;
+ }
+ free(url);
+ if (response.status != 200)
+ goto cleanup;
+ root = RzbSmoke_HttpJson(&response);
+ if (root == NULL)
+ goto cleanup;
+ if (!json_object_object_get_ex(root, "data", &data) ||
+ json_object_get_type(data) != json_type_array) {
+ json_object_put(root);
+ goto cleanup;
+ }
+ count = json_object_array_length(data);
+ for (index = 0; index < count; index++) {
+ json_object *row = json_object_array_get_idx(data, index);
+ const char *uuidText = NULL;
+ const char *dataTypeText = NULL;
+ const char *appTypeText = NULL;
+ json_object *eligible;
+
+ if (row != NULL &&
+ RzbSmoke_JsonString(row, "nugget_uuid", &uuidText) &&
+ strcmp(uuidText, config->nuggetUuid) == 0 &&
+ RzbSmoke_JsonString(row, "data_type", &dataTypeText) &&
+ strcmp(dataTypeText, config->dataType) == 0 &&
+ RzbSmoke_JsonString(row, "app_type", &appTypeText) &&
+ strcmp(appTypeText, config->appType) == 0 &&
+ json_object_object_get_ex(row, "eligible", &eligible) &&
+ json_object_get_boolean(eligible)) {
+ *present = true;
+ break;
+ }
+ }
+ ok = true;
+ json_object_put(root);
+
+cleanup:
+ RzbSmoke_HttpResponseClear(&response);
+ return ok;
+}
+
+static bool
+RzbSmoke_ApiLiveState(const struct RzbSmokeConfig *config, const char *bearer)
+{
+ bool present = false;
+ bool route = false;
+
+ if (!RzbSmoke_OnlineNugget(config, bearer, &present) || !present)
+ return false;
+ if (config->dataType == NULL)
+ return true;
+ return RzbSmoke_RoutePresent(config, bearer, &route) && route;
+}
+
+static bool
+RzbSmoke_ApiByeCleanup(const struct RzbSmokeConfig *config, const char *bearer)
+{
+ bool present = true;
+ bool route = true;
+
+ if (!RzbSmoke_OnlineNugget(config, bearer, &present) || present)
+ return false;
+ if (config->dataType == NULL)
+ return true;
+ return RzbSmoke_RoutePresent(config, bearer, &route) && !route;
+}
+
+static bool
+RzbSmoke_ActiveInspectionPresent(const struct RzbSmokeConfig *source,
+ const char *bearer,
+ struct RzbSmokeOperation *operation,
+ bool *present)
+{
+ char *eventId = RzbSmoke_QueryEncode(operation->eventId, false);
+ char *appType = RzbSmoke_QueryEncode(operation->inspectorAppType, false);
+ char *url = RzbSmoke_Format(
+ "%s/api/v1/inspections/active?event_id=%s&app_type=%s&limit=100",
+ source->apiUrl, eventId, appType);
+ char expectedBlockKey[96];
+ struct RzbSmokeHttpResponse response;
+ json_object *root;
+ json_object *data;
+ size_t index;
+ size_t count;
+ bool ok = false;
+
+ *present = false;
+ free(eventId);
+ free(appType);
+ if (url == NULL)
+ return false;
+ snprintf(expectedBlockKey, sizeof(expectedBlockKey), "%s:%llu",
+ operation->sha256, operation->size);
+ if (!RzbSmoke_HttpRequest("GET", url, bearer, NULL, NULL, &response)) {
+ free(url);
+ return false;
+ }
+ free(url);
+ if (response.status != 200)
+ goto cleanup;
+ root = RzbSmoke_HttpJson(&response);
+ if (root == NULL)
+ goto cleanup;
+ if (!json_object_object_get_ex(root, "data", &data) ||
+ json_object_get_type(data) != json_type_array) {
+ json_object_put(root);
+ goto cleanup;
+ }
+ count = json_object_array_length(data);
+ for (index = 0; index < count; index++) {
+ json_object *row = json_object_array_get_idx(data, index);
+ const char *eventIdText = NULL;
+ const char *inspectionIdText = NULL;
+ const char *appTypeText = NULL;
+ const char *blockKey = NULL;
+ const char *status = NULL;
+
+ if (row != NULL &&
+ RzbSmoke_JsonString(row, "event_id", &eventIdText) &&
+ strcmp(eventIdText, operation->eventId) == 0 &&
+ RzbSmoke_JsonString(row, "inspection_id", &inspectionIdText) &&
+ RzbSmoke_JsonString(row, "app_type", &appTypeText) &&
+ strcmp(appTypeText, operation->inspectorAppType) == 0 &&
+ RzbSmoke_JsonString(row, "block_key", &blockKey) &&
+ strcmp(blockKey, expectedBlockKey) == 0 &&
+ RzbSmoke_JsonString(row, "status", &status) &&
+ (strcmp(status, "pending") == 0 ||
+ strcmp(status, "running") == 0 ||
+ strcmp(status, "deferred") == 0)) {
+ if (operation->inspectionId == NULL)
+ operation->inspectionId = RzbSmoke_Strdup(inspectionIdText);
+ *present = true;
+ break;
+ }
+ }
+ ok = true;
+ json_object_put(root);
+
+cleanup:
+ RzbSmoke_HttpResponseClear(&response);
+ return ok;
+}
+
+static bool
+RzbSmoke_BlockPresent(const struct RzbSmokeConfig *source, const char *bearer,
+ const struct RzbSmokeOperation *operation, bool *present)
+{
+ char *sha = RzbSmoke_QueryEncode(operation->sha256, true);
+ char *url = RzbSmoke_Format("%s/api/v1/blocks/sha256/%s/%llu",
+ source->apiUrl, sha, operation->size);
+ struct RzbSmokeHttpResponse response;
+ bool ok;
+
+ *present = false;
+ free(sha);
+ if (url == NULL)
+ return false;
+ ok = RzbSmoke_HttpRequest("GET", url, bearer, NULL, NULL, &response);
+ free(url);
+ if (!ok)
+ return false;
+ if (response.status == 200) {
+ *present = true;
+ ok = true;
+ } else if (response.status == 404) {
+ ok = true;
+ } else {
+ ok = false;
+ }
+ RzbSmoke_HttpResponseClear(&response);
+ return ok;
+}
+
+static bool
+RzbSmoke_CheckRpc(amqp_connection_state_t connection, const char *operation)
+{
+ amqp_rpc_reply_t reply = amqp_get_rpc_reply(connection);
+
+ if (reply.reply_type == AMQP_RESPONSE_NORMAL)
+ return true;
+ fprintf(stderr, "RabbitMQ %s failed: reply_type=%d library_error=%d\n",
+ operation, reply.reply_type, reply.library_error);
+ return false;
+}
+
+static bool
+RzbSmoke_BrokerConnect(const char *url, struct RzbSmokeBroker *broker)
+{
+ char *mutableUrl = RzbSmoke_Strdup(url);
+ struct amqp_connection_info info;
+ amqp_socket_t *socket;
+
+ memset(broker, 0, sizeof(*broker));
+ if (mutableUrl == NULL)
+ return false;
+ amqp_default_connection_info(&info);
+ if (amqp_parse_url(mutableUrl, &info) != AMQP_STATUS_OK) {
+ free(mutableUrl);
+ return false;
+ }
+ broker->connection = amqp_new_connection();
+ socket = info.ssl ? amqp_ssl_socket_new(broker->connection)
+ : amqp_tcp_socket_new(broker->connection);
+ if (socket == NULL) {
+ free(mutableUrl);
+ return false;
+ }
+ if (info.ssl) {
+ const char *caFile = getenv("RZB_RABBITMQ__TLS__CA_FILE");
+ const char *clientCert = getenv("RZB_RABBITMQ__TLS__CLIENT_CERT_FILE");
+ const char *clientKey = getenv("RZB_RABBITMQ__TLS__CLIENT_KEY_FILE");
+ bool verifyTls = strcmp(RzbSmoke_Env("RZB_RABBITMQ__TLS__VERIFY_TLS", "true"), "false") != 0;
+ amqp_ssl_socket_set_verify_peer(socket, verifyTls);
+ amqp_ssl_socket_set_verify_hostname(socket, verifyTls);
+ if (caFile != NULL && caFile[0] != '\0' &&
+ amqp_ssl_socket_set_cacert(socket, caFile) != AMQP_STATUS_OK) {
+ free(mutableUrl);
+ return false;
+ }
+ if ((clientCert != NULL && clientCert[0] != '\0') !=
+ (clientKey != NULL && clientKey[0] != '\0')) {
+ free(mutableUrl);
+ return false;
+ }
+ if (clientCert != NULL && clientCert[0] != '\0' &&
+ amqp_ssl_socket_set_key(socket, clientCert, clientKey) != AMQP_STATUS_OK) {
+ free(mutableUrl);
+ return false;
+ }
+ }
+ if (amqp_socket_open(socket, info.host, info.port) != AMQP_STATUS_OK) {
+ free(mutableUrl);
+ return false;
+ }
+ broker->channel = 1;
+ if (amqp_login(broker->connection, info.vhost, 0, 131072, 10,
+ AMQP_SASL_METHOD_PLAIN, info.user,
+ info.password).reply_type != AMQP_RESPONSE_NORMAL) {
+ free(mutableUrl);
+ return false;
+ }
+ amqp_channel_open(broker->connection, broker->channel);
+ if (!RzbSmoke_CheckRpc(broker->connection, "channel_open")) {
+ free(mutableUrl);
+ return false;
+ }
+ free(mutableUrl);
+ return true;
+}
+
+static void
+RzbSmoke_BrokerClose(struct RzbSmokeBroker *broker)
+{
+ if (broker == NULL || broker->connection == NULL)
+ return;
+ amqp_channel_close(broker->connection, broker->channel, AMQP_REPLY_SUCCESS);
+ amqp_connection_close(broker->connection, AMQP_REPLY_SUCCESS);
+ amqp_destroy_connection(broker->connection);
+ free(broker->helloQueue);
+ free(broker->directedQueue);
+ free(broker->workQueue);
+ memset(broker, 0, sizeof(*broker));
+}
+
+static char *
+RzbSmoke_CopyBytesString(amqp_bytes_t bytes)
+{
+ char *copy = calloc(bytes.len + 1U, sizeof(char));
+
+ if (copy == NULL)
+ return NULL;
+ memcpy(copy, bytes.bytes, bytes.len);
+ return copy;
+}
+
+static bool
+RzbSmoke_DeclareTopology(struct RzbSmokeBroker *broker,
+ const struct RzbSmokeConfig *config)
+{
+ amqp_queue_declare_ok_t *hello;
+ amqp_table_entry_t quorumEntry;
+ amqp_table_t quorumArgs;
+
+ amqp_exchange_declare(
+ broker->connection, broker->channel,
+ amqp_cstring_bytes(RZB_NEXT_EXCHANGE_DISPATCHER_HELLO),
+ amqp_cstring_bytes("fanout"), 0, 0, 0, 0, amqp_empty_table);
+ if (!RzbSmoke_CheckRpc(broker->connection, "exchange_declare"))
+ return false;
+ hello = amqp_queue_declare(broker->connection, broker->channel,
+ amqp_empty_bytes, 0, 0, 1, 1,
+ amqp_empty_table);
+ if (!RzbSmoke_CheckRpc(broker->connection, "hello_queue_declare") ||
+ hello == NULL) {
+ return false;
+ }
+ broker->helloQueue = RzbSmoke_CopyBytesString(hello->queue);
+ broker->directedQueue = RzbNextCnc_DirectedCommandQueue(config->nuggetUuid);
+ if (broker->helloQueue == NULL || broker->directedQueue == NULL)
+ return false;
+ amqp_queue_bind(broker->connection, broker->channel,
+ amqp_cstring_bytes(broker->helloQueue),
+ amqp_cstring_bytes(RZB_NEXT_EXCHANGE_DISPATCHER_HELLO),
+ amqp_cstring_bytes(""), amqp_empty_table);
+ if (!RzbSmoke_CheckRpc(broker->connection, "hello_queue_bind"))
+ return false;
+ amqp_queue_declare(broker->connection, broker->channel,
+ amqp_cstring_bytes(broker->directedQueue), 0, 0, 1, 1,
+ amqp_empty_table);
+ if (!RzbSmoke_CheckRpc(broker->connection, "directed_queue_declare"))
+ return false;
+ if (config->role != RZB_SMOKE_ROLE_INSPECTOR)
+ return true;
+
+ broker->workQueue = RzbSmoke_Format("%s.%s",
+ RZB_NEXT_QUEUE_INSPECTOR_PREFIX,
+ config->appType);
+ if (broker->workQueue == NULL)
+ return false;
+ quorumEntry.key = amqp_cstring_bytes("x-queue-type");
+ quorumEntry.value.kind = AMQP_FIELD_KIND_UTF8;
+ quorumEntry.value.value.bytes = amqp_cstring_bytes("quorum");
+ quorumArgs.num_entries = 1;
+ quorumArgs.entries = &quorumEntry;
+ amqp_queue_declare(broker->connection, broker->channel,
+ amqp_cstring_bytes(broker->workQueue), 0, 1, 0, 0,
+ quorumArgs);
+ return RzbSmoke_CheckRpc(broker->connection, "inspector_work_queue_declare");
+}
+
+static bool
+RzbSmoke_DuplicateDirectedQueueRejected(const struct RzbSmokeConfig *config)
+{
+ struct RzbSmokeBroker duplicate;
+ char *directedQueue;
+ bool rejected = false;
+ amqp_rpc_reply_t reply;
+
+ if (!RzbSmoke_BrokerConnect(config->rabbitmqUrl, &duplicate))
+ return false;
+ directedQueue = RzbNextCnc_DirectedCommandQueue(config->nuggetUuid);
+ if (directedQueue == NULL)
+ goto cleanup;
+ amqp_queue_declare(duplicate.connection, duplicate.channel,
+ amqp_cstring_bytes(directedQueue), 0, 0, 1, 1,
+ amqp_empty_table);
+ reply = amqp_get_rpc_reply(duplicate.connection);
+ rejected = reply.reply_type != AMQP_RESPONSE_NORMAL;
+
+cleanup:
+ free(directedQueue);
+ RzbSmoke_BrokerClose(&duplicate);
+ return rejected;
+}
+
+static bool
+RzbSmoke_StartConsumers(struct RzbSmokeBroker *broker)
+{
+ amqp_basic_consume(broker->connection, broker->channel,
+ amqp_cstring_bytes(broker->helloQueue),
+ amqp_empty_bytes, 0, 0, 1, amqp_empty_table);
+ if (!RzbSmoke_CheckRpc(broker->connection, "hello_consume"))
+ return false;
+ amqp_basic_consume(broker->connection, broker->channel,
+ amqp_cstring_bytes(broker->directedQueue),
+ amqp_empty_bytes, 0, 0, 1, amqp_empty_table);
+ if (!RzbSmoke_CheckRpc(broker->connection, "directed_consume"))
+ return false;
+ if (broker->workQueue == NULL)
+ return true;
+ amqp_basic_consume(broker->connection, broker->channel,
+ amqp_cstring_bytes(broker->workQueue),
+ amqp_empty_bytes, 0, 0, 1, amqp_empty_table);
+ return RzbSmoke_CheckRpc(broker->connection, "inspector_work_consume");
+}
+
+static struct RzbNextMessageHeader *
+RzbSmoke_DeliveryHeaders(const amqp_table_t *table, size_t *count)
+{
+ struct RzbNextMessageHeader *headers;
+ int index;
+
+ *count = 0;
+ if (table == NULL || table->num_entries <= 0)
+ return NULL;
+ headers = calloc((size_t)table->num_entries, sizeof(*headers));
+ if (headers == NULL)
+ return NULL;
+ for (index = 0; index < table->num_entries; index++) {
+ amqp_table_entry_t entry = table->entries[index];
+ if (entry.value.kind == AMQP_FIELD_KIND_UTF8 ||
+ entry.value.kind == AMQP_FIELD_KIND_BYTES) {
+ headers[*count].name = RzbSmoke_CopyBytesString(entry.key);
+ headers[*count].value =
+ RzbSmoke_CopyBytesString(entry.value.value.bytes);
+ if (headers[*count].name != NULL && headers[*count].value != NULL)
+ (*count)++;
+ }
+ }
+ return headers;
+}
+
+static void
+RzbSmoke_DeliveryHeadersClear(struct RzbNextMessageHeader *headers, size_t count)
+{
+ size_t index;
+
+ for (index = 0; index < count; index++) {
+ free(headers[index].name);
+ free(headers[index].value);
+ }
+ free(headers);
+}
+
+static char *
+RzbSmoke_ConsumeOne(struct RzbSmokeBroker *broker, int timeoutSeconds)
+{
+ struct timeval timeout = { timeoutSeconds, 0 };
+ amqp_envelope_t envelope;
+ amqp_rpc_reply_t reply;
+ struct RzbNextMessageHeader *headers = NULL;
+ struct RzbNextDecodedRabbitMqMessage *decoded = NULL;
+ size_t headerCount = 0;
+ char *json = NULL;
+
+ memset(&envelope, 0, sizeof(envelope));
+ reply = amqp_consume_message(broker->connection, &envelope, &timeout, 0);
+ if (reply.reply_type != AMQP_RESPONSE_NORMAL)
+ return NULL;
+ if ((envelope.message.properties._flags & AMQP_BASIC_HEADERS_FLAG) != 0) {
+ headers = RzbSmoke_DeliveryHeaders(&envelope.message.properties.headers,
+ &headerCount);
+ }
+ if (RzbNextRabbitMq_DecodeMessage(envelope.message.body.bytes,
+ envelope.message.body.len, headers,
+ headerCount, NULL, 0, &decoded)) {
+ json = RzbSmoke_Strdup(decoded->jsonMessage);
+ amqp_basic_ack(broker->connection, broker->channel,
+ envelope.delivery_tag, 0);
+ } else {
+ amqp_basic_reject(broker->connection, broker->channel,
+ envelope.delivery_tag, 0);
+ }
+ RzbNextDecodedRabbitMqMessage_Destroy(decoded);
+ RzbSmoke_DeliveryHeadersClear(headers, headerCount);
+ amqp_destroy_envelope(&envelope);
+ return json;
+}
+
+static bool
+RzbSmoke_Publish(struct RzbSmokeBroker *broker, const char *jsonMessage,
+ uint64_t expirationSeconds)
+{
+ struct RzbNextPreparedRabbitMqMessage *prepared = NULL;
+ amqp_basic_properties_t properties;
+ amqp_table_entry_t *entries = NULL;
+ size_t index;
+ int result;
+ bool ok = false;
+
+ if (!RzbNextRabbitMq_PrepareMessage(jsonMessage, NULL, NULL, NULL,
+ &prepared)) {
+ return false;
+ }
+ memset(&properties, 0, sizeof(properties));
+ properties._flags = AMQP_BASIC_CONTENT_TYPE_FLAG |
+ AMQP_BASIC_DELIVERY_MODE_FLAG |
+ AMQP_BASIC_HEADERS_FLAG;
+ properties.content_type = amqp_cstring_bytes(prepared->contentType);
+ properties.delivery_mode = 1;
+ if (prepared->contentEncoding != NULL) {
+ properties._flags |= AMQP_BASIC_CONTENT_ENCODING_FLAG;
+ properties.content_encoding =
+ amqp_cstring_bytes(prepared->contentEncoding);
+ }
+ entries = calloc(prepared->headerCount, sizeof(*entries));
+ if (entries == NULL)
+ goto cleanup;
+ for (index = 0; index < prepared->headerCount; index++) {
+ entries[index].key = amqp_cstring_bytes(prepared->headers[index].name);
+ entries[index].value.kind = AMQP_FIELD_KIND_UTF8;
+ entries[index].value.value.bytes =
+ amqp_cstring_bytes(prepared->headers[index].value);
+ }
+ properties.headers.num_entries = (int)prepared->headerCount;
+ properties.headers.entries = entries;
+ if (expirationSeconds > 0U) {
+ char expiration[32];
+
+ snprintf(expiration, sizeof(expiration), "%llu",
+ (unsigned long long)(expirationSeconds * 1000U));
+ properties._flags |= AMQP_BASIC_EXPIRATION_FLAG;
+ properties.expiration = amqp_cstring_bytes(expiration);
+ result = amqp_basic_publish(
+ broker->connection, broker->channel,
+ amqp_cstring_bytes(prepared->route->exchange),
+ amqp_cstring_bytes(prepared->route->routingKey), 1, 0,
+ &properties,
+ (amqp_bytes_t){ prepared->bodySize, prepared->body });
+ } else {
+ result = amqp_basic_publish(
+ broker->connection, broker->channel,
+ amqp_cstring_bytes(prepared->route->exchange),
+ amqp_cstring_bytes(prepared->route->routingKey), 1, 0,
+ &properties,
+ (amqp_bytes_t){ prepared->bodySize, prepared->body });
+ }
+ ok = result == AMQP_STATUS_OK;
+
+cleanup:
+ free(entries);
+ RzbNextPreparedRabbitMqMessage_Destroy(prepared);
+ return ok;
+}
+
+static char *
+RzbSmoke_RegistrationRequest(RzbNextRuntime_t *runtime,
+ const struct RzbSmokeConfig *config,
+ const char *requestId, const char *createdAt)
+{
+ char *dataTypes = NULL;
+ char *message;
+
+ if (config->dataType != NULL) {
+ dataTypes = RzbSmoke_Format("\"data_types\":[\"%s\"],",
+ config->dataType);
+ }
+ message = RzbSmoke_Format(
+ "{"
+ "\"schema_name\":\"razorback.cnc.registration_request\","
+ "\"schema_version\":1,"
+ "\"request_id\":\"%s\","
+ "\"nugget_uuid\":\"%s\","
+ "\"process_uuid\":\"%s\","
+ "\"nugget_type\":\"%s\","
+ "\"app_type\":\"%s\","
+ "%s"
+ "\"capabilities\":{"
+ "\"component_version\":\"0.0.0\","
+ "\"sdk_name\":\"razorback-c\","
+ "\"sdk_version\":\"0.0.0\","
+ "\"supported_message_body_modes\":[\"inline\",\"zlib\",\"claim_check\"],"
+ "\"supports_deferred_results\":%s"
+ "},"
+ "\"desired_runtime_policy\":\"running\","
+ "\"created_at\":\"%s\""
+ "}",
+ requestId, config->nuggetUuid, RzbNextRuntime_ProcessUuid(runtime),
+ config->nuggetType, config->appType,
+ dataTypes == NULL ? "" : dataTypes,
+ config->role == RZB_SMOKE_ROLE_INSPECTOR ? "true" : "false",
+ createdAt);
+ free(dataTypes);
+ return message;
+}
+
+static char *
+RzbSmoke_BlockSubmission(const struct RzbSmokeConfig *config,
+ const struct RzbSmokeOperation *operation,
+ const char *createdAt)
+{
+ return RzbSmoke_Format(
+ "{"
+ "\"schema_name\":\"%s\","
+ "\"schema_version\":1,"
+ "\"event_id\":\"%s\","
+ "\"source_nugget_uuid\":\"%s\","
+ "\"block\":{"
+ "\"sha256\":\"%s\","
+ "\"size\":%llu,"
+ "\"data_type\":\"%s\""
+ "},"
+ "\"stored\":true,"
+ "\"event_metadata\":[{"
+ "\"name\":\"filename\","
+ "\"type\":\"string\","
+ "\"value\":\"c-sdk-smoke-%s.pdf\""
+ "}],"
+ "\"created_at\":\"%s\""
+ "}",
+ RZB_NEXT_SCHEMA_BLOCK_SUBMISSION, operation->eventId,
+ config->nuggetUuid, operation->sha256, operation->size,
+ operation->dataType, operation->eventId, createdAt);
+}
+
+static char *
+RzbSmoke_BlockUpdate(const struct RzbSmokeConfig *config,
+ const struct RzbSmokeOperation *operation,
+ const char *createdAt)
+{
+ return RzbSmoke_Format(
+ "{"
+ "\"schema_name\":\"%s\","
+ "\"schema_version\":1,"
+ "\"update_id\":\"%s\","
+ "\"source_nugget_uuid\":\"%s\","
+ "\"block\":{"
+ "\"sha256\":\"%s\","
+ "\"size\":%llu,"
+ "\"data_type\":\"%s\""
+ "},"
+ "\"metadata_updates\":[{"
+ "\"name\":\"filename\","
+ "\"type\":\"string\","
+ "\"created_at\":\"%s\","
+ "\"value\":\"c-sdk-smoke-update-%s.pdf\""
+ "}],"
+ "\"created_at\":\"%s\""
+ "}",
+ RZB_NEXT_SCHEMA_BLOCK_UPDATE, operation->updateId,
+ config->nuggetUuid, operation->sha256, operation->size,
+ operation->dataType, createdAt, operation->updateId, createdAt);
+}
+
+static bool
+RzbSmoke_PublishOperationalMessages(struct RzbSmokeBroker *broker,
+ const struct RzbSmokeConfig *config,
+ const struct RzbSmokeOperation *operation,
+ struct RzbSmokeReport *report)
+{
+ char *createdAt = RzbSmoke_Timestamp();
+ char *submission = NULL;
+ char *update = NULL;
+ bool ok = false;
+
+ if (createdAt == NULL)
+ return false;
+ submission = RzbSmoke_BlockSubmission(config, operation, createdAt);
+ update = RzbSmoke_BlockUpdate(config, operation, createdAt);
+ ok = submission != NULL && update != NULL &&
+ RzbSmoke_Publish(broker, submission, 0U) &&
+ RzbSmoke_Publish(broker, update, 0U);
+ if (ok) {
+ report->blockSubmissionPublished = true;
+ report->blockUpdatePublished = true;
+ }
+ free(createdAt);
+ free(submission);
+ free(update);
+ return ok;
+}
+
+static const char *
+RzbSmoke_SchemaName(const char *jsonMessage)
+{
+ static char schemaName[128];
+ json_object *object = json_tokener_parse(jsonMessage);
+ const char *value = NULL;
+
+ schemaName[0] = '\0';
+ if (object == NULL)
+ return schemaName;
+ if (RzbSmoke_JsonString(object, "schema_name", &value))
+ snprintf(schemaName, sizeof(schemaName), "%s", value);
+ json_object_put(object);
+ return schemaName;
+}
+
+static bool
+RzbSmoke_HandleInspectionWork(const struct RzbSmokeConfig *config,
+ const struct RzbSmokeOperation *operation,
+ const char *jsonMessage,
+ struct RzbSmokeBroker *broker,
+ struct RzbSmokeReport *report)
+{
+ json_object *root;
+ json_object *event;
+ json_object *block;
+ json_object *sizeObject;
+ const char *eventId = NULL;
+ const char *appType = NULL;
+ const char *workKind = NULL;
+ const char *sha256 = NULL;
+ const char *dataType = NULL;
+ char *createdAt = NULL;
+ char *result = NULL;
+ bool matched = false;
+ bool ok = false;
+
+ if (operation == NULL || config->role != RZB_SMOKE_ROLE_INSPECTOR)
+ return false;
+ root = json_tokener_parse(jsonMessage);
+ if (root == NULL)
+ return false;
+ ok = RzbSmoke_JsonString(root, "app_type", &appType) &&
+ strcmp(appType, config->appType) == 0 &&
+ RzbSmoke_JsonString(root, "work_kind", &workKind) &&
+ strcmp(workKind, "inspect") == 0 &&
+ json_object_object_get_ex(root, "event", &event) &&
+ RzbSmoke_JsonString(event, "event_id", &eventId) &&
+ strcmp(eventId, operation->eventId) == 0 &&
+ json_object_object_get_ex(root, "block", &block) &&
+ RzbSmoke_JsonString(block, "sha256", &sha256) &&
+ strcmp(sha256, operation->sha256) == 0 &&
+ RzbSmoke_JsonString(block, "data_type", &dataType) &&
+ strcmp(dataType, operation->dataType) == 0 &&
+ json_object_object_get_ex(block, "size", &sizeObject) &&
+ (unsigned long long)json_object_get_int64(sizeObject) ==
+ operation->size;
+ matched = ok;
+ if (ok) {
+ createdAt = RzbSmoke_Timestamp();
+ result = RzbNextAnalysisResult_BuildCompleted(
+ jsonMessage, config->nuggetUuid, createdAt, NULL,
+ "[{\"name\":\"smoke_result\",\"type\":\"string\",\"value\":\"completed\"}]",
+ NULL, NULL);
+ ok = result != NULL && RzbSmoke_Publish(broker, result, 0U);
+ }
+ if (ok) {
+ report->inspectionWorkReceived = true;
+ report->analysisResultPublished = true;
+ }
+ free(createdAt);
+ free(result);
+ json_object_put(root);
+ return matched ? ok : true;
+}
+
+static bool
+RzbSmoke_HandleMessage(RzbNextRuntime_t *runtime,
+ const struct RzbSmokeConfig *config,
+ const struct RzbSmokeOperation *operation,
+ struct RzbSmokeBroker *broker, const char *jsonMessage,
+ struct RzbSmokeReport *report)
+{
+ const char *schemaName = RzbSmoke_SchemaName(jsonMessage);
+
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_DISPATCHER_HELLO) == 0) {
+ if (RzbNextRuntime_ObserveDispatcherHello(runtime, jsonMessage) &&
+ !report->registrationPublished) {
+ char *requestId = RzbSmoke_Uuid();
+ char *createdAt = RzbSmoke_Timestamp();
+ char *registration =
+ RzbSmoke_RegistrationRequest(runtime, config, requestId,
+ createdAt);
+
+ if (requestId == NULL || createdAt == NULL ||
+ registration == NULL) {
+ free(requestId);
+ free(createdAt);
+ free(registration);
+ return false;
+ }
+ RzbNextRuntime_BeginRegistration(runtime, requestId);
+ if (!RzbSmoke_Publish(broker, registration, 0U)) {
+ free(requestId);
+ free(createdAt);
+ free(registration);
+ return false;
+ }
+ report->registrationPublished = true;
+ free(requestId);
+ free(createdAt);
+ free(registration);
+ }
+ return true;
+ }
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_REGISTRATION_ACCEPTED) == 0) {
+ struct RzbNextRuntimeTransition transition;
+
+ return RzbNextRuntime_RegistrationAccepted(runtime, jsonMessage,
+ &transition) &&
+ transition.kind == RZB_NEXT_RUNTIME_TRANSITION_REGISTERED;
+ }
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_REGISTRATION_REJECTED) == 0) {
+ struct RzbNextRuntimeTransition transition;
+
+ RzbNextRuntime_RegistrationRejected(runtime, jsonMessage, &transition);
+ return false;
+ }
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_CNC_DIRECTED_COMMAND) == 0) {
+ struct RzbNextRuntimeDirectedResult result;
+
+ if (!RzbNextRuntime_ApplyDirectedCommand(runtime, jsonMessage, &result))
+ return false;
+ if (result.effect == RZB_NEXT_RUNTIME_DIRECTED_SHUTDOWN) {
+ char *createdAt = RzbSmoke_Timestamp();
+ char *bye = RzbNextRuntime_BuildBye(runtime, "terminate_command",
+ createdAt);
+ bool ok = bye != NULL && RzbSmoke_Publish(broker, bye, 0U);
+
+ free(createdAt);
+ free(bye);
+ report->byePublished = ok;
+ return ok;
+ }
+ if (result.effect == RZB_NEXT_RUNTIME_DIRECTED_REREGISTER)
+ report->registrationPublished = false;
+ return true;
+ }
+ if (strcmp(schemaName, RZB_NEXT_SCHEMA_INSPECTION_WORK) == 0)
+ return RzbSmoke_HandleInspectionWork(config, operation, jsonMessage,
+ broker, report);
+ return false;
+}
+
+static bool
+RzbSmoke_PublishLiveness(RzbNextRuntime_t *runtime,
+ struct RzbSmokeBroker *broker,
+ struct RzbSmokeReport *report)
+{
+ struct RzbNextRuntimeLivenessPlan plan;
+ char *createdAt = RzbSmoke_Timestamp();
+ bool ok;
+
+ if (createdAt == NULL)
+ return false;
+ ok = RzbNextRuntime_LivenessPlan(runtime, createdAt, &plan);
+ free(createdAt);
+ if (!ok)
+ return false;
+ ok = RzbSmoke_Publish(broker, plan.message, plan.messageExpiration);
+ if (ok)
+ report->livenessPublished = true;
+ RzbNextRuntime_LivenessPlanClear(&plan);
+ return ok;
+}
+
+static bool
+RzbSmoke_ReportComplete(const struct RzbSmokeConfig *config,
+ const struct RzbSmokeOperation *operation,
+ const struct RzbSmokeReport *report)
+{
+ if (report->deliveries == 0 || !report->registrationPublished ||
+ !report->livenessPublished || !report->byePublished ||
+ !report->liveStateSeen) {
+ return false;
+ }
+ if (operation == NULL)
+ return true;
+ if (config->role == RZB_SMOKE_ROLE_INSPECTOR)
+ return report->inspectionWorkReceived && report->analysisResultPublished;
+ return report->blockSubmissionPublished && report->blockUpdatePublished &&
+ report->blockSeen;
+}
+
+static bool
+RzbSmoke_RunBrokerLoop(const struct RzbSmokeConfig *config, const char *bearer,
+ const struct RzbSmokeConfig *peerConfig,
+ struct RzbSmokeOperation *operation,
+ struct RzbSmokeBroker *broker, unsigned long runSeconds,
+ struct RzbSmokeReport *report)
+{
+ RzbNextRuntime_t *runtime =
+ RzbNextRuntime_CreateGenerated(config->nuggetUuid);
+ time_t deadline = time(NULL) + (time_t)runSeconds;
+ time_t nextLiveness = 0;
+ time_t nextApiCheck = 0;
+ time_t nextOperationalCheck = 0;
+ bool ok = false;
+
+ if (runtime == NULL)
+ return false;
+ RzbNextRuntime_Initialize(runtime);
+ while (time(NULL) < deadline) {
+ char *jsonMessage = RzbSmoke_ConsumeOne(broker, 1);
+
+ if (jsonMessage != NULL) {
+ report->deliveries++;
+ if (!RzbSmoke_HandleMessage(runtime, config, operation, broker,
+ jsonMessage, report)) {
+ free(jsonMessage);
+ goto cleanup;
+ }
+ free(jsonMessage);
+ }
+ if (RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_READY &&
+ time(NULL) >= nextLiveness) {
+ if (!RzbSmoke_PublishLiveness(runtime, broker, report))
+ goto cleanup;
+ nextLiveness = time(NULL) + 10;
+ }
+ if (RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_READY &&
+ !report->liveStateSeen && time(NULL) >= nextApiCheck) {
+ report->liveStateSeen = RzbSmoke_ApiLiveState(config, bearer);
+ nextApiCheck = time(NULL) + 1;
+ }
+ if (operation != NULL &&
+ config->role == RZB_SMOKE_ROLE_SOURCE &&
+ RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_READY &&
+ report->liveStateSeen && time(NULL) >= nextOperationalCheck) {
+ bool peerReady = true;
+
+ if (peerConfig != NULL)
+ peerReady = RzbSmoke_ApiLiveState(peerConfig, bearer);
+ if (!report->blockSubmissionPublished && peerReady) {
+ if (!RzbSmoke_PublishOperationalMessages(broker, config,
+ operation, report))
+ goto cleanup;
+ }
+ if (report->blockSubmissionPublished &&
+ !report->activeInspectionSeen) {
+ bool present = false;
+
+ if (RzbSmoke_ActiveInspectionPresent(config, bearer,
+ operation, &present) &&
+ present) {
+ report->activeInspectionSeen = true;
+ }
+ }
+ if (report->blockSubmissionPublished && !report->blockSeen) {
+ bool present = false;
+
+ if (RzbSmoke_BlockPresent(config, bearer, operation,
+ &present) &&
+ present) {
+ report->blockSeen = true;
+ }
+ }
+ nextOperationalCheck = time(NULL) + 1;
+ }
+ if (operation != NULL && RzbSmoke_ReportComplete(config, operation,
+ report)) {
+ break;
+ }
+ }
+ if (!report->byePublished) {
+ char *createdAt = RzbSmoke_Timestamp();
+ char *bye = RzbNextRuntime_BuildBye(runtime, "shutdown", createdAt);
+
+ if (bye == NULL || !RzbSmoke_Publish(broker, bye, 0U)) {
+ free(createdAt);
+ free(bye);
+ goto cleanup;
+ }
+ report->byePublished = true;
+ free(createdAt);
+ free(bye);
+ }
+ ok = RzbSmoke_ReportComplete(config, operation, report);
+ if (!ok) {
+ fprintf(stderr,
+ "dispatcher-next C %s live smoke incomplete: "
+ "deliveries=%zu registration=%s liveness=%s bye=%s "
+ "api_live_state=%s block_submission=%s block_update=%s "
+ "inspection_work=%s analysis_result=%s "
+ "active_inspection=%s block_seen=%s\n",
+ config->label, report->deliveries,
+ report->registrationPublished ? "true" : "false",
+ report->livenessPublished ? "true" : "false",
+ report->byePublished ? "true" : "false",
+ report->liveStateSeen ? "true" : "false",
+ report->blockSubmissionPublished ? "true" : "false",
+ report->blockUpdatePublished ? "true" : "false",
+ report->inspectionWorkReceived ? "true" : "false",
+ report->analysisResultPublished ? "true" : "false",
+ report->activeInspectionSeen ? "true" : "false",
+ report->blockSeen ? "true" : "false");
+ }
+
+cleanup:
+ RzbNextRuntime_Destroy(runtime);
+ return ok;
+}
+
+static bool
+RzbSmoke_RunLiveConfigured(const struct RzbSmokeConfig *config,
+ const char *bearer,
+ const struct RzbSmokeConfig *peerConfig,
+ struct RzbSmokeOperation *operation)
+{
+ struct RzbSmokeBroker broker;
+ struct RzbSmokeReport report;
+ unsigned long runSeconds;
+ unsigned long cleanupSeconds;
+ time_t cleanupDeadline;
+ bool cleaned = false;
+
+ memset(&report, 0, sizeof(report));
+ if (!RzbSmoke_BrokerConnect(config->rabbitmqUrl, &broker)) {
+ fprintf(stderr, "dispatcher-next C %s live smoke broker connect failed\n",
+ config->label);
+ return false;
+ }
+ if (!RzbSmoke_DeclareTopology(&broker, config) ||
+ !RzbSmoke_DuplicateDirectedQueueRejected(config) ||
+ !RzbSmoke_StartConsumers(&broker)) {
+ fprintf(stderr, "dispatcher-next C %s live smoke topology failed\n",
+ config->label);
+ RzbSmoke_BrokerClose(&broker);
+ return false;
+ }
+ runSeconds = RzbSmoke_EnvSeconds("RZB_SMOKE_LIVE_SECONDS",
+ RZB_SMOKE_DEFAULT_SECONDS);
+ if (!RzbSmoke_RunBrokerLoop(config, bearer, peerConfig, operation,
+ &broker, runSeconds, &report)) {
+ fprintf(stderr, "dispatcher-next C %s live smoke loop failed\n",
+ config->label);
+ RzbSmoke_BrokerClose(&broker);
+ return false;
+ }
+ RzbSmoke_BrokerClose(&broker);
+ cleanupSeconds = RzbSmoke_EnvSeconds("RZB_SMOKE_BYE_CLEANUP_SECONDS", 10);
+ cleanupDeadline = time(NULL) + (time_t)cleanupSeconds;
+ while (time(NULL) <= cleanupDeadline) {
+ if (RzbSmoke_ApiByeCleanup(config, bearer)) {
+ cleaned = true;
+ break;
+ }
+ sleep(1);
+ }
+ if (!cleaned) {
+ fprintf(stderr, "dispatcher-next C %s live smoke BYE cleanup failed\n",
+ config->label);
+ return false;
+ }
+ printf("dispatcher-next C %s live smoke client passed after %lus\n",
+ config->label, runSeconds);
+ return true;
+}
+
+static bool RZB_SMOKE_UNUSED
+RzbSmoke_RunLive(enum RzbSmokeRole role)
+{
+ struct RzbSmokeConfig config;
+ char *bearer;
+ bool ok;
+
+ RzbSmoke_DefaultConfig(role, &config);
+ bearer = RzbSmoke_BearerToken();
+ if (!RzbSmoke_EnvFalse("RZB_SMOKE_API_AUTH") && bearer == NULL) {
+ fprintf(stderr, "dispatcher-next C %s live smoke could not get token\n",
+ config.label);
+ return false;
+ }
+ if (!RzbSmoke_EnsureCatalog(&config, bearer)) {
+ fprintf(stderr, "dispatcher-next C %s live smoke catalog setup failed\n",
+ config.label);
+ free(bearer);
+ return false;
+ }
+ ok = RzbSmoke_RunLiveConfigured(&config, bearer, NULL, NULL);
+ free(bearer);
+ return ok;
+}
+
+static bool RZB_SMOKE_UNUSED
+RzbSmoke_RunLiveOperational(void)
+{
+ struct RzbSmokeConfig source;
+ struct RzbSmokeConfig inspector;
+ struct RzbSmokeOperation operation;
+ char *bearer;
+ pid_t child;
+ int status = 0;
+ bool sourceOk;
+ bool childOk;
+
+ RzbSmoke_DefaultConfig(RZB_SMOKE_ROLE_SOURCE, &source);
+ RzbSmoke_DefaultConfig(RZB_SMOKE_ROLE_INSPECTOR, &inspector);
+ if (!RzbSmoke_OperationInit(&inspector, &operation))
+ return false;
+ bearer = RzbSmoke_BearerToken();
+ if (!RzbSmoke_EnvFalse("RZB_SMOKE_API_AUTH") && bearer == NULL) {
+ fprintf(stderr, "dispatcher-next C operational live smoke could not get token\n");
+ RzbSmoke_OperationClear(&operation);
+ return false;
+ }
+ if (!RzbSmoke_EnsureCatalog(&source, bearer) ||
+ !RzbSmoke_EnsureCatalog(&inspector, bearer)) {
+ fprintf(stderr, "dispatcher-next C operational live smoke catalog setup failed\n");
+ free(bearer);
+ RzbSmoke_OperationClear(&operation);
+ return false;
+ }
+ child = fork();
+ if (child < 0) {
+ free(bearer);
+ RzbSmoke_OperationClear(&operation);
+ return false;
+ }
+ if (child == 0) {
+ bool ok = RzbSmoke_RunLiveConfigured(&inspector, bearer, NULL,
+ &operation);
+
+ free(bearer);
+ RzbSmoke_OperationClear(&operation);
+ _exit(ok ? 0 : 1);
+ }
+ sourceOk = RzbSmoke_RunLiveConfigured(&source, bearer, &inspector,
+ &operation);
+ if (waitpid(child, &status, 0) < 0)
+ childOk = false;
+ else
+ childOk = WIFEXITED(status) && WEXITSTATUS(status) == 0;
+ if (sourceOk && childOk) {
+ if (!RzbSmoke_WriteOperationReport(&operation, "c"))
+ sourceOk = false;
+ }
+ if (sourceOk && childOk) {
+ printf("dispatcher-next C operational live smoke passed for event %s "
+ "and block %s:%llu\n",
+ operation.eventId, operation.sha256, operation.size);
+ }
+ free(bearer);
+ RzbSmoke_OperationClear(&operation);
+ return sourceOk && childOk;
+}
+
+#endif /* RAZORBACK_TESTS_SMOKE_LIVE_NEXT_H */
diff --git a/tests/smoke_operational_next.c b/tests/smoke_operational_next.c
new file mode 100644
index 0000000..2cc6670
--- /dev/null
+++ b/tests/smoke_operational_next.c
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "smoke_live_next.h"
+
+int
+main(void)
+{
+ if (!RzbSmoke_LiveEnabled()) {
+ puts("dispatcher-next C operational smoke requires RZB_SMOKE_LIVE=1");
+ return 0;
+ }
+ return RzbSmoke_RunLiveOperational() ? 0 : 1;
+}
diff --git a/tests/smoke_source_next.c b/tests/smoke_source_next.c
new file mode 100644
index 0000000..e6c8a89
--- /dev/null
+++ b/tests/smoke_source_next.c
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include "smoke_live_next.h"
+
+#define SOURCE_NUGGET_UUID "10000000-0000-4000-8000-000000000001"
+#define REQUEST_ID "10000000-0000-4000-8000-000000000101"
+#define GENERATION "10000000-0000-4000-8000-000000000201"
+#define CREATED_AT "2026-06-25T00:00:00.000Z"
+
+static const char *DISPATCHER_HELLO =
+ "{"
+ "\"schema_name\":\"razorback.cnc.dispatcher_hello\","
+ "\"schema_version\":1,"
+ "\"dispatcher_id\":\"77777777-7777-4777-8777-777777777777\","
+ "\"created_at\":\"" CREATED_AT "\","
+ "\"started_at\":\"" CREATED_AT "\","
+ "\"ready\":true,"
+ "\"availability\":\"ready\","
+ "\"dependency_reason_codes\":[]"
+ "}";
+
+static const char *REGISTRATION_ACCEPTED =
+ "{"
+ "\"schema_name\":\"razorback.cnc.registration_accepted\","
+ "\"schema_version\":1,"
+ "\"request_id\":\"" REQUEST_ID "\","
+ "\"nugget_uuid\":\"" SOURCE_NUGGET_UUID "\","
+ "\"registration_generation\":\"" GENERATION "\","
+ "\"effective_runtime_policy\":\"running\","
+ "\"liveness_interval\":10,"
+ "\"liveness_freshness_window\":30,"
+ "\"liveness_clock_skew_tolerance\":5,"
+ "\"created_at\":\"" CREATED_AT "\""
+ "}";
+
+static void
+directed(char *buffer, size_t bufferSize, const char *command)
+{
+ snprintf(buffer, bufferSize,
+ "{"
+ "\"schema_name\":\"razorback.cnc.directed_command\","
+ "\"schema_version\":1,"
+ "\"command_id\":\"10000000-0000-4000-8000-000000000301\","
+ "\"target_nugget_uuid\":\"" SOURCE_NUGGET_UUID "\","
+ "\"registration_generation\":\"" GENERATION "\","
+ "\"command\":\"%s\","
+ "\"reason_code\":\"operator_requested\","
+ "\"created_at\":\"" CREATED_AT "\""
+ "}",
+ command);
+}
+
+int
+main(void)
+{
+ RzbNextRuntime_t *runtime;
+ struct RzbNextRuntimeTransition transition;
+ struct RzbNextRuntimeDirectedResult result;
+ struct RzbNextRuntimeLivenessPlan liveness;
+ char command[1024];
+ char *bye;
+
+ if (RzbSmoke_LiveEnabled())
+ return RzbSmoke_RunLive(RZB_SMOKE_ROLE_SOURCE) ? 0 : 1;
+
+ runtime = RzbNextRuntime_CreateGenerated(SOURCE_NUGGET_UUID);
+ assert(runtime != NULL);
+ assert(strcmp(RzbNextRuntime_ProcessUuid(runtime), SOURCE_NUGGET_UUID) != 0);
+
+ RzbNextRuntime_Initialize(runtime);
+ assert(RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_WAITING_FOR_DISPATCHER);
+ assert(RzbNextRuntime_ObserveDispatcherHello(runtime, DISPATCHER_HELLO));
+ assert(RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_REGISTERING);
+ assert(RzbNextRuntime_BeginRegistration(runtime, REQUEST_ID));
+ assert(RzbNextRuntime_RegistrationAccepted(runtime, REGISTRATION_ACCEPTED,
+ &transition));
+ assert(transition.kind == RZB_NEXT_RUNTIME_TRANSITION_REGISTERED);
+ assert(transition.ready);
+ assert(RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_READY);
+
+ assert(RzbNextRuntime_LivenessPlan(runtime, CREATED_AT, &liveness));
+ assert(liveness.interval == 10);
+ assert(liveness.messageExpiration == 30);
+ RzbNextRuntime_LivenessPlanClear(&liveness);
+
+ directed(command, sizeof(command), "pause");
+ assert(RzbNextRuntime_ApplyDirectedCommand(runtime, command, &result));
+ assert(result.effect == RZB_NEXT_RUNTIME_DIRECTED_PAUSE);
+ assert(RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_PAUSED);
+
+ directed(command, sizeof(command), "go");
+ assert(RzbNextRuntime_ApplyDirectedCommand(runtime, command, &result));
+ assert(result.effect == RZB_NEXT_RUNTIME_DIRECTED_RESUME);
+ assert(RzbNextRuntime_State(runtime) == RZB_NEXT_RUNTIME_READY);
+
+ bye = RzbNextRuntime_BuildBye(runtime, "shutdown", CREATED_AT);
+ assert(bye != NULL);
+ free(bye);
+
+ RzbNextRuntime_Destroy(runtime);
+ puts("dispatcher-next C source smoke client passed");
+ return 0;
+}
diff --git a/tests/test_config_next.c b/tests/test_config_next.c
new file mode 100644
index 0000000..bf1985f
--- /dev/null
+++ b/tests/test_config_next.c
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2011-2026 Cisco Systems, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ */
+
+#include "config.h"
+
+#include
+
+#include
+
+#include