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..b61d65de --- /dev/null +++ b/droplet/CMakeLists.txt @@ -0,0 +1,39 @@ +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 +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 + src/droplet.c + src/protocol_cloud.c + src/resizable_buffer.c + src/username.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 + src/resizable_buffer_test.c + src/resizable_buffer.c +) + +add_executable( + username_test + src/username_test.c + src/username.c +) diff --git a/droplet/README.md b/droplet/README.md new file mode 100644 index 00000000..2951ccd0 --- /dev/null +++ b/droplet/README.md @@ -0,0 +1,15 @@ +# droplet + +A cloud variable server in C. + +Development: + +```bash +rm -rf build && mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=DEBUG .. && make -j4 +``` + +Production: + +```bash +rm -rf build && mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=RELEASE .. && make -j4 +``` 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..cff6a311 --- /dev/null +++ b/droplet/playground/index.html @@ -0,0 +1,53 @@ + + + + + + + + +
+ + + + + + diff --git a/droplet/playground/stress.html b/droplet/playground/stress.html new file mode 100644 index 00000000..10b581e3 --- /dev/null +++ b/droplet/playground/stress.html @@ -0,0 +1,178 @@ + + + + + + + +
+ +
+ +
+ + +
+ + + + + + diff --git a/droplet/src/.clang-format b/droplet/src/.clang-format new file mode 100644 index 00000000..c8e7b33e --- /dev/null +++ b/droplet/src/.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/src/droplet.c b/droplet/src/droplet.c new file mode 100644 index 00000000..0b6732b5 --- /dev/null +++ b/droplet/src/droplet.c @@ -0,0 +1,109 @@ +#include "protocol_cloud.h" +#include "username.h" +#include +#include +#include + +static struct lws_protocols protocols[] = { + LWS_PLUGIN_PROTOCOL_CLOUD, + LWS_PROTOCOL_LIST_TERM +}; + +static bool interrupted; + +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"); + if (p) { + return atoi(p); + } + 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"; +} + +#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); + + username_init(); + +#ifndef NDEBUG + lws_set_log_level(LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE, NULL); +#else + lws_set_log_level(LLL_ERR | LLL_WARN, 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.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; + + 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); + if (!context) { + lwsl_err("lws_create_context 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/src/protocol_cloud.c b/droplet/src/protocol_cloud.c new file mode 100644 index 00000000..e8f848d1 --- /dev/null +++ b/droplet/src/protocol_cloud.c @@ -0,0 +1,591 @@ +#include "protocol_cloud.h" +#include "username.h" +#include +#include +#include +#include + +#define JSMN_STRICT +#include + +#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 + +/* 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) { + CLOSE_WITH_REASON(wsi, CLOSED_BAD_USERNAME, "Provide a valid User-Agent"); + return -1; + } + + /* + * 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) { + CLOSE_WITH_REASON(wsi, CLOSED_FOR_SECURITY, "Stop including Scratch cookies"); + return -1; + } + } + + return 0; +} + +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) { + CLOSE_WITH_REASON(pss->wsi, CLOSED_GENERIC, "Invalid JSON"); + 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) { + CLOSE_WITH_REASON(pss->wsi, CLOSED_GENERIC, "invalid method"); + 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) { + 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) { + 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) { + 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)) { + CLOSE_WITH_REASON(pss->wsi, CLOSED_BAD_USERNAME, "Invalid username"); + 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; + struct cloud_room* room = room_get_or_create(vhd, project_id_data, project_id_len); + if (!room) { + CLOSE_WITH_REASON(pss->wsi, CLOSED_OVERLOADED, "invalid room"); + return false; + } + + if (!room_add_connection(room, pss)) { + CLOSE_WITH_REASON(pss->wsi, CLOSED_OVERLOADED, "room is full"); + 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; + } + + /* + * 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 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 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 true; + } + + 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 true; + } + + /* 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) { + lwsl_wsi_user(pss->wsi, "Variable buffer push failed: %d", buffer_result); + return true; + } + + 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) { + 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"); + +#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; + } + + 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/src/protocol_cloud.h b/droplet/src/protocol_cloud.h new file mode 100644 index 00000000..6bec908d --- /dev/null +++ b/droplet/src/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), \ + 1 << 18, /* TODO: tune with prod numbers */ \ + 0, NULL, 0 \ + } diff --git a/droplet/src/resizable_buffer.c b/droplet/src/resizable_buffer.c new file mode 100644 index 00000000..b2b69ca9 --- /dev/null +++ b/droplet/src/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) { + /* Assume there will be more data, so over-allocate */ + 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/src/resizable_buffer.h b/droplet/src/resizable_buffer.h new file mode 100644 index 00000000..6e2bc65d --- /dev/null +++ b/droplet/src/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/src/resizable_buffer_test.c b/droplet/src/resizable_buffer_test.c new file mode 100644 index 00000000..34e6c47c --- /dev/null +++ b/droplet/src/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/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; +}