Last active
February 21, 2018 20:47
-
-
Save DragonOsman/9a7d4ee116efc575dece7d6de46bd31d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <boost/beast/core.hpp> | |
#include <boost/beast/http.hpp> | |
#include <boost/beast/version.hpp> | |
#include <boost/asio/ip/tcp.hpp> | |
#include <boost/asio/connect.hpp> | |
#include <boost/config.hpp> | |
#include <cstdlib> | |
#include <cctype> | |
#include <iostream> | |
#include <memory> | |
#include <string> | |
#include <thread> | |
#include "json.hpp" | |
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp> | |
using nlohmann::json; | |
namespace http = boost::beast::http; // from <boost/beast/http.hpp> | |
//------------------------------------------------------------------------------ | |
// Return a reasonable mime type based on the extension of a file. | |
boost::beast::string_view mime_type(boost::beast::string_view path); | |
// This class represents a cache for storing results from the | |
// currency exchange API used by openexchangerates.org | |
class cache_storage | |
{ | |
public: | |
cache_storage(const std::chrono::seconds &duration) | |
: m_cache{}, m_duration{ duration }, m_ioc{ 1 }, m_socket { m_ioc } | |
{ | |
} | |
// This function queries the currency API after making sure | |
// that the stored result(s) is/are old enough | |
// It also makes a new query to the API if needed | |
const json &query(const std::string &query_data); | |
private: | |
std::unordered_map<std::string, std::pair<std::chrono::time_point<std::chrono::steady_clock>, json>> m_cache; | |
std::chrono::seconds m_duration; | |
boost::asio::io_context m_ioc; | |
tcp::socket m_socket; | |
}; | |
// Parse POST body | |
std::map<std::string, std::string> parse(const std::string &data); | |
// Perform currency conversion | |
double convert(const std::string &to_currency, const double money_amount); | |
// Append an HTTP rel-path to a local filesystem path. | |
// The returned path is normalized for the platform. | |
std::string path_cat(boost::beast::string_view base, boost::beast::string_view path); | |
// This function produces an HTTP response for the given | |
// request. The type of the response object depends on the | |
// contents of the request, so the interface requires the | |
// caller to pass a generic lambda for receiving the response. | |
template<class Body, class Allocator, class Send> | |
void handle_request(boost::beast::string_view doc_root, http::request<Body, http::basic_fields<Allocator>>&& req, Send&& send); | |
//------------------------------------------------------------------------------ | |
// Report a failure | |
void fail(boost::system::error_code ec, char const* what); | |
// This is the C++11 equivalent of a generic lambda. | |
// The function object is used to send an HTTP message. | |
template<class Stream> | |
struct send_lambda | |
{ | |
Stream& stream_; | |
bool& close_; | |
boost::system::error_code& ec_; | |
explicit | |
send_lambda(Stream& stream, bool& close, boost::system::error_code& ec) | |
: stream_{ stream }, close_{ close }, ec_{ ec } | |
{ | |
} | |
template<bool isRequest, class Body, class Fields> | |
void operator()(http::message<isRequest, Body, Fields>&& msg) const; | |
}; | |
// Handles an HTTP server connection | |
void do_session(tcp::socket& socket, std::string const& doc_root); | |
//------------------------------------------------------------------------------ | |
int main(int argc, char* argv[]) | |
{ | |
try | |
{ | |
// Check command line arguments. | |
if (argc != 4) | |
{ | |
std::cerr << | |
"Usage: currency_converter <address> <port> <doc_root>\n" << | |
"Example:\n" << | |
" currency_converter 0.0.0.0 8080 .\n"; | |
return EXIT_FAILURE; | |
} | |
auto const address = boost::asio::ip::make_address(argv[1]); | |
auto const port = static_cast<unsigned short>(std::atoi(argv[2])); | |
std::string const doc_root = argv[3]; | |
// The io_context is required for all I/O | |
boost::asio::io_context ioc{ 1 }; | |
// The acceptor receives incoming connections | |
tcp::acceptor acceptor{ ioc, { address, port } }; | |
for (;;) | |
{ | |
// This will receive the new connection | |
tcp::socket socket{ ioc }; | |
// Block until we get a connection | |
acceptor.accept(socket); | |
// Launch the session, transferring ownership of the socket | |
std::thread{ std::bind(&do_session, std::move(socket), doc_root) }.detach(); | |
} | |
} | |
catch (const std::runtime_error &e) | |
{ | |
std::cerr << "Error: " << e.what() << '\n'; | |
return EXIT_FAILURE; | |
} | |
catch (const std::exception &e) | |
{ | |
std::cerr << "Error: " << e.what() << '\n'; | |
return EXIT_FAILURE + 1; | |
} | |
} | |
boost::beast::string_view mime_type(boost::beast::string_view path) | |
{ | |
using boost::beast::iequals; | |
const auto ext = [&path] | |
{ | |
auto const pos = path.rfind("."); | |
if (pos == boost::beast::string_view::npos) | |
{ | |
return boost::beast::string_view{}; | |
} | |
return path.substr(pos); | |
}(); | |
if (iequals(ext, ".htm")) | |
{ | |
return "text/html"; | |
} | |
if (iequals(ext, ".html")) | |
{ | |
return "text/html"; | |
} | |
if (iequals(ext, ".php")) | |
{ | |
return "text/html"; | |
} | |
if (iequals(ext, ".css")) | |
{ | |
return "text/css"; | |
} | |
if (iequals(ext, ".txt")) | |
{ | |
return "text/plain"; | |
} | |
if (iequals(ext, ".js")) | |
{ | |
return "application/javascript"; | |
} | |
if (iequals(ext, ".json")) | |
{ | |
return "application/json"; | |
} | |
if (iequals(ext, ".xml")) | |
{ | |
return "application/xml"; | |
} | |
if (iequals(ext, ".swf")) | |
{ | |
return "application/x-shockwave-flash"; | |
} | |
if (iequals(ext, ".flv")) | |
{ | |
return "video/x-flv"; | |
} | |
if (iequals(ext, ".png")) | |
{ | |
return "image/png"; | |
} | |
if (iequals(ext, ".jpe")) | |
{ | |
return "image/jpeg"; | |
} | |
if (iequals(ext, ".jpeg")) | |
{ | |
return "image/jpeg"; | |
} | |
if (iequals(ext, ".jpg")) | |
{ | |
return "image/jpeg"; | |
} | |
if (iequals(ext, ".gif")) | |
{ | |
return "image/gif"; | |
} | |
if (iequals(ext, ".bmp")) | |
{ | |
return "image/bmp"; | |
} | |
if (iequals(ext, ".ico")) | |
{ | |
return "image/vnd.microsoft.icon"; | |
} | |
if (iequals(ext, ".tiff")) | |
{ | |
return "image/tiff"; | |
} | |
if (iequals(ext, ".tif")) | |
{ | |
return "image/tiff"; | |
} | |
if (iequals(ext, ".svg")) | |
{ | |
return "image/svg+xml"; | |
} | |
if (iequals(ext, ".svgz")) | |
{ | |
return "image/svg+xml"; | |
} | |
return "application/text"; | |
} | |
std::string path_cat(boost::beast::string_view base, boost::beast::string_view path) | |
{ | |
if (base.empty()) | |
{ | |
return path.to_string(); | |
} | |
std::string result = base.to_string(); | |
#if BOOST_MSVC | |
char constexpr path_separator = '\\'; | |
if (result.back() == path_separator) | |
{ | |
result.resize(result.size() - 1); | |
} | |
result.append(path.data(), path.size()); | |
for (auto& c : result) | |
{ | |
if (c == '/') | |
{ | |
c = path_separator; | |
} | |
} | |
#else | |
char constexpr path_separator = '/'; | |
if (result.back() == path_separator) | |
{ | |
result.resize(result.size() - 1); | |
} | |
result.append(path.data(), path.size()); | |
#endif | |
return result; | |
} | |
std::map<std::string, std::string> parse(const std::string &data) | |
{ | |
enum class States | |
{ | |
Start, | |
Name, | |
Ignore, | |
Value | |
}; | |
std::map<std::string, std::string> parsed_values; | |
std::string name; | |
std::string value; | |
States state = States::Start; | |
for (char c : data) | |
{ | |
switch (state) | |
{ | |
case States::Start: | |
if (c == '"') | |
{ | |
state = States::Name; | |
} | |
break; | |
case States::Name: | |
if (c != '"') | |
{ | |
name += c; | |
} | |
else | |
{ | |
state = States::Ignore; | |
} | |
break; | |
case States::Ignore: | |
if (!isspace(c)) | |
{ | |
state = States::Value; | |
value += c; | |
} | |
break; | |
case States::Value: | |
if (c != '\n') | |
{ | |
value += c; | |
} | |
else | |
{ | |
parsed_values.insert(std::make_pair(name, value)); | |
name = ""; | |
value = ""; | |
state = States::Start; | |
} | |
break; | |
} | |
} | |
return parsed_values; | |
} | |
template<class Body, class Allocator, class Send> | |
void handle_request(boost::beast::string_view doc_root, http::request<Body, http::basic_fields<Allocator>>&& req, Send&& send) | |
{ | |
// Returns a bad request response | |
const auto bad_request = [&req](boost::beast::string_view why) | |
{ | |
http::response<http::string_body> res{ http::status::bad_request, req.version() }; | |
res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
res.set(http::field::content_type, "text/html"); | |
res.keep_alive(req.keep_alive()); | |
res.body() = why.to_string(); | |
res.prepare_payload(); | |
return res; | |
}; | |
// Returns a not found response | |
const auto not_found = [&req](boost::beast::string_view target) | |
{ | |
http::response<http::string_body> res{ http::status::not_found, req.version() }; | |
res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
res.set(http::field::content_type, "text/html"); | |
res.keep_alive(req.keep_alive()); | |
res.body() = "The resource '" + target.to_string() + "' was not found."; | |
res.prepare_payload(); | |
return res; | |
}; | |
// Returns a server error response | |
const auto server_error = [&req](boost::beast::string_view what) | |
{ | |
http::response<http::string_body> res{ http::status::internal_server_error, req.version() }; | |
res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
res.set(http::field::content_type, "text/html"); | |
res.keep_alive(req.keep_alive()); | |
res.body() = "An error occurred: '" + what.to_string() + "'"; | |
res.prepare_payload(); | |
return res; | |
}; | |
// Make sure we can handle the method | |
if (req.method() != http::verb::get && | |
req.method() != http::verb::head && | |
req.method() != http::verb::post) | |
{ | |
return send(bad_request("Unknown HTTP-method")); | |
} | |
// Request path must be absolute and not contain "..". | |
if (req.target().empty() || | |
req.target()[0] != '/' || | |
req.target().find("..") != boost::beast::string_view::npos) | |
{ | |
return send(bad_request("Illegal request-target")); | |
} | |
// Build the path to the requested file | |
std::string path = path_cat(doc_root, req.target()); | |
if (req.target().back() == '/') | |
{ | |
path.append("index.html"); | |
} | |
// In case of POST request, check the path URI | |
else if (req.target().front() == '/' && req.target().size() > 1 && req.method() == http::verb::post) | |
{ | |
using boost::beast::iequals; | |
const auto ext = [&path] | |
{ | |
const auto pos = path.rfind("."); | |
if (pos == boost::beast::string_view::npos) | |
{ | |
return boost::beast::string_view{}; | |
} | |
const auto pos2 = path.find(" "); | |
if (pos2 != boost::beast::string_view::npos) | |
{ | |
return boost::beast::string_view{}; | |
} | |
return boost::beast::string_view(path.substr(pos)); | |
}(); | |
if (!iequals(ext, ".exe") && !iequals(ext, ".cgi")) | |
{ | |
return send(bad_request("Illegal request-target")); | |
} | |
} | |
// Attempt to open the file | |
boost::beast::error_code ec; | |
http::file_body::value_type body; | |
body.open(path.c_str(), boost::beast::file_mode::scan, ec); | |
// Handle the case where the file doesn't exist | |
if (ec == boost::system::errc::no_such_file_or_directory) | |
{ | |
return send(not_found(req.target())); | |
} | |
// Handle an unknown error | |
if (ec) | |
{ | |
return send(server_error(ec.message())); | |
} | |
// Respond to HEAD request | |
if (req.method() == http::verb::head) | |
{ | |
http::response<http::empty_body> res{ http::status::ok, req.version() }; | |
res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
res.set(http::field::content_type, mime_type(path)); | |
res.content_length(body.size()); | |
res.keep_alive(req.keep_alive()); | |
return send(std::move(res)); | |
} | |
// Respond to GET request | |
else if (req.method() == http::verb::get) | |
{ | |
http::response<http::file_body> res{ | |
std::piecewise_construct, | |
std::make_tuple(std::move(body)), | |
std::make_tuple(http::status::ok, req.version()) }; | |
res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
res.set(http::field::content_type, mime_type(path)); | |
res.content_length(body.size()); | |
res.keep_alive(req.keep_alive()); | |
return send(std::move(res)); | |
} | |
// Respond to POST request | |
else if (req.method() == http::verb::post) | |
{ | |
boost::beast::string_view content_type = req[http::field::content_type]; | |
if (content_type.find("multipart/form-data") == std::string::npos && | |
content_type.find("application/x-www-form-urlencoded") == std::string::npos) | |
{ | |
return send(bad_request("Bad request")); | |
} | |
std::map<std::string, std::string> parsed_value = parse(req.body()); | |
std::cout << "POST body example: " << req.body() << '\n'; | |
std::cout << "parsed_value map:\n"; | |
for (const auto &x : parsed_value) | |
{ | |
std::cout << x.first << ": " << x.second << '\n'; | |
} | |
double money_amount = std::stod(parsed_value["currency_amount"]); | |
std::string to_currency = parsed_value["to_currency"]; | |
std::cout << "Line 477: to_currency is: " << to_currency << '\n'; | |
double conversion_result = convert(to_currency, money_amount); | |
http::string_body::value_type str_body = std::to_string(conversion_result); | |
http::response<http::string_body> res{ | |
std::piecewise_construct, | |
std::make_tuple(std::move(str_body)), | |
std::make_tuple(http::status::ok, req.version()) }; | |
res.set(http::field::server, BOOST_BEAST_VERSION_STRING); | |
res.set(http::field::content_type, mime_type(path)); | |
res.content_length(body.size()); | |
res.keep_alive(req.keep_alive()); | |
return send(std::move(res)); | |
} | |
} | |
void fail(boost::system::error_code ec, char const* what) | |
{ | |
std::cerr << what << ": " << ec.message() << "\n"; | |
} | |
template<class Stream> | |
template<bool isRequest, class Body, class Fields> | |
void send_lambda<Stream>::operator()(http::message<isRequest, Body, Fields>&& msg) const | |
{ | |
// Determine if we should close the connection after | |
close_ = msg.need_eof(); | |
// We need the serializer here because the serializer requires | |
// a non-const file_body, and the message oriented version of | |
// http::write only works with const messages. | |
http::serializer<isRequest, Body, Fields> sr{ msg }; | |
http::write(stream_, sr, ec_); | |
} | |
void do_session(tcp::socket& socket, std::string const& doc_root) | |
{ | |
bool close = false; | |
boost::system::error_code ec; | |
// This buffer is required to persist across reads | |
boost::beast::flat_buffer buffer; | |
// This lambda is used to send messages | |
send_lambda<tcp::socket> lambda{ socket, close, ec }; | |
for (;;) | |
{ | |
// Read a request | |
http::request<http::string_body> req; | |
http::read(socket, buffer, req, ec); | |
if (ec == http::error::end_of_stream) | |
{ | |
break; | |
} | |
if (ec) | |
{ | |
return fail(ec, "read"); | |
} | |
// Send the response | |
handle_request(doc_root, std::move(req), lambda); | |
if (ec) | |
{ | |
return fail(ec, "write"); | |
} | |
if (close) | |
{ | |
// This means we should close the connection, usually because | |
// the response indicated the "Connection: close" semantic. | |
break; | |
} | |
} | |
// Send a TCP shutdown | |
socket.shutdown(tcp::socket::shutdown_send, ec); | |
// At this point the connection is closed gracefully | |
} | |
double convert(const std::string &to_currency, const double money_amount) | |
{ | |
using namespace std::chrono_literals; | |
std::vector<std::string> currencies{ | |
"AED","AFN","ALL","AMD","ANG","AOA","ARS","AUD","AWG","AZN","BAM","BBD","BDT","BGN","BHD","BIF","BMD","BND","BOB","BRL","BSD","BTC","BTN","BWP", | |
"BYN","BYR","BZD","CAD","CDF","CHF","CLF","CLP","CNY","COP","CRC","CUC","CUP","CVE","CZK","DJF","DKK","DOP","DZD","EGP","ERN","ETB","EUR","FJD", | |
"FKP","GBP","GEL","GGP","GHS","GIP","GMD","GNF","GTQ","GYD","HKD","HNL","HRK","HTG","HUF","IDR","ILS","IMP","INR","IQD","IRR","ISK","JEP","JMD", | |
"JOD","JPY","KES","KGS","KHR","KMF","KPW","KRW","KWD","KYD","KZT","LAK","LBP","LKR","LRD","LSL","LTL","LVL","LYD","MAD","MDL","MGA","MKD","MMK", | |
"MNT","MOP","MRO","MUR","MVR","MWK","MXN","MYR","MZN","NAD","NGN","NIO","NOK","NPR","NZD","OMR","PAB","PEN","PGK","PHP","PKR","PLN","PYG","QAR", | |
"RON","RSD","RUB","RWF","SAR","SBD","SCR","SDG","SEK","SGD","SHP","SLL","SOS","SRD","STD","SVC","SYP","SZL","THB","TJS","TMT","TND","TOP","TRY", | |
"TTD","TWD","TZS","UAH","UGX","USD","UYU","UZS","VEF","VND","VUV","WST","XAF","XAG","XAU","XCD","XDR","XOF","XPF","YER","ZAR","ZMK","ZMW","ZWL" | |
}; | |
cache_storage cache{ 1h }; | |
json j_res; | |
try | |
{ | |
j_res = cache.query(to_currency); | |
} | |
catch (const std::exception &e) | |
{ | |
std::cerr << "Line 580: Error: " << e.what() << '\n'; | |
} | |
const std::string from_currency = "USD"; | |
double result = 0, rate = std::stod(j_res["quotes"][from_currency + to_currency].get<json::string_t>()); | |
std::cout << "Rate is: " << rate << '\n' | |
<< "To currency is: " << to_currency << '\n' | |
<< "currencies:\n" << "j_res:\n"; | |
for (auto const x : j_res) | |
{ | |
std::cout << x << '\n'; | |
} | |
for (auto const x : currencies) | |
{ | |
std::cout << x << '\n'; | |
} | |
/*if (to_currency == currencies[0] || to_currency == currencies[1] || to_currency == currencies[2] || to_currency == currencies[3] || | |
to_currency == currencies[4] || to_currency == currencies[5] || to_currency == currencies[6] || to_currency == currencies[7] || | |
to_currency == currencies[8] || to_currency == currencies[9] || to_currency == currencies[10] || to_currency == currencies[11] || | |
to_currency == currencies[12] || to_currency == currencies[13] || to_currency == currencies[14] || to_currency == currencies[15] || | |
to_currency == currencies[16] || to_currency == currencies[17] || to_currency == currencies[18] || to_currency == currencies[19] || | |
to_currency == currencies[20] || to_currency == currencies[21] || to_currency == currencies[22] || to_currency == currencies[23] || | |
to_currency == currencies[24] || to_currency == currencies[25] || to_currency == currencies[26] || to_currency == currencies[27] || | |
to_currency == currencies[28] || to_currency == currencies[29] || to_currency == currencies[30] || to_currency == currencies[31] || | |
)*/ | |
if (std::find(currencies.begin(), currencies.end(), to_currency) != currencies.end()) | |
{ | |
std::cout << "Match found\n"; | |
result = money_amount * rate; | |
} | |
return result; | |
} | |
const json &cache_storage::query(const std::string &query_data) | |
{ | |
auto found = m_cache.find(query_data); | |
boost::beast::error_code ec; | |
try | |
{ | |
if (found == m_cache.end() || (std::chrono::steady_clock::now() - found->second.first) > m_duration) | |
{ | |
std::string host{ "apilayer.net" }, api_endpoint{ "/api/live" }, | |
access_key{ "ACCESS_KEY" }, currencies_param{ query_data }, | |
target = api_endpoint + "?access_key=" + access_key + "¤cies=" + currencies_param, | |
port{ "80" }; | |
int version = 11; | |
std::cout << "Debug (Line 631): currencies_param=" << currencies_param << '\n' | |
<< "URL is http://" << host + target << '\n'; | |
// This object and the TCP socket perform our IO | |
tcp::resolver resolver{ m_ioc }; | |
// Look up the domain name | |
const auto results = resolver.resolve(host, port); | |
// Make the connection on the IP address we get from a lookup | |
boost::asio::connect(m_socket, results.begin(), results.end()); | |
// Set up an HTTP GET request message | |
http::request<http::string_body> req{ http::verb::get, target, version }; | |
req.set(http::field::host, host); | |
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); | |
// Send the HTTP request to the remote host | |
http::write(m_socket, req); | |
// This buffer is used for reading and must be persisted | |
boost::beast::flat_buffer buffer; | |
// Declare a container to hold the response | |
http::response<http::string_body> res; | |
// Receive the HTTP response | |
http::read(m_socket, buffer, res, ec); | |
std::cout << "Line 656: read on line 655; ec: " << ec.message() << '\n' | |
<< "res.body(): " << res.body() << '\n'; | |
found = m_cache.insert_or_assign(found, query_data, std::make_pair(std::chrono::steady_clock::now(), json::parse(res.body()))); | |
} | |
return found->second.second; | |
} | |
catch (const std::exception &e) | |
{ | |
std::cerr << "Line 664: Error: " << e.what() << '\n'; | |
} | |
catch (const boost::beast::error_code &ec) | |
{ | |
std::cerr << "Line 668: Error: " << ec.message() << '\n'; | |
} | |
} |
Yeah, I did change it to just openexchangerates.org
. I changed the API to a different one now, though, since I was having trouble with it. They don't support the "Keep Alive" header and I don't know why the connection was being closed in the first place.
I replied on the issue I'd opened on Beast's GitHub page about the end_of_stream
error. I mentioned there what problem I'm having now. And I linked to this page to show the complete C++ code. Then I saw your comment here.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
"https://openexchangerates.org/api"
is not a valid host name:https://en.wikipedia.org/wiki/Hostname