Use name-value pairs for HTTP post data and headers
And fuse them only if needed (e.g. in Libcurl.cpp). Also finally stop specifying the filename for a form item with the : separator hack.
This commit is contained in:
parent
5c816fe1ee
commit
a860cbeabf
@ -1,11 +1,23 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "common/String.h"
|
#include "common/String.h"
|
||||||
#include <map>
|
#include <vector>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace http
|
namespace http
|
||||||
{
|
{
|
||||||
|
struct Header
|
||||||
|
{
|
||||||
|
ByteString name;
|
||||||
|
ByteString value;
|
||||||
|
};
|
||||||
|
struct FormItem
|
||||||
|
{
|
||||||
|
ByteString name;
|
||||||
|
ByteString value;
|
||||||
|
std::optional<ByteString> filename;
|
||||||
|
};
|
||||||
using StringData = ByteString;
|
using StringData = ByteString;
|
||||||
using FormData = std::map<ByteString, ByteString>;
|
using FormData = std::vector<FormItem>;
|
||||||
using PostData = std::variant<StringData, FormData>;
|
using PostData = std::variant<StringData, FormData>;
|
||||||
};
|
};
|
||||||
|
@ -43,7 +43,7 @@ namespace http
|
|||||||
handle->verb = newVerb;
|
handle->verb = newVerb;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Request::AddHeader(ByteString header)
|
void Request::AddHeader(Header header)
|
||||||
{
|
{
|
||||||
assert(handle->state == RequestHandle::ready);
|
assert(handle->state == RequestHandle::ready);
|
||||||
handle->headers.push_back(header);
|
handle->headers.push_back(header);
|
||||||
@ -64,12 +64,12 @@ namespace http
|
|||||||
{
|
{
|
||||||
if (session.size())
|
if (session.size())
|
||||||
{
|
{
|
||||||
AddHeader("X-Auth-User-Id: " + ID);
|
AddHeader({ "X-Auth-User-Id", ID });
|
||||||
AddHeader("X-Auth-Session-Key: " + session);
|
AddHeader({ "X-Auth-Session-Key", session });
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddHeader("X-Auth-User: " + ID);
|
AddHeader({ "X-Auth-User", ID });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,7 +95,7 @@ namespace http
|
|||||||
return { handle->bytesTotal, handle->bytesDone };
|
return { handle->bytesTotal, handle->bytesDone };
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<ByteString> &Request::ResponseHeaders() const
|
const std::vector<Header> &Request::ResponseHeaders() const
|
||||||
{
|
{
|
||||||
std::lock_guard lk(handle->stateMx);
|
std::lock_guard lk(handle->stateMx);
|
||||||
assert(handle->state == RequestHandle::done);
|
assert(handle->state == RequestHandle::done);
|
||||||
|
@ -31,7 +31,7 @@ namespace http
|
|||||||
void FailEarly(ByteString error);
|
void FailEarly(ByteString error);
|
||||||
|
|
||||||
void Verb(ByteString newVerb);
|
void Verb(ByteString newVerb);
|
||||||
void AddHeader(ByteString header);
|
void AddHeader(Header header);
|
||||||
|
|
||||||
void AddPostData(PostData data);
|
void AddPostData(PostData data);
|
||||||
void AuthHeaders(ByteString ID, ByteString session);
|
void AuthHeaders(ByteString ID, ByteString session);
|
||||||
@ -40,7 +40,7 @@ namespace http
|
|||||||
bool CheckDone() const;
|
bool CheckDone() const;
|
||||||
|
|
||||||
std::pair<int64_t, int64_t> CheckProgress() const; // total, done
|
std::pair<int64_t, int64_t> CheckProgress() const; // total, done
|
||||||
const std::vector<ByteString> &ResponseHeaders() const;
|
const std::vector<Header> &ResponseHeaders() const;
|
||||||
void Wait();
|
void Wait();
|
||||||
|
|
||||||
int StatusCode() const; // status
|
int StatusCode() const; // status
|
||||||
|
@ -29,7 +29,7 @@ namespace http
|
|||||||
AddPostData(FormData{
|
AddPostData(FormData{
|
||||||
{ "Name", saveInfo.GetName().ToUtf8() },
|
{ "Name", saveInfo.GetName().ToUtf8() },
|
||||||
{ "Description", saveInfo.GetDescription().ToUtf8() },
|
{ "Description", saveInfo.GetDescription().ToUtf8() },
|
||||||
{ "Data:save.bin", ByteString(gameData.begin(), gameData.end()) },
|
{ "Data", ByteString(gameData.begin(), gameData.end()), "save.bin" },
|
||||||
{ "Publish", saveInfo.GetPublished() ? "Public" : "Private" },
|
{ "Publish", saveInfo.GetPublished() ? "Public" : "Private" },
|
||||||
{ "Key", user.SessionKey },
|
{ "Key", user.SessionKey },
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#include "client/http/Request.h"
|
#include "client/http/Request.h"
|
||||||
#include "CurlError.h"
|
#include "CurlError.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
#if defined(CURL_AT_LEAST_VERSION) && CURL_AT_LEAST_VERSION(7, 55, 0)
|
#if defined(CURL_AT_LEAST_VERSION) && CURL_AT_LEAST_VERSION(7, 55, 0)
|
||||||
# define REQUEST_USE_CURL_OFFSET_T
|
# define REQUEST_USE_CURL_OFFSET_T
|
||||||
@ -58,6 +59,7 @@ namespace http
|
|||||||
CURL *curlEasy = NULL;
|
CURL *curlEasy = NULL;
|
||||||
char curlErrorBuffer[CURL_ERROR_SIZE];
|
char curlErrorBuffer[CURL_ERROR_SIZE];
|
||||||
bool curlAddedToMulti = false;
|
bool curlAddedToMulti = false;
|
||||||
|
bool gotStatusLine = false;
|
||||||
|
|
||||||
RequestHandleHttp() : RequestHandle(CtorTag{})
|
RequestHandleHttp() : RequestHandle(CtorTag{})
|
||||||
{
|
{
|
||||||
@ -69,10 +71,28 @@ namespace http
|
|||||||
auto bytes = size * count;
|
auto bytes = size * count;
|
||||||
if (bytes >= 2 && ptr[bytes - 2] == '\r' && ptr[bytes - 1] == '\n')
|
if (bytes >= 2 && ptr[bytes - 2] == '\r' && ptr[bytes - 1] == '\n')
|
||||||
{
|
{
|
||||||
if (bytes > 2) // Don't include header list terminator (but include the status line).
|
if (bytes > 2 && handle->gotStatusLine) // Don't include header list terminator or the status line.
|
||||||
{
|
{
|
||||||
handle->responseHeaders.push_back(ByteString(ptr, ptr + bytes - 2));
|
auto line = ByteString(ptr, ptr + bytes - 2);
|
||||||
|
if (auto split = line.SplitBy(':'))
|
||||||
|
{
|
||||||
|
auto value = split.After();
|
||||||
|
while (value.size() && (value.front() == ' ' || value.front() == '\t'))
|
||||||
|
{
|
||||||
|
value = value.Substr(1);
|
||||||
|
}
|
||||||
|
while (value.size() && (value.back() == ' ' || value.back() == '\t'))
|
||||||
|
{
|
||||||
|
value = value.Substr(0, value.size() - 1);
|
||||||
|
}
|
||||||
|
handle->responseHeaders.push_back({ split.Before().ToLower(), value });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cerr << "skipping weird header: " << line << std::endl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
handle->gotStatusLine = true;
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@ -327,7 +347,7 @@ namespace http
|
|||||||
}
|
}
|
||||||
for (auto &header : handle->headers)
|
for (auto &header : handle->headers)
|
||||||
{
|
{
|
||||||
auto *newHeaders = curl_slist_append(handle->curlHeaders, header.c_str());
|
auto *newHeaders = curl_slist_append(handle->curlHeaders, (header.name + ": " + header.value).c_str());
|
||||||
if (!newHeaders)
|
if (!newHeaders)
|
||||||
{
|
{
|
||||||
// Hopefully this is what a NULL from curl_slist_append means.
|
// Hopefully this is what a NULL from curl_slist_append means.
|
||||||
@ -355,36 +375,32 @@ namespace http
|
|||||||
// Hopefully this is what a NULL from curl_mime_addpart means.
|
// Hopefully this is what a NULL from curl_mime_addpart means.
|
||||||
HandleCURLcode(CURLE_OUT_OF_MEMORY);
|
HandleCURLcode(CURLE_OUT_OF_MEMORY);
|
||||||
}
|
}
|
||||||
HandleCURLcode(curl_mime_data(part, &field.second[0], field.second.size()));
|
HandleCURLcode(curl_mime_data(part, &field.value[0], field.value.size()));
|
||||||
if (auto split = field.first.SplitBy(':'))
|
HandleCURLcode(curl_mime_name(part, field.name.c_str()));
|
||||||
|
if (field.filename.has_value())
|
||||||
{
|
{
|
||||||
HandleCURLcode(curl_mime_name(part, split.Before().c_str()));
|
HandleCURLcode(curl_mime_filename(part, field.filename->c_str()));
|
||||||
HandleCURLcode(curl_mime_filename(part, split.After().c_str()));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HandleCURLcode(curl_mime_name(part, field.first.c_str()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HandleCURLcode(curl_easy_setopt(handle->curlEasy, CURLOPT_MIMEPOST, handle->curlPostFields));
|
HandleCURLcode(curl_easy_setopt(handle->curlEasy, CURLOPT_MIMEPOST, handle->curlPostFields));
|
||||||
#else
|
#else
|
||||||
for (auto &field : formData)
|
for (auto &field : formData)
|
||||||
{
|
{
|
||||||
if (auto split = field.first.SplitBy(':'))
|
if (field.filename.has_value())
|
||||||
{
|
{
|
||||||
HandleCURLFORMcode(curl_formadd(&handle->curlPostFieldsFirst, &handle->curlPostFieldsLast,
|
HandleCURLFORMcode(curl_formadd(&handle->curlPostFieldsFirst, &handle->curlPostFieldsLast,
|
||||||
CURLFORM_COPYNAME, split.Before().c_str(),
|
CURLFORM_COPYNAME, field.name.c_str(),
|
||||||
CURLFORM_BUFFER, split.After().c_str(),
|
CURLFORM_BUFFER, field.filename->c_str(),
|
||||||
CURLFORM_BUFFERPTR, &field.second[0],
|
CURLFORM_BUFFERPTR, &field.value[0],
|
||||||
CURLFORM_BUFFERLENGTH, field.second.size(),
|
CURLFORM_BUFFERLENGTH, field.value.size(),
|
||||||
CURLFORM_END));
|
CURLFORM_END));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
HandleCURLFORMcode(curl_formadd(&handle->curlPostFieldsFirst, &handle->curlPostFieldsLast,
|
HandleCURLFORMcode(curl_formadd(&handle->curlPostFieldsFirst, &handle->curlPostFieldsLast,
|
||||||
CURLFORM_COPYNAME, field.first.c_str(),
|
CURLFORM_COPYNAME, field.name.c_str(),
|
||||||
CURLFORM_PTRCONTENTS, &field.second[0],
|
CURLFORM_PTRCONTENTS, &field.value[0],
|
||||||
CURLFORM_CONTENTLEN, field.second.size(),
|
CURLFORM_CONTENTLEN, field.value.size(),
|
||||||
CURLFORM_END));
|
CURLFORM_END));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -406,9 +422,9 @@ namespace http
|
|||||||
{
|
{
|
||||||
HandleCURLcode(curl_easy_setopt(handle->curlEasy, CURLOPT_HTTPGET, 1L));
|
HandleCURLcode(curl_easy_setopt(handle->curlEasy, CURLOPT_HTTPGET, 1L));
|
||||||
}
|
}
|
||||||
if (handle->verb.size())
|
if (handle->verb)
|
||||||
{
|
{
|
||||||
HandleCURLcode(curl_easy_setopt(handle->curlEasy, CURLOPT_CUSTOMREQUEST, handle->verb.c_str()));
|
HandleCURLcode(curl_easy_setopt(handle->curlEasy, CURLOPT_CUSTOMREQUEST, handle->verb->c_str()));
|
||||||
}
|
}
|
||||||
HandleCURLcode(curl_easy_setopt(handle->curlEasy, CURLOPT_FOLLOWLOCATION, 1L));
|
HandleCURLcode(curl_easy_setopt(handle->curlEasy, CURLOPT_FOLLOWLOCATION, 1L));
|
||||||
if constexpr (ENFORCE_HTTPS)
|
if constexpr (ENFORCE_HTTPS)
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace http
|
namespace http
|
||||||
{
|
{
|
||||||
@ -24,10 +25,10 @@ namespace http
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
ByteString uri;
|
ByteString uri;
|
||||||
ByteString verb;
|
std::optional<ByteString> verb;
|
||||||
bool isPost = false;
|
bool isPost = false;
|
||||||
PostData postData;
|
PostData postData;
|
||||||
std::vector<ByteString> headers;
|
std::vector<Header> headers;
|
||||||
|
|
||||||
enum State
|
enum State
|
||||||
{
|
{
|
||||||
@ -43,7 +44,7 @@ namespace http
|
|||||||
std::atomic<int64_t> bytesDone = 0;
|
std::atomic<int64_t> bytesDone = 0;
|
||||||
int statusCode = 0;
|
int statusCode = 0;
|
||||||
ByteString responseData;
|
ByteString responseData;
|
||||||
std::vector<ByteString> responseHeaders;
|
std::vector<Header> responseHeaders;
|
||||||
std::optional<ByteString> error;
|
std::optional<ByteString> error;
|
||||||
std::optional<ByteString> failEarly;
|
std::optional<ByteString> failEarly;
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static int Make(lua_State *l, const ByteString &uri, bool isPost, const ByteString &verb, RequestType type, const http::PostData &postData, const std::vector<ByteString> &headers)
|
static int Make(lua_State *l, const ByteString &uri, bool isPost, const ByteString &verb, RequestType type, const http::PostData &postData, const std::vector<http::Header> &headers)
|
||||||
{
|
{
|
||||||
auto authUser = Client::Ref().GetAuthUser();
|
auto authUser = Client::Ref().GetAuthUser();
|
||||||
if (type == getAuthToken && !authUser.UserID)
|
if (type == getAuthToken && !authUser.UserID)
|
||||||
@ -67,7 +67,7 @@ public:
|
|||||||
{
|
{
|
||||||
rh->request->Verb(verb);
|
rh->request->Verb(verb);
|
||||||
}
|
}
|
||||||
for (auto &header : headers)
|
for (const auto &header : headers)
|
||||||
{
|
{
|
||||||
rh->request->AddHeader(header);
|
rh->request->AddHeader(header);
|
||||||
}
|
}
|
||||||
@ -120,7 +120,7 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<int, ByteString> Finish(std::vector<ByteString> &headers)
|
std::pair<int, ByteString> Finish(std::vector<http::Header> &headers)
|
||||||
{
|
{
|
||||||
int status = 0;
|
int status = 0;
|
||||||
ByteString data;
|
ByteString data;
|
||||||
@ -212,14 +212,18 @@ static int http_request_finish(lua_State *l)
|
|||||||
auto *rh = (RequestHandle *)luaL_checkudata(l, 1, "HTTPRequest");
|
auto *rh = (RequestHandle *)luaL_checkudata(l, 1, "HTTPRequest");
|
||||||
if (!rh->Dead())
|
if (!rh->Dead())
|
||||||
{
|
{
|
||||||
std::vector<ByteString> headers;
|
std::vector<http::Header> headers;
|
||||||
auto [ status, data ] = rh->Finish(headers);
|
auto [ status, data ] = rh->Finish(headers);
|
||||||
tpt_lua_pushByteString(l, data);
|
tpt_lua_pushByteString(l, data);
|
||||||
lua_pushinteger(l, status);
|
lua_pushinteger(l, status);
|
||||||
lua_newtable(l);
|
lua_newtable(l);
|
||||||
for (auto i = 0; i < int(headers.size()); ++i)
|
for (auto i = 0; i < int(headers.size()); ++i)
|
||||||
{
|
{
|
||||||
lua_pushlstring(l, headers[i].data(), headers[i].size());
|
lua_newtable(l);
|
||||||
|
lua_pushlstring(l, headers[i].name.data(), headers[i].name.size());
|
||||||
|
lua_rawseti(l, -2, 1);
|
||||||
|
lua_pushlstring(l, headers[i].value.data(), headers[i].value.size());
|
||||||
|
lua_rawseti(l, -2, 1);
|
||||||
lua_rawseti(l, -2, i + 1);
|
lua_rawseti(l, -2, i + 1);
|
||||||
}
|
}
|
||||||
return 3;
|
return 3;
|
||||||
@ -246,17 +250,59 @@ static int http_request(lua_State *l, bool isPost)
|
|||||||
{
|
{
|
||||||
postData = http::FormData{};
|
postData = http::FormData{};
|
||||||
auto &formData = std::get<http::FormData>(postData);
|
auto &formData = std::get<http::FormData>(postData);
|
||||||
lua_pushnil(l);
|
auto size = lua_objlen(l, headersIndex);
|
||||||
while (lua_next(l, 2))
|
if (size)
|
||||||
{
|
{
|
||||||
lua_pushvalue(l, -2);
|
for (auto i = 0U; i < size; ++i)
|
||||||
formData.emplace(tpt_lua_toByteString(l, -1), tpt_lua_toByteString(l, -2));
|
{
|
||||||
lua_pop(l, 2);
|
lua_rawgeti(l, headersIndex, i + 1);
|
||||||
|
if (!lua_istable(l, -1))
|
||||||
|
{
|
||||||
|
luaL_error(l, "form item %i is not a table", i + 1);
|
||||||
|
}
|
||||||
|
lua_rawgeti(l, -1, 1);
|
||||||
|
if (!lua_isstring(l, -1))
|
||||||
|
{
|
||||||
|
luaL_error(l, "name of form item %i is not a string", i + 1);
|
||||||
|
}
|
||||||
|
auto name = tpt_lua_toByteString(l, -1);
|
||||||
|
lua_pop(l, 1);
|
||||||
|
lua_rawgeti(l, -1, 2);
|
||||||
|
if (!lua_isstring(l, -1))
|
||||||
|
{
|
||||||
|
luaL_error(l, "value of form item %i is not a string", i + 1);
|
||||||
|
}
|
||||||
|
auto value = tpt_lua_toByteString(l, -1);
|
||||||
|
lua_pop(l, 1);
|
||||||
|
std::optional<ByteString> filename;
|
||||||
|
lua_rawgeti(l, -1, 3);
|
||||||
|
if (!lua_isnoneornil(l, -1))
|
||||||
|
{
|
||||||
|
if (!lua_isstring(l, -1))
|
||||||
|
{
|
||||||
|
luaL_error(l, "filename of form item %i is not a string", i + 1);
|
||||||
|
}
|
||||||
|
filename = tpt_lua_toByteString(l, -1);
|
||||||
|
}
|
||||||
|
lua_pop(l, 1);
|
||||||
|
formData.push_back({ name, value, filename });
|
||||||
|
lua_pop(l, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lua_pushnil(l);
|
||||||
|
while (lua_next(l, 2))
|
||||||
|
{
|
||||||
|
lua_pushvalue(l, -2);
|
||||||
|
formData.push_back({ tpt_lua_toByteString(l, -1), tpt_lua_toByteString(l, -2) });
|
||||||
|
lua_pop(l, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ByteString> headers;
|
std::vector<http::Header> headers;
|
||||||
if (lua_istable(l, headersIndex))
|
if (lua_istable(l, headersIndex))
|
||||||
{
|
{
|
||||||
auto size = lua_objlen(l, headersIndex);
|
auto size = lua_objlen(l, headersIndex);
|
||||||
@ -265,7 +311,25 @@ static int http_request(lua_State *l, bool isPost)
|
|||||||
for (auto i = 0U; i < size; ++i)
|
for (auto i = 0U; i < size; ++i)
|
||||||
{
|
{
|
||||||
lua_rawgeti(l, headersIndex, i + 1);
|
lua_rawgeti(l, headersIndex, i + 1);
|
||||||
headers.push_back(tpt_lua_toByteString(l, -1));
|
if (!lua_istable(l, -1))
|
||||||
|
{
|
||||||
|
luaL_error(l, "header %i is not a table", i + 1);
|
||||||
|
}
|
||||||
|
lua_rawgeti(l, -1, 1);
|
||||||
|
if (!lua_isstring(l, -1))
|
||||||
|
{
|
||||||
|
luaL_error(l, "name of header %i is not a string", i + 1);
|
||||||
|
}
|
||||||
|
auto name = tpt_lua_toByteString(l, -1);
|
||||||
|
lua_pop(l, 1);
|
||||||
|
lua_rawgeti(l, -1, 2);
|
||||||
|
if (!lua_isstring(l, -1))
|
||||||
|
{
|
||||||
|
luaL_error(l, "value of header %i is not a string", i + 1);
|
||||||
|
}
|
||||||
|
auto value = tpt_lua_toByteString(l, -1);
|
||||||
|
lua_pop(l, 1);
|
||||||
|
headers.push_back({ name, value });
|
||||||
lua_pop(l, 1);
|
lua_pop(l, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -276,7 +340,7 @@ static int http_request(lua_State *l, bool isPost)
|
|||||||
while (lua_next(l, headersIndex))
|
while (lua_next(l, headersIndex))
|
||||||
{
|
{
|
||||||
lua_pushvalue(l, -2);
|
lua_pushvalue(l, -2);
|
||||||
headers.push_back(tpt_lua_toByteString(l, -1) + ByteString(": ") + tpt_lua_toByteString(l, -2));
|
headers.push_back({ tpt_lua_toByteString(l, -1), tpt_lua_toByteString(l, -2) });
|
||||||
lua_pop(l, 2);
|
lua_pop(l, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user