support chat commands on proxy server
This commit is contained in:
+393
@@ -0,0 +1,393 @@
|
||||
#include "Channel.hh"
|
||||
|
||||
#include <event2/buffer.h>
|
||||
#include <event2/bufferevent.h>
|
||||
#include <event2/event.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <phosg/Network.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "Version.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
extern bool use_terminal_colors;
|
||||
|
||||
|
||||
|
||||
static void flush_and_free_bufferevent(struct bufferevent* bev) {
|
||||
bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_FINISHED);
|
||||
bufferevent_free(bev);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Channel::Channel(
|
||||
GameVersion version,
|
||||
on_command_received_t on_command_received,
|
||||
on_error_t on_error,
|
||||
void* context_obj,
|
||||
const string& name,
|
||||
TerminalFormat terminal_send_color,
|
||||
TerminalFormat terminal_recv_color)
|
||||
: bev(nullptr, flush_and_free_bufferevent),
|
||||
version(version),
|
||||
name(name),
|
||||
terminal_send_color(terminal_send_color),
|
||||
terminal_recv_color(terminal_recv_color),
|
||||
on_command_received(on_command_received),
|
||||
on_error(on_error),
|
||||
context_obj(context_obj) {
|
||||
}
|
||||
|
||||
Channel::Channel(
|
||||
struct bufferevent* bev,
|
||||
GameVersion version,
|
||||
on_command_received_t on_command_received,
|
||||
on_error_t on_error,
|
||||
void* context_obj,
|
||||
const string& name,
|
||||
TerminalFormat terminal_send_color,
|
||||
TerminalFormat terminal_recv_color)
|
||||
: bev(nullptr, flush_and_free_bufferevent),
|
||||
version(version),
|
||||
name(name),
|
||||
terminal_send_color(terminal_send_color),
|
||||
terminal_recv_color(terminal_recv_color),
|
||||
on_command_received(on_command_received),
|
||||
on_error(on_error),
|
||||
context_obj(context_obj) {
|
||||
this->set_bufferevent(bev);
|
||||
}
|
||||
|
||||
void Channel::replace_with(
|
||||
Channel&& other,
|
||||
on_command_received_t on_command_received,
|
||||
on_error_t on_error,
|
||||
void* context_obj,
|
||||
const std::string& name) {
|
||||
this->set_bufferevent(other.bev.release());
|
||||
this->local_addr = other.local_addr;
|
||||
this->remote_addr = other.remote_addr;
|
||||
this->is_virtual_connection = other.is_virtual_connection;
|
||||
this->version = other.version;
|
||||
this->crypt_in = other.crypt_in;
|
||||
this->crypt_out = other.crypt_out;
|
||||
this->name = name;
|
||||
this->terminal_send_color = other.terminal_send_color;
|
||||
this->terminal_recv_color = other.terminal_recv_color;
|
||||
this->on_command_received = on_command_received;
|
||||
this->on_error = on_error;
|
||||
this->context_obj = context_obj;
|
||||
other.disconnect(); // Clears crypts, addrs, etc.
|
||||
}
|
||||
|
||||
void Channel::set_bufferevent(struct bufferevent* bev) {
|
||||
this->bev.reset(bev);
|
||||
|
||||
if (this->bev.get()) {
|
||||
int fd = bufferevent_getfd(this->bev.get());
|
||||
if (fd < 0) {
|
||||
this->is_virtual_connection = true;
|
||||
memset(&this->local_addr, 0, sizeof(this->local_addr));
|
||||
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
|
||||
} else {
|
||||
this->is_virtual_connection = false;
|
||||
get_socket_addresses(fd, &this->local_addr, &this->remote_addr);
|
||||
}
|
||||
|
||||
bufferevent_setcb(this->bev.get(),
|
||||
&Channel::dispatch_on_input, nullptr,
|
||||
&Channel::dispatch_on_error, this);
|
||||
bufferevent_enable(this->bev.get(), EV_READ | EV_WRITE);
|
||||
|
||||
} else {
|
||||
this->is_virtual_connection = false;
|
||||
memset(&this->local_addr, 0, sizeof(this->local_addr));
|
||||
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Channel::disconnect() {
|
||||
if (this->bev.get()) {
|
||||
// If the output buffer is not empty, move the bufferevent into the draining
|
||||
// pool instead of disconnecting it, to make sure all the data gets sent.
|
||||
struct evbuffer* out_buffer = bufferevent_get_output(this->bev.get());
|
||||
if (evbuffer_get_length(out_buffer) == 0) {
|
||||
this->bev.reset(); // Destructor flushes and frees the bufferevent
|
||||
} else {
|
||||
// The callbacks will free it when all the data is sent or the client
|
||||
// disconnects
|
||||
|
||||
auto on_output = +[](struct bufferevent* bev, void*) -> void {
|
||||
flush_and_free_bufferevent(bev);
|
||||
};
|
||||
|
||||
auto on_error = +[](struct bufferevent* bev, short events, void*) -> void {
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
log(WARNING, "Disconnecting channel caused error %d (%s)", err,
|
||||
evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
|
||||
bufferevent_flush(bev, EV_WRITE, BEV_FINISHED);
|
||||
bufferevent_free(bev);
|
||||
}
|
||||
};
|
||||
|
||||
struct bufferevent* bev = this->bev.release();
|
||||
bufferevent_setcb(bev, nullptr, on_output, on_error, bev);
|
||||
bufferevent_disable(bev, EV_READ);
|
||||
}
|
||||
}
|
||||
|
||||
memset(&this->local_addr, 0, sizeof(this->local_addr));
|
||||
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
|
||||
this->is_virtual_connection = false;
|
||||
this->crypt_in.reset();
|
||||
this->crypt_out.reset();
|
||||
}
|
||||
|
||||
|
||||
|
||||
Channel::Message Channel::recv(bool print_contents) {
|
||||
struct evbuffer* buf = bufferevent_get_input(this->bev.get());
|
||||
|
||||
size_t header_size = (this->version == GameVersion::BB) ? 8 : 4;
|
||||
PSOCommandHeader header;
|
||||
if (evbuffer_copyout(buf, &header, header_size)
|
||||
< static_cast<ssize_t>(header_size)) {
|
||||
throw out_of_range("no command available");
|
||||
}
|
||||
|
||||
if (this->crypt_in.get()) {
|
||||
this->crypt_in->decrypt(&header, header_size, false);
|
||||
}
|
||||
|
||||
size_t command_logical_size = header.size(version);
|
||||
|
||||
// If encryption is enabled, BB pads commands to 8-byte boundaries, and this
|
||||
// is not reflected in the size field. This logic does not occur if encryption
|
||||
// is not yet enabled.
|
||||
size_t command_physical_size = (this->crypt_in.get() && (version == GameVersion::BB))
|
||||
? ((command_logical_size + 7) & ~7) : command_logical_size;
|
||||
if (evbuffer_get_length(buf) < command_physical_size) {
|
||||
throw out_of_range("no command available");
|
||||
}
|
||||
|
||||
// If we get here, then there is a full command in the buffer. Some encryption
|
||||
// algorithms' advancement depends on the decrypted data, so we have to
|
||||
// actually decrypt the header again (with advance=true) to keep them in a
|
||||
// consistent state.
|
||||
|
||||
string header_data(header_size, '\0');
|
||||
if (evbuffer_remove(buf, header_data.data(), header_data.size())
|
||||
< static_cast<ssize_t>(header_data.size())) {
|
||||
throw logic_error("enough bytes available, but could not remove them");
|
||||
}
|
||||
if (this->crypt_in.get()) {
|
||||
this->crypt_in->decrypt(header_data.data(), header_data.size());
|
||||
}
|
||||
|
||||
string command_data(command_physical_size - header_size, '\0');
|
||||
if (evbuffer_remove(buf, command_data.data(), command_data.size())
|
||||
< static_cast<ssize_t>(command_data.size())) {
|
||||
throw logic_error("enough bytes available, but could not remove them");
|
||||
}
|
||||
if (this->crypt_in.get()) {
|
||||
this->crypt_in->decrypt(command_data.data(), command_data.size());
|
||||
}
|
||||
command_data.resize(command_logical_size - header_size);
|
||||
|
||||
if (print_contents && (this->terminal_recv_color != TerminalFormat::END)) {
|
||||
if (use_terminal_colors && this->terminal_recv_color != TerminalFormat::NORMAL) {
|
||||
print_color_escape(stderr, this->terminal_recv_color, TerminalFormat::BOLD, TerminalFormat::END);
|
||||
}
|
||||
|
||||
string name_token;
|
||||
if (!this->name.empty()) {
|
||||
name_token = " from " + this->name;
|
||||
}
|
||||
log(INFO, "Received%s (version=%s command=%04hX flag=%08X)",
|
||||
name_token.c_str(),
|
||||
name_for_version(this->version),
|
||||
header.command(this->version),
|
||||
header.flag(this->version));
|
||||
|
||||
vector<struct iovec> iovs;
|
||||
iovs.emplace_back(iovec{.iov_base = header_data.data(), .iov_len = header_data.size()});
|
||||
iovs.emplace_back(iovec{.iov_base = command_data.data(), .iov_len = command_data.size()});
|
||||
print_data(stderr, iovs, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR);
|
||||
|
||||
if (use_terminal_colors && this->terminal_recv_color != TerminalFormat::NORMAL) {
|
||||
print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
.command = header.command(this->version),
|
||||
.flag = header.flag(this->version),
|
||||
.data = move(command_data),
|
||||
};
|
||||
}
|
||||
|
||||
void Channel::send(uint16_t cmd, uint32_t flag, const void* data, size_t size,
|
||||
bool print_contents) {
|
||||
if (!this->connected()) {
|
||||
log(WARNING, "Attempted to send command on closed channel; dropping data");
|
||||
}
|
||||
|
||||
string send_data;
|
||||
size_t logical_size;
|
||||
size_t send_data_size = 0;
|
||||
switch (this->version) {
|
||||
case GameVersion::GC:
|
||||
case GameVersion::DC: {
|
||||
PSOCommandHeaderDCGC header;
|
||||
if (this->crypt_out.get()) {
|
||||
send_data_size = (sizeof(header) + size + 3) & ~3;
|
||||
} else {
|
||||
send_data_size = (sizeof(header) + size);
|
||||
}
|
||||
logical_size = send_data_size;
|
||||
header.command = cmd;
|
||||
header.flag = flag;
|
||||
header.size = send_data_size;
|
||||
send_data.append(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
break;
|
||||
}
|
||||
|
||||
case GameVersion::PC:
|
||||
case GameVersion::PATCH: {
|
||||
PSOCommandHeaderPC header;
|
||||
if (this->crypt_out.get()) {
|
||||
send_data_size = (sizeof(header) + size + 3) & ~3;
|
||||
} else {
|
||||
send_data_size = (sizeof(header) + size);
|
||||
}
|
||||
logical_size = send_data_size;
|
||||
header.size = send_data_size;
|
||||
header.command = cmd;
|
||||
header.flag = flag;
|
||||
send_data.append(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
break;
|
||||
}
|
||||
|
||||
case GameVersion::BB: {
|
||||
// BB has an annoying behavior here: command lengths must be multiples of
|
||||
// 4, but the actual data length must be a multiple of 8. If the size
|
||||
// field is not divisible by 8, 4 extra bytes are sent anyway. This
|
||||
// behavior only applies when encryption is enabled - any commands sent
|
||||
// before encryption is enabled have no size restrictions (except they
|
||||
// must include a full header and must fit in the client's receive
|
||||
// buffer), and no implicit extra bytes are sent.
|
||||
PSOCommandHeaderBB header;
|
||||
if (this->crypt_out.get()) {
|
||||
send_data_size = (sizeof(header) + size + 7) & ~7;
|
||||
} else {
|
||||
send_data_size = (sizeof(header) + size);
|
||||
}
|
||||
logical_size = (sizeof(header) + size + 3) & ~3;
|
||||
header.size = logical_size;
|
||||
header.command = cmd;
|
||||
header.flag = flag;
|
||||
send_data.append(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw logic_error("unimplemented game version in send_command");
|
||||
}
|
||||
|
||||
// All versions of PSO I've seen (PC, GC, BB) have a receive buffer 0x7C00
|
||||
// bytes in size
|
||||
if (send_data_size > 0x7C00) {
|
||||
throw runtime_error("outbound command too large");
|
||||
}
|
||||
|
||||
if (send_data.size() < send_data_size) {
|
||||
send_data.append(reinterpret_cast<const char*>(data), size);
|
||||
send_data.resize(send_data_size, '\0');
|
||||
}
|
||||
|
||||
if (print_contents && (this->terminal_send_color != TerminalFormat::END)) {
|
||||
string name_token;
|
||||
if (!this->name.empty()) {
|
||||
name_token = " to " + this->name;
|
||||
}
|
||||
if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) {
|
||||
print_color_escape(stderr, TerminalFormat::FG_YELLOW, TerminalFormat::BOLD, TerminalFormat::END);
|
||||
}
|
||||
log(INFO, "Sending%s (version=%s command=%04hX flag=%08X)",
|
||||
name_token.c_str(), name_for_version(version), cmd, flag);
|
||||
print_data(stderr, send_data.data(), logical_size, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR);
|
||||
if (use_terminal_colors && this->terminal_send_color != TerminalFormat::NORMAL) {
|
||||
print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->crypt_out.get()) {
|
||||
this->crypt_out->encrypt(send_data.data(), send_data.size());
|
||||
}
|
||||
|
||||
struct evbuffer* buf = bufferevent_get_output(this->bev.get());
|
||||
evbuffer_add(buf, send_data.data(), send_data.size());
|
||||
}
|
||||
|
||||
void Channel::send(uint16_t cmd, uint32_t flag, const string& data, bool print_contents) {
|
||||
this->send(cmd, flag, data.data(), data.size(), print_contents);
|
||||
}
|
||||
|
||||
void Channel::send(const void* data, size_t size, bool print_contents) {
|
||||
size_t header_size = (this->version == GameVersion::BB) ? 8 : 4;
|
||||
const auto* header = reinterpret_cast<const PSOCommandHeader*>(data);
|
||||
this->send(
|
||||
header->command(this->version),
|
||||
header->flag(this->version),
|
||||
reinterpret_cast<const uint8_t*>(data) + header_size,
|
||||
size - header_size,
|
||||
print_contents);
|
||||
}
|
||||
|
||||
void Channel::send(const string& data, bool print_contents) {
|
||||
return this->send(data.data(), data.size(), print_contents);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void Channel::dispatch_on_input(struct bufferevent*, void* ctx) {
|
||||
Channel* ch = reinterpret_cast<Channel*>(ctx);
|
||||
// The client can be disconnected during on_command_received, so we have to
|
||||
// make sure ch->bev is valid every time before calling recv()
|
||||
while (ch->bev.get()) {
|
||||
Message msg;
|
||||
try {
|
||||
msg = ch->recv();
|
||||
} catch (const out_of_range&) {
|
||||
break;
|
||||
} catch (const exception& e) {
|
||||
log(WARNING, "Error receiving on channel: %s", e.what());
|
||||
ch->on_error(*ch, BEV_EVENT_ERROR);
|
||||
break;
|
||||
}
|
||||
if (ch->on_command_received) {
|
||||
ch->on_command_received(*ch, msg.command, msg.flag, msg.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Channel::dispatch_on_error(struct bufferevent*, short events, void* ctx) {
|
||||
Channel* ch = reinterpret_cast<Channel*>(ctx);
|
||||
if (ch->on_error) {
|
||||
ch->on_error(*ch, events);
|
||||
} else {
|
||||
ch->disconnect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "PSOEncryption.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "Version.hh"
|
||||
|
||||
|
||||
|
||||
struct Channel {
|
||||
std::unique_ptr<struct bufferevent, void (*)(struct bufferevent*)> bev;
|
||||
struct sockaddr_storage local_addr;
|
||||
struct sockaddr_storage remote_addr;
|
||||
bool is_virtual_connection;
|
||||
|
||||
GameVersion version;
|
||||
std::shared_ptr<PSOEncryption> crypt_in;
|
||||
std::shared_ptr<PSOEncryption> crypt_out;
|
||||
|
||||
std::string name;
|
||||
TerminalFormat terminal_send_color;
|
||||
TerminalFormat terminal_recv_color;
|
||||
|
||||
struct Message {
|
||||
uint16_t command;
|
||||
uint32_t flag;
|
||||
std::string data;
|
||||
};
|
||||
|
||||
typedef void (*on_command_received_t)(Channel&, uint16_t, uint32_t, std::string&);
|
||||
typedef void (*on_error_t)(Channel&, short);
|
||||
|
||||
on_command_received_t on_command_received;
|
||||
on_error_t on_error;
|
||||
void* context_obj;
|
||||
|
||||
Channel(
|
||||
GameVersion version,
|
||||
on_command_received_t on_command_received,
|
||||
on_error_t on_error,
|
||||
void* context_obj,
|
||||
const std::string& name = "",
|
||||
TerminalFormat terminal_send_color = TerminalFormat::END,
|
||||
TerminalFormat terminal_recv_color = TerminalFormat::END);
|
||||
Channel(
|
||||
struct bufferevent* bev,
|
||||
GameVersion version,
|
||||
on_command_received_t on_command_received,
|
||||
on_error_t on_error,
|
||||
void* context_obj,
|
||||
const std::string& name = "",
|
||||
TerminalFormat terminal_send_color = TerminalFormat::END,
|
||||
TerminalFormat terminal_recv_color = TerminalFormat::END);
|
||||
Channel(const Channel& other) = delete;
|
||||
Channel(Channel&& other) = delete;
|
||||
Channel& operator=(const Channel& other) = delete;
|
||||
Channel& operator=(Channel&& other) = delete;
|
||||
|
||||
void replace_with(
|
||||
Channel&& other,
|
||||
on_command_received_t on_command_received,
|
||||
on_error_t on_error,
|
||||
void* context_obj,
|
||||
const std::string& name = "");
|
||||
|
||||
void set_bufferevent(struct bufferevent* bev);
|
||||
|
||||
inline bool connected() const {
|
||||
return this->bev.get() != nullptr;
|
||||
}
|
||||
void disconnect();
|
||||
|
||||
// Receives a message. Throws std::out_of_range if no messages are available.
|
||||
Message recv(bool print_contents = true);
|
||||
|
||||
// Sends a message with an automatically-constructed header.
|
||||
void send(uint16_t cmd, uint32_t flag = 0, const void* data = nullptr, size_t size = 0, bool print_contents = true);
|
||||
void send(uint16_t cmd, uint32_t flag, const std::string& data, bool print_contents = true);
|
||||
|
||||
// Sends a message with a pre-existing header (as the first few bytes in the
|
||||
// data)
|
||||
void send(const void* data = nullptr, size_t size = 0, bool print_contents = true);
|
||||
void send(const std::string& data, bool print_contents = true);
|
||||
|
||||
private:
|
||||
static void dispatch_on_input(struct bufferevent*, void* ctx);
|
||||
static void dispatch_on_error(struct bufferevent*, short events, void* ctx);
|
||||
};
|
||||
+229
-78
@@ -9,6 +9,7 @@
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "Server.hh"
|
||||
#include "ProxyServer.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "Client.hh"
|
||||
#include "SendCommands.hh"
|
||||
@@ -91,7 +92,7 @@ static void check_is_leader(shared_ptr<Lobby> l, shared_ptr<Client> c) {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Message commands
|
||||
|
||||
static void command_lobby_info(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_lobby_info(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string&) {
|
||||
// no preconditions - everyone can use this command
|
||||
|
||||
@@ -120,20 +121,61 @@ static void command_lobby_info(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
}
|
||||
}
|
||||
|
||||
static void command_ax(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
static void proxy_command_lobby_info(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, const std::u16string&) {
|
||||
vector<const char*> cheats_tokens;
|
||||
if (session.switch_assist) {
|
||||
cheats_tokens.emplace_back("SWA");
|
||||
}
|
||||
if (session.infinite_hp) {
|
||||
cheats_tokens.emplace_back("HP");
|
||||
}
|
||||
if (session.infinite_tp) {
|
||||
cheats_tokens.emplace_back("TP");
|
||||
}
|
||||
string cheats_str = cheats_tokens.empty() ? "none" : join(cheats_tokens, ",");
|
||||
|
||||
vector<const char*> behaviors_tokens;
|
||||
if (session.save_files) {
|
||||
behaviors_tokens.emplace_back("SF");
|
||||
}
|
||||
if (session.function_call_return_value >= 0) {
|
||||
behaviors_tokens.emplace_back("BFC");
|
||||
}
|
||||
string behaviors_str = behaviors_tokens.empty() ? "none" : join(behaviors_tokens, ",");
|
||||
|
||||
string section_id_override = "none";
|
||||
if (session.override_section_id >= 0) {
|
||||
section_id_override = name_for_section_id(session.override_section_id);
|
||||
}
|
||||
|
||||
send_text_message_printf(session.client_channel,
|
||||
"$C7GC: $C6%" PRIu32 "\n"
|
||||
"$C7Client ID: $C6%zu\n"
|
||||
"$C7Cheats: $C6%s\n"
|
||||
"$C7Flags: $C6%s\n"
|
||||
"$C7SecID override: $C6%s\n",
|
||||
session.remote_guild_card_number,
|
||||
session.lobby_client_id,
|
||||
cheats_str.c_str(),
|
||||
behaviors_str.c_str(),
|
||||
section_id_override.c_str());
|
||||
}
|
||||
|
||||
static void server_command_ax(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_privileges(c, Privilege::ANNOUNCE);
|
||||
string message = encode_sjis(args);
|
||||
log(INFO, "[Client message from %010u] %s\n", c->license->serial_number, message.c_str());
|
||||
}
|
||||
|
||||
static void command_announce(shared_ptr<ServerState> s, shared_ptr<Lobby>,
|
||||
static void server_command_announce(shared_ptr<ServerState> s, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_privileges(c, Privilege::ANNOUNCE);
|
||||
send_text_message(s, args);
|
||||
}
|
||||
|
||||
static void command_arrow(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_arrow(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
// no preconditions
|
||||
c->lobby_arrow_color = stoull(encode_sjis(args), nullptr, 0);
|
||||
@@ -142,7 +184,12 @@ static void command_arrow(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
}
|
||||
}
|
||||
|
||||
static void command_dbgid(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
static void proxy_command_arrow(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, const std::u16string& args) {
|
||||
session.server_channel.send(0x89, stoull(encode_sjis(args), nullptr, 0));
|
||||
}
|
||||
|
||||
static void server_command_dbgid(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const std::u16string&) {
|
||||
c->prefer_high_lobby_client_id = !c->prefer_high_lobby_client_id;
|
||||
}
|
||||
@@ -150,7 +197,7 @@ static void command_dbgid(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Lobby commands
|
||||
|
||||
static void command_cheat(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_cheat(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string&) {
|
||||
check_is_game(l, true);
|
||||
check_is_leader(l, c);
|
||||
@@ -174,7 +221,7 @@ static void command_cheat(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
}
|
||||
}
|
||||
|
||||
static void command_lobby_event(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_lobby_event(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_game(l, false);
|
||||
check_privileges(c, Privilege::CHANGE_EVENT);
|
||||
@@ -189,7 +236,24 @@ static void command_lobby_event(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
send_change_event(l, l->event);
|
||||
}
|
||||
|
||||
static void command_lobby_event_all(shared_ptr<ServerState> s, shared_ptr<Lobby>,
|
||||
static void proxy_command_lobby_event(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, const std::u16string& args) {
|
||||
if (args.empty()) {
|
||||
session.override_lobby_event = -1;
|
||||
} else {
|
||||
uint8_t new_event = event_for_name(args);
|
||||
if (new_event == 0xFF) {
|
||||
send_text_message(session.client_channel, u"$C6No such lobby event.");
|
||||
} else {
|
||||
session.override_lobby_event = new_event;
|
||||
if (session.version == GameVersion::GC || session.version == GameVersion::BB) {
|
||||
session.client_channel.send(0xDA, session.override_lobby_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_lobby_event_all(shared_ptr<ServerState> s, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_privileges(c, Privilege::CHANGE_EVENT);
|
||||
|
||||
@@ -209,7 +273,7 @@ static void command_lobby_event_all(shared_ptr<ServerState> s, shared_ptr<Lobby>
|
||||
}
|
||||
}
|
||||
|
||||
static void command_lobby_type(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_lobby_type(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_game(l, false);
|
||||
check_privileges(c, Privilege::CHANGE_EVENT);
|
||||
@@ -235,20 +299,41 @@ static void command_lobby_type(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Game commands
|
||||
|
||||
static void command_secid(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_secid(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_game(l, false);
|
||||
|
||||
if (!args[0]) {
|
||||
c->override_section_id = -1;
|
||||
send_text_message(l, u"$C6Override section ID\nremoved");
|
||||
send_text_message(c, u"$C6Override section ID\nremoved");
|
||||
} else {
|
||||
c->override_section_id = section_id_for_name(args);
|
||||
send_text_message(l, u"$C6Override section ID\nset");
|
||||
uint8_t new_secid = section_id_for_name(args);
|
||||
if (new_secid == 0xFF) {
|
||||
send_text_message(c, u"$C6Invalid section ID");
|
||||
} else {
|
||||
c->override_section_id = new_secid;
|
||||
send_text_message(c, u"$C6Override section ID\nset");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void command_password(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void proxy_command_secid(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, const std::u16string& args) {
|
||||
if (!args[0]) {
|
||||
session.override_section_id = -1;
|
||||
send_text_message(session.client_channel, u"$C6Override section ID\nremoved");
|
||||
} else {
|
||||
uint8_t new_secid = section_id_for_name(args);
|
||||
if (new_secid == 0xFF) {
|
||||
send_text_message(session.client_channel, u"$C6Invalid section ID");
|
||||
} else {
|
||||
session.override_section_id = new_secid;
|
||||
send_text_message(session.client_channel, u"$C6Override section ID\nset");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void server_command_password(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_game(l, true);
|
||||
check_is_leader(l, c);
|
||||
@@ -265,7 +350,7 @@ static void command_password(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
}
|
||||
}
|
||||
|
||||
static void command_min_level(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_min_level(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_game(l, true);
|
||||
check_is_leader(l, c);
|
||||
@@ -276,7 +361,7 @@ static void command_min_level(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
l->min_level + 1);
|
||||
}
|
||||
|
||||
static void command_max_level(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_max_level(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_game(l, true);
|
||||
check_is_leader(l, c);
|
||||
@@ -296,7 +381,7 @@ static void command_max_level(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Character commands
|
||||
|
||||
static void command_edit(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
static void server_command_edit(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_game(l, false);
|
||||
check_version(c, GameVersion::BB);
|
||||
@@ -383,14 +468,15 @@ static void command_edit(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
|
||||
// TODO: implement this
|
||||
// TODO: make sure the bank name is filesystem-safe
|
||||
/* static void command_change_bank(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
/* static void server_command_change_bank(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const std::u16string&) {
|
||||
check_version(c, GameVersion::BB);
|
||||
|
||||
TODO
|
||||
} */
|
||||
|
||||
static void command_convert_char_to_bb(shared_ptr<ServerState> s,
|
||||
// TODO: This can be implemented on the proxy server too.
|
||||
static void server_command_convert_char_to_bb(shared_ptr<ServerState> s,
|
||||
shared_ptr<Lobby> l, shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_game(l, false);
|
||||
check_not_version(c, GameVersion::BB);
|
||||
@@ -438,7 +524,7 @@ static string name_for_client(shared_ptr<Client> c) {
|
||||
return "Player";
|
||||
}
|
||||
|
||||
static void command_silence(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
static void server_command_silence(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_privileges(c, Privilege::SILENCE_USER);
|
||||
|
||||
@@ -460,7 +546,7 @@ static void command_silence(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
target->can_chat ? "un" : "");
|
||||
}
|
||||
|
||||
static void command_kick(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
static void server_command_kick(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_privileges(c, Privilege::KICK_USER);
|
||||
|
||||
@@ -482,7 +568,7 @@ static void command_kick(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
send_text_message_printf(l, "$C6%s kicked off", target_name.c_str());
|
||||
}
|
||||
|
||||
static void command_ban(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
static void server_command_ban(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_privileges(c, Privilege::BAN_USER);
|
||||
|
||||
@@ -536,7 +622,7 @@ static void command_ban(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Cheat commands
|
||||
|
||||
static void command_warp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_warp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l);
|
||||
@@ -565,7 +651,13 @@ static void command_warp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
send_warp(c, area);
|
||||
}
|
||||
|
||||
static void command_next(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void proxy_command_warp(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, const std::u16string& args) {
|
||||
uint32_t area = stoul(encode_sjis(args), nullptr, 0);
|
||||
send_warp(session.client_channel, session.lobby_client_id, area);
|
||||
}
|
||||
|
||||
static void server_command_next(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string&) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l);
|
||||
@@ -584,7 +676,7 @@ static void command_next(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
send_warp(c, new_area);
|
||||
}
|
||||
|
||||
static void command_what(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_what(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string&) {
|
||||
check_is_game(l, true);
|
||||
if (!l->episode || (l->episode > 3)) {
|
||||
@@ -618,7 +710,7 @@ static void command_what(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
}
|
||||
}
|
||||
|
||||
static void command_song(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
static void server_command_song(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_ep3(c, true);
|
||||
|
||||
@@ -626,7 +718,7 @@ static void command_song(shared_ptr<ServerState>, shared_ptr<Lobby>,
|
||||
send_ep3_change_music(c, song);
|
||||
}
|
||||
|
||||
static void command_infinite_hp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void server_command_infinite_hp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string&) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l);
|
||||
@@ -635,7 +727,14 @@ static void command_infinite_hp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
send_text_message_printf(c, "$C6Infinite HP %s", c->infinite_hp ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
static void command_infinite_tp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void proxy_command_infinite_hp(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, const std::u16string&) {
|
||||
session.infinite_hp = !session.infinite_hp;
|
||||
send_text_message_printf(session.client_channel, "$C6Infinite HP %s",
|
||||
session.infinite_hp ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
static void server_command_infinite_tp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string&) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l);
|
||||
@@ -644,7 +743,14 @@ static void command_infinite_tp(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
send_text_message_printf(c, "$C6Infinite TP %s", c->infinite_tp ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
static void command_switch_assist(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void proxy_command_infinite_tp(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, const std::u16string&) {
|
||||
session.infinite_tp = !session.infinite_tp;
|
||||
send_text_message_printf(session.client_channel, "$C6Infinite TP %s",
|
||||
session.infinite_tp ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
static void server_command_switch_assist(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string&) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l);
|
||||
@@ -653,7 +759,13 @@ static void command_switch_assist(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
send_text_message_printf(c, "$C6Switch assist %s", c->switch_assist ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
static void command_item(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
static void proxy_command_switch_assist(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, const std::u16string&) {
|
||||
session.switch_assist = !session.switch_assist;
|
||||
send_text_message_printf(session.client_channel, "$C6Switch assist %s", session.switch_assist ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
static void server_command_item(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args) {
|
||||
check_is_game(l, true);
|
||||
check_cheats_enabled(l);
|
||||
@@ -683,74 +795,113 @@ static void command_item(shared_ptr<ServerState>, shared_ptr<Lobby> l,
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
typedef void (*handler_t)(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
typedef void (*server_handler_t)(shared_ptr<ServerState> s, shared_ptr<Lobby> l,
|
||||
shared_ptr<Client> c, const std::u16string& args);
|
||||
typedef void (*proxy_handler_t)(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, const std::u16string& args);
|
||||
struct ChatCommandDefinition {
|
||||
handler_t handler;
|
||||
server_handler_t server_handler;
|
||||
proxy_handler_t proxy_handler;
|
||||
u16string usage;
|
||||
};
|
||||
|
||||
static const unordered_map<u16string, ChatCommandDefinition> chat_commands({
|
||||
// TODO: implement command_help and actually use the usage strings here
|
||||
{u"$allevent" , {command_lobby_event_all , u"Usage:\nallevent <name/ID>"}},
|
||||
{u"$ann" , {command_announce , u"Usage:\nann <message>"}},
|
||||
{u"$arrow" , {command_arrow , u"Usage:\narrow <color>"}},
|
||||
{u"$ax" , {command_ax , u"Usage:\nax <message>"}},
|
||||
{u"$ban" , {command_ban , u"Usage:\nban <name-or-number>"}},
|
||||
{u"$bbchar" , {command_convert_char_to_bb, u"Usage:\nbbchar <user> <pass> <1-4>"}},
|
||||
// {u"$bank", {command_bank , u"Usage:\nbank <bank name>"}},
|
||||
{u"$cheat" , {command_cheat , u"Usage:\ncheat"}},
|
||||
{u"$dbgid" , {command_dbgid , u"Usage:\ndngid"}},
|
||||
{u"$edit" , {command_edit , u"Usage:\nedit <stat> <value>"}},
|
||||
{u"$event" , {command_lobby_event , u"Usage:\nevent <name>"}},
|
||||
{u"$infhp" , {command_infinite_hp , u"Usage:\ninfhp"}},
|
||||
{u"$inftp" , {command_infinite_tp , u"Usage:\ninftp"}},
|
||||
{u"$item" , {command_item , u"Usage:\nitem <item-code>"}},
|
||||
{u"$kick" , {command_kick , u"Usage:\nkick <name-or-number>"}},
|
||||
{u"$li" , {command_lobby_info , u"Usage:\nli"}},
|
||||
{u"$maxlevel" , {command_max_level , u"Usage:\nmax_level <level>"}},
|
||||
{u"$minlevel" , {command_min_level , u"Usage:\nmin_level <level>"}},
|
||||
{u"$next" , {command_next , u"Usage:\nnext"}},
|
||||
{u"$password" , {command_password , u"Usage:\nlock [password]\nomit password to\nunlock game"}},
|
||||
{u"$secid" , {command_secid , u"Usage:\nsecid [section ID]\nomit section ID to\nrevert to normal"}},
|
||||
{u"$silence" , {command_silence , u"Usage:\nsilence <name-or-number>"}},
|
||||
{u"$song" , {command_song , u"Usage:\nsong <song-number>"}},
|
||||
{u"$swa" , {command_switch_assist , u"Usage:\nswa"}},
|
||||
{u"$type" , {command_lobby_type , u"Usage:\ntype <name>"}},
|
||||
{u"$warp" , {command_warp , u"Usage:\nwarp <area-number>"}},
|
||||
{u"$what" , {command_what , u"Usage:\nwhat"}},
|
||||
{u"$allevent" , {server_command_lobby_event_all , nullptr , u"Usage:\nallevent <name/ID>"}},
|
||||
{u"$ann" , {server_command_announce , nullptr , u"Usage:\nann <message>"}},
|
||||
{u"$arrow" , {server_command_arrow , proxy_command_arrow , u"Usage:\narrow <color>"}},
|
||||
{u"$ax" , {server_command_ax , nullptr , u"Usage:\nax <message>"}},
|
||||
{u"$ban" , {server_command_ban , nullptr , u"Usage:\nban <name-or-number>"}},
|
||||
// TODO: implement this on proxy server
|
||||
{u"$bbchar" , {server_command_convert_char_to_bb, nullptr , u"Usage:\nbbchar <user> <pass> <1-4>"}},
|
||||
{u"$cheat" , {server_command_cheat , nullptr , u"Usage:\ncheat"}},
|
||||
{u"$dbgid" , {server_command_dbgid , nullptr , u"Usage:\ndbgid"}},
|
||||
{u"$edit" , {server_command_edit , nullptr , u"Usage:\nedit <stat> <value>"}},
|
||||
{u"$event" , {server_command_lobby_event , proxy_command_lobby_event , u"Usage:\nevent <name>"}},
|
||||
{u"$infhp" , {server_command_infinite_hp , proxy_command_infinite_hp , u"Usage:\ninfhp"}},
|
||||
{u"$inftp" , {server_command_infinite_tp , proxy_command_infinite_tp , u"Usage:\ninftp"}},
|
||||
{u"$item" , {server_command_item , nullptr , u"Usage:\nitem <item-code>"}},
|
||||
{u"$kick" , {server_command_kick , nullptr , u"Usage:\nkick <name-or-number>"}},
|
||||
{u"$li" , {server_command_lobby_info , proxy_command_lobby_info , u"Usage:\nli"}},
|
||||
{u"$maxlevel" , {server_command_max_level , nullptr , u"Usage:\nmax_level <level>"}},
|
||||
{u"$minlevel" , {server_command_min_level , nullptr , u"Usage:\nmin_level <level>"}},
|
||||
// TODO: implement this on proxy server
|
||||
{u"$next" , {server_command_next , nullptr , u"Usage:\nnext"}},
|
||||
{u"$password" , {server_command_password , nullptr , u"Usage:\nlock [password]\nomit password to\nunlock game"}},
|
||||
{u"$secid" , {server_command_secid , proxy_command_secid , u"Usage:\nsecid [section ID]\nomit section ID to\nrevert to normal"}},
|
||||
{u"$silence" , {server_command_silence , nullptr , u"Usage:\nsilence <name-or-number>"}},
|
||||
// TODO: implement this on proxy server
|
||||
{u"$song" , {server_command_song , nullptr , u"Usage:\nsong <song-number>"}},
|
||||
{u"$swa" , {server_command_switch_assist , proxy_command_switch_assist, u"Usage:\nswa"}},
|
||||
{u"$type" , {server_command_lobby_type , nullptr , u"Usage:\ntype <name>"}},
|
||||
{u"$warp" , {server_command_warp , proxy_command_warp , u"Usage:\nwarp <area-number>"}},
|
||||
{u"$what" , {server_command_what , nullptr , u"Usage:\nwhat"}},
|
||||
});
|
||||
|
||||
struct SplitCommand {
|
||||
u16string name;
|
||||
u16string args;
|
||||
|
||||
SplitCommand(const u16string& text) {
|
||||
size_t space_pos = text.find(u' ');
|
||||
if (space_pos != string::npos) {
|
||||
this->name = text.substr(0, space_pos);
|
||||
this->args = text.substr(space_pos + 1);
|
||||
} else {
|
||||
this->name = text;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// This function is called every time any player sends a chat beginning with a
|
||||
// dollar sign. It is this function's responsibility to see if the chat is a
|
||||
// command, and to execute the command and block the chat if it is.
|
||||
void process_chat_command(std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l,
|
||||
std::shared_ptr<Client> c, const std::u16string& text) {
|
||||
|
||||
u16string command_name;
|
||||
u16string text_str(text);
|
||||
size_t space_pos = text_str.find(u' ');
|
||||
if (space_pos != string::npos) {
|
||||
command_name = text_str.substr(0, space_pos);
|
||||
text_str = text_str.substr(space_pos + 1);
|
||||
} else {
|
||||
command_name = text_str;
|
||||
text_str.clear();
|
||||
}
|
||||
SplitCommand cmd(text);
|
||||
|
||||
const ChatCommandDefinition* def = nullptr;
|
||||
try {
|
||||
def = &chat_commands.at(command_name);
|
||||
def = &chat_commands.at(cmd.name);
|
||||
} catch (const out_of_range&) {
|
||||
send_text_message(c, u"$C6Unknown command.");
|
||||
send_text_message(c, u"$C6Unknown command");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
def->handler(s, l, c, text_str.c_str());
|
||||
} catch (const precondition_failed& e) {
|
||||
send_text_message(c, e.what());
|
||||
} catch (const exception& e) {
|
||||
send_text_message_printf(c, "$C6Failed:\n%s", e.what());
|
||||
if (!def->server_handler) {
|
||||
send_text_message(c, u"$C6Command not available\non game server");
|
||||
} else {
|
||||
try {
|
||||
def->server_handler(s, l, c, cmd.args);
|
||||
} catch (const precondition_failed& e) {
|
||||
send_text_message(c, e.what());
|
||||
} catch (const exception& e) {
|
||||
send_text_message_printf(c, "$C6Failed:\n%s", e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void process_chat_command(std::shared_ptr<ServerState> s,
|
||||
ProxyServer::LinkedSession& session, const std::u16string& text) {
|
||||
SplitCommand cmd(text);
|
||||
|
||||
const ChatCommandDefinition* def = nullptr;
|
||||
try {
|
||||
def = &chat_commands.at(cmd.name);
|
||||
} catch (const out_of_range&) {
|
||||
send_text_message(session.client_channel, u"$C6Unknown command");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!def->proxy_handler) {
|
||||
send_text_message(session.client_channel, u"$C6Command not available\non proxy server");
|
||||
} else {
|
||||
try {
|
||||
def->proxy_handler(s, session, cmd.args);
|
||||
} catch (const precondition_failed& e) {
|
||||
send_text_message(session.client_channel, e.what());
|
||||
} catch (const exception& e) {
|
||||
send_text_message_printf(session.client_channel, "$C6Failed:\n%s", e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "ServerState.hh"
|
||||
#include "Lobby.hh"
|
||||
#include "Client.hh"
|
||||
#include "ProxyServer.hh"
|
||||
|
||||
void process_chat_command(std::shared_ptr<ServerState> s, std::shared_ptr<Lobby> l,
|
||||
std::shared_ptr<Client> c, const std::u16string& text);
|
||||
void process_chat_command(std::shared_ptr<ServerState> s,
|
||||
ProxyServer::LinkedSession& session, const std::u16string& text);
|
||||
|
||||
+2
-13
@@ -24,16 +24,14 @@ Client::Client(
|
||||
GameVersion version,
|
||||
ServerBehavior server_behavior)
|
||||
: version(version),
|
||||
bb_game_state(0),
|
||||
flags(flags_for_version(this->version, 0)),
|
||||
bev(bev),
|
||||
channel(bev, this->version, nullptr, nullptr, this, "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN),
|
||||
server_behavior(server_behavior),
|
||||
should_disconnect(false),
|
||||
should_send_to_lobby_server(false),
|
||||
proxy_destination_address(0),
|
||||
proxy_destination_port(0),
|
||||
play_time_begin(now()),
|
||||
last_recv_time(this->play_time_begin),
|
||||
last_send_time(0),
|
||||
x(0.0f),
|
||||
z(0.0f),
|
||||
area(0),
|
||||
@@ -50,15 +48,6 @@ Client::Client(
|
||||
pending_bb_save_player_index(0),
|
||||
dol_base_addr(0) {
|
||||
this->last_switch_enabled_command.subcommand = 0;
|
||||
int fd = bufferevent_getfd(this->bev);
|
||||
if (fd < 0) {
|
||||
this->is_virtual_connection = true;
|
||||
memset(&this->local_addr, 0, sizeof(this->local_addr));
|
||||
memset(&this->remote_addr, 0, sizeof(this->remote_addr));
|
||||
} else {
|
||||
this->is_virtual_connection = false;
|
||||
get_socket_addresses(fd, &this->local_addr, &this->remote_addr);
|
||||
}
|
||||
memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr));
|
||||
}
|
||||
|
||||
|
||||
+3
-15
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "Channel.hh"
|
||||
#include "CommandFormats.hh"
|
||||
#include "FunctionCompiler.hh"
|
||||
#include "License.hh"
|
||||
@@ -66,27 +67,15 @@ struct Client {
|
||||
uint8_t bb_game_state;
|
||||
uint16_t flags;
|
||||
|
||||
// Encryption
|
||||
std::shared_ptr<PSOEncryption> crypt_in;
|
||||
std::shared_ptr<PSOEncryption> crypt_out;
|
||||
|
||||
// Network
|
||||
struct sockaddr_storage local_addr;
|
||||
struct sockaddr_storage remote_addr;
|
||||
struct bufferevent* bev;
|
||||
Channel channel;
|
||||
struct sockaddr_storage next_connection_addr;
|
||||
ServerBehavior server_behavior;
|
||||
bool is_virtual_connection;
|
||||
bool should_disconnect;
|
||||
bool should_send_to_lobby_server;
|
||||
uint32_t proxy_destination_address;
|
||||
uint16_t proxy_destination_port;
|
||||
|
||||
// Timing & menus
|
||||
uint64_t play_time_begin; // time of connection (used for incrementing play time on BB)
|
||||
uint64_t last_recv_time; // time of last data received
|
||||
uint64_t last_send_time; // time of last data sent
|
||||
|
||||
// Lobby/positioning
|
||||
float x;
|
||||
float z;
|
||||
@@ -112,8 +101,7 @@ struct Client {
|
||||
uint32_t dol_base_addr;
|
||||
std::shared_ptr<DOLFileIndex::DOLFile> loading_dol_file;
|
||||
|
||||
Client(struct bufferevent* bev, GameVersion version,
|
||||
ServerBehavior server_behavior);
|
||||
Client(struct bufferevent* bev, GameVersion version, ServerBehavior server_behavior);
|
||||
|
||||
void set_license(std::shared_ptr<const License> l);
|
||||
|
||||
|
||||
@@ -131,99 +131,6 @@ void PSOCommandHeader::set_flag(GameVersion version, uint32_t flag) {
|
||||
|
||||
|
||||
|
||||
void for_each_received_command(
|
||||
struct bufferevent* bev,
|
||||
GameVersion version,
|
||||
PSOEncryption* crypt,
|
||||
function<void(uint16_t, uint16_t, string&)> fn) {
|
||||
struct evbuffer* buf = bufferevent_get_input(bev);
|
||||
|
||||
size_t header_size = (version == GameVersion::BB) ? 8 : 4;
|
||||
for (;;) {
|
||||
PSOCommandHeader header;
|
||||
if (evbuffer_copyout(buf, &header, header_size)
|
||||
< static_cast<ssize_t>(header_size)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (crypt) {
|
||||
crypt->decrypt(&header, header_size, false);
|
||||
}
|
||||
|
||||
size_t command_logical_size = header.size(version);
|
||||
|
||||
// If encryption is enabled, BB pads commands to 8-byte boundaries, and this
|
||||
// is not reflected in the size field. This logic does not occur if
|
||||
// encryption is not yet enabled.
|
||||
size_t command_physical_size = (crypt && (version == GameVersion::BB))
|
||||
? ((command_logical_size + header_size - 1) & ~(header_size - 1))
|
||||
: command_logical_size;
|
||||
if (evbuffer_get_length(buf) < command_physical_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If we get here, then there is a full command in the buffer. Some
|
||||
// encryption algorithms' advancement depends on the decrypted data, so we
|
||||
// have to actually decrypt the header again (with advance=true) to keep
|
||||
// them in a consistent state.
|
||||
|
||||
string header_data(header_size, '\0');
|
||||
if (evbuffer_remove(buf, header_data.data(), header_data.size())
|
||||
< static_cast<ssize_t>(header_data.size())) {
|
||||
throw logic_error("enough bytes available, but could not remove them");
|
||||
}
|
||||
|
||||
string command_data(command_physical_size - header_size, '\0');
|
||||
if (evbuffer_remove(buf, command_data.data(), command_data.size())
|
||||
< static_cast<ssize_t>(command_data.size())) {
|
||||
throw logic_error("enough bytes available, but could not remove them");
|
||||
}
|
||||
|
||||
if (crypt) {
|
||||
crypt->decrypt(header_data.data(), header_data.size());
|
||||
crypt->decrypt(command_data.data(), command_data.size());
|
||||
}
|
||||
command_data.resize(command_logical_size - header_size);
|
||||
|
||||
fn(header.command(version), header.flag(version), command_data);
|
||||
}
|
||||
}
|
||||
|
||||
void print_received_command(
|
||||
uint16_t command,
|
||||
uint32_t flag,
|
||||
const void* data,
|
||||
size_t size,
|
||||
GameVersion version,
|
||||
const char* name,
|
||||
TerminalFormat color) {
|
||||
if (use_terminal_colors) {
|
||||
print_color_escape(stderr, color, TerminalFormat::BOLD, TerminalFormat::END);
|
||||
}
|
||||
|
||||
string name_token;
|
||||
if (name && name[0]) {
|
||||
name_token = string(" from ") + name;
|
||||
}
|
||||
log(INFO, "Received%s (version=%s command=%04hX flag=%08X)",
|
||||
name_token.c_str(), name_for_version(version), command, flag);
|
||||
|
||||
PSOCommandHeader header;
|
||||
size_t header_size = header.header_size(version);
|
||||
header.set_command(version, command);
|
||||
header.set_flag(version, flag);
|
||||
header.set_size(version, size + header_size);
|
||||
|
||||
vector<struct iovec> iovs;
|
||||
iovs.emplace_back(iovec{.iov_base = &header, .iov_len = header_size});
|
||||
iovs.emplace_back(iovec{.iov_base = const_cast<void*>(data), .iov_len = size});
|
||||
print_data(stderr, iovs, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR);
|
||||
|
||||
if (use_terminal_colors) {
|
||||
print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END);
|
||||
}
|
||||
}
|
||||
|
||||
void check_size_v(size_t size, size_t min_size, size_t max_size) {
|
||||
if (size < min_size) {
|
||||
throw std::runtime_error(string_printf(
|
||||
|
||||
@@ -52,21 +52,6 @@ union PSOSubcommand {
|
||||
le_uint32_t dword;
|
||||
} __attribute__((packed));
|
||||
|
||||
void for_each_received_command(
|
||||
struct bufferevent* bev,
|
||||
GameVersion version,
|
||||
PSOEncryption* crypt,
|
||||
std::function<void(uint16_t, uint16_t, std::string&)> fn);
|
||||
|
||||
void print_received_command(
|
||||
uint16_t command,
|
||||
uint32_t flag,
|
||||
const void* data,
|
||||
size_t size,
|
||||
GameVersion version,
|
||||
const char* name = nullptr,
|
||||
TerminalFormat color = TerminalFormat::FG_GREEN);
|
||||
|
||||
// This function is used in a lot of places to check received command sizes and
|
||||
// cast them to the appropriate type
|
||||
template <typename T>
|
||||
|
||||
+216
-169
@@ -26,6 +26,7 @@
|
||||
#include <resource_file/Emulators/PPC32Emulator.hh>
|
||||
#endif
|
||||
|
||||
#include "ChatCommands.hh"
|
||||
#include "Compression.hh"
|
||||
#include "PSOProtocol.hh"
|
||||
#include "SendCommands.hh"
|
||||
@@ -37,21 +38,13 @@ using namespace std;
|
||||
|
||||
|
||||
static void forward_command(ProxyServer::LinkedSession& session, bool to_server,
|
||||
uint16_t command, uint32_t flag, string& data) {
|
||||
auto* bev = to_server ? session.server_bev.get() : session.client_bev.get();
|
||||
if (!bev) {
|
||||
uint16_t command, uint32_t flag, string& data, bool print_contents = true) {
|
||||
auto& ch = to_server ? session.server_channel : session.client_channel;
|
||||
if (!ch.connected()) {
|
||||
session.log(WARNING, "No endpoint is present; dropping command");
|
||||
} else {
|
||||
// Note: we intentionally don't pass name_str here because we already
|
||||
// printed the command before calling the handler
|
||||
send_command(
|
||||
bev,
|
||||
session.version,
|
||||
to_server ? session.server_output_crypt.get() : session.client_output_crypt.get(),
|
||||
command,
|
||||
flag,
|
||||
data.data(),
|
||||
data.size());
|
||||
// TODO: Don't print the command here (usually)
|
||||
ch.send(command, flag, data, print_contents);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +78,7 @@ static void send_text_message_to_client(
|
||||
while (w.size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
session.send_to_end(false, command, 0x00, w.str());
|
||||
session.client_channel.send(command, 0x00, w.str());
|
||||
}
|
||||
|
||||
|
||||
@@ -95,26 +88,33 @@ static void send_text_message_to_client(
|
||||
// function may have modified) is forwarded to the other end; if they return
|
||||
// false; it is not.
|
||||
|
||||
static bool process_default(shared_ptr<ServerState>,
|
||||
enum class HandlerResult {
|
||||
FORWARD = 0,
|
||||
SUPPRESS,
|
||||
MODIFIED,
|
||||
};
|
||||
|
||||
static HandlerResult process_default(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession&, uint16_t, uint32_t, string&) {
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_97(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_97(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string&) {
|
||||
// Trap 97 commands and always send 97 01 04 00. (If flag is 0, the client
|
||||
// triggers cheat protection and deletes a bunch of data.)
|
||||
session.send_to_end(false, 0x97, 0x01);
|
||||
// Trap 97 commands and always send 97 01 04 00. This protects the client from
|
||||
// cheat detection - if the flag is 0, the client triggers cheat detection and
|
||||
// deletes a bunch of data.
|
||||
session.client_channel.send(0x97, 0x01);
|
||||
// Also, update the newserv client config so we'll know not to show the
|
||||
// programs menu if they return to newserv.
|
||||
session.newserv_client_config.cfg.flags |= Client::Flag::SAVE_ENABLED;
|
||||
return false;
|
||||
return HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
static bool process_server_gc_9A(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_gc_9A(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string&) {
|
||||
if (!session.license) {
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
C_LoginWithUnusedSpace_GC_9E cmd;
|
||||
@@ -138,13 +138,13 @@ static bool process_server_gc_9A(shared_ptr<ServerState>,
|
||||
// If there's a guild card number, a shorter 9E is sent that ends
|
||||
// right after the client config data
|
||||
|
||||
session.send_to_end(
|
||||
true, 0x9E, 0x01, &cmd,
|
||||
session.server_channel.send(
|
||||
0x9E, 0x01, &cmd,
|
||||
sizeof(C_LoginWithUnusedSpace_GC_9E) - (session.remote_guild_card_number ? sizeof(cmd.unused_space) : 0));
|
||||
return false;
|
||||
return HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
static bool process_server_pc_gc_patch_02_17(shared_ptr<ServerState> s,
|
||||
static HandlerResult process_server_pc_gc_patch_02_17(shared_ptr<ServerState> s,
|
||||
ProxyServer::LinkedSession& session, uint16_t command, uint32_t flag, string& data) {
|
||||
if (session.version == GameVersion::PATCH && command == 0x17) {
|
||||
throw invalid_argument("patch server sent 17 server init");
|
||||
@@ -163,29 +163,29 @@ static bool process_server_pc_gc_patch_02_17(shared_ptr<ServerState> s,
|
||||
forward_command(session, false, command, flag, data);
|
||||
|
||||
if (session.version == GameVersion::GC) {
|
||||
session.server_input_crypt.reset(new PSOGCEncryption(cmd.server_key));
|
||||
session.server_output_crypt.reset(new PSOGCEncryption(cmd.client_key));
|
||||
session.client_input_crypt.reset(new PSOGCEncryption(cmd.client_key));
|
||||
session.client_output_crypt.reset(new PSOGCEncryption(cmd.server_key));
|
||||
session.server_channel.crypt_in.reset(new PSOGCEncryption(cmd.server_key));
|
||||
session.server_channel.crypt_out.reset(new PSOGCEncryption(cmd.client_key));
|
||||
session.client_channel.crypt_in.reset(new PSOGCEncryption(cmd.client_key));
|
||||
session.client_channel.crypt_out.reset(new PSOGCEncryption(cmd.server_key));
|
||||
} else { // PC or patch server (they both use PC encryption)
|
||||
session.server_input_crypt.reset(new PSOPCEncryption(cmd.server_key));
|
||||
session.server_output_crypt.reset(new PSOPCEncryption(cmd.client_key));
|
||||
session.client_input_crypt.reset(new PSOPCEncryption(cmd.client_key));
|
||||
session.client_output_crypt.reset(new PSOPCEncryption(cmd.server_key));
|
||||
session.server_channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key));
|
||||
session.server_channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key));
|
||||
session.client_channel.crypt_in.reset(new PSOPCEncryption(cmd.client_key));
|
||||
session.client_channel.crypt_out.reset(new PSOPCEncryption(cmd.server_key));
|
||||
}
|
||||
|
||||
return false;
|
||||
return HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
session.log(INFO, "Existing license in linked session");
|
||||
|
||||
// This isn't forwarded to the client, so don't recreate the client's crypts
|
||||
if ((session.version == GameVersion::PATCH) || (session.version == GameVersion::PC)) {
|
||||
session.server_input_crypt.reset(new PSOPCEncryption(cmd.server_key));
|
||||
session.server_output_crypt.reset(new PSOPCEncryption(cmd.client_key));
|
||||
session.server_channel.crypt_in.reset(new PSOPCEncryption(cmd.server_key));
|
||||
session.server_channel.crypt_out.reset(new PSOPCEncryption(cmd.client_key));
|
||||
} else if (session.version == GameVersion::GC) {
|
||||
session.server_input_crypt.reset(new PSOGCEncryption(cmd.server_key));
|
||||
session.server_output_crypt.reset(new PSOGCEncryption(cmd.client_key));
|
||||
session.server_channel.crypt_in.reset(new PSOGCEncryption(cmd.server_key));
|
||||
session.server_channel.crypt_out.reset(new PSOGCEncryption(cmd.client_key));
|
||||
} else {
|
||||
throw invalid_argument("unsupported version");
|
||||
}
|
||||
@@ -195,8 +195,8 @@ static bool process_server_pc_gc_patch_02_17(shared_ptr<ServerState> s,
|
||||
// in the patch server case, during the current session due to a hidden
|
||||
// redirect).
|
||||
if (session.version == GameVersion::PATCH) {
|
||||
session.send_to_end(true, 0x02, 0x00);
|
||||
return false;
|
||||
session.server_channel.send(0x02);
|
||||
return HandlerResult::SUPPRESS;
|
||||
|
||||
} else if (session.version == GameVersion::PC) {
|
||||
C_Login_PC_9D cmd;
|
||||
@@ -216,8 +216,8 @@ static bool process_server_pc_gc_patch_02_17(shared_ptr<ServerState> s,
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
cmd.name = session.character_name;
|
||||
session.send_to_end(true, 0x9D, 0x00, &cmd, sizeof(cmd));
|
||||
return false;
|
||||
session.server_channel.send(0x9D, 0x00, &cmd, sizeof(cmd));
|
||||
return HandlerResult::SUPPRESS;
|
||||
|
||||
} else if (session.version == GameVersion::GC) {
|
||||
if (command == 0x17) {
|
||||
@@ -229,8 +229,8 @@ static bool process_server_pc_gc_patch_02_17(shared_ptr<ServerState> s,
|
||||
cmd.serial_number2 = cmd.serial_number;
|
||||
cmd.access_key2 = cmd.access_key;
|
||||
cmd.password = session.license->gc_password;
|
||||
session.send_to_end(true, 0xDB, 0x00, &cmd, sizeof(cmd));
|
||||
return false;
|
||||
session.server_channel.send(0xDB, 0x00, &cmd, sizeof(cmd));
|
||||
return HandlerResult::SUPPRESS;
|
||||
|
||||
} else {
|
||||
// For command 02, send the same as if we had received 9A from the server
|
||||
@@ -242,7 +242,7 @@ static bool process_server_pc_gc_patch_02_17(shared_ptr<ServerState> s,
|
||||
}
|
||||
}
|
||||
|
||||
static bool process_server_bb_03(shared_ptr<ServerState> s,
|
||||
static HandlerResult process_server_bb_03(shared_ptr<ServerState> s,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
// Most servers don't include after_message or have a shorter after_message
|
||||
// than newserv does, so don't require it
|
||||
@@ -261,9 +261,9 @@ static bool process_server_bb_03(shared_ptr<ServerState> s,
|
||||
// being able to try all the crypts it knows to detect what type the client
|
||||
// uses, but the client can't do this since it sends the first encrypted
|
||||
// data on the connection.
|
||||
session.server_input_crypt.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session.server_channel.crypt_in.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session.detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), false));
|
||||
session.server_output_crypt.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session.server_channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session.detector_crypt, cmd.client_key.data(), sizeof(cmd.client_key), false));
|
||||
|
||||
// Forward the login command we saved during the unlinked session.
|
||||
@@ -271,9 +271,9 @@ static bool process_server_bb_03(shared_ptr<ServerState> s,
|
||||
*reinterpret_cast<le_uint32_t*>(session.login_command_bb.data() + 0x94) =
|
||||
session.remote_ip_crc ^ (1309539928UL + 1248334810UL);
|
||||
}
|
||||
session.send_to_end(true, 0x93, 0x00, session.login_command_bb);
|
||||
session.server_channel.send(0x93, 0x00, session.login_command_bb);
|
||||
|
||||
return false;
|
||||
return HandlerResult::SUPPRESS;
|
||||
|
||||
// If there's no detector crypt, then the session is new and was linked
|
||||
// immediately at connect time, and an 03 was not yet sent to the client, so
|
||||
@@ -281,25 +281,25 @@ static bool process_server_bb_03(shared_ptr<ServerState> s,
|
||||
} else {
|
||||
// Forward the command to the client before setting up the crypts, so the
|
||||
// client receives the unencrypted data
|
||||
session.send_to_end(false, 0x03, 0x00, data);
|
||||
session.client_channel.send(0x03, 0x00, data);
|
||||
|
||||
static const string expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8);
|
||||
session.detector_crypt.reset(new PSOBBMultiKeyDetectorEncryption(
|
||||
s->bb_private_keys, expected_first_data, cmd.client_key.data(), sizeof(cmd.client_key)));
|
||||
session.client_input_crypt = session.detector_crypt;
|
||||
session.client_output_crypt.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session.client_channel.crypt_in = session.detector_crypt;
|
||||
session.client_channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session.detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), true));
|
||||
session.server_input_crypt.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session.server_channel.crypt_in.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session.detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), false));
|
||||
session.server_output_crypt.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session.server_channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session.detector_crypt, cmd.client_key.data(), sizeof(cmd.client_key), false));
|
||||
|
||||
// We already forwarded the command, so don't do so again
|
||||
return false;
|
||||
return HandlerResult::SUPPRESS;
|
||||
}
|
||||
}
|
||||
|
||||
static bool process_server_dc_pc_gc_04(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_dc_pc_gc_04(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
// Some servers send a short 04 command if they don't use all of the 0x20
|
||||
// bytes available. We should be prepared to handle that.
|
||||
@@ -346,56 +346,64 @@ static bool process_server_dc_pc_gc_04(shared_ptr<ServerState>,
|
||||
// We don't actually have a client checksum, of course... hopefully just
|
||||
// random data will do (probably no private servers check this at all)
|
||||
le_uint64_t checksum = random_object<uint64_t>() & 0x0000FFFFFFFFFFFF;
|
||||
session.send_to_end(true, 0x96, 0x00, &checksum, sizeof(checksum));
|
||||
session.server_channel.send(0x96, 0x00, &checksum, sizeof(checksum));
|
||||
}
|
||||
|
||||
return true;
|
||||
return session.license ? HandlerResult::MODIFIED : HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_dc_pc_gc_06(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_dc_pc_gc_06(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
if (session.license) {
|
||||
auto& cmd = check_size_t<SC_TextHeader_01_06_11_B0_EE>(data,
|
||||
sizeof(SC_TextHeader_01_06_11_B0_EE), 0xFFFF);
|
||||
if (cmd.guild_card_number == session.remote_guild_card_number) {
|
||||
cmd.guild_card_number = session.license->serial_number;
|
||||
return HandlerResult::MODIFIED;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
template <typename CmdT>
|
||||
static bool process_server_41(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_41(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
bool modified = false;
|
||||
if (session.license) {
|
||||
auto& cmd = check_size_t<CmdT>(data);
|
||||
if (cmd.searcher_guild_card_number == session.remote_guild_card_number) {
|
||||
cmd.searcher_guild_card_number = session.license->serial_number;
|
||||
modified = true;
|
||||
}
|
||||
if (cmd.result_guild_card_number == session.remote_guild_card_number) {
|
||||
cmd.result_guild_card_number = session.license->serial_number;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return modified ? HandlerResult::MODIFIED : HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
template <typename CmdT>
|
||||
static bool process_server_81(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_81(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
bool modified = false;
|
||||
if (session.license) {
|
||||
auto& cmd = check_size_t<CmdT>(data);
|
||||
if (cmd.from_guild_card_number == session.remote_guild_card_number) {
|
||||
cmd.from_guild_card_number = session.license->serial_number;
|
||||
modified = true;
|
||||
}
|
||||
if (cmd.to_guild_card_number == session.remote_guild_card_number) {
|
||||
cmd.to_guild_card_number = session.license->serial_number;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return modified ? HandlerResult::MODIFIED : HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_88(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_88(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t flag, string& data) {
|
||||
bool modified = false;
|
||||
if (session.license) {
|
||||
size_t expected_size = sizeof(S_ArrowUpdateEntry_88) * flag;
|
||||
auto* entries = &check_size_t<S_ArrowUpdateEntry_88>(data,
|
||||
@@ -403,13 +411,14 @@ static bool process_server_88(shared_ptr<ServerState>,
|
||||
for (size_t x = 0; x < flag; x++) {
|
||||
if (entries[x].guild_card_number == session.remote_guild_card_number) {
|
||||
entries[x].guild_card_number = session.license->serial_number;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return modified ? HandlerResult::MODIFIED : HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_B2(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_B2(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t flag, string& data) {
|
||||
if (session.save_files) {
|
||||
string output_filename = string_printf("code.%" PRId64 ".bin", now());
|
||||
@@ -474,26 +483,27 @@ static bool process_server_B2(shared_ptr<ServerState>,
|
||||
C_ExecuteCodeResult_B3 cmd;
|
||||
cmd.return_value = session.function_call_return_value;
|
||||
cmd.checksum = 0;
|
||||
session.send_to_end(true, 0xB3, flag, &cmd, sizeof(cmd));
|
||||
return false;
|
||||
session.server_channel.send(0xB3, flag, &cmd, sizeof(cmd));
|
||||
return HandlerResult::SUPPRESS;
|
||||
} else {
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
}
|
||||
|
||||
static bool process_server_E7(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_E7(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
if (session.save_files) {
|
||||
string output_filename = string_printf("player.%" PRId64 ".bin", now());
|
||||
save_file(output_filename, data);
|
||||
session.log(INFO, "Wrote player data to file %s", output_filename.c_str());
|
||||
}
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
template <typename CmdT>
|
||||
static bool process_server_C4(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_C4(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t flag, string& data) {
|
||||
bool modified = false;
|
||||
if (session.license) {
|
||||
size_t expected_size = sizeof(CmdT) * flag;
|
||||
// Some servers (e.g. Schtserv) send extra data on the end of this command;
|
||||
@@ -502,24 +512,27 @@ static bool process_server_C4(shared_ptr<ServerState>,
|
||||
for (size_t x = 0; x < flag; x++) {
|
||||
if (entries[x].guild_card_number == session.remote_guild_card_number) {
|
||||
entries[x].guild_card_number = session.license->serial_number;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return modified ? HandlerResult::MODIFIED : HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_gc_E4(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_gc_E4(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
auto& cmd = check_size_t<S_CardLobbyGame_GC_E4>(data);
|
||||
bool modified = false;
|
||||
for (size_t x = 0; x < 4; x++) {
|
||||
if (cmd.entries[x].guild_card_number == session.remote_guild_card_number) {
|
||||
cmd.entries[x].guild_card_number = session.license->serial_number;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return modified ? HandlerResult::MODIFIED : HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_bb_22(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_bb_22(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
// We use this command (which is sent before the init encryption command) to
|
||||
// detect a particular server behavior that we'll have to work around later.
|
||||
@@ -535,10 +548,10 @@ static bool process_server_bb_22(shared_ptr<ServerState>,
|
||||
session.log(INFO, "Enabling remote IP CRC patch");
|
||||
session.enable_remote_ip_crc_patch = true;
|
||||
}
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_game_19_patch_14(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_game_19_patch_14(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t command, uint32_t, string& data) {
|
||||
// If the command is shorter than 6 bytes, use the previous server command to
|
||||
// fill it in. This simulates a behavior used by some private servers where a
|
||||
@@ -570,17 +583,17 @@ static bool process_server_game_19_patch_14(shared_ptr<ServerState>,
|
||||
sin->sin_addr.s_addr = cmd.address.load_raw();
|
||||
sin->sin_port = htons(cmd.port);
|
||||
|
||||
if (!session.client_bev.get()) {
|
||||
if (!session.client_channel.connected()) {
|
||||
session.log(WARNING, "Received reconnect command with no destination present");
|
||||
return false;
|
||||
return HandlerResult::SUPPRESS;
|
||||
|
||||
} else if (command == 0x14) {
|
||||
// On the patch server, hide redirects from the client completely. The new
|
||||
// destination server will presumably send a new 02 command to start
|
||||
// encryption; it appears that PSOBB doesn't fail if this happens, and
|
||||
// simply re-initializes its encryption appropriately.
|
||||
session.server_input_crypt.reset();
|
||||
session.server_output_crypt.reset();
|
||||
session.server_channel.crypt_in.reset();
|
||||
session.server_channel.crypt_out.reset();
|
||||
|
||||
struct sockaddr_in* dest_sin = reinterpret_cast<sockaddr_in*>(
|
||||
&session.next_destination);
|
||||
@@ -588,46 +601,41 @@ static bool process_server_game_19_patch_14(shared_ptr<ServerState>,
|
||||
dest_sin->sin_addr.s_addr = cmd.address.load_raw();
|
||||
dest_sin->sin_port = cmd.port;
|
||||
session.connect();
|
||||
return false;
|
||||
return HandlerResult::SUPPRESS;
|
||||
|
||||
} else {
|
||||
// If the client is on a virtual connection (fd < 0), only change
|
||||
// the port (so we'll know which version to treat the next
|
||||
// connection as). It's better to leave the address as-is so we
|
||||
// can circumvent the Plus/Ep3 same-network-server check.
|
||||
int fd = bufferevent_getfd(session.client_bev.get());
|
||||
if (fd >= 0) {
|
||||
struct sockaddr_storage sockname_ss;
|
||||
socklen_t len = sizeof(sockname_ss);
|
||||
getsockname(fd, reinterpret_cast<struct sockaddr*>(&sockname_ss), &len);
|
||||
if (sockname_ss.ss_family != AF_INET) {
|
||||
if (session.client_channel.is_virtual_connection) {
|
||||
cmd.port = session.local_port;
|
||||
} else {
|
||||
const struct sockaddr_in* sin = reinterpret_cast<const struct sockaddr_in*>(
|
||||
&session.client_channel.local_addr);
|
||||
if (sin->sin_family != AF_INET) {
|
||||
throw logic_error("existing connection is not ipv4");
|
||||
}
|
||||
|
||||
struct sockaddr_in* sockname_sin = reinterpret_cast<struct sockaddr_in*>(
|
||||
&sockname_ss);
|
||||
cmd.address.store_raw(sockname_sin->sin_addr.s_addr);
|
||||
cmd.port = ntohs(sockname_sin->sin_port);
|
||||
|
||||
} else {
|
||||
cmd.port = session.local_port;
|
||||
cmd.address.store_raw(sin->sin_addr.s_addr);
|
||||
cmd.port = ntohs(sin->sin_port);
|
||||
}
|
||||
return true;
|
||||
return HandlerResult::MODIFIED;
|
||||
}
|
||||
}
|
||||
|
||||
static bool process_server_gc_1A_D5(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_gc_1A_D5(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string&) {
|
||||
// If the client has the no-close-confirmation flag set in its
|
||||
// newserv client config, send a fake confirmation to the remote
|
||||
// server immediately.
|
||||
if (session.newserv_client_config.cfg.flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION) {
|
||||
session.send_to_end(true, 0xD6, 0x00, "", 0);
|
||||
// If the client is a version that sends close confirmations and the client
|
||||
// has the no-close-confirmation flag set in its newserv client config, send a
|
||||
// fake confirmation to the remote server immediately.
|
||||
if ((session.version == GameVersion::GC) &&
|
||||
(session.newserv_client_config.cfg.flags & Client::Flag::NO_MESSAGE_BOX_CLOSE_CONFIRMATION)) {
|
||||
session.server_channel.send(0xD6);
|
||||
}
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_60_62_6C_6D_C9_CB(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_60_62_6C_6D_C9_CB(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
check_implemented_subcommand(session.id, data);
|
||||
|
||||
@@ -644,11 +652,11 @@ static bool process_server_60_62_6C_6D_C9_CB(shared_ptr<ServerState>,
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static bool process_server_44_A6(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_44_A6(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t command, uint32_t, string& data) {
|
||||
if (session.save_files) {
|
||||
const auto& cmd = check_size_t<S_OpenFile_PC_GC_44_A6>(data);
|
||||
@@ -672,10 +680,10 @@ static bool process_server_44_A6(shared_ptr<ServerState>,
|
||||
session.saving_files.emplace(cmd.filename, move(sf));
|
||||
session.log(INFO, "Opened file %s", output_filename.c_str());
|
||||
}
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_13_A7(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_13_A7(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
if (session.save_files) {
|
||||
const auto& cmd = check_size_t<S_WriteFile_13_A7>(data);
|
||||
@@ -686,7 +694,7 @@ static bool process_server_13_A7(shared_ptr<ServerState>,
|
||||
} catch (const out_of_range&) {
|
||||
string filename = cmd.filename;
|
||||
session.log(WARNING, "Received data for non-open file %s", filename.c_str());
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
size_t bytes_to_write = cmd.data_size;
|
||||
@@ -709,33 +717,33 @@ static bool process_server_13_A7(shared_ptr<ServerState>,
|
||||
session.saving_files.erase(cmd.filename);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_gc_B8(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_gc_B8(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
if (session.save_files) {
|
||||
if (data.size() < 4) {
|
||||
session.log(WARNING, "Card list data size is too small; skipping file");
|
||||
return true;
|
||||
session.log(WARNING, "Card list data size is too small; not saving file");
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
StringReader r(data);
|
||||
size_t size = r.get_u32l();
|
||||
if (r.remaining() < size) {
|
||||
session.log(WARNING, "Card list data size extends beyond end of command; skipping file");
|
||||
return true;
|
||||
session.log(WARNING, "Card list data size extends beyond end of command; not saving file");
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
string output_filename = string_printf("cardupdate.%" PRIu64 ".mnr", now());
|
||||
save_file(output_filename, r.read(size));
|
||||
session.log(INFO, "Wrote %zu bytes to %s", size, output_filename.c_str());
|
||||
}
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
template <typename CmdT>
|
||||
static bool process_server_65_67_68(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_65_67_68(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t command, uint32_t flag, string& data) {
|
||||
if (command == 0x67) {
|
||||
session.lobby_players.clear();
|
||||
@@ -754,6 +762,7 @@ static bool process_server_65_67_68(shared_ptr<ServerState>,
|
||||
|
||||
size_t expected_size = offsetof(CmdT, entries) + sizeof(typename CmdT::Entry) * flag;
|
||||
auto& cmd = check_size_t<CmdT>(data, expected_size, expected_size);
|
||||
bool modified = false;
|
||||
|
||||
session.lobby_client_id = cmd.client_id;
|
||||
for (size_t x = 0; x < flag; x++) {
|
||||
@@ -763,6 +772,7 @@ static bool process_server_65_67_68(shared_ptr<ServerState>,
|
||||
} else {
|
||||
if (session.license && (cmd.entries[x].lobby_data.guild_card == session.remote_guild_card_number)) {
|
||||
cmd.entries[x].lobby_data.guild_card = session.license->serial_number;
|
||||
modified = true;
|
||||
}
|
||||
session.lobby_players[index].guild_card_number = cmd.entries[x].lobby_data.guild_card;
|
||||
ptext<char, 0x10> name = cmd.entries[x].disp.name;
|
||||
@@ -776,16 +786,18 @@ static bool process_server_65_67_68(shared_ptr<ServerState>,
|
||||
|
||||
if (session.override_lobby_event >= 0) {
|
||||
cmd.event = session.override_lobby_event;
|
||||
modified = true;
|
||||
}
|
||||
if (session.override_lobby_number >= 0) {
|
||||
cmd.lobby_number = session.override_lobby_number;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return modified ? HandlerResult::MODIFIED : HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
template <typename CmdT>
|
||||
static bool process_server_64(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_64(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t flag, string& data) {
|
||||
// We don't need to clear lobby_players here because we always
|
||||
// overwrite all 4 entries for this command
|
||||
@@ -796,11 +808,13 @@ static bool process_server_64(shared_ptr<ServerState>,
|
||||
? sizeof(CmdT)
|
||||
: offsetof(CmdT, players_ep3);
|
||||
auto& cmd = check_size_t<CmdT>(data, expected_size, expected_size);
|
||||
bool modified = false;
|
||||
|
||||
session.lobby_client_id = cmd.client_id;
|
||||
for (size_t x = 0; x < flag; x++) {
|
||||
if (cmd.lobby_data[x].guild_card == session.remote_guild_card_number) {
|
||||
cmd.lobby_data[x].guild_card = session.license->serial_number;
|
||||
modified = true;
|
||||
}
|
||||
session.lobby_players[x].guild_card_number = cmd.lobby_data[x].guild_card;
|
||||
if (data.size() == sizeof(CmdT)) {
|
||||
@@ -817,15 +831,17 @@ static bool process_server_64(shared_ptr<ServerState>,
|
||||
|
||||
if (session.override_section_id >= 0) {
|
||||
cmd.section_id = session.override_section_id;
|
||||
modified = true;
|
||||
}
|
||||
if (session.override_lobby_event >= 0) {
|
||||
cmd.event = session.override_lobby_event;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return modified ? HandlerResult::MODIFIED : HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_server_66_69(shared_ptr<ServerState>,
|
||||
static HandlerResult process_server_66_69(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
const auto& cmd = check_size_t<S_LeaveLobby_66_69_Ep3_E9>(data);
|
||||
size_t index = cmd.client_id;
|
||||
@@ -836,44 +852,71 @@ static bool process_server_66_69(shared_ptr<ServerState>,
|
||||
session.lobby_players[index].name.clear();
|
||||
session.log(INFO, "Removed lobby player (%zu)", index);
|
||||
}
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
static bool process_client_06(shared_ptr<ServerState>,
|
||||
static HandlerResult process_client_06(shared_ptr<ServerState> s,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
if (data.size() >= 12) {
|
||||
// If this chat message looks like a newserv chat command, suppress it
|
||||
if (session.suppress_newserv_commands &&
|
||||
(data[8] == '$' || (data[8] == '\t' && data[9] != 'C' && data[10] == '$'))) {
|
||||
session.log(WARNING, "Chat message appears to be a server command; dropping it");
|
||||
return false;
|
||||
u16string text;
|
||||
if (session.version == GameVersion::PC || session.version == GameVersion::BB) {
|
||||
const auto& cmd = check_size_t<C_Chat_06>(data, sizeof(C_Chat_06), 0xFFFF);
|
||||
text = u16string(cmd.text.pcbb, (data.size() - sizeof(C_Chat_06)) / sizeof(char16_t));
|
||||
} else {
|
||||
const auto& cmd = check_size_t<C_Chat_06>(data, sizeof(C_Chat_06), 0xFFFF);
|
||||
text = decode_sjis(cmd.text.dcgc, data.size() - sizeof(C_Chat_06));
|
||||
}
|
||||
strip_trailing_zeroes(text);
|
||||
|
||||
if (text.empty()) {
|
||||
return HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
bool is_command = (text[0] == '$' || (text[0] == '\t' && text[1] != 'C' && text[2] == '$'));
|
||||
if (is_command) {
|
||||
text = text.substr((text[0] == '$') ? 0 : 2);
|
||||
if (text.size() >= 2 && text[1] == '$') {
|
||||
send_chat_message(session.server_channel, text.substr(1));
|
||||
return HandlerResult::SUPPRESS;
|
||||
} else {
|
||||
process_chat_command(s, session, text);
|
||||
return HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
} else if (session.enable_chat_filter) {
|
||||
add_color_inplace(data.data() + 8, data.size() - 8);
|
||||
// TODO: We should return MODIFIED here if the message was changed by
|
||||
// the add_color_inplace call
|
||||
return HandlerResult::FORWARD;
|
||||
|
||||
} else {
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
} else {
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool process_client_40(shared_ptr<ServerState>,
|
||||
static HandlerResult process_client_40(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
bool modified = false;
|
||||
if (session.license) {
|
||||
auto& cmd = check_size_t<C_GuildCardSearch_40>(data);
|
||||
if (cmd.searcher_guild_card_number == session.license->serial_number) {
|
||||
cmd.searcher_guild_card_number = session.remote_guild_card_number;
|
||||
modified = true;
|
||||
}
|
||||
if (cmd.target_guild_card_number == session.license->serial_number) {
|
||||
cmd.target_guild_card_number = session.remote_guild_card_number;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return modified ? HandlerResult::MODIFIED : HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
template <typename CmdT>
|
||||
static bool process_client_81(shared_ptr<ServerState>,
|
||||
static HandlerResult process_client_81(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
auto& cmd = check_size_t<SC_SimpleMail_GC_81>(data);
|
||||
if (session.license) {
|
||||
@@ -886,11 +929,11 @@ static bool process_client_81(shared_ptr<ServerState>,
|
||||
}
|
||||
// GC clients send uninitialized memory here; don't forward it
|
||||
cmd.text.clear_after(cmd.text.len());
|
||||
return true;
|
||||
return HandlerResult::MODIFIED;
|
||||
}
|
||||
|
||||
template <typename SendGuildCardCmdT>
|
||||
static bool process_client_60_62_6C_6D_C9_CB(shared_ptr<ServerState> s,
|
||||
static HandlerResult process_client_60_62_6C_6D_C9_CB(shared_ptr<ServerState> s,
|
||||
ProxyServer::LinkedSession& session, uint16_t command, uint32_t flag, string& data) {
|
||||
if (session.license && !data.empty()) {
|
||||
if (data[0] == 0x06) {
|
||||
@@ -912,7 +955,7 @@ static bool process_client_60_62_6C_6D_C9_CB(shared_ptr<ServerState> s,
|
||||
sub2.byte[3] = (amount > 0xFF) ? 0xFF : amount;
|
||||
amount -= sub2.byte[3];
|
||||
}
|
||||
session.send_to_end(false, 0x60, 0x00, subs.data(), subs.size() * sizeof(PSOSubcommand));
|
||||
session.client_channel.send(0x60, 0x00, subs.data(), subs.size() * sizeof(PSOSubcommand));
|
||||
}
|
||||
} else if (data[0] == 0x48) {
|
||||
if (session.infinite_tp) {
|
||||
@@ -923,7 +966,7 @@ static bool process_client_60_62_6C_6D_C9_CB(shared_ptr<ServerState> s,
|
||||
subs[1].word[0] = 0x0000;
|
||||
subs[1].byte[2] = PlayerStatsChange::ADD_TP;
|
||||
subs[1].byte[3] = 0xFF;
|
||||
session.send_to_end(false, 0x60, 0x00, &subs[0], sizeof(subs));
|
||||
session.client_channel.send(0x60, 0x00, &subs[0], sizeof(subs));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -931,31 +974,31 @@ static bool process_client_60_62_6C_6D_C9_CB(shared_ptr<ServerState> s,
|
||||
}
|
||||
|
||||
template <>
|
||||
bool process_client_60_62_6C_6D_C9_CB<void>(shared_ptr<ServerState>,
|
||||
HandlerResult process_client_60_62_6C_6D_C9_CB<void>(shared_ptr<ServerState>,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
|
||||
check_implemented_subcommand(session.id, data);
|
||||
|
||||
if (!data.empty() && (data[0] == 0x05) && session.enable_switch_assist) {
|
||||
if (!data.empty() && (data[0] == 0x05) && session.switch_assist) {
|
||||
auto& cmd = check_size_t<G_SwitchStateChanged_6x05>(data);
|
||||
if (cmd.enabled && cmd.switch_id != 0xFFFF) {
|
||||
if (session.last_switch_enabled_command.subcommand == 0x05) {
|
||||
session.log(INFO, "Switch assist: replaying previous enable command");
|
||||
session.send_to_end(true, 0x60, 0x00, &session.last_switch_enabled_command,
|
||||
session.server_channel.send(0x60, 0x00, &session.last_switch_enabled_command,
|
||||
sizeof(session.last_switch_enabled_command));
|
||||
session.send_to_end(false, 0x60, 0x00, &session.last_switch_enabled_command,
|
||||
session.client_channel.send(0x60, 0x00, &session.last_switch_enabled_command,
|
||||
sizeof(session.last_switch_enabled_command));
|
||||
}
|
||||
session.last_switch_enabled_command = cmd;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
static bool process_client_dc_pc_gc_A0_A1(shared_ptr<ServerState> s,
|
||||
static HandlerResult process_client_dc_pc_gc_A0_A1(shared_ptr<ServerState> s,
|
||||
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string&) {
|
||||
if (!session.license) {
|
||||
return true;
|
||||
return HandlerResult::FORWARD;
|
||||
}
|
||||
|
||||
// For licensed sessions, send them back to newserv's main menu instead of
|
||||
@@ -969,7 +1012,7 @@ static bool process_client_dc_pc_gc_A0_A1(shared_ptr<ServerState> s,
|
||||
uint8_t leaving_id = x;
|
||||
uint8_t leader_id = session.lobby_client_id;
|
||||
S_LeaveLobby_66_69_Ep3_E9 cmd = {leaving_id, leader_id, 0};
|
||||
session.send_to_end(false, 0x69, leaving_id, &cmd, sizeof(cmd));
|
||||
session.client_channel.send(0x69, leaving_id, &cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
string encoded_name = encode_sjis(s->name);
|
||||
@@ -981,7 +1024,7 @@ static bool process_client_dc_pc_gc_A0_A1(shared_ptr<ServerState> s,
|
||||
update_client_config_cmd.player_tag = 0x00010000;
|
||||
update_client_config_cmd.guild_card_number = session.license->serial_number;
|
||||
update_client_config_cmd.cfg = session.newserv_client_config.cfg;
|
||||
session.send_to_end(false, 0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd));
|
||||
session.client_channel.send(0x04, 0x00, &update_client_config_cmd, sizeof(update_client_config_cmd));
|
||||
|
||||
static const vector<string> version_to_port_name({
|
||||
"dc-login", "pc-login", "bb-patch", "gc-us3", "bb-login"});
|
||||
@@ -995,34 +1038,29 @@ static bool process_client_dc_pc_gc_A0_A1(shared_ptr<ServerState> s,
|
||||
// here and they should be able to connect back to the game server. If
|
||||
// the client is on a real connection, we'll use the sockname of the
|
||||
// existing connection (like we do in the server 19 command handler).
|
||||
int fd = bufferevent_getfd(session.client_bev.get());
|
||||
if (fd < 0) {
|
||||
if (session.client_channel.is_virtual_connection) {
|
||||
struct sockaddr_in* dest_sin = reinterpret_cast<struct sockaddr_in*>(&session.next_destination);
|
||||
if (dest_sin->sin_family != AF_INET) {
|
||||
throw logic_error("ss not AF_INET");
|
||||
}
|
||||
reconnect_cmd.address.store_raw(dest_sin->sin_addr.s_addr);
|
||||
} else {
|
||||
struct sockaddr_storage sockname_ss;
|
||||
socklen_t len = sizeof(sockname_ss);
|
||||
getsockname(fd, reinterpret_cast<struct sockaddr*>(&sockname_ss), &len);
|
||||
if (sockname_ss.ss_family != AF_INET) {
|
||||
const struct sockaddr_in* sin = reinterpret_cast<const struct sockaddr_in*>(
|
||||
&session.client_channel.local_addr);
|
||||
if (sin->sin_family != AF_INET) {
|
||||
throw logic_error("existing connection is not ipv4");
|
||||
}
|
||||
|
||||
struct sockaddr_in* sockname_sin = reinterpret_cast<struct sockaddr_in*>(
|
||||
&sockname_ss);
|
||||
reconnect_cmd.address.store_raw(sockname_sin->sin_addr.s_addr);
|
||||
reconnect_cmd.address.store_raw(sin->sin_addr.s_addr);
|
||||
}
|
||||
|
||||
session.send_to_end(false, 0x19, 0x00, &reconnect_cmd, sizeof(reconnect_cmd));
|
||||
session.client_channel.send(0x19, 0x00, &reconnect_cmd, sizeof(reconnect_cmd));
|
||||
|
||||
return false;
|
||||
return HandlerResult::SUPPRESS;
|
||||
}
|
||||
|
||||
|
||||
|
||||
typedef bool (*process_command_t)(
|
||||
typedef HandlerResult (*process_command_t)(
|
||||
shared_ptr<ServerState> s,
|
||||
ProxyServer::LinkedSession& session,
|
||||
uint16_t command,
|
||||
@@ -1242,9 +1280,18 @@ void process_proxy_command(
|
||||
string& data) {
|
||||
auto fn = get_handler(session.version, from_server, command);
|
||||
try {
|
||||
bool should_forward = fn(s, session, command, flag, data);
|
||||
if (should_forward) {
|
||||
auto res = fn(s, session, command, flag, data);
|
||||
if (res == HandlerResult::FORWARD) {
|
||||
forward_command(session, !from_server, command, flag, data, false);
|
||||
} else if (res == HandlerResult::MODIFIED) {
|
||||
session.log(INFO, "The preceding command from the %s was modified in transit",
|
||||
from_server ? "server" : "client");
|
||||
forward_command(session, !from_server, command, flag, data);
|
||||
} else if (res == HandlerResult::SUPPRESS) {
|
||||
session.log(INFO, "The preceding command from the %s was not forwarded",
|
||||
from_server ? "server" : "client");
|
||||
} else {
|
||||
throw logic_error("invalid handler result");
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
session.log(ERROR, "Failed to process command: %s", e.what());
|
||||
|
||||
+176
-293
@@ -29,6 +29,7 @@
|
||||
#include "ProxyCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
using namespace std::placeholders;
|
||||
|
||||
|
||||
|
||||
@@ -37,13 +38,6 @@ static const uint32_t UNLICENSED_SESSION_TIMEOUT_USECS = 10 * 1000000; // 10 sec
|
||||
|
||||
|
||||
|
||||
static void flush_and_free_bufferevent(struct bufferevent* bev) {
|
||||
bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_FINISHED);
|
||||
bufferevent_free(bev);
|
||||
}
|
||||
|
||||
|
||||
|
||||
ProxyServer::ProxyServer(
|
||||
shared_ptr<struct event_base> base,
|
||||
shared_ptr<ServerState> state)
|
||||
@@ -134,7 +128,8 @@ void ProxyServer::connect_client(struct bufferevent* bev, uint16_t server_port)
|
||||
} catch (const out_of_range&) {
|
||||
this->log(INFO, "Virtual connection received on unregistered port %hu; closing it",
|
||||
server_port);
|
||||
flush_and_free_bufferevent(bev);
|
||||
bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_FINISHED);
|
||||
bufferevent_free(bev);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,7 +161,9 @@ void ProxyServer::on_client_connect(
|
||||
}
|
||||
auto session = emplace_ret.first->second;
|
||||
session->log(INFO, "Opened linked session");
|
||||
session->resume(bev);
|
||||
|
||||
Channel ch(bev, version, nullptr, nullptr, session.get(), "", TerminalFormat::FG_YELLOW, TerminalFormat::FG_GREEN);
|
||||
session->resume(move(ch));
|
||||
|
||||
// If no default destination exists, or the client is not a patch client,
|
||||
// create an unlinked session - we'll have to get the destination from the
|
||||
@@ -195,22 +192,15 @@ void ProxyServer::on_client_connect(
|
||||
uint32_t client_key = random_object<uint32_t>();
|
||||
auto cmd = prepare_server_init_contents_dc_pc_gc(
|
||||
false, server_key, client_key);
|
||||
send_command(
|
||||
session->bev.get(),
|
||||
session->version,
|
||||
session->crypt_out.get(),
|
||||
0x02,
|
||||
0,
|
||||
&cmd,
|
||||
sizeof(cmd),
|
||||
"unlinked proxy client");
|
||||
bufferevent_flush(session->bev.get(), EV_READ | EV_WRITE, BEV_FLUSH);
|
||||
session->channel.send(0x02, 0x00, &cmd, sizeof(cmd));
|
||||
// TODO: Is this actually needed?
|
||||
// bufferevent_flush(session->channel.bev.get(), EV_READ | EV_WRITE, BEV_FLUSH);
|
||||
if (version == GameVersion::PC) {
|
||||
session->crypt_out.reset(new PSOPCEncryption(server_key));
|
||||
session->crypt_in.reset(new PSOPCEncryption(client_key));
|
||||
session->channel.crypt_out.reset(new PSOPCEncryption(server_key));
|
||||
session->channel.crypt_in.reset(new PSOPCEncryption(client_key));
|
||||
} else {
|
||||
session->crypt_out.reset(new PSOGCEncryption(server_key));
|
||||
session->crypt_in.reset(new PSOGCEncryption(client_key));
|
||||
session->channel.crypt_out.reset(new PSOGCEncryption(server_key));
|
||||
session->channel.crypt_in.reset(new PSOGCEncryption(client_key));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -220,22 +210,14 @@ void ProxyServer::on_client_connect(
|
||||
random_data(server_key.data(), server_key.bytes());
|
||||
random_data(client_key.data(), client_key.bytes());
|
||||
auto cmd = prepare_server_init_contents_bb(server_key, client_key);
|
||||
send_command(
|
||||
session->bev.get(),
|
||||
session->version,
|
||||
session->crypt_out.get(),
|
||||
0x03,
|
||||
0,
|
||||
&cmd,
|
||||
sizeof(cmd),
|
||||
"unlinked proxy client");
|
||||
bufferevent_flush(session->bev.get(), EV_READ | EV_WRITE, BEV_FLUSH);
|
||||
|
||||
session->channel.send(0x03, 0x00, &cmd, sizeof(cmd));
|
||||
// TODO: Is this actually needed?
|
||||
// bufferevent_flush(session->bev.get(), EV_READ | EV_WRITE, BEV_FLUSH);
|
||||
static const string expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8);
|
||||
session->detector_crypt.reset(new PSOBBMultiKeyDetectorEncryption(
|
||||
this->state->bb_private_keys, expected_first_data, cmd.client_key.data(), sizeof(cmd.client_key)));
|
||||
session->crypt_in = session->detector_crypt;
|
||||
session->crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session->channel.crypt_in = session->detector_crypt;
|
||||
session->channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
session->detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), true));
|
||||
break;
|
||||
}
|
||||
@@ -251,27 +233,23 @@ ProxyServer::UnlinkedSession::UnlinkedSession(
|
||||
ProxyServer* server, struct bufferevent* bev, uint16_t local_port, GameVersion version)
|
||||
: server(server),
|
||||
log(string_printf("[ProxyServer:UnlinkedSession:%p] ", bev)),
|
||||
bev(bev, flush_and_free_bufferevent),
|
||||
channel(
|
||||
bev,
|
||||
version,
|
||||
ProxyServer::UnlinkedSession::on_input,
|
||||
ProxyServer::UnlinkedSession::on_error,
|
||||
this,
|
||||
string_printf("UnlinkedSession:%p", bev),
|
||||
TerminalFormat::FG_YELLOW,
|
||||
TerminalFormat::FG_GREEN),
|
||||
local_port(local_port),
|
||||
version(version) {
|
||||
memset(&this->next_destination, 0, sizeof(this->next_destination));
|
||||
bufferevent_setcb(this->bev.get(),
|
||||
&UnlinkedSession::dispatch_on_client_input, nullptr,
|
||||
&UnlinkedSession::dispatch_on_client_error, this);
|
||||
bufferevent_enable(this->bev.get(), EV_READ | EV_WRITE);
|
||||
}
|
||||
|
||||
void ProxyServer::UnlinkedSession::dispatch_on_client_input(
|
||||
struct bufferevent*, void* ctx) {
|
||||
reinterpret_cast<UnlinkedSession*>(ctx)->on_client_input();
|
||||
}
|
||||
void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint32_t, std::string& data) {
|
||||
auto* session = reinterpret_cast<UnlinkedSession*>(ch.context_obj);
|
||||
|
||||
void ProxyServer::UnlinkedSession::dispatch_on_client_error(
|
||||
struct bufferevent*, short events, void* ctx) {
|
||||
reinterpret_cast<UnlinkedSession*>(ctx)->on_client_error(events);
|
||||
}
|
||||
|
||||
void ProxyServer::UnlinkedSession::on_client_input() {
|
||||
bool should_close_unlinked_session = false;
|
||||
shared_ptr<const License> license;
|
||||
uint32_t sub_version = 0;
|
||||
@@ -280,58 +258,54 @@ void ProxyServer::UnlinkedSession::on_client_input() {
|
||||
string login_command_bb;
|
||||
|
||||
try {
|
||||
for_each_received_command(this->bev.get(), this->version, this->crypt_in.get(),
|
||||
[&](uint16_t command, uint32_t flag, const string& data) {
|
||||
print_received_command(command, flag, data.data(), data.size(),
|
||||
this->version, "unlinked proxy client");
|
||||
if (session->version == GameVersion::PC) {
|
||||
// We should only get a 9D while the session is unlinked; if we get
|
||||
// anything else, disconnect
|
||||
if (command != 0x9D) {
|
||||
throw runtime_error("command is not 9D");
|
||||
}
|
||||
const auto& cmd = check_size_t<C_Login_PC_9D>(
|
||||
data, sizeof(C_Login_PC_9D), sizeof(C_LoginWithUnusedSpace_PC_9D));
|
||||
license = session->server->state->license_manager->verify_pc(
|
||||
stoul(cmd.serial_number, nullptr, 16), cmd.access_key);
|
||||
sub_version = cmd.sub_version;
|
||||
character_name = cmd.name;
|
||||
|
||||
if (this->version == GameVersion::PC) {
|
||||
// We should only get a 9D while the session is unlinked; if we get
|
||||
// anything else, disconnect
|
||||
if (command != 0x9D) {
|
||||
throw runtime_error("command is not 9D");
|
||||
}
|
||||
const auto& cmd = check_size_t<C_Login_PC_9D>(data, sizeof(C_Login_PC_9D), sizeof(C_LoginWithUnusedSpace_PC_9D));
|
||||
license = this->server->state->license_manager->verify_pc(
|
||||
stoul(cmd.serial_number, nullptr, 16), cmd.access_key);
|
||||
sub_version = cmd.sub_version;
|
||||
character_name = cmd.name;
|
||||
} else if (session->version == GameVersion::GC) {
|
||||
// We should only get a 9E while the session is unlinked; if we get
|
||||
// anything else, disconnect
|
||||
if (command != 0x9E) {
|
||||
throw runtime_error("command is not 9E");
|
||||
}
|
||||
const auto& cmd = check_size_t<C_Login_GC_9E>(
|
||||
data, sizeof(C_Login_GC_9E), sizeof(C_LoginWithUnusedSpace_GC_9E));
|
||||
license = session->server->state->license_manager->verify_gc(
|
||||
stoul(cmd.serial_number, nullptr, 16), cmd.access_key);
|
||||
sub_version = cmd.sub_version;
|
||||
character_name = cmd.name;
|
||||
client_config.cfg = cmd.client_config.cfg;
|
||||
|
||||
} else if (this->version == GameVersion::GC) {
|
||||
// We should only get a 9E while the session is unlinked; if we get
|
||||
// anything else, disconnect
|
||||
if (command != 0x9E) {
|
||||
throw runtime_error("command is not 9E");
|
||||
}
|
||||
const auto& cmd = check_size_t<C_Login_GC_9E>(data, sizeof(C_Login_GC_9E), sizeof(C_LoginWithUnusedSpace_GC_9E));
|
||||
license = this->server->state->license_manager->verify_gc(
|
||||
stoul(cmd.serial_number, nullptr, 16), cmd.access_key);
|
||||
sub_version = cmd.sub_version;
|
||||
character_name = cmd.name;
|
||||
client_config.cfg = cmd.client_config.cfg;
|
||||
} else if (session->version == GameVersion::BB) {
|
||||
// We should only get a 93 while the session is unlinked; if we get
|
||||
// anything else, disconnect
|
||||
if (command != 0x93) {
|
||||
throw runtime_error("command is not 93");
|
||||
}
|
||||
const auto& cmd = check_size_t<C_Login_BB_93>(data);
|
||||
license = session->server->state->license_manager->verify_bb(
|
||||
cmd.username, cmd.password);
|
||||
login_command_bb = move(data);
|
||||
|
||||
} else if (this->version == GameVersion::BB) {
|
||||
// We should only get a 93 while the session is unlinked; if we get
|
||||
// anything else, disconnect
|
||||
if (command != 0x93) {
|
||||
throw runtime_error("command is not 93");
|
||||
}
|
||||
const auto& cmd = check_size_t<C_Login_BB_93>(data);
|
||||
license = this->server->state->license_manager->verify_bb(
|
||||
cmd.username, cmd.password);
|
||||
login_command_bb = data;
|
||||
|
||||
} else {
|
||||
throw logic_error("unsupported unlinked session version");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw logic_error("unsupported unlinked session version");
|
||||
}
|
||||
|
||||
} catch (const exception& e) {
|
||||
this->log(ERROR, "Failed to process command from unlinked client: %s", e.what());
|
||||
session->log(ERROR, "Failed to process command from unlinked client: %s", e.what());
|
||||
should_close_unlinked_session = true;
|
||||
}
|
||||
|
||||
struct bufferevent* session_key = this->bev.get();
|
||||
struct bufferevent* session_key = ch.bev.get();
|
||||
|
||||
// If license is non-null, then the client has a password and can be connected
|
||||
// to the remote lobby server.
|
||||
@@ -341,75 +315,80 @@ void ProxyServer::UnlinkedSession::on_client_input() {
|
||||
should_close_unlinked_session = true;
|
||||
|
||||
// Look up the linked session for this license (if any)
|
||||
shared_ptr<LinkedSession> session;
|
||||
shared_ptr<LinkedSession> linked_session;
|
||||
try {
|
||||
session = this->server->id_to_session.at(license->serial_number);
|
||||
session->log(INFO, "Resuming linked session from unlinked session");
|
||||
linked_session = session->server->id_to_session.at(license->serial_number);
|
||||
linked_session->log(INFO, "Resuming linked session from unlinked session");
|
||||
|
||||
} catch (const out_of_range&) {
|
||||
// If there's no open session for this license, then there must be a valid
|
||||
// destination somewhere - either in the client config or in the unlinked
|
||||
// session
|
||||
if (client_config.cfg.magic == CLIENT_CONFIG_MAGIC) {
|
||||
session.reset(new LinkedSession(
|
||||
this->server,
|
||||
this->local_port,
|
||||
this->version,
|
||||
linked_session.reset(new LinkedSession(
|
||||
session->server,
|
||||
session->local_port,
|
||||
session->version,
|
||||
license,
|
||||
client_config));
|
||||
session->log(INFO, "Opened licensed session for unlinked session based on client config");
|
||||
} else if (this->next_destination.ss_family == AF_INET) {
|
||||
session.reset(new LinkedSession(
|
||||
this->server,
|
||||
this->local_port,
|
||||
this->version,
|
||||
linked_session->log(INFO, "Opened licensed session for unlinked session based on client config");
|
||||
} else if (session->next_destination.ss_family == AF_INET) {
|
||||
linked_session.reset(new LinkedSession(
|
||||
session->server,
|
||||
session->local_port,
|
||||
session->version,
|
||||
license,
|
||||
this->next_destination));
|
||||
session->log(INFO, "Opened licensed session for unlinked session based on unlinked default destination");
|
||||
session->next_destination));
|
||||
linked_session->log(INFO, "Opened licensed session for unlinked session based on unlinked default destination");
|
||||
} else {
|
||||
this->log(ERROR, "Cannot open linked session: no valid destination in client config or unlinked session");
|
||||
session->log(ERROR, "Cannot open linked session: no valid destination in client config or unlinked session");
|
||||
}
|
||||
}
|
||||
|
||||
if (session.get()) {
|
||||
this->server->id_to_session.emplace(license->serial_number, session);
|
||||
if (session->version != this->version) {
|
||||
session->log(ERROR, "Linked session has different game version");
|
||||
if (linked_session.get()) {
|
||||
session->server->id_to_session.emplace(license->serial_number, linked_session);
|
||||
if (linked_session->version != session->version) {
|
||||
linked_session->log(ERROR, "Linked session has different game version");
|
||||
} else {
|
||||
// Resume the linked session using the unlinked session
|
||||
try {
|
||||
if (this->version == GameVersion::BB) {
|
||||
session->resume(move(this->bev), this->crypt_in, this->crypt_out,
|
||||
this->detector_crypt, move(login_command_bb));
|
||||
if (session->version == GameVersion::BB) {
|
||||
linked_session->resume(
|
||||
move(session->channel),
|
||||
session->detector_crypt,
|
||||
move(login_command_bb));
|
||||
} else {
|
||||
session->resume(move(this->bev), this->crypt_in, this->crypt_out,
|
||||
this->detector_crypt, sub_version, character_name);
|
||||
linked_session->resume(
|
||||
move(session->channel),
|
||||
session->detector_crypt,
|
||||
sub_version,
|
||||
character_name);
|
||||
}
|
||||
this->crypt_in.reset();
|
||||
this->crypt_out.reset();
|
||||
} catch (const exception& e) {
|
||||
session->log(ERROR, "Failed to resume linked session: %s", e.what());
|
||||
linked_session->log(ERROR, "Failed to resume linked session: %s", e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (should_close_unlinked_session) {
|
||||
this->log(INFO, "Closing session");
|
||||
this->server->bev_to_unlinked_session.erase(session_key);
|
||||
session->log(INFO, "Closing session");
|
||||
session->server->bev_to_unlinked_session.erase(session_key);
|
||||
// At this point, (*this) is destroyed! We must be careful not to touch it.
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyServer::UnlinkedSession::on_client_error(short events) {
|
||||
void ProxyServer::UnlinkedSession::on_error(Channel& ch, short events) {
|
||||
auto* session = reinterpret_cast<UnlinkedSession*>(ch.context_obj);
|
||||
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
this->log(WARNING, "Error %d (%s) in unlinked client stream", err,
|
||||
session->log(WARNING, "Error %d (%s) in unlinked client stream", err,
|
||||
evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) {
|
||||
this->log(WARNING, "Unlinked client has disconnected");
|
||||
this->server->bev_to_unlinked_session.erase(this->bev.get());
|
||||
session->log(WARNING, "Unlinked client has disconnected");
|
||||
session->server->bev_to_unlinked_session.erase(session->channel.bev.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,23 +401,34 @@ ProxyServer::LinkedSession::LinkedSession(
|
||||
GameVersion version)
|
||||
: server(server),
|
||||
id(id),
|
||||
client_name(string_printf("LinkedSession:%08" PRIX64 ":client", this->id)),
|
||||
server_name(string_printf("LinkedSession:%08" PRIX64 ":server", this->id)),
|
||||
log(string_printf("[ProxyServer:LinkedSession:%08" PRIX64 "] ", this->id)),
|
||||
timeout_event(event_new(this->server->base.get(), -1, EV_TIMEOUT,
|
||||
&LinkedSession::dispatch_on_timeout, this), event_free),
|
||||
license(nullptr),
|
||||
client_bev(nullptr, flush_and_free_bufferevent),
|
||||
server_bev(nullptr, flush_and_free_bufferevent),
|
||||
client_channel(
|
||||
version,
|
||||
nullptr,
|
||||
nullptr,
|
||||
this,
|
||||
string_printf("LinkedSession:%08" PRIX64 ":client", this->id),
|
||||
TerminalFormat::FG_YELLOW,
|
||||
TerminalFormat::FG_GREEN),
|
||||
server_channel(
|
||||
version,
|
||||
nullptr,
|
||||
nullptr,
|
||||
this,
|
||||
string_printf("LinkedSession:%08" PRIX64 ":server", this->id),
|
||||
TerminalFormat::FG_YELLOW,
|
||||
TerminalFormat::FG_RED),
|
||||
local_port(local_port),
|
||||
remote_ip_crc(0),
|
||||
enable_remote_ip_crc_patch(false),
|
||||
version(version),
|
||||
sub_version(0), // This is set during resume()
|
||||
remote_guild_card_number(0),
|
||||
suppress_newserv_commands(true),
|
||||
enable_chat_filter(true),
|
||||
enable_switch_assist(false),
|
||||
switch_assist(false),
|
||||
infinite_hp(false),
|
||||
infinite_tp(false),
|
||||
save_files(false),
|
||||
@@ -490,60 +480,48 @@ ProxyServer::LinkedSession::LinkedSession(
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::resume(
|
||||
unique_ptr<struct bufferevent, void(*)(struct bufferevent*)>&& client_bev,
|
||||
shared_ptr<PSOEncryption> client_input_crypt,
|
||||
shared_ptr<PSOEncryption> client_output_crypt,
|
||||
Channel&& client_channel,
|
||||
shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
|
||||
uint32_t sub_version,
|
||||
const string& character_name) {
|
||||
this->sub_version = sub_version;
|
||||
this->character_name = character_name;
|
||||
this->resume_inner(move(client_bev), client_input_crypt, client_output_crypt,
|
||||
detector_crypt);
|
||||
this->resume_inner(move(client_channel), detector_crypt);
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::resume(
|
||||
unique_ptr<struct bufferevent, void(*)(struct bufferevent*)>&& client_bev,
|
||||
shared_ptr<PSOEncryption> client_input_crypt,
|
||||
shared_ptr<PSOEncryption> client_output_crypt,
|
||||
Channel&& client_channel,
|
||||
shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
|
||||
string&& login_command_bb) {
|
||||
this->login_command_bb = move(login_command_bb);
|
||||
this->resume_inner(move(client_bev), client_input_crypt, client_output_crypt,
|
||||
detector_crypt);
|
||||
this->resume_inner(move(client_channel), detector_crypt);
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::resume(struct bufferevent* client_bev) {
|
||||
unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> bev_unique(
|
||||
client_bev, flush_and_free_bufferevent);
|
||||
void ProxyServer::LinkedSession::resume(Channel&& client_channel) {
|
||||
this->sub_version = 0;
|
||||
this->character_name.clear();
|
||||
this->resume_inner(move(bev_unique), nullptr, nullptr, nullptr);
|
||||
this->resume_inner(move(client_channel), nullptr);
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::resume_inner(
|
||||
unique_ptr<struct bufferevent, void(*)(struct bufferevent*)>&& client_bev,
|
||||
shared_ptr<PSOEncryption> client_input_crypt,
|
||||
shared_ptr<PSOEncryption> client_output_crypt,
|
||||
Channel&& client_channel,
|
||||
shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt) {
|
||||
if (this->client_bev.get()) {
|
||||
if (this->client_channel.connected()) {
|
||||
throw runtime_error("client connection is already open for this session");
|
||||
}
|
||||
if (this->next_destination.ss_family != AF_INET) {
|
||||
throw logic_error("attempted to resume an unlicensed linked session without destination set");
|
||||
}
|
||||
|
||||
this->client_bev = move(client_bev);
|
||||
bufferevent_setcb(this->client_bev.get(),
|
||||
&ProxyServer::LinkedSession::dispatch_on_client_input, nullptr,
|
||||
&ProxyServer::LinkedSession::dispatch_on_client_error, this);
|
||||
bufferevent_enable(this->client_bev.get(), EV_READ | EV_WRITE);
|
||||
this->client_channel.replace_with(
|
||||
move(client_channel),
|
||||
ProxyServer::LinkedSession::on_input,
|
||||
ProxyServer::LinkedSession::on_error,
|
||||
this,
|
||||
string_printf("LinkedSession:%08" PRIX64 ":client", this->id));
|
||||
|
||||
this->detector_crypt = detector_crypt;
|
||||
this->client_input_crypt = client_input_crypt;
|
||||
this->client_output_crypt = client_output_crypt;
|
||||
this->server_input_crypt.reset();
|
||||
this->server_output_crypt.reset();
|
||||
this->server_channel.disconnect();
|
||||
this->saving_files.clear();
|
||||
|
||||
this->connect();
|
||||
@@ -552,9 +530,6 @@ void ProxyServer::LinkedSession::resume_inner(
|
||||
void ProxyServer::LinkedSession::connect() {
|
||||
// Connect to the remote server. The command handlers will do the login steps
|
||||
// and set up forwarding
|
||||
this->server_bev.reset(bufferevent_socket_new(this->server->base.get(), -1,
|
||||
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS));
|
||||
|
||||
struct sockaddr_storage local_ss;
|
||||
struct sockaddr_in* local_sin = reinterpret_cast<struct sockaddr_in*>(&local_ss);
|
||||
memset(local_sin, 0, sizeof(*local_sin));
|
||||
@@ -568,14 +543,17 @@ void ProxyServer::LinkedSession::connect() {
|
||||
|
||||
string netloc_str = render_sockaddr_storage(local_ss);
|
||||
this->log(INFO, "Connecting to %s", netloc_str.c_str());
|
||||
if (bufferevent_socket_connect(this->server_bev.get(),
|
||||
|
||||
this->server_channel.set_bufferevent(bufferevent_socket_new(
|
||||
this->server->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS));
|
||||
if (bufferevent_socket_connect(this->server_channel.bev.get(),
|
||||
reinterpret_cast<const sockaddr*>(local_sin), sizeof(*local_sin)) != 0) {
|
||||
throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR()));
|
||||
}
|
||||
bufferevent_setcb(this->server_bev.get(),
|
||||
&ProxyServer::LinkedSession::dispatch_on_server_input, nullptr,
|
||||
&ProxyServer::LinkedSession::dispatch_on_server_error, this);
|
||||
bufferevent_enable(this->server_bev.get(), EV_READ | EV_WRITE);
|
||||
|
||||
this->server_channel.on_command_received = ProxyServer::LinkedSession::on_input;
|
||||
this->server_channel.on_error = ProxyServer::LinkedSession::on_error;
|
||||
this->server_channel.context_obj = this;
|
||||
|
||||
// Cancel the session delete timeout
|
||||
event_del(this->timeout_event.get());
|
||||
@@ -594,26 +572,6 @@ ProxyServer::LinkedSession::SavingFile::SavingFile(
|
||||
|
||||
|
||||
|
||||
void ProxyServer::LinkedSession::dispatch_on_client_input(
|
||||
struct bufferevent*, void* ctx) {
|
||||
reinterpret_cast<LinkedSession*>(ctx)->on_client_input();
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::dispatch_on_client_error(
|
||||
struct bufferevent*, short events, void* ctx) {
|
||||
reinterpret_cast<LinkedSession*>(ctx)->on_stream_error(events, false);
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::dispatch_on_server_input(
|
||||
struct bufferevent*, void* ctx) {
|
||||
reinterpret_cast<LinkedSession*>(ctx)->on_server_input();
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::dispatch_on_server_error(
|
||||
struct bufferevent*, short events, void* ctx) {
|
||||
reinterpret_cast<LinkedSession*>(ctx)->on_stream_error(events, true);
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::dispatch_on_timeout(
|
||||
evutil_socket_t, short, void* ctx) {
|
||||
reinterpret_cast<LinkedSession*>(ctx)->on_timeout();
|
||||
@@ -628,31 +586,27 @@ void ProxyServer::LinkedSession::on_timeout() {
|
||||
|
||||
|
||||
|
||||
void ProxyServer::LinkedSession::on_stream_error(
|
||||
short events, bool is_server_stream) {
|
||||
void ProxyServer::LinkedSession::on_error(Channel& ch, short events) {
|
||||
auto* session = reinterpret_cast<LinkedSession*>(ch.context_obj);
|
||||
bool is_server_stream = (&ch == &session->server_channel);
|
||||
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
this->log(WARNING, "Error %d (%s) in %s stream",
|
||||
session->log(WARNING, "Error %d (%s) in %s stream",
|
||||
err, evutil_socket_error_to_string(err),
|
||||
is_server_stream ? "server" : "client");
|
||||
}
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
|
||||
this->log(INFO, "%s has disconnected",
|
||||
session->log(INFO, "%s has disconnected",
|
||||
is_server_stream ? "Server" : "Client");
|
||||
this->disconnect();
|
||||
session->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::disconnect() {
|
||||
// Forward the disconnection to the other end
|
||||
this->server_bev.reset();
|
||||
this->client_bev.reset();
|
||||
|
||||
// Disable encryption for the next connection
|
||||
this->server_input_crypt.reset();
|
||||
this->server_output_crypt.reset();
|
||||
this->client_input_crypt.reset();
|
||||
this->client_output_crypt.reset();
|
||||
this->client_channel.disconnect();
|
||||
this->server_channel.disconnect();
|
||||
|
||||
// Set a timeout to delete the session entirely (in case the client doesn't
|
||||
// reconnect)
|
||||
@@ -662,105 +616,34 @@ void ProxyServer::LinkedSession::disconnect() {
|
||||
}
|
||||
|
||||
bool ProxyServer::LinkedSession::is_connected() const {
|
||||
return (this->server_bev.get() && this->client_bev.get());
|
||||
return (this->server_channel.connected() && this->client_channel.connected());
|
||||
}
|
||||
|
||||
|
||||
|
||||
void ProxyServer::LinkedSession::on_client_input() {
|
||||
void ProxyServer::LinkedSession::on_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data) {
|
||||
auto* session = reinterpret_cast<LinkedSession*>(ch.context_obj);
|
||||
bool is_server_stream = (&ch == &session->server_channel);
|
||||
|
||||
try {
|
||||
for_each_received_command(this->client_bev.get(), this->version, this->client_input_crypt.get(),
|
||||
[&](uint16_t command, uint32_t flag, string& data) {
|
||||
print_received_command(command, flag, data.data(), data.size(),
|
||||
this->version, this->client_name.c_str());
|
||||
process_proxy_command(
|
||||
this->server->state,
|
||||
*this,
|
||||
false, // from_server
|
||||
command,
|
||||
flag,
|
||||
data);
|
||||
});
|
||||
if (is_server_stream) {
|
||||
size_t bytes_to_save = min<size_t>(data.size(), sizeof(session->prev_server_command_bytes));
|
||||
memcpy(session->prev_server_command_bytes, data.data(), bytes_to_save);
|
||||
}
|
||||
process_proxy_command(
|
||||
session->server->state,
|
||||
*session,
|
||||
is_server_stream,
|
||||
command,
|
||||
flag,
|
||||
data);
|
||||
} catch (const exception& e) {
|
||||
this->log(ERROR, "Failed to process command from client: %s", e.what());
|
||||
this->disconnect();
|
||||
session->log(ERROR, "Failed to process command from %s: %s",
|
||||
is_server_stream ? "server" : "client", e.what());
|
||||
session->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::on_server_input() {
|
||||
try {
|
||||
for_each_received_command(this->server_bev.get(), this->version, this->server_input_crypt.get(),
|
||||
[&](uint16_t command, uint32_t flag, string& data) {
|
||||
print_received_command(command, flag, data.data(), data.size(),
|
||||
this->version, this->server_name.c_str(), TerminalFormat::FG_RED);
|
||||
size_t bytes_to_save = min<size_t>(data.size(), sizeof(this->prev_server_command_bytes));
|
||||
memcpy(this->prev_server_command_bytes, data.data(), bytes_to_save);
|
||||
process_proxy_command(
|
||||
this->server->state,
|
||||
*this,
|
||||
true, // from_server
|
||||
command,
|
||||
flag,
|
||||
data);
|
||||
});
|
||||
} catch (const exception& e) {
|
||||
this->log(ERROR, "Failed to process command from server: %s", e.what());
|
||||
this->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::send_to_end(
|
||||
bool to_server,
|
||||
uint16_t command,
|
||||
uint32_t flag,
|
||||
const void* data,
|
||||
size_t size) {
|
||||
if (size & 3) {
|
||||
throw runtime_error("command size is not a multiple of 4");
|
||||
}
|
||||
string name = string_printf("LinkedSession:%08" PRIX64 ":synthetic:%s",
|
||||
this->id, to_server ? "server" : "client");
|
||||
|
||||
auto* bev = to_server ? this->server_bev.get() : this->client_bev.get();
|
||||
if (!bev) {
|
||||
throw runtime_error("session endpoint is not connected");
|
||||
}
|
||||
send_command(
|
||||
bev,
|
||||
this->version,
|
||||
to_server ? this->server_output_crypt.get() : this->client_output_crypt.get(),
|
||||
command,
|
||||
flag,
|
||||
data,
|
||||
size,
|
||||
name.c_str());
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::send_to_end(
|
||||
bool to_server, uint16_t command, uint32_t flag, const string& data) {
|
||||
this->send_to_end(to_server, command, flag, data.data(), data.size());
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::send_to_end_with_header(
|
||||
bool to_server, const void* data, size_t size) {
|
||||
size_t header_size = PSOCommandHeader::header_size(this->version);
|
||||
if (size < header_size) {
|
||||
throw runtime_error("command is too small for header");
|
||||
}
|
||||
const auto* header = reinterpret_cast<const PSOCommandHeader*>(data);
|
||||
this->send_to_end(
|
||||
to_server,
|
||||
header->command(this->version),
|
||||
header->flag(this->version),
|
||||
reinterpret_cast<const uint8_t*>(data) + header_size,
|
||||
size - header_size);
|
||||
}
|
||||
|
||||
void ProxyServer::LinkedSession::send_to_end_with_header(
|
||||
bool to_server, const string& data) {
|
||||
this->send_to_end_with_header(to_server, data.data(), data.size());
|
||||
}
|
||||
|
||||
shared_ptr<ProxyServer::LinkedSession> ProxyServer::get_session() {
|
||||
if (this->id_to_session.empty()) {
|
||||
throw runtime_error("no sessions exist");
|
||||
|
||||
+12
-45
@@ -35,16 +35,14 @@ public:
|
||||
struct LinkedSession {
|
||||
ProxyServer* server;
|
||||
uint64_t id;
|
||||
std::string client_name;
|
||||
std::string server_name;
|
||||
PrefixedLogger log;
|
||||
|
||||
std::unique_ptr<struct event, void(*)(struct event*)> timeout_event;
|
||||
|
||||
std::shared_ptr<const License> license;
|
||||
|
||||
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> client_bev;
|
||||
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> server_bev;
|
||||
Channel client_channel;
|
||||
Channel server_channel;
|
||||
uint16_t local_port;
|
||||
struct sockaddr_storage next_destination;
|
||||
|
||||
@@ -60,9 +58,8 @@ public:
|
||||
uint32_t remote_guild_card_number;
|
||||
parray<uint8_t, 0x20> remote_client_config_data;
|
||||
ClientConfigBB newserv_client_config;
|
||||
bool suppress_newserv_commands;
|
||||
bool enable_chat_filter;
|
||||
bool enable_switch_assist;
|
||||
bool switch_assist;
|
||||
bool infinite_hp;
|
||||
bool infinite_tp;
|
||||
bool save_files;
|
||||
@@ -80,10 +77,6 @@ public:
|
||||
std::vector<LobbyPlayer> lobby_players;
|
||||
size_t lobby_client_id;
|
||||
|
||||
std::shared_ptr<PSOEncryption> client_input_crypt;
|
||||
std::shared_ptr<PSOEncryption> client_output_crypt;
|
||||
std::shared_ptr<PSOEncryption> server_input_crypt;
|
||||
std::shared_ptr<PSOEncryption> server_output_crypt;
|
||||
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt;
|
||||
|
||||
struct SavingFile {
|
||||
@@ -125,46 +118,25 @@ public:
|
||||
const struct sockaddr_storage& next_destination);
|
||||
|
||||
void resume(
|
||||
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)>&& client_bev,
|
||||
std::shared_ptr<PSOEncryption> client_input_crypt,
|
||||
std::shared_ptr<PSOEncryption> client_output_crypt,
|
||||
Channel&& client_channel,
|
||||
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
|
||||
uint32_t sub_version,
|
||||
const std::string& character_name);
|
||||
void resume(
|
||||
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)>&& client_bev,
|
||||
std::shared_ptr<PSOEncryption> client_input_crypt,
|
||||
std::shared_ptr<PSOEncryption> client_output_crypt,
|
||||
Channel&& client_channel,
|
||||
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt,
|
||||
std::string&& login_command_bb);
|
||||
void resume(struct bufferevent* client_bev);
|
||||
void resume(Channel&& client_channel);
|
||||
void resume_inner(
|
||||
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)>&& client_bev,
|
||||
std::shared_ptr<PSOEncryption> client_input_crypt,
|
||||
std::shared_ptr<PSOEncryption> client_output_crypt,
|
||||
Channel&& client_channel,
|
||||
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt);
|
||||
void connect();
|
||||
|
||||
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
|
||||
static void dispatch_on_client_error(struct bufferevent* bev, short events,
|
||||
void* ctx);
|
||||
static void dispatch_on_server_input(struct bufferevent* bev, void* ctx);
|
||||
static void dispatch_on_server_error(struct bufferevent* bev, short events,
|
||||
void* ctx);
|
||||
static void dispatch_on_timeout(evutil_socket_t fd, short what, void* ctx);
|
||||
void on_client_input();
|
||||
void on_server_input();
|
||||
void on_stream_error(short events, bool is_server_stream);
|
||||
static void on_input(Channel& ch, uint16_t, uint32_t, std::string& msg);
|
||||
static void on_error(Channel& ch, short events);
|
||||
void on_timeout();
|
||||
|
||||
void send_to_end(bool to_server, uint16_t command, uint32_t flag,
|
||||
const void* data = nullptr, size_t size = 0);
|
||||
void send_to_end(bool to_server, uint16_t command, uint32_t flag,
|
||||
const std::string& data);
|
||||
void send_to_end_with_header(
|
||||
bool to_server, const void* data, size_t size);
|
||||
void send_to_end_with_header(bool to_server, const std::string& data);
|
||||
|
||||
void disconnect();
|
||||
|
||||
bool is_connected() const;
|
||||
@@ -208,24 +180,19 @@ private:
|
||||
ProxyServer* server;
|
||||
|
||||
PrefixedLogger log;
|
||||
std::unique_ptr<struct bufferevent, void(*)(struct bufferevent*)> bev;
|
||||
Channel channel;
|
||||
uint16_t local_port;
|
||||
GameVersion version;
|
||||
struct sockaddr_storage next_destination;
|
||||
|
||||
std::shared_ptr<PSOEncryption> crypt_out;
|
||||
std::shared_ptr<PSOEncryption> crypt_in;
|
||||
std::shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt;
|
||||
|
||||
UnlinkedSession(ProxyServer* server, struct bufferevent* bev, uint16_t port, GameVersion version);
|
||||
|
||||
void receive_and_process_commands();
|
||||
|
||||
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
|
||||
static void dispatch_on_client_error(struct bufferevent* bev, short events,
|
||||
void* ctx);
|
||||
void on_client_input();
|
||||
void on_client_error(short events);
|
||||
static void on_input(Channel& ch, uint16_t command, uint32_t flag, std::string& msg);
|
||||
static void on_error(Channel& ch, short events);
|
||||
};
|
||||
|
||||
PrefixedLogger log;
|
||||
|
||||
+22
-21
@@ -1334,6 +1334,11 @@ void process_player_data(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
throw logic_error("player data command not implemented for version");
|
||||
}
|
||||
|
||||
auto player = c->game_data.player(false);
|
||||
if (player) {
|
||||
c->channel.name = remove_language_marker(encode_sjis(player->disp.name));
|
||||
}
|
||||
|
||||
if (command == 0x61 && !c->pending_bb_save_username.empty()) {
|
||||
string prev_bb_username = c->game_data.bb_username;
|
||||
size_t prev_bb_player_index = c->game_data.bb_player_index;
|
||||
@@ -1390,37 +1395,35 @@ void process_game_command(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
void process_chat_generic(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
const u16string& text) { // 06
|
||||
|
||||
if (!c->can_chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
u16string processed_text = remove_language_marker(text);
|
||||
if (processed_text.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto l = s->find_lobby(c->lobby_id);
|
||||
if (!l) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (processed_text[0] == L'$') {
|
||||
auto l = s->find_lobby(c->lobby_id);
|
||||
if (l) {
|
||||
if (processed_text[1] == L'$') {
|
||||
processed_text = processed_text.substr(1);
|
||||
} else {
|
||||
process_chat_command(s, l, c, processed_text);
|
||||
}
|
||||
} else {
|
||||
if (!c->can_chat) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto l = s->find_lobby(c->lobby_id);
|
||||
if (!l) {
|
||||
return;
|
||||
}
|
||||
if (!c->can_chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t x = 0; x < l->max_clients; x++) {
|
||||
if (!l->clients[x]) {
|
||||
continue;
|
||||
}
|
||||
send_chat_message(l->clients[x], c->license->serial_number,
|
||||
c->game_data.player()->disp.name.data(), processed_text.c_str());
|
||||
for (size_t x = 0; x < l->max_clients; x++) {
|
||||
if (!l->clients[x]) {
|
||||
continue;
|
||||
}
|
||||
send_chat_message(l->clients[x], c->license->serial_number,
|
||||
c->game_data.player()->disp.name.data(), processed_text.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2451,8 +2454,6 @@ void process_command(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
if (player) {
|
||||
encoded_name = remove_language_marker(encode_sjis(player->disp.name));
|
||||
}
|
||||
print_received_command(command, flag, data.data(), data.size(), c->version,
|
||||
encoded_name.c_str());
|
||||
|
||||
auto fn = handlers[static_cast<size_t>(c->version)][command & 0xFF];
|
||||
if (fn) {
|
||||
|
||||
+60
-142
@@ -25,119 +25,9 @@ extern FileContentsCache file_cache;
|
||||
|
||||
|
||||
|
||||
void send_command(
|
||||
struct bufferevent* bev,
|
||||
GameVersion version,
|
||||
PSOEncryption* crypt,
|
||||
uint16_t command,
|
||||
uint32_t flag,
|
||||
const void* data,
|
||||
size_t size,
|
||||
const char* name_str) {
|
||||
string send_data;
|
||||
size_t logical_size;
|
||||
|
||||
switch (version) {
|
||||
case GameVersion::GC:
|
||||
case GameVersion::DC: {
|
||||
PSOCommandHeaderDCGC header;
|
||||
header.command = command;
|
||||
header.flag = flag;
|
||||
header.size = (sizeof(header) + size + 3) & ~3;
|
||||
send_data.append(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
if (size) {
|
||||
send_data.append(reinterpret_cast<const char*>(data), size);
|
||||
send_data.resize(header.size, '\0');
|
||||
}
|
||||
logical_size = header.size;
|
||||
break;
|
||||
}
|
||||
|
||||
case GameVersion::PC:
|
||||
case GameVersion::PATCH: {
|
||||
PSOCommandHeaderPC header;
|
||||
header.size = (sizeof(header) + size + 3) & ~3;
|
||||
header.command = command;
|
||||
header.flag = flag;
|
||||
send_data.append(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
if (size) {
|
||||
send_data.append(reinterpret_cast<const char*>(data), size);
|
||||
send_data.resize(header.size, '\0');
|
||||
}
|
||||
logical_size = header.size;
|
||||
break;
|
||||
}
|
||||
|
||||
case GameVersion::BB: {
|
||||
// BB has an annoying behavior here: command lengths must be multiples of
|
||||
// 4, but the actual data length must be a multiple of 8. If the size
|
||||
// field is not divisible by 8, 4 extra bytes are sent anyway. This
|
||||
// behavior only applies when encryption is enabled - any commands sent
|
||||
// before encryption is enabled have no size restrictions (except they
|
||||
// must include a full header and must fit in the client's receive
|
||||
// buffer), and no implicit extra bytes are sent.
|
||||
PSOCommandHeaderBB header;
|
||||
header.size = (sizeof(header) + size + 3) & ~3;
|
||||
header.command = command;
|
||||
header.flag = flag;
|
||||
send_data.append(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
if (size) {
|
||||
send_data.append(reinterpret_cast<const char*>(data), size);
|
||||
if (crypt) {
|
||||
send_data.resize((send_data.size() + 7) & ~7, '\0');
|
||||
} else {
|
||||
send_data.resize(header.size, '\0');
|
||||
}
|
||||
}
|
||||
logical_size = header.size;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw logic_error("unimplemented game version in send_command");
|
||||
}
|
||||
|
||||
// Most client versions I've seen have a receive buffer 0x7C00 bytes in size
|
||||
if (send_data.size() > 0x7C00) {
|
||||
throw runtime_error("outbound command too large");
|
||||
}
|
||||
|
||||
if (name_str) {
|
||||
string name_token;
|
||||
if (name_str[0]) {
|
||||
name_token = string(" to ") + name_str;
|
||||
}
|
||||
if (use_terminal_colors) {
|
||||
print_color_escape(stderr, TerminalFormat::FG_YELLOW, TerminalFormat::BOLD, TerminalFormat::END);
|
||||
}
|
||||
log(INFO, "Sending%s (version=%s command=%04hX flag=%08X)",
|
||||
name_token.c_str(), name_for_version(version), command, flag);
|
||||
print_data(stderr, send_data.data(), logical_size, 0, nullptr, PrintDataFlags::PRINT_ASCII | PrintDataFlags::DISABLE_COLOR);
|
||||
if (use_terminal_colors) {
|
||||
print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END);
|
||||
}
|
||||
}
|
||||
|
||||
if (crypt) {
|
||||
crypt->encrypt(send_data.data(), send_data.size());
|
||||
}
|
||||
|
||||
struct evbuffer* buf = bufferevent_get_output(bev);
|
||||
evbuffer_add(buf, send_data.data(), send_data.size());
|
||||
}
|
||||
|
||||
void send_command(shared_ptr<Client> c, uint16_t command, uint32_t flag,
|
||||
const void* data, size_t size) {
|
||||
if (!c->bev) {
|
||||
return;
|
||||
}
|
||||
string encoded_name;
|
||||
auto player = c->game_data.player(false);
|
||||
if (player) {
|
||||
encoded_name = remove_language_marker(encode_sjis(player->disp.name));
|
||||
}
|
||||
send_command(c->bev, c->version, c->crypt_out.get(), command, flag, data,
|
||||
size, encoded_name.c_str());
|
||||
c->channel.send(command, flag, data, size);
|
||||
}
|
||||
|
||||
void send_command_excluding_client(shared_ptr<Lobby> l, shared_ptr<Client> c,
|
||||
@@ -236,12 +126,12 @@ void send_server_init_dc_pc_gc(shared_ptr<Client> c,
|
||||
switch (c->version) {
|
||||
case GameVersion::DC:
|
||||
case GameVersion::PC:
|
||||
c->crypt_out.reset(new PSOPCEncryption(server_key));
|
||||
c->crypt_in.reset(new PSOPCEncryption(client_key));
|
||||
c->channel.crypt_out.reset(new PSOPCEncryption(server_key));
|
||||
c->channel.crypt_in.reset(new PSOPCEncryption(client_key));
|
||||
break;
|
||||
case GameVersion::GC:
|
||||
c->crypt_out.reset(new PSOGCEncryption(server_key));
|
||||
c->crypt_in.reset(new PSOGCEncryption(client_key));
|
||||
c->channel.crypt_out.reset(new PSOGCEncryption(server_key));
|
||||
c->channel.crypt_in.reset(new PSOGCEncryption(client_key));
|
||||
break;
|
||||
default:
|
||||
throw invalid_argument("incorrect client version");
|
||||
@@ -270,8 +160,8 @@ void send_server_init_bb(shared_ptr<ServerState> s, shared_ptr<Client> c) {
|
||||
static const string expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8);
|
||||
shared_ptr<PSOBBMultiKeyDetectorEncryption> detector_crypt(new PSOBBMultiKeyDetectorEncryption(
|
||||
s->bb_private_keys, expected_first_data, cmd.client_key.data(), sizeof(cmd.client_key)));
|
||||
c->crypt_in = detector_crypt;
|
||||
c->crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
c->channel.crypt_in = detector_crypt;
|
||||
c->channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption(
|
||||
detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), true));
|
||||
}
|
||||
|
||||
@@ -285,8 +175,8 @@ void send_server_init_patch(shared_ptr<Client> c) {
|
||||
cmd.client_key = client_key;
|
||||
send_command_t(c, 0x02, 0x00, cmd);
|
||||
|
||||
c->crypt_out.reset(new PSOPCEncryption(server_key));
|
||||
c->crypt_in.reset(new PSOPCEncryption(client_key));
|
||||
c->channel.crypt_out.reset(new PSOPCEncryption(server_key));
|
||||
c->channel.crypt_in.reset(new PSOPCEncryption(client_key));
|
||||
}
|
||||
|
||||
void send_server_init(shared_ptr<ServerState> s, shared_ptr<Client> c,
|
||||
@@ -526,57 +416,70 @@ void send_enter_directory_patch(shared_ptr<Client> c, const string& dir) {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// message functions
|
||||
|
||||
void send_text(shared_ptr<Client> c, StringWriter& w, uint16_t command,
|
||||
const u16string& text) {
|
||||
if ((c->version == GameVersion::DC) || (c->version == GameVersion::GC)) {
|
||||
void send_text(Channel& ch, StringWriter& w, uint16_t command,
|
||||
const u16string& text, bool should_add_color) {
|
||||
if ((ch.version == GameVersion::DC) || (ch.version == GameVersion::GC)) {
|
||||
string data = encode_sjis(text);
|
||||
add_color(w, data.c_str(), data.size());
|
||||
if (should_add_color) {
|
||||
add_color(w, data.c_str(), data.size());
|
||||
} else {
|
||||
w.write(data);
|
||||
}
|
||||
w.put_u8(0);
|
||||
} else {
|
||||
add_color(w, text.c_str(), text.size());
|
||||
if (should_add_color) {
|
||||
add_color(w, text.c_str(), text.size());
|
||||
} else {
|
||||
w.write(text.data(), text.size() * sizeof(char16_t));
|
||||
}
|
||||
w.put_u16(0);
|
||||
}
|
||||
while (w.str().size() & 3) {
|
||||
w.put_u8(0);
|
||||
}
|
||||
send_command(c, command, 0x00, w.str());
|
||||
ch.send(command, 0x00, w.str());
|
||||
}
|
||||
|
||||
void send_header_text(shared_ptr<Client> c, uint16_t command,
|
||||
uint32_t guild_card_number, const u16string& text) {
|
||||
void send_text(Channel& ch, uint16_t command, const u16string& text, bool should_add_color) {
|
||||
StringWriter w;
|
||||
send_text(ch, w, command, text, should_add_color);
|
||||
}
|
||||
|
||||
void send_header_text(Channel& ch, uint16_t command,
|
||||
uint32_t guild_card_number, const u16string& text, bool should_add_color) {
|
||||
StringWriter w;
|
||||
w.put(SC_TextHeader_01_06_11_B0_EE({0, guild_card_number}));
|
||||
send_text(c, w, command, text);
|
||||
}
|
||||
|
||||
void send_text(shared_ptr<Client> c, uint16_t command,
|
||||
const u16string& text) {
|
||||
StringWriter w;
|
||||
send_text(c, w, command, text);
|
||||
send_text(ch, w, command, text, should_add_color);
|
||||
}
|
||||
|
||||
void send_message_box(shared_ptr<Client> c, const u16string& text) {
|
||||
uint16_t command = (c->version == GameVersion::PATCH) ? 0x13 : 0x1A;
|
||||
send_text(c, command, text);
|
||||
send_text(c->channel, command, text, true);
|
||||
}
|
||||
|
||||
void send_lobby_name(shared_ptr<Client> c, const u16string& text) {
|
||||
send_text(c, 0x8A, text);
|
||||
send_text(c->channel, 0x8A, text, false);
|
||||
}
|
||||
|
||||
void send_quest_info(shared_ptr<Client> c, const u16string& text,
|
||||
bool is_download_quest) {
|
||||
send_text(c, is_download_quest ? 0xA5 : 0xA3, text);
|
||||
send_text(c->channel, is_download_quest ? 0xA5 : 0xA3, text, true);
|
||||
}
|
||||
|
||||
void send_lobby_message_box(shared_ptr<Client> c, const u16string& text) {
|
||||
send_header_text(c, 0x01, 0, text);
|
||||
send_header_text(c->channel, 0x01, 0, text, true);
|
||||
}
|
||||
|
||||
void send_ship_info(shared_ptr<Client> c, const u16string& text) {
|
||||
send_header_text(c, 0x11, 0, text);
|
||||
send_header_text(c->channel, 0x11, 0, text, true);
|
||||
}
|
||||
|
||||
void send_text_message(Channel& ch, const std::u16string& text) {
|
||||
send_header_text(ch, 0xB0, 0, text, true);
|
||||
}
|
||||
|
||||
void send_text_message(shared_ptr<Client> c, const u16string& text) {
|
||||
send_header_text(c, 0xB0, 0, text);
|
||||
send_header_text(c->channel, 0xB0, 0, text, true);
|
||||
}
|
||||
|
||||
void send_text_message(shared_ptr<Lobby> l, const u16string& text) {
|
||||
@@ -595,6 +498,10 @@ void send_text_message(shared_ptr<ServerState> s, const u16string& text) {
|
||||
}
|
||||
}
|
||||
|
||||
void send_chat_message(Channel& ch, const u16string& text) {
|
||||
send_header_text(ch, 0x06, 0, text, false);
|
||||
}
|
||||
|
||||
void send_chat_message(shared_ptr<Client> c, uint32_t from_guild_card_number,
|
||||
const u16string& from_name, const u16string& text) {
|
||||
u16string data;
|
||||
@@ -604,7 +511,7 @@ void send_chat_message(shared_ptr<Client> c, uint32_t from_guild_card_number,
|
||||
data.append(remove_language_marker(from_name));
|
||||
data.append(u"\x09\x09J");
|
||||
data.append(text);
|
||||
send_header_text(c, 0x06, from_guild_card_number, data);
|
||||
send_header_text(c->channel, 0x06, from_guild_card_number, data, false);
|
||||
}
|
||||
|
||||
void send_simple_mail_gc(shared_ptr<Client> c, uint32_t from_guild_card_number,
|
||||
@@ -677,7 +584,8 @@ void send_card_search_result_t(
|
||||
// TODO: make this actually make sense... currently we just take the sockname
|
||||
// for the target client. This also doesn't work if the client is on a virtual
|
||||
// connection (the address and port are zero).
|
||||
const sockaddr_in* local_addr = reinterpret_cast<const sockaddr_in*>(&result->local_addr);
|
||||
const sockaddr_in* local_addr = reinterpret_cast<const sockaddr_in*>(
|
||||
&result->channel.local_addr);
|
||||
cmd.reconnect_command.address = local_addr->sin_addr.s_addr;
|
||||
cmd.reconnect_command.port = ntohs(local_addr->sin_port);
|
||||
cmd.reconnect_command.unused = 0;
|
||||
@@ -1200,6 +1108,16 @@ void send_player_stats_change(shared_ptr<Lobby> l, shared_ptr<Client> c,
|
||||
send_command_vt(l, 0x60, 0x00, subs);
|
||||
}
|
||||
|
||||
void send_warp(Channel& ch, uint8_t client_id, uint32_t area) {
|
||||
PSOSubcommand cmds[2];
|
||||
cmds[0].byte[0] = 0x94;
|
||||
cmds[0].byte[1] = 0x02;
|
||||
cmds[0].byte[2] = client_id;
|
||||
cmds[0].byte[3] = 0x00;
|
||||
cmds[1].dword = area;
|
||||
ch.send(0x62, client_id, cmds, 8);
|
||||
}
|
||||
|
||||
void send_warp(shared_ptr<Client> c, uint32_t area) {
|
||||
PSOSubcommand cmds[2];
|
||||
cmds[0].byte[0] = 0x94;
|
||||
|
||||
+9
-4
@@ -18,6 +18,9 @@
|
||||
|
||||
|
||||
|
||||
// TODO: Many of these functions should take a Channel& instead of a
|
||||
// shared_ptr<Client>. Refactor functions appropriately.
|
||||
|
||||
// Note: There are so many versions of this function for a few reasons:
|
||||
// - There are a lot of different target types (sometimes we want to send a
|
||||
// command to one client, sometimes to everyone in a lobby, etc.)
|
||||
@@ -26,9 +29,8 @@
|
||||
// pointer is given but size is accidentally not given (e.g. if the type of
|
||||
// data in the calling function is changed from string to void*).
|
||||
|
||||
void send_command(struct bufferevent* bev, GameVersion version,
|
||||
PSOEncryption* crypt, uint16_t command, uint32_t flag, const void* data,
|
||||
size_t size, const char* name_str = nullptr);
|
||||
void send_command(Channel& ch, uint16_t command, uint32_t flag,
|
||||
const void* data, size_t size);
|
||||
|
||||
void send_command(std::shared_ptr<Client> c, uint16_t command,
|
||||
uint32_t flag, const void* data, size_t size);
|
||||
@@ -137,9 +139,11 @@ void send_quest_info(std::shared_ptr<Client> c, const std::u16string& text,
|
||||
bool is_download_quest);
|
||||
void send_lobby_message_box(std::shared_ptr<Client> c, const std::u16string& text);
|
||||
void send_ship_info(std::shared_ptr<Client> c, const std::u16string& text);
|
||||
void send_text_message(Channel& ch, const std::u16string& text);
|
||||
void send_text_message(std::shared_ptr<Client> c, const std::u16string& text);
|
||||
void send_text_message(std::shared_ptr<Lobby> l, const std::u16string& text);
|
||||
void send_text_message(std::shared_ptr<ServerState> l, const std::u16string& text);
|
||||
void send_chat_message(Channel& ch, const std::u16string& text);
|
||||
void send_chat_message(std::shared_ptr<Client> c, uint32_t from_serial_number,
|
||||
const std::u16string& from_name, const std::u16string& text);
|
||||
void send_simple_mail(std::shared_ptr<Client> c, uint32_t from_serial_number,
|
||||
@@ -147,7 +151,7 @@ void send_simple_mail(std::shared_ptr<Client> c, uint32_t from_serial_number,
|
||||
|
||||
template <typename TargetT>
|
||||
__attribute__((format(printf, 2, 3))) void send_text_message_printf(
|
||||
std::shared_ptr<TargetT> t, const char* format, ...) {
|
||||
TargetT& t, const char* format, ...) {
|
||||
va_list va;
|
||||
va_start(va, format);
|
||||
std::string buf = string_vprintf(format, va);
|
||||
@@ -196,6 +200,7 @@ enum PlayerStatsChange {
|
||||
|
||||
void send_player_stats_change(std::shared_ptr<Lobby> l, std::shared_ptr<Client> c,
|
||||
PlayerStatsChange which, uint32_t amount);
|
||||
void send_warp(Channel& ch, uint8_t client_id, uint32_t area);
|
||||
void send_warp(std::shared_ptr<Client> c, uint32_t area);
|
||||
|
||||
void send_ep3_change_music(std::shared_ptr<Client> c, uint32_t song);
|
||||
|
||||
+41
-119
@@ -24,41 +24,25 @@
|
||||
#include "ReceiveCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
using namespace std::placeholders;
|
||||
|
||||
|
||||
|
||||
void Server::disconnect_client(struct bufferevent* bev) {
|
||||
this->disconnect_client(this->bev_to_client.at(bev));
|
||||
}
|
||||
|
||||
void Server::disconnect_client(shared_ptr<Client> c) {
|
||||
this->bev_to_client.erase(c->bev);
|
||||
struct bufferevent* bev = c->bev;
|
||||
c->bev = nullptr;
|
||||
|
||||
int fd = bufferevent_getfd(bev);
|
||||
if (fd < 0) {
|
||||
this->log(INFO, "Client on virtual connection %p disconnected", bev);
|
||||
if (c->channel.is_virtual_connection) {
|
||||
this->log(INFO, "Disconnecting client on virtual connection %p",
|
||||
c->channel.bev.get());
|
||||
} else {
|
||||
this->log(INFO, "Client on fd %d disconnected", fd);
|
||||
this->log(INFO, "Disconnecting client on fd %d",
|
||||
bufferevent_getfd(c->channel.bev.get()));
|
||||
}
|
||||
|
||||
// if the output buffer is not empty, move the client into the draining pool
|
||||
// instead of disconnecting it, to make sure all the data gets sent
|
||||
struct evbuffer* out_buffer = bufferevent_get_output(bev);
|
||||
if (evbuffer_get_length(out_buffer) == 0) {
|
||||
bufferevent_flush(bev, EV_WRITE, BEV_FINISHED);
|
||||
bufferevent_free(bev);
|
||||
} else {
|
||||
// the callbacks will free it when all the data is sent or the client
|
||||
// disconnects
|
||||
bufferevent_setcb(bev, nullptr,
|
||||
Server::dispatch_on_disconnecting_client_output,
|
||||
Server::dispatch_on_disconnecting_client_error, this);
|
||||
bufferevent_disable(bev, EV_READ);
|
||||
}
|
||||
this->channel_to_client.erase(&c->channel);
|
||||
c->channel.disconnect();
|
||||
|
||||
process_disconnect(this->state, c);
|
||||
// c is destroyed here (process_disconnect should remove any other references
|
||||
// to it, e.g. from Lobby objects)
|
||||
}
|
||||
|
||||
void Server::dispatch_on_listen_accept(
|
||||
@@ -73,25 +57,6 @@ void Server::dispatch_on_listen_error(struct evconnlistener* listener,
|
||||
reinterpret_cast<Server*>(ctx)->on_listen_error(listener);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_client_input(struct bufferevent* bev, void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_client_input(bev);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_client_error(struct bufferevent* bev, short events,
|
||||
void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_client_error(bev, events);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_disconnecting_client_output(struct bufferevent* bev,
|
||||
void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_disconnecting_client_output(bev);
|
||||
}
|
||||
|
||||
void Server::dispatch_on_disconnecting_client_error(struct bufferevent* bev,
|
||||
short events, void* ctx) {
|
||||
reinterpret_cast<Server*>(ctx)->on_disconnecting_client_error(bev, events);
|
||||
}
|
||||
|
||||
void Server::on_listen_accept(struct evconnlistener* listener,
|
||||
evutil_socket_t fd, struct sockaddr*, int) {
|
||||
|
||||
@@ -111,13 +76,12 @@ void Server::on_listen_accept(struct evconnlistener* listener,
|
||||
|
||||
struct bufferevent *bev = bufferevent_socket_new(this->base.get(), fd,
|
||||
BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
|
||||
shared_ptr<Client> c(new Client(bev, listening_socket->version,
|
||||
listening_socket->behavior));
|
||||
this->bev_to_client.emplace(make_pair(bev, c));
|
||||
|
||||
bufferevent_setcb(bev, &Server::dispatch_on_client_input, nullptr,
|
||||
&Server::dispatch_on_client_error, this);
|
||||
bufferevent_enable(bev, EV_READ | EV_WRITE);
|
||||
shared_ptr<Client> c(new Client(
|
||||
bev, listening_socket->version, listening_socket->behavior));
|
||||
c->channel.on_command_received = Server::on_client_input;
|
||||
c->channel.on_error = Server::on_client_error;
|
||||
c->channel.context_obj = this;
|
||||
this->channel_to_client.emplace(&c->channel, c);
|
||||
|
||||
process_connect(this->state, c);
|
||||
}
|
||||
@@ -128,19 +92,19 @@ void Server::connect_client(
|
||||
this->log(INFO, "Client connected on virtual connection %p", bev);
|
||||
|
||||
shared_ptr<Client> c(new Client(bev, version, initial_state));
|
||||
this->bev_to_client.emplace(make_pair(bev, c));
|
||||
c->channel.on_command_received = Server::on_client_input;
|
||||
c->channel.on_error = Server::on_client_error;
|
||||
c->channel.context_obj = this;
|
||||
|
||||
this->channel_to_client.emplace(&c->channel, c);
|
||||
|
||||
// Manually set the remote address, since the bufferevent has no fd and the
|
||||
// Client constructor can't figure out the virtual remote address
|
||||
auto* sin = reinterpret_cast<sockaddr_in*>(&c->remote_addr);
|
||||
// Channel constructor can't figure out the virtual remote address
|
||||
auto* sin = reinterpret_cast<sockaddr_in*>(&c->channel.remote_addr);
|
||||
sin->sin_family = AF_INET;
|
||||
sin->sin_addr.s_addr = htonl(address);
|
||||
sin->sin_port = htons(port);
|
||||
|
||||
bufferevent_setcb(bev, &Server::dispatch_on_client_input, nullptr,
|
||||
&Server::dispatch_on_client_error, this);
|
||||
bufferevent_enable(bev, EV_READ | EV_WRITE);
|
||||
|
||||
process_connect(this->state, c);
|
||||
}
|
||||
|
||||
@@ -151,73 +115,31 @@ void Server::on_listen_error(struct evconnlistener* listener) {
|
||||
event_base_loopexit(this->base.get(), nullptr);
|
||||
}
|
||||
|
||||
void Server::on_client_input(struct bufferevent* bev) {
|
||||
shared_ptr<Client> c;
|
||||
try {
|
||||
c = this->bev_to_client.at(bev);
|
||||
} catch (const out_of_range& e) {
|
||||
this->log(WARNING, "Received message from client with no configuration");
|
||||
|
||||
// ignore all the data
|
||||
// TODO: we probably should disconnect them or something
|
||||
struct evbuffer* in_buffer = bufferevent_get_input(bev);
|
||||
evbuffer_drain(in_buffer, evbuffer_get_length(in_buffer));
|
||||
return;
|
||||
}
|
||||
void Server::on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data) {
|
||||
Server* server = reinterpret_cast<Server*>(ch.context_obj);
|
||||
shared_ptr<Client> c = server->channel_to_client.at(&ch);
|
||||
|
||||
if (c->should_disconnect) {
|
||||
this->disconnect_client(bev);
|
||||
return;
|
||||
}
|
||||
|
||||
c->last_recv_time = now();
|
||||
this->receive_and_process_commands(c);
|
||||
|
||||
if (c->should_disconnect) {
|
||||
this->disconnect_client(bev);
|
||||
return;
|
||||
server->disconnect_client(c);
|
||||
} else {
|
||||
process_command(server->state, c, command, flag, data);
|
||||
if (c->should_disconnect) {
|
||||
server->disconnect_client(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Server::on_disconnecting_client_output(struct bufferevent* bev) {
|
||||
bufferevent_flush(bev, EV_WRITE, BEV_FINISHED);
|
||||
bufferevent_free(bev);
|
||||
}
|
||||
void Server::on_client_error(Channel& ch, short events) {
|
||||
Server* server = reinterpret_cast<Server*>(ch.context_obj);
|
||||
shared_ptr<Client> c = server->channel_to_client.at(&ch);
|
||||
|
||||
void Server::on_client_error(struct bufferevent* bev, short events) {
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
this->log(WARNING, "Client caused error %d (%s)", err,
|
||||
server->log(WARNING, "Client caused error %d (%s)", err,
|
||||
evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
|
||||
this->disconnect_client(bev);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::on_disconnecting_client_error(struct bufferevent* bev,
|
||||
short events) {
|
||||
if (events & BEV_EVENT_ERROR) {
|
||||
int err = EVUTIL_SOCKET_ERROR();
|
||||
this->log(WARNING, "Disconnecting client caused error %d (%s)", err,
|
||||
evutil_socket_error_to_string(err));
|
||||
}
|
||||
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
|
||||
bufferevent_flush(bev, EV_WRITE, BEV_FINISHED);
|
||||
bufferevent_free(bev);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::receive_and_process_commands(shared_ptr<Client> c) {
|
||||
try {
|
||||
for_each_received_command(c->bev, c->version, c->crypt_in.get(),
|
||||
[this, c](uint16_t command, uint16_t flag, const std::string& data) {
|
||||
process_command(this->state, c, command, flag, data);
|
||||
});
|
||||
} catch (const exception& e) {
|
||||
this->log(INFO, "Error in client stream: %s", e.what());
|
||||
c->should_disconnect = true;
|
||||
return;
|
||||
server->disconnect_client(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,11 +197,11 @@ void Server::add_socket(
|
||||
}
|
||||
|
||||
shared_ptr<Client> Server::get_client() const {
|
||||
if (this->bev_to_client.empty()) {
|
||||
if (this->channel_to_client.empty()) {
|
||||
throw runtime_error("no clients on game server");
|
||||
}
|
||||
if (this->bev_to_client.size() > 1) {
|
||||
if (this->channel_to_client.size() > 1) {
|
||||
throw runtime_error("multiple clients on game server");
|
||||
}
|
||||
return this->bev_to_client.begin()->second;
|
||||
}
|
||||
return this->channel_to_client.begin()->second;
|
||||
}
|
||||
|
||||
+3
-14
@@ -50,31 +50,20 @@ private:
|
||||
ServerBehavior behavior);
|
||||
};
|
||||
std::unordered_map<int, ListeningSocket> listening_sockets;
|
||||
std::unordered_map<struct bufferevent*, std::shared_ptr<Client>> bev_to_client;
|
||||
std::unordered_map<Channel*, std::shared_ptr<Client>> channel_to_client;
|
||||
|
||||
std::shared_ptr<ServerState> state;
|
||||
|
||||
static void dispatch_on_listen_accept(struct evconnlistener* listener,
|
||||
evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx);
|
||||
static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx);
|
||||
static void dispatch_on_client_input(struct bufferevent* bev, void* ctx);
|
||||
static void dispatch_on_client_error(struct bufferevent* bev, short events,
|
||||
void* ctx);
|
||||
static void dispatch_on_disconnecting_client_output(struct bufferevent* bev,
|
||||
void* ctx);
|
||||
static void dispatch_on_disconnecting_client_error(struct bufferevent* bev,
|
||||
short events, void* ctx);
|
||||
|
||||
void disconnect_client(struct bufferevent* bev);
|
||||
void disconnect_client(std::shared_ptr<Client> c);
|
||||
|
||||
void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd,
|
||||
struct sockaddr *address, int socklen);
|
||||
void on_listen_error(struct evconnlistener* listener);
|
||||
void on_client_input(struct bufferevent* bev);
|
||||
void on_client_error(struct bufferevent* bev, short events);
|
||||
void on_disconnecting_client_output(struct bufferevent* bev);
|
||||
void on_disconnecting_client_error(struct bufferevent* bev, short events);
|
||||
|
||||
void receive_and_process_commands(std::shared_ptr<Client> c);
|
||||
static void on_client_input(Channel& ch, uint16_t command, uint32_t flag, std::string& data);
|
||||
static void on_client_error(Channel& ch, short events);
|
||||
};
|
||||
|
||||
+12
-18
@@ -125,11 +125,6 @@ Proxy commands (these will only work when exactly one client is connected):\n\
|
||||
Enable or disable chat filtering (enabled by default). Chat filtering\n\
|
||||
applies newserv\'s standard character replacements to chat messages; for\n\
|
||||
example, $ becomes a tab character and # becomes a newline.\n\
|
||||
set-chat-safety <on|off>\n\
|
||||
Enable or disable chat safety (enabled by default). When chat safety is on,\n\
|
||||
all chat messages that begin with a $ are not sent to the remote server.\n\
|
||||
This can prevent embarrassing situations if the remote server isn\'t a\n\
|
||||
newserv instance and you have newserv commands in your chat shortcuts.\n\
|
||||
set-infinite-hp <on|off>\n\
|
||||
set-infinite-tp <on|off>\n\
|
||||
Enable or disable infinite HP or TP. When infinite HP is enabled, attacks\n\
|
||||
@@ -298,8 +293,11 @@ Proxy commands (these will only work when exactly one client is connected):\n\
|
||||
} catch (const exception&) { }
|
||||
|
||||
if (proxy_session.get()) {
|
||||
bool to_server = (command_name[1] == 's');
|
||||
proxy_session->send_to_end_with_header(to_server, data);
|
||||
if (command_name[1] == 's') {
|
||||
proxy_session->server_channel.send(data);
|
||||
} else {
|
||||
proxy_session->client_channel.send(data);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (command_name [1] == 's') {
|
||||
@@ -324,11 +322,11 @@ Proxy commands (these will only work when exactly one client is connected):\n\
|
||||
data.push_back('\0');
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
|
||||
session->send_to_end(true, 0x06, 0x00, data);
|
||||
session->server_channel.send(0x06, 0x00, data);
|
||||
|
||||
} else if (command_name == "marker") {
|
||||
auto session = this->get_proxy_session();
|
||||
session->send_to_end(true, 0x89, stoul(command_args));
|
||||
session->server_channel.send(0x89, stoul(command_args));
|
||||
|
||||
} else if (command_name == "warp") {
|
||||
auto session = this->get_proxy_session();
|
||||
@@ -338,8 +336,8 @@ Proxy commands (these will only work when exactly one client is connected):\n\
|
||||
cmds[0].word[1] = session->lobby_client_id;
|
||||
cmds[1].dword = stoul(command_args);
|
||||
|
||||
session->send_to_end(false, 0x60, 0x00, &cmds, sizeof(cmds));
|
||||
session->send_to_end(true, 0x60, 0x00, &cmds, sizeof(cmds));
|
||||
session->client_channel.send(0x60, 0x00, &cmds, sizeof(cmds));
|
||||
session->server_channel.send(0x60, 0x00, &cmds, sizeof(cmds));
|
||||
|
||||
} else if ((command_name == "info-board") || (command_name == "info-board-data")) {
|
||||
auto session = this->get_proxy_session();
|
||||
@@ -353,7 +351,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\
|
||||
data.push_back('\0');
|
||||
data.resize((data.size() + 3) & (~3));
|
||||
|
||||
session->send_to_end(true, 0xD9, 0x00, data);
|
||||
session->server_channel.send(0xD9, 0x00, data);
|
||||
|
||||
} else if (command_name == "set-override-section-id") {
|
||||
auto session = this->get_proxy_session();
|
||||
@@ -369,7 +367,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\
|
||||
session->override_lobby_event = -1;
|
||||
} else {
|
||||
session->override_lobby_event = event_for_name(command_args);
|
||||
session->send_to_end(false, 0xDA, session->override_lobby_event);
|
||||
session->client_channel.send(0xDA, session->override_lobby_event);
|
||||
}
|
||||
|
||||
} else if (command_name == "set-override-lobby-number") {
|
||||
@@ -384,10 +382,6 @@ Proxy commands (these will only work when exactly one client is connected):\n\
|
||||
auto session = this->get_proxy_session();
|
||||
set_boolean(&session->enable_chat_filter, command_args);
|
||||
|
||||
} else if (command_name == "set-chat-safety") {
|
||||
auto session = this->get_proxy_session();
|
||||
set_boolean(&session->suppress_newserv_commands, command_args);
|
||||
|
||||
} else if (command_name == "set-infinite-hp") {
|
||||
auto session = this->get_proxy_session();
|
||||
set_boolean(&session->infinite_hp, command_args);
|
||||
@@ -398,7 +392,7 @@ Proxy commands (these will only work when exactly one client is connected):\n\
|
||||
|
||||
} else if (command_name == "set-switch-assist") {
|
||||
auto session = this->get_proxy_session();
|
||||
set_boolean(&session->enable_switch_assist, command_args);
|
||||
set_boolean(&session->switch_assist, command_args);
|
||||
|
||||
} else if (command_name == "set-save-files") {
|
||||
auto session = this->get_proxy_session();
|
||||
|
||||
+4
-4
@@ -176,17 +176,17 @@ shared_ptr<Client> ServerState::find_client(const std::u16string* identifier,
|
||||
}
|
||||
|
||||
uint32_t ServerState::connect_address_for_client(std::shared_ptr<Client> c) {
|
||||
if (c->is_virtual_connection) {
|
||||
if (c->remote_addr.ss_family != AF_INET) {
|
||||
if (c->channel.is_virtual_connection) {
|
||||
if (c->channel.remote_addr.ss_family != AF_INET) {
|
||||
throw logic_error("virtual connection is missing remote IPv4 address");
|
||||
}
|
||||
const auto* sin = reinterpret_cast<const sockaddr_in*>(&c->remote_addr);
|
||||
const auto* sin = reinterpret_cast<const sockaddr_in*>(&c->channel.remote_addr);
|
||||
return IPStackSimulator::connect_address_for_remote_address(
|
||||
ntohl(sin->sin_addr.s_addr));
|
||||
} else {
|
||||
// TODO: we can do something smarter here, like use the sockname to find
|
||||
// out which interface the client is connected to, and return that address
|
||||
if (is_local_address(c->remote_addr)) {
|
||||
if (is_local_address(c->channel.remote_addr)) {
|
||||
return this->local_address;
|
||||
} else {
|
||||
return this->external_address;
|
||||
|
||||
@@ -143,8 +143,9 @@ u16string u16name_for_section_id(uint8_t section_id) {
|
||||
}
|
||||
|
||||
uint8_t section_id_for_name(const string& name) {
|
||||
string lower_name = tolower(name);
|
||||
try {
|
||||
return name_to_section_id.at(name);
|
||||
return name_to_section_id.at(lower_name);
|
||||
} catch (const out_of_range&) { }
|
||||
try {
|
||||
uint64_t x = stoul(name);
|
||||
|
||||
Reference in New Issue
Block a user