#include "CCServer.h" #include "CryptoUtils.h" //------------------------------------------------------------------------------ // // Based on Beast example: Advanced server // //------------------------------------------------------------------------------ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fs = boost::filesystem; // from namespace beast = boost::beast; // from namespace http = beast::http; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; // from static constexpr auto APIS_UINT8_MIN = std::numeric_limits::min(); static constexpr auto APIS_UINT8_MAX = std::numeric_limits::max(); #define MAXHESLO 225 #define MAX_DEKODHESLO (MAXHESLO-3) #define KodcharToChar(znak2,poloha,posun) static_cast(-(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 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(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& 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 decrypted(buffer.size() - cryptFileHeader.size()); is.read((char*)decrypted.data(), decrypted.size()); if (decrypted.size() > sizeof(uint32_t)) { auto headerLen = reinterpret_cast(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 lock(mutex_); map_.emplace(path, etag); } bool check_path(boost::beast::string_view path, boost::beast::string_view etag) const { std::shared_lock lock(mutex_); auto it = map_.find(static_cast(path)); if (it != map_.end() && it->second == etag) { return true; } return false; } void clear() { std::unique_lock lock(mutex_); map_.clear(); } private: mutable std::shared_mutex mutex_; std::unordered_map 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>&& req, Send&& send) { // Returns a bad request response auto const bad_request = [&req](beast::string_view why) { http::response 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 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 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 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> Tokenizer; boost::char_separator 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(body.tellg()); std::vector 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 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 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 { // 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> 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 void operator()(http::message&& msg) { // This holds a work item struct work_impl : work { http_session& self_; http::message msg_; work_impl( http_session& self, http::message&& 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(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 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> parser_; public: // Take ownership of the socket http_session( tcp::socket&& socket, std::shared_ptr 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 { net::io_context& ioc_; tcp::acceptor acceptor_; std::shared_ptr doc_root_; etag_cache& etag_cache_; public: listener( net::io_context& ioc, tcp::endpoint endpoint, std::shared_ptr 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( 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 scopeGuard(m_mutex); // Create and launch a listening port std::make_shared( m_ioc, m_endpoint, std::make_shared(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 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::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 m_threads; boost::signals2::signal m_onStopped; }; } CCServer::CCServer(const std::string& addressToListen, uint16_t portToListen, const std::string& docRoot, int threads) : m_impl(std::make_unique(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::slot_type & slot) { return m_impl->connectOnStopped(slot); } void CCServer::run() { m_impl->run(); } bool CCServer::isRunning() const { return m_impl->isRunning(); }