800 lines
26 KiB
C++
800 lines
26 KiB
C++
#include "ServerConnection.h"
|
|
#include "ProtocolHandler.h"
|
|
#include "Socket.h"
|
|
#include "audio/VoiceConnection.h"
|
|
#include "audio/AudioSender.h"
|
|
#include "../logger.h"
|
|
#include "../hwuid.h"
|
|
|
|
#include <sstream>
|
|
#include <thread>
|
|
#include <iostream>
|
|
#include <misc/net.h>
|
|
#include <misc/base64.h>
|
|
#include <misc/endianness.h>
|
|
#include <misc/strobf.h>
|
|
#include <iomanip>
|
|
#include <include/NanGet.h>
|
|
|
|
//#define FUZZ_VOICE
|
|
//#define SHUFFLE_VOICE
|
|
|
|
using namespace std;
|
|
using namespace std::chrono;
|
|
using namespace tc::connection;
|
|
|
|
string ErrorHandler::get_error_message(ErrorHandler::ErrorId id) const {
|
|
if(id == 0) {
|
|
return "success";
|
|
}
|
|
|
|
auto index = (-id - 1) % this->kErrorBacklog;
|
|
assert(index >= 0 && index < this->kErrorBacklog);
|
|
return this->error_messages[index];
|
|
}
|
|
|
|
ErrorHandler::ErrorId ErrorHandler::register_error(const string &message) {
|
|
auto index = this->error_index++ % this->kErrorBacklog;
|
|
this->error_messages[index] = message;
|
|
return -index - 1;
|
|
}
|
|
|
|
ServerConnection::ServerConnection() {
|
|
logger::debug(category::connection, tr("Allocated ServerConnection {}."), (void*) this);
|
|
}
|
|
|
|
ServerConnection::~ServerConnection() {
|
|
logger::debug(category::connection, tr("Begin deallocating ServerConnection {}."), (void*) this);
|
|
if(this->protocol_handler && this->protocol_handler->connection_state == connection_state::CONNECTED) {
|
|
this->protocol_handler->disconnect("server connection has been destoryed");
|
|
}
|
|
this->close_connection();
|
|
this->finalize();
|
|
|
|
logger::debug(category::connection, tr("Finished deallocating ServerConnection {}."), (void*) this);
|
|
}
|
|
|
|
NAN_MODULE_INIT(ServerConnection::Init) {
|
|
auto klass = Nan::New<v8::FunctionTemplate>(ServerConnection::new_instance);
|
|
klass->SetClassName(Nan::New("NativeServerConnection").ToLocalChecked());
|
|
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
|
|
|
Nan::SetPrototypeMethod(klass, "connect", ServerConnection::_connect);
|
|
Nan::SetPrototypeMethod(klass, "connected", ServerConnection::_connected);
|
|
Nan::SetPrototypeMethod(klass, "disconnect", ServerConnection::_disconnect);
|
|
Nan::SetPrototypeMethod(klass, "error_message", ServerConnection::_error_message);
|
|
Nan::SetPrototypeMethod(klass, "send_command", ServerConnection::_send_command);
|
|
Nan::SetPrototypeMethod(klass, "send_voice_data", ServerConnection::_send_voice_data);
|
|
Nan::SetPrototypeMethod(klass, "send_voice_data_raw", ServerConnection::_send_voice_data_raw);
|
|
Nan::SetPrototypeMethod(klass, "current_ping", ServerConnection::_current_ping);
|
|
Nan::SetPrototypeMethod(klass, "statistics", ServerConnection::statistics);
|
|
|
|
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
|
}
|
|
|
|
NAN_METHOD(ServerConnection::new_instance) {
|
|
//info.GetReturnValue().Set(Nan::New<v8::String>("Hello World").ToLocalChecked());
|
|
|
|
if (info.IsConstructCall()) {
|
|
auto instance = new ServerConnection();
|
|
instance->Wrap(info.This());
|
|
instance->initialize();
|
|
//Nan::Set(info.This(), New<String>("type").ToLocalChecked(), New<Number>(type));
|
|
info.GetReturnValue().Set(info.This());
|
|
} else {
|
|
v8::Local<v8::Function> cons = Nan::New(constructor());
|
|
|
|
Nan::TryCatch try_catch;
|
|
auto result = Nan::NewInstance(cons, 0, nullptr);
|
|
if(try_catch.HasCaught()) {
|
|
try_catch.ReThrow();
|
|
return;
|
|
}
|
|
info.GetReturnValue().Set(result.ToLocalChecked());
|
|
}
|
|
}
|
|
|
|
void ServerConnection::initialize() {
|
|
this->event_loop_exit = false;
|
|
this->event_thread = thread(&ServerConnection::event_loop, this);
|
|
this->protocol_handler = make_unique<ProtocolHandler>(this);
|
|
this->voice_connection = make_shared<VoiceConnection>(this);
|
|
this->voice_connection->_ref = this->voice_connection;
|
|
this->voice_connection->initialize_js_object();
|
|
|
|
this->execute_pending_commands = Nan::async_callback([&]{
|
|
Nan::HandleScope scope;
|
|
this->_execute_callback_commands();
|
|
});
|
|
this->execute_pending_voice = Nan::async_callback([&]{
|
|
Nan::HandleScope scope;
|
|
this->_execute_callback_voice();
|
|
});
|
|
this->execute_callback_disconnect = Nan::async_callback([&](std::string reason){
|
|
Nan::HandleScope scope;
|
|
this->_execute_callback_disconnect(reason);
|
|
});
|
|
|
|
|
|
this->call_connect_result = Nan::async_callback([&](ErrorHandler::ErrorId error_id) {
|
|
Nan::HandleScope scope;
|
|
/* lets update the server type */
|
|
{
|
|
auto js_this = this->handle();
|
|
Nan::Set(js_this, Nan::New<v8::String>("server_type").ToLocalChecked(), Nan::New<v8::Number>(this->protocol_handler->server_type));
|
|
}
|
|
|
|
/* lets call the connect callback */
|
|
{
|
|
v8::Local<v8::Value> argv[1];
|
|
argv[0] = Nan::New<v8::Number>(error_id);
|
|
|
|
if(this->callback_connect)
|
|
Nan::Call(*this->callback_connect, 1, argv);
|
|
this->callback_connect = nullptr;
|
|
}
|
|
});
|
|
|
|
|
|
this->call_disconnect_result = Nan::async_callback([&](ErrorHandler::ErrorId error_id) {
|
|
Nan::HandleScope scope;
|
|
v8::Local<v8::Value> argv[1];
|
|
argv[0] = Nan::New<v8::Number>(error_id);
|
|
|
|
if(this->callback_disconnect)
|
|
Nan::Call(*this->callback_disconnect, 1, argv);
|
|
this->callback_disconnect = nullptr;
|
|
});
|
|
|
|
auto js_this = this->handle();
|
|
Nan::Set(js_this, Nan::New<v8::String>("_voice_connection").ToLocalChecked(), this->voice_connection->js_handle());
|
|
|
|
}
|
|
|
|
void ServerConnection::finalize() {
|
|
this->event_loop_exit = true;
|
|
this->event_condition.notify_all();
|
|
this->event_thread.join();
|
|
}
|
|
|
|
void ServerConnection::event_loop() {
|
|
auto eval_timeout = [&]{
|
|
auto best = this->next_tick;
|
|
if(this->next_resend < best)
|
|
best = this->next_resend;
|
|
if(this->event_loop_execute_connection_close)
|
|
return system_clock::time_point{};
|
|
return best;
|
|
};
|
|
|
|
while(!this->event_loop_exit) {
|
|
auto timeout = eval_timeout();
|
|
{
|
|
unique_lock lock(this->event_lock);
|
|
this->event_condition.wait_until(lock, timeout, [&]{
|
|
if(eval_timeout() != timeout)
|
|
return true;
|
|
return this->event_loop_exit;
|
|
});
|
|
|
|
if(this->event_loop_exit)
|
|
break;
|
|
}
|
|
if(this->event_loop_execute_connection_close) {
|
|
this->close_connection();
|
|
this->event_loop_execute_connection_close = false;
|
|
}
|
|
auto date = chrono::system_clock::now();
|
|
if(this->next_tick <= date) {
|
|
this->next_tick = date + chrono::milliseconds(500);
|
|
this->execute_tick();
|
|
}
|
|
if(this->next_resend <= date) {
|
|
this->next_resend = date + chrono::seconds(5);
|
|
if(this->protocol_handler)
|
|
this->protocol_handler->execute_resend();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ServerConnection::schedule_resend(const std::chrono::system_clock::time_point &timeout) {
|
|
if(this->next_resend > timeout) {
|
|
this->next_resend = timeout;
|
|
this->event_condition.notify_one();
|
|
}
|
|
}
|
|
|
|
#ifdef WIN32
|
|
inline std::string wsa_error_str(int code) {
|
|
int err;
|
|
char msgbuf[256]; // for a message up to 255 bytes.
|
|
msgbuf[0] = '\0'; // Microsoft doesn't guarantee this on man page.
|
|
|
|
err = WSAGetLastError();
|
|
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, // flags
|
|
NULL, // lpsource
|
|
err, // message id
|
|
MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT), // languageid
|
|
msgbuf, // output buffer
|
|
sizeof(msgbuf), // size of msgbuf, bytes
|
|
NULL); // va_list of arguments
|
|
|
|
if (!*msgbuf)
|
|
sprintf(msgbuf, "%d", err); // provide error # if no string available
|
|
return std::string{msgbuf};
|
|
}
|
|
#endif
|
|
|
|
NAN_METHOD(ServerConnection::connect) {
|
|
if(!this->protocol_handler) {
|
|
Nan::ThrowError("ServerConnection not initialized");
|
|
return;
|
|
}
|
|
if(info.Length() != 1) {
|
|
Nan::ThrowError(tr("Invalid argument count"));
|
|
return;
|
|
}
|
|
|
|
v8::Local arguments = info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked();
|
|
|
|
if(!arguments->IsObject()) {
|
|
Nan::ThrowError(tr("Invalid argument"));
|
|
return;
|
|
}
|
|
|
|
auto remote_host = Nan::Get(arguments, Nan::New<v8::String>("remote_host").ToLocalChecked()).ToLocalChecked();
|
|
auto remote_port = Nan::Get(arguments, Nan::New<v8::String>("remote_port").ToLocalChecked()).ToLocalChecked();
|
|
auto timeout = Nan::Get(arguments, Nan::New<v8::String>("timeout").ToLocalChecked()).ToLocalChecked();
|
|
|
|
auto callback = Nan::Get(arguments, Nan::New<v8::String>("callback").ToLocalChecked()).ToLocalChecked();
|
|
|
|
auto identity_key = Nan::Get(arguments, Nan::New<v8::String>("identity_key").ToLocalChecked()).ToLocalChecked();
|
|
auto teamspeak = Nan::Get(arguments, Nan::New<v8::String>("teamspeak").ToLocalChecked()).ToLocalChecked();
|
|
|
|
if(!identity_key->IsString() && !identity_key->IsNullOrUndefined()) {
|
|
Nan::ThrowError(tr("Invalid identity"));
|
|
return;
|
|
}
|
|
|
|
if(!remote_host->IsString() || !remote_port->IsNumber()) {
|
|
Nan::ThrowError(tr("Invalid argument host/port"));
|
|
return;
|
|
}
|
|
|
|
if(!callback->IsFunction()) {
|
|
Nan::ThrowError(tr("Invalid callback"));
|
|
return;
|
|
}
|
|
|
|
unique_lock _disconnect_lock(this->disconnect_lock, defer_lock);
|
|
if(!_disconnect_lock.try_lock_for(chrono::milliseconds(500))) {
|
|
Nan::ThrowError(tr("failed to acquire disconnect lock"));
|
|
return;
|
|
}
|
|
|
|
this->callback_connect = make_unique<Nan::Callback>(callback.As<v8::Function>());
|
|
|
|
this->voice_connection->reset();
|
|
this->protocol_handler->reset();
|
|
|
|
std::optional<std::string> identity{std::nullopt};
|
|
if(identity_key->IsString()) {
|
|
auto identity_encoded = Nan::Utf8String{identity_key->ToString(Nan::GetCurrentContext()).ToLocalChecked()};
|
|
identity = std::make_optional(
|
|
base64::decode(std::string_view{*identity_encoded, (size_t) identity_encoded.length()})
|
|
);
|
|
|
|
}
|
|
|
|
if(!this->protocol_handler->initialize_identity(identity)) {
|
|
Nan::ThrowError(tr("failed to initialize crypto identity"));
|
|
return;
|
|
}
|
|
|
|
sockaddr_storage remote_address{};
|
|
/* resolve address */
|
|
{
|
|
addrinfo hints{}, *result;
|
|
memset(&hints, 0, sizeof(hints));
|
|
|
|
hints.ai_family = AF_UNSPEC;
|
|
|
|
auto remote_host_ = Nan::Utf8String{remote_host->ToString(Nan::GetCurrentContext()).ToLocalChecked()};
|
|
if(getaddrinfo(*remote_host_, nullptr, &hints, &result) != 0 || !result) {
|
|
this->call_connect_result(this->errors.register_error(tr("failed to resolve hostname")));
|
|
return;
|
|
}
|
|
|
|
memcpy(&remote_address, result->ai_addr, result->ai_addrlen);
|
|
freeaddrinfo(result);
|
|
}
|
|
|
|
switch(remote_address.ss_family) {
|
|
case AF_INET:
|
|
((sockaddr_in*) &remote_address)->sin_port = htons(remote_port->Int32Value(Nan::GetCurrentContext()).FromMaybe(0));
|
|
break;
|
|
|
|
case AF_INET6:
|
|
((sockaddr_in6*) &remote_address)->sin6_port = htons(remote_port->Int32Value(Nan::GetCurrentContext()).FromMaybe(0));
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
log_info(category::connection, tr("Connecting to {}."), net::to_string(remote_address));
|
|
this->socket = make_shared<UDPSocket>(remote_address);
|
|
if(!this->socket->initialize()) {
|
|
this->call_connect_result(this->errors.register_error("failed to initialize socket"));
|
|
this->socket = nullptr;
|
|
return;
|
|
}
|
|
|
|
this->socket->on_data = [&](const pipes::buffer_view& buffer) { this->protocol_handler->progress_packet(buffer); };
|
|
this->socket->on_fatal_error = [&](int code, int detail) {
|
|
#if WIN32
|
|
auto message = wsa_error_str(detail);
|
|
#else
|
|
auto message = std::string{strerror(detail)};
|
|
#endif
|
|
this->execute_callback_disconnect.call((code == 1 ? tr("Failed to received data: ") : tr("Failed to send data: ")) + message, false);
|
|
this->close_connection();
|
|
};
|
|
|
|
if(teamspeak->IsBoolean() && teamspeak->BooleanValue(info.GetIsolate())) {
|
|
this->protocol_handler->server_type = server_type::TEAMSPEAK;
|
|
}
|
|
this->protocol_handler->connect();
|
|
}
|
|
|
|
NAN_METHOD(ServerConnection::_connected) {
|
|
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->connected(info);
|
|
}
|
|
|
|
|
|
NAN_METHOD(ServerConnection::_connect) {
|
|
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->connect(info);
|
|
}
|
|
NAN_METHOD(ServerConnection::connected) {
|
|
bool result = this->protocol_handler && this->protocol_handler->connection_state >= connection_state::CONNECTING && this->protocol_handler->connection_state < connection_state::DISCONNECTING;
|
|
info.GetReturnValue().Set(result);
|
|
}
|
|
|
|
|
|
NAN_METHOD(ServerConnection::_disconnect) {
|
|
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->disconnect(info);
|
|
}
|
|
NAN_METHOD(ServerConnection::disconnect) {
|
|
if(info.Length() != 2) {
|
|
Nan::ThrowError("Invalid argument count");
|
|
return;
|
|
}
|
|
|
|
if(!info[1]->IsFunction() || !info[0]->IsString()) {
|
|
Nan::ThrowError("Invalid argument");
|
|
return;
|
|
}
|
|
this->callback_disconnect = make_unique<Nan::Callback>(info[1].As<v8::Function>());
|
|
if(!this->socket) {
|
|
this->call_disconnect_result(0); /* this->errors.register_error("not connected") */
|
|
return;
|
|
}
|
|
|
|
if(this->protocol_handler) {
|
|
this->protocol_handler->disconnect(*Nan::Utf8String(info[0]));
|
|
}
|
|
}
|
|
|
|
NAN_METHOD(ServerConnection::_error_message) {
|
|
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->error_message(info);
|
|
}
|
|
NAN_METHOD(ServerConnection::error_message) {
|
|
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
|
Nan::ThrowError("Invalid argument");
|
|
return;
|
|
}
|
|
|
|
auto error = this->errors.get_error_message(
|
|
(ErrorHandler::ErrorId) info[0]->IntegerValue(Nan::GetCurrentContext()).FromMaybe(0)
|
|
);
|
|
info.GetReturnValue().Set(Nan::New<v8::String>(error).ToLocalChecked());
|
|
}
|
|
|
|
//send_command(command: string, arguments: any[], switches: string[]);
|
|
template <typename T>
|
|
std::string _to_string(const T value) {
|
|
std::string result(15, '\0');
|
|
auto written = std::snprintf(&result[0], result.size(), "%.6f", value);
|
|
if(written < 0) {
|
|
log_warn(general, "Failed to format float value: {}; {}", value, written);
|
|
return "0";
|
|
}
|
|
result.resize(written);
|
|
return result;
|
|
}
|
|
|
|
NAN_METHOD(ServerConnection::_send_command) {
|
|
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->send_command(info);
|
|
}
|
|
|
|
struct TS3VersionSettings {
|
|
std::string build;
|
|
std::string platform;
|
|
std::string sign;
|
|
};
|
|
NAN_METHOD(ServerConnection::send_command) {
|
|
if(!this->protocol_handler) {
|
|
Nan::ThrowError("ServerConnection not initialized");
|
|
return;
|
|
}
|
|
|
|
if(info.Length() != 3) {
|
|
Nan::ThrowError("invalid argument count");
|
|
return;
|
|
}
|
|
|
|
if(!info[0]->IsString() || !info[1]->IsArray() || !info[2]->IsArray()) {
|
|
Nan::ThrowError("invalid argument type");
|
|
return;
|
|
}
|
|
|
|
auto begin = chrono::system_clock::now();
|
|
auto command = info[0]->ToString(Nan::GetCurrentContext()).ToLocalChecked();
|
|
auto arguments = info[1].As<v8::Array>();
|
|
auto switches = info[2].As<v8::Array>();
|
|
|
|
ts::Command cmd(*Nan::Utf8String(command));
|
|
for(size_t index = 0; index < arguments->Length(); index++) {
|
|
auto object = Nan::GetLocal<v8::Object>(arguments, (uint32_t) index);
|
|
if(object.IsEmpty() || !object->IsObject()) {
|
|
Nan::ThrowError(Nan::New<v8::String>("invalid parameter (" + to_string(index) + ")").ToLocalChecked());
|
|
return;
|
|
}
|
|
|
|
v8::Local<v8::Array> properties = object->ToObject(Nan::GetCurrentContext()).ToLocalChecked()->GetOwnPropertyNames(Nan::GetCurrentContext()).ToLocalChecked();
|
|
for(uint32_t i = 0; i < properties->Length(); i++) {
|
|
auto key = Nan::GetStringLocal(properties, i);
|
|
auto value = object->ToObject(Nan::GetCurrentContext()).ToLocalChecked()->Get(Nan::GetCurrentContext(), key).ToLocalChecked();
|
|
|
|
string key_string = *Nan::Utf8String(key);
|
|
if(value->IsInt32())
|
|
cmd[index][key_string] = value->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
|
else if(value->IsNumber() || value->IsNumberObject())
|
|
cmd[index][key_string] = _to_string<double>(value->NumberValue(Nan::GetCurrentContext()).FromMaybe(0)); /* requires our own conversation because node overrides stuff to 0,0000*/
|
|
else if(value->IsString())
|
|
cmd[index][key_string] = *Nan::Utf8String(value->ToString(Nan::GetCurrentContext()).ToLocalChecked());
|
|
else if(value->IsBoolean() || value->IsBooleanObject())
|
|
cmd[index][key_string] = value->BooleanValue(info.GetIsolate());
|
|
else if(value->IsNullOrUndefined())
|
|
cmd[index][key_string] = "";
|
|
else {
|
|
Nan::ThrowError(Nan::New<v8::String>("invalid parameter (" + to_string(index) + ":" + key_string + ")").ToLocalChecked());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
for(size_t index = 0; index < switches->Length(); index++) {
|
|
auto object = Nan::GetStringLocal(switches, (uint32_t) index);
|
|
if(object.IsEmpty()) {
|
|
Nan::ThrowError(Nan::New<v8::String>("invalid switch (" + to_string(index) + ")").ToLocalChecked());
|
|
return;
|
|
}
|
|
|
|
cmd.enableParm(*Nan::Utf8String(object));
|
|
}
|
|
|
|
if(this->protocol_handler->server_type == server_type::TEAMSPEAK) {
|
|
if(cmd.command() == "clientinit") {
|
|
/* If we have a return code here some strange stuff happens (Ghost client) */
|
|
if(cmd[0].has("return_code"))
|
|
cmd["return_code"] = nullptr;
|
|
|
|
TS3VersionSettings ts_version{};
|
|
#ifdef WIN32
|
|
/*
|
|
ts_version = {
|
|
"0.0.1 [Build: 1549713549]",
|
|
"Linux",
|
|
"7XvKmrk7uid2ixHFeERGqcC8vupeQqDypLtw2lY9slDNPojEv//F47UaDLG+TmVk4r6S0TseIKefzBpiRtLDAQ=="
|
|
};
|
|
*/
|
|
|
|
ts_version = {
|
|
"3.?.? [Build: 5680278000]",
|
|
"Windows",
|
|
"DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="
|
|
};
|
|
#else
|
|
/*
|
|
ts_version = {
|
|
"0.0.1 [Build: 1549713549]",
|
|
"Linux",
|
|
"7XvKmrk7uid2ixHFeERGqcC8vupeQqDypLtw2lY9slDNPojEv//F47UaDLG+TmVk4r6S0TseIKefzBpiRtLDAQ=="
|
|
};
|
|
*/
|
|
ts_version = {
|
|
"3.?.? [Build: 5680278000]",
|
|
"Linux",
|
|
"Hjd+N58Gv3ENhoKmGYy2bNRBsNNgm5kpiaQWxOj5HN2DXttG6REjymSwJtpJ8muC2gSwRuZi0R+8Laan5ts5CQ=="
|
|
};
|
|
#endif
|
|
if(std::getenv("teaclient_ts3_build") && std::getenv("teaclient_ts3_platform") && std::getenv("teaclient_ts3_sign")) {
|
|
ts_version = {
|
|
std::getenv("teaclient_ts3_build"),
|
|
std::getenv("teaclient_ts3_platform"),
|
|
std::getenv("teaclient_ts3_sign")
|
|
};
|
|
}
|
|
|
|
cmd["client_version"] = ts_version.build;
|
|
cmd["client_platform"] = ts_version.platform;
|
|
cmd["client_version_sign"] = ts_version.sign;
|
|
cmd[strobf("hwid").string()] = system_uuid(); /* we dont want anybody to patch this out */
|
|
}
|
|
}
|
|
|
|
this->protocol_handler->send_command(cmd, false);
|
|
auto end = chrono::system_clock::now();
|
|
}
|
|
NAN_METHOD(ServerConnection::_send_voice_data) {
|
|
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->send_voice_data(info);
|
|
}
|
|
|
|
NAN_METHOD(ServerConnection::send_voice_data) {
|
|
if(!this->protocol_handler) {
|
|
Nan::ThrowError("ServerConnection not initialized");
|
|
return;
|
|
}
|
|
|
|
if(info.Length() != 3) {
|
|
Nan::ThrowError("invalid argument count");
|
|
return;
|
|
}
|
|
|
|
if(!info[0]->IsUint8Array() || !info[1]->IsInt32() || !info[2]->IsBoolean()) {
|
|
Nan::ThrowError("invalid argument type");
|
|
return;
|
|
}
|
|
|
|
|
|
auto voice_data = info[0].As<v8::Uint8Array>()->Buffer();
|
|
this->send_voice_data(voice_data->GetContents().Data(), voice_data->GetContents().ByteLength(), (uint8_t) info[1]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0), info[2]->BooleanValue(info.GetIsolate()));
|
|
}
|
|
|
|
|
|
NAN_METHOD(ServerConnection::_send_voice_data_raw) {
|
|
return ObjectWrap::Unwrap<ServerConnection>(info.Holder())->send_voice_data_raw(info);
|
|
}
|
|
|
|
NAN_METHOD(ServerConnection::send_voice_data_raw) {
|
|
//send_voice_data_raw(buffer: Float32Array, channels: number, sample_rate: number, header: boolean);
|
|
|
|
if(info.Length() != 4) {
|
|
Nan::ThrowError("invalid argument count");
|
|
return;
|
|
}
|
|
|
|
if(!info[0]->IsFloat32Array() || !info[1]->IsInt32() || !info[2]->IsInt32()) {
|
|
Nan::ThrowError("invalid argument type");
|
|
return;
|
|
}
|
|
|
|
auto channels = info[1]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
|
auto sample_rate = info[2]->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
|
auto flag_head = info[2]->BooleanValue(info.GetIsolate());
|
|
|
|
auto voice_data = info[0].As<v8::Float32Array>()->Buffer();
|
|
auto vs = this->voice_connection ? this->voice_connection->voice_sender() : nullptr;
|
|
if(vs) {
|
|
vs->send_data((float*) voice_data->GetContents().Data(), voice_data->GetContents().ByteLength() / (4 * channels), sample_rate, channels);
|
|
}
|
|
}
|
|
|
|
#ifdef SHUFFLE_VOICE
|
|
static shared_ptr<ts::protocol::ClientPacket> shuffle_cached_packet;
|
|
#endif
|
|
void ServerConnection::send_voice_data(const void *buffer, size_t buffer_length, uint8_t codec, bool head) {
|
|
auto packet = ts::protocol::allocate_outgoing_client_packet(buffer_length + 3);
|
|
packet->type_and_flags_ = ts::protocol::PacketType::VOICE;
|
|
|
|
auto packet_payload = (uint8_t*) packet->payload;
|
|
*(uint16_t*) packet_payload = htons(this->voice_packet_id++);
|
|
packet_payload[2] = (uint8_t) codec;
|
|
if(buffer_length > 0 && buffer) {
|
|
memcpy(&packet_payload[3], buffer, buffer_length);
|
|
}
|
|
|
|
if(head) {
|
|
packet->type_and_flags_ |= ts::protocol::PacketFlag::Compressed;
|
|
}
|
|
packet->type_and_flags_ |= ts::protocol::PacketFlag::Unencrypted;
|
|
|
|
#ifdef FUZZ_VOICE
|
|
if((rand() % 10) < 2) {
|
|
log_info(category::connection, tr("Dropping voice packet"));
|
|
} else {
|
|
this->protocol_handler->send_packet(packet);
|
|
}
|
|
#elif defined(SHUFFLE_VOICE)
|
|
if(shuffle_cached_packet) {
|
|
this->protocol_handler->send_packet(packet);
|
|
this->protocol_handler->send_packet(std::exchange(shuffle_cached_packet, nullptr));
|
|
} else {
|
|
shuffle_cached_packet = packet;
|
|
}
|
|
#else
|
|
this->protocol_handler->send_packet(packet, false);
|
|
#endif
|
|
}
|
|
|
|
void ServerConnection::close_connection() {
|
|
lock_guard lock{this->disconnect_lock};
|
|
if(this->socket && this_thread::get_id() == this->socket->io_thread().get_id()) {
|
|
logger::debug(category::connection, tr("close_connection() called in IO thread. Closing connection within event loop!"));
|
|
if(!this->event_loop_execute_connection_close) {
|
|
this->event_loop_execute_connection_close = true;
|
|
this->event_condition.notify_one();
|
|
}
|
|
return;
|
|
}
|
|
|
|
this->event_loop_execute_connection_close = false;
|
|
if(this->socket) {
|
|
this->protocol_handler->do_close_connection();
|
|
}
|
|
if(this->protocol_handler) {
|
|
this->protocol_handler->do_close_connection();
|
|
}
|
|
this->socket = nullptr;
|
|
|
|
this->call_disconnect_result.call(0, true);
|
|
}
|
|
|
|
void ServerConnection::execute_tick() {
|
|
if(this->protocol_handler) {
|
|
this->protocol_handler->execute_tick();
|
|
}
|
|
|
|
if(auto vc{this->voice_connection}; vc) {
|
|
vc->execute_tick();
|
|
}
|
|
}
|
|
|
|
void ServerConnection::_execute_callback_commands() {
|
|
unique_ptr<ts::Command> next_command;
|
|
v8::Local<v8::Function> callback;
|
|
|
|
while(true) {
|
|
{
|
|
lock_guard lock(this->pending_commands_lock);
|
|
if(this->pending_commands.empty())
|
|
return;
|
|
next_command = move(this->pending_commands.front());
|
|
this->pending_commands.pop_front();
|
|
}
|
|
if(!next_command)
|
|
continue;
|
|
|
|
if(callback.IsEmpty()) {
|
|
callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_command").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
|
if(callback.IsEmpty()) {
|
|
logger::warn(category::connection, tr("Missing command callback! Dropping commands."));
|
|
lock_guard lock(this->pending_commands_lock);
|
|
this->pending_commands.clear();
|
|
return;
|
|
}
|
|
}
|
|
|
|
v8::Local<v8::Value> arguments[3];
|
|
arguments[0] = Nan::New<v8::String>(next_command->command()).ToLocalChecked();
|
|
|
|
auto parameters = Nan::New<v8::Array>((int) next_command->bulkCount());
|
|
for(size_t index = 0; index < next_command->bulkCount(); index++) {
|
|
auto object = Nan::New<v8::Object>();
|
|
auto& bulk = next_command->operator[](index);
|
|
|
|
for(const auto& key : bulk.keys())
|
|
Nan::Set(object, Nan::New<v8::String>(key).ToLocalChecked(), Nan::New<v8::String>(bulk[key].string()).ToLocalChecked());
|
|
|
|
Nan::Set(parameters, (uint32_t) index, object);
|
|
}
|
|
arguments[1] = parameters;
|
|
|
|
|
|
auto switched = Nan::New<v8::Array>((int) next_command->parms().size());
|
|
for(size_t index = 0; index < next_command->parms().size(); index++) {
|
|
auto& key = next_command->parms()[index];
|
|
Nan::Set(parameters, (uint32_t) index, Nan::New<v8::String>(key).ToLocalChecked());
|
|
}
|
|
arguments[2] = switched;
|
|
|
|
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 3, arguments);
|
|
}
|
|
}
|
|
|
|
void ServerConnection::_execute_callback_voice() {
|
|
unique_ptr<VoicePacket> next_packet;
|
|
v8::Local<v8::Function> callback;
|
|
|
|
while(true) {
|
|
{
|
|
lock_guard lock(this->pending_voice_lock);
|
|
if(this->pending_voice.empty())
|
|
return;
|
|
next_packet = move(this->pending_voice.front());
|
|
this->pending_voice.pop_front();
|
|
}
|
|
if(!next_packet)
|
|
continue;
|
|
|
|
if(callback.IsEmpty()) {
|
|
auto _callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_voice_data").ToLocalChecked()).ToLocalChecked();
|
|
if(_callback.IsEmpty() || _callback->IsUndefined()) {
|
|
logger::warn(category::audio, tr("Missing voice callback! Dropping packets!"));
|
|
lock_guard lock(this->pending_voice_lock);
|
|
this->pending_voice.clear();
|
|
return;
|
|
}
|
|
callback = _callback.As<v8::Function>();
|
|
}
|
|
|
|
v8::Local<v8::Value> arguments[5];
|
|
|
|
v8::Local<v8::ArrayBuffer> buffer = v8::ArrayBuffer::New(
|
|
Nan::GetCurrentContext()->GetIsolate(),
|
|
next_packet->voice_data.length()
|
|
);
|
|
memcpy(buffer->GetContents().Data(), next_packet->voice_data.data_ptr(), next_packet->voice_data.length());
|
|
arguments[0] = v8::Uint8Array::New(buffer, 0, buffer->ByteLength());
|
|
arguments[1] = Nan::New<v8::Integer>(next_packet->client_id);
|
|
arguments[2] = Nan::New<v8::Integer>(next_packet->codec_id);
|
|
arguments[3] = Nan::New<v8::Boolean>(next_packet->flag_head);
|
|
arguments[4] = Nan::New<v8::Integer>(next_packet->packet_id);
|
|
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 5, arguments);
|
|
}
|
|
}
|
|
|
|
void ServerConnection::_execute_callback_disconnect(const std::string &reason) {
|
|
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_disconnect").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
|
if(callback.IsEmpty()) {
|
|
cout << "Missing disconnect callback!" << endl;
|
|
return;
|
|
}
|
|
|
|
v8::Local<v8::Value> arguments[1];
|
|
arguments[0] = Nan::New<v8::String>(reason).ToLocalChecked();
|
|
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, arguments);
|
|
|
|
}
|
|
|
|
NAN_METHOD(ServerConnection::_current_ping) {
|
|
auto connection = ObjectWrap::Unwrap<ServerConnection>(info.Holder());
|
|
lock_guard lock{connection->disconnect_lock};
|
|
|
|
auto& phandler = connection->protocol_handler;
|
|
if(phandler)
|
|
info.GetReturnValue().Set((uint32_t) chrono::floor<microseconds>(phandler->current_ping()).count());
|
|
else
|
|
info.GetReturnValue().Set(-1);
|
|
}
|
|
|
|
NAN_METHOD(ServerConnection::statistics) {
|
|
auto connection = ObjectWrap::Unwrap<ServerConnection>(info.Holder());
|
|
lock_guard lock{connection->disconnect_lock};
|
|
|
|
auto& phandler = connection->protocol_handler;
|
|
if(phandler) {
|
|
auto statistics = phandler->statistics();
|
|
|
|
auto result = Nan::New<v8::Object>();
|
|
Nan::Set(result, Nan::LocalString("voice_bytes_received"), Nan::New<v8::Number>(statistics.voice_bytes_received));
|
|
Nan::Set(result, Nan::LocalString("voice_bytes_send"), Nan::New<v8::Number>(statistics.voice_bytes_send));
|
|
Nan::Set(result, Nan::LocalString("control_bytes_received"), Nan::New<v8::Number>(statistics.control_bytes_received));
|
|
Nan::Set(result, Nan::LocalString("control_bytes_send"), Nan::New<v8::Number>(statistics.control_bytes_send));
|
|
info.GetReturnValue().Set(result);
|
|
} else {
|
|
info.GetReturnValue().Set(Nan::Undefined());
|
|
}
|
|
} |