#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 /* === 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 #include 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