Magic_Game/Easy2D/network/HttpClient.cpp

387 lines
9.9 KiB
C++

// Copyright (c) 2016-2018 Easy2D - Nomango
//
// 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.
#include "../easy2d-network.h"
#include <thread>
#include <codecvt>
// CURL
#include "../third-party/curl/curl.h"
#pragma comment(lib, "libcurl.lib")
namespace
{
using namespace easy2d;
using namespace easy2d::network;
size_t write_data(void* buffer, size_t size, size_t nmemb, void* userp)
{
std::string* recv_buffer = (std::string*)userp;
size_t total = size * nmemb;
// add data to the end of recv_buffer
// write data maybe called more than once in a single request
recv_buffer->append((char*)buffer, total);
return total;
}
std::string convert_to_utf8(String const& str)
{
std::wstring_convert<std::codecvt_utf8<wchar_t>> utf8_conv;
std::string result;
try
{
result = utf8_conv.to_bytes(str.c_str());
}
catch (std::range_error&)
{
// bad conversion
result = str.to_string();
}
return result;
}
String convert_from_utf8(std::string const& str)
{
std::wstring_convert<std::codecvt_utf8<wchar_t>> utf8_conv;
String result;
try
{
result = utf8_conv.from_bytes(str);
}
catch (std::range_error&)
{
// bad conversion
result = str;
}
return result;
}
class Curl
{
public:
Curl()
: curl_(curl_easy_init())
, curl_headers_(nullptr)
{
}
~Curl()
{
if (curl_)
{
curl_easy_cleanup(curl_);
curl_ = nullptr;
}
if (curl_headers_)
{
curl_slist_free_all(curl_headers_);
curl_headers_ = nullptr;
}
}
bool Init(HttpClient* client, Array<std::string> const& headers, std::string const& url, std::string* response_data, std::string* response_header, char* error_buffer)
{
if (!SetOption(CURLOPT_ERRORBUFFER, error_buffer))
return false;
if (!SetOption(CURLOPT_TIMEOUT, client->GetTimeoutForRead()))
return false;
if (!SetOption(CURLOPT_CONNECTTIMEOUT, client->GetTimeoutForConnect()))
return false;
const auto ssl_ca_file = client->GetSSLVerification().to_string();
if (ssl_ca_file.empty()) {
if (!SetOption(CURLOPT_SSL_VERIFYPEER, 0L))
return false;
if (!SetOption(CURLOPT_SSL_VERIFYHOST, 0L))
return false;
}
else {
if (!SetOption(CURLOPT_SSL_VERIFYPEER, 1L))
return false;
if (!SetOption(CURLOPT_SSL_VERIFYHOST, 2L))
return false;
if (!SetOption(CURLOPT_CAINFO, ssl_ca_file.c_str()))
return false;
}
if (!SetOption(CURLOPT_NOSIGNAL, 1L))
return false;
if (!SetOption(CURLOPT_ACCEPT_ENCODING, ""))
return false;
// set request headers
if (!headers.empty())
{
for (const auto& header : headers)
{
curl_headers_ = curl_slist_append(curl_headers_, header.c_str());
}
if (!SetOption(CURLOPT_HTTPHEADER, curl_headers_))
return false;
}
return SetOption(CURLOPT_URL, url.c_str())
&& SetOption(CURLOPT_WRITEFUNCTION, write_data)
&& SetOption(CURLOPT_WRITEDATA, response_data)
&& SetOption(CURLOPT_HEADERFUNCTION, write_data)
&& SetOption(CURLOPT_HEADERDATA, response_header);
}
bool Perform(long* response_code)
{
if (CURLE_OK != curl_easy_perform(curl_))
return false;
CURLcode code = curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, response_code);
return code == CURLE_OK && (*response_code >= 200 && *response_code < 300);
}
template <typename ..._Args>
bool SetOption(CURLoption option, _Args&&... args)
{
return CURLE_OK == curl_easy_setopt(curl_, option, std::forward<_Args>(args)...);
}
public:
static inline bool GetRequest(
HttpClient* client,
Array<std::string> const& headers,
std::string const& url,
long* response_code,
std::string* response_data,
std::string* response_header,
char* error_buffer)
{
Curl curl;
return curl.Init(client, headers, url, response_data, response_header, error_buffer)
&& curl.SetOption(CURLOPT_FOLLOWLOCATION, true)
&& curl.Perform(response_code);
}
static inline bool PostRequest(
HttpClient* client,
Array<std::string> const& headers,
std::string const& url,
std::string const& request_data,
long* response_code,
std::string* response_data,
std::string* response_header,
char* error_buffer)
{
Curl curl;
return curl.Init(client, headers, url, response_data, response_header, error_buffer)
&& curl.SetOption(CURLOPT_POST, 1)
&& curl.SetOption(CURLOPT_POSTFIELDS, request_data.c_str())
&& curl.SetOption(CURLOPT_POSTFIELDSIZE, request_data.size())
&& curl.Perform(response_code);
}
static inline bool PutRequest(
HttpClient* client,
Array<std::string> const& headers,
std::string const& url,
std::string const& request_data,
long* response_code,
std::string* response_data,
std::string* response_header,
char* error_buffer)
{
Curl curl;
return curl.Init(client, headers, url, response_data, response_header, error_buffer)
&& curl.SetOption(CURLOPT_CUSTOMREQUEST, "PUT")
&& curl.SetOption(CURLOPT_POSTFIELDS, request_data.c_str())
&& curl.SetOption(CURLOPT_POSTFIELDSIZE, request_data.size())
&& curl.Perform(response_code);
}
static inline bool DeleteRequest(
HttpClient* client,
Array<std::string> const& headers,
std::string const& url,
long* response_code,
std::string* response_data,
std::string* response_header,
char* error_buffer)
{
Curl curl;
return curl.Init(client, headers, url, response_data, response_header, error_buffer)
&& curl.SetOption(CURLOPT_CUSTOMREQUEST, "DELETE")
&& curl.SetOption(CURLOPT_FOLLOWLOCATION, true)
&& curl.Perform(response_code);
}
private:
CURL* curl_;
curl_slist* curl_headers_;
};
}
namespace easy2d
{
namespace network
{
HttpClient::HttpClient()
: app_(nullptr)
, timeout_for_connect_(30000 /* 30 seconds */)
, timeout_for_read_(60000 /* 60 seconds */)
{
}
void HttpClient::SetupComponent(Application * app)
{
app_ = app;
::curl_global_init(CURL_GLOBAL_ALL);
std::thread thread(Closure(this, &HttpClient::NetworkThread));
thread.detach();
}
void HttpClient::DestroyComponent()
{
::curl_global_cleanup();
}
void HttpClient::Send(HttpRequestPtr const & request)
{
if (!request)
return;
request_mutex_.lock();
request_queue_.push(request);
request_mutex_.unlock();
sleep_condition_.notify_one();
}
void HttpClient::NetworkThread()
{
while (true)
{
HttpRequestPtr request;
{
std::lock_guard<std::mutex> lock(request_mutex_);
while (request_queue_.empty())
{
sleep_condition_.wait(request_mutex_);
}
request = request_queue_.front();
request_queue_.pop();
}
HttpResponsePtr response = new (std::nothrow) HttpResponse(request);
Perform(request, response);
response_mutex_.lock();
response_queue_.push(response);
response_mutex_.unlock();
if (app_)
{
app_->PreformFunctionInMainThread(Closure(this, &HttpClient::DispatchResponseCallback));
}
}
}
void HttpClient::Perform(HttpRequestPtr const & request, HttpResponsePtr const & response)
{
bool ok = false;
long response_code = 0;
char error_message[256] = { 0 };
std::string response_header;
std::string response_data;
std::string url = convert_to_utf8(request->GetUrl());
std::string data = convert_to_utf8(request->GetData());
Array<std::string> headers;
headers.reserve(request->GetHeaders().size());
for (const auto& pair : request->GetHeaders())
{
headers.push_back(pair.first.to_string() + ":" + pair.second.to_string());
}
switch (request->GetType())
{
case HttpRequest::Type::Get:
ok = Curl::GetRequest(this, headers, url, &response_code, &response_data, &response_header, error_message);
break;
case HttpRequest::Type::Post:
ok = Curl::PostRequest(this, headers, url, data, &response_code, &response_data, &response_header, error_message);
break;
case HttpRequest::Type::Put:
ok = Curl::PutRequest(this, headers, url, data, &response_code, &response_data, &response_header, error_message);
break;
case HttpRequest::Type::Delete:
ok = Curl::DeleteRequest(this, headers, url, &response_code, &response_data, &response_header, error_message);
break;
default:
E2D_ERROR_LOG(L"HttpClient: unknown request type, only GET, POST, PUT or DELETE is supported");
return;
}
response->SetResponseCode(response_code);
response->SetHeader(response_header);
response->SetData(convert_from_utf8(response_data));
if (!ok)
{
response->SetSucceed(false);
response->SetError(error_message);
}
else
{
response->SetSucceed(true);
}
}
void HttpClient::DispatchResponseCallback()
{
HttpResponsePtr response;
response_mutex_.lock();
if (!response_queue_.empty())
{
response = response_queue_.front();
response_queue_.pop();
}
response_mutex_.unlock();
if (response)
{
HttpRequestPtr request = response->GetRequest();
const auto& callback = request->GetResponseCallback();
if (callback)
{
callback(request, response);
}
}
}
}
}