#include "LWeb.h" #ifndef CC_BUILD_WEB #include "String.h" #include "Launcher.h" #include "Platform.h" #include "Stream.h" #include "Logger.h" #include "Window.h" #include "Options.h" #include "PackedCol.h" #include "Errors.h" #include "Utils.h" #include "Http.h" #include "LBackend.h" /*########################################################################################################################* *----------------------------------------------------------JSON-----------------------------------------------------------* *#########################################################################################################################*/ #define TOKEN_NONE 0 #define TOKEN_NUM 1 #define TOKEN_TRUE 2 #define TOKEN_FALSE 3 #define TOKEN_NULL 4 /* Consumes n characters from the JSON stream */ #define JsonContext_Consume(ctx, n) ctx->cur += n; ctx->left -= n; static const cc_string strTrue = String_FromConst("true"); static const cc_string strFalse = String_FromConst("false"); static const cc_string strNull = String_FromConst("null"); static cc_bool Json_IsWhitespace(char c) { return c == '\r' || c == '\n' || c == '\t' || c == ' '; } static cc_bool Json_IsNumber(char c) { return c == '-' || c == '.' || (c >= '0' && c <= '9'); } static cc_bool Json_ConsumeConstant(struct JsonContext* ctx, const cc_string* value) { int i; if (value->length > ctx->left) return false; for (i = 0; i < value->length; i++) { if (ctx->cur[i] != value->buffer[i]) return false; } JsonContext_Consume(ctx, value->length); return true; } static int Json_ConsumeToken(struct JsonContext* ctx) { char c; for (; ctx->left && Json_IsWhitespace(*ctx->cur); ) { JsonContext_Consume(ctx, 1); } if (!ctx->left) return TOKEN_NONE; c = *ctx->cur; if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == '"' || c == ':') { JsonContext_Consume(ctx, 1); return c; } /* number token forms part of value, don't consume it */ if (Json_IsNumber(c)) return TOKEN_NUM; if (Json_ConsumeConstant(ctx, &strTrue)) return TOKEN_TRUE; if (Json_ConsumeConstant(ctx, &strFalse)) return TOKEN_FALSE; if (Json_ConsumeConstant(ctx, &strNull)) return TOKEN_NULL; /* invalid token */ JsonContext_Consume(ctx, 1); return TOKEN_NONE; } static cc_string Json_ConsumeNumber(struct JsonContext* ctx) { int len = 0; for (; ctx->left && Json_IsNumber(*ctx->cur); len++) { JsonContext_Consume(ctx, 1); } return String_Init(ctx->cur - len, len, len); } static void Json_ConsumeString(struct JsonContext* ctx, cc_string* str) { int codepoint, h[4]; char c; str->length = 0; for (; ctx->left;) { c = *ctx->cur; JsonContext_Consume(ctx, 1); if (c == '"') return; if (c != '\\') { String_Append(str, c); continue; } /* form of \X */ if (!ctx->left) break; c = *ctx->cur; JsonContext_Consume(ctx, 1); if (c == '/' || c == '\\' || c == '"') { String_Append(str, c); continue; } if (c == 'n') { String_Append(str, '\n'); continue; } /* form of \uYYYY */ if (c != 'u' || ctx->left < 4) break; if (!PackedCol_Unhex(ctx->cur, h, 4)) break; codepoint = (h[0] << 12) | (h[1] << 8) | (h[2] << 4) | h[3]; /* don't want control characters in names/software */ /* TODO: Convert to CP437.. */ if (codepoint >= 32) String_Append(str, codepoint); JsonContext_Consume(ctx, 4); } ctx->failed = true; str->length = 0; } static cc_string Json_ConsumeValue(int token, struct JsonContext* ctx); static void Json_ConsumeObject(struct JsonContext* ctx) { char keyBuffer[STRING_SIZE]; cc_string value, oldKey = ctx->curKey; int token; ctx->depth++; ctx->OnNewObject(ctx); while (true) { token = Json_ConsumeToken(ctx); if (token == ',') continue; if (token == '}') break; if (token != '"') { ctx->failed = true; break; } String_InitArray(ctx->curKey, keyBuffer); Json_ConsumeString(ctx, &ctx->curKey); token = Json_ConsumeToken(ctx); if (token != ':') { ctx->failed = true; break; } token = Json_ConsumeToken(ctx); if (token == TOKEN_NONE) { ctx->failed = true; break; } value = Json_ConsumeValue(token, ctx); ctx->OnValue(ctx, &value); ctx->curKey = oldKey; } ctx->depth--; } static void Json_ConsumeArray(struct JsonContext* ctx) { cc_string value; int token; ctx->depth++; ctx->OnNewArray(ctx); while (true) { token = Json_ConsumeToken(ctx); if (token == ',') continue; if (token == ']') break; if (token == TOKEN_NONE) { ctx->failed = true; break; } value = Json_ConsumeValue(token, ctx); ctx->OnValue(ctx, &value); } ctx->depth--; } static cc_string Json_ConsumeValue(int token, struct JsonContext* ctx) { switch (token) { case '{': Json_ConsumeObject(ctx); break; case '[': Json_ConsumeArray(ctx); break; case '"': Json_ConsumeString(ctx, &ctx->_tmp); return ctx->_tmp; case TOKEN_NUM: return Json_ConsumeNumber(ctx); case TOKEN_TRUE: return strTrue; case TOKEN_FALSE: return strFalse; case TOKEN_NULL: break; } return String_Empty; } static void Json_NullOnNew(struct JsonContext* ctx) { } static void Json_NullOnValue(struct JsonContext* ctx, const cc_string* v) { } void Json_Init(struct JsonContext* ctx, STRING_REF char* str, int len) { ctx->cur = str; ctx->left = len; ctx->failed = false; ctx->curKey = String_Empty; ctx->depth = 0; ctx->OnNewArray = Json_NullOnNew; ctx->OnNewObject = Json_NullOnNew; ctx->OnValue = Json_NullOnValue; String_InitArray(ctx->_tmp, ctx->_tmpBuffer); } cc_bool Json_Parse(struct JsonContext* ctx) { int token; do { token = Json_ConsumeToken(ctx); Json_ConsumeValue(token, ctx); } while (token != TOKEN_NONE); return !ctx->failed; } static cc_bool Json_Handle(cc_uint8* data, cc_uint32 len, JsonOnValue onVal, JsonOnNew newArr, JsonOnNew newObj) { struct JsonContext ctx; /* NOTE: classicube.net uses \u JSON for non ASCII, no need to UTF8 convert characters here */ Json_Init(&ctx, (char*)data, len); if (onVal) ctx.OnValue = onVal; if (newArr) ctx.OnNewArray = newArr; if (newObj) ctx.OnNewObject = newObj; return Json_Parse(&ctx); } /*########################################################################################################################* *--------------------------------------------------------Web task---------------------------------------------------------* *#########################################################################################################################*/ static char servicesBuffer[FILENAME_SIZE]; static cc_string servicesServer = String_FromArray(servicesBuffer); static struct StringsBuffer ccCookies; static void LWebTask_Reset(struct LWebTask* task) { task->completed = false; task->working = true; task->success = false; } void LWebTask_Tick(struct LWebTask* task, LWebTask_ErrorCallback errorCallback) { struct HttpRequest item; if (task->completed) return; if (!Http_GetResult(task->reqID, &item)) return; task->working = false; task->completed = true; task->success = item.success; if (item.success) { task->Handle(item.data, item.size); } else if (errorCallback) { errorCallback(&item); } HttpRequest_Free(&item); } void LWebTasks_Init(void) { Options_Get(SOPT_SERVICES, &servicesServer, SERVICES_SERVER); } /*########################################################################################################################* *-------------------------------------------------------GetTokenTask------------------------------------------------------* *#########################################################################################################################*/ /* < GET /api/login/ > { > "username": null, > "authenticated": false, > "token": "f033ab37c30201f73f142449d037028d", > "errors": [] >} */ struct GetTokenTaskData GetTokenTask; static void GetTokenTask_OnValue(struct JsonContext* ctx, const cc_string* str) { if (String_CaselessEqualsConst(&ctx->curKey, "token")) { String_Copy(&GetTokenTask.token, str); } else if (String_CaselessEqualsConst(&ctx->curKey, "username")) { String_Copy(&GetTokenTask.username, str); } else if (String_CaselessEqualsConst(&ctx->curKey, "errors")) { if (str->length) GetTokenTask.error = true; } } static void GetTokenTask_Handle(cc_uint8* data, cc_uint32 len) { static cc_string err_msg = String_FromConst("Error parsing get login token response JSON"); cc_bool success = Json_Handle(data, len, GetTokenTask_OnValue, NULL, NULL); if (!success) Logger_WarnFunc(&err_msg); } void GetTokenTask_Run(void) { cc_string url; char urlBuffer[URL_MAX_SIZE]; static char tokenBuffer[STRING_SIZE]; static char userBuffer[STRING_SIZE]; if (GetTokenTask.Base.working) return; LWebTask_Reset(&GetTokenTask.Base); String_InitArray(url, urlBuffer); String_Format1(&url, "%s/login", &servicesServer); String_InitArray(GetTokenTask.token, tokenBuffer); String_InitArray(GetTokenTask.username, userBuffer); GetTokenTask.error = false; GetTokenTask.Base.Handle = GetTokenTask_Handle; GetTokenTask.Base.reqID = Http_AsyncGetDataEx(&url, 0, NULL, NULL, &ccCookies); } /*########################################################################################################################* *--------------------------------------------------------SignInTask-------------------------------------------------------* *#########################################################################################################################*/ /* < POST /api/login/ < username=AndrewPH&password=examplePassW0rd&token=f033ab37c30201f73f142449d037028d > { > "username": "AndrewPH", > "authenticated": true, > "token": "33e75ff09dd601bbe69f351039152189", > "errors": [] > } */ struct SignInTaskData SignInTask; static void SignInTask_LogError(const cc_string* str) { static char errBuffer[128]; cc_string err; if (String_CaselessEqualsConst(str, "username") || String_CaselessEqualsConst(str, "password")) { SignInTask.error = "&cWrong username or password"; } else if (String_CaselessEqualsConst(str, "verification")) { SignInTask.error = "&cAccount verification required"; } else if (String_CaselessEqualsConst(str, "login_code")) { SignInTask.error = "&cLogin code required (Check your emails)"; SignInTask.needMFA = true; } else if (str->length) { String_InitArray_NT(err, errBuffer); String_Format1(&err, "&c%s", str); errBuffer[err.length] = '\0'; SignInTask.error = errBuffer; } } static void SignInTask_OnValue(struct JsonContext* ctx, const cc_string* str) { if (String_CaselessEqualsConst(&ctx->curKey, "username")) { String_Copy(&SignInTask.username, str); } else if (String_CaselessEqualsConst(&ctx->curKey, "errors")) { SignInTask_LogError(str); } } static void SignInTask_Handle(cc_uint8* data, cc_uint32 len) { static cc_string err_msg = String_FromConst("Error parsing sign in response JSON"); cc_bool success = Json_Handle(data, len, SignInTask_OnValue, NULL, NULL); if (!success) Logger_WarnFunc(&err_msg); } static void SignInTask_Append(cc_string* dst, const char* key, const cc_string* value) { String_AppendConst(dst, key); Http_UrlEncodeUtf8(dst, value); } void SignInTask_Run(const cc_string* user, const cc_string* pass, const cc_string* mfaCode) { cc_string url; char urlBuffer[URL_MAX_SIZE]; static char userBuffer[STRING_SIZE]; cc_string args; char argsBuffer[1024]; if (SignInTask.Base.working) return; LWebTask_Reset(&SignInTask.Base); String_InitArray(url, urlBuffer); String_Format1(&url, "%s/login", &servicesServer); String_InitArray(SignInTask.username, userBuffer); SignInTask.error = NULL; SignInTask.needMFA = false; String_InitArray(args, argsBuffer); SignInTask_Append(&args, "username=", user); SignInTask_Append(&args, "&password=", pass); SignInTask_Append(&args, "&token=", &GetTokenTask.token); SignInTask_Append(&args, "&login_code=", mfaCode); SignInTask.Base.Handle = SignInTask_Handle; SignInTask.Base.reqID = Http_AsyncPostData(&url, 0, args.buffer, args.length, &ccCookies); } /*########################################################################################################################* *-----------------------------------------------------FetchServerTask-----------------------------------------------------* *#########################################################################################################################*/ /* < GET /api/server/a709fabdf836a2a102c952442bf2dab1 > { "servers" : [ > {"hash": "a709fabdf836a2a102c952442bf2dab1", "maxplayers": 70, "name": "Freebuild server", "players": 5, "software": "MCGalaxy", "uptime": 185447, "country_abbr": "CA"}, > ]} */ struct FetchServerData FetchServerTask; static struct ServerInfo* curServer; static void ServerInfo_Init(struct ServerInfo* info) { String_InitArray(info->hash, info->_hashBuffer); String_InitArray(info->name, info->_nameBuffer); String_InitArray(info->ip, info->_ipBuffer); String_InitArray(info->mppass, info->_mppassBuffer); String_InitArray(info->software, info->_softBuffer); info->players = 0; info->maxPlayers = 0; info->uptime = 0; info->featured = false; info->country[0] = 't'; info->country[1] = '1'; /* 'T1' for unrecognised country */ info->_order = -100000; } static void ServerInfo_Parse(struct JsonContext* ctx, const cc_string* val) { struct ServerInfo* info = curServer; if (String_CaselessEqualsConst(&ctx->curKey, "hash")) { String_Copy(&info->hash, val); } else if (String_CaselessEqualsConst(&ctx->curKey, "name")) { String_Copy(&info->name, val); } else if (String_CaselessEqualsConst(&ctx->curKey, "players")) { Convert_ParseInt(val, &info->players); } else if (String_CaselessEqualsConst(&ctx->curKey, "maxplayers")) { Convert_ParseInt(val, &info->maxPlayers); } else if (String_CaselessEqualsConst(&ctx->curKey, "uptime")) { Convert_ParseInt(val, &info->uptime); } else if (String_CaselessEqualsConst(&ctx->curKey, "mppass")) { String_Copy(&info->mppass, val); } else if (String_CaselessEqualsConst(&ctx->curKey, "ip")) { String_Copy(&info->ip, val); } else if (String_CaselessEqualsConst(&ctx->curKey, "port")) { Convert_ParseInt(val, &info->port); } else if (String_CaselessEqualsConst(&ctx->curKey, "software")) { String_Copy(&info->software, val); } else if (String_CaselessEqualsConst(&ctx->curKey, "featured")) { Convert_ParseBool(val, &info->featured); } else if (String_CaselessEqualsConst(&ctx->curKey, "country_abbr")) { /* Two letter country codes, see ISO 3166-1 alpha-2 */ if (val->length < 2) return; /* classicube.net only works with lowercase flag urls */ info->country[0] = val->buffer[0]; Char_MakeLower(info->country[0]); info->country[1] = val->buffer[1]; Char_MakeLower(info->country[1]); } } static void FetchServerTask_Handle(cc_uint8* data, cc_uint32 len) { curServer = &FetchServerTask.server; Json_Handle(data, len, ServerInfo_Parse, NULL, NULL); } void FetchServerTask_Run(const cc_string* hash) { cc_string url; char urlBuffer[URL_MAX_SIZE]; if (FetchServerTask.Base.working) return; LWebTask_Reset(&FetchServerTask.Base); ServerInfo_Init(&FetchServerTask.server); String_InitArray(url, urlBuffer); String_Format2(&url, "%s/server/%s", &servicesServer, hash); FetchServerTask.Base.Handle = FetchServerTask_Handle; FetchServerTask.Base.reqID = Http_AsyncGetDataEx(&url, 0, NULL, NULL, &ccCookies); } /*########################################################################################################################* *-----------------------------------------------------FetchServersTask----------------------------------------------------* *#########################################################################################################################*/ /* < GET /api/servers/ > { "servers" : [ > {"hash": "a709fabdf836a2a102c952442bf2dab1", "maxplayers": 70, "name": "Freebuild server", "players": 5, "software": "MCGalaxy", "uptime": 185447, "country_abbr": "CA"}, > {"hash": "23860c5e192cbaa4698408338efd61cc", "maxplayers": 30, "name": "Other server", "players": 0, software: "", "uptime": 54661, "country_abbr": "T1"} > ]} */ struct FetchServersData FetchServersTask; static void FetchServersTask_Count(struct JsonContext* ctx) { /* JSON is expected in this format: */ /* { "servers" : (depth = 1) */ /* [ (depth = 2) */ /* { server1 }, (depth = 3) */ /* { server2 }, (depth = 3) */ /* ... */ if (ctx->depth != 3) return; FetchServersTask.numServers++; } static void FetchServersTask_Next(struct JsonContext* ctx) { if (ctx->depth != 3) return; curServer++; ServerInfo_Init(curServer); } static void FetchServersTask_Handle(cc_uint8* data, cc_uint32 len) { static cc_string err_msg = String_FromConst("Error parsing servers list response JSON"); int count; cc_bool success; Mem_Free(FetchServersTask.servers); Mem_Free(FetchServersTask.orders); Session_Save(); FetchServersTask.numServers = 0; FetchServersTask.servers = NULL; FetchServersTask.orders = NULL; FetchServersTask.numServers = 0; success = Json_Handle(data, len, NULL, NULL, FetchServersTask_Count); count = FetchServersTask.numServers; if (!success) Logger_WarnFunc(&err_msg); if (count <= 0) return; FetchServersTask.servers = (struct ServerInfo*)Mem_Alloc(count, sizeof(struct ServerInfo), "servers list"); FetchServersTask.orders = (cc_uint16*)Mem_Alloc(count, 2, "servers order"); curServer = FetchServersTask.servers - 1; Json_Handle(data, len, ServerInfo_Parse, NULL, FetchServersTask_Next); } void FetchServersTask_Run(void) { cc_string url; char urlBuffer[URL_MAX_SIZE]; if (FetchServersTask.Base.working) return; LWebTask_Reset(&FetchServersTask.Base); String_InitArray(url, urlBuffer); String_Format1(&url, "%s/servers", &servicesServer); FetchServersTask.Base.Handle = FetchServersTask_Handle; FetchServersTask.Base.reqID = Http_AsyncGetDataEx(&url, 0, NULL, NULL, &ccCookies); } void FetchServersTask_ResetOrder(void) { int i; for (i = 0; i < FetchServersTask.numServers; i++) { FetchServersTask.orders[i] = i; } } /*########################################################################################################################* *-----------------------------------------------------CheckUpdateTask-----------------------------------------------------* *#########################################################################################################################*/ /* < GET /builds.json > {"latest_ts": 1718187640.9587102, "release_ts": 1693265172.020421, "release_version": "1.3.6"} */ struct CheckUpdateData CheckUpdateTask; static char relVersionBuffer[16]; CC_NOINLINE static cc_uint64 CheckUpdateTask_ParseTime(const cc_string* str) { cc_string time, fractional; cc_uint64 secs; /* timestamp is in form of "seconds.fractional" */ /* But only need to care about the seconds here */ String_UNSAFE_Separate(str, '.', &time, &fractional); Convert_ParseUInt64(&time, &secs); return secs; } static void CheckUpdateTask_OnValue(struct JsonContext* ctx, const cc_string* str) { if (String_CaselessEqualsConst(&ctx->curKey, "release_version")) { String_Copy(&CheckUpdateTask.latestRelease, str); } else if (String_CaselessEqualsConst(&ctx->curKey, "latest_ts")) { CheckUpdateTask.devTimestamp = CheckUpdateTask_ParseTime(str); } else if (String_CaselessEqualsConst(&ctx->curKey, "release_ts")) { CheckUpdateTask.relTimestamp = CheckUpdateTask_ParseTime(str); } } static void CheckUpdateTask_Handle(cc_uint8* data, cc_uint32 len) { static cc_string err_msg = String_FromConst("Error parsing update check response JSON"); cc_bool success = Json_Handle(data, len, CheckUpdateTask_OnValue, NULL, NULL); if (!success) Logger_WarnFunc(&err_msg); } void CheckUpdateTask_Run(void) { static const cc_string url = String_FromConst(UPDATES_SERVER "/builds.json"); if (CheckUpdateTask.Base.working) return; LWebTask_Reset(&CheckUpdateTask.Base); String_InitArray(CheckUpdateTask.latestRelease, relVersionBuffer); CheckUpdateTask.Base.Handle = CheckUpdateTask_Handle; CheckUpdateTask.Base.reqID = Http_AsyncGetData(&url, 0); } /*########################################################################################################################* *-----------------------------------------------------FetchUpdateTask-----------------------------------------------------* *#########################################################################################################################*/ struct FetchUpdateData FetchUpdateTask; static void FetchUpdateTask_Handle(cc_uint8* data, cc_uint32 len) { static const cc_string path = String_FromConst(UPDATE_FILE); cc_result res; res = Stream_WriteAllTo(&path, data, len); if (res) { Logger_SysWarn(res, "saving update"); return; } res = Updater_SetNewBuildTime(FetchUpdateTask.timestamp); if (res) Logger_SysWarn(res, "setting update time"); res = Updater_MarkExecutable(); if (res) Logger_SysWarn(res, "making update executable"); #ifdef CC_BUILD_WIN Options_SetBool("update-dirty", true); #endif } void FetchUpdateTask_Run(cc_bool release, int buildIndex) { cc_string url; char urlBuffer[URL_MAX_SIZE]; String_InitArray(url, urlBuffer); String_Format2(&url, UPDATES_SERVER "/%c/%c", release ? "release" : "latest", Updater_Info.builds[buildIndex].path); LWebTask_Reset(&FetchUpdateTask.Base); FetchUpdateTask.timestamp = release ? CheckUpdateTask.relTimestamp : CheckUpdateTask.devTimestamp; FetchUpdateTask.Base.Handle = FetchUpdateTask_Handle; FetchUpdateTask.Base.reqID = Http_AsyncGetData(&url, 0); } /*########################################################################################################################* *-----------------------------------------------------FetchFlagsTask------------------------------------------------------* *#########################################################################################################################*/ struct FetchFlagsData FetchFlagsTask; static int flagsCount, flagsCapacity; static struct Flag* flags; static void FetchFlagsTask_DownloadNext(void); static void FetchFlagsTask_Handle(cc_uint8* data, cc_uint32 len) { struct Flag* flag = &flags[FetchFlagsTask.count]; LBackend_DecodeFlag(flag, data, len); FetchFlagsTask.count++; FetchFlagsTask_DownloadNext(); } static void FetchFlagsTask_DownloadNext(void) { cc_string url; char urlBuffer[URL_MAX_SIZE]; String_InitArray(url, urlBuffer); if (FetchFlagsTask.Base.working) return; if (FetchFlagsTask.count == flagsCount) return; LWebTask_Reset(&FetchFlagsTask.Base); String_Format2(&url, RESOURCE_SERVER "/img/flags/%r%r.png", &flags[FetchFlagsTask.count].country[0], &flags[FetchFlagsTask.count].country[1]); FetchFlagsTask.Base.Handle = FetchFlagsTask_Handle; FetchFlagsTask.Base.reqID = Http_AsyncGetData(&url, 0); } static void FetchFlagsTask_Ensure(void) { if (flagsCount < flagsCapacity) return; flagsCapacity = flagsCount + 10; if (flags) { flags = (struct Flag*)Mem_Realloc(flags, flagsCapacity, sizeof(struct Flag), "flags"); } else { flags = (struct Flag*)Mem_Alloc(flagsCapacity, sizeof(struct Flag), "flags"); } } void FetchFlagsTask_Add(const struct ServerInfo* server) { int i; for (i = 0; i < flagsCount; i++) { if (flags[i].country[0] != server->country[0]) continue; if (flags[i].country[1] != server->country[1]) continue; /* flag is already or will be downloaded */ return; } FetchFlagsTask_Ensure(); Bitmap_Init(flags[flagsCount].bmp, 0, 0, NULL); flags[flagsCount].country[0] = server->country[0]; flags[flagsCount].country[1] = server->country[1]; flags[flagsCount].meta = NULL; flagsCount++; FetchFlagsTask_DownloadNext(); } struct Flag* Flags_Get(const struct ServerInfo* server) { int i; for (i = 0; i < FetchFlagsTask.count; i++) { if (flags[i].country[0] != server->country[0]) continue; if (flags[i].country[1] != server->country[1]) continue; return &flags[i]; } return NULL; } void Flags_Free(void) { int i; for (i = 0; i < FetchFlagsTask.count; i++) { Mem_Free(flags[i].bmp.scan0); } flagsCount = 0; FetchFlagsTask.count = 0; } /*########################################################################################################################* *------------------------------------------------------Session cache------------------------------------------------------* *#########################################################################################################################*/ static cc_string sessionKey = String_FromConst("session"); static cc_bool loadedSession; void Session_Load(void) { cc_string session; char buffer[3072]; if (loadedSession) return; loadedSession = true; /* Increase from max 512 to 2048 per entry */ StringsBuffer_SetLengthBits(&ccCookies, 11); String_InitArray(session, buffer); Options_GetSecure(LOPT_SESSION, &session); if (!session.length) return; EntryList_Set(&ccCookies, &sessionKey, &session, '='); } void Session_Save(void) { cc_string session = EntryList_UNSAFE_Get(&ccCookies, &sessionKey, '='); if (!session.length) return; Options_SetSecure(LOPT_SESSION, &session); } #endif