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 +#include +#include +#include + +static char * +write_temp_yaml(const char *contents) +{ + char template[] = "/tmp/rzb-config-next-XXXXXX"; + int fd; + FILE *file; + char *path; + + fd = mkstemp(template); + ck_assert_int_ge(fd, 0); + file = fdopen(fd, "wb"); + ck_assert_ptr_ne(file, NULL); + ck_assert_int_ge(fputs(contents, file), 0); + ck_assert_int_eq(fclose(file), 0); + path = strdup(template); + ck_assert_ptr_ne(path, NULL); + return path; +} + +static void +remove_temp_yaml(char *path) +{ + if (path == NULL) + return; + unlink(path); + free(path); +} + +START_TEST(test_config_next_applies_base_local_and_env_precedence) +{ + int maxInlineBytes = 0; + char *host = NULL; + bool tls = true; + char *baseFile; + char *localFile; + struct RzbNextConfigKey keys[] = { + { "message.max_inline_bytes", RZB_NEXT_CONFIG_INT, &maxInlineBytes }, + { "rabbitmq.host", RZB_NEXT_CONFIG_STRING, &host }, + { "rabbitmq.tls", RZB_NEXT_CONFIG_BOOL, &tls }, + { NULL, 0, NULL } + }; + + baseFile = write_temp_yaml( + "message:\n" + " max_inline_bytes: 1024\n" + "rabbitmq:\n" + " host: base-rabbitmq\n" + " tls: false\n" + ); + localFile = write_temp_yaml( + "rabbitmq:\n" + " host: local-rabbitmq\n" + ); + ck_assert_int_eq(setenv("RZB_TESTCFG_MESSAGE__MAX_INLINE_BYTES", + "2048", 1), 0); + ck_assert(RzbNextConfig_Load(baseFile, localFile, "RZB_TESTCFG", keys)); + ck_assert_int_eq(maxInlineBytes, 2048); + ck_assert_str_eq(host, "local-rabbitmq"); + ck_assert(!tls); + + unsetenv("RZB_TESTCFG_MESSAGE__MAX_INLINE_BYTES"); + free(host); + remove_temp_yaml(baseFile); + remove_temp_yaml(localFile); +} +END_TEST + +START_TEST(test_config_next_rejects_unknown_yaml_keys) +{ + int maxInlineBytes = 0; + char *baseFile; + struct RzbNextConfigKey keys[] = { + { "message.max_inline_bytes", RZB_NEXT_CONFIG_INT, &maxInlineBytes }, + { NULL, 0, NULL } + }; + + baseFile = write_temp_yaml( + "message:\n" + " max_inline_bytes: 1024\n" + " unknown: true\n" + ); + ck_assert(!RzbNextConfig_Load(baseFile, NULL, "RZB_TESTCFG", keys)); + remove_temp_yaml(baseFile); +} +END_TEST + +START_TEST(test_config_next_rejects_unknown_owned_env_overrides) +{ + int maxInlineBytes = 0; + char *baseFile; + struct RzbNextConfigKey keys[] = { + { "message.max_inline_bytes", RZB_NEXT_CONFIG_INT, &maxInlineBytes }, + { NULL, 0, NULL } + }; + + baseFile = write_temp_yaml( + "message:\n" + " max_inline_bytes: 1024\n" + ); + ck_assert_int_eq(setenv("RZB_TESTCFG_MESSAGE__UNKNOWN", "true", 1), 0); + ck_assert(!RzbNextConfig_Load(baseFile, NULL, "RZB_TESTCFG", keys)); + unsetenv("RZB_TESTCFG_MESSAGE__UNKNOWN"); + remove_temp_yaml(baseFile); +} +END_TEST + +START_TEST(test_config_next_ignores_unowned_env_overrides) +{ + int maxInlineBytes = 0; + char *baseFile; + struct RzbNextConfigKey keys[] = { + { "message.max_inline_bytes", RZB_NEXT_CONFIG_INT, &maxInlineBytes }, + { NULL, 0, NULL } + }; + + baseFile = write_temp_yaml( + "message:\n" + " max_inline_bytes: 1024\n" + ); + ck_assert_int_eq(setenv("RZB_TESTCFG_OTHER__UNKNOWN", "true", 1), 0); + ck_assert(RzbNextConfig_Load(baseFile, NULL, "RZB_TESTCFG", keys)); + ck_assert_int_eq(maxInlineBytes, 1024); + unsetenv("RZB_TESTCFG_OTHER__UNKNOWN"); + remove_temp_yaml(baseFile); +} +END_TEST + +static Suite * +config_next_suite(void) +{ + Suite *suite; + TCase *testcase; + + suite = suite_create("config_next"); + testcase = tcase_create("core"); + tcase_add_test(testcase, + test_config_next_applies_base_local_and_env_precedence); + tcase_add_test(testcase, test_config_next_rejects_unknown_yaml_keys); + tcase_add_test(testcase, + test_config_next_rejects_unknown_owned_env_overrides); + tcase_add_test(testcase, test_config_next_ignores_unowned_env_overrides); + suite_add_tcase(suite, testcase); + return suite; +} + +int +main(void) +{ + Suite *suite; + SRunner *runner; + int failed; + + suite = config_next_suite(); + runner = srunner_create(suite); + srunner_run_all(runner, CK_NORMAL); + failed = srunner_ntests_failed(runner); + srunner_free(runner); + return failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_fileserver.c b/tests/test_fileserver.c new file mode 100644 index 0000000..c3282e1 --- /dev/null +++ b/tests/test_fileserver.c @@ -0,0 +1,666 @@ +/* + * 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 + +#define TEST_SERVER_MAX_REQUESTS 4U +#define TEST_SERVER_REQUEST_SIZE 32768U + +struct TestResponse +{ + unsigned int status; + const char *reason; + const uint8_t *body; + size_t bodyLength; + const char *contentType; +}; + +struct TestServer +{ + int listenFd; + uint16_t port; + pthread_t thread; + const struct TestResponse *responses; + size_t responseCount; + size_t requestCount; + bool failed; + char requests[TEST_SERVER_MAX_REQUESTS][TEST_SERVER_REQUEST_SIZE]; +}; + +static struct BlockId * +create_sha256_block_id(const char *hashText, uint64_t length) +{ + struct BlockId *blockId; + + blockId = calloc(1, sizeof(*blockId)); + ck_assert_ptr_ne(blockId, NULL); + blockId->pHash = Hash_Create_From_String(HASH_TYPE_SHA256, hashText); + ck_assert_ptr_ne(blockId->pHash, NULL); + blockId->iLength = length; + uuid_clear(blockId->uuidDataType); + return blockId; +} + +static char * +hash_text_for_payload(const uint8_t *payload, size_t length) +{ + struct Hash *hash; + char *hashText; + + hash = Hash_Create_Type(HASH_TYPE_SHA256); + ck_assert_ptr_ne(hash, NULL); + ck_assert(Hash_Update(hash, (uint8_t *)payload, (uint32_t)length)); + ck_assert(Hash_Finalize(hash)); + hashText = Hash_ToText(hash); + ck_assert_ptr_ne(hashText, NULL); + Hash_Destroy(hash); + return hashText; +} + +static struct BlockId * +create_sha256_block_id_for_payload(const uint8_t *payload, size_t payloadLength, + uint64_t blockLength) +{ + char *hashText = hash_text_for_payload(payload, payloadLength); + struct BlockId *blockId = create_sha256_block_id(hashText, blockLength); + + free(hashText); + return blockId; +} + +static void +destroy_block_id(struct BlockId *blockId) +{ + if (blockId == NULL) + return; + Hash_Destroy(blockId->pHash); + free(blockId); +} + +static size_t +parse_content_length(const char *request) +{ + const char *line = request; + + while (line != NULL && *line != '\0') { + const char *next = strstr(line, "\r\n"); + size_t lineLength = next == NULL ? strlen(line) : (size_t)(next - line); + + if (lineLength >= 15U && + strncasecmp(line, "Content-Length:", 15U) == 0) { + return (size_t)strtoull(line + 15U, NULL, 10); + } + if (next == NULL) + break; + line = next + 2; + } + return 0; +} + +static void +test_server_read_request(int clientFd, char *buffer, size_t bufferSize) +{ + size_t used = 0; + size_t headerLength = 0; + size_t contentLength = 0; + + while (used + 1U < bufferSize) { + ssize_t count = recv(clientFd, buffer + used, bufferSize - used - 1U, 0); + char *headerEnd; + + if (count <= 0) + break; + used += (size_t)count; + buffer[used] = '\0'; + headerEnd = strstr(buffer, "\r\n\r\n"); + if (headerEnd == NULL) + continue; + if (headerLength == 0) { + headerLength = (size_t)(headerEnd - buffer) + 4U; + contentLength = parse_content_length(buffer); + } + if (used >= headerLength + contentLength) + break; + } + buffer[used] = '\0'; +} + +static bool +test_server_write_all(int fd, const void *data, size_t length) +{ + const uint8_t *bytes = data; + size_t written = 0; + + while (written < length) { + ssize_t count = send(fd, bytes + written, length - written, 0); + if (count <= 0) + return false; + written += (size_t)count; + } + return true; +} + +static void +test_server_send_response(int clientFd, const struct TestResponse *response, + bool *failed) +{ + char header[512]; + int length; + const char *contentType = response->contentType == NULL ? + "text/plain" : response->contentType; + + length = snprintf(header, sizeof(header), + "HTTP/1.1 %u %s\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n" + "Content-Type: %s\r\n" + "\r\n", + response->status, response->reason, + response->bodyLength, contentType); + if (length < 0 || (size_t)length >= sizeof(header) || + !test_server_write_all(clientFd, header, (size_t)length)) { + *failed = true; + return; + } + if (response->bodyLength > 0 && + !test_server_write_all(clientFd, response->body, + response->bodyLength)) { + *failed = true; + } +} + +static void * +test_server_thread(void *userData) +{ + struct TestServer *server = userData; + size_t index; + + for (index = 0; index < server->responseCount; index++) { + struct sockaddr_in address; + socklen_t addressLength = sizeof(address); + int clientFd; + + clientFd = accept(server->listenFd, (struct sockaddr *)&address, + &addressLength); + if (clientFd < 0) { + server->failed = true; + break; + } + test_server_read_request(clientFd, server->requests[index], + sizeof(server->requests[index])); + test_server_send_response(clientFd, &server->responses[index], + &server->failed); + close(clientFd); + server->requestCount++; + } + close(server->listenFd); + return NULL; +} + +static void +test_server_start(struct TestServer *server, + const struct TestResponse *responses, + size_t responseCount) +{ + struct sockaddr_in address; + socklen_t addressLength = sizeof(address); + struct timeval timeout = { 5, 0 }; + int enabled = 1; + + ck_assert(responseCount <= TEST_SERVER_MAX_REQUESTS); + memset(server, 0, sizeof(*server)); + server->responses = responses; + server->responseCount = responseCount; + server->listenFd = socket(AF_INET, SOCK_STREAM, 0); + ck_assert_msg(server->listenFd >= 0, "socket failed: %s", + strerror(errno)); + ck_assert_int_eq(setsockopt(server->listenFd, SOL_SOCKET, SO_REUSEADDR, + &enabled, sizeof(enabled)), 0); + ck_assert_int_eq(setsockopt(server->listenFd, SOL_SOCKET, SO_RCVTIMEO, + &timeout, sizeof(timeout)), 0); + memset(&address, 0, sizeof(address)); + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address.sin_port = htons(0); + ck_assert_int_eq(bind(server->listenFd, (struct sockaddr *)&address, + sizeof(address)), 0); + ck_assert_int_eq(listen(server->listenFd, 4), 0); + ck_assert_int_eq(getsockname(server->listenFd, (struct sockaddr *)&address, + &addressLength), 0); + server->port = ntohs(address.sin_port); + ck_assert_int_eq(pthread_create(&server->thread, NULL, test_server_thread, + server), 0); +} + +static void +test_server_join(struct TestServer *server) +{ + ck_assert_int_eq(pthread_join(server->thread, NULL), 0); + ck_assert(!server->failed); +} + +static char * +test_server_url(const struct TestServer *server) +{ + char *url = NULL; + + ck_assert_int_ne(asprintf(&url, "http://127.0.0.1:%u", server->port), -1); + ck_assert_ptr_ne(url, NULL); + return url; +} + +static char * +expected_block_path(const struct BlockId *blockId) +{ + char *hashText = Hash_ToText(blockId->pHash); + char *path = NULL; + + ck_assert_ptr_ne(hashText, NULL); + ck_assert_int_ne(asprintf(&path, "/%c/%c/%c/%c/%s.%ju", + hashText[0], hashText[1], hashText[2], + hashText[3], hashText, + (uintmax_t)blockId->iLength), -1); + ck_assert_ptr_ne(path, NULL); + free(hashText); + return path; +} + +static void +assert_request_starts_with_path(const char *request, const char *method, + const struct BlockId *blockId) +{ + char *path = expected_block_path(blockId); + char *expected = NULL; + + ck_assert_int_ne(asprintf(&expected, "%s %s HTTP/1.1", method, path), -1); + ck_assert_msg(strncmp(request, expected, strlen(expected)) == 0, + "request %s did not start with %s", request, expected); + free(expected); + free(path); +} + +START_TEST(test_build_url_uses_canonical_hash_path) +{ + const char *hashText = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + struct BlockId *blockId = create_sha256_block_id(hashText, 12345U); + RzbNextFileserverClient_t *client; + char *url = NULL; + + client = RzbNextFileserverClient_Create("https://files.example.test/root/", + 0, 0); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_BuildUrl(client, blockId, &url), + RZB_NEXT_FILESERVER_OK); + ck_assert_str_eq(url, + "https://files.example.test/root/0/1/2/3/" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef.12345"); + + free(url); + RzbNextFileserverClient_Destroy(client); + destroy_block_id(blockId); +} +END_TEST + +START_TEST(test_build_url_uses_compose_default_base_url) +{ + const char *hashText = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + struct BlockId *blockId = create_sha256_block_id(hashText, 12345U); + RzbNextFileserverClient_t *client; + char *url = NULL; + + unsetenv("RZB_FILESERVER__URL"); + unsetenv("RZB_FILESERVER_URL"); + + client = RzbNextFileserverClient_Create(NULL, 0, 0); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_BuildUrl(client, blockId, &url), + RZB_NEXT_FILESERVER_OK); + ck_assert_str_eq(url, + "http://file-server:8080/0/1/2/3/" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef.12345"); + + free(url); + RzbNextFileserverClient_Destroy(client); + destroy_block_id(blockId); +} +END_TEST + +START_TEST(test_build_url_rejects_non_sha256_identity) +{ + struct BlockId *blockId = create_sha256_block_id( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + 12345U + ); + RzbNextFileserverClient_t *client; + char *url = NULL; + + blockId->pHash->iType = HASH_TYPE_SHA1; + client = RzbNextFileserverClient_Create("https://files.example.test", 0, 0); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_BuildUrl(client, blockId, &url), + RZB_NEXT_FILESERVER_LOCAL_ERROR); + ck_assert_ptr_eq(url, NULL); + + RzbNextFileserverClient_Destroy(client); + destroy_block_id(blockId); +} +END_TEST + +START_TEST(test_store_bytes_posts_multipart_to_canonical_path) +{ + const uint8_t payload[] = "razorback c fileserver payload"; + const struct TestResponse responses[] = { + { 201U, "Created", NULL, 0, NULL } + }; + struct BlockId *blockId = create_sha256_block_id_for_payload( + payload, sizeof(payload) - 1U, sizeof(payload) - 1U + ); + struct TestServer server; + RzbNextFileserverClient_t *client; + char *url; + + test_server_start(&server, responses, 1U); + url = test_server_url(&server); + client = RzbNextFileserverClient_Create(url, 1, 1); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_StoreBytes(client, blockId, payload, + sizeof(payload) - 1U), + RZB_NEXT_FILESERVER_OK); + test_server_join(&server); + ck_assert_int_eq(server.requestCount, 1); + assert_request_starts_with_path(server.requests[0], "POST", blockId); + ck_assert_ptr_ne(strstr(server.requests[0], "name=\"file\""), NULL); + + RzbNextFileserverClient_Destroy(client); + free(url); + destroy_block_id(blockId); +} +END_TEST + +START_TEST(test_store_bytes_treats_duplicate_upload_as_success) +{ + const uint8_t payload[] = "duplicate block payload"; + const uint8_t body[] = "already exists"; + const struct TestResponse responses[] = { + { 400U, "Bad Request", body, sizeof(body) - 1U, NULL } + }; + struct BlockId *blockId = create_sha256_block_id_for_payload( + payload, sizeof(payload) - 1U, sizeof(payload) - 1U + ); + struct TestServer server; + RzbNextFileserverClient_t *client; + char *url; + + test_server_start(&server, responses, 1U); + url = test_server_url(&server); + client = RzbNextFileserverClient_Create(url, 1, 1); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_StoreBytes(client, blockId, payload, + sizeof(payload) - 1U), + RZB_NEXT_FILESERVER_OK); + test_server_join(&server); + ck_assert_int_eq(server.requestCount, 1); + + RzbNextFileserverClient_Destroy(client); + free(url); + destroy_block_id(blockId); +} +END_TEST + +START_TEST(test_store_bytes_retries_retryable_http_failure) +{ + const uint8_t payload[] = "retryable upload payload"; + const struct TestResponse responses[] = { + { 500U, "Internal Server Error", NULL, 0, NULL }, + { 204U, "No Content", NULL, 0, NULL } + }; + struct BlockId *blockId = create_sha256_block_id_for_payload( + payload, sizeof(payload) - 1U, sizeof(payload) - 1U + ); + struct TestServer server; + RzbNextFileserverClient_t *client; + char *url; + + test_server_start(&server, responses, 2U); + url = test_server_url(&server); + client = RzbNextFileserverClient_Create(url, 1, 1); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_StoreBytes(client, blockId, payload, + sizeof(payload) - 1U), + RZB_NEXT_FILESERVER_OK); + test_server_join(&server); + ck_assert_int_eq(server.requestCount, 2); + + RzbNextFileserverClient_Destroy(client); + free(url); + destroy_block_id(blockId); +} +END_TEST + +START_TEST(test_store_bytes_rejects_payload_mismatch_before_http) +{ + const uint8_t payload[] = "payload mismatch"; + struct BlockId *blockId = create_sha256_block_id_for_payload( + payload, sizeof(payload) - 1U, sizeof(payload) + ); + RzbNextFileserverClient_t *client; + + client = RzbNextFileserverClient_Create("http://127.0.0.1:1", 1, 1); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_StoreBytes(client, blockId, payload, + sizeof(payload) - 1U), + RZB_NEXT_FILESERVER_PAYLOAD_MISMATCH); + + RzbNextFileserverClient_Destroy(client); + destroy_block_id(blockId); +} +END_TEST + +START_TEST(test_fetch_to_file_streams_and_verifies_payload) +{ + const uint8_t payload[] = "fetch success payload"; + const struct TestResponse responses[] = { + { 200U, "OK", payload, sizeof(payload) - 1U, + "application/octet-stream" } + }; + struct BlockId *blockId = create_sha256_block_id_for_payload( + payload, sizeof(payload) - 1U, sizeof(payload) - 1U + ); + struct TestServer server; + RzbNextFileserverClient_t *client; + char *url; + char *fileName = NULL; + FILE *file; + uint8_t buffer[sizeof(payload)]; + size_t readLength; + + test_server_start(&server, responses, 1U); + url = test_server_url(&server); + client = RzbNextFileserverClient_Create(url, 1, 1); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_FetchToFile(client, blockId, &fileName), + RZB_NEXT_FILESERVER_OK); + test_server_join(&server); + ck_assert_int_eq(server.requestCount, 1); + assert_request_starts_with_path(server.requests[0], "GET", blockId); + ck_assert_ptr_ne(fileName, NULL); + file = fopen(fileName, "rb"); + ck_assert_ptr_ne(file, NULL); + readLength = fread(buffer, 1, sizeof(buffer), file); + ck_assert_int_eq(fclose(file), 0); + ck_assert_int_eq(readLength, sizeof(payload) - 1U); + ck_assert_int_eq(memcmp(buffer, payload, sizeof(payload) - 1U), 0); + + unlink(fileName); + free(fileName); + RzbNextFileserverClient_Destroy(client); + free(url); + destroy_block_id(blockId); +} +END_TEST + +START_TEST(test_fetch_to_file_reports_not_found) +{ + const uint8_t payload[] = "not found payload"; + const uint8_t body[] = "missing"; + const struct TestResponse responses[] = { + { 404U, "Not Found", body, sizeof(body) - 1U, NULL } + }; + struct BlockId *blockId = create_sha256_block_id_for_payload( + payload, sizeof(payload) - 1U, sizeof(payload) - 1U + ); + struct TestServer server; + RzbNextFileserverClient_t *client; + char *url; + char *fileName = NULL; + + test_server_start(&server, responses, 1U); + url = test_server_url(&server); + client = RzbNextFileserverClient_Create(url, 1, 1); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_FetchToFile(client, blockId, &fileName), + RZB_NEXT_FILESERVER_NOT_FOUND); + test_server_join(&server); + ck_assert_int_eq(server.requestCount, 1); + ck_assert_ptr_eq(fileName, NULL); + + RzbNextFileserverClient_Destroy(client); + free(url); + destroy_block_id(blockId); +} +END_TEST + +START_TEST(test_fetch_to_file_rejects_size_mismatch) +{ + const uint8_t payload[] = "size mismatch payload"; + const struct TestResponse responses[] = { + { 200U, "OK", payload, sizeof(payload) - 1U, + "application/octet-stream" } + }; + struct BlockId *blockId = create_sha256_block_id_for_payload( + payload, sizeof(payload) - 1U, sizeof(payload) + ); + struct TestServer server; + RzbNextFileserverClient_t *client; + char *url; + char *fileName = NULL; + + test_server_start(&server, responses, 1U); + url = test_server_url(&server); + client = RzbNextFileserverClient_Create(url, 1, 1); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_FetchToFile(client, blockId, &fileName), + RZB_NEXT_FILESERVER_PAYLOAD_MISMATCH); + test_server_join(&server); + ck_assert_int_eq(server.requestCount, 1); + ck_assert_ptr_eq(fileName, NULL); + + RzbNextFileserverClient_Destroy(client); + free(url); + destroy_block_id(blockId); +} +END_TEST + +START_TEST(test_fetch_to_file_rejects_hash_mismatch) +{ + const uint8_t payload[] = "hash mismatch payload"; + const uint8_t other[] = "different block body"; + const struct TestResponse responses[] = { + { 200U, "OK", payload, sizeof(payload) - 1U, + "application/octet-stream" } + }; + struct BlockId *blockId = create_sha256_block_id_for_payload( + other, sizeof(other) - 1U, sizeof(payload) - 1U + ); + struct TestServer server; + RzbNextFileserverClient_t *client; + char *url; + char *fileName = NULL; + + test_server_start(&server, responses, 1U); + url = test_server_url(&server); + client = RzbNextFileserverClient_Create(url, 1, 1); + ck_assert_ptr_ne(client, NULL); + ck_assert_int_eq(RzbNextFileserver_FetchToFile(client, blockId, &fileName), + RZB_NEXT_FILESERVER_PAYLOAD_MISMATCH); + test_server_join(&server); + ck_assert_int_eq(server.requestCount, 1); + ck_assert_ptr_eq(fileName, NULL); + + RzbNextFileserverClient_Destroy(client); + free(url); + destroy_block_id(blockId); +} +END_TEST + +static Suite * +fileserver_suite(void) +{ + Suite *suite; + TCase *testcase; + + suite = suite_create("fileserver"); + testcase = tcase_create("canonical-url"); + tcase_add_test(testcase, test_build_url_uses_canonical_hash_path); + tcase_add_test(testcase, test_build_url_uses_compose_default_base_url); + tcase_add_test(testcase, test_build_url_rejects_non_sha256_identity); + tcase_add_test(testcase, test_store_bytes_posts_multipart_to_canonical_path); + tcase_add_test(testcase, test_store_bytes_treats_duplicate_upload_as_success); + tcase_add_test(testcase, test_store_bytes_retries_retryable_http_failure); + tcase_add_test(testcase, test_store_bytes_rejects_payload_mismatch_before_http); + tcase_add_test(testcase, test_fetch_to_file_streams_and_verifies_payload); + tcase_add_test(testcase, test_fetch_to_file_reports_not_found); + tcase_add_test(testcase, test_fetch_to_file_rejects_size_mismatch); + tcase_add_test(testcase, test_fetch_to_file_rejects_hash_mismatch); + suite_add_tcase(suite, testcase); + return suite; +} + +int +main(void) +{ + Suite *suite = fileserver_suite(); + SRunner *runner = srunner_create(suite); + int failed; + + srunner_set_fork_status(runner, CK_NOFORK); + srunner_run_all(runner, CK_NORMAL); + failed = srunner_ntests_failed(runner); + srunner_free(runner); + return failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_health.c b/tests/test_health.c index 20fba7c..79ceb6d 100644 --- a/tests/test_health.c +++ b/tests/test_health.c @@ -353,20 +353,23 @@ START_TEST(test_health_livez_and_startupz) "GET /livez HTTP/1.1\r\n", response, sizeof(response)); ck_assert_ptr_nonnull(strstr(response, "HTTP/1.1 200 OK")); - ck_assert_ptr_nonnull(strstr(response, "\r\n\r\nok\n")); + ck_assert_ptr_nonnull(strstr(response, "\"service\":{\"name\":\"razorback-c-sdk-component\"")); + ck_assert_ptr_nonnull(strstr(response, "\"live\":true")); health_test_request(port, "GET /startupz HTTP/1.1\r\n", response, sizeof(response)); ck_assert_ptr_nonnull(strstr(response, "HTTP/1.1 503 Service Unavailable")); - ck_assert_ptr_nonnull(strstr(response, "\r\n\r\nunhealthy\n")); + ck_assert_ptr_nonnull(strstr(response, "\"status\":\"not_ready\"")); + ck_assert_ptr_nonnull(strstr(response, "\"startup_ready\":false")); + ck_assert_ptr_nonnull(strstr(response, "\"c_sdk_process_starting\"")); Razorback_Health_SetStartupComplete(true); health_test_request(port, "GET /startupz HTTP/1.1\r\n", response, sizeof(response)); ck_assert_ptr_nonnull(strstr(response, "HTTP/1.1 200 OK")); - ck_assert_ptr_nonnull(strstr(response, "\r\n\r\nok\n")); + ck_assert_ptr_nonnull(strstr(response, "\"startup_ready\":true")); } END_TEST @@ -391,7 +394,8 @@ START_TEST(test_health_readyz_requires_context_when_configured) "GET /readyz HTTP/1.1\r\n", response, sizeof(response)); ck_assert_ptr_nonnull(strstr(response, "HTTP/1.1 200 OK")); - ck_assert_ptr_nonnull(strstr(response, "\r\n\r\nok\n")); + ck_assert_ptr_nonnull(strstr(response, "\"ready\":true")); + ck_assert_ptr_nonnull(strstr(response, "\"workflows\":{\"c_sdk_process\":{\"state\":\"running\"")); health_test_reset_contexts(); health_test_destroy_context(&context); @@ -468,14 +472,18 @@ START_TEST(test_healthz_reports_aggregate_json) "GET /healthz HTTP/1.1\r\n", response, sizeof(response)); ck_assert_ptr_nonnull(strstr(response, "HTTP/1.1 200 OK")); - ck_assert_ptr_nonnull(strstr(response, "{\"live\":true,\"ready\":true,\"startup\":true}\n")); + ck_assert_ptr_nonnull(strstr(response, "\"status\":\"ok\"")); + ck_assert_ptr_nonnull(strstr(response, "\"ready\":true")); + ck_assert_ptr_nonnull(strstr(response, "\"startup\":true")); Razorback_Health_SetStartupComplete(false); health_test_request(port, "GET /healthz HTTP/1.1\r\n", response, sizeof(response)); ck_assert_ptr_nonnull(strstr(response, "HTTP/1.1 503 Service Unavailable")); - ck_assert_ptr_nonnull(strstr(response, "{\"live\":true,\"ready\":false,\"startup\":false}\n")); + ck_assert_ptr_nonnull(strstr(response, "\"status\":\"not_ready\"")); + ck_assert_ptr_nonnull(strstr(response, "\"ready\":false")); + ck_assert_ptr_nonnull(strstr(response, "\"startup\":false")); } END_TEST @@ -494,7 +502,7 @@ START_TEST(test_health_slow_client_does_not_block_listener) "GET /livez HTTP/1.1\r\n", response, sizeof(response)); ck_assert_ptr_nonnull(strstr(response, "HTTP/1.1 200 OK")); - ck_assert_ptr_nonnull(strstr(response, "\r\n\r\nok\n")); + ck_assert_ptr_nonnull(strstr(response, "\"live\":true")); close(slowClient); } @@ -530,7 +538,7 @@ START_TEST(test_health_start_rejects_concurrent_restart_during_stop) "GET /livez HTTP/1.1\r\n", response, sizeof(response)); ck_assert_ptr_nonnull(strstr(response, "HTTP/1.1 200 OK")); - ck_assert_ptr_nonnull(strstr(response, "\r\n\r\nok\n")); + ck_assert_ptr_nonnull(strstr(response, "\"live\":true")); close(slowClient); } diff --git a/tests/test_message_body.c b/tests/test_message_body.c new file mode 100644 index 0000000..a6dfb9a --- /dev/null +++ b/tests/test_message_body.c @@ -0,0 +1,343 @@ +/* + * 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 + +static char * +read_fixture(const char *name) +{ + char path[4096]; + FILE *file; + long size; + char *contents; + + snprintf(path, sizeof(path), "%s/%s", RAZORBACK_SCHEMA_FIXTURE_DIR, name); + file = fopen(path, "rb"); + ck_assert_ptr_ne(file, NULL); + ck_assert_int_eq(fseek(file, 0, SEEK_END), 0); + size = ftell(file); + ck_assert_int_ge(size, 0); + ck_assert_int_eq(fseek(file, 0, SEEK_SET), 0); + + contents = calloc((size_t)size + 1, sizeof(char)); + ck_assert_ptr_ne(contents, NULL); + ck_assert_uint_eq(fread(contents, 1, (size_t)size, file), (size_t)size); + fclose(file); + return contents; +} + +static struct ClaimCheckReference * +claim_check_template(void) +{ + return ClaimCheckReference_Create( + "http://object-store.local/claim-check/payload?signature=test", + "2026-06-17T21:15:00.000Z", + "claim-check", + "messages/2026/06/17/payload.zlib", + "application/json", + "razorback.messages.analysis_result", + 1 + ); +} + +START_TEST(test_message_body_default_policy_matches_messaging_threshold) +{ + struct MessageBodyPolicy policy; + + policy = MessageBodyPolicy_Default(); + ck_assert_uint_eq(policy.maxInlineBytes, + MESSAGE_BODY_DEFAULT_MAX_INLINE_BYTES); + ck_assert_uint_eq(policy.maxExpandedBytes, + MESSAGE_BODY_DEFAULT_MAX_EXPANDED_BYTES); + ck_assert_uint_eq(policy.maxInlineBytes, 921600U); + ck_assert_uint_eq(policy.maxExpandedBytes, 268435456U); +} +END_TEST + +START_TEST(test_message_body_keeps_small_payload_inline) +{ + const uint8_t body[] = "small"; + struct MessageBodyPolicy policy = MessageBodyPolicy_Default(); + struct EncodedMessageBody *encoded = NULL; + + policy.maxInlineBytes = 32; + ck_assert(MessageBody_Encode(&policy, body, sizeof(body) - 1, NULL, + &encoded)); + ck_assert_ptr_ne(encoded, NULL); + ck_assert_int_eq(encoded->mode, MESSAGE_BODY_MODE_INLINE); + ck_assert_str_eq(MessageBodyMode_ToString(encoded->mode), "inline"); + ck_assert_ptr_eq(encoded->contentEncoding, NULL); + ck_assert_ptr_eq(encoded->claimCheckBody, NULL); + ck_assert_uint_eq(encoded->transportBodySize, sizeof(body) - 1); + ck_assert_int_eq(memcmp(encoded->transportBody, body, sizeof(body) - 1), 0); + + EncodedMessageBody_Destroy(encoded); +} +END_TEST + +START_TEST(test_message_body_compresses_medium_payload_inline) +{ + uint8_t body[1024]; + struct MessageBodyPolicy policy = MessageBodyPolicy_Default(); + struct EncodedMessageBody *encoded = NULL; + uint8_t *decoded = NULL; + size_t decodedSize = 0; + + policy.maxInlineBytes = 128; + memset(body, 'a', sizeof(body)); + ck_assert(MessageBody_Encode(&policy, body, sizeof(body), NULL, &encoded)); + ck_assert_ptr_ne(encoded, NULL); + ck_assert_int_eq(encoded->mode, MESSAGE_BODY_MODE_ZLIB); + ck_assert_str_eq(MessageBodyMode_ToString(encoded->mode), "zlib"); + ck_assert_ptr_ne(encoded->contentEncoding, NULL); + ck_assert_str_eq(encoded->contentEncoding, MESSAGE_BODY_CONTENT_ENCODING_ZLIB); + ck_assert_uint_le(encoded->transportBodySize, policy.maxInlineBytes); + + ck_assert(MessageBody_DecodeInline(encoded->transportBody, + encoded->transportBodySize, + encoded->contentEncoding, + &decoded, + &decodedSize)); + ck_assert_uint_eq(decodedSize, sizeof(body)); + ck_assert_int_eq(memcmp(decoded, body, sizeof(body)), 0); + + free(decoded); + EncodedMessageBody_Destroy(encoded); +} +END_TEST + +START_TEST(test_message_body_rejects_zlib_expanded_body_over_policy_limit) +{ + uint8_t body[1024]; + struct MessageBodyPolicy policy = MessageBodyPolicy_Default(); + struct MessageBodyPolicy decodePolicy = MessageBodyPolicy_Default(); + struct EncodedMessageBody *encoded = NULL; + uint8_t *decoded = NULL; + size_t decodedSize = 0; + + policy.maxInlineBytes = 128; + decodePolicy.maxExpandedBytes = 512; + memset(body, 'a', sizeof(body)); + ck_assert(MessageBody_Encode(&policy, body, sizeof(body), NULL, &encoded)); + ck_assert_ptr_ne(encoded, NULL); + ck_assert_int_eq(encoded->mode, MESSAGE_BODY_MODE_ZLIB); + ck_assert(!MessageBody_DecodeInlineWithPolicy(&decodePolicy, + encoded->transportBody, + encoded->transportBodySize, + encoded->contentEncoding, + &decoded, + &decodedSize)); + ck_assert_ptr_eq(decoded, NULL); + ck_assert_uint_eq(decodedSize, 0U); + + EncodedMessageBody_Destroy(encoded); +} +END_TEST + +START_TEST(test_message_body_uses_claim_check_for_oversized_compressed_payload) +{ + uint8_t body[4096]; + struct MessageBodyPolicy policy = MessageBodyPolicy_Default(); + struct ClaimCheckReference *template = NULL; + struct ClaimCheckReference *reference = NULL; + struct EncodedMessageBody *encoded = NULL; + uint8_t *decoded = NULL; + size_t decodedSize = 0; + size_t index; + + policy.maxInlineBytes = 32; + for (index = 0; index < sizeof(body); index++) + body[index] = (uint8_t)((index * 31U + 17U) % 251U); + + template = claim_check_template(); + ck_assert_ptr_ne(template, NULL); + ck_assert(MessageBody_Encode(&policy, body, sizeof(body), template, + &encoded)); + ck_assert_ptr_ne(encoded, NULL); + ck_assert_int_eq(encoded->mode, MESSAGE_BODY_MODE_CLAIM_CHECK); + ck_assert_str_eq(MessageBodyMode_ToString(encoded->mode), "claim_check"); + ck_assert_ptr_eq(encoded->contentEncoding, NULL); + ck_assert_ptr_ne(encoded->claimCheckBody, NULL); + ck_assert_uint_eq(encoded->claimCheckBodySize, + encoded->claimCheckReference->storedCompressedSize); + ck_assert_uint_eq(encoded->claimCheckReference->uncompressedSize, + sizeof(body)); + ck_assert_str_eq(encoded->claimCheckReference->contentEncoding, + MESSAGE_BODY_CONTENT_ENCODING_ZLIB); + ck_assert_uint_eq(strlen(encoded->claimCheckReference->sha256), 64U); + + reference = ClaimCheckReference_FromJson((char *)encoded->transportBody); + ck_assert_ptr_ne(reference, NULL); + ck_assert_str_eq(reference->referenceSchemaName, + MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_NAME); + ck_assert_uint_eq(reference->referenceSchemaVersion, + MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_VERSION); + ck_assert_uint_eq(reference->storedCompressedSize, + encoded->claimCheckBodySize); + + ck_assert(MessageBody_DecodeClaimCheck(encoded->claimCheckBody, + encoded->claimCheckBodySize, + reference, + &decoded, + &decodedSize)); + ck_assert_uint_eq(decodedSize, sizeof(body)); + ck_assert_int_eq(memcmp(decoded, body, sizeof(body)), 0); + + free(decoded); + ClaimCheckReference_Destroy(reference); + EncodedMessageBody_Destroy(encoded); + ClaimCheckReference_Destroy(template); +} +END_TEST + +START_TEST(test_message_body_rejects_claim_check_expanded_body_over_policy_limit) +{ + uint8_t body[4096]; + struct MessageBodyPolicy policy = MessageBodyPolicy_Default(); + struct MessageBodyPolicy decodePolicy = MessageBodyPolicy_Default(); + struct ClaimCheckReference *template = NULL; + struct ClaimCheckReference *reference = NULL; + struct EncodedMessageBody *encoded = NULL; + uint8_t *decoded = NULL; + size_t decodedSize = 0; + size_t index; + + policy.maxInlineBytes = 32; + decodePolicy.maxExpandedBytes = 128; + for (index = 0; index < sizeof(body); index++) + body[index] = (uint8_t)((index * 31U + 17U) % 251U); + + template = claim_check_template(); + ck_assert_ptr_ne(template, NULL); + ck_assert(MessageBody_Encode(&policy, body, sizeof(body), template, + &encoded)); + ck_assert_ptr_ne(encoded, NULL); + ck_assert_int_eq(encoded->mode, MESSAGE_BODY_MODE_CLAIM_CHECK); + reference = ClaimCheckReference_FromJson((char *)encoded->transportBody); + ck_assert_ptr_ne(reference, NULL); + ck_assert(!MessageBody_DecodeClaimCheckWithPolicy(&decodePolicy, + encoded->claimCheckBody, + encoded->claimCheckBodySize, + reference, + &decoded, + &decodedSize)); + ck_assert_ptr_eq(decoded, NULL); + ck_assert_uint_eq(decodedSize, 0U); + + ClaimCheckReference_Destroy(reference); + EncodedMessageBody_Destroy(encoded); + ClaimCheckReference_Destroy(template); +} +END_TEST + +START_TEST(test_message_body_rejects_oversized_claim_check_without_template) +{ + uint8_t body[4096]; + struct MessageBodyPolicy policy = MessageBodyPolicy_Default(); + struct EncodedMessageBody *encoded = NULL; + size_t index; + + policy.maxInlineBytes = 32; + for (index = 0; index < sizeof(body); index++) + body[index] = (uint8_t)((index * 31U + 17U) % 251U); + + ck_assert(!MessageBody_Encode(&policy, body, sizeof(body), NULL, &encoded)); + ck_assert_ptr_eq(encoded, NULL); +} +END_TEST + +START_TEST(test_message_body_claim_check_fixture_round_trip) +{ + char *fixture; + char *serialized; + struct ClaimCheckReference *reference; + + fixture = read_fixture("claim_check_reference.valid.json"); + reference = ClaimCheckReference_FromJson(fixture); + ck_assert_ptr_ne(reference, NULL); + ck_assert_str_eq(reference->referenceSchemaName, + MESSAGE_BODY_CLAIM_CHECK_REFERENCE_SCHEMA_NAME); + ck_assert_uint_eq(reference->referenceSchemaVersion, 1U); + ck_assert_str_eq(reference->contentEncoding, + MESSAGE_BODY_CONTENT_ENCODING_ZLIB); + ck_assert_uint_eq(reference->uncompressedSize, 1048576U); + ck_assert_uint_eq(reference->storedCompressedSize, 65536U); + + serialized = ClaimCheckReference_ToJson(reference); + ck_assert_ptr_ne(serialized, NULL); + free(serialized); + ClaimCheckReference_Destroy(reference); + free(fixture); +} +END_TEST + +START_TEST(test_message_body_rejects_invalid_claim_check_fixture) +{ + char *fixture; + struct ClaimCheckReference *reference; + + fixture = read_fixture("claim_check_reference.invalid.json"); + reference = ClaimCheckReference_FromJson(fixture); + ck_assert_ptr_eq(reference, NULL); + free(fixture); +} +END_TEST + +static Suite * +message_body_suite(void) +{ + Suite *suite; + TCase *testcase; + + suite = suite_create("message_body"); + testcase = tcase_create("core"); + tcase_add_test(testcase, test_message_body_default_policy_matches_messaging_threshold); + tcase_add_test(testcase, test_message_body_keeps_small_payload_inline); + tcase_add_test(testcase, test_message_body_compresses_medium_payload_inline); + tcase_add_test(testcase, test_message_body_rejects_zlib_expanded_body_over_policy_limit); + tcase_add_test(testcase, test_message_body_uses_claim_check_for_oversized_compressed_payload); + tcase_add_test(testcase, test_message_body_rejects_claim_check_expanded_body_over_policy_limit); + tcase_add_test(testcase, test_message_body_rejects_oversized_claim_check_without_template); + tcase_add_test(testcase, test_message_body_claim_check_fixture_round_trip); + tcase_add_test(testcase, test_message_body_rejects_invalid_claim_check_fixture); + suite_add_tcase(suite, testcase); + return suite; +} + +int +main(void) +{ + Suite *suite; + SRunner *runner; + int failed; + + suite = message_body_suite(); + runner = srunner_create(suite); + srunner_run_all(runner, CK_NORMAL); + failed = srunner_ntests_failed(runner); + srunner_free(runner); + return failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_messages_next.c b/tests/test_messages_next.c new file mode 100644 index 0000000..97e575d --- /dev/null +++ b/tests/test_messages_next.c @@ -0,0 +1,855 @@ +/* + * 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 + +struct RouteFixture +{ + const char *directory; + const char *name; + const char *cacheRequestorUuid; + enum RzbNextTransport transport; + const char *exchange; + const char *routingKey; +}; + +struct MessageFixture +{ + const char *directory; + const char *name; +}; + +static char * +read_fixture(const char *directory, const char *name) +{ + char path[4096]; + FILE *file; + long size; + char *contents; + + snprintf(path, sizeof(path), "%s/%s/%s", RAZORBACK_SCHEMA_FIXTURE_ROOT, + directory, name); + file = fopen(path, "rb"); + ck_assert_msg(file != NULL, "failed to open fixture %s", path); + ck_assert_int_eq(fseek(file, 0, SEEK_END), 0); + size = ftell(file); + ck_assert_int_ge(size, 0); + ck_assert_int_eq(fseek(file, 0, SEEK_SET), 0); + + contents = calloc((size_t)size + 1, sizeof(char)); + ck_assert_ptr_ne(contents, NULL); + ck_assert_uint_eq(fread(contents, 1, (size_t)size, file), (size_t)size); + fclose(file); + return contents; +} + +static char * +copy_json_string(json_object *object) +{ + const char *jsonText; + char *copy; + size_t length; + + jsonText = json_object_to_json_string_ext(object, JSON_C_TO_STRING_PLAIN); + ck_assert_ptr_ne(jsonText, NULL); + length = strlen(jsonText) + 1; + copy = malloc(length); + ck_assert_ptr_ne(copy, NULL); + memcpy(copy, jsonText, length); + return copy; +} + +static char * +copy_string(const char *value) +{ + char *copy; + size_t length; + + if (value == NULL) + return NULL; + length = strlen(value) + 1; + copy = malloc(length); + ck_assert_ptr_ne(copy, NULL); + memcpy(copy, value, length); + return copy; +} + +static void +restore_env_value(const char *name, char *value) +{ + if (value == NULL) { + ck_assert_int_eq(unsetenv(name), 0); + } else { + ck_assert_int_eq(setenv(name, value, 1), 0); + free(value); + } +} + +static char * +mutate_fixture_version(const char *fixture) +{ + json_object *object; + json_object *value; + char *mutated; + const char *field = "schema_version"; + + object = json_tokener_parse(fixture); + ck_assert_ptr_ne(object, NULL); + if (json_object_object_get_ex(object, "reference_schema_version", &value)) + field = "reference_schema_version"; + json_object_object_add(object, field, json_object_new_int(2)); + mutated = copy_json_string(object); + json_object_put(object); + return mutated; +} + +static char * +mutate_fixture_schema(const char *fixture) +{ + json_object *object; + json_object *value; + const char *field = "schema_name"; + const char *wrongSchema = RZB_NEXT_SCHEMA_CACHE_REQUEST; + const char *currentSchema; + char *mutated; + + object = json_tokener_parse(fixture); + ck_assert_ptr_ne(object, NULL); + if (json_object_object_get_ex(object, "reference_schema_name", &value)) { + field = "reference_schema_name"; + currentSchema = json_object_get_string(value); + } else { + ck_assert(json_object_object_get_ex(object, "schema_name", &value)); + currentSchema = json_object_get_string(value); + } + if (currentSchema != NULL && + strcmp(currentSchema, RZB_NEXT_SCHEMA_CACHE_REQUEST) == 0) { + wrongSchema = RZB_NEXT_SCHEMA_CACHE_RESPONSE; + } + json_object_object_add(object, field, json_object_new_string(wrongSchema)); + mutated = copy_json_string(object); + json_object_put(object); + return mutated; +} + +static const char * +header_value(const struct RzbNextMessageHeader *headers, size_t headerCount, + const char *name) +{ + size_t index; + + for (index = 0; index < headerCount; index++) { + if (strcmp(headers[index].name, name) == 0) + return headers[index].value; + } + return NULL; +} + +static const char * +json_string_field(json_object *object, const char *field) +{ + json_object *value; + + ck_assert(json_object_object_get_ex(object, field, &value)); + ck_assert_int_eq(json_object_get_type(value), json_type_string); + return json_object_get_string(value); +} + +START_TEST(test_messages_next_accepts_known_schema_identities) +{ + static const struct MessageFixture fixtures[] = { + { "messages", "claim_check_reference.valid.json" }, + { "messages", "cnc_registration_request.valid.json" }, + { "messages", "cnc_registration_accepted.valid.json" }, + { "messages", "cnc_registration_rejected.valid.json" }, + { "messages", "cnc_liveness.valid.json" }, + { "messages", "cnc_bye.valid.json" }, + { "messages", "cnc_directed_command.valid.json" }, + { "messages", "cnc_dispatcher_hello.valid.json" }, + { "messages", "block_submission.valid.json" }, + { "messages", "block_update.valid.json" }, + { "messages", "inspection_work.valid.json" }, + { "messages", "analysis_result_envelope.valid.json" }, + { "messages", "cache_request.valid.json" }, + { "messages", "cache_response.valid.json" }, + { "messages", "catalog_invalidation_event.valid.json" }, + { "messages", "file_remove_request.valid.json" }, + { "messages", "file_remove_result.valid.json" }, + { "search_export", "search_export_record.valid.json" } + }; + size_t index; + + for (index = 0; index < sizeof(fixtures) / sizeof(fixtures[0]); index++) { + char *fixture = read_fixture(fixtures[index].directory, + fixtures[index].name); + + ck_assert_msg(RzbNextMessage_ValidateIdentity(fixture), + "fixture identity should be valid: %s", + fixtures[index].name); + ck_assert_msg(RzbNextMessage_Validate(fixture), + "fixture structure should be valid: %s", + fixtures[index].name); + free(fixture); + } + + ck_assert(RzbNextMessage_IsKnownSchema(RZB_NEXT_SCHEMA_ANALYSIS_RESULT)); + ck_assert(!RzbNextMessage_IsKnownSchema("razorback.messages.analysis_result")); +} +END_TEST + +START_TEST(test_metadata_value_limit_rejects_oversized_values) +{ + char *fixture; + char *savedLimit; + + fixture = read_fixture("messages", "block_submission.valid.json"); + savedLimit = copy_string(getenv(RZB_NEXT_METADATA_MAX_VALUE_BYTES_ENV)); + + ck_assert_uint_eq(RZB_NEXT_METADATA_DEFAULT_MAX_VALUE_BYTES, + 5U * 1024U * 1024U); + ck_assert_int_eq(setenv(RZB_NEXT_METADATA_MAX_VALUE_BYTES_ENV, "64", 1), 0); + ck_assert_uint_eq(RzbNextMetadata_MaxValueBytes(), 64U); + ck_assert_msg(RzbNextMessage_Validate(fixture), + "fixture metadata should fit under expanded test limit"); + + ck_assert_int_eq(setenv(RZB_NEXT_METADATA_MAX_VALUE_BYTES_ENV, "5", 1), 0); + ck_assert_uint_eq(RzbNextMetadata_MaxValueBytes(), 5U); + ck_assert_msg(!RzbNextMessage_Validate(fixture), + "fixture metadata should fail under tiny test limit"); + + restore_env_value(RZB_NEXT_METADATA_MAX_VALUE_BYTES_ENV, savedLimit); + free(fixture); +} +END_TEST + +START_TEST(test_analysis_result_completed_oversized_metadata_builds_error_result) +{ + char *work; + char *completed; + char *savedLimit; + json_object *completedObject; + json_object *errorObject; + const char *metadataJson = + "[{\"name\":\"large\",\"type\":\"string\",\"value\":\"abcdef\"}]"; + const char *inspectorUuid = "22222222-2222-4222-8222-222222222222"; + const char *createdAt = "2026-06-17T21:06:00.000Z"; + + work = read_fixture("messages", "inspection_work.valid.json"); + savedLimit = copy_string(getenv(RZB_NEXT_METADATA_MAX_VALUE_BYTES_ENV)); + ck_assert_int_eq(setenv(RZB_NEXT_METADATA_MAX_VALUE_BYTES_ENV, "5", 1), 0); + + completed = RzbNextAnalysisResult_BuildCompleted( + work, inspectorUuid, createdAt, NULL, metadataJson, NULL, NULL); + + ck_assert_ptr_ne(completed, NULL); + ck_assert(RzbNextMessage_Validate(completed)); + completedObject = json_tokener_parse(completed); + ck_assert_ptr_ne(completedObject, NULL); + ck_assert_str_eq(json_string_field(completedObject, "result_status"), + "error"); + ck_assert(json_object_object_get_ex(completedObject, "error", + &errorObject)); + ck_assert_str_eq(json_string_field(errorObject, "code"), + "metadata_value_too_large"); + + json_object_put(completedObject); + RzbNext_FreeString(completed); + restore_env_value(RZB_NEXT_METADATA_MAX_VALUE_BYTES_ENV, savedLimit); + free(work); +} +END_TEST + +START_TEST(test_analysis_result_helpers_build_from_inspection_work) +{ + char *work; + char *completed; + char *error; + char *deferred; + struct RzbNextRoute *route = NULL; + json_object *completedObject; + json_object *errorObject; + json_object *deferredObject; + const char *inspectorUuid = "22222222-2222-4222-8222-222222222222"; + const char *createdAt = "2026-06-17T21:06:00.000Z"; + + work = read_fixture("messages", "inspection_work.valid.json"); + completed = RzbNextAnalysisResult_BuildCompleted( + work, inspectorUuid, createdAt, NULL, NULL, + "{\"set_system_tags\":[\"SUSPICIOUS\"]}", NULL); + ck_assert_ptr_ne(completed, NULL); + ck_assert(RzbNextMessage_Validate(completed)); + ck_assert(RzbNextMessage_Route(completed, NULL, &route)); + ck_assert_str_eq(route->routingKey, RZB_NEXT_QUEUE_ANALYSIS_RESULT); + RzbNextRoute_Destroy(route); + route = NULL; + + completedObject = json_tokener_parse(completed); + ck_assert_ptr_ne(completedObject, NULL); + ck_assert_str_eq(json_string_field(completedObject, "schema_name"), + RZB_NEXT_SCHEMA_ANALYSIS_RESULT); + ck_assert_str_eq(json_string_field(completedObject, "inspection_id"), + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + ck_assert_str_eq(json_string_field(completedObject, "event_id"), + "88888888-8888-4888-8888-888888888888"); + ck_assert_str_eq(json_string_field(completedObject, "inspector_id"), + inspectorUuid); + ck_assert_str_eq(json_string_field(completedObject, "app_type"), + "pdf_inspector"); + ck_assert_str_eq(json_string_field(completedObject, "result_status"), + "completed"); + json_object_put(completedObject); + + error = RzbNextAnalysisResult_BuildError( + work, inspectorUuid, "inspector_failed", "inspector_failed", + "inspector failed", "{\"retryable\":false}", createdAt); + ck_assert_ptr_ne(error, NULL); + ck_assert(RzbNextMessage_Validate(error)); + errorObject = json_tokener_parse(error); + ck_assert_ptr_ne(errorObject, NULL); + ck_assert_str_eq(json_string_field(errorObject, "result_status"), + "error"); + json_object_put(errorObject); + + deferred = RzbNextAnalysisResult_BuildDeferred( + work, inspectorUuid, "external_scan_pending", + "external scan pending", "2026-06-17T21:07:00.000Z", NULL, + createdAt); + ck_assert_ptr_ne(deferred, NULL); + ck_assert(RzbNextMessage_Validate(deferred)); + deferredObject = json_tokener_parse(deferred); + ck_assert_ptr_ne(deferredObject, NULL); + ck_assert_str_eq(json_string_field(deferredObject, "result_status"), + "deferred"); + json_object_put(deferredObject); + + ck_assert_ptr_eq(RzbNextAnalysisResult_BuildCompleted( + work, "not-a-uuid", createdAt, NULL, NULL, NULL, + NULL), + NULL); + + RzbNext_FreeString(deferred); + RzbNext_FreeString(error); + RzbNext_FreeString(completed); + free(work); +} +END_TEST + +START_TEST(test_messages_next_rejects_identity_mutations_for_all_fixtures) +{ + static const struct MessageFixture fixtures[] = { + { "messages", "claim_check_reference.valid.json" }, + { "messages", "cnc_registration_request.valid.json" }, + { "messages", "cnc_registration_accepted.valid.json" }, + { "messages", "cnc_registration_rejected.valid.json" }, + { "messages", "cnc_liveness.valid.json" }, + { "messages", "cnc_bye.valid.json" }, + { "messages", "cnc_directed_command.valid.json" }, + { "messages", "cnc_dispatcher_hello.valid.json" }, + { "messages", "block_submission.valid.json" }, + { "messages", "block_update.valid.json" }, + { "messages", "inspection_work.valid.json" }, + { "messages", "analysis_result_envelope.valid.json" }, + { "messages", "cache_request.valid.json" }, + { "messages", "cache_response.valid.json" }, + { "messages", "catalog_invalidation_event.valid.json" }, + { "messages", "file_remove_request.valid.json" }, + { "messages", "file_remove_result.valid.json" }, + { "search_export", "search_export_record.valid.json" } + }; + size_t index; + + for (index = 0; index < sizeof(fixtures) / sizeof(fixtures[0]); index++) { + char *fixture = read_fixture(fixtures[index].directory, + fixtures[index].name); + char *badVersion = mutate_fixture_version(fixture); + char *badSchema = mutate_fixture_schema(fixture); + + ck_assert_msg(!RzbNextMessage_ValidateIdentity(badVersion), + "unsupported schema version should fail identity: %s", + fixtures[index].name); + ck_assert_msg(!RzbNextMessage_Validate(badVersion), + "unsupported schema version should fail validation: %s", + fixtures[index].name); + + ck_assert_msg(RzbNextMessage_ValidateIdentity(badSchema), + "known-but-wrong schema identity should still parse: %s", + fixtures[index].name); + ck_assert_msg(!RzbNextMessage_Validate(badSchema), + "known-but-wrong schema identity should fail validation: %s", + fixtures[index].name); + + free(badSchema); + free(badVersion); + free(fixture); + } +} +END_TEST + +START_TEST(test_messages_next_rejects_unknown_or_bad_identity) +{ + static const char bad_version[] = + "{\"schema_name\":\"razorback.cache.request\",\"schema_version\":2}"; + static const char bad_schema[] = + "{\"schema_name\":\"razorback.unknown\",\"schema_version\":1}"; + static const char bad_reference[] = + "{\"reference_schema_name\":\"razorback.messages.wrong_reference\"," + "\"reference_schema_version\":1}"; + + ck_assert(!RzbNextMessage_ValidateIdentity(NULL)); + ck_assert(!RzbNextMessage_ValidateIdentity("[]")); + ck_assert(!RzbNextMessage_ValidateIdentity("{bad json")); + ck_assert(!RzbNextMessage_ValidateIdentity(bad_version)); + ck_assert(!RzbNextMessage_ValidateIdentity(bad_schema)); + ck_assert(!RzbNextMessage_ValidateIdentity(bad_reference)); + + ck_assert(!RzbNextMessage_Validate(NULL)); + ck_assert(!RzbNextMessage_Validate("[]")); + ck_assert(!RzbNextMessage_Validate("{bad json")); + ck_assert(!RzbNextMessage_Validate(bad_version)); + ck_assert(!RzbNextMessage_Validate(bad_schema)); + ck_assert(!RzbNextMessage_Validate(bad_reference)); +} +END_TEST + +START_TEST(test_messages_next_rejects_invalid_schema_fixtures) +{ + static const struct MessageFixture fixtures[] = { + { "messages", "claim_check_reference.invalid.json" }, + { "messages", "cnc_registration_request.invalid.json" }, + { "messages", "cnc_registration_accepted.invalid.json" }, + { "messages", "cnc_registration_rejected.invalid.json" }, + { "messages", "cnc_liveness.invalid.json" }, + { "messages", "cnc_bye.invalid.json" }, + { "messages", "cnc_directed_command.invalid.json" }, + { "messages", "cnc_dispatcher_hello.invalid.json" }, + { "messages", "block_submission.invalid.json" }, + { "messages", "block_update.invalid.json" }, + { "messages", "inspection_work.invalid.json" }, + { "messages", "analysis_result_envelope.invalid.json" }, + { "messages", "cache_request.invalid.json" }, + { "messages", "cache_response.invalid.json" }, + { "messages", "catalog_invalidation_event.invalid.json" }, + { "messages", "file_remove_request.invalid.json" }, + { "messages", "file_remove_result.invalid.json" }, + { "search_export", "search_export_record.invalid.json" } + }; + size_t index; + + for (index = 0; index < sizeof(fixtures) / sizeof(fixtures[0]); index++) { + char *fixture = read_fixture(fixtures[index].directory, + fixtures[index].name); + + ck_assert_msg(!RzbNextMessage_Validate(fixture), + "fixture structure should be invalid: %s", + fixtures[index].name); + free(fixture); + } +} +END_TEST + +START_TEST(test_messages_next_routes_match_dispatcher_next_topology) +{ + static const struct RouteFixture fixtures[] = { + { "messages", "cnc_registration_request.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", RZB_NEXT_QUEUE_COMMAND }, + { "messages", "cnc_registration_accepted.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", + "COMMAND.22222222-2222-4222-8222-222222222222" }, + { "messages", "cnc_registration_rejected.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", + "COMMAND.22222222-2222-4222-8222-222222222222" }, + { "messages", "cnc_liveness.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", RZB_NEXT_QUEUE_COMMAND }, + { "messages", "cnc_bye.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", RZB_NEXT_QUEUE_COMMAND }, + { "messages", "cnc_directed_command.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", + "COMMAND.22222222-2222-4222-8222-222222222222" }, + { "messages", "cnc_dispatcher_hello.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, RZB_NEXT_EXCHANGE_DISPATCHER_HELLO, "" }, + { "messages", "block_submission.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", RZB_NEXT_QUEUE_INPUT }, + { "messages", "block_update.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", RZB_NEXT_QUEUE_BLOCK_UPDATE }, + { "messages", "inspection_work.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", "INSPECTOR.pdf_inspector" }, + { "messages", "analysis_result_envelope.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", RZB_NEXT_QUEUE_ANALYSIS_RESULT }, + { "messages", "cache_request.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", RZB_NEXT_QUEUE_CACHE_REQUEST }, + { "messages", "cache_response.valid.json", + "22222222-2222-4222-8222-222222222222", + RZB_NEXT_TRANSPORT_RABBITMQ, "", + "CACHE_RESPONSE.22222222-2222-4222-8222-222222222222" }, + { "messages", "catalog_invalidation_event.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, + RZB_NEXT_EXCHANGE_CATALOG_INVALIDATION, "catalog.app_type.updated" }, + { "messages", "file_remove_request.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, RZB_NEXT_EXCHANGE_FILE_REMOVE, + RZB_NEXT_QUEUE_FILE_REMOVE_FILE_STORE }, + { "messages", "file_remove_result.valid.json", NULL, + RZB_NEXT_TRANSPORT_RABBITMQ, "", RZB_NEXT_QUEUE_FILE_REMOVE_RESULT }, + { "search_export", "search_export_record.valid.json", NULL, + RZB_NEXT_TRANSPORT_KAFKA, "", + "88888888-8888-4888-8888-888888888888" } + }; + size_t index; + + for (index = 0; index < sizeof(fixtures) / sizeof(fixtures[0]); index++) { + char *fixture = read_fixture(fixtures[index].directory, + fixtures[index].name); + struct RzbNextRoute *route = NULL; + + ck_assert_msg(RzbNextMessage_Route(fixture, + fixtures[index].cacheRequestorUuid, + &route), + "fixture should route: %s", fixtures[index].name); + ck_assert_ptr_ne(route, NULL); + ck_assert_int_eq(route->transport, fixtures[index].transport); + ck_assert_str_eq(RzbNextTransport_ToString(route->transport), + fixtures[index].transport == RZB_NEXT_TRANSPORT_KAFKA ? + "kafka" : "rabbitmq"); + ck_assert_str_eq(route->exchange, fixtures[index].exchange); + ck_assert_str_eq(route->routingKey, fixtures[index].routingKey); + + RzbNextRoute_Destroy(route); + free(fixture); + } +} +END_TEST + +START_TEST(test_rabbitmq_prepare_and_decode_inline_message) +{ + char *fixture = read_fixture("messages", + "cnc_registration_request.valid.json"); + struct MessageBodyPolicy policy = MessageBodyPolicy_Default(); + struct RzbNextPreparedRabbitMqMessage *prepared = NULL; + struct RzbNextDecodedRabbitMqMessage *decoded = NULL; + + policy.maxInlineBytes = 4096; + ck_assert(RzbNextRabbitMq_PrepareMessage(fixture, NULL, &policy, NULL, + &prepared)); + ck_assert_ptr_ne(prepared, NULL); + ck_assert_str_eq(prepared->route->exchange, ""); + ck_assert_str_eq(prepared->route->routingKey, RZB_NEXT_QUEUE_COMMAND); + ck_assert_str_eq(prepared->contentType, "application/json"); + ck_assert_ptr_eq(prepared->contentEncoding, NULL); + ck_assert_str_eq(header_value(prepared->headers, prepared->headerCount, + RZB_NEXT_HEADER_SCHEMA_NAME), + RZB_NEXT_SCHEMA_CNC_REGISTRATION_REQUEST); + ck_assert_str_eq(header_value(prepared->headers, prepared->headerCount, + RZB_NEXT_HEADER_SCHEMA_VERSION), "1"); + ck_assert_str_eq(header_value(prepared->headers, prepared->headerCount, + RZB_NEXT_HEADER_BODY_MODE), "inline"); + ck_assert_ptr_eq(header_value(prepared->headers, prepared->headerCount, + RZB_NEXT_HEADER_CONTENT_ENCODING), NULL); + + ck_assert(RzbNextRabbitMq_DecodeMessage(prepared->body, prepared->bodySize, + prepared->headers, + prepared->headerCount, NULL, 0, + &decoded)); + ck_assert_ptr_ne(decoded, NULL); + ck_assert_ptr_eq(decoded->claimCheckReference, NULL); + ck_assert_str_eq(decoded->jsonMessage, fixture); + ck_assert(RzbNextMessage_Validate(decoded->jsonMessage)); + + RzbNextDecodedRabbitMqMessage_Destroy(decoded); + RzbNextPreparedRabbitMqMessage_Destroy(prepared); + free(fixture); +} +END_TEST + +START_TEST(test_rabbitmq_prepare_and_decode_zlib_message) +{ + char *fixture = read_fixture("messages", + "analysis_result_envelope.valid.json"); + struct MessageBodyPolicy policy = MessageBodyPolicy_Default(); + struct RzbNextPreparedRabbitMqMessage *prepared = NULL; + struct RzbNextDecodedRabbitMqMessage *decoded = NULL; + + policy.maxInlineBytes = 900; + ck_assert(RzbNextRabbitMq_PrepareMessage(fixture, NULL, &policy, NULL, + &prepared)); + ck_assert_ptr_ne(prepared, NULL); + ck_assert_str_eq(prepared->route->routingKey, + RZB_NEXT_QUEUE_ANALYSIS_RESULT); + ck_assert_str_eq(header_value(prepared->headers, prepared->headerCount, + RZB_NEXT_HEADER_BODY_MODE), "zlib"); + ck_assert_str_eq(header_value(prepared->headers, prepared->headerCount, + RZB_NEXT_HEADER_CONTENT_ENCODING), + MESSAGE_BODY_CONTENT_ENCODING_ZLIB); + ck_assert_str_eq(prepared->contentEncoding, + MESSAGE_BODY_CONTENT_ENCODING_ZLIB); + + ck_assert(RzbNextRabbitMq_DecodeMessage(prepared->body, prepared->bodySize, + prepared->headers, + prepared->headerCount, NULL, 0, + &decoded)); + ck_assert_str_eq(decoded->jsonMessage, fixture); + ck_assert_ptr_eq(decoded->claimCheckReference, NULL); + + RzbNextDecodedRabbitMqMessage_Destroy(decoded); + RzbNextPreparedRabbitMqMessage_Destroy(prepared); + free(fixture); +} +END_TEST + +START_TEST(test_rabbitmq_prepare_and_decode_claim_check_message) +{ + char *fixture = read_fixture("messages", + "analysis_result_envelope.valid.json"); + struct MessageBodyPolicy policy = MessageBodyPolicy_Default(); + struct ClaimCheckReference *template = NULL; + struct RzbNextPreparedRabbitMqMessage *prepared = NULL; + struct RzbNextDecodedRabbitMqMessage *decoded = NULL; + + policy.maxInlineBytes = 32; + template = ClaimCheckReference_Create( + "https://objects.example.invalid/messages/payload.zlib?signature=redacted", + "2026-06-18T00:00:00.000Z", + "razorback-claim-check", + "messages/payload.zlib", + "application/json", + RZB_NEXT_SCHEMA_ANALYSIS_RESULT, + RZB_NEXT_SCHEMA_VERSION + ); + ck_assert_ptr_ne(template, NULL); + + ck_assert(RzbNextRabbitMq_PrepareMessage(fixture, NULL, &policy, template, + &prepared)); + ck_assert_ptr_ne(prepared, NULL); + ck_assert_str_eq(header_value(prepared->headers, prepared->headerCount, + RZB_NEXT_HEADER_BODY_MODE), "claim_check"); + ck_assert_ptr_eq(header_value(prepared->headers, prepared->headerCount, + RZB_NEXT_HEADER_CONTENT_ENCODING), NULL); + ck_assert_ptr_ne(prepared->claimCheckBody, NULL); + ck_assert_ptr_ne(prepared->claimCheckReference, NULL); + ck_assert_str_eq(prepared->claimCheckReference->schemaName, + RZB_NEXT_SCHEMA_ANALYSIS_RESULT); + + ck_assert(RzbNextRabbitMq_DecodeMessage( + prepared->body, prepared->bodySize, prepared->headers, + prepared->headerCount, prepared->claimCheckBody, + prepared->claimCheckBodySize, &decoded)); + ck_assert_str_eq(decoded->jsonMessage, fixture); + ck_assert_ptr_ne(decoded->claimCheckReference, NULL); + ck_assert_str_eq(decoded->claimCheckReference->schemaName, + RZB_NEXT_SCHEMA_ANALYSIS_RESULT); + + RzbNextDecodedRabbitMqMessage_Destroy(decoded); + RzbNextPreparedRabbitMqMessage_Destroy(prepared); + ClaimCheckReference_Destroy(template); + free(fixture); +} +END_TEST + +START_TEST(test_rabbitmq_prepare_rejects_kafka_routed_message) +{ + char *fixture = read_fixture("search_export", + "search_export_record.valid.json"); + struct MessageBodyPolicy policy = MessageBodyPolicy_Default(); + struct RzbNextPreparedRabbitMqMessage *prepared = NULL; + + policy.maxInlineBytes = 4096; + ck_assert(!RzbNextRabbitMq_PrepareMessage(fixture, NULL, &policy, NULL, + &prepared)); + ck_assert_ptr_eq(prepared, NULL); + + free(fixture); +} +END_TEST + +START_TEST(test_cnc_helpers_gate_registration_and_extract_timing) +{ + char *hello; + char *accepted; + char *directedQueue; + uint64_t interval = 0; + uint64_t freshness = 0; + uint64_t skew = 0; + static const char notReadyHello[] = + "{" + "\"schema_name\":\"razorback.cnc.dispatcher_hello\"," + "\"schema_version\":1," + "\"dispatcher_id\":\"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa\"," + "\"created_at\":\"2026-06-17T21:00:00.000Z\"," + "\"started_at\":\"2026-06-17T20:59:00.000Z\"," + "\"ready\":false," + "\"availability\":\"starting\"," + "\"dependency_reason_codes\":[\"starting\"]" + "}"; + + hello = read_fixture("messages", "cnc_dispatcher_hello.valid.json"); + accepted = read_fixture("messages", "cnc_registration_accepted.valid.json"); + + ck_assert(RzbNextCnc_IsReadyDispatcherHello(hello)); + ck_assert(!RzbNextCnc_IsReadyDispatcherHello(notReadyHello)); + ck_assert(RzbNextCnc_RegistrationAcceptedTiming(accepted, &interval, + &freshness, &skew)); + ck_assert_uint_eq(interval, 10); + ck_assert_uint_eq(freshness, 30); + ck_assert_uint_eq(skew, 5); + + directedQueue = RzbNextCnc_DirectedCommandQueue( + "22222222-2222-4222-8222-222222222222"); + ck_assert_ptr_ne(directedQueue, NULL); + ck_assert_str_eq(directedQueue, + "COMMAND.22222222-2222-4222-8222-222222222222"); + RzbNext_FreeString(directedQueue); + ck_assert_ptr_eq(RzbNextCnc_DirectedCommandQueue("not-a-uuid"), NULL); + + free(accepted); + free(hello); +} +END_TEST + +START_TEST(test_messages_next_cache_response_route_requires_requestor) +{ + char *fixture; + struct RzbNextRoute *route = NULL; + + fixture = read_fixture("messages", "cache_response.valid.json"); + ck_assert(!RzbNextMessage_Route(fixture, NULL, &route)); + ck_assert_ptr_eq(route, NULL); + ck_assert(!RzbNextMessage_Route(fixture, "not-a-uuid", &route)); + ck_assert_ptr_eq(route, NULL); + free(fixture); +} +END_TEST + +START_TEST(test_messages_next_cache_submit_decision_maps_response_tags) +{ + char *fixture; + json_object *object; + json_object *tags; + char *mutated; + + fixture = read_fixture("messages", "cache_response.valid.json"); + ck_assert_int_eq(RzbNextCache_SubmitDecision("not-json"), + RZB_NEXT_CACHE_INVALID_REQUEST); + ck_assert_int_eq(RzbNextCache_SubmitDecision(fixture), + RZB_NEXT_CACHE_SKIP_KNOWN); + + object = json_tokener_parse(fixture); + ck_assert_ptr_ne(object, NULL); + json_object_object_add(object, "found", json_object_new_boolean(false)); + tags = json_object_new_array(); + json_object_object_add(object, "system_tags", tags); + mutated = copy_json_string(object); + ck_assert_int_eq(RzbNextCache_SubmitDecision(mutated), + RZB_NEXT_CACHE_SUBMIT_NEW); + free(mutated); + + json_object_object_add(object, "found", json_object_new_boolean(true)); + tags = json_object_new_array(); + json_object_array_add(tags, json_object_new_string("DIRTY")); + json_object_object_add(object, "system_tags", tags); + mutated = copy_json_string(object); + ck_assert_int_eq(RzbNextCache_SubmitDecision(mutated), + RZB_NEXT_CACHE_SUBMIT_FOR_REINSPECTION); + free(mutated); + + tags = json_object_new_array(); + json_object_array_add(tags, json_object_new_string("DIRTY")); + json_object_array_add(tags, json_object_new_string("NOT_STORED")); + json_object_object_add(object, "system_tags", tags); + mutated = copy_json_string(object); + ck_assert_int_eq(RzbNextCache_SubmitDecision(mutated), + RZB_NEXT_CACHE_RESTORE_AND_SUBMIT_FOR_REINSPECTION); + free(mutated); + + json_object_put(object); + free(fixture); +} +END_TEST + +START_TEST(test_messages_next_route_rejects_structurally_invalid_messages) +{ + char *fixture; + struct RzbNextRoute *route = NULL; + + fixture = read_fixture("messages", "cnc_directed_command.invalid.json"); + ck_assert(RzbNextMessage_ValidateIdentity(fixture)); + ck_assert(!RzbNextMessage_Validate(fixture)); + ck_assert(!RzbNextMessage_Route(fixture, NULL, &route)); + ck_assert_ptr_eq(route, NULL); + free(fixture); +} +END_TEST + +static Suite * +messages_next_suite(void) +{ + Suite *suite; + TCase *testcase; + + suite = suite_create("messages_next"); + testcase = tcase_create("core"); + tcase_add_test(testcase, test_messages_next_accepts_known_schema_identities); + tcase_add_test(testcase, test_metadata_value_limit_rejects_oversized_values); + tcase_add_test(testcase, + test_analysis_result_completed_oversized_metadata_builds_error_result); + tcase_add_test(testcase, + test_messages_next_rejects_identity_mutations_for_all_fixtures); + tcase_add_test(testcase, test_messages_next_rejects_unknown_or_bad_identity); + tcase_add_test(testcase, test_messages_next_rejects_invalid_schema_fixtures); + tcase_add_test(testcase, test_messages_next_routes_match_dispatcher_next_topology); + tcase_add_test(testcase, + test_analysis_result_helpers_build_from_inspection_work); + tcase_add_test(testcase, test_rabbitmq_prepare_and_decode_inline_message); + tcase_add_test(testcase, test_rabbitmq_prepare_and_decode_zlib_message); + tcase_add_test(testcase, test_rabbitmq_prepare_and_decode_claim_check_message); + tcase_add_test(testcase, test_rabbitmq_prepare_rejects_kafka_routed_message); + tcase_add_test(testcase, + test_cnc_helpers_gate_registration_and_extract_timing); + tcase_add_test(testcase, + test_messages_next_cache_response_route_requires_requestor); + tcase_add_test(testcase, + test_messages_next_cache_submit_decision_maps_response_tags); + tcase_add_test(testcase, + test_messages_next_route_rejects_structurally_invalid_messages); + suite_add_tcase(suite, testcase); + return suite; +} + +int +main(void) +{ + Suite *suite; + SRunner *runner; + int failed; + + suite = messages_next_suite(); + runner = srunner_create(suite); + srunner_run_all(runner, CK_NORMAL); + failed = srunner_ntests_failed(runner); + srunner_free(runner); + return failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_messages_roundtrip.c b/tests/test_messages_roundtrip.c index ce94cd8..d3def8c 100644 --- a/tests/test_messages_roundtrip.c +++ b/tests/test_messages_roundtrip.c @@ -30,7 +30,6 @@ #include #include #include -#include #include #include "init.h" @@ -238,7 +237,7 @@ init_dispatcher_context(struct RazorbackContext *context) context->uuidNuggetType)); context->locality = 4U; context->dispatcher.priority = 5U; - context->dispatcher.protocol = TRANSFER_MODE_HTTPS; + context->dispatcher.protocol = 3U; context->dispatcher.port = 8443U; context->dispatcher.flags = 0x12U; context->dispatcher.addressList = create_address_list(); diff --git a/tests/test_runtime_next.c b/tests/test_runtime_next.c new file mode 100644 index 0000000..ec28383 --- /dev/null +++ b/tests/test_runtime_next.c @@ -0,0 +1,762 @@ +/* + * 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 + +#define NUGGET_UUID "22222222-2222-4222-8222-222222222222" +#define PROCESS_UUID "33333333-3333-4333-8333-333333333333" +#define REQUEST_ID "11111111-1111-4111-8111-111111111111" + +static char *read_fixture(const char *directory, const char *name); +static char *json_copy(json_object *object); +static char *mutate_string_field(const char *jsonMessage, const char *field, + const char *value); +static char *mutate_directed_command(const char *jsonMessage, + const char *command); +static char *mutate_rejected_terminal(const char *jsonMessage); +static void assert_json_string_field(const char *jsonMessage, const char *field, + const char *expected); +static void assert_valid_uuid(const char *value); + +struct CallbackState +{ + uint32_t registered; + uint32_t ready; + uint32_t pause; + uint32_t resume; + uint32_t cacheInvalidate; + uint32_t shutdown; + uint32_t error; + bool failPause; + char lastGeneration[37]; + char lastInvalidationId[37]; + char lastError[128]; +}; + +static bool callback_registered( + RzbNextRuntime_t *runtime, + const struct RzbNextRuntimeTransition *transition, + void *userData +); +static bool callback_ready(RzbNextRuntime_t *runtime, void *userData); +static bool callback_pause(RzbNextRuntime_t *runtime, void *userData); +static bool callback_resume(RzbNextRuntime_t *runtime, void *userData); +static bool callback_cache_invalidate( + RzbNextRuntime_t *runtime, + const char *invalidationId, + void *userData +); +static bool callback_shutdown(RzbNextRuntime_t *runtime, void *userData); +static bool callback_error( + RzbNextRuntime_t *runtime, + const char *reasonCode, + bool retryable, + void *userData +); + +static char * +read_fixture(const char *directory, const char *name) +{ + char path[4096]; + FILE *file; + long size; + char *contents; + + snprintf(path, sizeof(path), "%s/%s/%s", RAZORBACK_SCHEMA_FIXTURE_ROOT, + directory, name); + file = fopen(path, "rb"); + ck_assert_msg(file != NULL, "failed to open fixture %s", path); + ck_assert_int_eq(fseek(file, 0, SEEK_END), 0); + size = ftell(file); + ck_assert_int_ge(size, 0); + ck_assert_int_eq(fseek(file, 0, SEEK_SET), 0); + + contents = calloc((size_t)size + 1U, sizeof(char)); + ck_assert_ptr_ne(contents, NULL); + ck_assert_uint_eq(fread(contents, 1, (size_t)size, file), (size_t)size); + fclose(file); + return contents; +} + +static char * +json_copy(json_object *object) +{ + const char *jsonText; + char *copy; + size_t length; + + jsonText = json_object_to_json_string_ext(object, JSON_C_TO_STRING_PLAIN); + length = strlen(jsonText) + 1U; + copy = malloc(length); + ck_assert_ptr_ne(copy, NULL); + memcpy(copy, jsonText, length); + json_object_put(object); + return copy; +} + +static char * +mutate_string_field(const char *jsonMessage, const char *field, const char *value) +{ + json_object *object = json_tokener_parse(jsonMessage); + + ck_assert_ptr_ne(object, NULL); + json_object_object_del(object, field); + json_object_object_add(object, field, json_object_new_string(value)); + return json_copy(object); +} + +static char * +mutate_directed_command(const char *jsonMessage, const char *command) +{ + json_object *object = json_tokener_parse(jsonMessage); + + ck_assert_ptr_ne(object, NULL); + json_object_object_del(object, "command"); + json_object_object_del(object, "reason_code"); + json_object_object_del(object, "invalidation_id"); + json_object_object_add(object, "command", json_object_new_string(command)); + json_object_object_add(object, "reason_code", + json_object_new_string("operator_requested")); + return json_copy(object); +} + +static char * +mutate_rejected_terminal(const char *jsonMessage) +{ + json_object *object = json_tokener_parse(jsonMessage); + + ck_assert_ptr_ne(object, NULL); + json_object_object_del(object, "retryable"); + json_object_object_del(object, "retry_after"); + json_object_object_del(object, "reason_code"); + json_object_object_add(object, "retryable", json_object_new_boolean(false)); + json_object_object_add(object, "reason_code", + json_object_new_string("unauthorized")); + return json_copy(object); +} + +static void +assert_json_string_field(const char *jsonMessage, const char *field, + const char *expected) +{ + json_object *object; + json_object *value; + const char *stringValue; + + object = json_tokener_parse(jsonMessage); + ck_assert_ptr_ne(object, NULL); + ck_assert(json_object_object_get_ex(object, field, &value)); + ck_assert(json_object_is_type(value, json_type_string)); + stringValue = json_object_get_string(value); + ck_assert_ptr_ne(stringValue, NULL); + ck_assert_str_eq(stringValue, expected); + json_object_put(object); +} + +static void +assert_valid_uuid(const char *value) +{ + size_t index; + + ck_assert_ptr_ne(value, NULL); + ck_assert_uint_eq(strlen(value), 36U); + for (index = 0U; index < 36U; index++) { + if (index == 8U || index == 13U || index == 18U || index == 23U) + ck_assert_int_eq(value[index], '-'); + } +} + +static bool +callback_registered(RzbNextRuntime_t *runtime, + const struct RzbNextRuntimeTransition *transition, + void *userData) +{ + struct CallbackState *state = userData; + + (void)runtime; + ck_assert_ptr_ne(state, NULL); + ck_assert_ptr_ne(transition, NULL); + state->registered++; + snprintf(state->lastGeneration, sizeof(state->lastGeneration), "%s", + transition->generation); + return true; +} + +static bool +callback_ready(RzbNextRuntime_t *runtime, void *userData) +{ + struct CallbackState *state = userData; + + ck_assert_ptr_ne(runtime, NULL); + ck_assert_ptr_ne(state, NULL); + state->ready++; + return true; +} + +static bool +callback_pause(RzbNextRuntime_t *runtime, void *userData) +{ + struct CallbackState *state = userData; + + ck_assert_ptr_ne(runtime, NULL); + ck_assert_ptr_ne(state, NULL); + state->pause++; + return !state->failPause; +} + +static bool +callback_resume(RzbNextRuntime_t *runtime, void *userData) +{ + struct CallbackState *state = userData; + + ck_assert_ptr_ne(runtime, NULL); + ck_assert_ptr_ne(state, NULL); + state->resume++; + return true; +} + +static bool +callback_cache_invalidate(RzbNextRuntime_t *runtime, + const char *invalidationId, + void *userData) +{ + struct CallbackState *state = userData; + + ck_assert_ptr_ne(runtime, NULL); + ck_assert_ptr_ne(state, NULL); + ck_assert_ptr_ne(invalidationId, NULL); + state->cacheInvalidate++; + snprintf(state->lastInvalidationId, sizeof(state->lastInvalidationId), "%s", + invalidationId); + return true; +} + +static bool +callback_shutdown(RzbNextRuntime_t *runtime, void *userData) +{ + struct CallbackState *state = userData; + + ck_assert_ptr_ne(runtime, NULL); + ck_assert_ptr_ne(state, NULL); + state->shutdown++; + return true; +} + +static bool +callback_error(RzbNextRuntime_t *runtime, + const char *reasonCode, + bool retryable, + void *userData) +{ + struct CallbackState *state = userData; + + (void)retryable; + ck_assert_ptr_ne(runtime, NULL); + ck_assert_ptr_ne(state, NULL); + ck_assert_ptr_ne(reasonCode, NULL); + state->error++; + snprintf(state->lastError, sizeof(state->lastError), "%s", reasonCode); + return true; +} + +START_TEST(test_runtime_create_enforces_single_context_and_generates_process_uuid) +{ + RzbNextRuntime_t *runtime; + RzbNextRuntime_t *second; + const char *generatedProcessUuid; + + runtime = RzbNextRuntime_Create(NUGGET_UUID, PROCESS_UUID); + ck_assert_ptr_ne(runtime, NULL); + ck_assert_str_eq(RzbNextRuntime_ProcessUuid(runtime), PROCESS_UUID); + second = RzbNextRuntime_Create( + "99999999-9999-4999-8999-999999999999", + "88888888-8888-4888-8888-888888888888"); + ck_assert_ptr_eq(second, NULL); + RzbNextRuntime_Destroy(runtime); + + runtime = RzbNextRuntime_CreateGenerated(NUGGET_UUID); + ck_assert_ptr_ne(runtime, NULL); + generatedProcessUuid = RzbNextRuntime_ProcessUuid(runtime); + assert_valid_uuid(generatedProcessUuid); + ck_assert_str_ne(generatedProcessUuid, PROCESS_UUID); + RzbNextRuntime_Destroy(runtime); +} +END_TEST + +START_TEST(test_runtime_common_metric_names_are_stable) +{ + ck_assert_str_eq(RAZORBACK_RUNTIME_DEPENDENCY_STATE_METRIC, + "razorback.runtime.dependency.state"); + ck_assert_str_eq(RAZORBACK_RUNTIME_WORKFLOW_STATE_METRIC, + "razorback.runtime.workflow.state"); + ck_assert_str_eq(RAZORBACK_RUNTIME_READINESS_STATE_METRIC, + "razorback.runtime.readiness.state"); + ck_assert_str_eq(RAZORBACK_RUNTIME_STARTUP_DURATION_METRIC, + "razorback.runtime.startup.duration"); + ck_assert_str_eq(RAZORBACK_RUNTIME_SHUTDOWN_DRAIN_DURATION_METRIC, + "razorback.runtime.shutdown.drain.duration"); + ck_assert_str_eq(RAZORBACK_RUNTIME_TELEMETRY_FLUSH_OUTCOME_METRIC, + "razorback.runtime.telemetry.flush.outcome"); +} +END_TEST + +START_TEST(test_runtime_health_and_dispatcher_hello_gate) +{ + RzbNextRuntime_t *runtime; + struct RzbNextRuntimeHealth health; + char *hello = read_fixture("messages", "cnc_dispatcher_hello.valid.json"); + char *notReady = mutate_string_field(hello, "availability", "starting"); + + runtime = RzbNextRuntime_Create(NUGGET_UUID, PROCESS_UUID); + ck_assert_ptr_ne(runtime, NULL); + health = RzbNextRuntime_Health(runtime); + ck_assert(health.healthz); + ck_assert(!health.readyz); + ck_assert(RzbNextRuntime_HealthLiveCheck(runtime)); + ck_assert(!RzbNextRuntime_HealthReadyCheck(runtime)); + + RzbNextRuntime_Initialize(runtime); + ck_assert_int_eq(RzbNextRuntime_State(runtime), + RZB_NEXT_RUNTIME_WAITING_FOR_DISPATCHER); + ck_assert(!RzbNextRuntime_ObserveDispatcherHello(runtime, notReady)); + ck_assert(RzbNextRuntime_ObserveDispatcherHello(runtime, hello)); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_REGISTERING); + + RzbNextRuntime_Destroy(runtime); + free(notReady); + free(hello); +} +END_TEST + +START_TEST(test_registration_acceptance_liveness_pause_go_and_bye) +{ + RzbNextRuntime_t *runtime; + struct RzbNextRuntimeTransition transition; + struct RzbNextRuntimeDirectedResult result; + struct RzbNextRuntimeLivenessPlan plan; + char *accepted = read_fixture("messages", "cnc_registration_accepted.valid.json"); + char *command = read_fixture("messages", "cnc_directed_command.valid.json"); + char *pause = mutate_directed_command(command, "pause"); + char *go = mutate_directed_command(command, "go"); + char *bye; + + runtime = RzbNextRuntime_Create(NUGGET_UUID, PROCESS_UUID); + ck_assert_ptr_ne(runtime, NULL); + ck_assert(RzbNextRuntime_BeginRegistration(runtime, REQUEST_ID)); + ck_assert(RzbNextRuntime_RegistrationAccepted(runtime, accepted, &transition)); + ck_assert_int_eq(transition.kind, RZB_NEXT_RUNTIME_TRANSITION_REGISTERED); + ck_assert(transition.ready); + ck_assert_str_eq(transition.generation, + "44444444-4444-4444-8444-444444444444"); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_READY); + ck_assert_str_eq(RzbNextRuntime_RegistrationGeneration(runtime), + "44444444-4444-4444-8444-444444444444"); + ck_assert(RzbNextRuntime_LivenessPlan(runtime, + "2026-06-17T21:00:10.000Z", + &plan)); + ck_assert_uint_eq(plan.interval, 10U); + ck_assert_uint_eq(plan.freshnessWindow, 30U); + ck_assert_uint_eq(plan.messageExpiration, 30U); + ck_assert(RzbNextMessage_Validate(plan.message)); + assert_json_string_field(plan.message, "runtime_policy", "running"); + assert_json_string_field(plan.message, "availability", "ready"); + RzbNextRuntime_LivenessPlanClear(&plan); + + ck_assert(RzbNextRuntime_ApplyDirectedCommand(runtime, pause, &result)); + ck_assert_int_eq(result.effect, RZB_NEXT_RUNTIME_DIRECTED_PAUSE); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_PAUSED); + ck_assert(RzbNextRuntime_LivenessPlan(runtime, + "2026-06-17T21:00:11.000Z", + &plan)); + assert_json_string_field(plan.message, "runtime_policy", "paused"); + assert_json_string_field(plan.message, "availability", "ready"); + RzbNextRuntime_LivenessPlanClear(&plan); + + ck_assert(RzbNextRuntime_ApplyDirectedCommand(runtime, go, &result)); + ck_assert_int_eq(result.effect, RZB_NEXT_RUNTIME_DIRECTED_RESUME); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_READY); + + bye = RzbNextRuntime_BuildBye(runtime, "shutdown", + "2026-06-17T21:00:20.000Z"); + ck_assert_ptr_ne(bye, NULL); + ck_assert(RzbNextMessage_Validate(bye)); + assert_json_string_field(bye, "reason", "shutdown"); + + RzbNext_FreeString(bye); + RzbNextRuntime_Destroy(runtime); + free(go); + free(pause); + free(command); + free(accepted); +} +END_TEST + +START_TEST(test_runtime_callbacks_follow_registration_and_directed_effects) +{ + RzbNextRuntime_t *runtime; + struct CallbackState state; + struct RzbNextRuntimeCallbacks callbacks; + struct RzbNextRuntimeTransition transition; + struct RzbNextRuntimeDirectedResult result; + char *accepted = read_fixture("messages", "cnc_registration_accepted.valid.json"); + char *command = read_fixture("messages", "cnc_directed_command.valid.json"); + char *pause = mutate_directed_command(command, "pause"); + char *go = mutate_directed_command(command, "go"); + char *terminate = mutate_directed_command(command, "terminate"); + + memset(&state, 0, sizeof(state)); + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.onRegistered = callback_registered; + callbacks.onReady = callback_ready; + callbacks.onPause = callback_pause; + callbacks.onResume = callback_resume; + callbacks.onCacheInvalidate = callback_cache_invalidate; + callbacks.onShutdown = callback_shutdown; + callbacks.onError = callback_error; + callbacks.userData = &state; + + runtime = RzbNextRuntime_Create(NUGGET_UUID, PROCESS_UUID); + ck_assert_ptr_ne(runtime, NULL); + ck_assert(RzbNextRuntime_SetCallbacks(runtime, &callbacks)); + ck_assert(RzbNextRuntime_BeginRegistration(runtime, REQUEST_ID)); + ck_assert(RzbNextRuntime_RegistrationAccepted(runtime, accepted, &transition)); + ck_assert_uint_eq(state.registered, 1U); + ck_assert_uint_eq(state.ready, 1U); + ck_assert_str_eq(state.lastGeneration, transition.generation); + + ck_assert(RzbNextRuntime_ApplyDirectedCommand(runtime, pause, &result)); + ck_assert_int_eq(result.effect, RZB_NEXT_RUNTIME_DIRECTED_PAUSE); + ck_assert_uint_eq(state.pause, 1U); + + ck_assert(RzbNextRuntime_ApplyDirectedCommand(runtime, go, &result)); + ck_assert_int_eq(result.effect, RZB_NEXT_RUNTIME_DIRECTED_RESUME); + ck_assert_uint_eq(state.resume, 1U); + + ck_assert(RzbNextRuntime_ApplyDirectedCommand(runtime, command, &result)); + ck_assert_int_eq(result.effect, RZB_NEXT_RUNTIME_DIRECTED_CACHE_INVALIDATE); + ck_assert_str_eq(result.invalidationId, + "66666666-6666-4666-8666-666666666666"); + ck_assert_uint_eq(state.cacheInvalidate, 1U); + ck_assert_str_eq(state.lastInvalidationId, + "66666666-6666-4666-8666-666666666666"); + + ck_assert(RzbNextRuntime_ApplyDirectedCommand(runtime, terminate, &result)); + ck_assert_int_eq(result.effect, RZB_NEXT_RUNTIME_DIRECTED_SHUTDOWN); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_DRAINING); + ck_assert_uint_eq(state.shutdown, 1U); + ck_assert_uint_eq(RzbNextRuntime_CallbackFailureCount(runtime), 0U); + ck_assert_ptr_eq(RzbNextRuntime_LastCallbackFailure(runtime), NULL); + + RzbNextRuntime_Destroy(runtime); + free(terminate); + free(go); + free(pause); + free(command); + free(accepted); +} +END_TEST + +START_TEST(test_runtime_callback_failure_is_diagnostic_only) +{ + RzbNextRuntime_t *runtime; + struct CallbackState state; + struct RzbNextRuntimeCallbacks callbacks; + struct RzbNextRuntimeDirectedResult result; + char *accepted = read_fixture("messages", "cnc_registration_accepted.valid.json"); + char *command = read_fixture("messages", "cnc_directed_command.valid.json"); + char *pause = mutate_directed_command(command, "pause"); + + memset(&state, 0, sizeof(state)); + state.failPause = true; + memset(&callbacks, 0, sizeof(callbacks)); + callbacks.onPause = callback_pause; + callbacks.onError = callback_error; + callbacks.userData = &state; + + runtime = RzbNextRuntime_Create(NUGGET_UUID, PROCESS_UUID); + ck_assert_ptr_ne(runtime, NULL); + ck_assert(RzbNextRuntime_SetCallbacks(runtime, &callbacks)); + ck_assert(RzbNextRuntime_BeginRegistration(runtime, REQUEST_ID)); + ck_assert(RzbNextRuntime_RegistrationAccepted(runtime, accepted, NULL)); + + ck_assert(RzbNextRuntime_ApplyDirectedCommand(runtime, pause, &result)); + ck_assert_int_eq(result.effect, RZB_NEXT_RUNTIME_DIRECTED_PAUSE); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_PAUSED); + ck_assert_uint_eq(state.pause, 1U); + ck_assert_uint_eq(state.error, 1U); + ck_assert_str_eq(state.lastError, "callback_failed"); + ck_assert_uint_eq(RzbNextRuntime_CallbackFailureCount(runtime), 1U); + ck_assert_str_eq(RzbNextRuntime_LastCallbackFailure(runtime), "onPause"); + + RzbNextRuntime_Destroy(runtime); + free(pause); + free(command); + free(accepted); +} +END_TEST + +START_TEST(test_operational_readiness_gate_pauses_resumes_and_drains) +{ + RzbNextRuntime_t *runtime; + struct RzbNextRuntimeHealth health; + char *accepted = read_fixture("messages", "cnc_registration_accepted.valid.json"); + + runtime = RzbNextRuntime_Create(NUGGET_UUID, PROCESS_UUID); + ck_assert_ptr_ne(runtime, NULL); + ck_assert(RzbNextRuntime_BeginRegistration(runtime, REQUEST_ID)); + ck_assert(RzbNextRuntime_RegistrationAccepted(runtime, accepted, NULL)); + ck_assert(RzbNextRuntime_AcceptsWork(runtime)); + ck_assert(RzbNextRuntime_StartWhenReady(runtime)); + ck_assert(RzbNextRuntime_StartWhenReady(runtime)); + ck_assert_uint_eq(RzbNextRuntime_InFlightWork(runtime), 2U); + + RzbNextRuntime_PauseNewWork(runtime); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_PAUSED); + ck_assert(!RzbNextRuntime_AcceptsWork(runtime)); + ck_assert(!RzbNextRuntime_StartWhenReady(runtime)); + health = RzbNextRuntime_Health(runtime); + ck_assert(health.healthz); + ck_assert(!health.readyz); + + ck_assert(RzbNextRuntime_ResumeWhenReady(runtime)); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_READY); + ck_assert(RzbNextRuntime_AcceptsWork(runtime)); + + RzbNextRuntime_PauseNewWork(runtime); + RzbNextRuntime_CompleteWork(runtime); + ck_assert_uint_eq(RzbNextRuntime_InFlightWork(runtime), 1U); + ck_assert(!RzbNextRuntime_Drain(runtime)); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_DRAINING); + ck_assert(!RzbNextRuntime_AcceptsWork(runtime)); + ck_assert(!RzbNextRuntime_ResumeWhenReady(runtime)); + RzbNextRuntime_CompleteWork(runtime); + ck_assert_uint_eq(RzbNextRuntime_InFlightWork(runtime), 0U); + ck_assert(RzbNextRuntime_Drain(runtime)); + + RzbNextRuntime_Destroy(runtime); + free(accepted); +} +END_TEST + +START_TEST(test_dependency_outage_pauses_readiness_until_recovered) +{ + RzbNextRuntime_t *runtime; + struct RzbNextRuntimeHealth health; + struct RzbNextRuntimeLivenessPlan plan; + struct RzbNextRuntimeDirectedResult result; + char *accepted = read_fixture("messages", "cnc_registration_accepted.valid.json"); + char *command = read_fixture("messages", "cnc_directed_command.valid.json"); + char *go = mutate_directed_command(command, "go"); + + runtime = RzbNextRuntime_Create(NUGGET_UUID, PROCESS_UUID); + ck_assert_ptr_ne(runtime, NULL); + ck_assert(RzbNextRuntime_BeginRegistration(runtime, REQUEST_ID)); + ck_assert(RzbNextRuntime_RegistrationAccepted(runtime, accepted, NULL)); + ck_assert(RzbNextRuntime_AcceptsWork(runtime)); + + ck_assert(RzbNextRuntime_DependencyUnavailable(runtime)); + + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_READY); + ck_assert(!RzbNextRuntime_AcceptsWork(runtime)); + ck_assert(!RzbNextRuntime_StartWhenReady(runtime)); + health = RzbNextRuntime_Health(runtime); + ck_assert(health.healthz); + ck_assert(!health.readyz); + ck_assert(RzbNextRuntime_LivenessPlan(runtime, + "2026-06-17T21:00:13.000Z", + &plan)); + assert_json_string_field(plan.message, "runtime_policy", "running"); + assert_json_string_field(plan.message, "availability", + "dependency_paused"); + RzbNextRuntime_LivenessPlanClear(&plan); + + ck_assert(RzbNextRuntime_ApplyDirectedCommand(runtime, go, &result)); + ck_assert_int_eq(result.effect, RZB_NEXT_RUNTIME_DIRECTED_RESUME); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_READY); + ck_assert(!RzbNextRuntime_AcceptsWork(runtime)); + + ck_assert(RzbNextRuntime_DependencyRecovered(runtime)); + + ck_assert(RzbNextRuntime_AcceptsWork(runtime)); + health = RzbNextRuntime_Health(runtime); + ck_assert(health.readyz); + ck_assert(RzbNextRuntime_LivenessPlan(runtime, + "2026-06-17T21:00:14.000Z", + &plan)); + assert_json_string_field(plan.message, "availability", "ready"); + RzbNextRuntime_LivenessPlanClear(&plan); + + RzbNextRuntime_Destroy(runtime); + free(go); + free(command); + free(accepted); +} +END_TEST + +START_TEST(test_stale_generation_triggers_reregister_and_pre_registration_ignored) +{ + RzbNextRuntime_t *runtime; + struct RzbNextRuntimeDirectedResult result; + char *accepted = read_fixture("messages", "cnc_registration_accepted.valid.json"); + char *command = read_fixture("messages", "cnc_directed_command.valid.json"); + char *stale = mutate_string_field(command, "registration_generation", + "77777777-7777-4777-8777-777777777777"); + + runtime = RzbNextRuntime_Create(NUGGET_UUID, PROCESS_UUID); + ck_assert_ptr_ne(runtime, NULL); + ck_assert(RzbNextRuntime_ApplyDirectedCommand(runtime, command, &result)); + ck_assert_int_eq(result.effect, RZB_NEXT_RUNTIME_DIRECTED_IGNORED_STALE); + + ck_assert(RzbNextRuntime_BeginRegistration(runtime, REQUEST_ID)); + ck_assert(RzbNextRuntime_RegistrationAccepted(runtime, accepted, NULL)); + ck_assert(RzbNextRuntime_ApplyDirectedCommand(runtime, stale, &result)); + ck_assert_int_eq(result.effect, RZB_NEXT_RUNTIME_DIRECTED_REREGISTER); + ck_assert(result.staleGeneration); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_REGISTERING); + ck_assert_ptr_eq(RzbNextRuntime_RegistrationGeneration(runtime), NULL); + + RzbNextRuntime_Destroy(runtime); + free(stale); + free(command); + free(accepted); +} +END_TEST + +START_TEST(test_registration_rejected_retryable_and_terminal) +{ + RzbNextRuntime_t *runtime; + struct RzbNextRuntimeTransition transition; + char *rejected = read_fixture("messages", "cnc_registration_rejected.valid.json"); + char *terminal = mutate_rejected_terminal(rejected); + + runtime = RzbNextRuntime_Create(NUGGET_UUID, PROCESS_UUID); + ck_assert_ptr_ne(runtime, NULL); + ck_assert(RzbNextRuntime_BeginRegistration(runtime, REQUEST_ID)); + ck_assert(RzbNextRuntime_RegistrationRejected(runtime, rejected, &transition)); + ck_assert_int_eq(transition.kind, RZB_NEXT_RUNTIME_TRANSITION_REJECTED); + ck_assert(transition.retryable); + ck_assert_uint_eq(transition.retryAfter, 15U); + ck_assert_str_eq(transition.reasonCode, "dependency_unavailable"); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_REGISTERING); + + ck_assert(RzbNextRuntime_BeginRegistration(runtime, REQUEST_ID)); + ck_assert(RzbNextRuntime_RegistrationRejected(runtime, terminal, &transition)); + ck_assert(!transition.retryable); + ck_assert_str_eq(transition.reasonCode, "unauthorized"); + ck_assert_int_eq(RzbNextRuntime_State(runtime), RZB_NEXT_RUNTIME_FAILED); + ck_assert(!RzbNextRuntime_HealthLiveCheck(runtime)); + + RzbNextRuntime_Destroy(runtime); + free(terminal); + free(rejected); +} +END_TEST + +START_TEST(test_retry_state_coalesces_dirty_rerun_and_terminal_stop) +{ + struct RzbNextRuntimeRetryPolicy policy = { 1U, 30U, 20U }; + struct RzbNextRuntimeRetryState state; + struct RzbNextRuntimeRetryDecision decision; + + RzbNextRuntimeRetry_Init(&state, policy); + decision = RzbNextRuntimeRetry_RequestRegistration(&state); + ck_assert_int_eq(decision.action, RZB_NEXT_RUNTIME_RETRY_START_NOW); + ck_assert(state.inFlight); + decision = RzbNextRuntimeRetry_RequestRegistration(&state); + ck_assert_int_eq(decision.action, + RZB_NEXT_RUNTIME_RETRY_ALREADY_IN_FLIGHT); + ck_assert(state.dirtyRerun); + decision = RzbNextRuntimeRetry_RetryableFailure(&state, 0U, 50U); + ck_assert_int_eq(decision.action, RZB_NEXT_RUNTIME_RETRY_START_NOW); + ck_assert(state.inFlight); + ck_assert(!state.dirtyRerun); + + decision = RzbNextRuntimeRetry_RetryableFailure(&state, 7U, 100U); + ck_assert_int_eq(decision.action, RZB_NEXT_RUNTIME_RETRY_AFTER); + ck_assert_uint_eq(decision.delay, 7U); + decision = RzbNextRuntimeRetry_RequestRegistration(&state); + ck_assert_int_eq(decision.action, RZB_NEXT_RUNTIME_RETRY_START_NOW); + RzbNextRuntimeRetry_Accepted(&state); + ck_assert(!state.inFlight); + ck_assert_uint_eq(state.consecutiveFailureCount, 0U); + + decision = RzbNextRuntimeRetry_TerminalFailure(&state); + ck_assert_int_eq(decision.action, RZB_NEXT_RUNTIME_RETRY_STOP); + ck_assert(!state.inFlight); +} +END_TEST + +static Suite * +runtime_next_suite(void) +{ + Suite *suite; + TCase *testcase; + + suite = suite_create("runtime_next"); + testcase = tcase_create("core"); + tcase_add_test(testcase, + test_runtime_create_enforces_single_context_and_generates_process_uuid); + tcase_add_test(testcase, test_runtime_common_metric_names_are_stable); + tcase_add_test(testcase, test_runtime_health_and_dispatcher_hello_gate); + tcase_add_test(testcase, + test_registration_acceptance_liveness_pause_go_and_bye); + tcase_add_test(testcase, + test_runtime_callbacks_follow_registration_and_directed_effects); + tcase_add_test(testcase, + test_runtime_callback_failure_is_diagnostic_only); + tcase_add_test(testcase, + test_operational_readiness_gate_pauses_resumes_and_drains); + tcase_add_test(testcase, + test_dependency_outage_pauses_readiness_until_recovered); + tcase_add_test(testcase, + test_stale_generation_triggers_reregister_and_pre_registration_ignored); + tcase_add_test(testcase, test_registration_rejected_retryable_and_terminal); + tcase_add_test(testcase, + test_retry_state_coalesces_dirty_rerun_and_terminal_stop); + suite_add_tcase(suite, testcase); + return suite; +} + +int +main(void) +{ + Suite *suite; + SRunner *runner; + int failed; + + suite = runtime_next_suite(); + runner = srunner_create(suite); + srunner_run_all(runner, CK_NORMAL); + failed = srunner_ntests_failed(runner); + srunner_free(runner); + return failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_schema_conformance.c b/tests/test_schema_conformance.c new file mode 100644 index 0000000..1d1ff2e --- /dev/null +++ b/tests/test_schema_conformance.c @@ -0,0 +1,65 @@ +/* + * 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 + +START_TEST(test_shared_schema_fixture_conformance_passes) +{ + char command[4096]; + int status; + + snprintf(command, sizeof(command), + "cd '%s' && python3 tools/schema_conformance.py", + RAZORBACK_REPO_ROOT); + status = system(command); + ck_assert_int_eq(status, 0); +} +END_TEST + +static Suite * +schema_conformance_suite(void) +{ + Suite *suite; + TCase *testcase; + + suite = suite_create("schema_conformance"); + testcase = tcase_create("core"); + tcase_add_test(testcase, test_shared_schema_fixture_conformance_passes); + suite_add_tcase(suite, testcase); + return suite; +} + +int +main(void) +{ + Suite *suite; + SRunner *runner; + int failed; + + suite = schema_conformance_suite(); + runner = srunner_create(suite); + srunner_run_all(runner, CK_NORMAL); + failed = srunner_ntests_failed(runner); + srunner_free(runner); + return failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/tests/test_telemetry_stubs.c b/tests/test_telemetry_stubs.c index 63b6b54..d6823aa 100644 --- a/tests/test_telemetry_stubs.c +++ b/tests/test_telemetry_stubs.c @@ -7,3 +7,83 @@ Telemetry_ClearContext(TelemetryContextCarrier_t **context) *context = NULL; } } + +double +Telemetry_GetMonotonicTimeSeconds(void) +{ + return 0.0; +} + +void +Telemetry_RecordRuntimeDependencyState( + const char *dependency, + const char *status, + const char *reasonCode +) +{ + (void)dependency; + (void)status; + (void)reasonCode; +} + +void +Telemetry_RecordRuntimeWorkflowState( + const char *workflow, + const char *state, + const char *reasonCode +) +{ + (void)workflow; + (void)state; + (void)reasonCode; +} + +void +Telemetry_RecordRuntimeReadinessState( + const char *state, + const char *outcome, + const char *reasonCode +) +{ + (void)state; + (void)outcome; + (void)reasonCode; +} + +void +Telemetry_RecordRuntimeStartupDuration( + double durationSeconds, + const char *outcome, + const char *reasonCode +) +{ + (void)durationSeconds; + (void)outcome; + (void)reasonCode; +} + +void +Telemetry_RecordRuntimeShutdownDrainDuration( + double durationSeconds, + const char *service, + const char *outcome, + const char *reasonCode +) +{ + (void)durationSeconds; + (void)service; + (void)outcome; + (void)reasonCode; +} + +void +Telemetry_RecordRuntimeTelemetryFlushOutcome( + const char *service, + const char *outcome, + const char *reasonCode +) +{ + (void)service; + (void)outcome; + (void)reasonCode; +} diff --git a/tools/build/debian12/build-deps.txt b/tools/build/debian12/build-deps.txt index 59beae9..d51d8b9 100644 --- a/tools/build/debian12/build-deps.txt +++ b/tools/build/debian12/build-deps.txt @@ -13,6 +13,7 @@ libssl-dev libconfig-dev libssh-dev libjson-c-dev +libyaml-dev libmagic-dev librabbitmq-dev libprotobuf-dev @@ -23,4 +24,3 @@ libabsl-dev nlohmann-json3-dev libgtest-dev zlib1g-dev - diff --git a/tools/build/debian12/run-deps.txt b/tools/build/debian12/run-deps.txt index 77085f4..1c232b5 100644 --- a/tools/build/debian12/run-deps.txt +++ b/tools/build/debian12/run-deps.txt @@ -5,9 +5,9 @@ libssl3 libconfig9 libssh-4 libjson-c5 +libyaml-0-2 libmagic1 librabbitmq4 libgrpc++1.51 libprotobuf32 zlib1g - diff --git a/tools/schema_conformance.py b/tools/schema_conformance.py new file mode 100644 index 0000000..15fd9c0 --- /dev/null +++ b/tools/schema_conformance.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 Cisco Systems, Inc. +# SPDX-License-Identifier: GPL-2.0-only + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def main() -> int: + root = Path(__file__).resolve().parents[1] + schema_root = root / "schemas" / "razorback" + tool = schema_root / "tools" / "conformance.py" + subprocess.run([sys.executable, str(tool), "--root", str(schema_root)], check=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())