This commit is contained in:
Aria 2025-03-21 22:23:30 +11:00
commit 9c94d113d3
Signed by untrusted user who does not match committer: aria
GPG key ID: 19AB7AA462B8AB3B
10260 changed files with 1237388 additions and 0 deletions

View file

@ -0,0 +1,25 @@
INCLUDE_DIRECTORIES (
${STAR_EXTERN_INCLUDES}
${STAR_CORE_INCLUDES}
${STAR_BASE_INCLUDES}
${STAR_PLATFORM_INCLUDES}
${STAR_GAME_INCLUDES}
)
SET (star_server_HEADERS
StarServerQueryThread.hpp
StarServerRconClient.hpp
StarServerRconThread.hpp
)
SET (star_server_SOURCES
StarServerQueryThread.cpp
StarServerRconClient.cpp
StarServerRconThread.cpp
main.cpp
)
ADD_EXECUTABLE (starbound_server
$<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core> $<TARGET_OBJECTS:star_base> $<TARGET_OBJECTS:star_game>
${star_server_HEADERS} ${star_server_SOURCES})
TARGET_LINK_LIBRARIES (starbound_server ${STAR_EXT_LIBS})

View file

@ -0,0 +1,278 @@
#include "StarServerQueryThread.hpp"
#include "StarLogging.hpp"
#include "StarRoot.hpp"
#include "StarConfiguration.hpp"
#include "StarVersion.hpp"
#include "StarUniverseServer.hpp"
#include "StarIterator.hpp"
namespace Star {
ServerQueryThread::ServerQueryThread(UniverseServer* universe, HostAddressWithPort const& bindAddress)
: Thread("QueryServer"),
m_universe(universe),
m_queryServer(bindAddress),
m_stop(true),
m_lastChallengeCheck(Time::monotonicMilliseconds()) {
m_playersResponse.resize(A2S_PACKET_SIZE);
m_playersResponse.setByteOrder(ByteOrder::LittleEndian);
m_playersResponse.setNullTerminatedStrings(true);
m_rulesResponse.resize(A2S_PACKET_SIZE);
m_rulesResponse.setByteOrder(ByteOrder::LittleEndian);
m_rulesResponse.setNullTerminatedStrings(true);
m_generalResponse.resize(A2S_PACKET_SIZE);
m_generalResponse.setByteOrder(ByteOrder::LittleEndian);
m_generalResponse.setNullTerminatedStrings(true);
m_serverPort = 0;
m_lastActiveTime = 0;
auto& root = Root::singleton();
auto cfg = root.configuration();
m_maxPlayers = cfg->get("maxPlayers").toUInt();
m_serverName = cfg->get("serverName").toString();
m_lastPlayersResponse = 0;
m_lastRulesResponse = 0;
}
ServerQueryThread::~ServerQueryThread() {
stop();
join();
}
void ServerQueryThread::start() {
m_stop = false;
Thread::start();
m_lastActiveTime = Time::monotonicMilliseconds();
}
void ServerQueryThread::stop() {
m_stop = true;
m_queryServer.close();
}
void ServerQueryThread::sendTo(HostAddressWithPort const& address, DataStreamBuffer* ds) {
m_queryServer.send(address, ds->ptr(), ds->size());
}
uint8_t ServerQueryThread::serverPlayerCount() {
return m_universe->numberOfClients();
}
bool ServerQueryThread::serverPassworded() {
// TODO: implement
return false;
}
String ServerQueryThread::serverWorldNames() {
auto activeWorlds = m_universe->activeWorlds();
if (activeWorlds.empty())
return String("Unknown");
return StringList(activeWorlds.transformed(printWorldId)).join(",");
}
const char* ServerQueryThread::serverPlugins() {
// TODO: implement
return "none";
}
bool ServerQueryThread::processPacket(HostAddressWithPort const& address, char const* data, size_t length) {
uint8_t* buf = (uint8_t*)data;
if (length < 5 || buf[0] != 0xff || buf[1] != 0xff || buf[2] != 0xff || buf[3] != 0xff) {
// short packet or missing header
return false;
}
// Process packet
switch (buf[4]) {
case A2S_INFO_REQUEST: {
// We use -6 and not -5 as the string should be NULL terminated
// but instead of the std::string constructor stopping at the NULL
// it includes it :(
std::string str((const char*)(buf + 5), length - 6);
if (str.compare(A2S_INFO_REQUEST_STRING) != 0) {
// Invalid request
return false;
}
m_generalResponse.clear();
m_generalResponse << A2S_HEAD_INT << A2S_INFO_REPLY << A2S_VERSION << m_serverName << serverWorldNames()
<< GAME_DIR << GAME_DESC << A2S_APPID // Should be SteamAppId but this isn't a short :(
<< serverPlayerCount() << m_maxPlayers << (uint8_t)0x00 // bots
<< A2S_TYPE_DEDICATED // dedicated
#ifdef STAR_SYSTEM_FAMILY_WINDOWS
<< A2S_ENV_WINDOWS // os
#else
<< A2S_ENV_LINUX // os
#endif
<< serverPassworded() << A2S_VAC_OFF // secure
<< StarVersionString << A2S_EDF_PORT // EDF
<< m_serverPort;
sendTo(address, &m_generalResponse);
return true;
}
case A2S_CHALLENGE_REQUEST:
sendChallenge(address);
return true;
case A2S_PLAYER_REQUEST:
if (challengeRequest(address, data, length))
return true;
if (!validChallenge(address, data, length))
return false;
buildPlayerResponse();
sendTo(address, &m_playersResponse);
return true;
case A2S_RULES_REQUEST:
if (challengeRequest(address, data, length))
return true;
if (!validChallenge(address, data, length))
return false;
buildRuleResponse();
sendTo(address, &m_rulesResponse);
return true;
}
return false;
}
void ServerQueryThread::buildPlayerResponse() {
int64_t now = Time::monotonicMilliseconds();
if (now < m_lastPlayersResponse + responseCacheTime) {
return;
}
auto clientIds = m_universe->clientIds();
uint8_t cnt = (uint8_t)clientIds.count();
int32_t kills = 0; // Not currently supported
float timeConnected = 60; // Not supported defaults to 1min
m_playersResponse.clear();
m_playersResponse << A2S_HEAD_INT << A2S_PLAYER_REPLY << cnt;
uint8_t i = 0;
for (auto clientId : clientIds) {
m_playersResponse << i++ << m_universe->clientNick(clientId) << kills << timeConnected;
}
m_lastPlayersResponse = now;
}
void ServerQueryThread::buildRuleResponse() {
int64_t now = Time::monotonicMilliseconds();
if (now < m_lastRulesResponse + responseCacheTime) {
return;
}
uint16_t cnt = 1;
m_rulesResponse.clear();
m_rulesResponse << A2S_HEAD_INT << A2S_RULES_REPLY << cnt << "plugins" << serverPlugins();
m_lastRulesResponse = now;
}
void ServerQueryThread::sendChallenge(HostAddressWithPort const& address) {
auto challenge = make_shared<RequestChallenge>();
m_validChallenges[address.address()] = challenge;
m_generalResponse.clear();
m_generalResponse << A2S_HEAD_INT << A2S_CHALLENGE_RESPONSE << challenge->getChallenge();
sendTo(address, &m_generalResponse);
}
void ServerQueryThread::pruneChallenges() {
int64_t now = Time::monotonicMilliseconds();
if (now < m_lastChallengeCheck + challengeCheckInterval) {
return;
}
auto expire = now - challengeCheckInterval;
auto it = makeSMutableMapIterator(m_validChallenges);
while (it.hasNext()) {
auto const& pair = it.next();
if (pair.second->before(expire)) {
it.remove();
}
}
m_lastChallengeCheck = now;
}
void ServerQueryThread::run() {
HostAddressWithPort udpAddress;
char udpData[MaxUdpData];
while (!m_stop) {
try {
auto len = m_queryServer.receive(&udpAddress, udpData, MaxUdpData, 100);
pruneChallenges();
if (len != 0)
processPacket(udpAddress, udpData, len);
} catch (SocketClosedException const&) {
} catch (std::exception const& e) {
Logger::error("ServerQueryThread exception caught: %s", outputException(e, true));
}
}
}
ServerQueryThread::RequestChallenge::RequestChallenge()
: m_time(Time::monotonicMilliseconds()), m_challenge(Random::randi32()) {}
bool ServerQueryThread::RequestChallenge::before(uint64_t time) {
return m_time < time;
}
int ServerQueryThread::RequestChallenge::getChallenge() {
return m_challenge;
}
bool ServerQueryThread::validChallenge(HostAddressWithPort const& address, char const* data, size_t len) {
if (len != 9) {
// too much or too little data
return false;
}
if (m_validChallenges.count(address.address()) == 0) {
// Don't know this source address ignore
return false;
}
uint8_t const* b = (uint8_t const*)data;
int32_t challenge = ((int32_t)b[8] & 0xff) << 24 | ((int32_t)b[7] & 0xff) << 16 | ((int32_t)b[6] & 0xff) << 8
| ((int32_t)b[5] & 0xff);
// Note: No byte order swapping needed as protcol performs no conversion
if (m_validChallenges.get(address.address())->getChallenge() != challenge) {
// Challenges didnt match ignore
return false;
}
// All good
return true;
}
bool ServerQueryThread::challengeRequest(HostAddressWithPort const& address, char const* data, size_t len) {
if (len != 9) {
// too much or too little data
return false;
}
uint8_t const* buf = (uint8_t const*)data;
if ((buf[5] == 0xff) && (buf[6] == 0xff) && (buf[7] == 0xff) && (buf[8] == 0xff)) {
sendChallenge(address);
return true;
}
return false;
}
}

View file

@ -0,0 +1,108 @@
#ifndef STAR_SERVER_QUERY_THREAD_HPP
#define STAR_SERVER_QUERY_THREAD_HPP
#include "StarThread.hpp"
#include "StarHostAddress.hpp"
#include "StarUdp.hpp"
#include "StarMap.hpp"
#include "StarDataStreamDevices.hpp"
#include <random>
namespace Star {
STAR_CLASS(UniverseServer);
STAR_CLASS(ServerQueryThread);
class ServerQueryThread : public Thread {
public:
ServerQueryThread(UniverseServer* universe, HostAddressWithPort const& bindAddress);
~ServerQueryThread();
void start();
void stop();
protected:
virtual void run();
private:
static const uint8_t A2A_PING_REQUEST = 0x69;
static const uint8_t A2A_PING_REPLY = 0x6a;
static const uint8_t A2S_CHALLENGE_REQUEST = 0x57;
static const uint8_t A2S_CHALLENGE_RESPONSE = 0x41;
static const uint8_t A2S_INFO_REQUEST = 0x54;
static const uint8_t A2S_INFO_REPLY = 0x49;
static const uint8_t A2S_PLAYER_REQUEST = 0x55;
static const uint8_t A2S_PLAYER_REPLY = 0x44;
static const uint8_t A2S_RULES_REQUEST = 0x56;
static const uint8_t A2S_RULES_REPLY = 0x45;
static const uint8_t A2S_VERSION = 0x07;
static const uint8_t A2S_STR_TERM = 0x00;
static const uint8_t A2S_EDF_GID = 0x01;
static const uint8_t A2S_EDF_SID = 0x10;
static const uint8_t A2S_EDF_TAGS = 0x20;
static const uint8_t A2S_EDF_STV = 0x40;
static const uint8_t A2S_EDF_PORT = 0x80;
static const uint8_t A2S_ENV_WINDOWS = 'W';
static const uint8_t A2S_ENV_LINUX = 'L';
static const uint8_t A2S_TYPE_DEDICATED = 'D';
static const uint8_t A2S_TYPE_LISTEN = 'L';
static const uint8_t A2S_TYPE_TV = 'P';
static const uint8_t A2S_VAC_OFF = 0x00;
static const uint8_t A2S_VAC_ON = 0x01;
static constexpr const char* A2S_INFO_REQUEST_STRING = "Source Engine Query";
static const uint16_t A2S_APPID = (uint16_t)0xfffe;
static const uint16_t A2S_PACKET_SIZE = (uint16_t)0x4e0;
static const uint32_t A2S_HEAD_INT = 0xffffffff;
static constexpr const char* GAME_DIR = "starbound";
static constexpr const char* GAME_DESC = "Starbound";
static constexpr const char* GAME_TYPE = "SMP";
static const int32_t challengeCheckInterval = 30000;
static const int32_t responseCacheTime = 5000;
void sendTo(HostAddressWithPort const& address, DataStreamBuffer* ds);
bool processPacket(HostAddressWithPort const& address, char const* data, size_t length);
void buildPlayerResponse();
void buildRuleResponse();
bool validChallenge(HostAddressWithPort const& address, char const* data, size_t length);
void sendChallenge(HostAddressWithPort const& address);
void pruneChallenges();
bool challengeRequest(HostAddressWithPort const& address, char const* data, size_t length);
// Server API
uint8_t serverPlayerCount();
bool serverPassworded();
const char* serverPlugins();
String serverWorldNames();
UniverseServer* m_universe;
UdpServer m_queryServer;
bool m_stop;
DataStreamBuffer m_playersResponse;
DataStreamBuffer m_rulesResponse;
DataStreamBuffer m_generalResponse;
class RequestChallenge {
public:
RequestChallenge();
bool before(uint64_t time);
int32_t getChallenge();
private:
uint64_t m_time;
int32_t m_challenge;
};
uint16_t m_serverPort;
uint8_t m_maxPlayers;
String m_serverName;
HashMap<HostAddress, shared_ptr<RequestChallenge>> m_validChallenges;
int64_t m_lastChallengeCheck;
int64_t m_lastPlayersResponse;
int64_t m_lastRulesResponse;
int64_t m_lastActiveTime;
};
}
#endif

View file

@ -0,0 +1,145 @@
#include "StarServerRconThread.hpp"
#include "StarServerRconClient.hpp"
#include "StarLogging.hpp"
#include "StarRoot.hpp"
#include "StarConfiguration.hpp"
#include "StarUniverseServer.hpp"
#include "StarLexicalCast.hpp"
namespace Star {
ServerRconClient::ServerRconClient(UniverseServer* universe, TcpSocketPtr socket)
: Thread("RconClient"),
m_universe(universe),
m_socket(socket),
m_packetBuffer(MaxPacketSize),
m_stop(true),
m_authed(false) {
auto& root = Root::singleton();
auto cfg = root.configuration();
m_packetBuffer.setByteOrder(ByteOrder::LittleEndian);
m_packetBuffer.setNullTerminatedStrings(true);
m_rconPassword = cfg->get("rconServerPassword").toString();
}
ServerRconClient::~ServerRconClient() {
stop();
join();
}
String ServerRconClient::handleCommand(String commandLine) {
String command = commandLine.extract();
if (command == "echo") {
return commandLine;
} else if (command == "broadcast" || command == "say") {
m_universe->adminBroadcast(commandLine);
return strf("OK: said %s", commandLine);
} else if (command == "stop") {
m_universe->stop();
return "OK: shutting down";
} else {
return m_universe->adminCommand(strf("%s %s", command, commandLine));
}
}
void ServerRconClient::receive(size_t size) {
m_packetBuffer.reset(size);
auto ptr = m_packetBuffer.ptr();
while (size > 0) {
size_t r = m_socket->receive(ptr, size);
if (r == 0)
throw NoMoreRequests();
size -= r;
ptr += r;
}
}
void ServerRconClient::send(uint32_t requestId, uint32_t cmd, String str) {
m_packetBuffer.clear();
m_packetBuffer << (uint32_t)(str.utf8Size() + 10) << requestId << cmd << str << (uint8_t)0x00;
m_socket->send(m_packetBuffer.ptr(), m_packetBuffer.size());
}
void ServerRconClient::sendAuthFailure() {
send(SERVERDATA_AUTH_FAILURE, SERVERDATA_AUTH_RESPONSE, "");
}
void ServerRconClient::sendCmdResponse(uint32_t requestId, String response) {
size_t len = response.length();
// Always send at least one packet even if the response was blank
do {
auto dataLen = (len >= MaxPacketSize) ? MaxPacketSize : len;
send(requestId, SERVERDATA_RESPONSE_VALUE, response.substr(0, dataLen));
response = response.substr(dataLen);
len = response.length();
} while (len > 0);
}
void ServerRconClient::start() {
m_stop = false;
Thread::start();
}
void ServerRconClient::stop() {
m_stop = true;
m_socket->close();
}
void ServerRconClient::processRequest() {
receive(4);
uint32_t size = m_packetBuffer.read<uint32_t>();
receive(size);
uint32_t requestId;
m_packetBuffer >> requestId;
uint32_t cmd;
m_packetBuffer >> cmd;
switch (cmd) {
case SERVERDATA_AUTH: {
String password;
m_packetBuffer >> password;
if (!m_rconPassword.empty() && m_rconPassword.equals(password)) {
m_authed = true;
send(requestId, SERVERDATA_RESPONSE_VALUE);
send(requestId, SERVERDATA_AUTH_RESPONSE);
} else {
m_authed = false;
sendAuthFailure();
}
break;
}
case SERVERDATA_EXECCOMMAND:
if (m_authed) {
String command;
m_packetBuffer >> command;
try {
Logger::info("RCON %s: %s", m_socket->remoteAddress(), command);
sendCmdResponse(requestId, handleCommand(command));
} catch (std::exception const& e) {
sendCmdResponse(requestId, strf("RCON: Error executing: %s: %s", command, outputException(e, true)));
}
} else {
sendAuthFailure();
}
break;
default:
sendCmdResponse(requestId, strf("Unknown request %06x", cmd));
}
}
void ServerRconClient::run() {
try {
while (!m_stop)
processRequest();
} catch (NoMoreRequests const&) {
} catch (std::exception const& e) {
Logger::error("ServerRconClient exception caught: %s", outputException(e, false));
}
}
}

View file

@ -0,0 +1,51 @@
#ifndef STAR_SERVER_RCON_CLIENT_HPP
#define STAR_SERVER_RCON_CLIENT_HPP
#include "StarThread.hpp"
#include "StarTcp.hpp"
#include "StarMap.hpp"
#include "StarDataStreamDevices.hpp"
namespace Star {
class UniverseServer;
class ServerRconClient : public Thread {
public:
static const uint32_t SERVERDATA_AUTH = 0x03;
static const uint32_t SERVERDATA_EXECCOMMAND = 0x02;
static const uint32_t SERVERDATA_RESPONSE_VALUE = 0x00;
static const uint32_t SERVERDATA_AUTH_RESPONSE = 0x02;
static const uint32_t SERVERDATA_AUTH_FAILURE = 0xffffffff;
ServerRconClient(UniverseServer* universe, TcpSocketPtr socket);
~ServerRconClient();
void start();
void stop();
protected:
virtual void run();
private:
static size_t const MaxPacketSize = 4096;
STAR_EXCEPTION(NoMoreRequests, StarException);
void receive(size_t size);
void send(uint32_t requestId, uint32_t cmd, String str = "");
void sendAuthFailure();
void sendCmdResponse(uint32_t requestId, String response);
void closeSocket();
void processRequest();
String handleCommand(String commandLine);
UniverseServer* m_universe;
TcpSocketPtr m_socket;
DataStreamBuffer m_packetBuffer;
bool m_stop;
bool m_authed;
String m_rconPassword;
};
typedef shared_ptr<ServerRconClient> ServerRconClientPtr;
}
#endif

View file

@ -0,0 +1,62 @@
#include "StarServerRconThread.hpp"
#include "StarLogging.hpp"
#include "StarRoot.hpp"
#include "StarConfiguration.hpp"
#include "StarUniverseServer.hpp"
#include "StarServerRconClient.hpp"
#include "StarIterator.hpp"
namespace Star {
ServerRconThread::ServerRconThread(UniverseServer* universe, HostAddressWithPort const& address)
: Thread("RconServer"), m_universe(universe), m_rconServer(address), m_stop(true) {
if (Root::singleton().configuration()->get("rconServerPassword").toString().empty())
Logger::warn("rconServerPassword is not configured requests will NOT be processed");
}
ServerRconThread::~ServerRconThread() {
stop();
join();
}
void ServerRconThread::clearClients(bool all) {
auto it = makeSMutableMapIterator(m_clients);
while (it.hasNext()) {
auto const& pair = it.next();
auto client = pair.second;
if (all)
client->stop();
else if (!client->isRunning())
it.remove();
}
}
void ServerRconThread::start() {
m_stop = false;
Thread::start();
}
void ServerRconThread::stop() {
m_stop = true;
m_rconServer.stop();
clearClients(true);
}
void ServerRconThread::run() {
try {
auto timeout = Root::singleton().configuration()->get("rconServerTimeout").toInt();
while (!m_stop) {
if (auto client = m_rconServer.accept(100)) {
client->setTimeout(timeout);
auto rconClient = make_shared<ServerRconClient>(m_universe, client);
rconClient->start();
m_clients[client->remoteAddress().address()] = rconClient;
clearClients();
}
}
} catch (std::exception const& e) {
Logger::error("ServerRconThread exception caught: %s", e.what());
}
}
}

View file

@ -0,0 +1,36 @@
#ifndef STAR_SERVER_RCON_THREAD_HPP
#define STAR_SERVER_RCON_THREAD_HPP
#include "StarThread.hpp"
#include "StarTcp.hpp"
#include "StarMap.hpp"
#include "StarServerRconClient.hpp"
namespace Star {
STAR_CLASS(UniverseServer);
STAR_CLASS(ServerRconThread);
class ServerRconThread : public Thread {
public:
ServerRconThread(UniverseServer* universe, HostAddressWithPort const& address);
~ServerRconThread();
void start();
void stop();
protected:
virtual void run();
private:
void clearClients(bool all = false);
UniverseServer* m_universe;
TcpServer m_rconServer;
bool m_stop;
HashMap<HostAddress, ServerRconClientPtr> m_clients;
};
}
#endif

94
source/server/main.cpp Normal file
View file

@ -0,0 +1,94 @@
#include "StarFile.hpp"
#include "StarRandom.hpp"
#include "StarLexicalCast.hpp"
#include "StarLogging.hpp"
#include "StarUniverseServer.hpp"
#include "StarRootLoader.hpp"
#include "StarConfiguration.hpp"
#include "StarVersion.hpp"
#include "StarServerQueryThread.hpp"
#include "StarServerRconThread.hpp"
#include "StarSignalHandler.hpp"
using namespace Star;
Json const AdditionalDefaultConfiguration = Json::parseJson(R"JSON(
{
"configurationVersion" : {
"server" : 4
},
"runQueryServer" : false,
"queryServerPort" : 21025,
"queryServerBind" : "::",
"runRconServer" : false,
"rconServerPort" : 21026,
"rconServerBind" : "::",
"rconServerPassword" : "",
"rconServerTimeout" : 1000,
"allowAssetsMismatch" : true,
"serverOverrideAssetsDigest" : null
}
)JSON");
int main(int argc, char** argv) {
try {
RootLoader rootLoader({{}, AdditionalDefaultConfiguration, String("starbound_server.log"), LogLevel::Info, false, String("starbound_server.config")});
RootUPtr root = rootLoader.commandInitOrDie(argc, argv).first;
root->fullyLoad();
SignalHandler signalHandler;
signalHandler.setHandleFatal(true);
signalHandler.setHandleInterrupt(true);
auto configuration = root->configuration();
{
Logger::info("Server Version %s (%s) Source ID: %s Protocol: %s", StarVersionString, StarArchitectureString, StarSourceIdentifierString, StarProtocolVersion);
UniverseServerUPtr server = make_unique<UniverseServer>(root->toStoragePath("universe"));
server->setListeningTcp(true);
server->start();
ServerQueryThreadUPtr queryServer;
if (configuration->get("runQueryServer").toBool()) {
queryServer = make_unique<ServerQueryThread>(server.get(), HostAddressWithPort(configuration->get("queryServerBind").toString(), configuration->get("queryServerPort").toInt()));
queryServer->start();
}
ServerRconThreadUPtr rconServer;
if (configuration->get("runRconServer").toBool()) {
rconServer = make_unique<ServerRconThread>(server.get(), HostAddressWithPort(configuration->get("rconServerBind").toString(), configuration->get("rconServerPort").toInt()));
rconServer->start();
}
while (server->isRunning()) {
if (signalHandler.interruptCaught()) {
Logger::info("Interrupt caught!");
server->stop();
break;
}
Thread::sleep(100);
}
server->join();
if (queryServer) {
queryServer->stop();
queryServer->join();
}
if (rconServer) {
rconServer->stop();
rconServer->join();
}
}
Logger::info("Server shutdown gracefully");
} catch (std::exception const& e) {
fatalException(e, true);
}
return 0;
}