diff options
author | WlodekM <[email protected]> | 2024-06-16 10:35:45 +0300 |
---|---|---|
committer | WlodekM <[email protected]> | 2024-06-16 10:35:45 +0300 |
commit | abef6da56913f1c55528103e60a50451a39628b1 (patch) | |
tree | b3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/Http_Worker.c |
initial commit
Diffstat (limited to 'src/Http_Worker.c')
-rw-r--r-- | src/Http_Worker.c | 1387 |
1 files changed, 1387 insertions, 0 deletions
diff --git a/src/Http_Worker.c b/src/Http_Worker.c new file mode 100644 index 0000000..260b4c8 --- /dev/null +++ b/src/Http_Worker.c @@ -0,0 +1,1387 @@ +#include "Core.h" +#ifndef CC_BUILD_WEB +#include "_HttpBase.h" + +/* Allocates initial data buffer to store response contents */ +static void Http_BufferInit(struct HttpRequest* req) { + req->progress = 0; + req->_capacity = req->contentLength ? req->contentLength : 1; + req->data = (cc_uint8*)Mem_Alloc(req->_capacity, 1, "http data"); + req->size = 0; +} + +/* Ensures data buffer has enough space left to append amount bytes, reallocates if not */ +static void Http_BufferEnsure(struct HttpRequest* req, cc_uint32 amount) { + cc_uint32 newSize = req->size + amount; + if (newSize <= req->_capacity) return; + + req->_capacity = newSize; + req->data = (cc_uint8*)Mem_Realloc(req->data, newSize, 1, "http data+"); +} + +/* Increases size and updates current progress */ +static void Http_BufferExpanded(struct HttpRequest* req, cc_uint32 read) { + req->size += read; + if (req->contentLength) req->progress = (int)(100.0f * req->size / req->contentLength); +} + + +/*########################################################################################################################* +*---------------------------------------------------Common backend code---------------------------------------------------* +*#########################################################################################################################*/ +static void Http_ParseCookie(struct HttpRequest* req, const cc_string* value) { + cc_string name, data; + int dataEnd; + String_UNSAFE_Separate(value, '=', &name, &data); + /* Cookie is: __cfduid=xyz; expires=abc; path=/; domain=.classicube.net; HttpOnly */ + /* However only the __cfduid=xyz part of the cookie should be stored */ + dataEnd = String_IndexOf(&data, ';'); + if (dataEnd >= 0) data.length = dataEnd; + + EntryList_Set(req->cookies, &name, &data, '='); +} + +static void Http_ParseContentLength(struct HttpRequest* req, const cc_string* value) { + int contentLen = 0; + Convert_ParseInt(value, &contentLen); + + if (contentLen <= 0) return; + req->contentLength = contentLen; +} + +/* Parses a HTTP header */ +static void Http_ParseHeader(struct HttpRequest* req, const cc_string* line) { + static const cc_string httpVersion = String_FromConst("HTTP"); + cc_string name, value, parts[3]; + int numParts; + + /* HTTP[version] [status code] [status reason] */ + if (String_CaselessStarts(line, &httpVersion)) { + numParts = String_UNSAFE_Split(line, ' ', parts, 3); + if (numParts >= 2) Convert_ParseInt(&parts[1], &req->statusCode); + } + /* For all other headers: name: value */ + if (!String_UNSAFE_Separate(line, ':', &name, &value)) return; + + if (String_CaselessEqualsConst(&name, "ETag")) { + String_CopyToRawArray(req->etag, &value); + } else if (String_CaselessEqualsConst(&name, "Content-Length")) { + Http_ParseContentLength(req, &value); + } else if (String_CaselessEqualsConst(&name, "X-Dropbox-Content-Length")) { + /* dropbox stopped returning Content-Length header since switching to chunked transfer */ + /* https://www.dropboxforum.com/t5/Discuss-Dropbox-Developer-API/Dropbox-media-can-t-be-access-by-azure-blob/td-p/575458 */ + Http_ParseContentLength(req, &value); + } else if (String_CaselessEqualsConst(&name, "Last-Modified")) { + String_CopyToRawArray(req->lastModified, &value); + } else if (req->cookies && String_CaselessEqualsConst(&name, "Set-Cookie")) { + Http_ParseCookie(req, &value); + } +} + +/* Adds a http header to the request headers. */ +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value); + +/* Adds all the appropriate headers for a request. */ +static void Http_SetRequestHeaders(struct HttpRequest* req) { + static const cc_string contentType = String_FromConst("application/x-www-form-urlencoded"); + cc_string str, cookies; char cookiesBuffer[1024]; + int i; + + if (req->lastModified[0]) { + str = String_FromRawArray(req->lastModified); + Http_AddHeader(req, "If-Modified-Since", &str); + } + if (req->etag[0]) { + str = String_FromRawArray(req->etag); + Http_AddHeader(req, "If-None-Match", &str); + } + + if (req->data) Http_AddHeader(req, "Content-Type", &contentType); + if (!req->cookies || !req->cookies->count) return; + + String_InitArray(cookies, cookiesBuffer); + for (i = 0; i < req->cookies->count; i++) { + if (i) String_AppendConst(&cookies, "; "); + str = StringsBuffer_UNSAFE_Get(req->cookies, i); + String_AppendString(&cookies, &str); + } + Http_AddHeader(req, "Cookie", &cookies); +} + +/* TODO: Rewrite to use a local variable instead */ +static cc_string* Http_GetUserAgent_UNSAFE(void) { + static char userAgentBuffer[STRING_SIZE]; + static cc_string userAgent; + + String_InitArray(userAgent, userAgentBuffer); + String_AppendConst(&userAgent, GAME_APP_NAME); + String_AppendConst(&userAgent, Platform_AppNameSuffix); + return &userAgent; +} + + +#if defined CC_BUILD_CURL +/*########################################################################################################################* +*-----------------------------------------------------libcurl backend-----------------------------------------------------* +*#########################################################################################################################*/ +#include "Errors.h" +#include <stddef.h> +/* === BEGIN CURL HEADERS === */ +typedef void CURL; +struct curl_slist; +typedef int CURLcode; + +#define CURL_GLOBAL_DEFAULT ((1<<0) | (1<<1)) /* SSL and Win32 options */ +#define CURLOPT_WRITEDATA (10000 + 1) +#define CURLOPT_URL (10000 + 2) +#define CURLOPT_ERRORBUFFER (10000 + 10) +#define CURLOPT_WRITEFUNCTION (20000 + 11) +#define CURLOPT_POSTFIELDS (10000 + 15) +#define CURLOPT_USERAGENT (10000 + 18) +#define CURLOPT_HTTPHEADER (10000 + 23) +#define CURLOPT_HEADERDATA (10000 + 29) +#define CURLOPT_VERBOSE (0 + 41) +#define CURLOPT_HEADER (0 + 42) +#define CURLOPT_NOBODY (0 + 44) +#define CURLOPT_POST (0 + 47) +#define CURLOPT_FOLLOWLOCATION (0 + 52) +#define CURLOPT_POSTFIELDSIZE (0 + 60) +#define CURLOPT_SSL_VERIFYPEER (0 + 64) +#define CURLOPT_MAXREDIRS (0 + 68) +#define CURLOPT_HEADERFUNCTION (20000 + 79) +#define CURLOPT_HTTPGET (0 + 80) +#define CURLOPT_SSL_VERIFYHOST (0 + 81) +#define CURLOPT_HTTP_VERSION (0 + 84) + +#define CURL_HTTP_VERSION_1_1 2L /* stick to HTTP 1.1 */ + +#if defined _WIN32 +#define APIENTRY __cdecl +#else +#define APIENTRY +#endif + +static CURLcode (APIENTRY *_curl_global_init)(long flags); +static void (APIENTRY *_curl_global_cleanup)(void); +static CURL* (APIENTRY *_curl_easy_init)(void); +static CURLcode (APIENTRY *_curl_easy_perform)(CURL *c); +static CURLcode (APIENTRY *_curl_easy_setopt)(CURL *c, int opt, ...); +static void (APIENTRY *_curl_easy_cleanup)(CURL* c); +static void (APIENTRY *_curl_slist_free_all)(struct curl_slist* l); +static struct curl_slist* (APIENTRY *_curl_slist_append)(struct curl_slist* l, const char* v); +static const char* (APIENTRY *_curl_easy_strerror)(CURLcode res); +/* === END CURL HEADERS === */ + +#if defined CC_BUILD_WIN +static const cc_string curlLib = String_FromConst("libcurl.dll"); +static const cc_string curlAlt = String_FromConst("curl.dll"); +#elif defined CC_BUILD_DARWIN +static const cc_string curlLib = String_FromConst("libcurl.4.dylib"); +static const cc_string curlAlt = String_FromConst("libcurl.dylib"); +#elif defined CC_BUILD_NETBSD +static const cc_string curlLib = String_FromConst("libcurl.so"); +static const cc_string curlAlt = String_FromConst("/usr/pkg/lib/libcurl.so"); +#elif defined CC_BUILD_BSD +static const cc_string curlLib = String_FromConst("libcurl.so"); +static const cc_string curlAlt = String_FromConst("libcurl.so"); +#elif defined CC_BUILD_SERENITY +static const cc_string curlLib = String_FromConst("/usr/local/lib/libcurl.so"); +static const cc_string curlAlt = String_FromConst("/usr/local/lib/libcurl.so"); +#elif defined CC_BUILD_OS2 +static const cc_string curlLib = String_FromConst("/@unixroot/usr/lib/curl4.dll"); +static const cc_string curlAlt = String_FromConst("/@unixroot/usr/local/lib/curl4.dll"); +#else +static const cc_string curlLib = String_FromConst("libcurl.so.4"); +static const cc_string curlAlt = String_FromConst("libcurl.so.3"); +#endif + +static cc_bool LoadCurlFuncs(void) { + static const struct DynamicLibSym funcs[] = { +#if !defined CC_BUILD_OS2 + DynamicLib_Sym(curl_global_init), DynamicLib_Sym(curl_global_cleanup), + DynamicLib_Sym(curl_easy_init), DynamicLib_Sym(curl_easy_perform), + DynamicLib_Sym(curl_easy_setopt), DynamicLib_Sym(curl_easy_cleanup), + DynamicLib_Sym(curl_slist_free_all), DynamicLib_Sym(curl_slist_append) +#else + DynamicLib_SymC(curl_global_init), DynamicLib_SymC(curl_global_cleanup), + DynamicLib_SymC(curl_easy_init), DynamicLib_SymC(curl_easy_perform), + DynamicLib_SymC(curl_easy_setopt), DynamicLib_SymC(curl_easy_cleanup), + DynamicLib_SymC(curl_slist_free_all), DynamicLib_SymC(curl_slist_append) +#endif + }; + cc_bool success; + void* lib; + + success = DynamicLib_LoadAll(&curlLib, funcs, Array_Elems(funcs), &lib); + if (!lib) { + success = DynamicLib_LoadAll(&curlAlt, funcs, Array_Elems(funcs), &lib); + } + + /* Non-essential function missing in older curl versions */ + _curl_easy_strerror = DynamicLib_Get2(lib, "curl_easy_strerror"); + return success; +} + +static CURL* curl; +static cc_bool curlSupported, curlVerbose; + +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + const char* err; + + if (!_curl_easy_strerror) return false; + err = _curl_easy_strerror((CURLcode)res); + if (!err) return false; + + String_AppendConst(dst, err); + return true; +} + +static void HttpBackend_Init(void) { + static const cc_string msg = String_FromConst("Failed to init libcurl. All HTTP requests will therefore fail."); + CURLcode res; + + if (!LoadCurlFuncs()) { Logger_WarnFunc(&msg); return; } + res = _curl_global_init(CURL_GLOBAL_DEFAULT); + if (res) { Logger_SimpleWarn(res, "initing curl"); return; } + curl = _curl_easy_init(); + if (!curl) { Logger_SimpleWarn(res, "initing curl_easy"); return; } + + curlSupported = true; + curlVerbose = Options_GetBool("curl-verbose", false); +} + +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value) { + cc_string tmp; char tmpBuffer[1024]; + String_InitArray_NT(tmp, tmpBuffer); + String_Format2(&tmp, "%c: %s", key, value); + + tmp.buffer[tmp.length] = '\0'; + req->meta = _curl_slist_append((struct curl_slist*)req->meta, tmp.buffer); +} + +/* Processes a HTTP header downloaded from the server */ +static size_t Http_ProcessHeader(char* buffer, size_t size, size_t nitems, void* userdata) { + struct HttpRequest* req = (struct HttpRequest*)userdata; + size_t len = nitems; + cc_string line; + /* line usually has \r\n at end */ + if (len && buffer[len - 1] == '\n') len--; + if (len && buffer[len - 1] == '\r') len--; + + line = String_Init(buffer, len, len); + Http_ParseHeader(req, &line); + return nitems; +} + +/* Processes a chunk of data downloaded from the web server */ +static size_t Http_ProcessData(char *buffer, size_t size, size_t nitems, void* userdata) { + struct HttpRequest* req = (struct HttpRequest*)userdata; + + if (!req->_capacity) Http_BufferInit(req); + Http_BufferEnsure(req, nitems); + + Mem_Copy(&req->data[req->size], buffer, nitems); + Http_BufferExpanded(req, nitems); + return nitems; +} + +/* Sets general curl options for a request */ +static void Http_SetCurlOpts(struct HttpRequest* req) { + _curl_easy_setopt(curl, CURLOPT_USERAGENT, GAME_APP_NAME); + _curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + _curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 20L); + _curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + + _curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, Http_ProcessHeader); + _curl_easy_setopt(curl, CURLOPT_HEADERDATA, req); + _curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, Http_ProcessData); + _curl_easy_setopt(curl, CURLOPT_WRITEDATA, req); + + if (curlVerbose) _curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + + if (httpsVerify) return; + _curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); +} + +static cc_result HttpBackend_Do(struct HttpRequest* req, cc_string* url) { + char urlStr[NATIVE_STR_LEN]; + void* post_data = req->data; + CURLcode res; + if (!curlSupported) return ERR_NOT_SUPPORTED; + + req->meta = NULL; + Http_SetRequestHeaders(req); + _curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req->meta); + + Http_SetCurlOpts(req); + String_EncodeUtf8(urlStr, url); + _curl_easy_setopt(curl, CURLOPT_URL, urlStr); + + if (req->requestType == REQUEST_TYPE_HEAD) { + _curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + } else if (req->requestType == REQUEST_TYPE_POST) { + _curl_easy_setopt(curl, CURLOPT_POST, 1L); + _curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, req->size); + _curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->data); + + /* per curl docs, we must persist POST data until request finishes */ + req->data = NULL; + req->size = 0; + } else { + /* Undo POST/HEAD state */ + _curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + } + + /* must be at least CURL_ERROR_SIZE (256) in size */ + req->error = Mem_TryAllocCleared(257, 1); + _curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, req->error); + /* TODO stackalloc instead and then copy to dynamic array later? */ + /* probably not worth the extra complexity though */ + + req->_capacity = 0; + req->progress = HTTP_PROGRESS_FETCHING_DATA; + res = _curl_easy_perform(curl); + req->progress = 100; + + /* Free error string if it isn't needed */ + if (req->error && !req->error[0]) { + Mem_Free(req->error); + req->error = NULL; + } + + _curl_slist_free_all((struct curl_slist*)req->meta); + /* can free now that request has finished */ + Mem_Free(post_data); + _curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, NULL); + return res; +} +#elif defined CC_BUILD_HTTPCLIENT +#include "Errors.h" +#include "PackedCol.h" +#include "SSL.h" + +/*########################################################################################################################* +*---------------------------------------------------------HttpUrl---------------------------------------------------------* +*#########################################################################################################################*/ +/* Components of a URL */ +struct HttpUrl { + cc_bool https; /* Whether HTTPS or just HTTP protocol */ + cc_string address; /* Address of server (e.g. "classicube.net:8080") */ + cc_string resource; /* Path being accessed (and query string) */ + char _addressBuffer[STRING_SIZE + 8]; + char _resourceBuffer[STRING_SIZE * 4]; +}; + +static void HttpUrl_EncodeUrl(cc_string* dst, const cc_string* src) { + cc_uint8 data[4]; + int i, len; + char c; + + for (i = 0; i < src->length; i++) { + c = src->buffer[i]; + len = Convert_CP437ToUtf8(c, data); + + /* URL path/query must not be URL encoded (it normally would be) */ + if (c == '/' || c == '?' || c == '=') { + String_Append(dst, c); + } else { + Http_UrlEncode(dst, data, len); + } + } +} + +/* Splits up the components of a URL */ +static void HttpUrl_Parse(const cc_string* src, struct HttpUrl* url) { + cc_string scheme, path, addr, resource; + /* URL is of form [scheme]://[server host]:[server port]/[resource] */ + /* For simplicity, parsed as [scheme]://[server address]/[resource] */ + int idx = String_IndexOfConst(src, "://"); + + scheme = idx == -1 ? String_Empty : String_UNSAFE_Substring(src, 0, idx); + path = idx == -1 ? *src : String_UNSAFE_SubstringAt(src, idx + 3); + + url->https = String_CaselessEqualsConst(&scheme, "https"); + String_UNSAFE_Separate(&path, '/', &addr, &resource); + + String_InitArray(url->address, url->_addressBuffer); + String_Copy(&url->address, &addr); + + String_InitArray(url->resource, url->_resourceBuffer); + String_Append(&url->resource, '/'); + /* Address may have unicode characters - need to percent encode them */ + HttpUrl_EncodeUrl(&url->resource, &resource); +} + +static cc_result HttpUrl_ResolveRedirect(struct HttpUrl* parts, const cc_string* url) { + /* absolute URL */ + if (String_IndexOfConst(url, "http://") == 0 || String_IndexOfConst(url, "https://") == 0) { + HttpUrl_Parse(url, parts); + return 0; + } + + /* Root relative URL */ + if (url->buffer[0] == '/' && (url->length == 1 || url->buffer[1] != '/')) { + parts->resource.length = 0; + HttpUrl_EncodeUrl(&parts->resource, url); + return 0; + } + + /* TODO scheme relative or relative URL or invalid */ + return HTTP_ERR_RELATIVE; +} + + +/*########################################################################################################################* +*------------------------------------------------------HttpConnection-----------------------------------------------------* +*#########################################################################################################################*/ +struct HttpConnection { + cc_socket socket; + void* sslCtx; + cc_bool valid; +}; + +static void HttpConnection_Close(struct HttpConnection* conn) { + if (conn->sslCtx) { + SSL_Free(conn->sslCtx); + conn->sslCtx = NULL; + } + + if (conn->socket != -1) { + Socket_Close(conn->socket); + conn->socket = -1; + } + conn->valid = false; +} + +static void ExtractHostPort(const struct HttpUrl* url, cc_string* host, cc_string* port) { + /* address can have the form of either "host" or "host:port" */ + /* Slightly more complicated because IPv6 hosts can be e.g. [::1] */ + const cc_string* addr = &url->address; + int idx = String_LastIndexOf(addr, ':'); + + if (idx == -1) { + *host = *addr; + *port = String_Empty; + } else { + *host = String_UNSAFE_Substring(addr, 0, idx); + *port = String_UNSAFE_SubstringAt(addr, idx + 1); + } +} + +static cc_result HttpConnection_Open(struct HttpConnection* conn, const struct HttpUrl* url) { + cc_string host, port; + cc_uint16 portNum; + cc_result res; + cc_sockaddr addrs[SOCKET_MAX_ADDRS]; + int i, numValidAddrs; + + ExtractHostPort(url, &host, &port); + if (!Convert_ParseUInt16(&port, &portNum)) { + portNum = url->https ? 443 : 80; + } + + conn->socket = -1; + conn->sslCtx = NULL; + + if ((res = Socket_ParseAddress(&host, portNum, addrs, &numValidAddrs))) return res; + res = ERR_INVALID_ARGUMENT; /* in case 0 valid addresses */ + + /* TODO: Connect in parallel instead of serial, but that's a lot of work */ + for (i = 0; i < numValidAddrs; i++) + { + res = Socket_Connect(&conn->socket, &addrs[i], false); + if (!res) break; + + HttpConnection_Close(conn); + } + if (res) return res; + + conn->valid = true; + if (!url->https) return 0; + return SSL_Init(conn->socket, &host, &conn->sslCtx); +} + +static cc_result HttpConnection_Read(struct HttpConnection* conn, cc_uint8* data, cc_uint32 count, cc_uint32* read) { + if (conn->sslCtx) + return SSL_Read(conn->sslCtx, data, count, read); + + return Socket_Read(conn->socket, data, count, read); +} + +static cc_result HttpConnection_Write(struct HttpConnection* conn, const cc_uint8* data, cc_uint32 count) { + if (conn->sslCtx) + return SSL_WriteAll(conn->sslCtx, data, count); + + return Socket_WriteAll(conn->socket, data, count); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Connection Pool-----------------------------------------------------* +*#########################################################################################################################*/ +static struct ConnectionPoolEntry { + struct HttpConnection conn; + cc_string addr; + char addrBuffer[STRING_SIZE]; + cc_bool https; +} connection_pool[10]; + +static cc_result ConnectionPool_Insert(int i, struct HttpConnection** conn, const struct HttpUrl* url) { + struct ConnectionPoolEntry* e = &connection_pool[i]; + *conn = &e->conn; + + String_InitArray(e->addr, e->addrBuffer); + String_Copy(&e->addr, &url->address); + e->https = url->https; + return HttpConnection_Open(&e->conn, url); +} + +static cc_result ConnectionPool_Open(struct HttpConnection** conn, const struct HttpUrl* url) { + struct ConnectionPoolEntry* e; + int i; + + for (i = 0; i < Array_Elems(connection_pool); i++) + { + e = &connection_pool[i]; + if (e->conn.valid && e->https == url->https && String_Equals(&e->addr, &url->address)) { + *conn = &connection_pool[i].conn; + return 0; + } + } + + for (i = 0; i < Array_Elems(connection_pool); i++) + { + e = &connection_pool[i]; + if (!e->conn.valid) return ConnectionPool_Insert(i, conn, url); + } + + /* TODO: Should we be consistent in which entry gets evicted? */ + i = (cc_uint8)Stopwatch_Measure() % Array_Elems(connection_pool); + HttpConnection_Close(&connection_pool[i].conn); + return ConnectionPool_Insert(i, conn, url); +} + + +/*########################################################################################################################* +*--------------------------------------------------------HttpClient-------------------------------------------------------* +*#########################################################################################################################*/ +enum HTTP_RESPONSE_STATE { + HTTP_RESPONSE_STATE_INITIAL, + HTTP_RESPONSE_STATE_HEADER, + HTTP_RESPONSE_STATE_DATA, + HTTP_RESPONSE_STATE_CHUNK_HEADER, + HTTP_RESPONSE_STATE_CHUNK_END_R, + HTTP_RESPONSE_STATE_CHUNK_END_N, + HTTP_RESPONSE_STATE_CHUNK_TRAILERS, + HTTP_RESPONSE_STATE_DONE +}; +#define HTTP_HEADER_MAX_LENGTH 4096 +#define HTTP_LOCATION_MAX_LENGTH 256 + +struct HttpClientState { + enum HTTP_RESPONSE_STATE state; + struct HttpConnection* conn; + struct HttpRequest* req; + cc_uint32 dataLeft; /* Number of bytes still to read from the current chunk or body */ + int chunked; + cc_bool autoClose; + cc_string header, location; + struct HttpUrl url; + char _headerBuffer[HTTP_HEADER_MAX_LENGTH]; + char _locationBuffer[HTTP_LOCATION_MAX_LENGTH]; +}; + +static void HttpClientState_Reset(struct HttpClientState* state) { + state->state = HTTP_RESPONSE_STATE_INITIAL; + state->chunked = 0; + state->dataLeft = 0; + state->autoClose = false; + String_InitArray(state->header, state->_headerBuffer); + String_InitArray(state->location, state->_locationBuffer); +} + +static void HttpClientState_Init(struct HttpClientState* state) { + HttpClientState_Reset(state); +} + + +static void HttpClient_Serialise(struct HttpClientState* state) { + static const char* verbs[] = { "GET", "HEAD", "POST" }; + + struct HttpRequest* req = state->req; + cc_string* buffer = (cc_string*)req->meta; + /* TODO move to other functions */ + /* Write request message headers */ + String_Format2(buffer, "%c %s HTTP/1.1\r\n", + verbs[req->requestType], &state->url.resource); + + Http_AddHeader(req, "Host", &state->url.address); + Http_AddHeader(req, "User-Agent", Http_GetUserAgent_UNSAFE()); + if (req->data) String_Format1(buffer, "Content-Length: %i\r\n", &req->size); + + Http_SetRequestHeaders(req); + String_AppendConst(buffer, "\r\n"); + + /* Write request message body */ + if (req->data) { + String_AppendAll(buffer, req->data, req->size); + HttpRequest_Free(req); + } /* TODO post redirect handling */ +} + +static cc_result HttpClient_SendRequest(struct HttpClientState* state) { + char inputBuffer[16384]; + cc_string inputMsg; + + String_InitArray(inputMsg, inputBuffer); + state->req->meta = &inputMsg; + state->req->progress = HTTP_PROGRESS_FETCHING_DATA; + HttpClient_Serialise(state); + + return HttpConnection_Write(state->conn, (cc_uint8*)inputBuffer, inputMsg.length); +} + + +static void HttpClient_ParseHeader(struct HttpClientState* state, const cc_string* line) { + static const cc_string HTTP_10_VERSION = String_FromConst("HTTP/1.0"); + cc_string name, value; + /* HTTP 1.0 defaults to auto closing connection */ + if (String_CaselessStarts(line, &HTTP_10_VERSION)) state->autoClose = true; + + /* name: value */ + if (!String_UNSAFE_Separate(line, ':', &name, &value)) return; + + if (String_CaselessEqualsConst(&name, "Transfer-Encoding")) { + state->chunked = String_CaselessEqualsConst(&value, "chunked"); + } else if (String_CaselessEqualsConst(&name, "Location")) { + String_Copy(&state->location, &value); + } else if (String_CaselessEqualsConst(&name, "Connection")) { + if (String_CaselessEqualsConst(&value, "keep-alive")) state->autoClose = false; + if (String_CaselessEqualsConst(&value, "close")) state->autoClose = true; + } +} + +/* RFC 7230, section 3.3.3 - Message Body Length */ +static cc_bool HttpClient_HasBody(struct HttpRequest* req) { + /* HEAD responses never have a message body */ + if (req->requestType == REQUEST_TYPE_HEAD) return false; + /* 1XX (Information) responses don't have message body */ + if (req->statusCode >= 100 && req->statusCode <= 199) return false; + /* 204 (No Content) and 304 (Not Modified) also don't */ + if (req->statusCode == 204 || req->statusCode == 304) return false; + + return true; +} + +static int HttpClient_BeginBody(struct HttpRequest* req, struct HttpClientState* state) { + if (!HttpClient_HasBody(req)) + return HTTP_RESPONSE_STATE_DONE; + + if (state->chunked) { + Http_BufferInit(req); + return HTTP_RESPONSE_STATE_CHUNK_HEADER; + } + if (req->contentLength) { + Http_BufferInit(req); + return HTTP_RESPONSE_STATE_DATA; + } + /* Zero length response */ + return HTTP_RESPONSE_STATE_DONE; +} + +/* RFC 7230, section 4.1 - Chunked Transfer Coding */ +static int HttpClient_GetChunkLength(const cc_string* line) { + int length = 0, i, part; + + for (i = 0; i < line->length; i++) + { + char c = line->buffer[i]; + /* RFC 7230, section 4.1.1 - Chunk Extensions */ + if (c == ';') break; + + part = PackedCol_DeHex(c); + if (part == -1) return -1; + length = (length << 4) | part; + } + return length; +} + +/* https://httpwg.org/specs/rfc7230.html */ +static cc_result HttpClient_Process(struct HttpClientState* state, char* buffer, int total) { + struct HttpRequest* req = state->req; + cc_uint32 left, avail, read; + int offset = 0, chunkLen; + + while (offset < total) { + switch (state->state) { + case HTTP_RESPONSE_STATE_INITIAL: + state->state = HTTP_RESPONSE_STATE_HEADER; + break; + + case HTTP_RESPONSE_STATE_HEADER: + { + for (; offset < total;) + { + char c = buffer[offset++]; + if (c == '\r') continue; + if (c != '\n') { + /* Warn when a header would be truncated */ + if (state->header.length == HTTP_HEADER_MAX_LENGTH) + return HTTP_ERR_TRUNCATED; + + String_Append(&state->header, c); + continue; + } + + /* Zero length header = end of message headers */ + if (state->header.length == 0) { + state->state = HttpClient_BeginBody(req, state); + + /* The rest of the request body is just content/data */ + if (state->state == HTTP_RESPONSE_STATE_DATA) { + Http_BufferEnsure(req, req->contentLength); + state->dataLeft = req->contentLength; + } + break; + } + + Http_ParseHeader(state->req, &state->header); + HttpClient_ParseHeader(state, &state->header); + state->header.length = 0; + } + } + break; + + case HTTP_RESPONSE_STATE_DATA: + { + left = total - offset; + avail = state->dataLeft; + read = min(left, avail); + + Mem_Copy(req->data + req->size, buffer + offset, read); + Http_BufferExpanded(req, read); state->dataLeft -= read; + offset += read; + + if (!state->dataLeft) { + state->state = state->chunked ? HTTP_RESPONSE_STATE_CHUNK_END_R : HTTP_RESPONSE_STATE_DONE; + } + } + break; + + + /* RFC 7230, section 4.1 - Chunked Transfer Coding */ + case HTTP_RESPONSE_STATE_CHUNK_HEADER: + { + for (; offset < total;) + { + char c = buffer[offset++]; + if (c == '\r') continue; + if (c != '\n') { String_Append(&state->header, c); continue; } + + chunkLen = HttpClient_GetChunkLength(&state->header); + if (chunkLen < 0) return HTTP_ERR_CHUNK_SIZE; + state->header.length = 0; + + if (chunkLen == 0) { + state->state = HTTP_RESPONSE_STATE_CHUNK_TRAILERS; + } else { + state->state = HTTP_RESPONSE_STATE_DATA; + Http_BufferEnsure(req, chunkLen); + state->dataLeft = chunkLen; + } + break; + } + } + break; + + /* Chunks are terminated by \r\n */ + case HTTP_RESPONSE_STATE_CHUNK_END_R: + if (buffer[offset++] != '\r') return ERR_INVALID_ARGUMENT; + + state->state = HTTP_RESPONSE_STATE_CHUNK_END_N; + break; + + case HTTP_RESPONSE_STATE_CHUNK_END_N: + if (buffer[offset++] != '\n') return ERR_INVALID_ARGUMENT; + + state->state = HTTP_RESPONSE_STATE_CHUNK_HEADER; + break; + + /* RFC 7230, section 4.1.2 - Chunked Trailer Part */ + case HTTP_RESPONSE_STATE_CHUNK_TRAILERS: + { + for (; offset < total;) + { + char c = buffer[offset++]; + if (c == '\r') continue; + if (c != '\n') { String_Append(&state->header, c); continue; } + + /* Zero length header = end of message trailers */ + if (state->header.length == 0) { + state->state = HTTP_RESPONSE_STATE_DONE; + break; + } + state->header.length = 0; + } + } + break; + + default: + return 0; + } + } + return 0; +} + +#define INPUT_BUFFER_LEN 8192 +static cc_result HttpClient_ParseResponse(struct HttpClientState* state) { + struct HttpRequest* req = state->req; + cc_uint8 buffer[INPUT_BUFFER_LEN]; + cc_uint8* dst; + cc_uint32 total; + cc_result res; + + for (;;) + { + dst = state->dataLeft > INPUT_BUFFER_LEN ? (req->data + req->size) : buffer; + res = HttpConnection_Read(state->conn, dst, INPUT_BUFFER_LEN, &total); + if (res) return res; + + if (total == 0) { + Platform_Log1("Http read unexpectedly returned 0 in state %i", &state->state); + return state->state == HTTP_RESPONSE_STATE_INITIAL ? HTTP_ERR_NO_RESPONSE : ERR_END_OF_STREAM; + } + + if (dst != buffer) { + /* When there is more than INPUT_BUFFER_LEN bytes of unread data/content, */ + /* there is no need to run the HTTP client state machine - just read directly */ + /* into the output buffer to avoid a pointless Mem_Copy call */ + Http_BufferExpanded(req, total); + state->dataLeft -= total; + } else { + res = HttpClient_Process(state, (char*)buffer, total); + } + + if (res) return res; + if (state->state == HTTP_RESPONSE_STATE_DONE) return 0; + } +} + +static cc_bool HttpClient_IsRedirect(struct HttpRequest* req) { + return req->statusCode >= 300 && req->statusCode <= 399 && req->statusCode != 304; +} + +static cc_result HttpClient_HandleRedirect(struct HttpClientState* state) { + cc_result res = HttpUrl_ResolveRedirect(&state->url, &state->location); + if (res) return res; + + HttpRequest_Free(state->req); + Platform_Log1(" Redirecting to: %s", &state->location); + state->req->contentLength = 0; /* TODO */ + return 0; +} + + +/*########################################################################################################################* +*-----------------------------------------------Http backend implementation-----------------------------------------------* +*#########################################################################################################################*/ +static void HttpBackend_Init(void) { + SSLBackend_Init(httpsVerify); + //httpOnly = true; // TODO: insecure +} + +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value) { + String_Format2((cc_string*)req->meta, "%c:%s\r\n", key, value); +} + +static cc_result HttpBackend_PerformRequest(struct HttpClientState* state) { + cc_result res; + + res = ConnectionPool_Open(&state->conn, &state->url); + if (res) { HttpConnection_Close(state->conn); return res; } + + res = HttpClient_SendRequest(state); + if (res) { HttpConnection_Close(state->conn); return res; } + + res = HttpClient_ParseResponse(state); + if (res) HttpConnection_Close(state->conn); + + return res; +} + +static cc_result HttpBackend_Do(struct HttpRequest* req, cc_string* urlStr) { + struct HttpClientState state; + cc_bool retried = false; + int redirects = 0; + cc_result res; + + HttpClientState_Init(&state); + HttpUrl_Parse(urlStr, &state.url); + state.req = req; + + for (;;) { + res = HttpBackend_PerformRequest(&state); + /* TODO: Can we handle this while preserving the TCP connection */ + if (res == SSL_ERR_CONTEXT_DEAD && !retried) { + Platform_LogConst("Resetting connection due to SSL context being dropped.."); + res = HttpBackend_PerformRequest(&state); + retried = true; + } + if (res == HTTP_ERR_NO_RESPONSE && !retried) { + Platform_LogConst("Resetting connection due to empty response.."); + res = HttpBackend_PerformRequest(&state); + retried = true; + } + + if (res || !HttpClient_IsRedirect(req)) break; + if (redirects >= 20) return HTTP_ERR_REDIRECTS; + + /* TODO FOLLOW LOCATION PROPERLY */ + redirects++; + res = HttpClient_HandleRedirect(&state); + if (res) break; + HttpClientState_Reset(&state); + } + return res; +} + +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + return SSLBackend_DescribeError(res, dst); +} +#elif defined CC_BUILD_ANDROID +/*########################################################################################################################* +*-----------------------------------------------------Android backend-----------------------------------------------------* +*#########################################################################################################################*/ +struct HttpRequest* java_req; +static jmethodID JAVA_httpInit, JAVA_httpSetHeader, JAVA_httpPerform, JAVA_httpSetData; +static jmethodID JAVA_httpDescribeError; + +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + char buffer[NATIVE_STR_LEN]; + cc_string err; + JNIEnv* env; + jvalue args[1]; + jobject obj; + + JavaGetCurrentEnv(env); + args[0].i = res; + obj = JavaSCall_Obj(env, JAVA_httpDescribeError, args); + if (!obj) return false; + + err = JavaGetString(env, obj, buffer); + String_AppendString(dst, &err); + (*env)->DeleteLocalRef(env, obj); + return true; +} + +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value) { + JNIEnv* env; + jvalue args[2]; + + JavaGetCurrentEnv(env); + args[0].l = JavaMakeConst(env, key); + args[1].l = JavaMakeString(env, value); + + JavaSCall_Void(env, JAVA_httpSetHeader, args); + (*env)->DeleteLocalRef(env, args[0].l); + (*env)->DeleteLocalRef(env, args[1].l); +} + +/* Processes a HTTP header downloaded from the server */ +static void JNICALL java_HttpParseHeader(JNIEnv* env, jobject o, jstring header) { + char buffer[NATIVE_STR_LEN]; + cc_string line = JavaGetString(env, header, buffer); + Http_ParseHeader(java_req, &line); +} + +/* Processes a chunk of data downloaded from the web server */ +static void JNICALL java_HttpAppendData(JNIEnv* env, jobject o, jbyteArray arr, jint len) { + struct HttpRequest* req = java_req; + if (!req->_capacity) Http_BufferInit(req); + + Http_BufferEnsure(req, len); + (*env)->GetByteArrayRegion(env, arr, 0, len, (jbyte*)(&req->data[req->size])); + Http_BufferExpanded(req, len); +} + +static const JNINativeMethod methods[] = { + { "httpParseHeader", "(Ljava/lang/String;)V", java_HttpParseHeader }, + { "httpAppendData", "([BI)V", java_HttpAppendData } +}; +static void CacheMethodRefs(JNIEnv* env) { + JAVA_httpInit = JavaGetSMethod(env, "httpInit", "(Ljava/lang/String;Ljava/lang/String;)I"); + JAVA_httpSetHeader = JavaGetSMethod(env, "httpSetHeader", "(Ljava/lang/String;Ljava/lang/String;)V"); + JAVA_httpPerform = JavaGetSMethod(env, "httpPerform", "()I"); + JAVA_httpSetData = JavaGetSMethod(env, "httpSetData", "([B)I"); + + JAVA_httpDescribeError = JavaGetSMethod(env, "httpDescribeError", "(I)Ljava/lang/String;"); +} + +static void HttpBackend_Init(void) { + JNIEnv* env; + JavaGetCurrentEnv(env); + JavaRegisterNatives(env, methods); + CacheMethodRefs(env); +} + +static cc_result Http_InitReq(JNIEnv* env, struct HttpRequest* req, cc_string* url) { + static const char* verbs[3] = { "GET", "HEAD", "POST" }; + jvalue args[2]; + jint res; + + args[0].l = JavaMakeString(env, url); + args[1].l = JavaMakeConst(env, verbs[req->requestType]); + + res = JavaSCall_Int(env, JAVA_httpInit, args); + (*env)->DeleteLocalRef(env, args[0].l); + (*env)->DeleteLocalRef(env, args[1].l); + return res; +} + +static cc_result Http_SetData(JNIEnv* env, struct HttpRequest* req) { + jvalue args[1]; + jint res; + + args[0].l = JavaMakeBytes(env, req->data, req->size); + res = JavaSCall_Int(env, JAVA_httpSetData, args); + (*env)->DeleteLocalRef(env, args[0].l); + return res; +} + +static cc_result HttpBackend_Do(struct HttpRequest* req, cc_string* url) { + JNIEnv* env; + jint res; + + JavaGetCurrentEnv(env); + if ((res = Http_InitReq(env, req, url))) return res; + java_req = req; + + Http_SetRequestHeaders(req); + Http_AddHeader(req, "User-Agent", Http_GetUserAgent_UNSAFE()); + if (req->data && (res = Http_SetData(env, req))) return res; + + req->_capacity = 0; + req->progress = HTTP_PROGRESS_FETCHING_DATA; + res = JavaSCall_Int(env, JAVA_httpPerform, NULL); + req->progress = 100; + return res; +} +#elif defined CC_BUILD_CFNETWORK +/*########################################################################################################################* +*----------------------------------------------------CFNetwork backend----------------------------------------------------* +*#########################################################################################################################*/ +#include "Errors.h" +#include <stddef.h> +#include <CFNetwork/CFNetwork.h> + +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +static void HttpBackend_Init(void) { + +} + +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value) { + char tmp[NATIVE_STR_LEN]; + CFStringRef keyCF, valCF; + CFHTTPMessageRef msg = (CFHTTPMessageRef)req->meta; + String_EncodeUtf8(tmp, value); + + keyCF = CFStringCreateWithCString(NULL, key, kCFStringEncodingUTF8); + valCF = CFStringCreateWithCString(NULL, tmp, kCFStringEncodingUTF8); + CFHTTPMessageSetHeaderFieldValue(msg, keyCF, valCF); + CFRelease(keyCF); + CFRelease(valCF); +} + +static void Http_CheckHeader(const void* k, const void* v, void* ctx) { + cc_string line; char lineBuffer[2048]; + char keyBuf[128] = { 0 }; + char valBuf[1024] = { 0 }; + String_InitArray(line, lineBuffer); + + CFStringGetCString((CFStringRef)k, keyBuf, sizeof(keyBuf), kCFStringEncodingUTF8); + CFStringGetCString((CFStringRef)v, valBuf, sizeof(valBuf), kCFStringEncodingUTF8); + + String_Format2(&line, "%c:%c", keyBuf, valBuf); + Http_ParseHeader((struct HttpRequest*)ctx, &line); + ctx = NULL; +} + +static cc_result ParseResponseHeaders(struct HttpRequest* req, CFReadStreamRef stream) { + CFHTTPMessageRef response = CFReadStreamCopyProperty(stream, kCFStreamPropertyHTTPResponseHeader); + if (!response) return ERR_INVALID_ARGUMENT; + + CFDictionaryRef headers = CFHTTPMessageCopyAllHeaderFields(response); + CFDictionaryApplyFunction(headers, Http_CheckHeader, req); + req->statusCode = CFHTTPMessageGetResponseStatusCode(response); + + CFRelease(headers); + CFRelease(response); + return 0; +} + +static cc_result HttpBackend_Do(struct HttpRequest* req, cc_string* url) { + static CFStringRef verbs[] = { CFSTR("GET"), CFSTR("HEAD"), CFSTR("POST") }; + cc_bool gotHeaders = false; + char tmp[NATIVE_STR_LEN]; + CFHTTPMessageRef request; + CFStringRef urlCF; + CFURLRef urlRef; + cc_result result = 0; + + String_EncodeUtf8(tmp, url); + urlCF = CFStringCreateWithCString(NULL, tmp, kCFStringEncodingUTF8); + urlRef = CFURLCreateWithString(NULL, urlCF, NULL); + // TODO e.g. "http://www.example.com/skin/1 2.png" causes this to return null + // TODO release urlCF + if (!urlRef) return ERR_INVALID_DATA_URL; + + request = CFHTTPMessageCreateRequest(NULL, verbs[req->requestType], urlRef, kCFHTTPVersion1_1); + req->meta = request; + Http_SetRequestHeaders(req); + Http_AddHeader(req, "User-Agent", Http_GetUserAgent_UNSAFE()); + CFRelease(urlRef); + + if (req->data && req->size) { + CFDataRef body = CFDataCreate(NULL, req->data, req->size); + CFHTTPMessageSetBody(request, body); + CFRelease(body); /* TODO: ???? */ + + req->data = NULL; + req->size = 0; + Mem_Free(req->data); + } + + CFReadStreamRef stream = CFReadStreamCreateForHTTPRequest(NULL, request); + CFReadStreamSetProperty(stream, kCFStreamPropertyHTTPShouldAutoredirect, kCFBooleanTrue); + //CFHTTPReadStreamSetRedirectsAutomatically(stream, TRUE); + CFReadStreamOpen(stream); + UInt8 buf[1024]; + + for (;;) { + CFIndex read = CFReadStreamRead(stream, buf, sizeof(buf)); + if (read <= 0) break; + + // reading headers before for loop doesn't work + if (!gotHeaders) { + gotHeaders = true; + if ((result = ParseResponseHeaders(req, stream))) break; + } + + if (!req->_capacity) Http_BufferInit(req); + Http_BufferEnsure(req, read); + + Mem_Copy(&req->data[req->size], buf, read); + Http_BufferExpanded(req, read); + } + + if (!gotHeaders) + result = ParseResponseHeaders(req, stream); + + //Thread_Sleep(1000); + CFRelease(request); + return result; +} +#elif !defined CC_BUILD_NETWORKING +/*########################################################################################################################* +*------------------------------------------------------Null backend-------------------------------------------------------* +*#########################################################################################################################*/ +#include "Errors.h" + +static cc_bool HttpBackend_DescribeError(cc_result res, cc_string* dst) { + return false; +} + +static void HttpBackend_Init(void) { } + +static void Http_AddHeader(struct HttpRequest* req, const char* key, const cc_string* value) { } + +static cc_result HttpBackend_Do(struct HttpRequest* req, cc_string* url) { + req->progress = 100; + return ERR_NOT_SUPPORTED; +} +#endif + + +static void* workerWaitable; +static void* workerThread; + +static void* pendingMutex; +static struct RequestList pendingReqs; + +static void* curRequestMutex; +static struct HttpRequest http_curRequest; + + +/*########################################################################################################################* +*----------------------------------------------------Http public api------------------------------------------------------* +*#########################################################################################################################*/ +cc_bool Http_GetResult(int reqID, struct HttpRequest* item) { + int i; + Mutex_Lock(processedMutex); + { + i = RequestList_Find(&processedReqs, reqID); + if (i >= 0) HttpRequest_Copy(item, &processedReqs.entries[i]); + if (i >= 0) RequestList_RemoveAt(&processedReqs, i); + } + Mutex_Unlock(processedMutex); + return i >= 0; +} + +cc_bool Http_GetCurrent(int* reqID, int* progress) { + Mutex_Lock(curRequestMutex); + { + *reqID = http_curRequest.id; + *progress = http_curRequest.progress; + } + Mutex_Unlock(curRequestMutex); + return *reqID != 0; +} + +int Http_CheckProgress(int reqID) { + int curReqID, progress; + Http_GetCurrent(&curReqID, &progress); + + if (reqID != curReqID) progress = HTTP_PROGRESS_NOT_WORKING_ON; + return progress; +} + +void Http_ClearPending(void) { + Mutex_Lock(pendingMutex); + { + RequestList_Free(&pendingReqs); + } + Mutex_Unlock(pendingMutex); +} + +void Http_TryCancel(int reqID) { + Mutex_Lock(pendingMutex); + { + RequestList_TryFree(&pendingReqs, reqID); + } + Mutex_Unlock(pendingMutex); + + Mutex_Lock(processedMutex); + { + RequestList_TryFree(&processedReqs, reqID); + } + Mutex_Unlock(processedMutex); +} + + +/*########################################################################################################################* +*-----------------------------------------------------Http worker---------------------------------------------------------* +*#########################################################################################################################*/ +/* Sets up state to begin a http request */ +static void PrepareCurrentRequest(struct HttpRequest* req, cc_string* url) { + static const char* verbs[] = { "GET", "HEAD", "POST" }; + Http_GetUrl(req, url); + Platform_Log2("Fetching %s (%c)", url, verbs[req->requestType]); + /* TODO change to verbs etc */ + + Mutex_Lock(curRequestMutex); + { + HttpRequest_Copy(&http_curRequest, req); + http_curRequest.progress = HTTP_PROGRESS_MAKING_REQUEST; + } + Mutex_Unlock(curRequestMutex); +} + +static void PerformRequest(struct HttpRequest* req, cc_string* url) { + cc_uint64 beg, end; + int elapsed; + + beg = Stopwatch_Measure(); + req->result = HttpBackend_Do(req, url); + end = Stopwatch_Measure(); + + elapsed = Stopwatch_ElapsedMS(beg, end); + Platform_Log4("HTTP: result %e (http %i) in %i ms (%i bytes)", + &req->result, &req->statusCode, &elapsed, &req->size); + + Http_FinishRequest(req); +} + +static void ClearCurrentRequest(void) { + Mutex_Lock(curRequestMutex); + { + http_curRequest.id = 0; + http_curRequest.progress = HTTP_PROGRESS_NOT_WORKING_ON; + } + Mutex_Unlock(curRequestMutex); +} + +static void DoRequest(struct HttpRequest* request) { + char urlBuffer[URL_MAX_SIZE]; cc_string url; + + String_InitArray(url, urlBuffer); + PrepareCurrentRequest(request, &url); + PerformRequest(&http_curRequest, &url); + ClearCurrentRequest(); +} + +static void WorkerLoop(void) { + struct HttpRequest request; + cc_bool hasRequest; + + for (;;) { + hasRequest = false; + + Mutex_Lock(pendingMutex); + { + if (pendingReqs.count) { + HttpRequest_Copy(&request, &pendingReqs.entries[0]); + hasRequest = true; + RequestList_RemoveAt(&pendingReqs, 0); + } + } + Mutex_Unlock(pendingMutex); + + if (hasRequest) { + DoRequest(&request); + } else { + /* Block until another thread submits a request to do */ + Platform_LogConst("Going back to sleep..."); + Waitable_Wait(workerWaitable); + } + } +} + +/* Adds a req to the list of pending requests, waking up worker thread if needed */ +static void HttpBackend_Add(struct HttpRequest* req, cc_uint8 flags) { +#if defined CC_BUILD_PSP || defined CC_BUILD_NDS + /* TODO why doesn't threading work properly on PSP */ + DoRequest(req); +#else + Mutex_Lock(pendingMutex); + { + RequestList_Append(&pendingReqs, req, flags); + } + Mutex_Unlock(pendingMutex); + Waitable_Signal(workerWaitable); +#endif +} + +/*########################################################################################################################* +*-----------------------------------------------------Http component------------------------------------------------------* +*#########################################################################################################################*/ +static void Http_Init(void) { + Http_InitCommon(); + http_curRequest.progress = HTTP_PROGRESS_NOT_WORKING_ON; + /* Http component gets initialised multiple times on Android */ + if (workerThread) return; + + HttpBackend_Init(); + RequestList_Init(&pendingReqs); + RequestList_Init(&processedReqs); + + workerWaitable = Waitable_Create("HTTP wakeup"); + pendingMutex = Mutex_Create("HTTP pending"); + processedMutex = Mutex_Create("HTTP processed"); + curRequestMutex = Mutex_Create("HTTP current"); + + Thread_Run(&workerThread, WorkerLoop, 128 * 1024, "HTTP"); +} +#endif |