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;
+}