summary refs log tree commit diff
path: root/src/LWeb.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/LWeb.c
initial commit
Diffstat (limited to 'src/LWeb.c')
-rw-r--r--src/LWeb.c728
1 files changed, 728 insertions, 0 deletions
diff --git a/src/LWeb.c b/src/LWeb.c
new file mode 100644
index 0000000..c0248ff
--- /dev/null
+++ b/src/LWeb.c
@@ -0,0 +1,728 @@
+#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