From 329a05b048a6fe3a3970e6d6603d39ff621e08fb Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 12:33:31 -0500 Subject: [PATCH 01/17] droplet is a cloud variable server in C --- .gitmodules | 6 + droplet/.gitignore | 12 + droplet/CMakeLists.txt | 28 ++ droplet/README.md | 11 + droplet/droplet/.clang-format | 192 +++++++++ droplet/droplet/droplet.c | 79 ++++ droplet/droplet/protocol_cloud.c | 504 ++++++++++++++++++++++++ droplet/droplet/protocol_cloud.h | 69 ++++ droplet/droplet/resizable_buffer.c | 111 ++++++ droplet/droplet/resizable_buffer.h | 60 +++ droplet/droplet/resizable_buffer_test.c | 24 ++ droplet/jsmn | 1 + droplet/libwebsockets | 1 + droplet/playground/index.html | 85 ++++ droplet/playground/stress.html | 178 +++++++++ 15 files changed, 1361 insertions(+) create mode 100644 .gitmodules create mode 100644 droplet/.gitignore create mode 100644 droplet/CMakeLists.txt create mode 100644 droplet/README.md create mode 100644 droplet/droplet/.clang-format create mode 100644 droplet/droplet/droplet.c create mode 100644 droplet/droplet/protocol_cloud.c create mode 100644 droplet/droplet/protocol_cloud.h create mode 100644 droplet/droplet/resizable_buffer.c create mode 100644 droplet/droplet/resizable_buffer.h create mode 100644 droplet/droplet/resizable_buffer_test.c create mode 160000 droplet/jsmn create mode 160000 droplet/libwebsockets create mode 100644 droplet/playground/index.html create mode 100644 droplet/playground/stress.html diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..d4895fe9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "droplet/libwebsockets"] + path = droplet/libwebsockets + url = https://github.com/warmcat/libwebsockets +[submodule "droplet/jsmn"] + path = droplet/jsmn + url = https://github.com/zserge/jsmn diff --git a/droplet/.gitignore b/droplet/.gitignore new file mode 100644 index 00000000..f6c28c50 --- /dev/null +++ b/droplet/.gitignore @@ -0,0 +1,12 @@ +# macOS +.DS_Store + +# cmake +build + +# various C +*.out +*.o +*.so +*.a +callgrind.out.* diff --git a/droplet/CMakeLists.txt b/droplet/CMakeLists.txt new file mode 100644 index 00000000..c0f6a967 --- /dev/null +++ b/droplet/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.13) + +project(droplet LANGUAGES C) + +add_subdirectory(libwebsockets) + +set(CMAKE_C_FLAGS_DEBUG "-g -Wall -pedantic ${CMAKE_C_FLAGS_DEBUG}") +set(CMAKE_C_FLAGS_RELEASE "-g -Wall -pedantic -O3 -march=native ${CMAKE_C_FLAGS_RELEASE}") + +add_executable( + droplet + droplet/droplet.c + droplet/protocol_cloud.c + droplet/resizable_buffer.c +) +target_include_directories( + droplet PRIVATE + jsmn + ${PROJECT_BINARY_DIR}/libwebsockets + ${PROJECT_BINARY_DIR}/libwebsockets/include +) +target_link_libraries(droplet websockets) + +add_executable( + resizable_buffer_test + droplet/resizable_buffer_test.c + droplet/resizable_buffer.c +) diff --git a/droplet/README.md b/droplet/README.md new file mode 100644 index 00000000..8fd72624 --- /dev/null +++ b/droplet/README.md @@ -0,0 +1,11 @@ +# droplet + +A cloud variable server in C. + +```bash +rm -rf build && mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=DEBUG -DLWS_WITH_SSL=0 -DLWS_WITH_SHARED=0 -DLWS_WITH_MINIMAL_EXAMPLES=0 .. && make -j4 +``` + +```bash +-DCMAKE_BUILD_TYPE=RELEASE +``` diff --git a/droplet/droplet/.clang-format b/droplet/droplet/.clang-format new file mode 100644 index 00000000..c8e7b33e --- /dev/null +++ b/droplet/droplet/.clang-format @@ -0,0 +1,192 @@ +--- +Language: Cpp +# BasedOnStyle: WebKit +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +AlignArrayOfStructures: None +AlignConsecutiveMacros: None +AlignConsecutiveAssignments: None +AlignConsecutiveBitFields: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Right +AlignOperands: DontAlign +AlignTrailingComments: false +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: true +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: MultiLine +AttributeMacros: + - __capability +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: All +BreakBeforeConceptDeclarations: true +BreakBeforeBraces: WebKit +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 0 +CommentPragmas: '^ IWYU pragma:' +QualifierAlignment: Leave +CompactNamespaces: false +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: false +DeriveLineEnding: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +PackConstructorInitializers: BinPack +BasedOnStyle: '' +ConstructorInitializerAllOnOneLineOrOnePerLine: false +AllowAllConstructorInitializersOnNextLine: true +FixNamespaceComments: false +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IfMacros: + - KJ_IF_MAYBE +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IncludeIsMainRegex: '(Test)?$' +IncludeIsMainSourceRegex: '' +IndentAccessModifiers: false +IndentCaseLabels: false +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentRequires: false +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: true +LambdaBodyIndentation: Signature +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: Inner +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 4 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakOpenParenthesis: 0 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 60 +PenaltyIndentedWhitespace: 0 +PointerAlignment: Left +PPIndentWidth: -1 +ReferenceAlignment: Pointer +ReflowComments: true +RemoveBracesLLVM: false +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SortIncludes: CaseSensitive +SortJavaStaticImport: Before +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeParensOptions: + AfterControlStatements: true + AfterForeachMacros: true + AfterFunctionDefinitionName: false + AfterFunctionDeclarationName: false + AfterIfMacros: true + AfterOverloadedOperator: false + BeforeNonEmptyParentheses: false +SpaceAroundPointerQualifiers: Default +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: Never +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +BitFieldColonSpacing: Both +Standard: Latest +StatementAttributeLikeMacros: + - Q_EMIT +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE + - NS_SWIFT_NAME + - CF_SWIFT_NAME +... + diff --git a/droplet/droplet/droplet.c b/droplet/droplet/droplet.c new file mode 100644 index 00000000..a990b956 --- /dev/null +++ b/droplet/droplet/droplet.c @@ -0,0 +1,79 @@ +#include "protocol_cloud.h" +#include +#include +#include + +static struct lws_protocols protocols[] = { + LWS_PLUGIN_PROTOCOL_CLOUD, + LWS_PROTOCOL_LIST_TERM +}; + +static const struct lws_http_mount mount = { + .mount_next = NULL, + .mountpoint = "/", + .origin = "./playground", + .def = "index.html", + .protocol = NULL, + .cgienv = NULL, + .extra_mimetypes = NULL, + .interpret = NULL, + .cgi_timeout = 0, + .cache_max_age = 0, + .auth_mask = 0, + .cache_reusable = 0, + .cache_revalidate = 0, + .cache_intermediaries = 0, + .cache_no = 0, + .origin_protocol = LWSMPRO_FILE, + .mountpoint_len = 1, + .basic_auth_login_file = NULL, +}; + +static bool interrupted; + +static void sigint_handler(int sig) +{ + interrupted = true; +} + +static int get_port(int argc, const char** argv) +{ + const char* p = lws_cmdline_option(argc, argv, "-p"); + if (p) { + return atoi(p); + } + return 9082; +} + +int main(int argc, const char** argv) +{ + signal(SIGINT, sigint_handler); + +#ifndef NDEBUG + lws_set_log_level(LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); +#else + lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); +#endif + + struct lws_context_creation_info info = { 0 }; + info.port = get_port(argc, argv); + info.mounts = &mount; + info.protocols = protocols; + + lwsl_user("Starting on http://localhost:%d | ws://localhost:%d", info.port, info.port); + struct lws_context* context = lws_create_context(&info); + if (!context) { + lwsl_err("lws init failed\n"); + return 1; + } + + lwsl_cx_user(context, "Entering event loop"); + int n = 0; + while (n >= 0 && !interrupted) { + n = lws_service(context, 0); + } + + lwsl_cx_user(context, "Event loop interrupted"); + lws_context_destroy(context); + return 0; +} diff --git a/droplet/droplet/protocol_cloud.c b/droplet/droplet/protocol_cloud.c new file mode 100644 index 00000000..286b2119 --- /dev/null +++ b/droplet/droplet/protocol_cloud.c @@ -0,0 +1,504 @@ +#include "protocol_cloud.h" +#include +#include +#include +#include + +#define JSMN_STRICT +#include + +#define MAX_JSON_TOKENS 64 + +static const jsmntok_t* json_get_key(const unsigned char* data, const jsmntok_t* tokens, int num_tokens, const char* name) +{ + if (num_tokens < 1) { + return NULL; + } + + /* If top level object isn't an object, nothing we can do */ + if (tokens[0].type != JSMN_OBJECT) { + return NULL; + } + + size_t name_len = strlen(name); + + /* + * First token was already checked to be an object, and the last token can't + * be a key, so don't check them. + */ + int i = 1; + while (i < num_tokens - 1) { + /* If we encounter a strange looking key, don't try to continue */ + if (tokens[i].type != JSMN_STRING || tokens[i].size < 1) { + return NULL; + } + + /* token.size is how many tokens are inside, not the length of the token itself */ + size_t token_length = tokens[i].end - tokens[i].start; + if (token_length == name_len && memcmp(data + tokens[i].start, name, name_len) == 0) { + return &tokens[i + 1]; + } else { + i += 1 + tokens[i].size; + } + } + + return NULL; +} + +static struct cloud_room* room_get_or_create(struct cloud_per_vhost_data* vhd, const unsigned char* name, size_t name_len) +{ + /* TODO: this is O(n), can easily be O(log n) or better */ + + if (name_len > MAX_ROOM_NAME_LENGTH) { + return NULL; + } + + for (size_t i = 0; i < MAX_ROOMS; i++) { + struct cloud_room* room = &vhd->rooms[i]; + if (room->active && room->name_len == name_len && memcmp(room->name, name, name_len) == 0) { + return room; + } + } + + /* + * Rooms can be deleted, so insert at the earliest spot to reduce average iterations + * to get this room later. + */ + for (size_t i = 0; i < MAX_ROOMS; i++) { + struct cloud_room* room = &vhd->rooms[i]; + if (!room->active) { + memcpy(room->name, name, name_len); + room->name_len = name_len; + + /* variables are initialized as needed */ + room->variables_len = 0; + + for (size_t j = 0; j < MAX_ROOM_CONNECTIONS; j++) { + room->connections[j] = NULL; + } + + room->active = true; + + return room; + } + } + + return NULL; +} + +static void room_free(struct cloud_room* room) +{ + for (size_t i = 0; i < MAX_ROOM_VARIABLES; i++) { + resizable_buffer_free(&room->variables[i].value_buffer); + } +} + +static bool room_add_connection(struct cloud_room* room, struct cloud_per_session_data* pss) +{ + /* TODO: this is O(n), can easily be better */ + + for (size_t i = 0; i < MAX_ROOM_CONNECTIONS; i++) { + if (room->connections[i] == NULL) { + room->connections[i] = pss; + return true; + } + } + + return false; +} + +static void room_remove_connection(struct cloud_room* room, struct cloud_per_session_data* pss) +{ + /* TODO: this is O(n), can easily be better */ + + for (size_t i = 0; i < MAX_ROOM_CONNECTIONS; i++) { + if (room->connections[i] == pss) { + room->connections[i] = NULL; + break; + } + } +} + +/* + * Returns the index of the variable in room->variables or -1 if it can't be found or created. The + * index is returned instead of a pointer to the variable as the index is useful when updating the + * sequence number of the client that sent this. + */ +static int room_get_or_create_variable_idx(struct cloud_room* room, const unsigned char* name, size_t name_len) +{ + /* TODO: this is O(n), can easily be O(log n) or better */ + + if (name_len > MAX_VARIABLE_NAME_LENGTH) { + return -1; + } + + size_t i = 0; + struct cloud_variable* var; + for (; i < room->variables_len; i++) { + var = &room->variables[i]; + if (var->name_len == name_len && memcmp(var->name, name, name_len) == 0) { + return i; + } + } + + /* Variables are append only, so just add it after the last valid variable */ + if (i < MAX_ROOM_VARIABLES) { + var = &room->variables[i]; + + var->sequence_number = 0; + + memcpy(var->name, name, name_len); + var->name_len = name_len; + + resizable_buffer_init(&var->value_buffer, MAX_VARIABLE_VALUE_LENGTH); + + room->variables_len++; + + return i; + } + + return -1; +} + +static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_session_data* pss, const unsigned char* data, size_t len) +{ + jsmn_parser parser; + jsmntok_t tokens[MAX_JSON_TOKENS]; + jsmn_init(&parser); + + /* char* and unsigned char* are the same in memory */ + int num_tokens = jsmn_parse(&parser, (char*)data, len, tokens, MAX_JSON_TOKENS); + if (num_tokens < 0) { + lwsl_wsi_user(pss->wsi, "Invalid JSON: %d", num_tokens); + return false; + } + + lwsl_wsi_user(pss->wsi, "Parsed %d JSON tokens", num_tokens); + + const jsmntok_t* method_json = json_get_key(data, tokens, num_tokens, "method"); + if (method_json == NULL || method_json->type != JSMN_STRING) { + lwsl_wsi_user(pss->wsi, "method missing or not a string"); + return false; + } + + const unsigned char* method_data = data + method_json->start; + size_t method_len = method_json->end - method_json->start; + + if (!pss->room) { + /* Client must perform handshake */ + + static const char* handshake = "handshake"; + if (method_len != strlen(handshake) || memcmp(method_data, handshake, strlen(handshake)) != 0) { + lwsl_wsi_user(pss->wsi, "method was not handshake"); + return false; + } + + const jsmntok_t* user_json = json_get_key(data, tokens, num_tokens, "user"); + if (user_json == NULL || user_json->type != JSMN_STRING) { + lwsl_wsi_user(pss->wsi, "handshake user missing or not a string"); + return false; + } + + const jsmntok_t* project_id_json = json_get_key(data, tokens, num_tokens, "project_id"); + if (project_id_json == NULL || project_id_json->type != JSMN_STRING) { + lwsl_wsi_user(pss->wsi, "handshake project_id missing or not a string"); + return false; + } + + /* TODO: username validation */ + + const unsigned char* project_id_data = data + project_id_json->start; + size_t project_id_len = project_id_json->end - project_id_json->start; + struct cloud_room* room = room_get_or_create(vhd, project_id_data, project_id_len); + if (!room) { + lwsl_wsi_user(pss->wsi, "Failed to find or create room"); + return false; + } + + if (!room_add_connection(room, pss)) { + lwsl_wsi_user(pss->wsi, "Failed to add to room"); + return false; + } + + lwsl_wsi_user(pss->wsi, "Joined room"); + pss->room = room; + + /* Send initial variable status */ + pss->tx_due = true; + lws_callback_on_writable(pss->wsi); + + return true; + } + + static const char* set = "set"; + if (method_len != strlen(set) || memcmp(method_data, set, strlen(set)) != 0) { + lwsl_wsi_user(pss->wsi, "method was not set"); + return false; + } + + const jsmntok_t* name_json = json_get_key(data, tokens, num_tokens, "name"); + if (name_json == NULL || name_json->type != JSMN_STRING) { + lwsl_wsi_user(pss->wsi, "name missing or not a string"); + return false; + } + + const jsmntok_t* value_json = json_get_key(data, tokens, num_tokens, "value"); + if (value_json == NULL || (value_json->type != JSMN_STRING && value_json->type != JSMN_PRIMITIVE)) { + lwsl_wsi_user(pss->wsi, "value missing or not a string or primitive"); + return false; + } + + const unsigned char* name_data = data + name_json->start; + size_t name_len = name_json->end - name_json->start; + + int variable_idx = room_get_or_create_variable_idx(pss->room, name_data, name_len); + if (variable_idx < 0) { + lwsl_wsi_user(pss->wsi, "Could not find or create variable: %d", variable_idx); + return false; + } + + struct cloud_variable* variable = &pss->room->variables[variable_idx]; + const unsigned char* value_data = data + value_json->start; + size_t value_len = value_json->end - value_json->start; + + resizable_buffer_clear(&variable->value_buffer); + enum resizable_buffer_error buffer_result = resizable_buffer_push(&variable->value_buffer, value_data, value_len); + if (buffer_result != resizable_buffer_ok) { + lwsl_wsi_user(pss->wsi, "Variable buffer push failed: %d", buffer_result); + return false; + } + + variable->sequence_number++; + + /* Don't need to send new value to the client that sent it */ + pss->variable_sequence_numbers[variable_idx] = variable->sequence_number; + + for (size_t i = 0; i < MAX_ROOM_CONNECTIONS; i++) { + struct cloud_per_session_data* other_pss = pss->room->connections[i]; + if (other_pss != NULL && other_pss != pss) { + other_pss->tx_due = true; + lws_callback_on_writable(other_pss->wsi); + } + } + + return true; +} + +int callback_cloud(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len) +{ + switch (reason) { + case LWS_CALLBACK_PROTOCOL_INIT: { + struct lws_vhost* vhost = lws_get_vhost(wsi); + lwsl_vhost_user(vhost, "Initializing cloud protocol"); + + struct cloud_per_vhost_data* vhd = lws_protocol_vh_priv_zalloc( + vhost, + lws_get_protocol(wsi), + sizeof(struct cloud_per_vhost_data)); + + /* + * Not necessary, but for clarity, we just need the rooms to be marked + * as inactive right now. The rest will be initialized when a room is + * actually created. + */ + for (size_t i = 0; i < MAX_ROOMS; i++) { + vhd->rooms[i].active = false; + } + + return 0; + } + + case LWS_CALLBACK_PROTOCOL_DESTROY: { + lwsl_user("Destroying cloud protocol"); + + struct cloud_per_vhost_data* vhd = (struct cloud_per_vhost_data*)lws_protocol_vh_priv_get( + lws_get_vhost(wsi), + lws_get_protocol(wsi)); + + for (size_t i = 0; i < MAX_ROOMS; i++) { + struct cloud_room* room = &vhd->rooms[i]; + if (room->active) { + room_free(room); + } + } + + return 0; + } + + case LWS_CALLBACK_ESTABLISHED: { + lwsl_wsi_user(wsi, "Connection established"); + + struct cloud_per_session_data* pss = (struct cloud_per_session_data*)user; + pss->wsi = wsi; + + /* + * The largest single legal message between client and server will have a maximum length + * variable name and maximum length value. Add padding to account for JSON, LWS_PRE, etc. + */ + size_t largest_single_update = MAX_VARIABLE_NAME_LENGTH + MAX_VARIABLE_VALUE_LENGTH + 100; + resizable_buffer_init(&pss->rx_buffer, largest_single_update); + resizable_buffer_init(&pss->tx_buffer, largest_single_update); + + pss->room = NULL; + memset(&pss->variable_sequence_numbers, 0, sizeof(int) * MAX_ROOM_VARIABLES); + + return 0; + } + + case LWS_CALLBACK_CLOSED: { + lwsl_wsi_user(wsi, "Connection closed"); + + struct cloud_per_session_data* pss = (struct cloud_per_session_data*)user; + resizable_buffer_free(&pss->rx_buffer); + resizable_buffer_free(&pss->tx_buffer); + if (pss->room) { + room_remove_connection(pss->room, pss); + } + + return 0; + } + + case LWS_CALLBACK_SERVER_WRITEABLE: { + struct cloud_per_session_data* pss = (struct cloud_per_session_data*)user; + + /* Ignore WRITEABLE callbacks generated by LWS */ + if (!pss->tx_due) { + lwsl_wsi_user(wsi, "Ignoring WRITEABLE"); + return 0; + } + pss->tx_due = false; + + resizable_buffer_clear(&pss->tx_buffer); + resizable_buffer_push_uninit(&pss->tx_buffer, LWS_PRE); + + /* + * Updated in the loop when a variable is successfully written to tx_buffer + * Includes LWS_PRE + */ + size_t truncate_to = 0; + enum resizable_buffer_error buffer_result; + + for (size_t i = 0; i < pss->room->variables_len; i++) { + struct cloud_variable* variable = &pss->room->variables[i]; + int our_sequence_number = pss->variable_sequence_numbers[i]; + int latest_sequence_number = variable->sequence_number; + + if (our_sequence_number != latest_sequence_number) { + lwsl_wsi_user(wsi, "Variable %lu out of date %d != %d", i, our_sequence_number, latest_sequence_number); + + static const char* prefix = "{\"method\":\"set\",\"name\":\""; + static const char* middle = "\",\"value\":"; + static const char* postfix = "}"; + static const char* newline = "\n"; + + if (truncate_to > 0) { + buffer_result = resizable_buffer_push(&pss->tx_buffer, (unsigned char*)newline, strlen(newline)); + if (buffer_result != resizable_buffer_ok) { + lwsl_wsi_user(wsi, "Failed to write newline: %d", buffer_result); + break; + } + } + + buffer_result = resizable_buffer_push(&pss->tx_buffer, (unsigned char*)prefix, strlen(prefix)); + if (buffer_result != resizable_buffer_ok) { + lwsl_wsi_user(wsi, "Failed to write prefix: %d", buffer_result); + break; + } + + buffer_result = resizable_buffer_push(&pss->tx_buffer, variable->name, variable->name_len); + if (buffer_result != resizable_buffer_ok) { + lwsl_wsi_user(wsi, "Failed to write name: %d", buffer_result); + break; + } + + buffer_result = resizable_buffer_push(&pss->tx_buffer, (unsigned char*)middle, strlen(middle)); + if (buffer_result != resizable_buffer_ok) { + lwsl_wsi_user(wsi, "Failed to write middle: %d", buffer_result); + break; + } + + buffer_result = resizable_buffer_push(&pss->tx_buffer, variable->value_buffer.buffer, variable->value_buffer.length); + if (buffer_result != resizable_buffer_ok) { + lwsl_wsi_user(wsi, "Failed to write value: %d", buffer_result); + break; + } + + buffer_result = resizable_buffer_push(&pss->tx_buffer, (unsigned char*)postfix, strlen(postfix)); + if (buffer_result != resizable_buffer_ok) { + lwsl_wsi_user(wsi, "Failed to write postfix: %d", buffer_result); + break; + } + + pss->variable_sequence_numbers[i] = latest_sequence_number; + truncate_to = pss->tx_buffer.length; + } + } + + if (truncate_to > 0) { + resizable_buffer_truncate(&pss->tx_buffer, truncate_to); + lws_write(wsi, pss->tx_buffer.buffer + LWS_PRE, pss->tx_buffer.length - LWS_PRE, LWS_WRITE_TEXT); + + /* + * If we successfully wrote at least once update but then hit an error, schedule another + * update to try again. Note that if we hit an error writing the very first variable, + * we shouldn't schedule immediately as that would make a busy loop. + */ + if (buffer_result != resizable_buffer_ok) { + lwsl_wsi_user(wsi, "Scheduling another WRITEABLE callback for leftover variables"); + pss->tx_due = true; + lws_callback_on_writable(wsi); + } + } + + return 0; + } + + case LWS_CALLBACK_RECEIVE: { + struct cloud_per_vhost_data* vhd = (struct cloud_per_vhost_data*)lws_protocol_vh_priv_get( + lws_get_vhost(wsi), + lws_get_protocol(wsi)); + struct cloud_per_session_data* pss = (struct cloud_per_session_data*)user; + + if (lws_is_final_fragment(wsi)) { + lwsl_wsi_user(wsi, "Received %lu bytes (final)", len); + + if (pss->rx_buffer.length == 0) { + if (!handle_full_rx(vhd, pss, in, len)) { + lwsl_wsi_user(wsi, "RX handle w/o partial failed"); + return -1; + } + } else { + enum resizable_buffer_error buffer_result = resizable_buffer_push(&pss->rx_buffer, in, len); + if (buffer_result != resizable_buffer_ok) { + resizable_buffer_clear(&pss->rx_buffer); + lwsl_wsi_user(wsi, "Final partial buffer push failed: %d", buffer_result); + return -1; + } + + if (!handle_full_rx(vhd, pss, pss->rx_buffer.buffer, pss->rx_buffer.length)) { + resizable_buffer_clear(&pss->rx_buffer); + lwsl_wsi_user(wsi, "RX handle w/ partial failed"); + return -1; + } + + resizable_buffer_clear(&pss->rx_buffer); + } + } else { + lwsl_wsi_user(wsi, "Received %lu bytes (partial)", len); + + enum resizable_buffer_error buffer_result = resizable_buffer_push(&pss->rx_buffer, in, len); + if (buffer_result != resizable_buffer_ok) { + lwsl_wsi_user(wsi, "Partial buffer push failed: %d", buffer_result); + return -1; + } + } + + return 0; + } + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} diff --git a/droplet/droplet/protocol_cloud.h b/droplet/droplet/protocol_cloud.h new file mode 100644 index 00000000..a5e03e6e --- /dev/null +++ b/droplet/droplet/protocol_cloud.h @@ -0,0 +1,69 @@ +#pragma once + +#include "resizable_buffer.h" +#include +#include + +#define MAX_ROOMS 2048 +#define MAX_ROOM_NAME_LENGTH 128 +#define MAX_ROOM_CONNECTIONS 128 +#define MAX_ROOM_VARIABLES 128 +#define MAX_VARIABLE_NAME_LENGTH 128 +#define MAX_VARIABLE_VALUE_LENGTH 100000 + +struct cloud_per_session_data { + struct lws* wsi; + + /* Buffer for partially received messages */ + struct resizable_buffer rx_buffer; + + /* Whether we have requested a WRITEABLE callback from LWS */ + bool tx_due; + + /* Buffer for partially sent messages */ + struct resizable_buffer tx_buffer; + + /* The room connected to, only use if status is status_active */ + struct cloud_room* room; + + int variable_sequence_numbers[MAX_ROOM_VARIABLES]; +}; + +struct cloud_variable { + /* Incremented each time the variable is modified. */ + int sequence_number; + + /* Not null terminated */ + unsigned char name[MAX_VARIABLE_NAME_LENGTH]; + size_t name_len; + + struct resizable_buffer value_buffer; +}; + +struct cloud_room { + bool active; + + /* Not null terminated */ + unsigned char* name[MAX_ROOM_NAME_LENGTH]; + size_t name_len; + + struct cloud_variable variables[MAX_ROOM_VARIABLES]; + size_t variables_len; + + struct cloud_per_session_data* connections[MAX_ROOM_CONNECTIONS]; +}; + +struct cloud_per_vhost_data { + struct cloud_room rooms[MAX_ROOMS]; +}; + +int callback_cloud(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len); + +#define LWS_PLUGIN_PROTOCOL_CLOUD \ + { \ + "cloud", \ + callback_cloud, \ + sizeof(struct cloud_per_session_data), \ + 4096, /* TODO: tune this number */ \ + 0, NULL, 0 \ + } diff --git a/droplet/droplet/resizable_buffer.c b/droplet/droplet/resizable_buffer.c new file mode 100644 index 00000000..c294ba00 --- /dev/null +++ b/droplet/droplet/resizable_buffer.c @@ -0,0 +1,111 @@ +#include "resizable_buffer.h" +#include +#include + +static size_t min(const size_t a, const size_t b) +{ + if (a > b) { + return b; + } + return a; +} + +void resizable_buffer_init(struct resizable_buffer* rb, size_t max_capacity) +{ + rb->length = 0; + rb->capacity = 0; + rb->max_capacity = max_capacity; + rb->buffer = NULL; +} + +enum resizable_buffer_error resizable_buffer_push_uninit(struct resizable_buffer* rb, const size_t len) +{ + if (len == 0) { + return resizable_buffer_ok; + } + + if (len > rb->max_capacity) { + return resizable_buffer_full; + } + + if (!rb->buffer) { + // We assume there will be more data + size_t new_capacity = min(rb->max_capacity, len * 2); + + rb->buffer = malloc(new_capacity); + if (!rb->buffer) { + return resizable_buffer_oom; + } + rb->capacity = new_capacity; + } + + size_t needed_capacity = rb->length + len; + if (needed_capacity > rb->max_capacity) { + return resizable_buffer_full; + } + + if (needed_capacity > rb->capacity) { + // Grow exponentially to reduce avoid constantly reallocating + size_t new_capacity = rb->capacity; + while (new_capacity < needed_capacity) { + new_capacity *= 2; + } + new_capacity = min(new_capacity, rb->max_capacity); + + unsigned char* new_buffer = realloc(rb->buffer, new_capacity); + if (new_buffer == NULL) { + return resizable_buffer_oom; + } + + rb->buffer = new_buffer; + rb->capacity = new_capacity; + } + + rb->length += len; + return resizable_buffer_ok; +} + +enum resizable_buffer_error resizable_buffer_push(struct resizable_buffer* rb, const unsigned char* in, const size_t len) +{ + enum resizable_buffer_error error = resizable_buffer_push_uninit(rb, len); + if (error != resizable_buffer_ok) { + return error; + } + + memcpy(rb->buffer + rb->length - len, in, len); + return resizable_buffer_ok; +} + +void resizable_buffer_clear(struct resizable_buffer* rb) +{ + rb->length = 0; +} + +void resizable_buffer_truncate(struct resizable_buffer* rb, size_t len) +{ + rb->length = len; +} + +void resizable_buffer_debug_print(const struct resizable_buffer* rb) +{ + printf("length: %lu capacity: %lu max_capacity: %lu buffer: %p\n", + rb->length, rb->capacity, rb->max_capacity, rb->buffer); + + if (rb->buffer && rb->length != 0) { + for (size_t i = 0; i < rb->length; i++) { + char* it = (char*)(rb->buffer) + i; + printf("char: %c hex: %02x\n", *it, *it); + } + } +} + +void resizable_buffer_free(struct resizable_buffer* rb) +{ + if (rb->buffer) { + free(rb->buffer); + rb->buffer = NULL; + rb->buffer = 0; + rb->length = 0; + rb->capacity = 0; + } +} diff --git a/droplet/droplet/resizable_buffer.h b/droplet/droplet/resizable_buffer.h new file mode 100644 index 00000000..6e2bc65d --- /dev/null +++ b/droplet/droplet/resizable_buffer.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +enum resizable_buffer_error { + resizable_buffer_ok = 0, + resizable_buffer_full = 1, + resizable_buffer_oom = 2 +}; + +struct resizable_buffer { + /* Index in buffer after the end of the data */ + size_t length; + + /* Size of the buffer */ + size_t capacity; + + /* Maximum capacity of the buffer */ + size_t max_capacity; + + /* malloc()'d, may be NULL if no data */ + unsigned char* buffer; +}; + +/* + * Initialize empty resizable_buffer with given max capacity. + */ +void resizable_buffer_init(struct resizable_buffer* rb, size_t max_capacity); + +/* + * Expand the data's length without initializing data + */ +enum resizable_buffer_error resizable_buffer_push_uninit(struct resizable_buffer* rb, const size_t len); + +/* + * Copy data to the back of the buffer + */ +enum resizable_buffer_error resizable_buffer_push(struct resizable_buffer* rb, const unsigned char* in, const size_t len); + +/* + * Set's a buffer's length to zero. Does not zero or free() the backing buffer. + */ +void resizable_buffer_clear(struct resizable_buffer* rb); + +/* + * Shrink a buffer to a specific size. Does not zero or free() now-unused memory. + */ +void resizable_buffer_truncate(struct resizable_buffer* rb, size_t len); + +/* + * Print debug information about a buffer + */ +void resizable_buffer_debug_print(const struct resizable_buffer* rb); + +/* + * Free the memory used by the resizable_buffer, but does not free(rb) + * You can later call resizable_buffer_init on the same rb and it will work + */ +void resizable_buffer_free(struct resizable_buffer* rb); diff --git a/droplet/droplet/resizable_buffer_test.c b/droplet/droplet/resizable_buffer_test.c new file mode 100644 index 00000000..34e6c47c --- /dev/null +++ b/droplet/droplet/resizable_buffer_test.c @@ -0,0 +1,24 @@ +#include "resizable_buffer.h" +#include + +int main() +{ + struct resizable_buffer rb; + resizable_buffer_init(&rb, 1024); + resizable_buffer_debug_print(&rb); + + char* str1 = "Hello, "; + resizable_buffer_push(&rb, (unsigned char*)str1, strlen(str1) + 1); + resizable_buffer_debug_print(&rb); + + char* str2 = "world!"; + resizable_buffer_push(&rb, (unsigned char*)str2, strlen(str2) + 1); + resizable_buffer_debug_print(&rb); + + resizable_buffer_clear(&rb); + resizable_buffer_debug_print(&rb); + + resizable_buffer_free(&rb); + + return 0; +} diff --git a/droplet/jsmn b/droplet/jsmn new file mode 160000 index 00000000..25647e69 --- /dev/null +++ b/droplet/jsmn @@ -0,0 +1 @@ +Subproject commit 25647e692c7906b96ffd2b05ca54c097948e879c diff --git a/droplet/libwebsockets b/droplet/libwebsockets new file mode 160000 index 00000000..8674bf15 --- /dev/null +++ b/droplet/libwebsockets @@ -0,0 +1 @@ +Subproject commit 8674bf1585c0196f071eb6f0ae2184c9ca053301 diff --git a/droplet/playground/index.html b/droplet/playground/index.html new file mode 100644 index 00000000..66c2aa74 --- /dev/null +++ b/droplet/playground/index.html @@ -0,0 +1,85 @@ + + + + + + + + +
+ + + + + + diff --git a/droplet/playground/stress.html b/droplet/playground/stress.html new file mode 100644 index 00000000..bf65e11c --- /dev/null +++ b/droplet/playground/stress.html @@ -0,0 +1,178 @@ + + + + + + + +
+ +
+ +
+ + +
+ + + + + + From 328c4c9c94d21878ef4b99a8fb9dcc798715152b Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 14:08:18 -0500 Subject: [PATCH 02/17] droplet: enable compiler hardening --- droplet/CMakeLists.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/droplet/CMakeLists.txt b/droplet/CMakeLists.txt index c0f6a967..042787bc 100644 --- a/droplet/CMakeLists.txt +++ b/droplet/CMakeLists.txt @@ -4,8 +4,9 @@ project(droplet LANGUAGES C) add_subdirectory(libwebsockets) -set(CMAKE_C_FLAGS_DEBUG "-g -Wall -pedantic ${CMAKE_C_FLAGS_DEBUG}") -set(CMAKE_C_FLAGS_RELEASE "-g -Wall -pedantic -O3 -march=native ${CMAKE_C_FLAGS_RELEASE}") +# we want -fhardened but we are using too old GCC +set(CMAKE_C_FLAGS_DEBUG "-g -Wall -pedantic -fsanitize=address -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -ftrivial-auto-var-init=zero -fPIE -pie -Wl,-z,relro,-z,now -fstack-protector-strong -fstack-clash-protection -fcf-protection=full ${CMAKE_C_FLAGS_DEBUG}") +set(CMAKE_C_FLAGS_RELEASE "-g -Wall -pedantic -O3 -march=native -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -ftrivial-auto-var-init=zero -fPIE -pie -Wl,-z,relro,-z,now -fstack-protector-strong -fstack-clash-protection -fcf-protection=full ${CMAKE_C_FLAGS_RELEASE}") add_executable( droplet From ecb5dd7e3cf67ba21f14dac02bfe4025a6de3f48 Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 14:17:03 -0500 Subject: [PATCH 03/17] droplet: rename source folder to src --- droplet/CMakeLists.txt | 10 +++++----- droplet/{droplet => src}/.clang-format | 0 droplet/{droplet => src}/droplet.c | 0 droplet/{droplet => src}/protocol_cloud.c | 0 droplet/{droplet => src}/protocol_cloud.h | 0 droplet/{droplet => src}/resizable_buffer.c | 0 droplet/{droplet => src}/resizable_buffer.h | 0 droplet/{droplet => src}/resizable_buffer_test.c | 0 8 files changed, 5 insertions(+), 5 deletions(-) rename droplet/{droplet => src}/.clang-format (100%) rename droplet/{droplet => src}/droplet.c (100%) rename droplet/{droplet => src}/protocol_cloud.c (100%) rename droplet/{droplet => src}/protocol_cloud.h (100%) rename droplet/{droplet => src}/resizable_buffer.c (100%) rename droplet/{droplet => src}/resizable_buffer.h (100%) rename droplet/{droplet => src}/resizable_buffer_test.c (100%) diff --git a/droplet/CMakeLists.txt b/droplet/CMakeLists.txt index 042787bc..a38054d2 100644 --- a/droplet/CMakeLists.txt +++ b/droplet/CMakeLists.txt @@ -10,9 +10,9 @@ set(CMAKE_C_FLAGS_RELEASE "-g -Wall -pedantic -O3 -march=native -D_FORTIFY_SOURC add_executable( droplet - droplet/droplet.c - droplet/protocol_cloud.c - droplet/resizable_buffer.c + src/droplet.c + src/protocol_cloud.c + src/resizable_buffer.c ) target_include_directories( droplet PRIVATE @@ -24,6 +24,6 @@ target_link_libraries(droplet websockets) add_executable( resizable_buffer_test - droplet/resizable_buffer_test.c - droplet/resizable_buffer.c + src/resizable_buffer_test.c + src/resizable_buffer.c ) diff --git a/droplet/droplet/.clang-format b/droplet/src/.clang-format similarity index 100% rename from droplet/droplet/.clang-format rename to droplet/src/.clang-format diff --git a/droplet/droplet/droplet.c b/droplet/src/droplet.c similarity index 100% rename from droplet/droplet/droplet.c rename to droplet/src/droplet.c diff --git a/droplet/droplet/protocol_cloud.c b/droplet/src/protocol_cloud.c similarity index 100% rename from droplet/droplet/protocol_cloud.c rename to droplet/src/protocol_cloud.c diff --git a/droplet/droplet/protocol_cloud.h b/droplet/src/protocol_cloud.h similarity index 100% rename from droplet/droplet/protocol_cloud.h rename to droplet/src/protocol_cloud.h diff --git a/droplet/droplet/resizable_buffer.c b/droplet/src/resizable_buffer.c similarity index 100% rename from droplet/droplet/resizable_buffer.c rename to droplet/src/resizable_buffer.c diff --git a/droplet/droplet/resizable_buffer.h b/droplet/src/resizable_buffer.h similarity index 100% rename from droplet/droplet/resizable_buffer.h rename to droplet/src/resizable_buffer.h diff --git a/droplet/droplet/resizable_buffer_test.c b/droplet/src/resizable_buffer_test.c similarity index 100% rename from droplet/droplet/resizable_buffer_test.c rename to droplet/src/resizable_buffer_test.c From 9365f3adb81b5e54f1c8929aa104a1f0150805df Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 14:33:16 -0500 Subject: [PATCH 04/17] droplet: add CLI argument for mount origin --- droplet/src/droplet.c | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/droplet/src/droplet.c b/droplet/src/droplet.c index a990b956..6b612acb 100644 --- a/droplet/src/droplet.c +++ b/droplet/src/droplet.c @@ -8,27 +8,6 @@ static struct lws_protocols protocols[] = { LWS_PROTOCOL_LIST_TERM }; -static const struct lws_http_mount mount = { - .mount_next = NULL, - .mountpoint = "/", - .origin = "./playground", - .def = "index.html", - .protocol = NULL, - .cgienv = NULL, - .extra_mimetypes = NULL, - .interpret = NULL, - .cgi_timeout = 0, - .cache_max_age = 0, - .auth_mask = 0, - .cache_reusable = 0, - .cache_revalidate = 0, - .cache_intermediaries = 0, - .cache_no = 0, - .origin_protocol = LWSMPRO_FILE, - .mountpoint_len = 1, - .basic_auth_login_file = NULL, -}; - static bool interrupted; static void sigint_handler(int sig) @@ -45,6 +24,15 @@ static int get_port(int argc, const char** argv) return 9082; } +static const char* get_mount_origin(int argc, const char** argv) +{ + const char* w = lws_cmdline_option(argc, argv, "-w"); + if (w) { + return w; + } + return "./playground"; +} + int main(int argc, const char** argv) { signal(SIGINT, sigint_handler); @@ -55,15 +43,24 @@ int main(int argc, const char** argv) lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); #endif + struct lws_http_mount mount = { 0 }; + mount.mountpoint = "/"; + mount.origin = get_mount_origin(argc, argv); + mount.def = "index.html"; + mount.origin_protocol = LWSMPRO_FILE; + mount.mountpoint_len = 1; + struct lws_context_creation_info info = { 0 }; info.port = get_port(argc, argv); info.mounts = &mount; info.protocols = protocols; - lwsl_user("Starting on http://localhost:%d | ws://localhost:%d", info.port, info.port); + lwsl_user("Starting on http://localhost:%d | ws://localhost:%d\n", info.port, info.port); + lwsl_user("Serving HTTP requests from %s\n", mount.origin); + struct lws_context* context = lws_create_context(&info); if (!context) { - lwsl_err("lws init failed\n"); + lwsl_err("lws_create_context failed\n"); return 1; } From fa6d6356be627e2c181745425f955b864359f500 Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 15:19:43 -0500 Subject: [PATCH 05/17] droplet: style nits --- droplet/playground/index.html | 80 ++++++++++------------------------ droplet/playground/stress.html | 14 +++--- droplet/src/resizable_buffer.c | 4 +- 3 files changed, 33 insertions(+), 65 deletions(-) diff --git a/droplet/playground/index.html b/droplet/playground/index.html index 66c2aa74..cff6a311 100644 --- a/droplet/playground/index.html +++ b/droplet/playground/index.html @@ -13,73 +13,41 @@ -
- - +
+ + diff --git a/droplet/playground/stress.html b/droplet/playground/stress.html index bf65e11c..98abaebc 100644 --- a/droplet/playground/stress.html +++ b/droplet/playground/stress.html @@ -1,7 +1,7 @@ - + @@ -33,15 +33,15 @@ requestAnimationFrame(() => { updateQueued = false; - let text = ''; + let text = ""; if (errors.length) { - text += '--- ERRORS ---\n'; - text += errors.join('\n'); - text += '\n--- ERRORS ---\n'; + text += "--- ERRORS ---\n"; + text += errors.join("\n"); + text += "\n--- ERRORS ---\n"; } - text += '\topen\ttxMess\ttxBytes\trxMess\trxBytes\n'; + text += "\topen\ttxMess\ttxBytes\trxMess\trxBytes\n"; let txMessages = 0; let txBytes = 0; @@ -65,7 +65,7 @@ const r = Math.round.bind(Math); text += `avg\t\t${r(txMessages / time)}\t${r(txBytes / time)}\t${r(rxMessages / time)}\t${r(rxBytes / time)}\n`; - document.getElementById('metrics').value = text; + document.getElementById("metrics").value = text; }, 0); } diff --git a/droplet/src/resizable_buffer.c b/droplet/src/resizable_buffer.c index c294ba00..b2b69ca9 100644 --- a/droplet/src/resizable_buffer.c +++ b/droplet/src/resizable_buffer.c @@ -29,7 +29,7 @@ enum resizable_buffer_error resizable_buffer_push_uninit(struct resizable_buffer } if (!rb->buffer) { - // We assume there will be more data + /* Assume there will be more data, so over-allocate */ size_t new_capacity = min(rb->max_capacity, len * 2); rb->buffer = malloc(new_capacity); @@ -45,7 +45,7 @@ enum resizable_buffer_error resizable_buffer_push_uninit(struct resizable_buffer } if (needed_capacity > rb->capacity) { - // Grow exponentially to reduce avoid constantly reallocating + /* Grow exponentially to reduce avoid constantly reallocating */ size_t new_capacity = rb->capacity; while (new_capacity < needed_capacity) { new_capacity *= 2; From 3bb82d475016ab46c0ca553fb7b780a11e493cb8 Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 15:21:30 -0500 Subject: [PATCH 06/17] droplet: primitive username validation --- droplet/CMakeLists.txt | 7 +++++++ droplet/src/droplet.c | 3 +++ droplet/src/protocol_cloud.c | 8 +++++++- droplet/src/username.c | 34 ++++++++++++++++++++++++++++++++++ droplet/src/username.h | 15 +++++++++++++++ droplet/src/username_test.c | 24 ++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 droplet/src/username.c create mode 100644 droplet/src/username.h create mode 100644 droplet/src/username_test.c diff --git a/droplet/CMakeLists.txt b/droplet/CMakeLists.txt index a38054d2..2fe45007 100644 --- a/droplet/CMakeLists.txt +++ b/droplet/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable( src/droplet.c src/protocol_cloud.c src/resizable_buffer.c + src/username.c ) target_include_directories( droplet PRIVATE @@ -27,3 +28,9 @@ add_executable( src/resizable_buffer_test.c src/resizable_buffer.c ) + +add_executable( + username_test + src/username_test.c + src/username.c +) diff --git a/droplet/src/droplet.c b/droplet/src/droplet.c index 6b612acb..31035e1c 100644 --- a/droplet/src/droplet.c +++ b/droplet/src/droplet.c @@ -1,4 +1,5 @@ #include "protocol_cloud.h" +#include "username.h" #include #include #include @@ -37,6 +38,8 @@ int main(int argc, const char** argv) { signal(SIGINT, sigint_handler); + username_init(); + #ifndef NDEBUG lws_set_log_level(LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); #else diff --git a/droplet/src/protocol_cloud.c b/droplet/src/protocol_cloud.c index 286b2119..bf5f6389 100644 --- a/droplet/src/protocol_cloud.c +++ b/droplet/src/protocol_cloud.c @@ -1,4 +1,5 @@ #include "protocol_cloud.h" +#include "username.h" #include #include #include @@ -205,7 +206,12 @@ static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_se return false; } - /* TODO: username validation */ + const unsigned char* username_data = data + user_json->start; + size_t username_len = user_json->end - user_json->start; + if (!username_validate(username_data, username_len)) { + lwsl_wsi_user(pss->wsi, "Username is invalid"); + return false; + } const unsigned char* project_id_data = data + project_id_json->start; size_t project_id_len = project_id_json->end - project_id_json->start; diff --git a/droplet/src/username.c b/droplet/src/username.c new file mode 100644 index 00000000..effe915c --- /dev/null +++ b/droplet/src/username.c @@ -0,0 +1,34 @@ +#include "username.h" +#include + +/* Both inclusive */ +static const int MIN_LENGTH = 1; +static const int MAX_LENGTH = 20; + +static const char* ALLOWED = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-0123456789"; + +static bool lookup_table[256]; + +void username_init() +{ + size_t len = strlen(ALLOWED); + for (size_t i = 0; i < len; i++) { + lookup_table[(unsigned char)ALLOWED[i]] = true; + } +} + +bool username_validate(const unsigned char* username, size_t len) +{ + if (len < MIN_LENGTH || len > MAX_LENGTH) { + return false; + } + + for (size_t i = 0; i < len; i++) { + unsigned char ch = username[i]; + if (!lookup_table[ch]) { + return false; + } + } + + return true; +} diff --git a/droplet/src/username.h b/droplet/src/username.h new file mode 100644 index 00000000..a0937119 --- /dev/null +++ b/droplet/src/username.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +/* + * Initialize memory spaces used by the username subsystem. Call once. + */ +void username_init(); + +/* + * Check if a username is valid. Username is not null terminated. + * Returns true if valid. + */ +bool username_validate(const unsigned char* username, size_t len); diff --git a/droplet/src/username_test.c b/droplet/src/username_test.c new file mode 100644 index 00000000..1a6eb000 --- /dev/null +++ b/droplet/src/username_test.c @@ -0,0 +1,24 @@ +#include "username.h" +#include +#include + +int main(int argc, const char** argv) +{ + if (argc < 2) { + fprintf(stderr, "missing arguments\n"); + return 1; + } + + username_init(); + + /* + * Usernames from argv will be null terminated, but they do not have to be null + * terminated in general. + */ + + for (int i = 1; i < argc; i++) { + printf("%s: %d\n", argv[i], username_validate((const unsigned char*)argv[i], strlen(argv[i]))); + } + + return 0; +} From 4b79c3293ce54e4a94e3bdd54b58a929f59c5b33 Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 16:08:37 -0500 Subject: [PATCH 07/17] droplet: refuse empty user agents and scratch cookies --- droplet/src/protocol_cloud.c | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/droplet/src/protocol_cloud.c b/droplet/src/protocol_cloud.c index bf5f6389..5cc54f44 100644 --- a/droplet/src/protocol_cloud.c +++ b/droplet/src/protocol_cloud.c @@ -10,6 +10,57 @@ #define MAX_JSON_TOKENS 64 +/* + * Maximum size of buffer used to check for unsafe HTTP cookie header. + * scratchsessionsid is about 400 bytes, so we need at least that much plus + * a bit extra for safety. + */ +#define MAX_COOKIE_LEN 512 + +enum invalid_headers { + headers_valid, + invalid_user_agent, + invalid_cookie +}; + +static enum invalid_headers check_headers(struct lws* wsi) +{ + /* Require a non-empty User-Agent */ + int user_agent_len = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_USER_AGENT); + if (user_agent_len == 0) { + static char* reason = "Invalid User-Agent"; + lws_close_reason(wsi, 4006, (unsigned char*)reason, strlen(reason)); + return invalid_user_agent; + } + + /* + * We are aware of at least one cloud variable library that sent Scratch + * session tokens to us for no reason. This is an unreasonable risk, so + * refuse to allow the connection. + * + * This isn't a spec-compliant cookie parser by any means, but it doesn't + * need to be. We're just trying to make it a bit harder to do the wrong + * thing. + */ + static const char* scratchsessionsid_header = "scratchsessionsid="; + int cookie_len = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_COOKIE); + /* + * Length returned by LWS don't include the null terminator that will be + * added by the copy methods. + */ + if (cookie_len >= strlen(scratchsessionsid_header) && cookie_len < MAX_COOKIE_LEN - 1) { + static char temp[MAX_COOKIE_LEN]; + int result = lws_hdr_copy(wsi, temp, MAX_COOKIE_LEN, WSI_TOKEN_HTTP_COOKIE); + if (result > 0 && memcmp(temp, scratchsessionsid_header, strlen(scratchsessionsid_header)) == 0) { + static char* reason = "Stop giving us your Scratch session token"; + lws_close_reason(wsi, 4005, (unsigned char*)reason, strlen(reason)); + return invalid_cookie; + } + } + + return headers_valid; +} + static const jsmntok_t* json_get_key(const unsigned char* data, const jsmntok_t* tokens, int num_tokens, const char* name) { if (num_tokens < 1) { @@ -334,6 +385,10 @@ int callback_cloud(struct lws* wsi, enum lws_callback_reasons reason, void* user case LWS_CALLBACK_ESTABLISHED: { lwsl_wsi_user(wsi, "Connection established"); + if (check_headers(wsi) != headers_valid) { + return -1; + } + struct cloud_per_session_data* pss = (struct cloud_per_session_data*)user; pss->wsi = wsi; From 2201e5e2c5ee60b075b3d4f1a389aee049230800 Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 16:38:51 -0500 Subject: [PATCH 08/17] droplet: improve close codes --- droplet/src/protocol_cloud.c | 69 +++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/droplet/src/protocol_cloud.c b/droplet/src/protocol_cloud.c index 5cc54f44..8415cdd5 100644 --- a/droplet/src/protocol_cloud.c +++ b/droplet/src/protocol_cloud.c @@ -17,20 +17,28 @@ */ #define MAX_COOKIE_LEN 512 -enum invalid_headers { - headers_valid, - invalid_user_agent, - invalid_cookie -}; - -static enum invalid_headers check_headers(struct lws* wsi) +/* See protocol.md */ +#define CLOSED_GENERIC 4000 +#define CLOSED_BAD_USERNAME 4002 +#define CLOSED_OVERLOADED 4003 +#define CLOSED_PROJECT_DISABLED 4004 +#define CLOSED_FOR_SECURITY 4005 +#define CLOSED_IDENTIFY_YOURSELF 4006 + +#define CLOSE_WITH_REASON(wsi, code, reason) \ + do { \ + lwsl_wsi_user(wsi, reason); \ + lws_close_reason(wsi, code, (unsigned char*)reason, strlen(reason)); \ + } while (0); + +/* Returns 0 on valid, -1 otherwise */ +static int check_headers(struct lws* wsi) { /* Require a non-empty User-Agent */ int user_agent_len = lws_hdr_total_length(wsi, WSI_TOKEN_HTTP_USER_AGENT); if (user_agent_len == 0) { - static char* reason = "Invalid User-Agent"; - lws_close_reason(wsi, 4006, (unsigned char*)reason, strlen(reason)); - return invalid_user_agent; + CLOSE_WITH_REASON(wsi, CLOSED_BAD_USERNAME, "Provide a valid User-Agent"); + return -1; } /* @@ -52,13 +60,12 @@ static enum invalid_headers check_headers(struct lws* wsi) static char temp[MAX_COOKIE_LEN]; int result = lws_hdr_copy(wsi, temp, MAX_COOKIE_LEN, WSI_TOKEN_HTTP_COOKIE); if (result > 0 && memcmp(temp, scratchsessionsid_header, strlen(scratchsessionsid_header)) == 0) { - static char* reason = "Stop giving us your Scratch session token"; - lws_close_reason(wsi, 4005, (unsigned char*)reason, strlen(reason)); - return invalid_cookie; + CLOSE_WITH_REASON(wsi, CLOSED_FOR_SECURITY, "Stop including Scratch cookies"); + return -1; } } - return headers_valid; + return 0; } static const jsmntok_t* json_get_key(const unsigned char* data, const jsmntok_t* tokens, int num_tokens, const char* name) @@ -221,7 +228,7 @@ static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_se /* char* and unsigned char* are the same in memory */ int num_tokens = jsmn_parse(&parser, (char*)data, len, tokens, MAX_JSON_TOKENS); if (num_tokens < 0) { - lwsl_wsi_user(pss->wsi, "Invalid JSON: %d", num_tokens); + CLOSE_WITH_REASON(pss->wsi, CLOSED_GENERIC, "Invalid JSON"); return false; } @@ -229,7 +236,7 @@ static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_se const jsmntok_t* method_json = json_get_key(data, tokens, num_tokens, "method"); if (method_json == NULL || method_json->type != JSMN_STRING) { - lwsl_wsi_user(pss->wsi, "method missing or not a string"); + CLOSE_WITH_REASON(pss->wsi, CLOSED_GENERIC, "invalid method"); return false; } @@ -241,26 +248,26 @@ static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_se static const char* handshake = "handshake"; if (method_len != strlen(handshake) || memcmp(method_data, handshake, strlen(handshake)) != 0) { - lwsl_wsi_user(pss->wsi, "method was not handshake"); + CLOSE_WITH_REASON(pss->wsi, CLOSED_GENERIC, "invalid method"); return false; } const jsmntok_t* user_json = json_get_key(data, tokens, num_tokens, "user"); if (user_json == NULL || user_json->type != JSMN_STRING) { - lwsl_wsi_user(pss->wsi, "handshake user missing or not a string"); + CLOSE_WITH_REASON(pss->wsi, CLOSED_GENERIC, "invalid user"); return false; } const jsmntok_t* project_id_json = json_get_key(data, tokens, num_tokens, "project_id"); if (project_id_json == NULL || project_id_json->type != JSMN_STRING) { - lwsl_wsi_user(pss->wsi, "handshake project_id missing or not a string"); + CLOSE_WITH_REASON(pss->wsi, CLOSED_PROJECT_DISABLED, "invalid project_id"); return false; } const unsigned char* username_data = data + user_json->start; size_t username_len = user_json->end - user_json->start; if (!username_validate(username_data, username_len)) { - lwsl_wsi_user(pss->wsi, "Username is invalid"); + CLOSE_WITH_REASON(pss->wsi, CLOSED_BAD_USERNAME, "Invalid username"); return false; } @@ -268,12 +275,12 @@ static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_se size_t project_id_len = project_id_json->end - project_id_json->start; struct cloud_room* room = room_get_or_create(vhd, project_id_data, project_id_len); if (!room) { - lwsl_wsi_user(pss->wsi, "Failed to find or create room"); + CLOSE_WITH_REASON(pss->wsi, CLOSED_OVERLOADED, "invalid room"); return false; } if (!room_add_connection(room, pss)) { - lwsl_wsi_user(pss->wsi, "Failed to add to room"); + CLOSE_WITH_REASON(pss->wsi, CLOSED_OVERLOADED, "room is full"); return false; } @@ -287,22 +294,28 @@ static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_se return true; } + /* + * Once we're handshaked, any errors handling the messages are not that critical, + * so we can keep the connection open. For example if someone sends a variable that's + * a bit too big, we don't need to completely destroy the connection. + */ + static const char* set = "set"; if (method_len != strlen(set) || memcmp(method_data, set, strlen(set)) != 0) { lwsl_wsi_user(pss->wsi, "method was not set"); - return false; + return true; } const jsmntok_t* name_json = json_get_key(data, tokens, num_tokens, "name"); if (name_json == NULL || name_json->type != JSMN_STRING) { lwsl_wsi_user(pss->wsi, "name missing or not a string"); - return false; + return true; } const jsmntok_t* value_json = json_get_key(data, tokens, num_tokens, "value"); if (value_json == NULL || (value_json->type != JSMN_STRING && value_json->type != JSMN_PRIMITIVE)) { lwsl_wsi_user(pss->wsi, "value missing or not a string or primitive"); - return false; + return true; } const unsigned char* name_data = data + name_json->start; @@ -311,7 +324,7 @@ static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_se int variable_idx = room_get_or_create_variable_idx(pss->room, name_data, name_len); if (variable_idx < 0) { lwsl_wsi_user(pss->wsi, "Could not find or create variable: %d", variable_idx); - return false; + return true; } struct cloud_variable* variable = &pss->room->variables[variable_idx]; @@ -322,7 +335,7 @@ static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_se enum resizable_buffer_error buffer_result = resizable_buffer_push(&variable->value_buffer, value_data, value_len); if (buffer_result != resizable_buffer_ok) { lwsl_wsi_user(pss->wsi, "Variable buffer push failed: %d", buffer_result); - return false; + return true; } variable->sequence_number++; @@ -385,7 +398,7 @@ int callback_cloud(struct lws* wsi, enum lws_callback_reasons reason, void* user case LWS_CALLBACK_ESTABLISHED: { lwsl_wsi_user(wsi, "Connection established"); - if (check_headers(wsi) != headers_valid) { + if (check_headers(wsi) != 0) { return -1; } From 8b842f9055a62bfe5cf7ae445e5c498130166107 Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 19:00:19 -0500 Subject: [PATCH 09/17] droplet: fix string handling --- droplet/src/protocol_cloud.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/droplet/src/protocol_cloud.c b/droplet/src/protocol_cloud.c index 8415cdd5..3da0f979 100644 --- a/droplet/src/protocol_cloud.c +++ b/droplet/src/protocol_cloud.c @@ -327,10 +327,18 @@ static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_se return true; } - struct cloud_variable* variable = &pss->room->variables[variable_idx]; - const unsigned char* value_data = data + value_json->start; - size_t value_len = value_json->end - value_json->start; + /* For strings, we include the surrounding quotes in the internal value */ + const unsigned char* value_data; + size_t value_len; + if (value_json->type == JSMN_STRING) { + value_data = data + value_json->start - 1; + value_len = value_json->end - value_json->start + 2; + } else { + value_data = data + value_json->start; + value_len = value_json->end - value_json->start; + } + struct cloud_variable* variable = &pss->room->variables[variable_idx]; resizable_buffer_clear(&variable->value_buffer); enum resizable_buffer_error buffer_result = resizable_buffer_push(&variable->value_buffer, value_data, value_len); if (buffer_result != resizable_buffer_ok) { From bf29c11b2b56dffdfd853836aed39091c68892bf Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 20:40:50 -0500 Subject: [PATCH 10/17] droplet: don't request writable callback if already requested --- droplet/src/protocol_cloud.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/droplet/src/protocol_cloud.c b/droplet/src/protocol_cloud.c index 3da0f979..71fe0f99 100644 --- a/droplet/src/protocol_cloud.c +++ b/droplet/src/protocol_cloud.c @@ -353,7 +353,7 @@ static bool handle_full_rx(struct cloud_per_vhost_data* vhd, struct cloud_per_se for (size_t i = 0; i < MAX_ROOM_CONNECTIONS; i++) { struct cloud_per_session_data* other_pss = pss->room->connections[i]; - if (other_pss != NULL && other_pss != pss) { + if (other_pss != NULL && other_pss != pss && !other_pss->tx_due) { other_pss->tx_due = true; lws_callback_on_writable(other_pss->wsi); } From 698cbcf5e777b2ceec0c0fcb2fb9aba1c1ff7c7f Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 21:11:07 -0500 Subject: [PATCH 11/17] droplet: allow listening on unix socket --- droplet/src/droplet.c | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/droplet/src/droplet.c b/droplet/src/droplet.c index 31035e1c..6993047f 100644 --- a/droplet/src/droplet.c +++ b/droplet/src/droplet.c @@ -16,6 +16,11 @@ static void sigint_handler(int sig) interrupted = true; } +static const char* get_unix_socket(int argc, const char** argv) +{ + return lws_cmdline_option(argc, argv, "-u"); +} + static int get_port(int argc, const char** argv) { const char* p = lws_cmdline_option(argc, argv, "-p"); @@ -54,11 +59,19 @@ int main(int argc, const char** argv) mount.mountpoint_len = 1; struct lws_context_creation_info info = { 0 }; - info.port = get_port(argc, argv); info.mounts = &mount; info.protocols = protocols; - lwsl_user("Starting on http://localhost:%d | ws://localhost:%d\n", info.port, info.port); + const char* unix_socket_path = get_unix_socket(argc, argv); + if (unix_socket_path) { + info.options |= LWS_SERVER_OPTION_UNIX_SOCK; + info.iface = unix_socket_path; + lwsl_user("Starting on unix socket %s\n", unix_socket_path); + } else { + info.port = get_port(argc, argv); + lwsl_user("Starting on http://localhost:%d | ws://localhost:%d\n", info.port, info.port); + } + lwsl_user("Serving HTTP requests from %s\n", mount.origin); struct lws_context* context = lws_create_context(&info); From f35a221d26004280e2c8d16eaeb2c74331767179 Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 21:11:38 -0500 Subject: [PATCH 12/17] droplet: disable LLL_NOTICE logging in release --- droplet/src/droplet.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/droplet/src/droplet.c b/droplet/src/droplet.c index 6993047f..161fd96d 100644 --- a/droplet/src/droplet.c +++ b/droplet/src/droplet.c @@ -48,7 +48,7 @@ int main(int argc, const char** argv) #ifndef NDEBUG lws_set_log_level(LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); #else - lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); + lws_set_log_level(LLL_ERR | LLL_WARN, NULL); #endif struct lws_http_mount mount = { 0 }; From 66cc8796db9e670b3651b8ba564c31a0c007c15a Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 21:44:05 -0500 Subject: [PATCH 13/17] droplet: enable TCP keepalives --- droplet/src/droplet.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/droplet/src/droplet.c b/droplet/src/droplet.c index 161fd96d..b68b540e 100644 --- a/droplet/src/droplet.c +++ b/droplet/src/droplet.c @@ -62,6 +62,10 @@ int main(int argc, const char** argv) info.mounts = &mount; info.protocols = protocols; + info.ka_time = 120; + info.ka_probes = 30; + info.ka_interval = 4; + const char* unix_socket_path = get_unix_socket(argc, argv); if (unix_socket_path) { info.options |= LWS_SERVER_OPTION_UNIX_SOCK; From 0cba3083da7f061232388c497e6ac89af3d5365f Mon Sep 17 00:00:00 2001 From: Muffin Date: Sun, 7 Jul 2024 23:13:09 -0500 Subject: [PATCH 14/17] droplet: allow pmd --- droplet/playground/stress.html | 2 +- droplet/src/droplet.c | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/droplet/playground/stress.html b/droplet/playground/stress.html index 98abaebc..10b581e3 100644 --- a/droplet/playground/stress.html +++ b/droplet/playground/stress.html @@ -82,7 +82,7 @@ const randInt = (lower, upper) => Math.floor(Math.random() * (upper - lower) + lower); const randStr = (size) => { - const buffer = new Uint8Array(Math.random() * 100); + const buffer = new Uint8Array(Math.random() * 10000); for (let i = 0; i < buffer.length; i++) { buffer[i] = 48 + randInt(0, 9); } diff --git a/droplet/src/droplet.c b/droplet/src/droplet.c index b68b540e..e5fff502 100644 --- a/droplet/src/droplet.c +++ b/droplet/src/droplet.c @@ -39,6 +39,15 @@ static const char* get_mount_origin(int argc, const char** argv) return "./playground"; } +#ifndef LWS_WITHOUT_EXTENSIONS +static const struct lws_extension extensions[] = { + { "permessage-deflate", + lws_extension_callback_pm_deflate, + "permessage-deflate" }, + { NULL, NULL, NULL /* terminator */ } +}; +#endif + int main(int argc, const char** argv) { signal(SIGINT, sigint_handler); @@ -46,7 +55,7 @@ int main(int argc, const char** argv) username_init(); #ifndef NDEBUG - lws_set_log_level(LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); + lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); #else lws_set_log_level(LLL_ERR | LLL_WARN, NULL); #endif @@ -62,6 +71,10 @@ int main(int argc, const char** argv) info.mounts = &mount; info.protocols = protocols; +#ifndef LWS_WITHOUT_EXTENSIONS + info.extensions = extensions; +#endif + info.ka_time = 120; info.ka_probes = 30; info.ka_interval = 4; From 444905eaf5b8e87505196608fc60c6c6a8a0d257 Mon Sep 17 00:00:00 2001 From: Muffin Date: Wed, 10 Jul 2024 22:31:38 -0500 Subject: [PATCH 15/17] droplet: move LWS defines into CMakeLists --- droplet/CMakeLists.txt | 3 +++ droplet/README.md | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/droplet/CMakeLists.txt b/droplet/CMakeLists.txt index 2fe45007..0f898d21 100644 --- a/droplet/CMakeLists.txt +++ b/droplet/CMakeLists.txt @@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.13) project(droplet LANGUAGES C) +set(LWS_WITH_SSL OFF CACHE BOOL "") +set(LWS_WITH_MINIMAL_EXAMPLES OFF CACHE BOOL "") +set(LWS_WITH_SHARED OFF CACHE BOOL "") add_subdirectory(libwebsockets) # we want -fhardened but we are using too old GCC diff --git a/droplet/README.md b/droplet/README.md index 8fd72624..2951ccd0 100644 --- a/droplet/README.md +++ b/droplet/README.md @@ -2,10 +2,14 @@ A cloud variable server in C. +Development: + ```bash -rm -rf build && mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=DEBUG -DLWS_WITH_SSL=0 -DLWS_WITH_SHARED=0 -DLWS_WITH_MINIMAL_EXAMPLES=0 .. && make -j4 +rm -rf build && mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=DEBUG .. && make -j4 ``` +Production: + ```bash --DCMAKE_BUILD_TYPE=RELEASE +rm -rf build && mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=RELEASE .. && make -j4 ``` From 8566e00823f12dfff04d17933f22c9ab0a3a03d8 Mon Sep 17 00:00:00 2001 From: Muffin Date: Wed, 10 Jul 2024 23:02:21 -0500 Subject: [PATCH 16/17] droplet: improve pmd support --- droplet/src/droplet.c | 2 +- droplet/src/protocol_cloud.c | 5 +++++ droplet/src/protocol_cloud.h | 14 +++++++------- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/droplet/src/droplet.c b/droplet/src/droplet.c index e5fff502..0b6732b5 100644 --- a/droplet/src/droplet.c +++ b/droplet/src/droplet.c @@ -55,7 +55,7 @@ int main(int argc, const char** argv) username_init(); #ifndef NDEBUG - lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); + lws_set_log_level(LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); #else lws_set_log_level(LLL_ERR | LLL_WARN, NULL); #endif diff --git a/droplet/src/protocol_cloud.c b/droplet/src/protocol_cloud.c index 71fe0f99..e8f848d1 100644 --- a/droplet/src/protocol_cloud.c +++ b/droplet/src/protocol_cloud.c @@ -406,6 +406,11 @@ int callback_cloud(struct lws* wsi, enum lws_callback_reasons reason, void* user case LWS_CALLBACK_ESTABLISHED: { lwsl_wsi_user(wsi, "Connection established"); +#ifndef LWS_WITHOUT_EXTENSIONS + /* TODO: tune with production numbers */ + lws_set_extension_option(wsi, "permessage-deflate", "rx_buf_size", "22"); +#endif + if (check_headers(wsi) != 0) { return -1; } diff --git a/droplet/src/protocol_cloud.h b/droplet/src/protocol_cloud.h index a5e03e6e..6bec908d 100644 --- a/droplet/src/protocol_cloud.h +++ b/droplet/src/protocol_cloud.h @@ -59,11 +59,11 @@ struct cloud_per_vhost_data { int callback_cloud(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len); -#define LWS_PLUGIN_PROTOCOL_CLOUD \ - { \ - "cloud", \ - callback_cloud, \ - sizeof(struct cloud_per_session_data), \ - 4096, /* TODO: tune this number */ \ - 0, NULL, 0 \ +#define LWS_PLUGIN_PROTOCOL_CLOUD \ + { \ + "cloud", \ + callback_cloud, \ + sizeof(struct cloud_per_session_data), \ + 1 << 18, /* TODO: tune with prod numbers */ \ + 0, NULL, 0 \ } From 26be50285d55bd29ff5d56725c343e3126272333 Mon Sep 17 00:00:00 2001 From: Muffin Date: Wed, 10 Jul 2024 23:04:07 -0500 Subject: [PATCH 17/17] droplet: relax compile options --- droplet/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/droplet/CMakeLists.txt b/droplet/CMakeLists.txt index 0f898d21..b61d65de 100644 --- a/droplet/CMakeLists.txt +++ b/droplet/CMakeLists.txt @@ -8,8 +8,8 @@ set(LWS_WITH_SHARED OFF CACHE BOOL "") add_subdirectory(libwebsockets) # we want -fhardened but we are using too old GCC -set(CMAKE_C_FLAGS_DEBUG "-g -Wall -pedantic -fsanitize=address -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -ftrivial-auto-var-init=zero -fPIE -pie -Wl,-z,relro,-z,now -fstack-protector-strong -fstack-clash-protection -fcf-protection=full ${CMAKE_C_FLAGS_DEBUG}") -set(CMAKE_C_FLAGS_RELEASE "-g -Wall -pedantic -O3 -march=native -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -ftrivial-auto-var-init=zero -fPIE -pie -Wl,-z,relro,-z,now -fstack-protector-strong -fstack-clash-protection -fcf-protection=full ${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_DEBUG "-g -Wall -pedantic -fsanitize=address -fPIE -pie -Wl,-z,relro,-z,now -fstack-protector-strong -fstack-clash-protection ${CMAKE_C_FLAGS_DEBUG}") +set(CMAKE_C_FLAGS_RELEASE "-g -Wall -pedantic -O3 -march=native -fPIE -pie -Wl,-z,relro,-z,now -fstack-protector-strong -fstack-clash-protection ${CMAKE_C_FLAGS_RELEASE}") add_executable( droplet