Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add HttpRequest method #2

Merged
merged 6 commits into from
Oct 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions GlobalMethods.h
Original file line number Diff line number Diff line change
Expand Up @@ -2636,6 +2636,90 @@ namespace LuaGlobalFunctions
return 0;
}

/**
* Performs a non-blocking HTTP request.
*
* When the passed callback function is called, the parameters `(status, body, headers)` are passed to it.
*
* -- GET example (prints a random word)
* HttpRequest("GET", "https://random-word-api.herokuapp.com/word", function(status, body, headers)
* print("Random word: " .. string.sub(body, 3, body:len() - 2))
* end)
*
* -- POST example with JSON request body
* HttpRequest("POST", "https://jsonplaceholder.typicode.com/posts", '{"userId": 1,"title": "Foo","body": "Bar!"}', "application/json", function(status, body, headers)
* print(body)
* end)
*
* -- Example with request headers
* HttpRequest("GET", "https://postman-echo.com/headers", { Accept = "application/json", ["User-Agent"] = "Eluna Lua Engine" }, function(status, body, headers)
* print(body)
* end)
*
* @proto (httpMethod, url, function)
* @proto (httpMethod, url, headers, function)
* @proto (httpMethod, url, body, contentType, function)
* @proto (httpMethod, url, body, contentType, headers, function)
*
* @param string httpMethod : the HTTP method to use (possible values are: `"GET"`, `"HEAD"`, `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`, `"OPTIONS"`)
* @param string url : the URL to query
* @param table headers : a table with string key-value pairs containing the request headers
* @param string body : the request's body (only used for POST, PUT and PATCH requests)
* @param string contentType : the body's content-type
* @param function function : function that will be called when the request is executed
*/
int HttpRequest(lua_State* L)
{
std::string httpVerb = Eluna::CHECKVAL<std::string>(L, 1);
std::string url = Eluna::CHECKVAL<std::string>(L, 2);
std::string body;
std::string bodyContentType;
httplib::Headers headers;

int headersIdx = 3;
int callbackIdx = 3;

if (!lua_istable(L, headersIdx) && lua_isstring(L, headersIdx) && lua_isstring(L, headersIdx + 1))
{
body = Eluna::CHECKVAL<std::string>(L, 3);
bodyContentType = Eluna::CHECKVAL<std::string>(L, 4);
headersIdx = 5;
callbackIdx = 5;
}

if (lua_istable(L, headersIdx))
{
++callbackIdx;

lua_pushnil(L); // First key
while (lua_next(L, headersIdx) != 0)
{
// Uses 'key' (at index -2) and 'value' (at index -1)
if (lua_isstring(L, -2))
{
std::string key(lua_tostring(L, -2));
std::string value(lua_tostring(L, -1));
headers.insert(std::pair<std::string, std::string>(key, value));
}
// Removes 'value'; keeps 'key' for next iteration
lua_pop(L, 1);
}
}

lua_pushvalue(L, callbackIdx);
int funcRef = luaL_ref(L, LUA_REGISTRYINDEX);
if (funcRef >= 0)
{
Eluna::GEluna->httpManager.PushRequest(new HttpWorkItem(funcRef, httpVerb, url, body, bodyContentType, headers));
}
else
{
luaL_argerror(L, callbackIdx, "unable to make a ref to function");
}

return 0;
}

/**
* Returns an object representing a `long long` (64-bit) value.
*
Expand Down
276 changes: 276 additions & 0 deletions HttpManager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
#include <thread>
extern "C"
{
#include "lua.h"
#include "lauxlib.h"
};

#if defined TRINITY || defined AZEROTHCORE
#define CPPHTTPLIB_OPENSSL_SUPPORT
#endif
#include "libs/httplib.h"
#include "HttpManager.h"
#include "LuaEngine.h"

HttpWorkItem::HttpWorkItem(int funcRef, const std::string& httpVerb, const std::string& url, const std::string& body, const std::string& contentType, const httplib::Headers& headers)
: funcRef(funcRef),
httpVerb(httpVerb),
url(url),
body(body),
contentType(contentType),
headers(headers)
{ }

HttpResponse::HttpResponse(int funcRef, int statusCode, const std::string& body, const httplib::Headers& headers)
: funcRef(funcRef),
statusCode(statusCode),
body(body),
headers(headers)
{ }

HttpManager::HttpManager()
: workQueue(16),
responseQueue(16),
startedWorkerThread(false),
cancelationToken(false),
condVar(),
condVarMutex(),
parseUrlRegex("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?")
{
StartHttpWorker();
}

HttpManager::~HttpManager()
{
StopHttpWorker();
}

void HttpManager::PushRequest(HttpWorkItem* item)
{
std::unique_lock<std::mutex> lock(condVarMutex);
workQueue.push(item);
condVar.notify_one();
}

void HttpManager::StartHttpWorker()
{
ClearQueues();

if (!startedWorkerThread)
{
cancelationToken.store(false);
workerThread = std::thread(&HttpManager::HttpWorkerThread, this);
startedWorkerThread = true;
}
}

void HttpManager::ClearQueues()
{
while (workQueue.front())
{
HttpWorkItem* item = *workQueue.front();
if (item != nullptr)
{
delete item;
}
workQueue.pop();
}

while (responseQueue.front())
{
HttpResponse* item = *responseQueue.front();
if (item != nullptr)
{
delete item;
}
responseQueue.pop();
}
}

void HttpManager::StopHttpWorker()
{
if (!startedWorkerThread)
{
return;
}

cancelationToken.store(true);
condVar.notify_one();
workerThread.join();
ClearQueues();
startedWorkerThread = false;
}

void HttpManager::HttpWorkerThread()
{
while (true)
{
{
std::unique_lock<std::mutex> lock(condVarMutex);
condVar.wait(lock, [&] { return workQueue.front() != nullptr || cancelationToken.load(); });
}

if (cancelationToken.load())
{
break;
}
if (!workQueue.front())
{
continue;
}

HttpWorkItem* req = *workQueue.front();
workQueue.pop();
if (!req)
{
continue;
}

try
{
std::string host;
std::string path;

if (!ParseUrl(req->url, host, path)) {
ELUNA_LOG_ERROR("[Eluna]: Could not parse URL %s", req->url.c_str());
continue;
}

httplib::Client cli(host);
cli.set_connection_timeout(0, 3000000); // 3 seconds
cli.set_read_timeout(5, 0); // 5 seconds
cli.set_write_timeout(5, 0); // 5 seconds

httplib::Result res = DoRequest(cli, req, path);
httplib::Error err = res.error();
if (err != httplib::Error::Success)
{
ELUNA_LOG_ERROR("[Eluna]: HTTP request error: %s", httplib::to_string(err).c_str());
continue;
}

if (res->status == 301)
{
std::string location = res->get_header_value("Location");
std::string host;
std::string path;

if (!ParseUrl(location, host, path))
{
ELUNA_LOG_ERROR("[Eluna]: Could not parse URL after redirect: %s", location.c_str());
continue;
}
httplib::Client cli2(host);
cli2.set_connection_timeout(0, 3000000); // 3 seconds
cli2.set_read_timeout(5, 0); // 5 seconds
cli2.set_write_timeout(5, 0); // 5 seconds
res = DoRequest(cli2, req, path);
}

responseQueue.push(new HttpResponse(req->funcRef, res->status, res->body, res->headers));
}
catch (const std::exception& ex)
{
ELUNA_LOG_ERROR("[Eluna]: HTTP request error: %s", ex.what());
}

delete req;
}
}

httplib::Result HttpManager::DoRequest(httplib::Client& client, HttpWorkItem* req, const std::string& urlPath)
{
const char* path = urlPath.c_str();
if (req->httpVerb == "GET")
{
return client.Get(path, req->headers);
}
if (req->httpVerb == "HEAD")
{
return client.Head(path, req->headers);
}
if (req->httpVerb == "POST")
{
return client.Post(path, req->headers, req->body, req->contentType.c_str());
}
if (req->httpVerb == "PUT")
{
return client.Put(path, req->headers, req->body, req->contentType.c_str());
}
if (req->httpVerb == "PATCH")
{
return client.Patch(path, req->headers, req->body, req->contentType.c_str());
}
if (req->httpVerb == "DELETE")
{
return client.Delete(path, req->headers);
}
if (req->httpVerb == "OPTIONS")
{
return client.Options(path, req->headers);
}

ELUNA_LOG_ERROR("[Eluna]: HTTP request error: invalid HTTP verb %s", req->httpVerb.c_str());
return client.Get(path, req->headers);
}

bool HttpManager::ParseUrl(const std::string& url, std::string& host, std::string& path)
{
std::smatch matches;

if (!std::regex_search(url, matches, parseUrlRegex))
{
return false;
}

std::string scheme = matches[2];
std::string authority = matches[4];
std::string query = matches[7];
host = scheme + "://" + authority;
path = matches[5];
if (path.empty())
{
path = "/";
}
path += (query.empty() ? "" : "?") + query;

return true;
}

void HttpManager::HandleHttpResponses()
{
while (!responseQueue.empty())
{
HttpResponse* res = *responseQueue.front();
responseQueue.pop();

if (res == nullptr)
{
continue;
}

LOCK_ELUNA;

lua_State* L = Eluna::GEluna->L;

// Get function
lua_rawgeti(L, LUA_REGISTRYINDEX, res->funcRef);

// Push parameters
Eluna::Push(L, res->statusCode);
Eluna::Push(L, res->body);
lua_newtable(L);
for (const auto& item : res->headers) {
Eluna::Push(L, item.first);
Eluna::Push(L, item.second);
lua_settable(L, -3);
}

// Call function
Eluna::GEluna->ExecuteCall(3, 0);

luaL_unref(L, LUA_REGISTRYINDEX, res->funcRef);

delete res;
}
}
Loading