tinymux/mux/modules/engine/player.cpp
Stephen Dennis 8808b4c375 feat(#2136): flip fargs to const UTF8 * const — and convert every site the compiler surfaced
The flip: FUNCTION/XFUNCTION/FUN::fun/delim_check and the module
interfaces take `const UTF8 * const fargs[]`.  Double-const is
load-bearing: C++ qualification conversion needs const at both pointer
levels, so builder-side `UTF8 *[]` arrays convert implicitly — the
evaluator, the JIT marshaller, and every owner site need zero casts,
and slot reassignment inside bodies becomes a compile error for free.

The conversions: the flip landed first so the compiler enumerated every
violation; this commit is that inventory worked to zero — ~250 sites
across funceval, funceval2, functions, funmath, help, mail, session,
powers, levels, predicates, conf, walkdb, stringutil, timeutil/
date_scan (regenerated, one-line diff), exp3, and mux_main, each
classified per docs/campaign-2136-const-fargs.md's four recipes.

New idioms (functions.h): trim_space_sep_n() — non-destructive trim for
(pointer, length) consumers, so trim-then-scan sites need no copy at
all; FargVec — the argv counterpart of FargCopy for CS_ARGV handlers.
countwords() and DecodeListOfIntegers() rewritten non-destructive.

The flip deleted more than it added: #2157's fun_munge list1 copy, the
engine_com help-topic copy, fun_index's in-place NUL write, and five
const_casts (process_sex x4, sha1_helper).  const_cast budget: zero
added.

Trap recorded in the brief: an old-signature definition doesn't fail
the build — it becomes a C++ overload, and the new-signature symbol
stays undefined until dlopen(RTLD_NOW).  delim_check, the conn_bridge
bridges, the dbt_spike stub, and exp3::Call were all silently shadowed;
muxscript was the only host that noticed, because netmux's own net.cpp
resolved the flat-namespace lookup.  After any signature flip, grep the
old spelling.

Verified: make test EXPECT_CONFIG="jit=yes" (35 passed / 0 failed) and
make test-scenario, including the new tests/scenario/sidefx_fargs.py
that live-probes the class-3 wrappers smoke never touches (pemit/
trigger/link/tel/wipe/destroy).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 14:34:23 -06:00

1845 lines
50 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*! \file player.cpp
* \brief Player-related routines.
*
* As opposed to other types of objects, players have passwords, have more
* limited names, can log in, etc.
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "functions.h"
#include "sha1.h"
#include "shacrypt.h"
#define NUM_GOOD 4 // # of successful logins to save data for.
#define NUM_BAD 3 // # of failed logins to save data for.
typedef struct hostdtm HOSTDTM;
struct hostdtm
{
const UTF8 *host;
const UTF8 *dtm;
};
typedef struct logindata LDATA;
struct logindata
{
HOSTDTM good[NUM_GOOD];
HOSTDTM bad[NUM_BAD];
int tot_good;
int tot_bad;
int new_bad;
};
NAMETAB method_nametab[] =
{
{T("sha1"), 4, CA_GOD, CRYPT_SHA1},
{T("des"), 3, CA_GOD, CRYPT_DES},
{T("md5"), 3, CA_GOD, CRYPT_MD5},
{T("sha256"), 6, CA_GOD, CRYPT_SHA256},
{T("sha512"), 6, CA_GOD, CRYPT_SHA512},
{ nullptr, 0, 0, 0}
};
/* ---------------------------------------------------------------------------
* decrypt_logindata, encrypt_logindata: Decode and encode login info.
*/
// grabto() returns nullptr once the ';'-separated fields in A_LOGINDATA are
// exhausted, which happens when the attribute is malformed or truncated (e.g.
// "#803" with no trailing fields). Treat a missing field as an empty string so
// callers never dereference nullptr: mux_atoi64() crashes on a nullptr argument,
// and the host/dtm pointers are later handed to tprintf()/notify().
//
static UTF8 *grab_field(UTF8 **str)
{
static UTF8 empty[1] = { '\0' };
UTF8 *p = grabto(str, ';');
return (nullptr != p) ? p : empty;
}
static void decrypt_logindata(UTF8 *atrbuf, LDATA *info)
{
int i;
info->tot_good = 0;
info->tot_bad = 0;
info->new_bad = 0;
for (i = 0; i < NUM_GOOD; i++)
{
info->good[i].host = nullptr;
info->good[i].dtm = nullptr;
}
for (i = 0; i < NUM_BAD; i++)
{
info->bad[i].host = nullptr;
info->bad[i].dtm = nullptr;
}
if (*atrbuf == '#')
{
atrbuf++;
info->tot_good = mux_atoi64(grab_field(&atrbuf));
for (i = 0; i < NUM_GOOD; i++)
{
info->good[i].host = grab_field(&atrbuf);
info->good[i].dtm = grab_field(&atrbuf);
}
info->new_bad = mux_atoi64(grab_field(&atrbuf));
info->tot_bad = mux_atoi64(grab_field(&atrbuf));
for (i = 0; i < NUM_BAD; i++)
{
info->bad[i].host = grab_field(&atrbuf);
info->bad[i].dtm = grab_field(&atrbuf);
}
}
}
static void encrypt_logindata(UTF8 *atrbuf, LDATA *info)
{
// Make sure the SPRINTF call tracks NUM_GOOD and NUM_BAD for the number
// of host/dtm pairs of each type.
//
UTF8 nullc = '\0';
int i;
for (i = 0; i < NUM_GOOD; i++)
{
if (!info->good[i].host)
info->good[i].host = &nullc;
if (!info->good[i].dtm)
info->good[i].dtm = &nullc;
}
for (i = 0; i < NUM_BAD; i++)
{
if (!info->bad[i].host)
info->bad[i].host = &nullc;
if (!info->bad[i].dtm)
info->bad[i].dtm = &nullc;
}
LBuf bp = LBuf_Src("encrypt_logindata");
mux_sprintf(bp.get(), LBUF_SIZE,
T("#%d;%s;%s;%s;%s;%s;%s;%s;%s;%d;%d;%s;%s;%s;%s;%s;%s;"),
info->tot_good,
info->good[0].host, info->good[0].dtm,
info->good[1].host, info->good[1].dtm,
info->good[2].host, info->good[2].dtm,
info->good[3].host, info->good[3].dtm,
info->new_bad, info->tot_bad,
info->bad[0].host, info->bad[0].dtm,
info->bad[1].host, info->bad[1].dtm,
info->bad[2].host, info->bad[2].dtm);
mux_strncpy(atrbuf, bp, LBUF_SIZE-1);
}
/* ---------------------------------------------------------------------------
* record_login: Record successful or failed login attempt.
* If successful, report last successful login and number of failures since
* last successful login.
*/
void record_login
(
dbref player,
bool isgood,
const UTF8 *ldate,
const UTF8 *lhost,
const UTF8 *lusername,
const UTF8 *lipaddr
)
{
LDATA login_info;
dbref aowner;
int aflags, i;
LBuf atrbuf = LBuf_Adopt(atr_get("record_login.143", player, A_LOGINDATA, &aowner, &aflags));
// Diagnostic breadcrumb: a well-formed A_LOGINDATA has
// 2*NUM_GOOD + 2*NUM_BAD + 3 (== 17) ';'-separated fields (see
// encrypt_logindata's template). A non-empty value shorter than that was
// truncated at rest -- decrypt_logindata tolerates it now, but the truncation
// source is still unknown, so log which player read a malformed value.
{
const UTF8 *v = atrbuf;
if ('\0' != v[0])
{
size_t nSemi = 0;
for (const UTF8 *p = v; '\0' != *p; p++)
{
if (';' == *p)
{
nSemi++;
}
}
if ( '#' != v[0]
|| nSemi < 2 * NUM_GOOD + 2 * NUM_BAD + 3)
{
STARTLOG(LOG_PROBLEMS, "DB", "LOGIN");
log_printf(T("record_login(#%d): malformed A_LOGINDATA (truncated?): %s"), player, v);
ENDLOG;
}
}
}
decrypt_logindata(atrbuf, &login_info);
if (isgood)
{
if (login_info.new_bad > 0)
{
// Empty notify = blank line. Never M_("") — gettext("") is the
// catalog header (#1443).
//
notify(player, T(""));
notify(player, tprintf(MN_(
"**** %d failed connect since your last successful connect. ****",
"**** %d failed connects since your last successful connect. ****",
login_info.new_bad), login_info.new_bad));
notify(player, tprintf(M_("Most recent attempt was from %s on %s."),
login_info.bad[0].host, login_info.bad[0].dtm));
notify(player, T(""));
login_info.new_bad = 0;
}
if ( login_info.good[0].host
&& *login_info.good[0].host
&& login_info.good[0].dtm
&& *login_info.good[0].dtm)
{
notify(player, tprintf(M_("Last connect was from %s on %s."),
login_info.good[0].host, login_info.good[0].dtm));
}
for (i = NUM_GOOD - 1; i > 0; i--)
{
login_info.good[i].dtm = login_info.good[i - 1].dtm;
login_info.good[i].host = login_info.good[i - 1].host;
}
login_info.good[0].dtm = ldate;
login_info.good[0].host = lhost;
login_info.tot_good++;
if (*lusername)
{
atr_add_raw(player, A_LASTSITE, tprintf(T("%s@%s"), lusername, lhost));
}
else
{
atr_add_raw(player, A_LASTSITE, lhost);
}
// Add the players last IP too.
//
atr_add_raw(player, A_LASTIP, lipaddr);
}
else
{
for (i = NUM_BAD - 1; i > 0; i--)
{
login_info.bad[i].dtm = login_info.bad[i - 1].dtm;
login_info.bad[i].host = login_info.bad[i - 1].host;
}
login_info.bad[0].dtm = ldate;
login_info.bad[0].host = lhost;
login_info.tot_bad++;
login_info.new_bad++;
}
encrypt_logindata(atrbuf, &login_info);
atr_add_raw(player, A_LOGINDATA, atrbuf);
}
const UTF8 Base64Table[65] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
#define ENCODED_LENGTH(x) ((((x)+2)/3)*4)
static void EncodeBase64(size_t nIn, const UTF8 *pIn, UTF8 *pOut)
{
size_t nTriples = nIn/3;
size_t nLeftover = nIn%3;
uint32_t stage;
const uint8_t *p = (const uint8_t *)pIn;
uint8_t *q = ( uint8_t *)pOut;
while (nTriples--)
{
stage = (p[0] << 16) | (p[1] << 8) | p[2];
q[0] = Base64Table[(stage >> 18) ];
q[1] = Base64Table[(stage >> 12) & 0x3F];
q[2] = Base64Table[(stage >> 6) & 0x3F];
q[3] = Base64Table[(stage ) & 0x3F];
q += 4;
p += 3;
}
switch (nLeftover)
{
case 1:
stage = p[0] << 16;
q[0] = Base64Table[(stage >> 18) ];
q[1] = Base64Table[(stage >> 12) & 0x3F];
q[2] = '=';
q[3] = '=';
q += 4;
break;
case 2:
stage = (p[0] << 16) | (p[1] << 8);
q[0] = Base64Table[(stage >> 18) ];
q[1] = Base64Table[(stage >> 12) & 0x3F];
q[2] = Base64Table[(stage >> 6) & 0x3F];
q[3] = '=';
q += 4;
break;
}
q[0] = '\0';
}
// Historically, TinyMUX DES passwords use a fixed salt of 'XX', but DES-based
// crypt is not limited to this in general. Because of the fixed salt, any
// encrypted password that did not begin with a salt of 'XX' was interpreted
// as a clear-text password.
//
// A fixed salt completely undermines the purpose of salting passwords, but
// to support the legacy behavior, and to provide a path for clear-text
// passwords, the default behavior is to continue limiting salt to 'XX'. To
// remove this limit, uncomment the line that follows:
//
//#define ENABLE_PROPER_DES
const UTF8 szFail[] = "$FAIL$$";
const UTF8 szSHA1Prefix[] = "$SHA1$";
#define SHA1_PREFIX_LENGTH (sizeof(szSHA1Prefix)-1)
#define SHA1_HASH_LENGTH 5*sizeof(uint32_t)
#define SHA1_ENCODED_HASH_LENGTH ENCODED_LENGTH(SHA1_HASH_LENGTH)
#define SHA1_SALT_LENGTH 9
#define SHA1_ENCODED_SALT_LENGTH ENCODED_LENGTH(SHA1_SALT_LENGTH)
#define DES_SALT_LENGTH 2
const UTF8 szMD5Prefix[] = "$1$";
#define MD5_PREFIX_LENGTH (sizeof(szMD5Prefix)-1)
#define MD5_SALT_LENGTH 16
const UTF8 szSHA256Prefix[] = "$5$";
#define SHA256_PREFIX_LENGTH (sizeof(szSHA256Prefix)-1)
#define SHA256_SALT_LENGTH 16
const UTF8 szSHA512Prefix[] = "$6$";
#define SHA512_PREFIX_LENGTH (sizeof(szSHA512Prefix)-1)
#define SHA512_SALT_LENGTH 16
const UTF8 szP6HPrefix[] = "$P6H$";
#define P6H_PREFIX_LENGTH (sizeof(szP6HPrefix)-1)
#define P6H_XX_HASH_LENGTH_MAX 40
const UTF8 szP6HPrefix1SHA1[] = "$P6H$$1:sha1:";
#define P6H_VAHT_1SHA1_PREFIX_LENGTH (sizeof(szP6HPrefix1SHA1)-1)
#define P6H_VAHT_HASH_LENGTH_MAX (2*SHA1_HASH_LENGTH)
#define P6H_VAHT_TIMESTAMP_LENGTH_MAX 11
// These are known but passed through as CRYPT_OTHER:
//
// Blowfish $2a$
// The rounds= value for newly generated $5$/$6$ hashes, clamped to the
// sha-crypt spec range. Kept explicit in every hash we generate so the
// stored string carries its own work factor.
//
static unsigned long hash_rounds(void)
{
long n = mudconf.password_hash_rounds;
if (n < 1000)
{
n = 1000;
}
else if (999999999 < n)
{
n = 999999999;
}
return static_cast<unsigned long>(n);
}
static const UTF8 *GenerateSalt(int iType)
{
// Must be large enough for any supported format: prefix + salt + NUL.
// DES: 0 + 2 + 1 = 3
// MD5: 3 + 16 + 1 = 20
// SHA1: 6 + 12 + 1 = 19
// SHA256: 3 + rounds=999999999$ (18) + 16 + 1 = 38
// SHA512: 3 + rounds=999999999$ (18) + 16 + 1 = 38
//
static constexpr size_t MAX_SALT_SIZE = 48;
thread_local UTF8 szSalt[MAX_SALT_SIZE];
szSalt[0] = '\0';
if (CRYPT_SHA1 == iType)
{
UTF8 szSaltRaw[SHA1_SALT_LENGTH+1];
for (int i = 0; i < SHA1_SALT_LENGTH; i++)
{
szSaltRaw[i] = static_cast<UTF8>(RandomINT32(0, 255));
}
szSaltRaw[SHA1_SALT_LENGTH] = '\0';
mux_strncpy(szSalt, szSHA1Prefix, SHA1_PREFIX_LENGTH);
EncodeBase64(SHA1_SALT_LENGTH, szSaltRaw, szSalt + SHA1_PREFIX_LENGTH);
}
else if (CRYPT_DES == iType)
{
#if defined(ENABLE_PROPER_DES)
for (int i = 0; i < DES_SALT_LENGTH; i++)
{
// Map random number to set 'a-zA-Z0-9./'.
//
int32_t j = RandomINT32(0, sizeof(Base64Table)-1);
UTF8 ch = Base64Table[j];
if ('+' == ch)
{
ch = '.';
}
szSalt[i] = ch;
}
szSalt[DES_SALT_LENGTH] = '\0';
#else
return T("XX");
#endif
}
else if ( CRYPT_MD5 == iType
|| CRYPT_SHA256 == iType
|| CRYPT_SHA512 == iType)
{
const UTF8 *pPrefix = nullptr;
size_t nPrefix = 0;
size_t nSalt = 0;
if (CRYPT_MD5 == iType)
{
pPrefix = szMD5Prefix;
nPrefix = MD5_PREFIX_LENGTH;
nSalt = MD5_SALT_LENGTH;
}
else if (CRYPT_SHA256 == iType)
{
pPrefix = szSHA256Prefix;
nPrefix = SHA256_PREFIX_LENGTH;
nSalt = SHA256_SALT_LENGTH;
}
else if (CRYPT_SHA512 == iType)
{
pPrefix = szSHA512Prefix;
nPrefix = SHA512_PREFIX_LENGTH;
nSalt = SHA512_SALT_LENGTH;
}
mux_strncpy(szSalt, pPrefix, nPrefix);
// sha-crypt settings carry an explicit work factor (#1962).
//
if ( CRYPT_SHA256 == iType
|| CRYPT_SHA512 == iType)
{
nPrefix += mux_snprintf(szSalt + nPrefix, MAX_SALT_SIZE - nPrefix,
T("rounds=%lu$"), hash_rounds());
}
for (size_t i = nPrefix; i < nPrefix + nSalt; i++)
{
// Map random number to set 'a-zA-Z0-9./'.
//
int32_t j = RandomINT32(0, sizeof(Base64Table)-1);
UTF8 ch = Base64Table[j];
if ('+' == ch)
{
ch = '.';
}
szSalt[i] = ch;
}
szSalt[nPrefix + nSalt] = '\0';
}
return szSalt;
}
void ChangePassword(dbref player, const UTF8 *szPassword)
{
int iTypeOut;
const UTF8 *pEncodedPassword = nullptr;
int methods[] = { CRYPT_SHA512, CRYPT_SHA256, CRYPT_MD5, CRYPT_SHA1, CRYPT_DES };
for (size_t i = 0; i < sizeof(methods)/sizeof(methods[0]); i++)
{
if ( (mudconf.password_methods & methods[i])
&& nullptr != (pEncodedPassword = mux_crypt(szPassword, GenerateSalt(methods[i]), &iTypeOut)))
{
break;
}
}
if (nullptr == pEncodedPassword)
{
pEncodedPassword = mux_crypt(szPassword, GenerateSalt(CRYPT_SHA1), &iTypeOut);
mux_assert(nullptr != pEncodedPassword);
}
s_Pass(player, pEncodedPassword);
}
#if defined(UNIX_DIGEST) && defined(HAVE_SHA_INIT)
const UTF8 *p6h_xx_crypt(const UTF8 *szPassword)
{
// Calculate SHA-0 Hash.
//
SHA_CTX shac;
UTF8 szHashRaw[SHA_DIGEST_LENGTH];
SHA_Init(&shac);
SHA_Update(&shac, szPassword, strlen(reinterpret_cast<const char *>(szPassword)));
SHA_Final(szHashRaw, &shac);
// 1 2
// 1234567890123456789012345678
// $P6H$$XXhhhhhhhhhhhhhhhhhhhh
//
thread_local UTF8 buf[P6H_PREFIX_LENGTH + 1 + P6H_XX_HASH_LENGTH_MAX + 1 + 16];
mux_strncpy(buf, szP6HPrefix, P6H_PREFIX_LENGTH);
buf[P6H_PREFIX_LENGTH] = '$';
unsigned int a = (static_cast<unsigned int>(szHashRaw[0])) << 24
| (static_cast<unsigned int>(szHashRaw[1])) << 16
| (static_cast<unsigned int>(szHashRaw[2])) << 8
| (static_cast<unsigned int>(szHashRaw[3]));
unsigned int b = (static_cast<unsigned int>(szHashRaw[4])) << 24
| (static_cast<unsigned int>(szHashRaw[5])) << 16
| (static_cast<unsigned int>(szHashRaw[6])) << 8
| (static_cast<unsigned int>(szHashRaw[7]));
mux_sprintf(buf + P6H_PREFIX_LENGTH + 1, P6H_XX_HASH_LENGTH_MAX, T("XX%lu%lu"), a, b);
return buf;
}
#endif
const UTF8 *p6h_vaht_crypt(const UTF8 *szPassword, const UTF8 *szSetting)
{
// Layout is fixed-width up to the timestamp:
//
// $P6H$$1:sha1:<40 hex digits>:<timestamp>
//
// The timestamp is copied below from a constant offset past the hash
// field, so the entire fixed part must be present before that read is in
// bounds. Checking only the prefix let a truncated or corrupt A_PASS --
// anything from "$P6H$$1:sha1:" up to one byte short of the separator --
// run safe_str off the end of the attribute value (#1182). Require the
// full layout, including the separator, and fail closed otherwise.
//
constexpr size_t nP6HVahtFixed =
P6H_VAHT_1SHA1_PREFIX_LENGTH + P6H_VAHT_HASH_LENGTH_MAX + 1;
size_t nSetting = strlen(reinterpret_cast<const char *>(szSetting));
if ( nP6HVahtFixed <= nSetting
&& memcmp(szSetting, szP6HPrefix1SHA1, P6H_VAHT_1SHA1_PREFIX_LENGTH) == 0
&& ':' == szSetting[P6H_VAHT_1SHA1_PREFIX_LENGTH + P6H_VAHT_HASH_LENGTH_MAX])
{
// Calculate SHA-1 Hash.
//
#ifdef UNIX_DIGEST
uint8_t md[EVP_MAX_MD_SIZE];
#else
uint8_t md[MUX_SHA1_DIGEST_LENGTH];
#endif
unsigned int len = 0;
const UTF8 *parts[] = { szPassword };
const size_t lens[] = { strlen(reinterpret_cast<const char *>(szPassword)) };
if (mux_sha1_digest(parts, lens, 1, md, &len))
{
// 1 2 3 4 5 6
// 123456789012345678901234567890123456789012345678901234567890123456
// $P6H$$1:sha1:hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh:tttttttttttt
//
thread_local UTF8 buff[LBUF_SIZE];
UTF8 *bufc = buff;
safe_str(szP6HPrefix1SHA1, buff, &bufc);
safe_hex(md, len, false, buff, &bufc);
safe_chr(':', buff, &bufc);
safe_str(szSetting + P6H_VAHT_1SHA1_PREFIX_LENGTH + P6H_VAHT_HASH_LENGTH_MAX + 1, buff, &bufc);
*bufc = '\0';
return buff;
}
}
return szFail;
}
// There is no longer any support for DES-encrypted passwords on the Windows
// build. To convert these, using #1 to @newpassword, go through an older
// version of TinyMUX, or go through a Unix host.
//
const UTF8 *mux_crypt(const UTF8 *szPassword, const UTF8 *szSetting, int *piType)
{
const UTF8 *pSaltField = nullptr;
size_t nSaltField = 0;
*piType = CRYPT_FAIL;
if (szSetting[0] == '$')
{
const UTF8 *p = reinterpret_cast<const UTF8 *>(strchr(reinterpret_cast<const char *>(szSetting)+1, '$'));
if (p)
{
p++;
size_t nAlgo = p - szSetting;
if ( nAlgo == SHA1_PREFIX_LENGTH
&& memcmp(szSetting, szSHA1Prefix, SHA1_PREFIX_LENGTH) == 0)
{
// SHA-1
//
pSaltField = p;
p = reinterpret_cast<const UTF8 *>(strchr(reinterpret_cast<const char *>(pSaltField), '$'));
if (p)
{
nSaltField = p - pSaltField;
}
else
{
nSaltField = strlen(reinterpret_cast<const char *>(pSaltField));
}
if (nSaltField <= SHA1_ENCODED_SALT_LENGTH)
{
*piType = CRYPT_SHA1;
}
}
else if ( nAlgo == MD5_PREFIX_LENGTH
&& memcmp(szSetting, szMD5Prefix, MD5_PREFIX_LENGTH) == 0)
{
*piType = CRYPT_MD5;
}
else if ( nAlgo == SHA256_PREFIX_LENGTH
&& memcmp(szSetting, szSHA256Prefix, SHA256_PREFIX_LENGTH) == 0)
{
*piType = CRYPT_SHA256;
}
else if ( nAlgo == SHA512_PREFIX_LENGTH
&& memcmp(szSetting, szSHA512Prefix, SHA512_PREFIX_LENGTH) == 0)
{
*piType = CRYPT_SHA512;
}
else if ( nAlgo == P6H_PREFIX_LENGTH
&& memcmp(szSetting, szP6HPrefix, P6H_PREFIX_LENGTH) == 0)
{
#ifdef UNIX_DIGEST
if ('X' == p[0] && 'X' == p[1])
{
*piType = CRYPT_P6H_XX;
}
else
#endif
{
*piType = CRYPT_P6H_VAHT;
}
}
else
{
*piType = CRYPT_OTHER;
}
}
}
else if (szSetting[0] == '_')
{
*piType = CRYPT_DES_EXT;
}
else
{
#if defined(ENABLE_PROPER_DES)
// Strictly speaking, we can say the algorithm is DES.
//
*piType = CRYPT_DES;
#else
// However, in order to support clear-text passwords, we restrict
// ourselves to only verifying an existing DES-encrypted password and
// we assume a fixed salt of 'XX'. If you have been using a different
// salt, the following code won't work.
//
size_t nSetting = strlen(reinterpret_cast<const char *>(szSetting));
if ( 2 <= nSetting
&& memcmp(szSetting, "XX", 2) == 0)
{
*piType = CRYPT_DES;
}
else
{
*piType = CRYPT_CLEARTEXT;
}
#endif
}
switch (*piType)
{
case CRYPT_FAIL:
return szFail;
case CRYPT_CLEARTEXT:
return szPassword;
#if defined(UNIX_DIGEST) && defined(HAVE_SHA_INIT)
case CRYPT_P6H_XX:
return p6h_xx_crypt(szPassword);
#endif
case CRYPT_P6H_VAHT:
return p6h_vaht_crypt(szPassword, szSetting);
case CRYPT_SHA256:
case CRYPT_SHA512:
// Standard sha-crypt, computed by the portable in-tree construction
// over OS crypto primitives on BOTH platforms (#1962). A $6$ hash
// written on a Unix box verifies on Windows and vice versa, and
// rounds= handling no longer depends on which libc is present
// (macOS crypt(3) lacks $5$/$6$ entirely).
//
return mux_sha_crypt(szPassword, szSetting);
case CRYPT_OTHER:
case CRYPT_DES_EXT:
case CRYPT_MD5:
#if defined(WINDOWS_CRYPT)
// Beyond the formats above, the Windows release of TinyMUX only
// supports SHA1 and clear-text.
//
return szFail;
#endif // WINDOWS_CRYPT
case CRYPT_DES:
#if defined(HAVE_CRYPT)
return reinterpret_cast<const UTF8 *>(crypt(reinterpret_cast<const char *>(szPassword), reinterpret_cast<const char *>(szSetting)));
#else
return szFail;
#endif
}
// Calculate SHA-1 Hash.
//
#ifdef UNIX_DIGEST
uint8_t md[EVP_MAX_MD_SIZE+1];
#else
uint8_t md[MUX_SHA1_DIGEST_LENGTH+1];
#endif
unsigned int len = 0;
const UTF8 *parts[] = { pSaltField, szPassword };
const size_t lens[] = { nSaltField, strlen(reinterpret_cast<const char *>(szPassword)) };
if (!mux_sha1_digest(parts, lens, 2, md, &len))
{
return szFail;
}
md[len] = '\0';
// 1 2 3 4
// 12345678901234567890123456789012345678901234567
// $SHA1$ssssssssssss$hhhhhhhhhhhhhhhhhhhhhhhhhhhh
//
thread_local UTF8 buf[SHA1_PREFIX_LENGTH + SHA1_ENCODED_SALT_LENGTH + 1 + SHA1_ENCODED_HASH_LENGTH + 1 + 16];
mux_strncpy(buf, szSHA1Prefix, SHA1_PREFIX_LENGTH);
memcpy(buf + SHA1_PREFIX_LENGTH, pSaltField, nSaltField);
buf[SHA1_PREFIX_LENGTH + nSaltField] = '$';
EncodeBase64(len, md, buf + SHA1_PREFIX_LENGTH + nSaltField + 1);
return buf;
}
/* ---------------------------------------------------------------------------
* check_pass: Test a password to see if it is correct.
*/
// Strength order for the never-downgrade-by-accident rule below. Formats
// not listed (P6H imports, cleartext, unrecognized) rank lowest and always
// upgrade on a successful login.
//
static int method_rank(int iType)
{
switch (iType)
{
case CRYPT_SHA512: return 6;
case CRYPT_SHA256: return 5;
case CRYPT_SHA1: return 4;
case CRYPT_MD5: return 3;
case CRYPT_DES:
case CRYPT_DES_EXT: return 2;
default: return 0;
}
}
// rounds= of a stored $5$/$6$ setting; the sha-crypt default when absent.
//
static unsigned long stored_sha_rounds(const UTF8 *szSetting)
{
const char *p = reinterpret_cast<const char *>(szSetting);
if ( '$' == p[0]
&& ('5' == p[1] || '6' == p[1])
&& '$' == p[2]
&& 0 == strncmp(p + 3, "rounds=", 7))
{
return strtoul(p + 10, nullptr, 10);
}
return 5000UL;
}
// Decide whether a successfully verified password should be re-encoded.
//
// This is the auto-upgrade path: P6H imports and legacy formats convert to
// the configured method the first time the player logs in. Two rules
// sharpen the historical "type not in password_methods" trigger:
//
// - Same-type rounds refresh: a $5$/$6$ hash whose stored rounds differ
// from the current password_hash_rounds policy re-encodes even though
// its type is configured, so a rounds change propagates on login.
//
// - The IMPLICIT default never downgrades: with password_methods unset,
// the effective target is SHA1, and the old unconditional trigger both
// re-hashed every SHA1 login (churn) and would silently rewrite a
// $5$/$6$ hash down to $SHA1$ after a config reset. An explicitly
// configured method set may still downgrade -- that is the operator's
// stated intent.
//
static bool password_needs_rehash(int iType, const UTF8 *szSetting)
{
if (0 != (iType & mudconf.password_methods))
{
if ( ( CRYPT_SHA512 == iType
|| CRYPT_SHA256 == iType)
&& stored_sha_rounds(szSetting) != hash_rounds())
{
return true;
}
return false;
}
if ( 0 == mudconf.password_methods
&& method_rank(CRYPT_SHA1) <= method_rank(iType))
{
return false;
}
return true;
}
static bool check_pass(dbref player, const UTF8 *pPassword)
{
bool bValidPass = false;
int iType;
int aflags;
dbref aowner;
LBuf pTarget = LBuf_Adopt(atr_get("check_pass.466", player, A_PASS, &aowner, &aflags));
if (*pTarget)
{
// mux_crypt() can return nullptr — crypt(3) returns NULL on a malformed
// or unsupported salt setting on some platforms (POSIX-sanctioned; e.g.
// a $5$/$6$ password DB migrated to a libc whose crypt() lacks that
// method). ChangePassword() already guards this; check_pass() must too,
// or strcmp(nullptr, …) crashes the server on a login attempt. Fail the
// authentication closed when the hash can't be computed.
//
const UTF8 *pHashed = mux_crypt(pPassword, pTarget, &iType);
if ( nullptr != pHashed
&& strcmp(reinterpret_cast<const char *>(pHashed), reinterpret_cast<const char *>(pTarget.get())) == 0)
{
bValidPass = true;
if (password_needs_rehash(iType, pTarget))
{
ChangePassword(player, pPassword);
}
}
}
return bValidPass;
}
/* ---------------------------------------------------------------------------
* connect_player: Try to connect to an existing player.
*/
dbref connect_player(UTF8 *name, UTF8 *password, UTF8 *host, UTF8 *username, UTF8 *ipaddr)
{
CLinearTimeAbsolute ltaNow;
ltaNow.GetLocal();
const UTF8 *time_str = ltaNow.ReturnDateString(7);
dbref player = lookup_player(NOTHING, name, false);
if (player == NOTHING)
{
return NOTHING;
}
if (!check_pass(player, password))
{
record_login(player, false, time_str, host, username, ipaddr);
return NOTHING;
}
// Compare to last connect see if player gets salary.
//
int aflags;
dbref aowner;
LBuf player_last = LBuf_Adopt(atr_get("connect_player.516", player, A_LAST, &aowner, &aflags));
if (strncmp(reinterpret_cast<const char *>(player_last.get()), reinterpret_cast<const char *>(time_str), 10) != 0)
{
LBuf allowance = LBuf_Adopt(atr_pget(player, A_ALLOWANCE, &aowner, &aflags));
if (*allowance == '\0')
{
giveto(player, mudconf.paycheck);
}
else
{
giveto(player, mux_atoi64(allowance));
}
}
atr_add_raw(player, A_LAST, time_str);
return player;
}
void AddToPublicChannel(dbref player)
{
if ( mudconf.public_channel[0] != '\0'
&& mudconf.public_channel_alias[0] != '\0')
{
do_addcom(player, player, player, 0, 0, 2,
mudconf.public_channel_alias, mudconf.public_channel, nullptr, 0);
}
}
void AddToPlayerChannels(dbref player)
{
if ('\0' == mudconf.player_channels[0])
{
return;
}
LBuf buff = LBuf_Src("AddToPlayerChannels");
mux_strncpy(buff, mudconf.player_channels, LBUF_SIZE - 1);
UTF8 *p = buff.get();
while ('\0' != *p)
{
// Skip leading spaces.
//
while (mux_isspace(*p))
{
p++;
}
if ('\0' == *p)
{
break;
}
// Channel name.
//
UTF8 *channel = p;
while ('\0' != *p && !mux_isspace(*p))
{
p++;
}
if ('\0' != *p)
{
*p++ = '\0';
}
// Skip spaces between channel and alias.
//
while (mux_isspace(*p))
{
p++;
}
if ('\0' == *p)
{
// Odd trailing token (channel without alias) — skip.
//
break;
}
// Alias.
//
UTF8 *alias = p;
while ('\0' != *p && !mux_isspace(*p))
{
p++;
}
if ('\0' != *p)
{
*p++ = '\0';
}
do_addcom(player, player, player, 0, 0, 2, alias, channel,
nullptr, 0);
}
}
/* ---------------------------------------------------------------------------
* create_player: Create a new player.
*/
dbref create_player
(
const UTF8 *name,
const UTF8 *password,
dbref creator,
bool isrobot,
const UTF8 **pmsg
)
{
*pmsg = nullptr;
// Potentially throttle the rate of player creation.
//
if (ThrottlePlayerCreate())
{
*pmsg = T("The limit of new players for this hour has been reached. Please try again later.");
return NOTHING;
}
// Make sure the password is OK. Name is checked in create_obj.
//
LBuf pbuf = LBuf_Adopt(trim_spaces(password));
if (!ok_password(pbuf, pmsg))
{
return NOTHING;
}
// Check if the name is protected by another player.
//
if (!protectname_check(name, NOTHING))
{
*pmsg = T("That name is protected by another player.");
return NOTHING;
}
// If so, go create him.
//
dbref player = create_obj(creator, TYPE_PLAYER, name, isrobot);
if (player == NOTHING)
{
*pmsg = T("Either there is already a player with that name, or that name is illegal.");
return NOTHING;
}
// Initialize everything.
//
ChangePassword(player, pbuf);
s_Home(player, start_home());
pbuf.reset();
if (mudconf.talk_mode_default)
{
s_Flags(player, FLAG_WORD2, Flags2(player) | TALKMODE);
}
local_data_create(player);
ServerEventsSinkNode *p = g_pServerEventsSinkListHead;
while (nullptr != p)
{
p->pSink->data_create(player);
p = p->pNext;
}
return player;
}
/* ---------------------------------------------------------------------------
* do_password: Change the password for a player
*/
void do_password
(
dbref executor,
dbref caller,
dbref enactor,
int eval,
int key,
int nargs,
UTF8 *oldpass,
UTF8 *newpass,
const UTF8 *cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref aowner;
int aflags;
LBuf target = LBuf_Adopt(atr_get("do_password.618", executor, A_PASS, &aowner, &aflags));
const UTF8 *pmsg;
if ( !*target
|| !check_pass(executor, oldpass))
{
notify(executor, M_("Sorry."));
}
else if (ok_password(newpass, &pmsg))
{
ChangePassword(executor, newpass);
notify(executor,M_("Password changed."));
}
else
{
notify(executor, pmsg);
}
}
/* ---------------------------------------------------------------------------
* do_last: Display login history data.
*/
static void disp_from_on(dbref player, const UTF8 *dtm_str, const UTF8 *host_str)
{
if (dtm_str && *dtm_str && host_str && *host_str)
{
notify(player,
tprintf(M_(" From: %s On: %s"), dtm_str, host_str));
}
}
void do_last(dbref executor, dbref caller, dbref enactor, int eval, int key, UTF8 *who, const UTF8 *cargs[], int ncargs)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(key);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
dbref target, aowner;
int i, aflags;
if ( !who
|| !*who)
{
target = Owner(executor);
}
else if (string_compare(who, T("me")) == 0)
{
target = Owner(executor);
}
else
{
target = lookup_player(executor, who, true);
}
if (target == NOTHING)
{
notify(executor, M_("I couldnt find that player."));
}
else if (!( WizRoy(executor)
|| Controls(executor, target)))
{
notify(executor, NOPERM_MESSAGE);
}
else
{
LBuf atrbuf = LBuf_Adopt(atr_get("do_last.684", target, A_LOGINDATA, &aowner, &aflags));
LDATA login_info;
decrypt_logindata(atrbuf, &login_info);
notify(executor, tprintf(M_("Total successful connects: %d"), login_info.tot_good));
for (i = 0; i < NUM_GOOD; i++)
{
disp_from_on(executor, login_info.good[i].host, login_info.good[i].dtm);
}
notify(executor, tprintf(M_("Total failed connects: %d"), login_info.tot_bad));
for (i = 0; i < NUM_BAD; i++)
{
disp_from_on(executor, login_info.bad[i].host, login_info.bad[i].dtm);
}
}
}
/* ---------------------------------------------------------------------------
* add_player_name, delete_player_name, lookup_player:
* Manage playername->dbref mapping
*/
typedef struct
{
dbref dbPlayer;
bool bAlias;
} player_name_entry;
bool add_player_name(dbref player, const UTF8 *name, bool bAlias)
{
if ( !Good_obj(player)
|| !isPlayer(player))
{
return false;
}
bool stat = false;
// Convert to all lowercase.
//
size_t nCased;
UTF8 *pCased = mux_strlwr(name, nCased);
auto it = mudstate.player_htab.find(std::vector<UTF8>(pCased, pCased + nCased));
if (it != mudstate.player_htab.end())
{
player_name_entry *p = static_cast<player_name_entry *>(it->second);
// Entry found in the hashtable. Succeed if the numbers are already
// correctly in the hash table.
//
if ( Good_obj(p->dbPlayer)
&& isPlayer(p->dbPlayer))
{
if ( p->dbPlayer == player
&& p->bAlias == bAlias)
{
return true;
}
else
{
return false;
}
}
// It's an invalid entry. Clobber it.
//
player_name_entry *pOrig = p;
p = nullptr;
try
{
p = new player_name_entry;
}
catch (...)
{
; // Nothing.
}
if (nullptr != p)
{
p->dbPlayer = player;
p->bAlias = bAlias;
it->second = p;
stat = true;
delete pOrig;
pOrig = nullptr;
}
}
else
{
player_name_entry *p = nullptr;
try
{
p = new player_name_entry;
}
catch (...)
{
; // Nothing.
}
if (nullptr != p)
{
p->dbPlayer = player;
p->bAlias = bAlias;
mudstate.player_htab.emplace(std::vector<UTF8>(pCased, pCased + nCased), p);
stat = true;
}
}
return stat;
}
bool delete_player_name(dbref player, const UTF8 *name, bool bAlias)
{
if (NOTHING == player)
{
return false;
}
size_t nCased;
UTF8 *pCased = mux_strlwr(name, nCased);
auto it = mudstate.player_htab.find(std::vector<UTF8>(pCased, pCased + nCased));
if (it == mudstate.player_htab.end())
{
return false;
}
player_name_entry *p = static_cast<player_name_entry *>(it->second);
if ( Good_obj(p->dbPlayer)
&& isPlayer(p->dbPlayer)
&& ( p->dbPlayer != player
|| p->bAlias != bAlias))
{
return false;
}
delete p;
p = nullptr;
mudstate.player_htab.erase(it);
return true;
}
#ifdef SELFCHECK
void delete_all_player_names()
{
for (auto &[key, val] : mudstate.player_htab)
{
player_name_entry *pne = static_cast<player_name_entry *>(val);
delete pne;
}
mudstate.player_htab.clear();
}
#endif
dbref lookup_player_name(const UTF8 *name, bool &bAlias)
{
dbref thing = NOTHING;
size_t nCased;
UTF8 *pCased = mux_strlwr(name, nCased);
auto it = mudstate.player_htab.find(std::vector<UTF8>(pCased, pCased + nCased));
if (it != mudstate.player_htab.end())
{
player_name_entry *p = static_cast<player_name_entry *>(it->second);
if ( nullptr != p
&& Good_obj(p->dbPlayer))
{
thing = p->dbPlayer;
bAlias = p->bAlias;
}
}
return thing;
}
dbref lookup_player(dbref doer, const UTF8 *name, bool check_who)
{
if (string_compare(name, T("me")) == 0)
{
return doer;
}
while (LOOKUP_TOKEN == name[0])
{
name++;
}
dbref thing = NOTHING;
if (NUMBER_TOKEN == name[0])
{
name++;
if (!is_integer(name, nullptr))
{
return NOTHING;
}
thing = mux_atoi64(name);
if (!Good_obj(thing))
{
return NOTHING;
}
if ( !( isPlayer(thing)
|| God(doer)))
{
thing = NOTHING;
}
return thing;
}
bool bAlias = false;
thing = lookup_player_name(name, bAlias);
if ( NOTHING == thing
&& check_who)
{
thing = find_connected_name(doer, name);
if (Hidden(thing))
{
thing = NOTHING;
}
}
return thing;
}
void load_player_names(void)
{
dbref i;
DO_WHOLE_DB(i)
{
if (isPlayer(i))
{
add_player_name(i, Name(i), false);
}
}
LBuf alias = LBuf_Src("load_player_names");
DO_WHOLE_DB(i)
{
if (isPlayer(i))
{
dbref aowner;
int aflags;
atr_pget_str(alias, i, A_ALIAS, &aowner, &aflags);
if (alias[0])
{
add_player_name(i, alias, true);
}
}
}
}
/* ---------------------------------------------------------------------------
* badname_add, badname_check, badname_list: Add/look for/display bad names.
*/
void badname_add(UTF8 *bad_name)
{
// Make a new node and link it in at the top.
//
BADNAME *bp = nullptr;
try
{
bp = new BADNAME;
}
catch (...)
{
; // Nothing.
}
if (nullptr != bp)
{
bp->name = StringClone(bad_name);
bp->next = mudstate.badname_head;
mudstate.badname_head = bp;
}
else
{
STARTLOG(LOG_PROBLEMS, "NAM", "MEM");
log_printf(T("badname_add: out of memory."));
ENDLOG;
}
}
void badname_remove(UTF8 *bad_name)
{
// Look for an exact match on the bad name and remove if found.
//
BADNAME *bp;
BADNAME *backp = nullptr;
for (bp = mudstate.badname_head; bp; backp = bp, bp = bp->next)
{
if (!string_compare(bad_name, bp->name))
{
if (backp)
{
backp->next = bp->next;
}
else
{
mudstate.badname_head = bp->next;
}
MEMFREE(bp->name);
bp->name = nullptr;
delete bp;
bp = nullptr;
return;
}
}
}
bool badname_check(const UTF8 *bad_name)
{
BADNAME *bp;
// Walk the badname list, doing wildcard matching. If we get a hit then
// return false. If no matches in the list, return true.
//
for (bp = mudstate.badname_head; bp; bp = bp->next)
{
mudstate.wild_invk_ctr = 0;
if (quick_wild(bp->name, bad_name))
{
return false;
}
}
return true;
}
void badname_list(dbref player, const UTF8 *prefix)
{
BADNAME *bp;
UTF8 *bufp;
// Construct an lbuf with all the names separated by spaces.
//
LBuf buff = LBuf_Src("badname_list");
bufp = buff.get();
safe_str(prefix, buff, &bufp);
for (bp = mudstate.badname_head; bp; bp = bp->next)
{
safe_chr(' ', buff, &bufp);
safe_str(bp->name, buff, &bufp);
}
*bufp = '\0';
// Now display it.
//
notify(player, buff);
}
// ---------------------------------------------------------------------------
// protectname_check: Check if a name is protected by another player.
// Returns true if the name is available (not protected by someone else).
//
bool protectname_check(const UTF8 *name, dbref player)
{
SEP sepPipe = { 1, { '|' } };
dbref i;
DO_WHOLE_DB(i)
{
if ( !isPlayer(i)
|| i == player)
{
continue;
}
dbref aowner;
int aflags;
LBuf pProtect = LBuf_Adopt(atr_pget(i, A_PROTECTNAME, &aowner, &aflags));
if ('\0' != pProtect[0])
{
UTF8 *bp = pProtect;
UTF8 *token;
while (nullptr != (token = split_token(&bp, sepPipe)))
{
if (0 == string_compare(token, name))
{
return false;
}
}
}
}
return true;
}
// ---------------------------------------------------------------------------
// do_protect: @protect command - reserve player names.
//
void do_protect
(
dbref executor,
dbref caller,
dbref enactor,
int eval,
int key,
int nargs,
UTF8 *arg1,
UTF8 *arg2,
const UTF8 *cargs[],
int ncargs
)
{
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nargs);
UNUSED_PARAMETER(arg2);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (!isPlayer(executor))
{
notify(executor, M_("Only players may use @protect."));
return;
}
// Names are stored pipe-delimited so that names containing spaces
// (allowed when player_name_spaces is on) are handled correctly.
//
SEP sepPipe = { 1, { '|' } };
dbref aowner;
int aflags;
if (key & PROTECT_ALL)
{
if (!Wizard(executor))
{
notify(executor, NOPERM_MESSAGE);
return;
}
bool found_any = false;
dbref i;
DO_WHOLE_DB(i)
{
if (!isPlayer(i))
{
continue;
}
LBuf pProtect = LBuf_Adopt(atr_pget(i, A_PROTECTNAME, &aowner, &aflags));
if ('\0' != pProtect[0])
{
LBuf display = LBuf_Src("do_protect.all");
UTF8 *dp = display.get();
UTF8 *bp = pProtect;
UTF8 *token;
while (nullptr != (token = split_token(&bp, sepPipe)))
{
if (dp != display)
{
safe_str(T(", "), display, &dp);
}
safe_str(token, display, &dp);
}
*dp = '\0';
notify(executor, tprintf(M_("%s: %s"), Name(i), display.get()));
found_any = true;
}
}
if (!found_any)
{
notify(executor, M_("No protected names in the database."));
}
return;
}
if (key & PROTECT_LIST)
{
dbref target = executor;
if ( nullptr != arg1
&& '\0' != arg1[0]
&& Wizard(executor))
{
target = lookup_player(executor, arg1, true);
if (NOTHING == target)
{
notify(executor, M_("No such player."));
return;
}
}
LBuf pProtect = LBuf_Adopt(atr_pget(target, A_PROTECTNAME, &aowner, &aflags));
if ('\0' == pProtect[0])
{
notify(executor, M_("No protected names."));
}
else
{
// Display pipe-delimited list as comma-separated for readability.
//
LBuf display = LBuf_Src("do_protect.list");
UTF8 *dp = display.get();
UTF8 *bp = pProtect;
UTF8 *token;
while (nullptr != (token = split_token(&bp, sepPipe)))
{
if (dp != display)
{
safe_str(T(", "), display, &dp);
}
safe_str(token, display, &dp);
}
*dp = '\0';
notify(executor, tprintf(M_("Protected names for %s: %s"), Name(target), display.get()));
}
return;
}
if ( nullptr == arg1
|| '\0' == arg1[0])
{
notify(executor, M_("Protect what name?"));
return;
}
if (key & PROTECT_ALIAS)
{
// Set a protected name as this player's alias.
// The name must be in the player's protected list.
//
LBuf pProtect = LBuf_Adopt(atr_pget(executor, A_PROTECTNAME, &aowner, &aflags));
bool found = false;
if ('\0' != pProtect[0])
{
UTF8 *bp = pProtect;
UTF8 *token;
while (nullptr != (token = split_token(&bp, sepPipe)))
{
if (0 == string_compare(token, arg1))
{
found = true;
break;
}
}
}
if (!found)
{
notify(executor, M_("That name is not in your protected list."));
return;
}
// Check the name isn't already in use by someone else.
//
bool bAlias = false;
dbref nPlayer = lookup_player_name(arg1, bAlias);
if ( NOTHING != nPlayer
&& ( nPlayer != executor
|| !bAlias))
{
notify(executor, M_("That name is already in use."));
return;
}
// Remove old alias if any, set new one.
//
{
LBuf oldalias = LBuf_Adopt(atr_pget(executor, A_ALIAS, &aowner, &aflags));
if ('\0' != oldalias[0])
{
delete_player_name(executor, oldalias, true);
}
}
atr_add(executor, A_ALIAS, arg1, Owner(executor), aflags);
if (add_player_name(executor, arg1, true))
{
notify(executor, tprintf(M_("Alias set to %s."), arg1));
}
else
{
notify(executor, M_("That name is already in use or is illegal, alias cleared."));
atr_clr(executor, A_ALIAS);
}
return;
}
if (key & PROTECT_UNALIAS)
{
// Remove the player's alias, but only if it matches a protected name.
//
LBuf oldalias = LBuf_Adopt(atr_pget(executor, A_ALIAS, &aowner, &aflags));
if ('\0' == oldalias[0])
{
notify(executor, M_("You have no alias set."));
return;
}
if (0 != string_compare(oldalias, arg1))
{
notify(executor, tprintf(M_("Your alias is %s, not %s."),
oldalias.get(), arg1));
return;
}
delete_player_name(executor, oldalias, true);
atr_clr(executor, A_ALIAS);
notify(executor, tprintf(M_("Alias %s removed."), oldalias.get()));
return;
}
if (key & PROTECT_DEL)
{
// Remove a protected name.
//
LBuf pProtect = LBuf_Adopt(atr_pget(executor, A_PROTECTNAME, &aowner, &aflags));
if ('\0' == pProtect[0])
{
notify(executor, M_("You have no protected names."));
return;
}
LBuf newlist = LBuf_Src("do_protect.del");
UTF8 *np = newlist.get();
bool found = false;
UTF8 *bp = pProtect;
UTF8 *token;
while (nullptr != (token = split_token(&bp, sepPipe)))
{
if (!found && 0 == string_compare(token, arg1))
{
found = true;
continue;
}
if (np != newlist)
{
safe_chr('|', newlist, &np);
}
safe_str(token, newlist, &np);
}
*np = '\0';
if (!found)
{
notify(executor, M_("That name is not in your protected list."));
}
else
{
atr_add_raw(executor, A_PROTECTNAME, newlist);
notify(executor, tprintf(M_("Name %s removed from protected list."), arg1));
}
return;
}
// Default: /add
//
if (!ValidatePlayerName(arg1))
{
notify(executor, M_("That is not a valid player name."));
return;
}
if (!badname_check(arg1))
{
notify(executor, M_("That name is not allowed."));
return;
}
// Check the per-player limit.
//
LBuf pProtect = LBuf_Adopt(atr_pget(executor, A_PROTECTNAME, &aowner, &aflags));
int count = 0;
if ('\0' != pProtect[0])
{
UTF8 *bp = pProtect;
UTF8 *token;
while (nullptr != (token = split_token(&bp, sepPipe)))
{
if (0 == string_compare(token, arg1))
{
notify(executor, M_("That name is already in your protected list."));
return;
}
count++;
}
}
if (count >= mudconf.max_name_protect)
{
notify(executor, tprintf(M_("You may only protect %d names."), mudconf.max_name_protect));
return;
}
// Check if name is protected by someone else.
//
if (!protectname_check(arg1, executor))
{
notify(executor, M_("That name is already protected by another player."));
return;
}
// Add the name.
//
LBuf newlist = LBuf_Src("do_protect.add");
UTF8 *np = newlist.get();
if ('\0' != pProtect[0])
{
safe_str(pProtect, newlist, &np);
safe_chr('|', newlist, &np);
}
safe_str(arg1, newlist, &np);
*np = '\0';
atr_add_raw(executor, A_PROTECTNAME, newlist);
notify(executor, tprintf(M_("Name %s added to protected list."), arg1));
}