mirror of https://github.com/ClassiCube/ClassiCube
843 lines
26 KiB
C
843 lines
26 KiB
C
#include "Core.h"
|
|
|
|
#if defined CC_BUILD_WEB
|
|
/* Implemented in web http backend without C worker thread(s) */
|
|
#elif !defined CC_BUILD_NETWORKING
|
|
#include "Http.h"
|
|
#include "Game.h"
|
|
|
|
void HttpRequest_Free(struct HttpRequest* request) { }
|
|
|
|
cc_bool Http_GetResult(int reqID, struct HttpRequest* item) {
|
|
return false;
|
|
}
|
|
|
|
int Http_AsyncGetSkin(const cc_string* skinName, cc_uint8 flags) {
|
|
return -1;
|
|
}
|
|
int Http_AsyncGetData(const cc_string* url, cc_uint8 flags) {
|
|
return -1;
|
|
}
|
|
int Http_AsyncGetHeaders(const cc_string* url, cc_uint8 flags) {
|
|
return -1;
|
|
}
|
|
int Http_AsyncPostData(const cc_string* url, cc_uint8 flags, const void* data, cc_uint32 size, struct StringsBuffer* cookies) {
|
|
return -1;
|
|
}
|
|
int Http_AsyncGetDataEx(const cc_string* url, cc_uint8 flags, const cc_string* lastModified, const cc_string* etag, struct StringsBuffer* cookies) {
|
|
return -1;
|
|
}
|
|
|
|
int Http_CheckProgress(int reqID) {
|
|
return -1;
|
|
}
|
|
|
|
void Http_LogError(const char* action, const struct HttpRequest* item) { }
|
|
|
|
void Http_TryCancel(int reqID) { }
|
|
|
|
void Http_UrlEncodeUtf8(cc_string* dst, const cc_string* src) { }
|
|
|
|
static void Http_NullInit(void) { }
|
|
|
|
struct IGameComponent Http_Component = {
|
|
Http_NullInit
|
|
};
|
|
#else
|
|
#include "_HttpBase.h"
|
|
|
|
#if CC_NET_BACKEND == CC_NET_BACKEND_BUILTIN
|
|
#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_Create(&conn->socket, &addrs[i], false);
|
|
if (res) { HttpConnection_Close(conn); continue; }
|
|
|
|
res = Socket_Connect(conn->socket, &addrs[i]);
|
|
if (res) { HttpConnection_Close(conn); continue; }
|
|
|
|
break; /* Successful connection */
|
|
}
|
|
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_LOCATION_MAX_LENGTH 256
|
|
|
|
#if CC_BUILD_MAXSTACK <= (48 * 1024)
|
|
#define HTTP_HEADER_MAX_LENGTH 2048
|
|
#define INPUT_BUFFER_LEN 4096
|
|
#define SEND_BUFFER_LEN 4096
|
|
#else
|
|
#define HTTP_HEADER_MAX_LENGTH 4096
|
|
#define INPUT_BUFFER_LEN 8192
|
|
#define SEND_BUFFER_LEN 16384
|
|
#endif
|
|
|
|
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[SEND_BUFFER_LEN];
|
|
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)
|
|
return HTTP_RESPONSE_STATE_CHUNK_HEADER;
|
|
if (req->contentLength)
|
|
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, ok;
|
|
|
|
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) {
|
|
state->dataLeft = req->contentLength;
|
|
ok = Http_BufferExpand(req, state->dataLeft);
|
|
if (!ok) return ERR_OUT_OF_MEMORY;
|
|
}
|
|
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;
|
|
|
|
state->dataLeft = chunkLen;
|
|
ok = Http_BufferExpand(req, state->dataLeft);
|
|
if (!ok) return ERR_OUT_OF_MEMORY;
|
|
}
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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 == ReturnCode_SocketDropped && !retried) {
|
|
Platform_LogConst("Resetting connection due to being dropped..");
|
|
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);
|
|
}
|
|
#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("Download queue empty, 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
|