[Loginserver] Worldserver Name Sanitization (#1739)

* Sanitize bad words in server names

* Add config options and enforcement for dev/test servers and servers starting with a special character

* Refine bad word logic

* Add installer to dev/test servers

* Change server prefixes

* Special char prefix

* Formatting

* Remove multi words

* Add server types enum

* Add error constants

* Remove sanitize from world level

* Use strn0cpy
This commit is contained in:
Chris Miles 2021-11-12 23:02:05 -06:00 committed by GitHub
parent 8b83a13560
commit a9d1034298
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 726 additions and 243 deletions

View File

@ -19,32 +19,33 @@
#include <cctype> #include <cctype>
#ifdef _WINDOWS #ifdef _WINDOWS
#include <windows.h> #include <windows.h>
#define snprintf _snprintf #define snprintf _snprintf
#define strncasecmp _strnicmp #define strncasecmp _strnicmp
#define strcasecmp _stricmp #define strcasecmp _stricmp
#else #else
#include <stdlib.h>
#include <stdio.h> #include <stdlib.h>
#include <stdio.h>
#include <iostream> #include <iostream>
#endif #endif
#ifndef va_copy #ifndef va_copy
#define va_copy(d,s) ((d) = (s)) #define va_copy(d,s) ((d) = (s))
#endif #endif
// original source: // original source:
// https://github.com/facebook/folly/blob/master/folly/String.cpp // https://github.com/facebook/folly/blob/master/folly/String.cpp
// //
const std::string vStringFormat(const char* format, va_list args) const std::string vStringFormat(const char *format, va_list args)
{ {
std::string output; std::string output;
va_list tmpargs; va_list tmpargs;
va_copy(tmpargs,args); va_copy(tmpargs, args);
int characters_used = vsnprintf(nullptr, 0, format, tmpargs); int characters_used = vsnprintf(nullptr, 0, format, tmpargs);
va_end(tmpargs); va_end(tmpargs);
@ -52,7 +53,7 @@ const std::string vStringFormat(const char* format, va_list args)
if (characters_used > 0) { if (characters_used > 0) {
output.resize(characters_used + 1); output.resize(characters_used + 1);
va_copy(tmpargs,args); va_copy(tmpargs, args);
characters_used = vsnprintf(&output[0], output.capacity(), format, tmpargs); characters_used = vsnprintf(&output[0], output.capacity(), format, tmpargs);
va_end(tmpargs); va_end(tmpargs);
@ -60,8 +61,9 @@ const std::string vStringFormat(const char* format, va_list args)
// We shouldn't have a format error by this point, but I can't imagine what error we // We shouldn't have a format error by this point, but I can't imagine what error we
// could have by this point. Still, return empty string; // could have by this point. Still, return empty string;
if (characters_used < 0) if (characters_used < 0) {
output.clear(); output.clear();
}
} }
return output; return output;
} }
@ -87,8 +89,9 @@ const std::string str_toupper(std::string s)
const std::string ucfirst(std::string s) const std::string ucfirst(std::string s)
{ {
std::string output = s; std::string output = s;
if (!s.empty()) if (!s.empty()) {
output[0] = static_cast<char>(::toupper(s[0])); output[0] = static_cast<char>(::toupper(s[0]));
}
return output; return output;
} }
@ -102,18 +105,20 @@ const std::string StringFormat(const char *format, ...)
return output; return output;
} }
std::vector<std::string> SplitString(const std::string &str, const char delim) { std::vector<std::string> SplitString(const std::string &str, const char delim)
{
std::vector<std::string> ret; std::vector<std::string> ret;
std::string::size_type start = 0; std::string::size_type start = 0;
auto end = str.find(delim); auto end = str.find(delim);
while (end != std::string::npos) { while (end != std::string::npos) {
ret.emplace_back(str, start, end - start); ret.emplace_back(str, start, end - start);
start = end + 1; start = end + 1;
end = str.find(delim, start); end = str.find(delim, start);
} }
// this will catch the last word since the string is unlikely to end with a delimiter // this will catch the last word since the string is unlikely to end with a delimiter
if (str.length() > start) if (str.length() > start) {
ret.emplace_back(str, start, str.length() - start); ret.emplace_back(str, start, str.length() - start);
}
return ret; return ret;
} }
@ -153,14 +158,16 @@ std::string get_between(const std::string &s, std::string start_delim, std::stri
return ""; return "";
} }
std::string::size_type search_deliminated_string(const std::string &haystack, const std::string &needle, const char deliminator) std::string::size_type
search_deliminated_string(const std::string &haystack, const std::string &needle, const char deliminator)
{ {
// this shouldn't go out of bounds, even without obvious bounds checks // this shouldn't go out of bounds, even without obvious bounds checks
auto pos = haystack.find(needle); auto pos = haystack.find(needle);
while (pos != std::string::npos) { while (pos != std::string::npos) {
auto c = haystack[pos + needle.length()]; auto c = haystack[pos + needle.length()];
if ((c == '\0' || c == deliminator) && (pos == 0 || haystack[pos - 1] == deliminator)) if ((c == '\0' || c == deliminator) && (pos == 0 || haystack[pos - 1] == deliminator)) {
return pos; return pos;
}
pos = haystack.find(needle, pos + needle.length()); pos = haystack.find(needle, pos + needle.length());
} }
return std::string::npos; return std::string::npos;
@ -180,7 +187,7 @@ std::string implode(std::string glue, std::vector<std::string> src)
} }
std::string final_output = output.str(); std::string final_output = output.str();
final_output.resize (output.str().size () - glue.size()); final_output.resize(output.str().size() - glue.size());
return final_output; return final_output;
} }
@ -202,80 +209,83 @@ std::vector<std::string> wrap(std::vector<std::string> &src, std::string charact
return new_vector; return new_vector;
} }
std::string EscapeString(const std::string &s) { std::string EscapeString(const std::string &s)
{
std::string ret; std::string ret;
size_t sz = s.length(); size_t sz = s.length();
for(size_t i = 0; i < sz; ++i) { for (size_t i = 0; i < sz; ++i) {
char c = s[i]; char c = s[i];
switch(c) { switch (c) {
case '\x00': case '\x00':
ret += "\\x00"; ret += "\\x00";
break; break;
case '\n': case '\n':
ret += "\\n"; ret += "\\n";
break; break;
case '\r': case '\r':
ret += "\\r"; ret += "\\r";
break; break;
case '\\': case '\\':
ret += "\\\\"; ret += "\\\\";
break; break;
case '\'': case '\'':
ret += "\\'"; ret += "\\'";
break; break;
case '\"': case '\"':
ret += "\\\""; ret += "\\\"";
break; break;
case '\x1a': case '\x1a':
ret += "\\x1a"; ret += "\\x1a";
break; break;
default: default:
ret.push_back(c); ret.push_back(c);
break; break;
} }
} }
return ret; return ret;
} }
std::string EscapeString(const char *src, size_t sz) { std::string EscapeString(const char *src, size_t sz)
{
std::string ret; std::string ret;
for(size_t i = 0; i < sz; ++i) { for (size_t i = 0; i < sz; ++i) {
char c = src[i]; char c = src[i];
switch(c) { switch (c) {
case '\x00': case '\x00':
ret += "\\x00"; ret += "\\x00";
break; break;
case '\n': case '\n':
ret += "\\n"; ret += "\\n";
break; break;
case '\r': case '\r':
ret += "\\r"; ret += "\\r";
break; break;
case '\\': case '\\':
ret += "\\\\"; ret += "\\\\";
break; break;
case '\'': case '\'':
ret += "\\'"; ret += "\\'";
break; break;
case '\"': case '\"':
ret += "\\\""; ret += "\\\"";
break; break;
case '\x1a': case '\x1a':
ret += "\\x1a"; ret += "\\x1a";
break; break;
default: default:
ret.push_back(c); ret.push_back(c);
break; break;
} }
} }
return ret; return ret;
} }
bool StringIsNumber(const std::string &s) { bool StringIsNumber(const std::string &s)
{
try { try {
auto r = stod(s); auto r = stod(s);
return true; return true;
@ -285,15 +295,18 @@ bool StringIsNumber(const std::string &s) {
} }
} }
void ToLowerString(std::string &s) { void ToLowerString(std::string &s)
{
std::transform(s.begin(), s.end(), s.begin(), ::tolower); std::transform(s.begin(), s.end(), s.begin(), ::tolower);
} }
void ToUpperString(std::string &s) { void ToUpperString(std::string &s)
{
std::transform(s.begin(), s.end(), s.begin(), ::toupper); std::transform(s.begin(), s.end(), s.begin(), ::toupper);
} }
std::string JoinString(const std::vector<std::string>& ar, const std::string &delim) { std::string JoinString(const std::vector<std::string> &ar, const std::string &delim)
{
std::string ret; std::string ret;
for (size_t i = 0; i < ar.size(); ++i) { for (size_t i = 0; i < ar.size(); ++i) {
if (i != 0) { if (i != 0) {
@ -313,7 +326,7 @@ void find_replace(std::string &string_subject, const std::string &search_string,
} }
size_t start_pos = 0; size_t start_pos = 0;
while((start_pos = string_subject.find(search_string, start_pos)) != std::string::npos) { while ((start_pos = string_subject.find(search_string, start_pos)) != std::string::npos) {
string_subject.replace(start_pos, search_string.length(), replace_string); string_subject.replace(start_pos, search_string.length(), replace_string);
start_pos += replace_string.length(); start_pos += replace_string.length();
} }
@ -334,9 +347,9 @@ void ParseAccountString(const std::string &s, std::string &account, std::string
auto split = SplitString(s, ':'); auto split = SplitString(s, ':');
if (split.size() == 2) { if (split.size() == 2) {
loginserver = split[0]; loginserver = split[0];
account = split[1]; account = split[1];
} }
else if(split.size() == 1) { else if (split.size() == 1) {
account = split[0]; account = split[0];
} }
} }
@ -345,9 +358,11 @@ void ParseAccountString(const std::string &s, std::string &account, std::string
// normal strncpy doesnt put a null term on copied strings, this one does // normal strncpy doesnt put a null term on copied strings, this one does
// ref: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wcecrt/htm/_wcecrt_strncpy_wcsncpy.asp // ref: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wcecrt/htm/_wcecrt_strncpy_wcsncpy.asp
char* strn0cpy(char* dest, const char* source, uint32 size) { char *strn0cpy(char *dest, const char *source, uint32 size)
if (!dest) {
if (!dest) {
return 0; return 0;
}
if (size == 0 || source == 0) { if (size == 0 || source == 0) {
dest[0] = 0; dest[0] = 0;
return dest; return dest;
@ -359,123 +374,159 @@ char* strn0cpy(char* dest, const char* source, uint32 size) {
// String N w/null Copy Truncated? // String N w/null Copy Truncated?
// return value =true if entire string(source) fit, false if it was truncated // return value =true if entire string(source) fit, false if it was truncated
bool strn0cpyt(char* dest, const char* source, uint32 size) { bool strn0cpyt(char *dest, const char *source, uint32 size)
if (!dest) {
if (!dest) {
return 0; return 0;
}
if (size == 0 || source == 0) { if (size == 0 || source == 0) {
dest[0] = 0; dest[0] = 0;
return false; return false;
} }
strncpy(dest, source, size); strncpy(dest, source, size);
dest[size - 1] = 0; dest[size - 1] = 0;
return (bool)(source[strlen(dest)] == 0); return (bool) (source[strlen(dest)] == 0);
} }
const char *MakeLowerString(const char *source) { const char *MakeLowerString(const char *source)
{
static char str[128]; static char str[128];
if (!source) if (!source) {
return nullptr; return nullptr;
}
MakeLowerString(source, str); MakeLowerString(source, str);
return str; return str;
} }
void MakeLowerString(const char *source, char *target) { void MakeLowerString(const char *source, char *target)
{
if (!source || !target) { if (!source || !target) {
*target = 0; *target = 0;
return; return;
} }
while (*source) while (*source) {
{
*target = tolower(*source); *target = tolower(*source);
target++; source++; target++;
source++;
} }
*target = 0; *target = 0;
} }
uint32 hextoi(const char* num) { uint32 hextoi(const char *num)
if (num == nullptr) {
if (num == nullptr) {
return 0; return 0;
}
int len = strlen(num); int len = strlen(num);
if (len < 3) if (len < 3) {
return 0; return 0;
}
if (num[0] != '0' || (num[1] != 'x' && num[1] != 'X')) if (num[0] != '0' || (num[1] != 'x' && num[1] != 'X')) {
return 0; return 0;
}
uint32 ret = 0; uint32 ret = 0;
int mul = 1; int mul = 1;
for (int i = len - 1; i >= 2; i--) { for (int i = len - 1; i >= 2; i--) {
if (num[i] >= 'A' && num[i] <= 'F') if (num[i] >= 'A' && num[i] <= 'F') {
ret += ((num[i] - 'A') + 10) * mul; ret += ((num[i] - 'A') + 10) * mul;
else if (num[i] >= 'a' && num[i] <= 'f') }
else if (num[i] >= 'a' && num[i] <= 'f') {
ret += ((num[i] - 'a') + 10) * mul; ret += ((num[i] - 'a') + 10) * mul;
else if (num[i] >= '0' && num[i] <= '9') }
else if (num[i] >= '0' && num[i] <= '9') {
ret += (num[i] - '0') * mul; ret += (num[i] - '0') * mul;
else }
else {
return 0; return 0;
}
mul *= 16; mul *= 16;
} }
return ret; return ret;
} }
uint64 hextoi64(const char* num) { uint64 hextoi64(const char *num)
if (num == nullptr) {
if (num == nullptr) {
return 0; return 0;
}
int len = strlen(num); int len = strlen(num);
if (len < 3) if (len < 3) {
return 0; return 0;
}
if (num[0] != '0' || (num[1] != 'x' && num[1] != 'X')) if (num[0] != '0' || (num[1] != 'x' && num[1] != 'X')) {
return 0; return 0;
}
uint64 ret = 0; uint64 ret = 0;
int mul = 1; int mul = 1;
for (int i = len - 1; i >= 2; i--) { for (int i = len - 1; i >= 2; i--) {
if (num[i] >= 'A' && num[i] <= 'F') if (num[i] >= 'A' && num[i] <= 'F') {
ret += ((num[i] - 'A') + 10) * mul; ret += ((num[i] - 'A') + 10) * mul;
else if (num[i] >= 'a' && num[i] <= 'f') }
else if (num[i] >= 'a' && num[i] <= 'f') {
ret += ((num[i] - 'a') + 10) * mul; ret += ((num[i] - 'a') + 10) * mul;
else if (num[i] >= '0' && num[i] <= '9') }
else if (num[i] >= '0' && num[i] <= '9') {
ret += (num[i] - '0') * mul; ret += (num[i] - '0') * mul;
else }
else {
return 0; return 0;
}
mul *= 16; mul *= 16;
} }
return ret; return ret;
} }
bool atobool(const char* iBool) { bool atobool(const char *iBool)
{
if (iBool == nullptr) if (iBool == nullptr) {
return false; return false;
if (!strcasecmp(iBool, "true")) }
if (!strcasecmp(iBool, "true")) {
return true; return true;
if (!strcasecmp(iBool, "false")) }
if (!strcasecmp(iBool, "false")) {
return false; return false;
if (!strcasecmp(iBool, "yes")) }
if (!strcasecmp(iBool, "yes")) {
return true; return true;
if (!strcasecmp(iBool, "no")) }
if (!strcasecmp(iBool, "no")) {
return false; return false;
if (!strcasecmp(iBool, "on")) }
if (!strcasecmp(iBool, "on")) {
return true; return true;
if (!strcasecmp(iBool, "off")) }
if (!strcasecmp(iBool, "off")) {
return false; return false;
if (!strcasecmp(iBool, "enable")) }
if (!strcasecmp(iBool, "enable")) {
return true; return true;
if (!strcasecmp(iBool, "disable")) }
if (!strcasecmp(iBool, "disable")) {
return false; return false;
if (!strcasecmp(iBool, "enabled")) }
if (!strcasecmp(iBool, "enabled")) {
return true; return true;
if (!strcasecmp(iBool, "disabled")) }
if (!strcasecmp(iBool, "disabled")) {
return false; return false;
if (!strcasecmp(iBool, "y")) }
if (!strcasecmp(iBool, "y")) {
return true; return true;
if (!strcasecmp(iBool, "n")) }
if (!strcasecmp(iBool, "n")) {
return false; return false;
if (atoi(iBool)) }
if (atoi(iBool)) {
return true; return true;
}
return false; return false;
} }
@ -484,21 +535,19 @@ char *CleanMobName(const char *in, char *out)
{ {
unsigned i, j; unsigned i, j;
for (i = j = 0; i < strlen(in); i++) for (i = j = 0; i < strlen(in); i++) {
{
// convert _ to space.. any other conversions like this? I *think* this // convert _ to space.. any other conversions like this? I *think* this
// is the only non alpha char that's not stripped but converted. // is the only non alpha char that's not stripped but converted.
if (in[i] == '_') if (in[i] == '_') {
{
out[j++] = ' '; out[j++] = ' ';
} }
else else {
{ if (isalpha(in[i]) || (in[i] == '`')) { // numbers, #, or any other crap just gets skipped
if (isalpha(in[i]) || (in[i] == '`')) // numbers, #, or any other crap just gets skipped
out[j++] = in[i]; out[j++] = in[i];
}
} }
} }
out[j] = 0; // terimnate the string before returning it out[j] = 0; // terimnate the string before returning it
return out; return out;
} }
@ -506,8 +555,9 @@ char *CleanMobName(const char *in, char *out)
void RemoveApostrophes(std::string &s) void RemoveApostrophes(std::string &s)
{ {
for (unsigned int i = 0; i < s.length(); ++i) for (unsigned int i = 0; i < s.length(); ++i)
if (s[i] == '\'') if (s[i] == '\'') {
s[i] = '_'; s[i] = '_';
}
} }
char *RemoveApostrophes(const char *s) char *RemoveApostrophes(const char *s)
@ -517,8 +567,9 @@ char *RemoveApostrophes(const char *s)
strcpy(NewString, s); strcpy(NewString, s);
for (unsigned int i = 0; i < strlen(NewString); ++i) for (unsigned int i = 0; i < strlen(NewString); ++i)
if (NewString[i] == '\'') if (NewString[i] == '\'') {
NewString[i] = '_'; NewString[i] = '_';
}
return NewString; return NewString;
} }
@ -537,11 +588,12 @@ const char *ConvertArrayF(float input, char *returnchar)
bool isAlphaNumeric(const char *text) bool isAlphaNumeric(const char *text)
{ {
for (unsigned int charIndex = 0; charIndex<strlen(text); charIndex++) { for (unsigned int charIndex = 0; charIndex < strlen(text); charIndex++) {
if ((text[charIndex] < 'a' || text[charIndex] > 'z') && if ((text[charIndex] < 'a' || text[charIndex] > 'z') &&
(text[charIndex] < 'A' || text[charIndex] > 'Z') && (text[charIndex] < 'A' || text[charIndex] > 'Z') &&
(text[charIndex] < '0' || text[charIndex] > '9')) (text[charIndex] < '0' || text[charIndex] > '9')) {
return false; return false;
}
} }
return true; return true;
@ -596,11 +648,10 @@ std::string numberToWords(unsigned long long int n)
} }
// first letter capitalized and rest made lower case // first letter capitalized and rest made lower case
std::string FormatName(const std::string& char_name) std::string FormatName(const std::string &char_name)
{ {
std::string formatted(char_name); std::string formatted(char_name);
if (!formatted.empty()) if (!formatted.empty()) {
{
std::transform(formatted.begin(), formatted.end(), formatted.begin(), ::tolower); std::transform(formatted.begin(), formatted.end(), formatted.begin(), ::tolower);
formatted[0] = ::toupper(formatted[0]); formatted[0] = ::toupper(formatted[0]);
} }
@ -620,6 +671,12 @@ bool IsAllowedWorldServerCharacterList(char c)
void SanitizeWorldServerName(char *name) void SanitizeWorldServerName(char *name)
{ {
std::string server_long_name = name; std::string server_long_name = name;
strcpy(name, SanitizeWorldServerName(server_long_name).c_str());
}
std::string SanitizeWorldServerName(std::string server_long_name)
{
server_long_name.erase( server_long_name.erase(
std::remove_if( std::remove_if(
server_long_name.begin(), server_long_name.begin(),
@ -632,5 +689,333 @@ void SanitizeWorldServerName(char *name)
server_long_name = trim(server_long_name); server_long_name = trim(server_long_name);
strcpy(name, server_long_name.c_str()); // bad word filter
for (auto &piece: split_string(server_long_name, " ")) {
for (auto &word: GetBadWords()) {
// for shorter words that can actually be part of legitimate words
// make sure that it isn't part of another word by matching on a space
if (str_tolower(piece) == word) {
find_replace(
server_long_name,
piece,
repeat("*", (int) word.length())
);
continue;
}
auto pos = str_tolower(piece).find(word);
if (str_tolower(piece).find(word) != std::string::npos && piece.length() > 4 && word.length() > 4) {
auto found_word = piece.substr(pos, word.length());
std::string replaced_piece = piece.substr(pos, word.length());
find_replace(
server_long_name,
replaced_piece,
repeat("*", (int) word.length())
);
}
}
}
return server_long_name;
}
std::string repeat(std::string s, int n)
{
std::string s1 = s;
for (int i = 1; i < n; i++) {
s += s1;
}
return s;
}
std::vector<std::string> GetBadWords()
{
return std::vector<std::string>{
"2g1c",
"acrotomophilia",
"anal",
"anilingus",
"anus",
"apeshit",
"arsehole",
"ass",
"asshole",
"assmunch",
"autoerotic",
"babeland",
"bangbros",
"bangbus",
"bareback",
"barenaked",
"bastard",
"bastardo",
"bastinado",
"bbw",
"bdsm",
"beaner",
"beaners",
"beaver",
"beastiality",
"bestiality",
"bimbos",
"birdlock",
"bitch",
"bitches",
"blowjob",
"blumpkin",
"bollocks",
"bondage",
"boner",
"boob",
"boobs",
"bukkake",
"bulldyke",
"bullshit",
"bung",
"bunghole",
"busty",
"butt",
"buttcheeks",
"butthole",
"camel toe",
"camgirl",
"camslut",
"camwhore",
"carpetmuncher",
"cialis",
"circlejerk",
"clit",
"clitoris",
"clusterfuck",
"cock",
"cocks",
"coprolagnia",
"coprophilia",
"cornhole",
"coon",
"coons",
"creampie",
"cum",
"cumming",
"cumshot",
"cumshots",
"cunnilingus",
"cunt",
"darkie",
"daterape",
"deepthroat",
"dendrophilia",
"dick",
"dildo",
"dingleberry",
"dingleberries",
"doggiestyle",
"doggystyle",
"dolcett",
"domination",
"dominatrix",
"dommes",
"hump",
"dvda",
"ecchi",
"ejaculation",
"erotic",
"erotism",
"escort",
"eunuch",
"fag",
"faggot",
"fecal",
"felch",
"fellatio",
"feltch",
"femdom",
"figging",
"fingerbang",
"fingering",
"fisting",
"footjob",
"frotting",
"fuck",
"fuckin",
"fucking",
"fucktards",
"fudgepacker",
"futanari",
"gangbang",
"gangbang",
"gaysex",
"genitals",
"goatcx",
"goatse",
"gokkun",
"goodpoop",
"goregasm",
"grope",
"g-spot",
"guro",
"handjob",
"hentai",
"homoerotic",
"honkey",
"hooker",
"horny",
"humping",
"incest",
"intercourse",
"jailbait",
"jigaboo",
"jiggaboo",
"jiggerboo",
"jizz",
"juggs",
"kike",
"kinbaku",
"kinkster",
"kinky",
"knobbing",
"livesex",
"lolita",
"lovemaking",
"masturbate",
"masturbating",
"masturbation",
"milf",
"mong",
"motherfucker",
"muffdiving",
"nambla",
"nawashi",
"negro",
"neonazi",
"nigga",
"nigger",
"nimphomania",
"nipple",
"nipples",
"nsfw",
"nude",
"nudity",
"nutten",
"nympho",
"nymphomania",
"octopussy",
"omorashi",
"orgasm",
"orgy",
"paedophile",
"paki",
"panties",
"panty",
"pedobear",
"pedophile",
"pegging",
"penis",
"pikey",
"pissing",
"pisspig",
"playboy",
"ponyplay",
"poof",
"poon",
"poontang",
"punany",
"poopchute",
"porn",
"porno",
"pornography",
"pthc",
"pubes",
"pussy",
"queaf",
"queef",
"quim",
"raghead",
"rape",
"raping",
"rapist",
"rectum",
"rimjob",
"rimming",
"sadism",
"santorum",
"scat",
"schlong",
"scissoring",
"semen",
"sex",
"sexcam",
"sexo",
"sexy",
"sexual",
"sexually",
"sexuality",
"shemale",
"shibari",
"shit",
"shitblimp",
"shitty",
"shota",
"shrimping",
"skeet",
"slanteye",
"slut",
"s&m",
"smut",
"snatch",
"snowballing",
"sodomize",
"sodomy",
"spastic",
"spic",
"splooge",
"spooge",
"spunk",
"strapon",
"strappado",
"suck",
"sucks",
"swastika",
"swinger",
"threesome",
"throating",
"thumbzilla",
"tight white",
"tit",
"tits",
"titties",
"titty",
"topless",
"tosser",
"towelhead",
"tranny",
"tribadism",
"tubgirl",
"tushy",
"twat",
"twink",
"twinkie",
"undressing",
"upskirt",
"urophilia",
"vagina",
"viagra",
"vibrator",
"vorarephilia",
"voyeur",
"voyeurweb",
"voyuer",
"vulva",
"wank",
"wetback",
"whore",
"worldsex",
"xx",
"xxx",
"yaoi",
"yiffy",
"zoophilia"
};
} }

View File

@ -208,7 +208,11 @@ void RemoveApostrophes(std::string &s);
std::string convert2digit(int n, std::string suffix); std::string convert2digit(int n, std::string suffix);
std::string numberToWords(unsigned long long int n); std::string numberToWords(unsigned long long int n);
std::string FormatName(const std::string& char_name); std::string FormatName(const std::string& char_name);
bool IsAllowedWorldServerCharacterList(char c);
void SanitizeWorldServerName(char *name); void SanitizeWorldServerName(char *name);
std::string SanitizeWorldServerName(std::string server_long_name);
std::string repeat(std::string s, int n);
std::vector<std::string> GetBadWords();
template<typename InputIterator, typename OutputIterator> template<typename InputIterator, typename OutputIterator>
auto CleanMobName(InputIterator first, InputIterator last, OutputIterator result) auto CleanMobName(InputIterator first, InputIterator last, OutputIterator result)

View File

@ -22,7 +22,7 @@ SET(eqlogin_headers
loginserver_command_handler.h loginserver_command_handler.h
loginserver_webserver.h loginserver_webserver.h
login_server.h login_server.h
login_structures.h login_types.h
options.h options.h
server_manager.h server_manager.h
world_server.h world_server.h

View File

@ -7,44 +7,9 @@
#include "../common/eq_stream_intf.h" #include "../common/eq_stream_intf.h"
#include "../common/net/dns.h" #include "../common/net/dns.h"
#include "../common/net/daybreak_connection.h" #include "../common/net/daybreak_connection.h"
#include "login_structures.h" #include "login_types.h"
#include <memory> #include <memory>
enum LSClientVersion {
cv_titanium,
cv_sod
};
enum LSClientStatus {
cs_not_sent_session_ready,
cs_waiting_for_login,
cs_creating_account,
cs_failed_to_login,
cs_logged_in
};
namespace LS {
namespace ServerStatusFlags {
enum eServerStatusFlags {
Up = 0, // default
Down = 1,
Unused = 2,
Locked = 4 // can be combined with Down to show "Locked (Down)"
};
}
namespace ServerTypeFlags {
enum eServerTypeFlags {
None = 0,
Standard = 1,
Unknown2 = 2,
Unknown4 = 4,
Preferred = 8,
Legends = 16 // can be combined with Preferred flag to override color in Legends section with Preferred color (green)
};
}
}
/** /**
* Client class, controls a single client and it's connection to the login server * Client class, controls a single client and it's connection to the login server
*/ */

View File

@ -4,39 +4,34 @@
#pragma pack(1) #pragma pack(1)
// unencrypted base message header in all packets // unencrypted base message header in all packets
struct LoginBaseMessage_Struct struct LoginBaseMessage_Struct {
{
int32_t sequence; // request type/login sequence (2: handshake, 3: login, 4: serverlist, ...) int32_t sequence; // request type/login sequence (2: handshake, 3: login, 4: serverlist, ...)
bool compressed; // true: deflated bool compressed; // true: deflated
int8_t encrypt_type; // 1: invert (unused) 2: des (2 for encrypted player logins and order expansions) (client uses what it sent, ignores in reply) int8_t encrypt_type; // 1: invert (unused) 2: des (2 for encrypted player logins and order expansions) (client uses what it sent, ignores in reply)
int32_t unk3; // unused? int32_t unk3; // unused?
}; };
struct LoginBaseReplyMessage_Struct struct LoginBaseReplyMessage_Struct {
{
bool success; // 0: failure (shows error string) 1: success bool success; // 0: failure (shows error string) 1: success
int32_t error_str_id; // last error eqlsstr id, default: 101 (no error) int32_t error_str_id; // last error eqlsstr id, default: 101 (no error)
char str[1]; // variable length, unknown (may be unused, this struct is a common pattern elsewhere) char str[1]; // variable length, unknown (may be unused, this struct is a common pattern elsewhere)
}; };
struct LoginHandShakeReply_Struct struct LoginHandShakeReply_Struct {
{ LoginBaseMessage_Struct base_header;
LoginBaseMessage_Struct base_header;
LoginBaseReplyMessage_Struct base_reply; LoginBaseReplyMessage_Struct base_reply;
char unknown[1]; // variable length string char unknown[1]; // variable length string
}; };
// for reference, login buffer is variable (minimum size 8 due to encryption) // for reference, login buffer is variable (minimum size 8 due to encryption)
struct PlayerLogin_Struct struct PlayerLogin_Struct {
{
LoginBaseMessage_Struct base_header; LoginBaseMessage_Struct base_header;
char username[1]; char username[1];
char password[1]; char password[1];
}; };
// variable length, can use directly if not serializing strings // variable length, can use directly if not serializing strings
struct PlayerLoginReply_Struct struct PlayerLoginReply_Struct {
{
// base header excluded to make struct data easier to encrypt // base header excluded to make struct data easier to encrypt
//LoginBaseMessage_Struct base_header; //LoginBaseMessage_Struct base_header;
LoginBaseReplyMessage_Struct base_reply; LoginBaseReplyMessage_Struct base_reply;
@ -59,8 +54,7 @@ struct PlayerLoginReply_Struct
}; };
// variable length, for reference // variable length, for reference
struct LoginClientServerData_Struct struct LoginClientServerData_Struct {
{
char ip[1]; char ip[1];
int32_t server_type; // legends, preferred, standard int32_t server_type; // legends, preferred, standard
int32_t server_id; int32_t server_id;
@ -72,29 +66,78 @@ struct LoginClientServerData_Struct
}; };
// variable length, for reference // variable length, for reference
struct ServerListReply_Struct struct ServerListReply_Struct {
{ LoginBaseMessage_Struct base_header;
LoginBaseMessage_Struct base_header;
LoginBaseReplyMessage_Struct base_reply; LoginBaseReplyMessage_Struct base_reply;
int32_t server_count; int32_t server_count;
LoginClientServerData_Struct servers[0]; LoginClientServerData_Struct servers[0];
}; };
struct PlayEverquestRequest_Struct { struct PlayEverquestRequest_Struct {
LoginBaseMessage_Struct base_header; LoginBaseMessage_Struct base_header;
uint32 server_number; uint32 server_number;
}; };
// SCJoinServerReply // SCJoinServerReply
struct PlayEverquestResponse_Struct { struct PlayEverquestResponse_Struct {
LoginBaseMessage_Struct base_header; LoginBaseMessage_Struct base_header;
LoginBaseReplyMessage_Struct base_reply; LoginBaseReplyMessage_Struct base_reply;
uint32 server_number; uint32 server_number;
}; };
#pragma pack() #pragma pack()
enum LSClientVersion {
cv_titanium,
cv_sod
};
enum LSClientStatus {
cs_not_sent_session_ready,
cs_waiting_for_login,
cs_creating_account,
cs_failed_to_login,
cs_logged_in
};
namespace LS {
namespace ServerStatusFlags {
enum eServerStatusFlags {
Up = 0, // default
Down = 1,
Unused = 2,
Locked = 4 // can be combined with Down to show "Locked (Down)"
};
}
namespace ServerTypeFlags {
enum eServerTypeFlags {
None = 0,
Standard = 1,
Unknown2 = 2,
Unknown4 = 4,
Preferred = 8,
Legends = 16 // can be combined with Preferred flag to override color in Legends section with Preferred color (green)
};
}
enum ServerType {
Standard = 3,
Preferred = 2,
Legends = 1,
};
namespace ErrStr {
constexpr static int NO_ERROR = 101; // No Error
constexpr static int SERVER_UNAVAILABLE = 326; // That server is currently unavailable. Please check the EverQuest webpage for current server status and try again later.
constexpr static int ACCOUNT_SUSPENDED = 337; // This account is currently suspended. Please contact customer service for more information.
constexpr static int ACCOUNT_BANNED = 338; // This account is currently banned. Please contact customer service for more information.
constexpr static int WORLD_MAX_CAPACITY = 339; // The world server is currently at maximum capacity and not allowing further logins until the number of players online decreases. Please try again later.
constexpr static int ERROR_1018_ACTIVE_CHARACTER = 111; // Error 1018: You currently have an active character on that EverQuest Server, please allow a minute for synchronization and try again.
constexpr static int UNKNOWN_ERROR = 102; // Error - Unknown Error Occurred
};
}
#endif #endif

View File

@ -12,6 +12,8 @@
"worldservers": { "worldservers": {
"unregistered_allowed": true, "unregistered_allowed": true,
"show_player_count": false, "show_player_count": false,
"dev_test_servers_list_bottom": false,
"special_character_start_list_bottom": false,
"reject_duplicate_servers": false "reject_duplicate_servers": false
}, },
"web_api": { "web_api": {

View File

@ -62,9 +62,30 @@ void LoadServerConfig()
"worldservers", "worldservers",
"reject_duplicate_servers", "reject_duplicate_servers",
false false
)); )
server.options.AllowUnregistered(server.config.GetVariableBool("worldservers", "unregistered_allowed", true)); );
server.options.SetShowPlayerCount(server.config.GetVariableBool("worldservers", "show_player_count", false)); server.options.SetShowPlayerCount(server.config.GetVariableBool("worldservers", "show_player_count", false));
server.options.AllowUnregistered(
server.config.GetVariableBool(
"worldservers",
"unregistered_allowed",
true
)
);
server.options.SetWorldDevTestServersListBottom(
server.config.GetVariableBool(
"worldservers",
"dev_test_servers_list_bottom",
false
)
);
server.options.SetWorldSpecialCharacterStartListBottom(
server.config.GetVariableBool(
"worldservers",
"special_character_start_list_bottom",
false
)
);
/** /**
* Account * Account
@ -242,6 +263,14 @@ int main(int argc, char **argv)
LogInfo("[Config] [WorldServer] IsRejectingDuplicateServers [{0}]", server.options.IsRejectingDuplicateServers()); LogInfo("[Config] [WorldServer] IsRejectingDuplicateServers [{0}]", server.options.IsRejectingDuplicateServers());
LogInfo("[Config] [WorldServer] IsUnregisteredAllowed [{0}]", server.options.IsUnregisteredAllowed()); LogInfo("[Config] [WorldServer] IsUnregisteredAllowed [{0}]", server.options.IsUnregisteredAllowed());
LogInfo("[Config] [WorldServer] ShowPlayerCount [{0}]", server.options.IsShowPlayerCountEnabled()); LogInfo("[Config] [WorldServer] ShowPlayerCount [{0}]", server.options.IsShowPlayerCountEnabled());
LogInfo(
"[Config] [WorldServer] DevAndTestServersListBottom [{0}]",
server.options.IsWorldDevTestServersListBottom()
);
LogInfo(
"[Config] [WorldServer] SpecialCharactersStartListBottom [{0}]",
server.options.IsWorldSpecialCharacterStartListBottom()
);
LogInfo("[Config] [Security] GetEncryptionMode [{0}]", server.options.GetEncryptionMode()); LogInfo("[Config] [Security] GetEncryptionMode [{0}]", server.options.GetEncryptionMode());
LogInfo("[Config] [Security] IsTokenLoginAllowed [{0}]", server.options.IsTokenLoginAllowed()); LogInfo("[Config] [Security] IsTokenLoginAllowed [{0}]", server.options.IsTokenLoginAllowed());
LogInfo("[Config] [Security] IsPasswordLoginAllowed [{0}]", server.options.IsPasswordLoginAllowed()); LogInfo("[Config] [Security] IsPasswordLoginAllowed [{0}]", server.options.IsPasswordLoginAllowed());

View File

@ -121,6 +121,20 @@ public:
{ {
Options::show_player_count = show_player_count; Options::show_player_count = show_player_count;
} }
inline bool IsWorldDevTestServersListBottom() const { return world_dev_test_servers_list_bottom; }
inline void SetWorldDevTestServersListBottom(bool dev_test_servers_list_bottom)
{
Options::world_dev_test_servers_list_bottom = dev_test_servers_list_bottom;
}
inline bool IsWorldSpecialCharacterStartListBottom() const
{
return world_special_character_start_list_bottom;
}
inline void SetWorldSpecialCharacterStartListBottom(bool world_special_character_start_list_bottom)
{
Options::world_special_character_start_list_bottom = world_special_character_start_list_bottom;
}
private: private:
bool allow_unregistered; bool allow_unregistered;
@ -129,6 +143,8 @@ private:
bool dump_in_packets; bool dump_in_packets;
bool dump_out_packets; bool dump_out_packets;
bool reject_duplicate_servers; bool reject_duplicate_servers;
bool world_dev_test_servers_list_bottom;
bool world_special_character_start_list_bottom;
bool allow_token_login; bool allow_token_login;
bool allow_password_login; bool allow_password_login;
bool show_player_count; bool show_player_count;

View File

@ -1,6 +1,6 @@
#include "server_manager.h" #include "server_manager.h"
#include "login_server.h" #include "login_server.h"
#include "login_structures.h" #include "login_types.h"
#include <stdlib.h> #include <stdlib.h>
#include "../common/eqemu_logsys.h" #include "../common/eqemu_logsys.h"

View File

@ -1,6 +1,6 @@
#include "world_server.h" #include "world_server.h"
#include "login_server.h" #include "login_server.h"
#include "login_structures.h" #include "login_types.h"
#include "../common/ip_util.h" #include "../common/ip_util.h"
#include "../common/string_util.h" #include "../common/string_util.h"
@ -198,15 +198,15 @@ void WorldServer::ProcessUserToWorldResponseLegacy(uint16_t opcode, const EQ::Ne
LogDebug("User-To-World Response received"); LogDebug("User-To-World Response received");
} }
auto *user_to_world_response = (UsertoWorldResponseLegacy_Struct *) packet.Data(); auto *r = (UsertoWorldResponseLegacy_Struct *) packet.Data();
LogDebug("Trying to find client with user id of [{0}]", user_to_world_response->lsaccountid); LogDebug("Trying to find client with user id of [{0}]", r->lsaccountid);
Client *client = server.client_manager->GetClient(user_to_world_response->lsaccountid, "eqemu"); Client *client = server.client_manager->GetClient(r->lsaccountid, "eqemu");
if (client) { if (client) {
LogDebug( LogDebug(
"Found client with user id of [{0}] and account name of [{1}]", "Found client with user id of [{0}] and account name of [{1}]",
user_to_world_response->lsaccountid, r->lsaccountid,
client->GetAccountName() client->GetAccountName()
); );
@ -217,9 +217,9 @@ void WorldServer::ProcessUserToWorldResponseLegacy(uint16_t opcode, const EQ::Ne
auto *per = (PlayEverquestResponse_Struct *) outapp->pBuffer; auto *per = (PlayEverquestResponse_Struct *) outapp->pBuffer;
per->base_header.sequence = client->GetPlaySequence(); per->base_header.sequence = client->GetPlaySequence();
per->server_number = client->GetPlayServerID(); per->server_number = client->GetPlayServerID();
if (user_to_world_response->response > 0) { if (r->response > 0) {
per->base_reply.success = true; per->base_reply.success = true;
SendClientAuth( SendClientAuth(
client->GetConnection()->GetRemoteAddr(), client->GetConnection()->GetRemoteAddr(),
@ -230,27 +230,27 @@ void WorldServer::ProcessUserToWorldResponseLegacy(uint16_t opcode, const EQ::Ne
); );
} }
switch (user_to_world_response->response) { switch (r->response) {
case UserToWorldStatusSuccess: case UserToWorldStatusSuccess:
per->base_reply.error_str_id = 101; per->base_reply.error_str_id = LS::ErrStr::NO_ERROR;
break; break;
case UserToWorldStatusWorldUnavail: case UserToWorldStatusWorldUnavail:
per->base_reply.error_str_id = 326; per->base_reply.error_str_id = LS::ErrStr::SERVER_UNAVAILABLE;
break; break;
case UserToWorldStatusSuspended: case UserToWorldStatusSuspended:
per->base_reply.error_str_id = 337; per->base_reply.error_str_id = LS::ErrStr::ACCOUNT_SUSPENDED;
break; break;
case UserToWorldStatusBanned: case UserToWorldStatusBanned:
per->base_reply.error_str_id = 338; per->base_reply.error_str_id = LS::ErrStr::ACCOUNT_BANNED;
break; break;
case UserToWorldStatusWorldAtCapacity: case UserToWorldStatusWorldAtCapacity:
per->base_reply.error_str_id = 339; per->base_reply.error_str_id = LS::ErrStr::WORLD_MAX_CAPACITY;
break; break;
case UserToWorldStatusAlreadyOnline: case UserToWorldStatusAlreadyOnline:
per->base_reply.error_str_id = 111; per->base_reply.error_str_id = LS::ErrStr::ERROR_1018_ACTIVE_CHARACTER;
break; break;
default: default:
per->base_reply.error_str_id = 102; per->base_reply.error_str_id = LS::ErrStr::UNKNOWN_ERROR;
} }
if (server.options.IsWorldTraceOn()) { if (server.options.IsWorldTraceOn()) {
@ -275,7 +275,7 @@ void WorldServer::ProcessUserToWorldResponseLegacy(uint16_t opcode, const EQ::Ne
else { else {
LogError( LogError(
"Received User-To-World Response for [{0}] but could not find the client referenced!", "Received User-To-World Response for [{0}] but could not find the client referenced!",
user_to_world_response->lsaccountid r->lsaccountid
); );
} }
} }
@ -335,7 +335,7 @@ void WorldServer::ProcessUserToWorldResponse(uint16_t opcode, const EQ::Net::Pac
auto *per = (PlayEverquestResponse_Struct *) outapp->pBuffer; auto *per = (PlayEverquestResponse_Struct *) outapp->pBuffer;
per->base_header.sequence = client->GetPlaySequence(); per->base_header.sequence = client->GetPlaySequence();
per->server_number = client->GetPlayServerID(); per->server_number = client->GetPlayServerID();
LogDebug( LogDebug(
"Found sequence and play of [{0}] [{1}]", "Found sequence and play of [{0}] [{1}]",
@ -358,25 +358,25 @@ void WorldServer::ProcessUserToWorldResponse(uint16_t opcode, const EQ::Net::Pac
switch (user_to_world_response->response) { switch (user_to_world_response->response) {
case UserToWorldStatusSuccess: case UserToWorldStatusSuccess:
per->base_reply.error_str_id = 101; per->base_reply.error_str_id = LS::ErrStr::NO_ERROR;
break; break;
case UserToWorldStatusWorldUnavail: case UserToWorldStatusWorldUnavail:
per->base_reply.error_str_id = 326; per->base_reply.error_str_id = LS::ErrStr::SERVER_UNAVAILABLE;
break; break;
case UserToWorldStatusSuspended: case UserToWorldStatusSuspended:
per->base_reply.error_str_id = 337; per->base_reply.error_str_id = LS::ErrStr::ACCOUNT_SUSPENDED;
break; break;
case UserToWorldStatusBanned: case UserToWorldStatusBanned:
per->base_reply.error_str_id = 338; per->base_reply.error_str_id = LS::ErrStr::ACCOUNT_BANNED;
break; break;
case UserToWorldStatusWorldAtCapacity: case UserToWorldStatusWorldAtCapacity:
per->base_reply.error_str_id = 339; per->base_reply.error_str_id = LS::ErrStr::WORLD_MAX_CAPACITY;
break; break;
case UserToWorldStatusAlreadyOnline: case UserToWorldStatusAlreadyOnline:
per->base_reply.error_str_id = 111; per->base_reply.error_str_id = LS::ErrStr::ERROR_1018_ACTIVE_CHARACTER;
break; break;
default: default:
per->base_reply.error_str_id = 102; per->base_reply.error_str_id = LS::ErrStr::UNKNOWN_ERROR;
} }
if (server.options.IsTraceOn()) { if (server.options.IsTraceOn()) {
@ -571,6 +571,12 @@ void WorldServer::Handle_NewLSInfo(ServerNewLSInfo_Struct *new_world_server_info
GetServerLongName(), GetServerLongName(),
GetRemoteIp() GetRemoteIp()
); );
WorldServer::FormatWorldServerName(
new_world_server_info_packet->server_long_name,
world_registration.server_list_type
);
SetLongName(new_world_server_info_packet->server_long_name);
} }
/** /**
@ -1025,21 +1031,21 @@ bool WorldServer::ValidateWorldServerAdminLogin(
return false; return false;
} }
void WorldServer::SerializeForClientServerList(SerializeBuffer& out, bool use_local_ip) const void WorldServer::SerializeForClientServerList(SerializeBuffer &out, bool use_local_ip) const
{ {
// see LoginClientServerData_Struct // see LoginClientServerData_Struct
if (use_local_ip) { if (use_local_ip) {
out.WriteString(GetLocalIP()); out.WriteString(GetLocalIP());
} else { }
else {
out.WriteString(GetRemoteIP()); out.WriteString(GetRemoteIP());
} }
switch (GetServerListID()) switch (GetServerListID()) {
{ case LS::ServerType::Legends:
case 1:
out.WriteInt32(LS::ServerTypeFlags::Legends); out.WriteInt32(LS::ServerTypeFlags::Legends);
break; break;
case 2: case LS::ServerType::Preferred:
out.WriteInt32(LS::ServerTypeFlags::Preferred); out.WriteInt32(LS::ServerTypeFlags::Preferred);
break; break;
default: default:
@ -1348,3 +1354,37 @@ void WorldServer::OnKeepAlive(EQ::Timer *t)
ServerPacket pack(ServerOP_KeepAlive, 0); ServerPacket pack(ServerOP_KeepAlive, 0);
m_connection->SendPacket(&pack); m_connection->SendPacket(&pack);
} }
void WorldServer::FormatWorldServerName(char *name, int8 server_list_type)
{
std::string server_long_name = name;
server_long_name = trim(server_long_name);
bool name_set_to_bottom = false;
if (server_list_type == LS::ServerType::Standard) {
if (server.options.IsWorldDevTestServersListBottom()) {
std::string s = str_tolower(server_long_name);
if (s.find("dev") != std::string::npos) {
server_long_name = fmt::format("|D| {}", server_long_name);
name_set_to_bottom = true;
}
else if (s.find("test") != std::string::npos) {
server_long_name = fmt::format("|T| {}", server_long_name);
name_set_to_bottom = true;
}
else if (s.find("installer") != std::string::npos) {
server_long_name = fmt::format("|I| {}", server_long_name);
name_set_to_bottom = true;
}
}
if (server.options.IsWorldSpecialCharacterStartListBottom() && !name_set_to_bottom) {
auto first_char = server_long_name.c_str()[0];
if (IsAllowedWorldServerCharacterList(first_char) && first_char != '|') {
server_long_name = fmt::format("|*| {}", server_long_name);
name_set_to_bottom = true;
}
}
}
strn0cpy(name, server_long_name.c_str(), 201);
}

View File

@ -194,6 +194,7 @@ private:
void OnKeepAlive(EQ::Timer *t); void OnKeepAlive(EQ::Timer *t);
std::unique_ptr<EQ::Timer> m_keepalive; std::unique_ptr<EQ::Timer> m_keepalive;
static void FormatWorldServerName(char *name, int8 server_list_type);
}; };
#endif #endif

View File

@ -603,8 +603,6 @@ void LoginServer::SendInfo()
WorldConfig::SetLocalAddress(l->local_ip_address); WorldConfig::SetLocalAddress(l->local_ip_address);
} }
SanitizeWorldServerName(l->server_long_name);
LogInfo( LogInfo(
"[LoginServer::SendInfo] protocol_version [{}] server_version [{}] long_name [{}] short_name [{}] account_name [{}] remote_ip_address [{}] local_ip [{}]", "[LoginServer::SendInfo] protocol_version [{}] server_version [{}] long_name [{}] short_name [{}] account_name [{}] remote_ip_address [{}] local_ip [{}]",
l->protocol_version, l->protocol_version,