CCEngineServer/CMakeProject1/CCServer.cpp
2021-08-20 17:58:25 +02:00

858 lines
25 KiB
C++

#include "CCServer.h"
#include "CryptoUtils.h"
//------------------------------------------------------------------------------
//
// Based on Beast example: Advanced server
//
//------------------------------------------------------------------------------
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/asio/strand.hpp>
#include <boost/crc.hpp>
#include <boost/iostreams/filtering_stream.hpp>
#include <boost/filesystem.hpp>
#include <boost/signals2.hpp>
#include <boost/tokenizer.hpp>
#include <algorithm>
#include <cstdlib>
#include <functional>
#include <iostream>
#include <fstream>
#include <shared_mutex>
#include <memory>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
namespace fs = boost::filesystem; // from <boost/filesystem.hpp>
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
static constexpr auto APIS_UINT8_MIN = std::numeric_limits<uint8_t>::min();
static constexpr auto APIS_UINT8_MAX = std::numeric_limits<uint8_t>::max();
#define MAXHESLO 225
#define MAX_DEKODHESLO (MAXHESLO-3)
#define KodcharToChar(znak2,poloha,posun) static_cast<uint8_t>(-(password[poloha])+(~(znak2))-(posun))
namespace
{
static const std::string cryptFileHeader("##CAPISF## Copyright (c) 2003 APIS spol. s r.o. SLOVAKIA~");
//static const std::string passReal("flckdscwjregfvfgbujvbnqajmadmgfjmhfmghplgghesrrhsufservvysfidgfysdnmsybsaftoprecofoplyrmnfsvcapridssedalenttrmfusposvresalbostreqnnapswolnfrdweneplmnzcavsfgusqootrpqaressnvclpacfdrgesklojphsdqtyczvxcbfhtryntinokusgkhjgchtx");
static const uint8_t passHidden2[] = { 0x89, 0xdb, 0x2c, 0x79, 0xd6, 0xa5, 0x7a, 0x7b, 0x6d, 0xae, 0x67, 0xee, 0xb2, 0x9a, 0x2c, 0xbe, 0xb7, 0xac, 0x6a, 0x56, 0xe8, 0xb2, 0xda, 0xde, 0xaa, 0x79, 0xda, 0xa6, 0xcc, 0x28, 0x96, 0x77, 0xeb, 0x77, 0x07, 0xa7, 0x7a, 0x99, 0x66, 0x9f, 0x37, 0x1a, 0xbe, 0xc7, 0xe0, 0xba, 0xca, 0xa8, 0xa2, 0xda, 0xe9, 0xa9, 0xaa, 0xde, 0xb2, 0xc9, 0xef, 0x72, 0x5a, 0x5a, 0x71, 0xf7, 0x6b, 0x81, 0xeb, 0x24, 0x96, 0x88, 0xe9, 0x86, 0xc7, 0x6a, 0xb7, 0x27, 0x33, 0xbf, 0x17, 0x1b, 0x7e, 0x1b, 0x6b, 0xca, 0x7b, 0x62, 0x9e, 0x89, 0x2e, 0xb2, 0x09, 0x21, 0x8e, 0x07, 0x21, 0xb7, 0x10, 0x34 };
class apis_read_filter
{
public:
typedef char char_type;
explicit apis_read_filter(size_t initialPosition)
: m_position(initialPosition)
{ }
struct category
: boost::iostreams::input
, boost::iostreams::filter_tag
, boost::iostreams::multichar_tag
, boost::iostreams::optimally_buffered_tag
{};
std::streamsize optimal_buffer_size() const { return 0; }
template<typename Source>
std::streamsize read(Source& src, char_type* s, std::streamsize n)
{
std::streamsize result = boost::iostreams::read(src, s, n);
if (result == -1)
{
return -1;
}
int dwPacketPos = ((m_position / MAX_DEKODHESLO) % 29) + 2;
int iPoloha = m_position % MAX_DEKODHESLO;
m_position += static_cast<size_t>(result);
for (int i = 0; i < result; i++)
{
if (iPoloha == MAX_DEKODHESLO)
{
iPoloha = 0;
dwPacketPos++;
}
if (dwPacketPos >= 31)
{
dwPacketPos = 2;
}
static const uint8_t passHidden[] = { 0x7e, 0x57, 0x24, 0x76, 0xc7, 0x30, 0x8e, 0xb7, 0xa0, 0x7e, 0xf7, 0xe0, 0x6e, 0xe8, 0xef, 0x6e, 0x7a, 0x9a, 0x8e, 0x66, 0x9d, 0x9a, 0x07, 0xe3, 0x9a, 0x17, 0xe6, 0x82, 0x1a, 0x65, 0x82, 0x08, 0x5e, 0xb2, 0xba, 0xe1, 0xb2, 0xe7, 0xec, 0x7a, 0xbb, 0xef, 0xca, 0xc7, 0xe2, 0x76, 0x07, 0xf2, 0xb1, 0xd9, 0xe6, 0xb3, 0x26, 0xec, 0x69, 0xfb, 0x68, 0xa6, 0xb7, 0x9c, 0xa1, 0xfa, 0x29, 0x97, 0x2a, 0xe6, 0x9d, 0xfb, 0x2f, 0x71, 0xaa, 0x6b };
if (iPoloha < sizeof(passHidden) * 4 / 3)
{
static const auto password = base64Encode(passHidden, sizeof(passHidden));
s[i] = KodcharToChar(s[i], iPoloha, iPoloha + dwPacketPos);
}
else
{
static const auto password = base64Encode(passHidden2, sizeof(passHidden2));
s[i] = KodcharToChar(s[i], iPoloha - sizeof(passHidden) * 4 / 3, iPoloha + dwPacketPos);
}
++iPoloha;
}
return result;
}
private:
size_t m_position;
};
void decryptBuffer(std::vector<uint8_t>& buffer)
{
bool encrypted = false;
if (buffer.size() >= cryptFileHeader.size())
{
encrypted = std::equal(cryptFileHeader.begin(), cryptFileHeader.end(), buffer.begin());
}
if (!encrypted)
{
return;
}
boost::iostreams::array_source src{ (const char*)buffer.data() + cryptFileHeader.size(), buffer.size() - cryptFileHeader.size()};
boost::iostreams::filtering_istream is;
is.push(apis_read_filter({ cryptFileHeader.size() }));
is.push(src);
std::vector<uint8_t> decrypted(buffer.size() - cryptFileHeader.size());
is.read((char*)decrypted.data(), decrypted.size());
if (decrypted.size() > sizeof(uint32_t))
{
auto headerLen = reinterpret_cast<uint32_t*>(decrypted.data())[0];
if (decrypted.size() > headerLen)
{
decrypted.erase(decrypted.begin(), decrypted.begin() + headerLen);
}
}
buffer = decrypted;
}
class etag_cache
{
public:
void insert(boost::beast::string_view path, boost::beast::string_view etag)
{
std::unique_lock<std::shared_mutex> lock(mutex_);
map_.emplace(path, etag);
}
bool check_path(boost::beast::string_view path, boost::beast::string_view etag) const
{
std::shared_lock<std::shared_mutex> lock(mutex_);
auto it = map_.find(static_cast<std::string>(path));
if (it != map_.end() && it->second == etag)
{
return true;
}
return false;
}
void clear()
{
std::unique_lock<std::shared_mutex> lock(mutex_);
map_.clear();
}
private:
mutable std::shared_mutex mutex_;
std::unordered_map<std::string, std::string> map_;
};
// Return a reasonable mime type based on the extension of a file.
beast::string_view
mime_type(beast::string_view path)
{
using beast::iequals;
auto const ext = [&path]
{
auto const pos = path.rfind(".");
if (pos == beast::string_view::npos)
return 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";
}
// Append an HTTP rel-path to a local filesystem path.
// The returned path is normalized for the platform.
std::string
path_cat(
beast::string_view base,
beast::string_view path)
{
if (base.empty())
return std::string(path);
std::string result(base);
#ifdef 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;
}
// 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(
beast::string_view doc_root,
etag_cache& cache,
http::request<Body, http::basic_fields<Allocator>>&& req,
Send&& send)
{
// Returns a bad request response
auto const bad_request =
[&req](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() = std::string(why);
res.prepare_payload();
return res;
};
// Returns a not found response
auto const not_found =
[&req](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 '" + std::string(target) + "' was not found.";
res.prepare_payload();
return res;
};
// Returns a server error response
auto const server_error =
[&req](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: '" + std::string(what) + "'";
res.prepare_payload();
return res;
};
// Returns a not modified response
auto const not_modified =
[&req](boost::beast::string_view etag)
{
http::response<http::empty_body> res{ http::status::not_modified, req.version() };
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
res.set(http::field::etag, etag);
res.keep_alive(req.keep_alive());
res.prepare_payload();
return res;
};
// Make sure we can handle the method
if (req.method() != http::verb::get &&
req.method() != http::verb::head)
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("..") != beast::string_view::npos)
return send(bad_request("Illegal request-target"));
auto it = req.find(http::field::if_none_match);
if (it != req.end())
{
auto etags = std::string(it->value());
typedef boost::tokenizer<boost::char_separator<char>> Tokenizer;
boost::char_separator<char> sep(", ");
Tokenizer tok{ etags, sep };
for (Tokenizer::iterator tokIt = tok.begin(); tokIt != tok.end(); ++tokIt)
{
std::string etag(*tokIt);
boost::string_view checksum(etag);
if (checksum.front() == '"')
{
checksum.remove_prefix(1);
checksum.remove_suffix(1);
}
if (cache.check_path(req.target(), checksum))
{
return send(not_modified(etag));
}
}
}
// Build the path to the requested file
beast::string_view targetPath = req.target();
auto pos = targetPath.find_first_of('?');
if (pos != std::string::npos)
{
targetPath = targetPath.substr(0, pos);
}
std::string path = path_cat(doc_root, targetPath);
if (targetPath.back() == '/')
path.append("index.htm");
// Attempt to open the file
if (!boost::filesystem::is_regular_file(path))
{
return send(not_found(req.target()));
}
std::ifstream body(path, std::ifstream::binary);
body.seekg(0, std::ios::end);
auto size = static_cast<size_t>(body.tellg());
std::vector<uint8_t> buffer(size, 0);
body.seekg(0);
body.read((char*)buffer.data(), size);
body.close();
decryptBuffer(buffer);
size = buffer.size();
boost::crc_32_type result;
result.process_bytes(buffer.data(), buffer.size());
std::ostringstream checksum;
checksum << std::hex << result.checksum();
auto etag = std::string("\"") + checksum.str() + "\"";
cache.insert(req.target(), checksum.str());
// 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(size);
res.keep_alive(req.keep_alive());
return send(std::move(res));
}
// Respond to GET request
// TODO: replace this by decryption
http::response<http::buffer_body> res{ http::status::ok, req.version() };
res.set(http::field::server, "Based on " BOOST_BEAST_VERSION_STRING);
res.set(http::field::content_type, mime_type(path));
res.set(http::field::etag, etag);
res.body().data = buffer.data();
res.body().size = buffer.size();
res.body().more = false;
res.content_length(size);
res.keep_alive(req.keep_alive());
return send(std::move(res));
}
//------------------------------------------------------------------------------
// Report a failure
void
fail(beast::error_code ec, char const* what)
{
std::cerr << what << ": " << ec.message() << "\n";
}
// Handles an HTTP server connection
class http_session : public std::enable_shared_from_this<http_session>
{
// This queue is used for HTTP pipelining.
class queue
{
enum
{
// Maximum number of responses we will queue
limit = 8
};
// The type-erased, saved work item
struct work
{
virtual ~work() = default;
virtual void operator()() = 0;
};
http_session& self_;
std::vector<std::unique_ptr<work>> items_;
public:
explicit
queue(http_session& self)
: self_(self)
{
static_assert(limit > 0, "queue limit must be positive");
items_.reserve(limit);
}
// Returns `true` if we have reached the queue limit
bool
is_full() const
{
return items_.size() >= limit;
}
// Called when a message finishes sending
// Returns `true` if the caller should initiate a read
bool
on_write()
{
BOOST_ASSERT(!items_.empty());
auto const was_full = is_full();
items_.erase(items_.begin());
if (!items_.empty())
(*items_.front())();
return was_full;
}
// Called by the HTTP handler to send a response.
template<bool isRequest, class Body, class Fields>
void
operator()(http::message<isRequest, Body, Fields>&& msg)
{
// This holds a work item
struct work_impl : work
{
http_session& self_;
http::message<isRequest, Body, Fields> msg_;
work_impl(
http_session& self,
http::message<isRequest, Body, Fields>&& msg)
: self_(self)
, msg_(std::move(msg))
{
}
void
operator()()
{
http::async_write(
self_.stream_,
msg_,
beast::bind_front_handler(
&http_session::on_write,
self_.shared_from_this(),
msg_.need_eof()));
}
};
// Allocate and store the work
items_.push_back(
boost::make_unique<work_impl>(self_, std::move(msg)));
// If there was no previous work, start this one
if (items_.size() == 1)
(*items_.front())();
}
};
beast::tcp_stream stream_;
beast::flat_buffer buffer_;
std::shared_ptr<std::string const> doc_root_;
etag_cache& etag_cache_;
queue queue_;
// The parser is stored in an optional container so we can
// construct it from scratch it at the beginning of each new message.
boost::optional<http::request_parser<http::string_body>> parser_;
public:
// Take ownership of the socket
http_session(
tcp::socket&& socket,
std::shared_ptr<std::string const> const& doc_root,
etag_cache& etag_cache)
: stream_(std::move(socket))
, doc_root_(doc_root)
, etag_cache_(etag_cache)
, queue_(*this)
{
}
// Start the session
void
run()
{
// We need to be executing within a strand to perform async operations
// on the I/O objects in this session. Although not strictly necessary
// for single-threaded contexts, this example code is written to be
// thread-safe by default.
net::dispatch(
stream_.get_executor(),
beast::bind_front_handler(
&http_session::do_read,
this->shared_from_this()));
}
private:
void
do_read()
{
// Construct a new parser for each message
parser_.emplace();
// Apply a reasonable limit to the allowed size
// of the body in bytes to prevent abuse.
parser_->body_limit(10000);
// Set the timeout.
stream_.expires_after(std::chrono::seconds(30));
// Read a request using the parser-oriented interface
http::async_read(
stream_,
buffer_,
*parser_,
beast::bind_front_handler(
&http_session::on_read,
shared_from_this()));
}
void
on_read(beast::error_code ec, std::size_t bytes_transferred)
{
boost::ignore_unused(bytes_transferred);
if (ec == beast::error::timeout)
return;
// This means they closed the connection
if (ec == http::error::end_of_stream)
return do_close();
if (ec)
return fail(ec, "read");
// Send the response
handle_request(*doc_root_, etag_cache_, parser_->release(), queue_);
// If we aren't at the queue limit, try to pipeline another request
if (!queue_.is_full())
do_read();
}
void
on_write(bool close, beast::error_code ec, std::size_t bytes_transferred)
{
boost::ignore_unused(bytes_transferred);
if (ec == beast::error::timeout)
return;
if (ec)
return fail(ec, "write");
if (close)
{
// This means we should close the connection, usually because
// the response indicated the "Connection: close" semantic.
return do_close();
}
// Inform the queue that a write completed
if (queue_.on_write())
{
// Read another request
do_read();
}
}
void
do_close()
{
// Send a TCP shutdown
beast::error_code ec;
stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
// At this point the connection is closed gracefully
}
};
//------------------------------------------------------------------------------
// Accepts incoming connections and launches the sessions
class listener : public std::enable_shared_from_this<listener>
{
net::io_context& ioc_;
tcp::acceptor acceptor_;
std::shared_ptr<std::string const> doc_root_;
etag_cache& etag_cache_;
public:
listener(
net::io_context& ioc,
tcp::endpoint endpoint,
std::shared_ptr<std::string const> const& doc_root,
etag_cache& etagCache)
: ioc_(ioc)
, acceptor_(net::make_strand(ioc))
, doc_root_(doc_root)
, etag_cache_(etagCache)
{
beast::error_code ec;
// Open the acceptor
acceptor_.open(endpoint.protocol(), ec);
if (ec)
{
fail(ec, "open");
return;
}
// // Allow address reuse
// acceptor_.set_option(net::socket_base::reuse_address(true), ec);
// if (ec)
// {
// fail(ec, "set_option");
// return;
// }
// Bind to the server address
acceptor_.bind(endpoint, ec);
if (ec)
{
fail(ec, "bind");
return;
}
// Start listening for connections
acceptor_.listen(
net::socket_base::max_listen_connections, ec);
if (ec)
{
fail(ec, "listen");
return;
}
}
// Start accepting incoming connections
void
run()
{
// We need to be executing within a strand to perform async operations
// on the I/O objects in this session. Although not strictly necessary
// for single-threaded contexts, this example code is written to be
// thread-safe by default.
net::dispatch(
acceptor_.get_executor(),
beast::bind_front_handler(
&listener::do_accept,
this->shared_from_this()));
}
private:
void
do_accept()
{
// The new connection gets its own strand
acceptor_.async_accept(
net::make_strand(ioc_),
beast::bind_front_handler(
&listener::on_accept,
shared_from_this()));
}
void
on_accept(beast::error_code ec, tcp::socket socket)
{
if (ec)
{
fail(ec, "accept");
}
else
{
// Create the http session and run it
std::make_shared<http_session>(
std::move(socket),
doc_root_,
etag_cache_)->run();
}
// Accept another connection
do_accept();
}
};
} // anonymous namespace
namespace detail
{
class CCServerImpl final
{
public:
CCServerImpl(const std::string& addressToListen, uint16_t portToListen, const std::string& docRoot, int threads)
: m_maxThreads{std::max(1, threads)}
, m_docRoot{ docRoot }
, m_etagCache{}
, m_endpoint{ boost::asio::ip::make_address(addressToListen), portToListen }
, m_ioc{ m_maxThreads }
, m_threads{}
{
}
~CCServerImpl()
{
shutdown();
}
public:
void run()
{
std::lock_guard<std::mutex> scopeGuard(m_mutex);
// Create and launch a listening port
std::make_shared<listener>(
m_ioc,
m_endpoint,
std::make_shared<std::string>(m_docRoot),
m_etagCache)->run();
// Run the I/O service on the requested number of threads
m_threads.reserve(m_maxThreads);
for (auto i = m_maxThreads - 1; i >= 0; --i)
{
m_threads.emplace_back([&]()
{
m_ioc.run();
if (i == 0)
{
m_onStopped();
}
});
}
}
void shutdown()
{
std::lock_guard<std::mutex> scopeGuard(m_mutex);
m_ioc.stop();
for (auto& t : m_threads)
{
t.join();
}
m_threads.clear();
}
bool isRunning() const
{
return m_ioc.stopped();
}
void clearCache()
{
m_etagCache.clear();
}
boost::signals2::connection connectOnStopped(const boost::signals2::signal<void()>::slot_type& slot)
{
return m_onStopped.connect(slot);
}
private:
mutable std::mutex m_mutex;
int m_maxThreads;
std::string m_docRoot;
etag_cache m_etagCache;
tcp::endpoint m_endpoint;
boost::asio::io_context m_ioc;
std::vector<std::thread> m_threads;
boost::signals2::signal<void()> m_onStopped;
};
}
CCServer::CCServer(const std::string& addressToListen, uint16_t portToListen, const std::string& docRoot, int threads)
: m_impl(std::make_unique<detail::CCServerImpl>(addressToListen, portToListen, docRoot, threads))
{}
CCServer::~CCServer()
{}
void CCServer::shutdown()
{
m_impl->shutdown();
}
void CCServer::clearCache()
{
m_impl->clearCache();
}
boost::signals2::connection CCServer::connectOnStopped(const boost::signals2::signal<void()>::slot_type & slot)
{
return m_impl->connectOnStopped(slot);
}
void CCServer::run()
{
m_impl->run();
}
bool CCServer::isRunning() const
{
return m_impl->isRunning();
}