Skip to content

Instantly share code, notes, and snippets.

@RElesgoe
Created April 13, 2018 06:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RElesgoe/adea8f20cef0f5d5480b94cf59ba1e28 to your computer and use it in GitHub Desktop.
Save RElesgoe/adea8f20cef0f5d5480b94cf59ba1e28 to your computer and use it in GitHub Desktop.
/*
* Copyright (C) 2000 Onlyer (onlyer@263.net)
* Copyright (C) 2001 Ross Combs (ross@bnetd.org)
* Copyright (C) 2002 Gianluigi Tiesi (sherpya@netfarm.it)
* Copyright (C) 2004 CreepLord (creeplord@pvpgn.org)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include "common/setup_before.h"
#define VERSIONCHECK_INTERNAL_ACCESS
#include "versioncheck.h"
#include <cctype>
#include <cerrno>
#include <climits>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <forward_list>
#include <fstream>
#include <iomanip>
#include <regex>
#include <sstream>
#include <string>
#include "compat/strcasecmp.h"
#include "common/format.h"
#include "common/eventlog.h"
#include "common/util.h"
#include "common/field_sizes.h"
#include "common/token.h"
#include "common/proginfo.h"
#include "json/json.hpp"
#include "prefs.h"
#include "common/setup_after.h"
using json = nlohmann::json;
namespace pvpgn
{
namespace bnetd
{
std::forward_list<VersionCheck> vc_entries = std::forward_list<VersionCheck>();
bool versioncheck_conf_is_loaded = false;
struct file_metadata parse_file_metadata(const std::string& unparsed_metadata);
bool compare_file_metadata(const struct file_metadata& pattern, const struct file_metadata& match, bool skip_timestamp_match);
bool load_versioncheck_conf(const std::string& filename)
{
if (versioncheck_conf_is_loaded)
{
eventlog(eventlog_level_error, __FUNCTION__, "Could not load {}, a versioncheck configuration file is already loaded", filename);
return false;
}
std::ifstream file_stream(filename, std::ios::in);
if (!file_stream.is_open())
{
eventlog(eventlog_level_error, __FUNCTION__, "Could not open file \"{}\" for reading", filename);
return false;
}
json jconf;
try
{
file_stream >> jconf;
}
catch (const std::exception& e)
{
eventlog(eventlog_level_error, __FUNCTION__, "Could not parse {}: {}", filename, e.what());
return false;
}
int entry_success_count = 0;
int entry_total_count = 0;
for (auto jclient : jconf.items())
{
for (auto jarchitecture : jclient.value().items())
{
for (auto jversionid : jarchitecture.value().items())
{
// FIXME: do error checking for these
std::string checkrevision_filename = jversionid.value()["checkRevisionFile"].get<std::string>();
std::string checkrevision_equation = jversionid.value()["equation"].get<std::string>();
for (auto jentry : jversionid.value()["entries"])
{
try
{
entry_total_count += 1;
VersionCheck entry(jentry["title"].get<std::string>(),
jversionid.key(),
jentry["version"].get<std::string>(),
jentry["hash"].get<std::string>(),
jarchitecture.key(), jclient.key(),
jentry["fileMetadata"].get<std::string>(),
jentry["versionTag"].get<std::string>());
vc_entries.push_front(entry);
entry_success_count += 1;
}
catch (const std::exception& e)
{
eventlog(eventlog_level_error, __FUNCTION__, "Failed to load versioncheck entry: {}", e.what());
continue;
}
}
}
}
}
eventlog(eventlog_level_info, __FUNCTION__, "Successfully loaded {} out of {} versioncheck entries", entry_success_count, entry_total_count);
versioncheck_conf_is_loaded = true;
return true;
}
void unload_versioncheck_conf()
{
if (versioncheck_conf_is_loaded)
{
vc_entries.clear();
eventlog(eventlog_level_info, __FUNCTION__, "Successfully unloaded all version check entries");
versioncheck_conf_is_loaded = false;
}
else
{
eventlog(eventlog_level_error, __FUNCTION__, "Caught attempt to unload all version check entries with no loaded entries");
}
}
const checkrevision_file select_checkrevision_file(t_tag architecture, t_tag client, std::uint32_t version_id)
{
static const checkrevision_file invalid_checkrevision = { "ver-IX86-1.mpq", "A=42 B=42 C=42 4 A=A^S B=B^B C=C^C A=A^S" };
for (const auto& entry : vc_entries)
{
if (entry.m_architecture == architecture
&& entry.m_client == client
&& entry.m_version_id == version_id)
{
return entry;
}
}
return invalid_checkrevision;
}
VersionCheck::VersionCheck(const std::string& title, const std::string& version_id, const std::string& game_version,
const std::string& checksum, const std::string& architecture, const std::string& client,
const std::string& metadata, const std::string& version_tag)
{
this->m_version_id = std::stoul(version_id.c_str(), nullptr, 0); // version byte
if (verstr_to_vernum(game_version.c_str(), reinterpret_cast<unsigned long *>(this->m_game_version)) < 0)
{
throw std::runtime_error("Invalid version \"" + game_version + "\" in entry \"" + title + "\"");
}
this->m_checksum = std::stoul(checksum, nullptr, 0);
this->m_architecture = tag_str_to_uint(architecture.c_str());
if (!tag_check_arch(this->m_architecture))
{
throw std::runtime_error("Invalid architecture \"" + architecture + "\" in entry \"" + title + "\"");
}
this->m_client = tag_str_to_uint(client.c_str());
if (!tag_check_client(this->m_client))
{
throw std::runtime_error("Invalid client \"" + client + "\" in entry \"" + title + "\"");
}
this->m_metadata = parse_file_metadata(metadata);
if (this->m_metadata.filename.empty()
&& this->m_metadata.file_size == 0
&& this->m_metadata.timestamp == 0)
{
throw std::runtime_error("Invalid file_metadata \"" + metadata + "\" in entry \"" + title + "\"");
}
// FIXME: check for uniqueness
this->m_version_tag = version_tag;
}
bool VersionCheck::validate_checkrevision_data(std::uint32_t game_version, std::uint32_t checksum, const std::string& unparsed_file_metadata) const
{
if (this->m_version_tag == "NoVC")
{
return false;
}
if (this->m_game_version != game_version)
{
eventlog(eventlog_level_info, __FUNCTION__, "Failed CheckRevision: Invalid game version \"{X}\"", game_version);
return false;
}
if (this->m_checksum != 0
&& prefs_get_allow_bad_version())
{
if (this->m_checksum != checksum)
{
eventlog(eventlog_level_info, __FUNCTION__, "Failed CheckRevision: Invalid checksum \"{X}\"", checksum);
return false;
}
}
else
{
// checksum can be disabled globally via setting allow_bad_version = true in conf/bnetd.conf
// it can also be disabled on individual versioncheck entries by setting 0 in the checksum field in conf/versioncheck.conf
eventlog(eventlog_level_debug, __FUNCTION__, "Skipping checksum validation");
}
if (unparsed_file_metadata != "NULL"
&& std::strcmp(prefs_get_version_exeinfo_match(), "true") == 0)
{
if (unparsed_file_metadata == "badexe")
{
// missing or too long file metadata received
eventlog(eventlog_level_info, __FUNCTION__, "Failed CheckRevision: Invalid file metadata \"{}\"", unparsed_file_metadata);
return false;
}
struct file_metadata parsed_metadata = parse_file_metadata(unparsed_file_metadata);;
if (parsed_metadata.filename.empty()
&& parsed_metadata.file_size == 0
&& parsed_metadata.timestamp == 0)
{
eventlog(eventlog_level_info, __FUNCTION__, "Failed CheckRevision: Invalid file metadata \"{}\"", unparsed_file_metadata);
return false;
}
// Skip timestamp matching if client is WC3
// WC3 installers change the file timestamp to time of installation
if (!compare_file_metadata(this->m_metadata, parsed_metadata,
this->m_client == CLIENTTAG_WARCRAFT3_UINT || this->m_client == CLIENTTAG_WAR3XP_UINT ? true : false)
)
{
eventlog(eventlog_level_info, __FUNCTION__, "Failed CheckRevision: Invalid file metadata \"{}\"", unparsed_file_metadata);
return false;
}
}
else
{
// metadata matching can be disabled globally via setting version_exeinfo_match = false in conf/bnetd.conf
// it can also be disabled on individual versioncheck entries by setting "NULL" in the exeinfo field in conf/versioncheck.conf
eventlog(eventlog_level_debug, __FUNCTION__, "Skipping file metadata validation");
}
return true;
}
std::string VersionCheck::get_version_tag() const noexcept
{
return this->m_version_tag;
}
struct file_metadata parse_file_metadata(const std::string& unparsed_metadata)
{
// happens when using war3-noCD and having deleted war3.org
if (unparsed_metadata.empty())
{
eventlog(eventlog_level_error, __FUNCTION__, "got empty file metadata string");
return {};
}
if (unparsed_metadata == "NULL")
{
eventlog(eventlog_level_info, __FUNCTION__, "got \"NULL\" as file metadata string");
return {};
}
// happens when AUTHREQ had no owner/exeinfo entry
if (unparsed_metadata == "badexe")
{
eventlog(eventlog_level_error, __FUNCTION__, "got \"badexe\" as file metadata string");
return {};
}
std::smatch tokens;
try
{
if (std::regex_match(unparsed_metadata, tokens, std::regex{ R"(([[:print:]]+\.exe) (\d\d/\d\d/\d\d \d\d:\d\d:\d\d) (\d+))" }) == false)
{
eventlog(eventlog_level_error, __FUNCTION__, "got invalid file metadata string \"{}\"", unparsed_metadata);
return {};
}
}
catch (const std::regex_error& e)
{
eventlog(eventlog_level_error, __FUNCTION__, "{} (code {})", e.what(), e.code());
return {};
}
// Example metadata string
// "Warcraft III.exe 07/07/17 20:15:59 562152"
std::string filename = tokens[1];
std::tm timestamp_raw;
timestamp_raw.tm_isdst = -1;
std::istringstream ss(tokens[2]);
// Since year is 2 digits,
// Range [69,99] results in values 1969 to 1999, range [00,68] results in 2000-2068
ss >> std::get_time(&timestamp_raw, "%D %T");
if (ss.fail())
{
eventlog(eventlog_level_error, __FUNCTION__, "got invalid date and time in file metadata string");
return {};
}
std::time_t timestamp = std::mktime(&timestamp_raw);
if (timestamp == -1)
{
eventlog(eventlog_level_error, __FUNCTION__, "time could not be represented as a std::time_t object");
return {};
}
std::uint64_t filesize;
try
{
filesize = std::stoull(tokens[3]);
}
catch (const std::exception& e)
{
eventlog(eventlog_level_error, __FUNCTION__, "could not convert file size in file metadata string: {}", e.what());
return {};
}
return { timestamp, filesize, filename };
}
bool compare_file_metadata(const struct file_metadata& pattern, const struct file_metadata& match, bool skip_timestamp_match)
{
if (skip_timestamp_match == false
&& pattern.timestamp != match.timestamp)
{
return false;
}
if (pattern.file_size != match.file_size)
{
return false;
}
// Case insensitive comparison of filename
if (strcasecmp(pattern.filename.c_str(), match.filename.c_str()) != 0)
{
return false;
}
return true;
}
}
}
/*
* Copyright (C) 2000 Onlyer (onlyer@263.net)
* Copyright (C) 2001 Ross Combs (ross@bnetd.org)
* Copyright (C) 2002 Gianluigi Tiesi (sherpya@netfarm.it)
* Copyright (C) 2004 CreepLord (creeplord@pvpgn.org)
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#ifndef PVPGN_BNETD_VERSIONCHECK_H
#define PVPGN_BNETD_VERSIONCHECK_H
#include <cstdint>
#include <ctime>
#include <string>
#include "common/tag.h"
namespace pvpgn
{
namespace bnetd
{
class VersionCheck;
struct file_metadata
{
std::time_t timestamp;
std::uint64_t file_size;
std::string filename;
};
struct checkrevision_file
{
std::string filename;
std::string equation;
};
bool load_versioncheck_conf(const std::string& filename);
void unload_versioncheck_conf();
const checkrevision_file select_checkrevision_file(t_tag architecture, t_tag client, std::uint32_t version_id);
class VersionCheck
{
public:
bool validate_checkrevision_data(std::uint32_t game_version, std::uint32_t checksum, const std::string& unparsed_file_metadata) const;
/*******************************************************************************/
// Getters
/*******************************************************************************/
std::string get_version_tag() const noexcept;
/*******************************************************************************/
// Deconstructors
/*******************************************************************************/
~VersionCheck() = default;
private:
/*******************************************************************************/
// Friend functions
/*******************************************************************************/
friend bool load_versioncheck_conf(const std::string& filename);
friend const checkrevision_file select_checkrevision_file(t_tag architecture, t_tag client, std::uint32_t version_id);
/*******************************************************************************/
// Constructors
/*******************************************************************************/
VersionCheck(const std::string& title, const std::string& version_id, const std::string& game_version,
const std::string& checksum, const std::string& architecture, const std::string& client,
const std::string& metadata, const std::string& version_tag);
/*******************************************************************************/
// Member variables
/*******************************************************************************/
std::uint32_t m_version_id; // AKA "Version Byte"
std::uint32_t m_game_version; // Windows file version
std::uint32_t m_checksum;
t_tag m_architecture;
t_tag m_client;
struct file_metadata m_metadata;
std::string m_version_tag;
};
} // namespace bnetd
} // namespace pvpgn
#endif //PVPGN_BNETD_VERSIONCHECK_H
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment