25ad97555f
Voice goes through a separate relay (moonwell-voice-relay, from the MoonWell client repo), never the world socket. mangosd only vouches: `.mwvoice` answers the player with an HMAC-signed ticket and the relay address as an addon line (prefix MWV), and every 250 ms the world thread sends the relay each character in the world: faction, map, instance, position, group and guild, so no client can claim a place, group or guild of its own. The feed is read after the map update, under the lock that keeps async packets out. Sends are counted and logged every 30 s. MoonWell.Voice.* in mangosd.conf (off by default); the production compose adds the voice-relay container (UDP 3725) and the voice_secret secret, deploy/local enables voice against a relay on the Windows host. Checked: the changed units build in vmangos-buildenv; a ticket and a feed datagram made by this code verify in the relay tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
278 lines
9.8 KiB
C++
278 lines
9.8 KiB
C++
/*
|
|
* 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 "MoonWellVoice.h"
|
|
#include "ByteBuffer.h"
|
|
#include "Chat.h"
|
|
#include "Config/Config.h"
|
|
#include "Group.h"
|
|
#include "Log.h"
|
|
#include "Player.h"
|
|
#include "WorldPacket.h"
|
|
#include "WorldSession.h"
|
|
|
|
#include <openssl/evp.h>
|
|
#include <openssl/hmac.h>
|
|
|
|
#include <chrono>
|
|
#include <cerrno>
|
|
#include <cstring>
|
|
|
|
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h>
|
|
#else
|
|
#include <fcntl.h>
|
|
#include <netdb.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
namespace MoonWellVoice
|
|
{
|
|
// Must match the relay: feed::MAX_PER_DATAGRAM keeps a datagram under a 1280-byte MTU.
|
|
static uint8 const VERSION = 1;
|
|
static size_t const MAC_LEN = 16;
|
|
static size_t const MAX_PER_DATAGRAM = 31;
|
|
// A failed socket or name lookup is retried this often, so the relay may start after mangosd.
|
|
static uint32 const REOPEN_MS = 10 * IN_MILLISECONDS;
|
|
|
|
static bool s_enabled = false;
|
|
static std::string s_secret;
|
|
static std::string s_address;
|
|
static std::string s_feed;
|
|
static uint32 s_interval = 250;
|
|
static uint32 s_ticketLifetime = HOUR;
|
|
static uint32 s_sinceFeed = 0;
|
|
static uint32 s_sinceOpen = REOPEN_MS;
|
|
// What the feed did since the last report, logged every REPORT_MS when it changed, so a feed
|
|
// that stops or fails shows in the log.
|
|
static uint32 const REPORT_MS = 30 * IN_MILLISECONDS;
|
|
static uint32 s_sinceReport = 0;
|
|
static uint32 s_sent = 0;
|
|
static uint32 s_failed = 0;
|
|
static int s_lastError = 0;
|
|
static uint32 s_lastCount = 0;
|
|
#ifdef _WIN32
|
|
static SOCKET s_socket = INVALID_SOCKET;
|
|
static bool IsOpen() { return s_socket != INVALID_SOCKET; }
|
|
static void CloseSocket() { if (IsOpen()) closesocket(s_socket); s_socket = INVALID_SOCKET; }
|
|
#else
|
|
static int s_socket = -1;
|
|
static bool IsOpen() { return s_socket >= 0; }
|
|
static void CloseSocket() { if (IsOpen()) close(s_socket); s_socket = -1; }
|
|
#endif
|
|
|
|
void LoadConfig()
|
|
{
|
|
CloseSocket();
|
|
s_enabled = sConfig.GetBoolDefault("MoonWell.Voice.Enable", false);
|
|
s_secret = sConfig.GetStringDefault("MoonWell.Voice.Secret", "");
|
|
s_address = sConfig.GetStringDefault("MoonWell.Voice.Address", "");
|
|
s_feed = sConfig.GetStringDefault("MoonWell.Voice.Feed", "127.0.0.1:3726");
|
|
s_interval = std::max(100, sConfig.GetIntDefault("MoonWell.Voice.FeedInterval", 250));
|
|
s_ticketLifetime = std::max(60, sConfig.GetIntDefault("MoonWell.Voice.TicketLifetime", HOUR));
|
|
s_sinceOpen = REOPEN_MS;
|
|
|
|
if (!s_enabled)
|
|
return;
|
|
// The relay refuses a shorter secret, so the core would sign what it never accepts.
|
|
if (s_secret.size() < 32 || s_address.empty())
|
|
{
|
|
sLog.Out(LOG_BASIC, LOG_LVL_ERROR, "MoonWell.Voice: Secret (32+ chars) and Address are required, voice disabled");
|
|
s_enabled = false;
|
|
}
|
|
}
|
|
|
|
bool IsEnabled()
|
|
{
|
|
return s_enabled;
|
|
}
|
|
|
|
// The relay's truncated HMAC-SHA256: the label first, so a ticket never verifies as a feed
|
|
// datagram nor the other way round.
|
|
static void Sign(char const* label, ByteBuffer& buffer)
|
|
{
|
|
std::vector<uint8> message(label, label + strlen(label));
|
|
message.insert(message.end(), buffer.contents(), buffer.contents() + buffer.size());
|
|
uint8 mac[EVP_MAX_MD_SIZE];
|
|
#if defined(OPENSSL_VERSION_MAJOR) && (OPENSSL_VERSION_MAJOR >= 3)
|
|
size_t macLen = 0;
|
|
EVP_Q_mac(nullptr, "HMAC", nullptr, "SHA256", nullptr, s_secret.data(), s_secret.size(),
|
|
message.data(), message.size(), mac, sizeof(mac), &macLen);
|
|
#else
|
|
unsigned int macLen = 0;
|
|
HMAC(EVP_sha256(), s_secret.data(), int(s_secret.size()), message.data(), message.size(), mac, &macLen);
|
|
#endif
|
|
buffer.append(mac, MAC_LEN);
|
|
}
|
|
|
|
static uint64 UnixMs()
|
|
{
|
|
using namespace std::chrono;
|
|
return uint64(duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count());
|
|
}
|
|
|
|
// The relay is named host:port; a host name is looked up here, so a container name works.
|
|
static void OpenSocket()
|
|
{
|
|
size_t colon = s_feed.rfind(':');
|
|
if (colon == std::string::npos)
|
|
{
|
|
sLog.Out(LOG_BASIC, LOG_LVL_ERROR, "MoonWell.Voice.Feed '%s' is not host:port", s_feed.c_str());
|
|
return;
|
|
}
|
|
std::string host = s_feed.substr(0, colon);
|
|
std::string port = s_feed.substr(colon + 1);
|
|
|
|
addrinfo hints = {};
|
|
hints.ai_family = AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_DGRAM;
|
|
addrinfo* found = nullptr;
|
|
if (getaddrinfo(host.c_str(), port.c_str(), &hints, &found) != 0 || !found)
|
|
{
|
|
sLog.Out(LOG_BASIC, LOG_LVL_ERROR, "MoonWell.Voice: cannot resolve the relay %s", s_feed.c_str());
|
|
return;
|
|
}
|
|
s_socket = socket(found->ai_family, found->ai_socktype, found->ai_protocol);
|
|
// Connected, so a send needs no address; non-blocking, so a full buffer drops a tick and
|
|
// never stalls the world thread.
|
|
if (IsOpen() && connect(s_socket, found->ai_addr, int(found->ai_addrlen)) != 0)
|
|
CloseSocket();
|
|
freeaddrinfo(found);
|
|
if (!IsOpen())
|
|
return;
|
|
#ifdef _WIN32
|
|
u_long nonBlocking = 1;
|
|
ioctlsocket(s_socket, FIONBIO, &nonBlocking);
|
|
#else
|
|
fcntl(s_socket, F_SETFL, fcntl(s_socket, F_GETFL, 0) | O_NONBLOCK);
|
|
#endif
|
|
sLog.Out(LOG_BASIC, LOG_LVL_MINIMAL, "MoonWell.Voice: feeding the relay at %s", s_feed.c_str());
|
|
}
|
|
|
|
bool FeedDue(uint32 diff)
|
|
{
|
|
if (!s_enabled)
|
|
return false;
|
|
s_sinceFeed += diff;
|
|
s_sinceReport += diff;
|
|
if (s_sinceReport >= REPORT_MS)
|
|
{
|
|
s_sinceReport = 0;
|
|
if (s_sent || s_failed)
|
|
sLog.Out(LOG_BASIC, LOG_LVL_MINIMAL, "MoonWell.Voice: %u feed datagrams sent, %u failed (last error %d), %u characters in the last",
|
|
s_sent, s_failed, s_lastError, s_lastCount);
|
|
s_sent = 0;
|
|
s_failed = 0;
|
|
}
|
|
if (!IsOpen())
|
|
{
|
|
s_sinceOpen += diff;
|
|
if (s_sinceOpen < REOPEN_MS)
|
|
return false;
|
|
s_sinceOpen = 0;
|
|
OpenSocket();
|
|
if (!IsOpen())
|
|
return false;
|
|
}
|
|
if (s_sinceFeed < s_interval)
|
|
return false;
|
|
s_sinceFeed = 0;
|
|
return true;
|
|
}
|
|
|
|
static void SendDatagram(uint64 sentMs, ByteBuffer const& entries, uint8 count)
|
|
{
|
|
ByteBuffer datagram(entries.size() + 32);
|
|
datagram.append("MWVF", 4);
|
|
datagram << VERSION << sentMs << count;
|
|
datagram.append(entries.contents(), entries.size());
|
|
Sign("moonwell-voice feed", datagram);
|
|
if (send(s_socket, reinterpret_cast<char const*>(datagram.contents()), int(datagram.size()), 0) >= 0)
|
|
++s_sent;
|
|
else
|
|
{
|
|
++s_failed;
|
|
#ifdef _WIN32
|
|
s_lastError = WSAGetLastError();
|
|
#else
|
|
s_lastError = errno;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void SendFeed(std::vector<Player*> const& players)
|
|
{
|
|
if (!IsOpen())
|
|
return;
|
|
uint64 sentMs = UnixMs();
|
|
s_lastCount = uint32(players.size());
|
|
ByteBuffer entries(MAX_PER_DATAGRAM * 37);
|
|
uint8 count = 0;
|
|
for (Player* player : players)
|
|
{
|
|
Group const* group = player->GetGroup();
|
|
entries << uint64(player->GetObjectGuid().GetRawValue());
|
|
entries << uint8(player->GetTeam() == ALLIANCE ? 0 : 1);
|
|
entries << uint32(player->GetMapId());
|
|
entries << uint32(player->GetInstanceId());
|
|
entries << player->GetPositionX() << player->GetPositionY() << player->GetPositionZ();
|
|
entries << uint32(group ? group->GetId() : 0);
|
|
entries << uint32(player->GetGuildId());
|
|
if (++count == MAX_PER_DATAGRAM)
|
|
{
|
|
SendDatagram(sentMs, entries, count);
|
|
entries.clear();
|
|
count = 0;
|
|
}
|
|
}
|
|
if (count)
|
|
SendDatagram(sentMs, entries, count);
|
|
}
|
|
|
|
static void SendLine(Player* player, std::string const& payload)
|
|
{
|
|
std::string text = "MWV\t" + payload;
|
|
WorldPacket data;
|
|
ChatHandler::BuildChatPacket(data, CHAT_MSG_WHISPER, text.c_str(), LANG_ADDON, CHAT_TAG_NONE,
|
|
player->GetObjectGuid(), player->GetName());
|
|
player->GetSession()->SendPacket(&data);
|
|
}
|
|
|
|
// "T<ticket hex>\t<relay address>", or "off" when voice is disabled.
|
|
void SendTicket(Player* player)
|
|
{
|
|
if (!s_enabled)
|
|
{
|
|
SendLine(player, "off");
|
|
return;
|
|
}
|
|
ByteBuffer ticket(64);
|
|
ticket << VERSION << uint64(player->GetObjectGuid().GetRawValue()) << uint64(UnixMs() / 1000 + s_ticketLifetime);
|
|
Sign("moonwell-voice token", ticket);
|
|
|
|
static char const digits[] = "0123456789abcdef";
|
|
std::string hex;
|
|
for (size_t i = 0; i < ticket.size(); ++i)
|
|
{
|
|
hex += digits[ticket.contents()[i] >> 4];
|
|
hex += digits[ticket.contents()[i] & 0xF];
|
|
}
|
|
SendLine(player, "T" + hex + "\t" + s_address);
|
|
}
|
|
}
|