Merge pull request #2 from r-o-b-o-t-o/feat/http-requests

feat: add HttpRequest method
This commit is contained in:
55Honey
2021-10-20 21:11:21 +02:00
committed by GitHub
9 changed files with 8487 additions and 1 deletions

View File

@@ -2637,6 +2637,90 @@ namespace LuaGlobalFunctions
return 0; 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. * Returns an object representing a `long long` (64-bit) value.
* *

276
HttpManager.cpp Normal file
View File

@@ -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;
}
}

61
HttpManager.h Normal file
View File

@@ -0,0 +1,61 @@
#ifndef ELUNA_HTTP_MANAGER_H
#define ELUNA_HTTP_MANAGER_H
#include <regex>
#include "libs/httplib.h"
#include "libs/rigtorp/SPSCQueue.h"
struct HttpWorkItem
{
public:
HttpWorkItem(int funcRef, const std::string& httpVerb, const std::string& url, const std::string& body, const std::string &contentType, const httplib::Headers& headers);
int funcRef;
std::string httpVerb;
std::string url;
std::string body;
std::string contentType;
httplib::Headers headers;
};
struct HttpResponse
{
public:
HttpResponse(int funcRef, int statusCode, const std::string& body, const httplib::Headers& headers);
int funcRef;
int statusCode;
std::string body;
httplib::Headers headers;
};
class HttpManager
{
public:
HttpManager();
~HttpManager();
void StartHttpWorker();
void StopHttpWorker();
void PushRequest(HttpWorkItem* item);
void HandleHttpResponses();
private:
void ClearQueues();
void HttpWorkerThread();
bool ParseUrl(const std::string& url, std::string& host, std::string& path);
httplib::Result DoRequest(httplib::Client& client, HttpWorkItem* req, const std::string& path);
rigtorp::SPSCQueue<HttpWorkItem*> workQueue;
rigtorp::SPSCQueue<HttpResponse*> responseQueue;
std::thread workerThread;
bool startedWorkerThread;
std::atomic_bool cancelationToken;
std::condition_variable condVar;
std::mutex condVarMutex;
std::regex parseUrlRegex;
};
#endif // #ifndef ELUNA_HTTP_MANAGER_H

View File

@@ -155,6 +155,7 @@ enabled(false),
L(NULL), L(NULL),
eventMgr(NULL), eventMgr(NULL),
httpManager(),
ServerEventBindings(NULL), ServerEventBindings(NULL),
PlayerEventBindings(NULL), PlayerEventBindings(NULL),

View File

@@ -20,6 +20,7 @@
#include "World.h" #include "World.h"
#include "Hooks.h" #include "Hooks.h"
#include "ElunaUtility.h" #include "ElunaUtility.h"
#include "HttpManager.h"
#include <mutex> #include <mutex>
#include <memory> #include <memory>
@@ -178,7 +179,6 @@ private:
void DestroyBindStores(); void DestroyBindStores();
void CreateBindStores(); void CreateBindStores();
void InvalidateObjects(); void InvalidateObjects();
bool ExecuteCall(int params, int res);
// Use ReloadEluna() to make eluna reload // Use ReloadEluna() to make eluna reload
// This is called on world update to reload eluna // This is called on world update to reload eluna
@@ -237,6 +237,7 @@ public:
lua_State* L; lua_State* L;
EventMgr* eventMgr; EventMgr* eventMgr;
HttpManager httpManager;
BindingMap< EventKey<Hooks::ServerEvents> >* ServerEventBindings; BindingMap< EventKey<Hooks::ServerEvents> >* ServerEventBindings;
BindingMap< EventKey<Hooks::PlayerEvents> >* PlayerEventBindings; BindingMap< EventKey<Hooks::PlayerEvents> >* PlayerEventBindings;
@@ -301,6 +302,8 @@ public:
ElunaTemplate<T>::Push(luastate, ptr); ElunaTemplate<T>::Push(luastate, ptr);
} }
bool ExecuteCall(int params, int res);
/* /*
* Returns `true` if Eluna has instance data for `map`. * Returns `true` if Eluna has instance data for `map`.
*/ */

View File

@@ -145,6 +145,7 @@ luaL_Reg GlobalMethods[] =
{ "CreateUint64", &LuaGlobalFunctions::CreateULongLong }, { "CreateUint64", &LuaGlobalFunctions::CreateULongLong },
{ "StartGameEvent", &LuaGlobalFunctions::StartGameEvent }, { "StartGameEvent", &LuaGlobalFunctions::StartGameEvent },
{ "StopGameEvent", &LuaGlobalFunctions::StopGameEvent }, { "StopGameEvent", &LuaGlobalFunctions::StopGameEvent },
{ "HttpRequest", &LuaGlobalFunctions::HttpRequest },
{ NULL, NULL } { NULL, NULL }
}; };

View File

@@ -321,6 +321,7 @@ void Eluna::OnWorldUpdate(uint32 diff)
} }
eventMgr->globalProcessor->Update(diff); eventMgr->globalProcessor->Update(diff);
httpManager.HandleHttpResponses();
START_HOOK(WORLD_EVENT_ON_UPDATE); START_HOOK(WORLD_EVENT_ON_UPDATE);
Push(diff); Push(diff);

7832
libs/httplib.h Normal file

File diff suppressed because it is too large Load Diff

227
libs/rigtorp/SPSCQueue.h Normal file
View File

@@ -0,0 +1,227 @@
/*
Copyright (c) 2020 Erik Rigtorp <erik@rigtorp.se>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#pragma once
#include <atomic>
#include <cassert>
#include <cstddef>
#include <memory> // std::allocator
#include <new> // std::hardware_destructive_interference_size
#include <stdexcept>
#include <type_traits> // std::enable_if, std::is_*_constructible
namespace rigtorp {
template <typename T, typename Allocator = std::allocator<T>> class SPSCQueue {
#if defined(__cpp_if_constexpr) && defined(__cpp_lib_void_t)
template <typename Alloc2, typename = void>
struct has_allocate_at_least : std::false_type {};
template <typename Alloc2>
struct has_allocate_at_least<
Alloc2, std::void_t<typename Alloc2::value_type,
decltype(std::declval<Alloc2 &>().allocate_at_least(
size_t{}))>> : std::true_type {};
#endif
public:
explicit SPSCQueue(const size_t capacity,
const Allocator &allocator = Allocator())
: capacity_(capacity), allocator_(allocator) {
// The queue needs at least one element
if (capacity_ < 1) {
capacity_ = 1;
}
capacity_++; // Needs one slack element
// Prevent overflowing size_t
if (capacity_ > SIZE_MAX - 2 * kPadding) {
capacity_ = SIZE_MAX - 2 * kPadding;
}
#if defined(__cpp_if_constexpr) && defined(__cpp_lib_void_t)
if constexpr (has_allocate_at_least<Allocator>::value) {
auto res = allocator_.allocate_at_least(capacity_ + 2 * kPadding);
slots_ = res.ptr;
capacity_ = res.count - 2 * kPadding;
} else {
slots_ = std::allocator_traits<Allocator>::allocate(
allocator_, capacity_ + 2 * kPadding);
}
#else
slots_ = std::allocator_traits<Allocator>::allocate(
allocator_, capacity_ + 2 * kPadding);
#endif
static_assert(alignof(SPSCQueue<T>) == kCacheLineSize, "");
static_assert(sizeof(SPSCQueue<T>) >= 3 * kCacheLineSize, "");
assert(reinterpret_cast<char *>(&readIdx_) -
reinterpret_cast<char *>(&writeIdx_) >=
static_cast<std::ptrdiff_t>(kCacheLineSize));
}
~SPSCQueue() {
while (front()) {
pop();
}
std::allocator_traits<Allocator>::deallocate(allocator_, slots_,
capacity_ + 2 * kPadding);
}
// non-copyable and non-movable
SPSCQueue(const SPSCQueue &) = delete;
SPSCQueue &operator=(const SPSCQueue &) = delete;
template <typename... Args>
void emplace(Args &&...args) noexcept(
std::is_nothrow_constructible<T, Args &&...>::value) {
static_assert(std::is_constructible<T, Args &&...>::value,
"T must be constructible with Args&&...");
auto const writeIdx = writeIdx_.load(std::memory_order_relaxed);
auto nextWriteIdx = writeIdx + 1;
if (nextWriteIdx == capacity_) {
nextWriteIdx = 0;
}
while (nextWriteIdx == readIdxCache_) {
readIdxCache_ = readIdx_.load(std::memory_order_acquire);
}
new (&slots_[writeIdx + kPadding]) T(std::forward<Args>(args)...);
writeIdx_.store(nextWriteIdx, std::memory_order_release);
}
template <typename... Args>
bool try_emplace(Args &&...args) noexcept(
std::is_nothrow_constructible<T, Args &&...>::value) {
static_assert(std::is_constructible<T, Args &&...>::value,
"T must be constructible with Args&&...");
auto const writeIdx = writeIdx_.load(std::memory_order_relaxed);
auto nextWriteIdx = writeIdx + 1;
if (nextWriteIdx == capacity_) {
nextWriteIdx = 0;
}
if (nextWriteIdx == readIdxCache_) {
readIdxCache_ = readIdx_.load(std::memory_order_acquire);
if (nextWriteIdx == readIdxCache_) {
return false;
}
}
new (&slots_[writeIdx + kPadding]) T(std::forward<Args>(args)...);
writeIdx_.store(nextWriteIdx, std::memory_order_release);
return true;
}
void push(const T &v) noexcept(std::is_nothrow_copy_constructible<T>::value) {
static_assert(std::is_copy_constructible<T>::value,
"T must be copy constructible");
emplace(v);
}
template <typename P, typename = typename std::enable_if<
std::is_constructible<T, P &&>::value>::type>
void push(P &&v) noexcept(std::is_nothrow_constructible<T, P &&>::value) {
emplace(std::forward<P>(v));
}
bool
try_push(const T &v) noexcept(std::is_nothrow_copy_constructible<T>::value) {
static_assert(std::is_copy_constructible<T>::value,
"T must be copy constructible");
return try_emplace(v);
}
template <typename P, typename = typename std::enable_if<
std::is_constructible<T, P &&>::value>::type>
bool try_push(P &&v) noexcept(std::is_nothrow_constructible<T, P &&>::value) {
return try_emplace(std::forward<P>(v));
}
T *front() noexcept {
auto const readIdx = readIdx_.load(std::memory_order_relaxed);
if (readIdx == writeIdxCache_) {
writeIdxCache_ = writeIdx_.load(std::memory_order_acquire);
if (writeIdxCache_ == readIdx) {
return nullptr;
}
}
return &slots_[readIdx + kPadding];
}
void pop() noexcept {
static_assert(std::is_nothrow_destructible<T>::value,
"T must be nothrow destructible");
auto const readIdx = readIdx_.load(std::memory_order_relaxed);
assert(writeIdx_.load(std::memory_order_acquire) != readIdx);
slots_[readIdx + kPadding].~T();
auto nextReadIdx = readIdx + 1;
if (nextReadIdx == capacity_) {
nextReadIdx = 0;
}
readIdx_.store(nextReadIdx, std::memory_order_release);
}
size_t size() const noexcept {
std::ptrdiff_t diff = writeIdx_.load(std::memory_order_acquire) -
readIdx_.load(std::memory_order_acquire);
if (diff < 0) {
diff += capacity_;
}
return static_cast<size_t>(diff);
}
bool empty() const noexcept { return size() == 0; }
size_t capacity() const noexcept { return capacity_ - 1; }
private:
#ifdef __cpp_lib_hardware_interference_size
static constexpr size_t kCacheLineSize =
std::hardware_destructive_interference_size;
#else
static constexpr size_t kCacheLineSize = 64;
#endif
// Padding to avoid false sharing between slots_ and adjacent allocations
static constexpr size_t kPadding = (kCacheLineSize - 1) / sizeof(T) + 1;
private:
size_t capacity_;
T *slots_;
#if defined(__has_cpp_attribute) && __has_cpp_attribute(no_unique_address)
Allocator allocator_ [[no_unique_address]];
#else
Allocator allocator_;
#endif
// Align to cache line size in order to avoid false sharing
// readIdxCache_ and writeIdxCache_ is used to reduce the amount of cache
// coherency traffic
alignas(kCacheLineSize) std::atomic<size_t> writeIdx_ = {0};
alignas(kCacheLineSize) size_t readIdxCache_ = 0;
alignas(kCacheLineSize) std::atomic<size_t> readIdx_ = {0};
alignas(kCacheLineSize) size_t writeIdxCache_ = 0;
// Padding to avoid adjacent allocations to share cache line with
// writeIdxCache_
char padding_[kCacheLineSize - sizeof(writeIdxCache_)];
};
} // namespace rigtorp