A lot of updates
This commit is contained in:
@@ -1,535 +0,0 @@
|
||||
#ifdef WIN32
|
||||
#include <WinSock2.h>
|
||||
#endif
|
||||
|
||||
#include "ProtocolHandler.h"
|
||||
#include "ServerConnection.h"
|
||||
#include "Socket.h"
|
||||
#include "../logger.h"
|
||||
#include <misc/base64.h>
|
||||
#include <misc/endianness.h>
|
||||
#include <protocol/buffers.h>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <protocol/Packet.h>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc::connection;
|
||||
using namespace ts::protocol;
|
||||
using namespace ts;
|
||||
|
||||
ProtocolHandler::ProtocolHandler(ServerConnection* handle) : handle(handle) {
|
||||
this->compression_handler.max_packet_size = 128 * 1024; /* max 128Kb */
|
||||
}
|
||||
|
||||
ProtocolHandler::~ProtocolHandler() {
|
||||
}
|
||||
|
||||
|
||||
void ProtocolHandler::reset() {
|
||||
this->server_type = server_type::UNKNOWN;
|
||||
this->disconnect_id++; /* we've been resetted any pending disconnects are not from interest anymore */
|
||||
this->client_id = 0;
|
||||
this->acknowledge_handler.reset();
|
||||
this->connection_state = connection_state::INITIALIZING;
|
||||
|
||||
{ /* initialize pow handler */
|
||||
this->pow.state = pow_state::COOKIE_SET;
|
||||
this->pow.last_buffer = pipes::buffer{};
|
||||
this->pow.last_resend = system_clock::time_point{};
|
||||
this->pow.last_response = system_clock::time_point{};
|
||||
|
||||
this->pow.client_control_data[0] = 0; /* clear set flag, so the client generates a new pack */
|
||||
}
|
||||
|
||||
{
|
||||
this->crypto.alpha[0] = 0;
|
||||
this->crypto.initiv_command = "";
|
||||
this->crypto.beta_length = 0;
|
||||
|
||||
if(this->crypto.identity.k)
|
||||
ecc_free(&this->crypto.identity);
|
||||
memset(&this->crypto.identity, 0, sizeof(this->crypto.identity));
|
||||
}
|
||||
|
||||
for(auto& buffer : this->_packet_buffers) {
|
||||
lock_guard lock(buffer.buffer_lock);
|
||||
buffer.reset();
|
||||
}
|
||||
|
||||
this->_packet_id_manager.reset();
|
||||
this->crypt_handler.reset();
|
||||
|
||||
this->ping.ping_received_timestamp = system_clock::time_point{};
|
||||
}
|
||||
|
||||
void ProtocolHandler::connect() {
|
||||
this->connection_state = connection_state::INIT_LOW;
|
||||
this->connect_timestamp = system_clock::now();
|
||||
this->pow_send_cookie_get();
|
||||
|
||||
{
|
||||
auto command = this->generate_client_initiv();
|
||||
auto packet = make_shared<ClientPacket>(PacketTypeInfo::Command, pipes::buffer_view{command.data(), command.size()});
|
||||
packet->enable_flag(PacketFlag::NewProtocol);
|
||||
this->send_packet(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolHandler::execute_tick() {
|
||||
auto now = system_clock::now();
|
||||
if(this->connection_state < connection_state::DISCONNECTED) {
|
||||
if(!this->pow.last_buffer.empty() && this->pow.last_resend < now - seconds(1)) {
|
||||
this->pow.last_resend = now;
|
||||
this->send_packet(make_shared<ClientPacket>(PacketTypeInfo::Init1, PacketFlag::Unencrypted, this->pow.last_buffer));
|
||||
}
|
||||
|
||||
if(this->connection_state == connection_state::INIT_LOW || this->connection_state == connection_state::INIT_HIGH) {
|
||||
if(this->connect_timestamp < now - seconds(15)) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error("timeout (" + to_string(this->connection_state) + ")"), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(this->connection_state == connection_state::DISCONNECTING) {
|
||||
if(this->disconnect_timestamp < now - seconds(5)) { /* disconnect timeout */
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->execute_resend();
|
||||
|
||||
/* ping */
|
||||
if(this->connection_state == connection_state::CONNECTED) {
|
||||
if(this->ping.ping_send_timestamp + seconds(1) < now)
|
||||
this->ping_send_request();
|
||||
|
||||
if(this->ping.ping_received_timestamp.time_since_epoch().count() > 0) {
|
||||
if(now - this->ping.ping_received_timestamp > seconds(30)) {
|
||||
this->handle->execute_callback_disconnect.call(tr("ping timeout"), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
} else
|
||||
this->ping.ping_received_timestamp = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolHandler::execute_resend() {
|
||||
if(this->connection_state >= connection_state::DISCONNECTED)
|
||||
return;
|
||||
|
||||
deque<pipes::buffer> buffers;
|
||||
auto now = system_clock::now();
|
||||
system_clock::time_point next = now + seconds(5); /* in real we're doing it all 500ms */
|
||||
string error;
|
||||
|
||||
auto resended = this->acknowledge_handler.execute_resend(now, next, buffers, error);
|
||||
if(resended < 0) {
|
||||
log_error(category::connection, tr("Failed to receive acknowledge: {}"), error);
|
||||
|
||||
this->handle->execute_callback_disconnect(tr("packet resend failed"));
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
|
||||
auto socket = this->handle->get_socket();
|
||||
if(socket) {
|
||||
for(const auto& buffer : buffers)
|
||||
socket->send_message(buffer);
|
||||
}
|
||||
|
||||
this->handle->schedule_resend(next);
|
||||
}
|
||||
|
||||
void ProtocolHandler::progress_packet(const pipes::buffer_view &buffer) {
|
||||
if(this->connection_state >= connection_state::DISCONNECTED)
|
||||
return;
|
||||
|
||||
if(buffer.length() < ServerPacket::META_SIZE) {
|
||||
log_error(category::connection, tr("Received a packet which is too small. ({})"), buffer.length());
|
||||
return;
|
||||
}
|
||||
|
||||
auto packet = std::shared_ptr<ts::protocol::ServerPacket>(ts::protocol::ServerPacket::from_buffer(buffer).release());
|
||||
auto packet_type = packet->type();
|
||||
auto packet_id = packet->packetId();
|
||||
auto ordered = packet_type.type() == protocol::COMMAND || packet_type.type() == protocol::COMMAND_LOW;
|
||||
|
||||
/* special handling */
|
||||
if(packet_type.type() == protocol::INIT1) {
|
||||
this->handlePacketInit(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
if(packet_type.type() < 0 || packet_type.type() >= this->_packet_buffers.size()) {
|
||||
log_error(category::connection, tr("Received packet with invalid type. ({})"), packet_type.type());
|
||||
return;
|
||||
}
|
||||
|
||||
auto& read_queue = this->_packet_buffers[packet_type.type()];
|
||||
packet->generationId(read_queue.generation(packet_id));
|
||||
|
||||
if(ordered) {
|
||||
unique_lock queue_lock(read_queue.buffer_lock);
|
||||
auto result = read_queue.accept_index(packet_id);
|
||||
if(result != 0) { /* packet index is ahead buffer index */
|
||||
log_error(category::connection, tr("Failed to verify command packet: {} (Index: {} Current index: {})"), result, packet_id, read_queue.current_index());
|
||||
|
||||
if(result == -1) { /* underflow */
|
||||
/* we've already got the packet, but the client dosn't know that so we've to send the acknowledge again */
|
||||
if(packet->type() == PacketTypeInfo::Command || packet->type() == PacketTypeInfo::CommandLow)
|
||||
this->send_acknowledge(packet->packetId(), packet->type() == PacketTypeInfo::CommandLow);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
packet->setEncrypted(!packet->has_flag(PacketFlag::Unencrypted));
|
||||
if(packet->type() == PacketTypeInfo::Command || packet->type() == PacketTypeInfo::CommandLow){
|
||||
packet->setCompressed(packet->has_flag(PacketFlag::Compressed));
|
||||
}
|
||||
//NOTICE I found out that the Compressed flag is set if the packet contains an audio header
|
||||
|
||||
string error = "success";
|
||||
if(!this->crypt_handler.progressPacketIn(packet.get(), error, false)){
|
||||
if(!this->crypt_handler.use_default()) {
|
||||
if(!this->crypt_handler.progressPacketIn(packet.get(), error, true)){
|
||||
log_error(category::connection, tr("Failed to decrypt packet ({}), even with default key: {}"), packet_type.name(), error);
|
||||
return;
|
||||
} else {
|
||||
log_error(category::connection, tr("Successfully decrypt packet ({} | {}) with default key."), packet_type.name(), packet_id);
|
||||
//FIXME Test if we're in init high
|
||||
}
|
||||
} else {
|
||||
log_error(category::connection, tr("Failed to decrypt packet ({}) with default key: {}"), packet_type.name(), error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(packet->type() == PacketTypeInfo::Command || packet->type() == PacketTypeInfo::CommandLow){
|
||||
if(packet->has_flag(PacketFlag::Unencrypted))
|
||||
return;
|
||||
}
|
||||
|
||||
if(packet->type() == PacketTypeInfo::Command || packet->type() == PacketTypeInfo::CommandLow)
|
||||
this->send_acknowledge(packet->packetId(), packet->type() == PacketTypeInfo::CommandLow);
|
||||
|
||||
{
|
||||
unique_lock queue_lock(read_queue.buffer_lock);
|
||||
|
||||
if(ordered) { /* ordered */
|
||||
if(!read_queue.insert_index(packet_id, std::forward<shared_ptr<ServerPacket>>(packet))) {
|
||||
log_warn(category::connection, tr("Failed to insert ordered packet into queue. ({} | {} | {})"), packet_type.name(), read_queue.current_index(), packet_id);
|
||||
}
|
||||
} else {
|
||||
if(!read_queue.push_back(std::forward<shared_ptr<ServerPacket>>(packet))) {
|
||||
log_warn(category::connection, tr("Failed to insert unordered packet into queue. ({} | {} | {})"), packet_type.name(), read_queue.current_index(), packet_id);
|
||||
/* return; dont stop here because we've to progress the packets */
|
||||
} else {
|
||||
read_queue.index_set(packet_id); /* may we've skipped one packet id */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while(this->handle_packets());
|
||||
}
|
||||
|
||||
bool ProtocolHandler::handle_packets() {
|
||||
if(this->connection_state >= connection_state::DISCONNECTED)
|
||||
return false;
|
||||
|
||||
bool reexecute_handle = false;
|
||||
shared_ptr<ServerPacket> current_packet = nullptr;
|
||||
|
||||
packet_buffer_t* buffer = nullptr;
|
||||
unique_lock<std::recursive_timed_mutex> buffer_lock;
|
||||
unique_lock<std::recursive_timed_mutex> buffer_execute_lock;
|
||||
std::string error = "success";
|
||||
|
||||
|
||||
{
|
||||
auto base_index = this->_packet_buffers_index;
|
||||
auto select_index = base_index;
|
||||
auto max_index = this->_packet_buffers.size();
|
||||
for(uint8_t index = 0; index < max_index; index++) {
|
||||
if(!buffer) select_index++;
|
||||
|
||||
auto& buf = this->_packet_buffers[base_index++ % max_index];
|
||||
unique_lock ring_lock(buf.buffer_lock, try_to_lock);
|
||||
if(!ring_lock.owns_lock()) continue;
|
||||
|
||||
if(buf.front_set()) {
|
||||
if(!buffer) { /* lets still test for reexecute */
|
||||
buffer_execute_lock = unique_lock(buf.execute_lock, try_to_lock);
|
||||
if(!buffer_execute_lock.owns_lock()) continue;
|
||||
|
||||
buffer_lock = move(ring_lock);
|
||||
buffer = &buf;
|
||||
} else {
|
||||
reexecute_handle |= true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->_packet_buffers_index = select_index % max_index; /* garante that we will not hangup with commands! */
|
||||
}
|
||||
|
||||
if(buffer){
|
||||
uint16_t sequence_length = 0;
|
||||
current_packet = buffer->slot_value(sequence_length++);
|
||||
|
||||
if(current_packet) {
|
||||
if((current_packet->type() == PacketTypeInfo::Command || current_packet->type() == PacketTypeInfo::CommandLow) && current_packet->has_flag(PacketFlag::Fragmented)) {
|
||||
do {
|
||||
if(sequence_length >= buffer->capacity()) {
|
||||
log_warn(category::connection, tr("Received fragmented packets which have a too long order. Dropping queue, which will cause a client drop."));
|
||||
buffer->clear();
|
||||
return false;
|
||||
}
|
||||
current_packet = buffer->slot_value(sequence_length++);
|
||||
} while(current_packet && !current_packet->has_flag(PacketFlag::Fragmented));
|
||||
}
|
||||
} else {
|
||||
log_critical(category::connection, tr("buffer->slot_value(sequence_length++) returned nullptr!"));
|
||||
//FIXME!
|
||||
//logCritical(this->client->getServer()->getServerId(), "buffer->slot_value(sequence_length++) returned nullptr!")
|
||||
};
|
||||
|
||||
if(current_packet) { //We could reconstruct a new packet!
|
||||
if(sequence_length > 1) { //We have to merge
|
||||
vector<pipes::buffer> append;
|
||||
append.reserve(sequence_length - 1);
|
||||
|
||||
uint16_t packet_count = 0;
|
||||
current_packet = buffer->pop_front();
|
||||
packet_count++;
|
||||
do {
|
||||
auto packet = buffer->pop_front();
|
||||
packet_count++;
|
||||
if(!packet) {
|
||||
log_critical(category::connection, tr("readQueue->peekNext(seqIndex++) => nullptr_t!"));
|
||||
return false;
|
||||
}
|
||||
|
||||
append.push_back(packet->data());
|
||||
if(packet->has_flag(PacketFlag::Fragmented)) break;
|
||||
} while(packet_count < sequence_length);
|
||||
|
||||
if(packet_count != sequence_length) {
|
||||
log_critical(category::connection, tr("seqIndex != index failed! seqIndex: {} seqLength: {} This may cause a application crash!"), packet_count, sequence_length);
|
||||
sequence_length = packet_count;
|
||||
current_packet = nullptr;
|
||||
} else {
|
||||
current_packet->append_data(append);
|
||||
}
|
||||
} else {
|
||||
if(buffer->pop_front() != current_packet) {
|
||||
log_critical(category::connection, tr("buffer->pop_front() != current_packet failed."));
|
||||
}
|
||||
}
|
||||
reexecute_handle |= buffer->front_set();
|
||||
buffer_lock.unlock(); //We got our packet so release it
|
||||
|
||||
if(current_packet) {
|
||||
if(!this->compression_handler.progressPacketIn(current_packet.get(), error)) {
|
||||
log_error(category::connection, tr("Failed to decompress received packet. Error: {}"), error);
|
||||
current_packet = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(current_packet){
|
||||
auto startTime = chrono::system_clock::now();
|
||||
try {
|
||||
if(current_packet->type() == PacketTypeInfo::Command || current_packet->type() == PacketTypeInfo::CommandLow)
|
||||
this->handlePacketCommand(current_packet);
|
||||
else if(current_packet->type() == PacketTypeInfo::Ack || current_packet->type() == PacketTypeInfo::AckLow)
|
||||
this->handlePacketAck(current_packet);
|
||||
else if(current_packet->type() == PacketTypeInfo::Voice || current_packet->type() == PacketTypeInfo::VoiceWhisper)
|
||||
this->handlePacketVoice(current_packet);
|
||||
else if(current_packet->type() == PacketTypeInfo::Ping || current_packet->type() == PacketTypeInfo::Pong)
|
||||
this->handlePacketPing(current_packet);
|
||||
} catch (std::exception& ex) {
|
||||
log_critical(category::connection, tr("Exception reached root tree! {}"), ex.what());
|
||||
}
|
||||
|
||||
auto end = chrono::system_clock::now();
|
||||
if(end - startTime > chrono::milliseconds(10)) {
|
||||
if(current_packet->type() != PacketTypeInfo::Command && current_packet->type() != PacketTypeInfo::CommandLow) {
|
||||
//FIXME!
|
||||
/*
|
||||
logError(this->client->getServerId(),
|
||||
"{} Handling of packet {} needs more than 10ms ({}ms)",
|
||||
CLIENT_STR_LOG_PREFIX_(this->client),
|
||||
current_packet->type().name(),
|
||||
duration_cast<milliseconds>(end - startTime).count()
|
||||
);
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
if(buffer_execute_lock.owns_lock())
|
||||
buffer_execute_lock.unlock();
|
||||
|
||||
return reexecute_handle;
|
||||
}
|
||||
|
||||
bool ProtocolHandler::create_datagram_packets(std::vector<pipes::buffer> &result, const std::shared_ptr<ts::protocol::ClientPacket> &packet) {
|
||||
string error = "success";
|
||||
|
||||
if(packet->type().compressable() && !packet->memory_state.fragment_entry) {
|
||||
packet->enable_flag(PacketFlag::Compressed);
|
||||
if(!this->compression_handler.progressPacketOut(packet.get(), error)) {
|
||||
log_error(category::connection, tr("Could not compress outgoing packet.\nThis could cause fatal failed for the client.\nError: {}"), error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if(packet->data().length() > packet->type().max_length()){
|
||||
if(!packet->type().fragmentable()) {
|
||||
log_error(category::connection, tr("We've tried to send a too long, not fragmentable packet. Dropping packet of type {} with length {}"), packet->type().name(), packet->data().length());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<shared_ptr<ClientPacket>> siblings;
|
||||
siblings.reserve(8);
|
||||
|
||||
|
||||
{ //Split packets
|
||||
auto buffer = packet->data();
|
||||
|
||||
const auto max_length = packet->type().max_length();
|
||||
while(buffer.length() > max_length * 2) {
|
||||
siblings.push_back(make_shared<ClientPacket>(packet->type(), buffer.view(0, max_length).dup(ts::buffer::allocate_buffer(max_length))));
|
||||
buffer = buffer.range(max_length);
|
||||
}
|
||||
|
||||
if(buffer.length() > max_length) { //Divide rest by 2
|
||||
siblings.push_back(make_shared<ClientPacket>(packet->type(), buffer.view(0, buffer.length() / 2).dup(ts::buffer::allocate_buffer(buffer.length() / 2))));
|
||||
buffer = buffer.range(buffer.length() / 2);
|
||||
}
|
||||
siblings.push_back(make_shared<ClientPacket>(packet->type(), buffer));
|
||||
|
||||
for(const auto& frag : siblings) {
|
||||
frag->setFragmentedEntry(true);
|
||||
frag->enable_flag(PacketFlag::NewProtocol);
|
||||
}
|
||||
}
|
||||
|
||||
assert(siblings.size() >= 2);
|
||||
siblings.front()->enable_flag(PacketFlag::Fragmented);
|
||||
if(packet->has_flag(PacketFlag::Compressed))
|
||||
siblings.front()->enable_flag(PacketFlag::Compressed);
|
||||
|
||||
siblings.back()->enable_flag(PacketFlag::Fragmented);
|
||||
|
||||
if(packet->getListener())
|
||||
siblings.back()->setListener(std::move(packet->getListener())); //Move the listener to the last :)
|
||||
|
||||
result.reserve(siblings.size());
|
||||
for(const auto& frag : siblings)
|
||||
create_datagram_packets(result, frag);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!packet->memory_state.id_branded) {
|
||||
packet->clientId(this->client_id);
|
||||
if(packet->type().type() == PacketType::INIT1) {
|
||||
packet->applyPacketId(101, 0);
|
||||
} else {
|
||||
packet->applyPacketId(this->_packet_id_manager);
|
||||
}
|
||||
//log_trace(category::connection, tr("Packet {} got packet id {}"), packet->type().name(), packet->packetId());
|
||||
}
|
||||
if(!this->crypt_handler.progressPacketOut(packet.get(), error, false)) {
|
||||
log_error(category::connection, tr("Failed to encrypt packet: {}"), error);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
#ifndef CONNECTION_NO_STATISTICS
|
||||
if(this->client && this->client->getServer())
|
||||
this->client->connectionStatistics->logOutgoingPacket(packet);
|
||||
#endif
|
||||
*/
|
||||
result.push_back(packet->buffer());
|
||||
|
||||
this->acknowledge_handler.process_packet(*packet);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProtocolHandler::send_command(const ts::Command &cmd, const std::function<void(bool)> &ack_callback) {
|
||||
auto data = cmd.build();
|
||||
auto packet = make_shared<ClientPacket>(PacketTypeInfo::Command, pipes::buffer_view{data.data(), data.size()});
|
||||
if(ack_callback) {
|
||||
auto begin = chrono::system_clock::now();
|
||||
packet->setListener(make_unique<threads::Future<bool>>());
|
||||
packet->getListener()->waitAndGetLater([ack_callback, begin](bool f) {
|
||||
auto end = chrono::system_clock::now();
|
||||
if(ack_callback)
|
||||
ack_callback(f);
|
||||
|
||||
log_trace(category::connection, tr("Time needed for command: {}"), chrono::duration_cast<chrono::milliseconds>(end - begin).count());
|
||||
});
|
||||
}
|
||||
packet->enable_flag(PacketFlag::NewProtocol);
|
||||
this->send_packet(packet);
|
||||
}
|
||||
|
||||
void ProtocolHandler::send_packet(const std::shared_ptr<ts::protocol::ClientPacket> &packet) {
|
||||
std::vector<pipes::buffer> result;
|
||||
if(!this->create_datagram_packets(result, packet) || result.empty()) {
|
||||
log_error(category::connection, tr("Failed to create datagram packets!"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto socket = this->handle->get_socket();
|
||||
if(!socket) {
|
||||
log_error(category::connection, tr("Failed to get socket!"));
|
||||
return;
|
||||
}
|
||||
|
||||
for(const auto& buffer : result)
|
||||
socket->send_message(buffer);
|
||||
}
|
||||
|
||||
void ProtocolHandler::send_acknowledge(uint16_t packet_id, bool low) {
|
||||
char buffer[2];
|
||||
le2be16(packet_id, buffer);
|
||||
auto packet = make_shared<protocol::ClientPacket>(low ? protocol::PacketTypeInfo::AckLow : protocol::PacketTypeInfo::Ack, 0, pipes::buffer_view{buffer, 2});
|
||||
if(this->connection_state >= connection_state::CONNECTING) {
|
||||
;//packet->toggle(protocol::PacketFlag::NewProtocol, !low);
|
||||
//LivingBots DDOS protection dont want a new protocol here!
|
||||
}
|
||||
this->send_packet(packet);
|
||||
}
|
||||
|
||||
void ProtocolHandler::do_close_connection() {
|
||||
this->connection_state = connection_state::DISCONNECTED;
|
||||
for(auto& buffer : this->_packet_buffers) {
|
||||
lock_guard lock(buffer.buffer_lock);
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolHandler::disconnect(const std::string &reason) {
|
||||
if(this->connection_state >= connection_state::DISCONNECTING)
|
||||
return;
|
||||
|
||||
this->connection_state = connection_state::DISCONNECTING;
|
||||
this->disconnect_timestamp = system_clock::now();
|
||||
|
||||
auto did = ++this->disconnect_id;
|
||||
Command cmd("clientdisconnect");
|
||||
cmd["reasonmsg"] = reason;
|
||||
this->send_command(cmd, [&, did](bool success){
|
||||
/* if !success then we'll have prop already triggered the timeout and this here is obsolete */
|
||||
if(success && this->connection_state == connection_state::DISCONNECTING && this->disconnect_id == did)
|
||||
this->handle->close_connection();
|
||||
});
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
#define NO_LOG
|
||||
#ifdef WIN32
|
||||
#include <WinSock2.h> //Needs to be included; No clue why
|
||||
#endif
|
||||
|
||||
#include <protocol/ringbuffer.h>
|
||||
#include <protocol/Packet.h>
|
||||
#include <protocol/CryptionHandler.h>
|
||||
#include <protocol/CompressionHandler.h>
|
||||
#include <protocol/AcknowledgeManager.h>
|
||||
#include "ServerConnection.h"
|
||||
|
||||
namespace ts {
|
||||
namespace connection {
|
||||
class CryptionHandler;
|
||||
class CompressionHandler;
|
||||
}
|
||||
}
|
||||
|
||||
namespace tc {
|
||||
namespace connection {
|
||||
class ServerConnection;
|
||||
|
||||
namespace connection_state {
|
||||
enum value {
|
||||
INITIALIZING,
|
||||
INIT_LOW,
|
||||
INIT_HIGH,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCONNECTING,
|
||||
DISCONNECTED
|
||||
};
|
||||
};
|
||||
namespace pow_state {
|
||||
enum value : uint8_t {
|
||||
COOKIE_GET,
|
||||
COOKIE_SET,
|
||||
PUZZLE_GET,
|
||||
PUZZLE_SET,
|
||||
PUZZLE_SOLVE,
|
||||
PUZZLE_RESET,
|
||||
COMPLETED,
|
||||
COMMAND_RESET = 127,
|
||||
UNSET = 0xFB
|
||||
};
|
||||
};
|
||||
|
||||
class ProtocolHandler {
|
||||
typedef ts::protocol::PacketRingBuffer<ts::protocol::ServerPacket, 86> packet_buffer_t;
|
||||
typedef std::array<packet_buffer_t, 8> packet_buffers_t;
|
||||
friend class ServerConnection;
|
||||
public:
|
||||
ProtocolHandler(ServerConnection*);
|
||||
~ProtocolHandler();
|
||||
|
||||
void reset();
|
||||
void connect();
|
||||
void execute_tick();
|
||||
void execute_resend();
|
||||
|
||||
void progress_packet(const pipes::buffer_view& /* buffer */);
|
||||
bool handle_packets(); /* if true we have more left */
|
||||
void send_packet(const std::shared_ptr<ts::protocol::ClientPacket>& /* packet */);
|
||||
void send_command(const ts::Command& /* command */, const std::function<void(bool)> & /* acknowledge callback */ = NULL);
|
||||
|
||||
void disconnect(const std::string& /* message */);
|
||||
|
||||
void send_acknowledge(uint16_t /* packet id */, bool /* low */);
|
||||
|
||||
ecc_key& get_identity_key() { return this->crypto.identity; }
|
||||
|
||||
inline std::chrono::microseconds current_ping() { return this->ping.value; }
|
||||
|
||||
connection_state::value connection_state = connection_state::INITIALIZING;
|
||||
server_type::value server_type = server_type::TEASPEAK;
|
||||
private:
|
||||
void do_close_connection(); /* only call from ServerConnection. Close all connections via ServerConnection! */
|
||||
|
||||
void handlePacketCommand(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
void handlePacketAck(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
void handlePacketVoice(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
void handlePacketPing(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
void handlePacketInit(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
|
||||
bool create_datagram_packets(std::vector<pipes::buffer> &result, const std::shared_ptr<ts::protocol::ClientPacket> &packet);
|
||||
|
||||
ServerConnection* handle;
|
||||
|
||||
std::chrono::system_clock::time_point connect_timestamp;
|
||||
std::chrono::system_clock::time_point disconnect_timestamp;
|
||||
uint8_t disconnect_id = 0;
|
||||
|
||||
struct {
|
||||
pow_state::value state;
|
||||
|
||||
uint32_t client_ts3_build_timestamp = 173265950 /* TS3 */; /* needs to be lower than 173265950 for old stuff, else new protocol */
|
||||
uint8_t client_control_data[4] = {0,0,0,0};
|
||||
uint8_t server_control_data[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
|
||||
uint8_t server_data[100];
|
||||
|
||||
std::chrono::system_clock::time_point last_response;
|
||||
std::chrono::system_clock::time_point last_resend;
|
||||
pipes::buffer last_buffer;
|
||||
} pow;
|
||||
void pow_send_cookie_get();
|
||||
|
||||
struct {
|
||||
uint8_t alpha[10];
|
||||
uint8_t beta[54];
|
||||
uint8_t beta_length; /* 10 or 54 */
|
||||
|
||||
ecc_key identity{};
|
||||
|
||||
std::string initiv_command;
|
||||
} crypto;
|
||||
std::string generate_client_initiv();
|
||||
|
||||
uint16_t client_id = 0;
|
||||
ts::protocol::PacketIdManager _packet_id_manager;
|
||||
packet_buffers_t _packet_buffers;
|
||||
uint8_t _packet_buffers_index = 0;
|
||||
|
||||
ts::connection::CryptionHandler crypt_handler;
|
||||
ts::connection::CompressionHandler compression_handler;
|
||||
ts::connection::AcknowledgeManager acknowledge_handler;
|
||||
|
||||
void handleCommandInitIVExpend(ts::Command&);
|
||||
void handleCommandInitIVExpend2(ts::Command&);
|
||||
void handleCommandInitServer(ts::Command&);
|
||||
|
||||
struct {
|
||||
std::chrono::system_clock::time_point ping_send_timestamp;
|
||||
std::chrono::system_clock::time_point ping_received_timestamp;
|
||||
std::chrono::microseconds value;
|
||||
uint16_t ping_id;
|
||||
|
||||
std::chrono::microseconds interval = std::chrono::microseconds(2500);
|
||||
} ping;
|
||||
void ping_send_request();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#include "ProtocolHandler.h"
|
||||
#include "ServerConnection.h"
|
||||
#include "../logger.h"
|
||||
#include <protocol/buffers.h>
|
||||
#include <thread>
|
||||
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::connection;
|
||||
using namespace ts::protocol;
|
||||
using namespace ts;
|
||||
|
||||
void ProtocolHandler::handleCommandInitServer(ts::Command &cmd) {
|
||||
this->client_id = cmd["aclid"];
|
||||
this->connection_state = connection_state::CONNECTED;
|
||||
|
||||
log_info(category::connection, tr("Received own client id: {}"), this->client_id);
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
#include "ProtocolHandler.h"
|
||||
#include "ServerConnection.h"
|
||||
#include "Socket.h"
|
||||
#include "../logger.h"
|
||||
#include <protocol/buffers.h>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <tomcrypt.h>
|
||||
#include <tommath.h>
|
||||
#include <misc/base64.h>
|
||||
#include <misc/digest.h>
|
||||
#include <License.h>
|
||||
#include <ed25519/ed25519.h>
|
||||
#include <ed25519/sha512.h>
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::connection;
|
||||
using namespace ts::protocol;
|
||||
using namespace ts;
|
||||
|
||||
inline void generate_random(uint8_t *destination, size_t length) {
|
||||
while(length-- > 0)
|
||||
*(destination++) = (uint8_t) rand();
|
||||
}
|
||||
|
||||
std::string ProtocolHandler::generate_client_initiv() {
|
||||
if(!this->crypto.initiv_command.empty())
|
||||
return this->crypto.initiv_command;
|
||||
|
||||
/* setup basic parameters */
|
||||
if((this->crypto.alpha[0] & 0x01) == 0) {
|
||||
generate_random(this->crypto.alpha, 10);
|
||||
this->crypto.alpha[0] |= 0x01;
|
||||
}
|
||||
|
||||
Command command("clientinitiv");
|
||||
command["alpha"] = base64::encode((char*) this->crypto.alpha, 10);
|
||||
command["ot"] = 1;
|
||||
command["ip"] = "unknown";
|
||||
|
||||
if(this->server_type != server_type::TEAMSPEAK) /* if TEAMSPEAK then TS3 has been enforced */
|
||||
command.enableParm("teaspeak"); /* using "old" encryption system, we expect a teaspeak=1 within the response */
|
||||
{
|
||||
size_t buffer_length = 265;
|
||||
char buffer[265];
|
||||
auto result = ecc_export((unsigned char *) buffer, (unsigned long*) &buffer_length, PK_PUBLIC, &this->crypto.identity);
|
||||
if(result == CRYPT_OK)
|
||||
command["omega"] = base64::encode(buffer, buffer_length);
|
||||
else
|
||||
log_error(category::connection, tr("Failed to export identiry ({})"), result);
|
||||
}
|
||||
|
||||
this->crypto.initiv_command = command.build(true);
|
||||
return this->crypto.initiv_command;
|
||||
}
|
||||
|
||||
void ProtocolHandler::handleCommandInitIVExpend(ts::Command &cmd) {
|
||||
this->pow.last_buffer = pipes::buffer{};
|
||||
|
||||
auto alpha = base64::decode(cmd["alpha"]);
|
||||
auto beta = base64::decode(cmd["beta"]);
|
||||
auto omega = base64::decode(cmd["omega"]);
|
||||
|
||||
if(alpha.length() != 10 || memcmp(alpha.data(), this->crypto.alpha, 10) != 0) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("alpha key missmatch")), true);
|
||||
this->handle->close_connection();
|
||||
|
||||
log_error(category::connection, tr("InitIVExpend contains invalid alpha"));
|
||||
return;
|
||||
}
|
||||
|
||||
ecc_key server_key{};
|
||||
if(ecc_import((u_char*) omega.data(), omega.length(), &server_key) != CRYPT_OK) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to import server key")), true);
|
||||
this->handle->close_connection();
|
||||
|
||||
log_error(category::connection, tr("InitIVExpend contains invalid key"));
|
||||
return;
|
||||
}
|
||||
|
||||
string error;
|
||||
if(!this->crypt_handler.setupSharedSecret(alpha, beta, &server_key, &this->crypto.identity, error)) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to setup encryption")), true);
|
||||
this->handle->close_connection();
|
||||
|
||||
log_error(category::connection, tr("Failed to setup crypto ({})"), error);
|
||||
return;
|
||||
}
|
||||
|
||||
if(this->server_type == server_type::UNKNOWN) {
|
||||
if(cmd[0].has("teaspeak") && cmd["teaspeak"].as<bool>()) {
|
||||
this->server_type = server_type::TEASPEAK;
|
||||
} else {
|
||||
this->server_type = server_type::TEAMSPEAK;
|
||||
}
|
||||
}
|
||||
|
||||
this->handle->call_connect_result.call(0, true);
|
||||
this->connection_state = connection_state::CONNECTING;
|
||||
}
|
||||
|
||||
|
||||
int __ed_sha512_init(sha512_context* ctx) {
|
||||
ctx->context = new hash_state{};
|
||||
return sha512_init((hash_state*) ctx->context) == CRYPT_OK;
|
||||
}
|
||||
|
||||
int __ed_sha512_final(sha512_context* ctx, unsigned char *out) {
|
||||
assert(ctx->context);
|
||||
|
||||
auto result = sha512_done((hash_state*) ctx->context, out) == CRYPT_OK;
|
||||
delete (hash_state*) ctx->context;
|
||||
return result;
|
||||
}
|
||||
int __ed_sha512_update(sha512_context* ctx, const unsigned char *msg, size_t len) {
|
||||
assert(ctx->context);
|
||||
return sha512_process((hash_state*) ctx->context, msg, len) == CRYPT_OK;
|
||||
}
|
||||
|
||||
static sha512_functions __ed_sha512_functions {
|
||||
__ed_sha512_init,
|
||||
__ed_sha512_final,
|
||||
__ed_sha512_update
|
||||
};
|
||||
|
||||
void ProtocolHandler::handleCommandInitIVExpend2(ts::Command &cmd) {
|
||||
this->pow.last_buffer = pipes::buffer{};
|
||||
|
||||
/* setup ed functions */
|
||||
if(&__ed_sha512_functions != &_ed_sha512_functions)
|
||||
_ed_sha512_functions = __ed_sha512_functions;
|
||||
|
||||
auto beta = base64::decode(cmd["beta"]);
|
||||
auto omega = base64::decode(cmd["omega"]);
|
||||
auto proof = base64::decode(cmd["proof"]);
|
||||
|
||||
auto crypto_chain_data = base64::decode(cmd["l"]);
|
||||
auto crypto_root = cmd[0].has("root") ? base64::decode(cmd["root"]) : string((char*) license::teamspeak::public_root, 32);
|
||||
auto crypto_hash = digest::sha256(crypto_chain_data);
|
||||
|
||||
/* suspecius, tries the server to hide himself? We dont know */
|
||||
if(this->server_type == server_type::UNKNOWN) {
|
||||
if(cmd[0].has("root"))
|
||||
this->server_type = server_type::TEASPEAK;
|
||||
else
|
||||
this->server_type = server_type::TEAMSPEAK;
|
||||
}
|
||||
|
||||
ecc_key server_key{};
|
||||
if(ecc_import((u_char*) omega.data(), omega.length(), &server_key) != CRYPT_OK) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to import server key")), true);
|
||||
this->handle->close_connection();
|
||||
|
||||
log_error(category::connection, tr("InitIVExpend contains invalid key"));
|
||||
return;
|
||||
}
|
||||
|
||||
int result, crypt_result;
|
||||
if((crypt_result = ecc_verify_hash((u_char*) proof.data(), proof.length(), (u_char*) crypto_hash.data(), crypto_hash.length(), &result, &server_key)) != CRYPT_OK || result != 1) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to verify server integrity")), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
|
||||
string error;
|
||||
auto crypto_chain = license::teamspeak::LicenseChain::parse(crypto_chain_data, error, false);
|
||||
if(!crypto_chain) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to read crypto chain")), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
|
||||
auto server_public_key = crypto_chain->generatePublicKey(*(license::teamspeak::LicensePublicKey*) crypto_root.data());
|
||||
crypto_chain->print();
|
||||
|
||||
u_char seed[32];
|
||||
ed25519_create_seed(seed);
|
||||
u_char public_key[32], private_key[64]; /* We need 64 bytes because we're doing some SHA512 actions */
|
||||
ed25519_create_keypair(public_key, private_key, seed);
|
||||
|
||||
/* send clientek response */
|
||||
{
|
||||
size_t sign_buffer_length = 200;
|
||||
char sign_buffer[200];
|
||||
|
||||
prng_state prng_state{};
|
||||
memset(&prng_state, 0, sizeof(prng_state));
|
||||
|
||||
auto proof_data = digest::sha256(string((char*) public_key, 32) + beta);
|
||||
if(ecc_sign_hash((uint8_t*) proof_data.data(), proof_data.length(), (uint8_t*) sign_buffer, (unsigned long*) &sign_buffer_length, &prng_state, find_prng("sprng"), &this->crypto.identity) != CRYPT_OK) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to generate proof of identity")), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
|
||||
Command response("clientek");
|
||||
response["ek"] = base64::encode((char*) public_key, 32);
|
||||
response["proof"] = base64::encode(sign_buffer, sign_buffer_length);
|
||||
/* no need to send this because we're sending the clientinit as the begin packet along with the POW init */
|
||||
//this->_packet_id_manager.nextPacketId(PacketTypeInfo::Command); /* skip the first because we've send our first command within the low level handshake packets */
|
||||
this->send_command(response, [&](bool success){
|
||||
if(success) {
|
||||
/* trigger connected; because the connection has been established on protocol layer */
|
||||
this->handle->call_connect_result.call(0, true);
|
||||
this->connection_state = connection_state::CONNECTING;
|
||||
}
|
||||
}); /* needs to be encrypted at the time! */
|
||||
}
|
||||
|
||||
|
||||
if(!this->crypt_handler.setupSharedSecretNew(string((char*) this->crypto.alpha, 10), beta, (char*) private_key, server_public_key.data())) {
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("failed to setup encryption")), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
#include "ProtocolHandler.h"
|
||||
#include "ServerConnection.h"
|
||||
#include "Socket.h"
|
||||
#include "../logger.h"
|
||||
#include "Error.h"
|
||||
|
||||
#include <misc/endianness.h>
|
||||
#include <protocol/buffers.h>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <tomcrypt.h>
|
||||
#include <tommath.h>
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc::connection;
|
||||
using namespace ts::protocol;
|
||||
using namespace ts;
|
||||
|
||||
inline void generate_random(uint8_t *destination, size_t length) {
|
||||
while(length-- > 0)
|
||||
*(destination++) = (uint8_t) rand();
|
||||
}
|
||||
|
||||
inline void write_reversed(uint8_t* destination, uint8_t* source, size_t length) {
|
||||
destination += length;
|
||||
while(length-- > 0)
|
||||
*(--destination) = *(source++);
|
||||
}
|
||||
|
||||
inline bool solve_puzzle(mp_int& x, mp_int& n, mp_int& result, uint32_t level) {
|
||||
mp_int exp{};
|
||||
mp_init(&exp);
|
||||
mp_2expt(&exp, level);
|
||||
|
||||
|
||||
if (mp_exptmod(&x, &exp, &n, &result) != CRYPT_OK) { //Sometimes it fails (unknown why :D)
|
||||
mp_clear(&exp);
|
||||
return false;
|
||||
}
|
||||
|
||||
mp_clear(&exp);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProtocolHandler::handlePacketInit(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
this->pow.last_response = system_clock::now();
|
||||
|
||||
auto data = packet->data();
|
||||
auto packet_state = static_cast<pow_state::value>(data[0]);
|
||||
|
||||
if(packet_state == pow_state::COMMAND_RESET) {
|
||||
log_trace(category::connection, tr("[POW] Received reset"));
|
||||
this->pow.state = pow_state::COOKIE_SET; /* next expected packet state */
|
||||
this->pow_send_cookie_get();
|
||||
return;
|
||||
}
|
||||
|
||||
log_trace(category::connection, tr("[POW] State {} | {}"), packet_state, data.length());
|
||||
if(packet_state != this->pow.state)
|
||||
return; //TODO handle error?
|
||||
this->acknowledge_handler.reset(); /* we don't need an ack anymore for our init packet */
|
||||
|
||||
if(packet_state == pow_state::COOKIE_SET) {
|
||||
if(data.length() != 21 && data.length() != 5) {
|
||||
log_trace(category::connection, tr("[POW] Dropping cookie packet (got {} bytes expect 21 or 5 bytes)"), data.length());
|
||||
return;
|
||||
}
|
||||
|
||||
if(data.length() == 21) {
|
||||
memcpy(&this->pow.server_control_data[0], &data[1], 16);
|
||||
//TODO test client data reserved bytes
|
||||
} else {
|
||||
auto errc = ntohl(*(uint32_t*) &data[1]);
|
||||
auto err = ts::findError(errc);
|
||||
|
||||
log_error(category::connection, tr("[POW] Received error code: {:x} ({})"), errc, err.message);
|
||||
this->handle->call_connect_result.call(this->handle->errors.register_error(tr("received error: ") + to_string(errc) + " (" + err.message + ")"), true);
|
||||
this->handle->close_connection();
|
||||
return;
|
||||
}
|
||||
|
||||
/* send puzzle get request */
|
||||
{
|
||||
this->pow.state = pow_state::PUZZLE_SET; /* next expected packet state */
|
||||
|
||||
uint8_t response_buffer[25];
|
||||
le2be32(this->pow.client_ts3_build_timestamp, &response_buffer[0]);
|
||||
response_buffer[4] = pow_state::PUZZLE_GET;
|
||||
|
||||
memcpy(&response_buffer[5], this->pow.server_control_data, 16);
|
||||
memcpy(&response_buffer[21], &data[17], 4);
|
||||
|
||||
this->pow.last_buffer = pipes::buffer_view{response_buffer, 25}.own_buffer();
|
||||
this->pow.last_resend = system_clock::now();
|
||||
|
||||
this->send_packet(make_shared<ClientPacket>(PacketTypeInfo::Init1, PacketFlag::Unencrypted, this->pow.last_buffer));
|
||||
}
|
||||
|
||||
return;
|
||||
} else if(packet_state == pow_state::PUZZLE_SET) {
|
||||
constexpr auto expected_bytes = 1 + 64 * 2 + 4 + 100;
|
||||
if(data.length() != 1 + 64 * 2 + 4 + 100) {
|
||||
log_trace(category::connection, tr("[POW] Dropping puzzle packet (got {} bytes expect {} bytes)"), data.length(), expected_bytes);
|
||||
return;
|
||||
}
|
||||
|
||||
mp_int point_x{}, point_n{}, result{};
|
||||
if(mp_read_unsigned_bin(&point_x, (u_char*) &data[1], 64) < 0)
|
||||
return; //TODO handle error
|
||||
|
||||
if(mp_read_unsigned_bin(&point_n, (u_char*) &data[65], 64) < 0) {
|
||||
mp_clear_multi(&point_x, nullptr);
|
||||
return; //TODO handle error
|
||||
}
|
||||
|
||||
log_trace(category::connection, tr("[POW] Received puzzle with level {}"), be2le32(&data[1 + 64 + 64]));
|
||||
if(!solve_puzzle(point_x, point_n, result, be2le32(&data[1 + 64 + 64]))) {
|
||||
mp_clear_multi(&point_x, &point_n, nullptr);
|
||||
log_trace(connection, tr("[POW] Failed to solve puzzle!"));
|
||||
return; //TODO handle error
|
||||
}
|
||||
|
||||
{
|
||||
auto command = this->generate_client_initiv();
|
||||
size_t response_buffer_length = 301 + command.size();
|
||||
auto response_buffer = buffer::allocate_buffer(response_buffer_length);
|
||||
|
||||
le2be32(this->pow.client_ts3_build_timestamp, &response_buffer[0]);
|
||||
response_buffer[4] = pow_state::PUZZLE_SOLVE;
|
||||
memcpy(&response_buffer[5], &data[1], 64 * 2 + 100 + 4);
|
||||
|
||||
auto offset = 4 + 1 + 2 * 64 + 04 + 100;
|
||||
memset(&response_buffer[offset], 0, 64);
|
||||
mp_to_unsigned_bin(&result, (u_char*) &response_buffer[offset]);
|
||||
|
||||
memcpy(&response_buffer[301], command.data(), command.size());
|
||||
|
||||
this->pow.last_buffer = response_buffer;
|
||||
this->pow.last_resend = system_clock::now();
|
||||
this->send_packet(make_shared<ClientPacket>(PacketTypeInfo::Init1, PacketFlag::Unencrypted, this->pow.last_buffer));
|
||||
}
|
||||
mp_clear_multi(&point_x, &point_n, &result, nullptr);
|
||||
this->connection_state = connection_state::INIT_HIGH;
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolHandler::pow_send_cookie_get() {
|
||||
this->pow.state = pow_state::COOKIE_SET; /* next expected packet state */
|
||||
|
||||
if((this->pow.client_control_data[0] & 0x01U) == 0) {
|
||||
generate_random(this->pow.client_control_data, 4);
|
||||
this->pow.client_control_data[0] |= 0x01U;
|
||||
}
|
||||
|
||||
this->pow.client_ts3_build_timestamp = floor < seconds > ((system_clock::now() - hours{24}).time_since_epoch()).count();
|
||||
|
||||
uint8_t response_buffer[21];
|
||||
le2be32(this->pow.client_ts3_build_timestamp, &response_buffer[0]);
|
||||
response_buffer[4] = pow_state::COOKIE_GET;
|
||||
memset(&response_buffer[5], 0, 4);
|
||||
memcpy(&response_buffer[9], &this->pow.client_control_data, 4);
|
||||
memset(&response_buffer[13], 0, 8);
|
||||
|
||||
this->pow.last_buffer = pipes::buffer_view{response_buffer, 21}.own_buffer();
|
||||
this->pow.last_resend = system_clock::now();
|
||||
this->send_packet(make_shared<ClientPacket>(PacketTypeInfo::Init1, PacketFlag::Unencrypted, this->pow.last_buffer));
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
#include "ProtocolHandler.h"
|
||||
#include "ServerConnection.h"
|
||||
#include "Socket.h"
|
||||
#include <protocol/buffers.h>
|
||||
#include <thread>
|
||||
#include <iostream>
|
||||
#include <tomcrypt.h>
|
||||
#include <tommath.h>
|
||||
#include <misc/base64.h>
|
||||
#include <misc/endianness.h>
|
||||
#include <query/command2.h>
|
||||
#include <protocol/Packet.h>
|
||||
#include "audio/VoiceConnection.h"
|
||||
#include "../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::connection;
|
||||
using namespace ts::protocol;
|
||||
using namespace ts;
|
||||
|
||||
//#define LOG_PING
|
||||
|
||||
void ProtocolHandler::handlePacketAck(const std::shared_ptr<ts::protocol::ServerPacket> &ack) {
|
||||
string error;
|
||||
log_trace(category::connection, tr("Handle packet acknowledge for {}"), be2le16(&ack->data()[0]));
|
||||
if(!this->acknowledge_handler.process_acknowledge(*ack, error)) { }
|
||||
}
|
||||
|
||||
void ProtocolHandler::handlePacketCommand(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
//cout << "Received command: " << packet->data().string() << endl;
|
||||
|
||||
std::unique_ptr<Command> command;
|
||||
try {
|
||||
command = make_unique<Command>(packet->asCommand());
|
||||
} catch(const std::invalid_argument& ex) {
|
||||
log_error(category::connection, tr("Failed to parse command (invalid_argument): {}"), ex.what());
|
||||
return;
|
||||
} catch(const ts::command_malformed_exception& ex) {
|
||||
log_error(category::connection, tr("Failed to parse command (command_malformed_exception): {}@{}"), ex.what(), ex.index());
|
||||
return;
|
||||
} catch(const std::exception& ex) {
|
||||
log_error(category::connection, tr("Failed to parse command (exception): {}"), ex.what());
|
||||
return;
|
||||
}
|
||||
|
||||
if(command->command() == "initivexpand") {
|
||||
this->handleCommandInitIVExpend(*command);
|
||||
} else if(command->command() == "initivexpand2") {
|
||||
this->handleCommandInitIVExpend2(*command);
|
||||
} else if(command->command() == "initserver") {
|
||||
this->handleCommandInitServer(*command);
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->handle->pending_commands_lock);
|
||||
this->handle->pending_commands.push_back(move(command));
|
||||
}
|
||||
this->handle->execute_pending_commands();
|
||||
}
|
||||
|
||||
void ProtocolHandler::handlePacketVoice(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
this->handle->voice_connection->process_packet(packet);
|
||||
}
|
||||
|
||||
void ProtocolHandler::handlePacketPing(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
if(packet->type() == PacketTypeInfo::Pong) {
|
||||
uint16_t id = be2le16((char*) packet->data().data_ptr());
|
||||
#ifdef LOG_PING
|
||||
cout << "Received pong (" << id << "|" << this->ping.ping_id << ")" << endl;
|
||||
#endif
|
||||
if(id == this->ping.ping_id) {
|
||||
this->ping.ping_received_timestamp = chrono::system_clock::now();
|
||||
this->ping.value = chrono::duration_cast<chrono::microseconds>(this->ping.ping_received_timestamp - this->ping.ping_send_timestamp);
|
||||
#ifdef LOG_PING
|
||||
cout << "Updating client ping: " << chrono::duration_cast<chrono::microseconds>(this->ping.value).count() << "us" << endl;
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#ifdef LOG_PING
|
||||
cout << "Received ping, sending pong" << endl;
|
||||
#endif
|
||||
|
||||
char buffer[2];
|
||||
le2be16(packet->packetId(), buffer);
|
||||
this->send_packet(make_shared<ClientPacket>(PacketTypeInfo::Pong, PacketFlag::Unencrypted, pipes::buffer_view{buffer, 2}));
|
||||
}
|
||||
}
|
||||
|
||||
void ProtocolHandler::ping_send_request() {
|
||||
auto packet = make_shared<ClientPacket>(PacketTypeInfo::Ping, pipes::buffer_view{});
|
||||
packet->enable_flag(PacketFlag::Unencrypted);
|
||||
this->send_packet(packet);
|
||||
assert(packet->memory_state.id_branded);
|
||||
|
||||
this->ping.ping_send_timestamp = chrono::system_clock::now();
|
||||
this->ping.ping_id = packet->packetId();
|
||||
}
|
||||
@@ -1,727 +0,0 @@
|
||||
#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>
|
||||
|
||||
//#define FUZZ_VOICE
|
||||
//#define SHUFFLE_VOICE
|
||||
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
using namespace tc::connection;
|
||||
|
||||
string ErrorHandler::get_message(ErrorHandler::error_id id) {
|
||||
if(id == 0)
|
||||
return "success";
|
||||
|
||||
auto index = (-id - 1) % this->error_varianz;
|
||||
assert(index >= 0 && index < this->error_varianz);
|
||||
return this->error_messages[index];
|
||||
}
|
||||
|
||||
ErrorHandler::error_id ErrorHandler::register_error(const string &message) {
|
||||
auto index = this->error_index++ % this->error_varianz;
|
||||
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);
|
||||
|
||||
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::error_id 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::error_id 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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
if(identity_key->IsString()) {
|
||||
auto& identity = this->protocol_handler->get_identity_key();
|
||||
auto key = base64::decode(*Nan::Utf8String(identity_key->ToString(Nan::GetCurrentContext()).ToLocalChecked()));
|
||||
if(ecc_import((u_char*) key.data(), (unsigned long) key.length(), &identity) != CRYPT_OK) {
|
||||
Nan::ThrowError(tr("failed to import identity"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
auto& identity = this->protocol_handler->get_identity_key();
|
||||
prng_state rndState{};
|
||||
memset(&rndState, 0, sizeof(prng_state));
|
||||
int err;
|
||||
|
||||
if((err = ecc_make_key_ex(&rndState, find_prng("sprng"), &identity, <c_ecc_sets[5])) != CRYPT_OK) {
|
||||
Nan::ThrowError(tr("failed to generate ephemeral 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));
|
||||
case AF_INET6:
|
||||
((sockaddr_in6*) &remote_address)->sin6_port = htons(remote_port->Int32Value(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
default:break;
|
||||
}
|
||||
|
||||
logger::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); };
|
||||
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_message((ErrorHandler::error_id) 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 = arguments->Get((uint32_t) index);
|
||||
if(!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 = properties->Get(i)->ToString(Nan::GetCurrentContext()).ToLocalChecked();
|
||||
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 = switches->Get((uint32_t) index);
|
||||
if(!object->IsString()) {
|
||||
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);
|
||||
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(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 _buffer = pipes::buffer{ts::protocol::ClientPacket::META_SIZE + buffer_length + 3};
|
||||
auto packet = ts::protocol::ClientPacket::from_buffer(_buffer);
|
||||
memset(&_buffer[ts::protocol::ClientPacket::META_MAC_SIZE], 0, ts::protocol::ClientPacket::META_HEADER_SIZE); /* reset all header data */
|
||||
packet->type(ts::protocol::PacketTypeInfo::Voice);
|
||||
|
||||
auto data_buffer = packet->data();
|
||||
le2be16(this->voice_packet_id++, &data_buffer[0]); /* set voice packet id */
|
||||
data_buffer[2] = (uint8_t) codec; /* set voice codec */
|
||||
if(buffer_length > 0 && buffer)
|
||||
memcpy(&data_buffer[3], buffer, buffer_length);
|
||||
|
||||
if(head) /* head packet */
|
||||
packet->enable_flag(ts::protocol::PacketFlag::Compressed);
|
||||
packet->enable_flag(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(std::shared_ptr<ts::protocol::ClientPacket>(packet.release()));
|
||||
#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->socket->finalize();
|
||||
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();
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
parameters->Set((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];
|
||||
parameters->Set((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());
|
||||
auto& phandler = connection->protocol_handler;
|
||||
if(phandler)
|
||||
info.GetReturnValue().Set((uint32_t) chrono::floor<microseconds>(phandler->current_ping()).count());
|
||||
else
|
||||
info.GetReturnValue().Set(-1);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <nan.h>
|
||||
#include <NanEventCallback.h>
|
||||
#include <condition_variable>
|
||||
#include <pipes/buffer.h>
|
||||
|
||||
namespace ts {
|
||||
class Command;
|
||||
}
|
||||
|
||||
namespace tc {
|
||||
namespace connection {
|
||||
namespace server_type {
|
||||
enum value : uint8_t {
|
||||
UNKNOWN,
|
||||
TEASPEAK,
|
||||
TEAMSPEAK
|
||||
};
|
||||
}
|
||||
|
||||
class UDPSocket;
|
||||
class ProtocolHandler;
|
||||
class VoiceConnection;
|
||||
|
||||
class ErrorHandler {
|
||||
public:
|
||||
typedef int16_t error_id;
|
||||
static constexpr error_id error_success = 0;
|
||||
static constexpr error_id error_varianz = 5;
|
||||
|
||||
|
||||
std::array<std::string, error_varianz> error_messages;
|
||||
error_id error_index = 0;
|
||||
|
||||
std::string get_message(error_id);
|
||||
error_id register_error(const std::string& /* message */);
|
||||
};
|
||||
|
||||
class ServerConnection : public Nan::ObjectWrap {
|
||||
friend class ProtocolHandler;
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
|
||||
static NAN_METHOD(new_instance);
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
ServerConnection();
|
||||
~ServerConnection() override;
|
||||
|
||||
NAN_METHOD(connect);
|
||||
NAN_METHOD(connected);
|
||||
NAN_METHOD(disconnect);
|
||||
|
||||
NAN_METHOD(error_message);
|
||||
NAN_METHOD(send_command);
|
||||
NAN_METHOD(send_voice_data);
|
||||
void send_voice_data(const void* /* buffer */, size_t /* buffer length */, uint8_t /* codec */, bool /* head */);
|
||||
|
||||
NAN_METHOD(send_voice_data_raw);
|
||||
|
||||
void initialize();
|
||||
void finalize();
|
||||
|
||||
void close_connection(); /* directly closes connection without notify etc */
|
||||
std::shared_ptr<UDPSocket> get_socket() { return this->socket; }
|
||||
private:
|
||||
struct VoicePacket {
|
||||
pipes::buffer voice_data;
|
||||
uint16_t client_id;
|
||||
uint16_t packet_id;
|
||||
uint8_t codec_id;
|
||||
bool flag_head;
|
||||
};
|
||||
|
||||
static NAN_METHOD(_connect);
|
||||
static NAN_METHOD(_connected);
|
||||
static NAN_METHOD(_disconnect);
|
||||
static NAN_METHOD(_send_command);
|
||||
static NAN_METHOD(_send_voice_data);
|
||||
static NAN_METHOD(_send_voice_data_raw);
|
||||
static NAN_METHOD(_error_message);
|
||||
static NAN_METHOD(_current_ping);
|
||||
|
||||
std::unique_ptr<Nan::Callback> callback_connect;
|
||||
std::unique_ptr<Nan::Callback> callback_disconnect;
|
||||
Nan::callback_t<ErrorHandler::error_id> call_connect_result;
|
||||
Nan::callback_t<ErrorHandler::error_id> call_disconnect_result;
|
||||
Nan::callback_t<> execute_pending_commands;
|
||||
Nan::callback_t<> execute_pending_voice;
|
||||
Nan::callback_t<std::string> execute_callback_disconnect;
|
||||
|
||||
ErrorHandler errors;
|
||||
std::shared_ptr<UDPSocket> socket;
|
||||
std::unique_ptr<ProtocolHandler> protocol_handler;
|
||||
std::recursive_timed_mutex disconnect_lock;
|
||||
|
||||
std::thread event_thread;
|
||||
std::mutex event_lock;
|
||||
std::condition_variable event_condition;
|
||||
bool event_loop_exit = false; /* set to true if we want to exit */
|
||||
void event_loop();
|
||||
|
||||
bool event_loop_execute_connection_close = false;
|
||||
|
||||
std::chrono::system_clock::time_point next_tick;
|
||||
std::chrono::system_clock::time_point next_resend;
|
||||
void execute_tick();
|
||||
|
||||
void schedule_resend(const std::chrono::system_clock::time_point& /* timestamp */);
|
||||
|
||||
std::mutex pending_commands_lock;
|
||||
std::deque<std::unique_ptr<ts::Command>> pending_commands;
|
||||
void _execute_callback_commands();
|
||||
|
||||
std::mutex pending_voice_lock;
|
||||
std::deque<std::unique_ptr<VoicePacket>> pending_voice;
|
||||
void _execute_callback_voice();
|
||||
uint16_t voice_packet_id = 0;
|
||||
|
||||
void _execute_callback_disconnect(const std::string&);
|
||||
|
||||
std::shared_ptr<VoiceConnection> voice_connection;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
#include "Socket.h"
|
||||
#include "../logger.h"
|
||||
#include <thread>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <WinSock2.h>
|
||||
typedef int socklen_t;
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <netinet/ip.h>
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
using namespace tc::connection;
|
||||
|
||||
UDPSocket::UDPSocket(const sockaddr_storage &address) {
|
||||
memcpy(&this->_remote_address, &address, sizeof(sockaddr_storage));
|
||||
}
|
||||
|
||||
UDPSocket::~UDPSocket() {
|
||||
this->finalize();
|
||||
}
|
||||
|
||||
bool UDPSocket::initialize() {
|
||||
if(this->file_descriptor > 0)
|
||||
return false;
|
||||
|
||||
this->file_descriptor = socket(this->_remote_address.ss_family, SOCK_DGRAM, 0);
|
||||
if(this->file_descriptor < 2) {
|
||||
this->file_descriptor = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: Make configurable
|
||||
*/
|
||||
//uint8_t value = IPTOS_DSCP_EF;
|
||||
//if(setsockopt(this->file_descriptor, IPPROTO_IP, IP_TOS, &value, sizeof(value)) < 0)
|
||||
// log_warn(category::connection, "Failed to set TOS high priority on socket");
|
||||
|
||||
this->io_base = event_base_new();
|
||||
if(!this->io_base) { /* may too many file descriptors already open */
|
||||
this->finalize();
|
||||
return false;
|
||||
}
|
||||
this->event_read = event_new(this->io_base, this->file_descriptor, EV_READ | EV_PERSIST, &UDPSocket::_callback_read, this);
|
||||
this->event_write = event_new(this->io_base, this->file_descriptor, EV_WRITE, &UDPSocket::_callback_write, this);
|
||||
event_add(this->event_read, nullptr);
|
||||
|
||||
this->_io_thread = thread(&UDPSocket::_io_execute, this);
|
||||
#ifdef WIN32
|
||||
//TODO set thread name
|
||||
#else
|
||||
auto handle = this->_io_thread.native_handle();
|
||||
pthread_setname_np(handle, "UDPSocket loop");
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
void UDPSocket::finalize() {
|
||||
if(this->file_descriptor == 0)
|
||||
return;
|
||||
|
||||
unique_lock lock(this->io_lock);
|
||||
auto event_read = this->event_read, event_write = this->event_write;
|
||||
auto io_base = this->io_base;
|
||||
this->io_base = nullptr;
|
||||
this->event_read = nullptr;
|
||||
this->event_write = nullptr;
|
||||
lock.unlock();
|
||||
|
||||
assert(this_thread::get_id() != this->_io_thread.get_id());
|
||||
if(event_read)
|
||||
event_del_block(event_read);
|
||||
if(event_write)
|
||||
event_del_block(event_write);
|
||||
|
||||
if(event_write)
|
||||
event_del(event_write);
|
||||
if(event_read)
|
||||
event_del(event_read);
|
||||
|
||||
if(io_base) {
|
||||
timeval seconds{1, 0};
|
||||
event_base_loopexit(io_base, &seconds);
|
||||
event_base_loopexit(io_base, nullptr);
|
||||
}
|
||||
|
||||
if(this->_io_thread.joinable())
|
||||
this->_io_thread.join();
|
||||
|
||||
if(io_base)
|
||||
event_base_free(io_base);
|
||||
|
||||
#ifdef WIN32
|
||||
if(::closesocket(this->file_descriptor) != 0) {
|
||||
#else
|
||||
if(::close(this->file_descriptor) != 0) {
|
||||
#endif
|
||||
if(errno != EBADF)
|
||||
logger::warn(category::socket, tr("Failed to close file descriptor ({}/{})"), to_string(errno), strerror(errno));
|
||||
}
|
||||
this->file_descriptor = 0;
|
||||
}
|
||||
|
||||
void UDPSocket::_callback_write(evutil_socket_t fd, short, void *_ptr_socket) {
|
||||
((UDPSocket*) _ptr_socket)->callback_write(fd);
|
||||
}
|
||||
|
||||
void UDPSocket::_callback_read(evutil_socket_t fd, short, void *_ptr_socket) {
|
||||
((UDPSocket*) _ptr_socket)->callback_read(fd);
|
||||
}
|
||||
|
||||
void UDPSocket::_io_execute(void *_ptr_socket) {
|
||||
((UDPSocket*) _ptr_socket)->io_execute();
|
||||
}
|
||||
|
||||
void UDPSocket::io_execute() {
|
||||
while(this->io_base) {
|
||||
event_base_loop(this->io_base, 0);
|
||||
}
|
||||
}
|
||||
void UDPSocket::callback_read(evutil_socket_t fd) {
|
||||
sockaddr source_address{};
|
||||
socklen_t source_address_length = sizeof(sockaddr);
|
||||
|
||||
ssize_t buffer_length = 1600; /* IPv6 MTU is ~1.5k */
|
||||
char buffer[1600];
|
||||
|
||||
buffer_length = recvfrom(fd, (char*) buffer, buffer_length, 0, &source_address, &source_address_length);
|
||||
if(buffer_length <= 0) {
|
||||
if(errno == EAGAIN)
|
||||
return;
|
||||
|
||||
logger::warn(category::socket, tr("Failed to receive data: {}/{}"), errno, strerror(errno));
|
||||
return; /* this should never happen! */
|
||||
}
|
||||
|
||||
if(this->on_data)
|
||||
this->on_data(pipes::buffer_view{buffer, (size_t) buffer_length});
|
||||
}
|
||||
|
||||
void UDPSocket::callback_write(evutil_socket_t fd) {
|
||||
unique_lock lock(this->io_lock);
|
||||
if(this->write_queue.empty())
|
||||
return;
|
||||
|
||||
auto buffer = this->write_queue.front();
|
||||
this->write_queue.pop_front();
|
||||
lock.unlock();
|
||||
|
||||
auto written = sendto(fd, buffer.data_ptr<char>(), buffer.length(), 0, (sockaddr*) &this->_remote_address, sizeof(this->_remote_address));
|
||||
if(written != buffer.length()) {
|
||||
if(errno == EAGAIN) {
|
||||
lock.lock();
|
||||
this->write_queue.push_front(buffer);
|
||||
if(this->event_write)
|
||||
event_add(this->event_write, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
return; /* this should never happen! */
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
if(!this->write_queue.empty() && this->event_write)
|
||||
event_add(this->event_write, nullptr);
|
||||
}
|
||||
|
||||
void UDPSocket::send_message(const pipes::buffer_view &buffer) {
|
||||
auto buf = buffer.own_buffer();
|
||||
|
||||
unique_lock lock(this->io_lock);
|
||||
this->write_queue.push_back(buf);
|
||||
if(this->event_write)
|
||||
event_add(this->event_write, nullptr);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <thread>
|
||||
#include <event.h>
|
||||
#include <memory>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
|
||||
#include <pipes/buffer.h>
|
||||
#include <functional>
|
||||
|
||||
#ifndef WIN32
|
||||
#include <netinet/in.h>
|
||||
#else
|
||||
#include <WinSock2.h>
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
namespace connection {
|
||||
class UDPSocket {
|
||||
public:
|
||||
UDPSocket(const sockaddr_storage& /* target */);
|
||||
~UDPSocket();
|
||||
|
||||
const sockaddr_storage& remote_address() { return this->_remote_address; }
|
||||
|
||||
bool initialize();
|
||||
void finalize();
|
||||
|
||||
void send_message(const pipes::buffer_view& /* message */);
|
||||
|
||||
std::function<void(const pipes::buffer_view& /* message */)> on_data;
|
||||
|
||||
const std::thread& io_thread() { return this->_io_thread; }
|
||||
private:
|
||||
static void _io_execute(void *_ptr_socket);
|
||||
static void _callback_read(evutil_socket_t, short, void*);
|
||||
static void _callback_write(evutil_socket_t, short, void*);
|
||||
|
||||
void io_execute();
|
||||
void callback_read(evutil_socket_t);
|
||||
void callback_write(evutil_socket_t);
|
||||
|
||||
sockaddr_storage _remote_address;
|
||||
|
||||
int file_descriptor = 0;
|
||||
|
||||
std::recursive_mutex io_lock;
|
||||
std::thread _io_thread;
|
||||
event_base* io_base = nullptr;
|
||||
|
||||
event* event_read = nullptr;
|
||||
event* event_write = nullptr;
|
||||
|
||||
std::deque<pipes::buffer> write_queue;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// Created by wolverindev on 19.06.19.
|
||||
//
|
||||
|
||||
#include "AudioEventLoop.h"
|
||||
using namespace tc;
|
||||
|
||||
event::EventExecutor* audio::decode_event_loop = nullptr;
|
||||
event::EventExecutor* audio::encode_event_loop = nullptr;
|
||||
|
||||
void audio::init_event_loops() {
|
||||
audio::shutdown_event_loops(); /* just to ensure */
|
||||
|
||||
audio::decode_event_loop = new event::EventExecutor("a en/decode ");
|
||||
audio::encode_event_loop = audio::decode_event_loop;
|
||||
|
||||
audio::decode_event_loop->initialize(2);
|
||||
}
|
||||
|
||||
void audio::shutdown_event_loops() {
|
||||
if(audio::decode_event_loop) {
|
||||
delete audio::decode_event_loop;
|
||||
audio::decode_event_loop = nullptr;
|
||||
}
|
||||
audio::encode_event_loop = nullptr;
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../EventLoop.h"
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
extern event::EventExecutor* encode_event_loop;
|
||||
extern event::EventExecutor* decode_event_loop;
|
||||
|
||||
extern void init_event_loops();
|
||||
extern void shutdown_event_loops();
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
#include "AudioSender.h"
|
||||
#include "VoiceConnection.h"
|
||||
#include "../ServerConnection.h"
|
||||
#include "../../logger.h"
|
||||
#include "AudioEventLoop.h"
|
||||
#include "../../audio/AudioMerger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::audio;
|
||||
using namespace tc::audio::codec;
|
||||
using namespace tc::connection;
|
||||
|
||||
VoiceSender::VoiceSender(tc::connection::VoiceConnection *handle) : handle(handle) {}
|
||||
|
||||
VoiceSender::~VoiceSender() {
|
||||
audio::encode_event_loop->cancel(static_pointer_cast<event::EventEntry>(this->_ref.lock()));
|
||||
this->clear_buffer(); /* buffer might be accessed within encode_raw_frame, but this could not be trigered while this will be deallocated! */
|
||||
}
|
||||
|
||||
bool VoiceSender::initialize_codec(std::string& error, connection::codec::value codec, size_t channels, size_t rate, bool reset_encoder) {
|
||||
auto& data = this->codec[codec];
|
||||
bool new_allocated = !data;
|
||||
if(new_allocated) {
|
||||
data = make_unique<AudioCodec>();
|
||||
data->packet_counter = 0;
|
||||
data->last_packet = chrono::system_clock::now();
|
||||
}
|
||||
|
||||
auto info = codec::get_info(codec);
|
||||
if(!info || !info->supported) {
|
||||
if(new_allocated)
|
||||
log_error(category::voice_connection, tr("Tried to send voice packet but we dont support the current codec ({})"), codec);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!data->converter) {
|
||||
data->converter = info->new_converter(error);
|
||||
if(!data->converter)
|
||||
return false;
|
||||
} else if(reset_encoder) {
|
||||
data->converter->reset_encoder();
|
||||
}
|
||||
|
||||
if(!data->resampler || data->resampler->input_rate() != rate)
|
||||
data->resampler = make_shared<AudioResampler>(rate, data->converter->sample_rate(), data->converter->channels());
|
||||
|
||||
if(!data->resampler->valid()) {
|
||||
error = "resampler is invalid";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VoiceSender::set_voice_send_enabled(bool flag) {
|
||||
this->voice_send_enabled = flag;
|
||||
}
|
||||
|
||||
void VoiceSender::send_data(const void *data, size_t samples, size_t rate, size_t channels) {
|
||||
unique_lock lock(this->_execute_lock);
|
||||
if(!this->handle) {
|
||||
log_warn(category::voice_connection, tr("Dropping raw audio frame because of an invalid handle."));
|
||||
return;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
if(!this->voice_send_enabled) {
|
||||
log_warn(category::voice_connection, tr("Dropping raw audio frame because voice sending has been disabled!"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto frame = make_unique<AudioFrame>();
|
||||
frame->sample_rate = rate;
|
||||
frame->channels = channels;
|
||||
frame->buffer = pipes::buffer{(void*) data, samples * channels * 4};
|
||||
frame->timestamp = chrono::system_clock::now();
|
||||
|
||||
{
|
||||
lock_guard buffer_lock(this->raw_audio_buffer_lock);
|
||||
this->raw_audio_buffers.push_back(move(frame));
|
||||
}
|
||||
|
||||
audio::encode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->_ref.lock()));
|
||||
}
|
||||
|
||||
void VoiceSender::send_stop() {
|
||||
unique_lock lock(this->_execute_lock);
|
||||
if(!this->handle) {
|
||||
log_warn(category::voice_connection, tr("Dropping audio end frame because of an invalid handle."));
|
||||
return;
|
||||
}
|
||||
lock.unlock();
|
||||
|
||||
|
||||
auto frame = make_unique<AudioFrame>();
|
||||
frame->sample_rate = 0;
|
||||
frame->channels = 0;
|
||||
frame->buffer = pipes::buffer{nullptr, 0};
|
||||
frame->timestamp = chrono::system_clock::now();
|
||||
|
||||
{
|
||||
lock_guard buffer_lock(this->raw_audio_buffer_lock);
|
||||
this->raw_audio_buffers.push_back(move(frame));
|
||||
}
|
||||
|
||||
audio::encode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->_ref.lock()));
|
||||
}
|
||||
|
||||
void VoiceSender::finalize() {
|
||||
lock_guard lock(this->_execute_lock);
|
||||
this->handle = nullptr;
|
||||
}
|
||||
|
||||
void VoiceSender::event_execute(const std::chrono::system_clock::time_point &point) {
|
||||
static auto max_time = chrono::milliseconds(10);
|
||||
|
||||
bool reschedule = false;
|
||||
auto now = chrono::system_clock::now();
|
||||
while(true) {
|
||||
unique_lock buffer_lock(this->raw_audio_buffer_lock);
|
||||
if(this->raw_audio_buffers.empty())
|
||||
break;
|
||||
|
||||
if(chrono::system_clock::now() - now > max_time) {
|
||||
reschedule = true;
|
||||
break;
|
||||
}
|
||||
|
||||
auto entry = move(this->raw_audio_buffers.front());
|
||||
this->raw_audio_buffers.pop_front();
|
||||
buffer_lock.unlock();
|
||||
|
||||
//TODO: Drop too old buffers!
|
||||
this->encode_raw_frame(entry);
|
||||
}
|
||||
|
||||
if(reschedule) {
|
||||
log_warn(category::voice_connection, tr("Audio data decode will take longer than {} us. Enqueueing for later"), chrono::duration_cast<chrono::microseconds>(max_time).count());
|
||||
audio::decode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->_ref.lock()));
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceSender::encode_raw_frame(const std::unique_ptr<AudioFrame> &frame) {
|
||||
auto codec = this->_current_codec;
|
||||
auto& codec_data = this->codec[codec];
|
||||
|
||||
bool flag_head = true, flag_reset = true;
|
||||
if(codec_data) {
|
||||
if(codec_data->last_packet + chrono::seconds(1) < frame->timestamp)
|
||||
codec_data->packet_counter = 0;
|
||||
|
||||
flag_head = codec_data->packet_counter < 5;
|
||||
flag_reset = codec_data->packet_counter == 0;
|
||||
|
||||
codec_data->packet_counter++;
|
||||
codec_data->last_packet = frame->timestamp;
|
||||
}
|
||||
|
||||
if(frame->channels == 0 || frame->sample_rate == 0 || frame->buffer.empty()) {
|
||||
lock_guard lock(this->_execute_lock);
|
||||
if(!this->handle) {
|
||||
log_warn(category::voice_connection, tr("Dropping audio end because of an invalid handle."));
|
||||
return;
|
||||
}
|
||||
|
||||
if(codec_data)
|
||||
codec_data->packet_counter = 0;
|
||||
auto server = this->handle->handle();
|
||||
server->send_voice_data(this->_buffer, 0, codec, flag_head);
|
||||
return;
|
||||
}
|
||||
|
||||
string error;
|
||||
if(flag_reset) {
|
||||
log_trace(category::voice_connection, tr("Resetting encoder for voice sender"));
|
||||
}
|
||||
if(!this->initialize_codec(error, codec, frame->channels, frame->sample_rate, flag_reset)) {
|
||||
log_error(category::voice_connection, tr("Failed to initialize codec: {}"), error);
|
||||
return;
|
||||
}
|
||||
|
||||
auto merged_channel_byte_size = codec_data->converter->channels() * (frame->buffer.length() / frame->channels);
|
||||
auto estimated_resampled_byte_size = codec_data->resampler->estimated_output_size(merged_channel_byte_size);
|
||||
this->ensure_buffer(max(estimated_resampled_byte_size, merged_channel_byte_size));
|
||||
|
||||
auto codec_channels = codec_data->converter->channels();
|
||||
if(!audio::merge::merge_channels_interleaved(this->_buffer, codec_channels, frame->buffer.data_ptr(), frame->channels, frame->buffer.length() / frame->channels / 4)) {
|
||||
log_warn(category::voice_connection, tr("Failed to merge channels to output stream channel count! Dropping local voice packet"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto resampled_samples = codec_data->resampler->process(this->_buffer, this->_buffer, merged_channel_byte_size / codec_channels / 4);
|
||||
if(resampled_samples <= 0) {
|
||||
log_error(category::voice_connection, tr("Resampler returned {}"), resampled_samples);
|
||||
return;
|
||||
}
|
||||
|
||||
if(resampled_samples * codec_channels * 4 != codec_data->converter->bytes_per_frame()) {
|
||||
log_error(category::voice_connection,
|
||||
tr("Could not encode audio frame. Frame length is not equal to code frame length! Codec: {}, Packet: {}"),
|
||||
codec_data->converter->bytes_per_frame(), resampled_samples * codec_channels * 4
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
char _packet_buffer[512];
|
||||
auto encoded_bytes = codec_data->converter->encode(error, this->_buffer, _packet_buffer, 512);
|
||||
if(encoded_bytes <= 0) {
|
||||
log_error(category::voice_connection, tr("Failed to encode voice: {}"), error);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->_execute_lock);
|
||||
if(!this->handle) {
|
||||
log_warn(category::voice_connection, tr("Dropping audio frame because of an invalid handle."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto server = this->handle->handle();
|
||||
server->send_voice_data(_packet_buffer, encoded_bytes, codec, flag_head);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include "VoiceClient.h"
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace codec {
|
||||
class Converter;
|
||||
}
|
||||
|
||||
class AudioResampler;
|
||||
class AudioOutputSource;
|
||||
}
|
||||
|
||||
namespace connection {
|
||||
class VoiceConnection;
|
||||
class VoiceSender : private event::EventEntry {
|
||||
template<typename _Tp, typename _Up>
|
||||
friend inline std::shared_ptr<_Tp> std::static_pointer_cast(const std::shared_ptr<_Up>& __r) noexcept;
|
||||
friend class VoiceConnection;
|
||||
public:
|
||||
VoiceSender(VoiceConnection*);
|
||||
virtual ~VoiceSender();
|
||||
|
||||
codec::value get_codec() { return this->_current_codec; }
|
||||
void set_codec(codec::value value) { this->_current_codec = value; }
|
||||
|
||||
|
||||
void finalize();
|
||||
void send_data(const void* /* buffer */, size_t /* samples */, size_t /* sample rate */, size_t /* channels */);
|
||||
void send_stop();
|
||||
|
||||
void set_voice_send_enabled(bool /* flag */);
|
||||
private:
|
||||
std::weak_ptr<VoiceSender> _ref;
|
||||
VoiceConnection* handle;
|
||||
|
||||
struct AudioCodec {
|
||||
size_t packet_counter = 0;
|
||||
std::chrono::system_clock::time_point last_packet;
|
||||
|
||||
std::shared_ptr<audio::codec::Converter> converter;
|
||||
std::shared_ptr<audio::AudioResampler> resampler;
|
||||
};
|
||||
std::array<std::unique_ptr<AudioCodec>, codec::MAX + 1> codec{nullptr};
|
||||
|
||||
bool initialize_codec(std::string&, codec::value /* codec */, size_t /* channels */, size_t /* source sample rate */, bool /* reset decoder */);
|
||||
|
||||
codec::value _current_codec = codec::OPUS_VOICE;
|
||||
|
||||
std::mutex _execute_lock;
|
||||
|
||||
void* _buffer = nullptr;
|
||||
size_t _buffer_size = 0;
|
||||
void clear_buffer() {
|
||||
if(this->_buffer)
|
||||
::free(this->_buffer);
|
||||
this->_buffer = nullptr;
|
||||
this->_buffer_size = 0;
|
||||
}
|
||||
|
||||
void ensure_buffer(size_t length) {
|
||||
if(!this->_buffer || this->_buffer_size < length) {
|
||||
if(this->_buffer)
|
||||
::free(this->_buffer);
|
||||
this->_buffer = malloc(length);
|
||||
this->_buffer_size = length;
|
||||
}
|
||||
}
|
||||
|
||||
struct AudioFrame {
|
||||
pipes::buffer buffer;
|
||||
size_t sample_rate;
|
||||
size_t channels;
|
||||
|
||||
std::chrono::system_clock::time_point timestamp;
|
||||
};
|
||||
|
||||
std::mutex raw_audio_buffer_lock;
|
||||
std::deque<std::unique_ptr<AudioFrame>> raw_audio_buffers;
|
||||
|
||||
bool voice_send_enabled = false;
|
||||
|
||||
void encode_raw_frame(const std::unique_ptr<AudioFrame>&);
|
||||
void event_execute(const std::chrono::system_clock::time_point &point) override;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
#include "VoiceClient.h"
|
||||
#include "../../audio/AudioOutput.h"
|
||||
#include "../../audio/codec/Converter.h"
|
||||
#include "../../audio/codec/OpusConverter.h"
|
||||
#include "../../audio/AudioMerger.h"
|
||||
#include "../../audio/js/AudioOutputStream.h"
|
||||
#include "AudioEventLoop.h"
|
||||
#include "../../logger.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::audio::codec;
|
||||
using namespace tc::connection;
|
||||
|
||||
extern tc::audio::AudioOutput* global_audio_output;
|
||||
|
||||
#define DEBUG_PREMATURE_PACKETS
|
||||
|
||||
#ifdef WIN32
|
||||
#define _field_(name, value) value
|
||||
#else
|
||||
#define _field_(name, value) .name = value
|
||||
#endif
|
||||
const codec::condec_info codec::info[6] = {
|
||||
{
|
||||
_field_(supported, false),
|
||||
_field_(name, "speex_narrowband"),
|
||||
_field_(new_converter, nullptr)
|
||||
},
|
||||
{
|
||||
_field_(supported, false),
|
||||
_field_(name, "speex_wideband"),
|
||||
_field_(new_converter, nullptr)
|
||||
},
|
||||
{
|
||||
_field_(supported, false),
|
||||
_field_(name, "speex_ultra_wideband"),
|
||||
_field_(new_converter, nullptr)
|
||||
},
|
||||
{
|
||||
_field_(supported, false),
|
||||
_field_(name, "celt_mono"),
|
||||
_field_(new_converter, nullptr)
|
||||
},
|
||||
{
|
||||
_field_(supported, true),
|
||||
_field_(name, "opus_voice"),
|
||||
_field_(new_converter, [](string& error) -> shared_ptr<Converter> {
|
||||
auto result = make_shared<OpusConverter>(1, 48000, 960);
|
||||
if(!result->initialize(error, OPUS_APPLICATION_VOIP))
|
||||
return nullptr;
|
||||
return dynamic_pointer_cast<Converter>(result);
|
||||
})
|
||||
},
|
||||
{
|
||||
_field_(supported, true),
|
||||
_field_(name, "opus_music"),
|
||||
_field_(new_converter, [](string& error) -> shared_ptr<Converter> {
|
||||
auto result = make_shared<OpusConverter>(2, 48000, 960);
|
||||
if(!result->initialize(error, OPUS_APPLICATION_AUDIO))
|
||||
return nullptr;
|
||||
return dynamic_pointer_cast<Converter>(result);
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
void VoiceClientWrap::do_wrap(const v8::Local<v8::Object> &object) {
|
||||
this->Wrap(object);
|
||||
|
||||
auto handle = this->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
Nan::Set(object, Nan::New<v8::String>("client_id").ToLocalChecked(), Nan::New<v8::Number>(handle->client_id()));
|
||||
handle->on_state_changed = [&]{ this->call_state_changed(); };
|
||||
|
||||
this->call_state_changed = Nan::async_callback([&]{
|
||||
Nan::HandleScope scope;
|
||||
this->_call_state_changed();
|
||||
});
|
||||
}
|
||||
|
||||
void VoiceClientWrap::_call_state_changed() {
|
||||
auto handle = this->_handle.lock();
|
||||
if(!handle) {
|
||||
log_warn(category::voice_connection, tr("State changed on invalid handle!"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto state = handle->state();
|
||||
auto call_playback_callback = state == VoiceClient::state::playing && !this->_currently_playing;
|
||||
auto call_stopped_callback = state == VoiceClient::state::stopped && this->_currently_playing;
|
||||
|
||||
if(state == VoiceClient::state::stopped)
|
||||
this->_currently_playing = false;
|
||||
if(state == VoiceClient::state::playing)
|
||||
this->_currently_playing = true;
|
||||
|
||||
if(call_playback_callback) {
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_playback").ToLocalChecked()).ToLocalChecked();
|
||||
if(callback->IsFunction())
|
||||
callback.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||
}
|
||||
if(call_stopped_callback) {
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_stopped").ToLocalChecked()).ToLocalChecked();
|
||||
if(callback->IsFunction())
|
||||
callback.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||
}
|
||||
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_state_changed").ToLocalChecked()).ToLocalChecked();
|
||||
if(callback->IsFunction()) {
|
||||
v8::Local<v8::Value> argv[1] = {
|
||||
Nan::New<v8::Number>(state)
|
||||
};
|
||||
callback.As<v8::Function>()->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, argv);
|
||||
}
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(VoiceClientWrap::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(VoiceClientWrap::NewInstance);
|
||||
klass->SetClassName(Nan::New("VoiceConnection").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_state", VoiceClientWrap::_get_state);
|
||||
Nan::SetPrototypeMethod(klass, "get_volume", VoiceClientWrap::_get_volume);
|
||||
Nan::SetPrototypeMethod(klass, "set_volume", VoiceClientWrap::_set_volume);
|
||||
Nan::SetPrototypeMethod(klass, "abort_replay", VoiceClientWrap::_abort_replay);
|
||||
Nan::SetPrototypeMethod(klass, "get_stream", VoiceClientWrap::_get_stream);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceClientWrap::NewInstance) {
|
||||
if(!info.IsConstructCall())
|
||||
Nan::ThrowError("invalid invoke!");
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceClientWrap::_get_volume) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceClientWrap>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(handle->get_volume());
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceClientWrap::_set_volume) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceClientWrap>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->set_volume(info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
NAN_METHOD(VoiceClientWrap::_abort_replay) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceClientWrap>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->cancel_replay();
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceClientWrap::_get_state) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceClientWrap>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(handle->state());
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(VoiceClientWrap::_get_stream) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceClientWrap>(info.Holder());
|
||||
|
||||
auto handle = client->_handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("weak handle");
|
||||
return;
|
||||
}
|
||||
|
||||
auto wrapper = new audio::AudioOutputStreamWrapper(handle->output_stream(), false);
|
||||
auto object = Nan::NewInstance(Nan::New(audio::AudioOutputStreamWrapper::constructor()), 0, nullptr).ToLocalChecked();
|
||||
wrapper->do_wrap(object);
|
||||
info.GetReturnValue().Set(object);
|
||||
}
|
||||
|
||||
VoiceClientWrap::VoiceClientWrap(const std::shared_ptr<VoiceClient>& client) : _handle(client) { }
|
||||
|
||||
VoiceClientWrap::~VoiceClientWrap() {}
|
||||
|
||||
VoiceClient::VoiceClient(const std::shared_ptr<VoiceConnection>&, uint16_t client_id) : _client_id(client_id) {
|
||||
this->output_source = global_audio_output->create_source();
|
||||
this->output_source->overflow_strategy = audio::overflow_strategy::ignore;
|
||||
this->output_source->max_latency = (size_t) ceil(this->output_source->sample_rate * 1);
|
||||
this->output_source->min_buffer = (size_t) ceil(this->output_source->sample_rate * 0.04);
|
||||
|
||||
this->output_source->on_underflow = [&]{
|
||||
if(this->_state == state::stopping)
|
||||
this->set_state(state::stopped);
|
||||
else if(this->_state != state::stopped) {
|
||||
if(this->_last_received_packet + chrono::seconds(1) < chrono::system_clock::now()) {
|
||||
this->set_state(state::stopped);
|
||||
log_warn(category::audio, tr("Client {} has a audio buffer underflow and not received any data for one second. Stopping replay."), this->_client_id);
|
||||
} else {
|
||||
if(this->_state != state::buffering) {
|
||||
log_warn(category::audio, tr("Client {} has a audio buffer underflow. Buffer again and try to replay prematured packets."), this->_client_id);
|
||||
this->set_state(state::buffering);
|
||||
}
|
||||
|
||||
play_premature_packets = true; /* try to replay any premature packets because we assume that the other packets got lost */
|
||||
audio::decode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->ref()));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
this->output_source->on_overflow = [&](size_t count){
|
||||
log_warn(category::audio, tr("Client {} has a audio buffer overflow of {}."), this->_client_id, count);
|
||||
};
|
||||
}
|
||||
|
||||
VoiceClient::~VoiceClient() {
|
||||
if(v8::Isolate::GetCurrent())
|
||||
this->finalize_js_object();
|
||||
else {
|
||||
assert(this->_js_handle.IsEmpty());
|
||||
}
|
||||
|
||||
this->output_source->on_underflow = nullptr; /* to ensure */
|
||||
global_audio_output->delete_source(this->output_source);
|
||||
}
|
||||
|
||||
void VoiceClient::initialize_js_object() {
|
||||
if(!this->_js_handle.IsEmpty())
|
||||
return;
|
||||
|
||||
auto object_wrap = new VoiceClientWrap(this->ref());
|
||||
auto object = Nan::NewInstance(Nan::New(VoiceClientWrap::constructor()), 0, nullptr).ToLocalChecked();
|
||||
Nan::TryCatch tc;
|
||||
object_wrap->do_wrap(object);
|
||||
if(tc.HasCaught()) {
|
||||
tc.ReThrow();
|
||||
return;
|
||||
}
|
||||
|
||||
this->_js_handle.Reset(Nan::GetCurrentContext()->GetIsolate(), object);
|
||||
}
|
||||
|
||||
void VoiceClient::finalize_js_object() {
|
||||
this->_js_handle.Reset();
|
||||
}
|
||||
|
||||
#define target_buffer_length 16384
|
||||
void VoiceClient::process_packet(uint16_t packet_id, const pipes::buffer_view& buffer, codec::value codec, bool head) {
|
||||
if(this->_volume == 0)
|
||||
return;
|
||||
|
||||
if(codec < 0 || codec > this->codec.size()) {
|
||||
log_warn(category::voice_connection, tr("Received voice packet from client {} with unknown codec ({})"), this->_client_id, codec);
|
||||
return;
|
||||
}
|
||||
|
||||
auto encoded_buffer = make_unique<EncodedBuffer>();
|
||||
encoded_buffer->packet_id = packet_id;
|
||||
encoded_buffer->codec = codec;
|
||||
encoded_buffer->receive_timestamp = chrono::system_clock::now();
|
||||
encoded_buffer->buffer = buffer.own_buffer();
|
||||
encoded_buffer->head = head;
|
||||
|
||||
this->_last_received_packet = encoded_buffer->receive_timestamp;
|
||||
{
|
||||
lock_guard lock(this->audio_decode_queue_lock);
|
||||
this->audio_decode_queue.push_back(move(encoded_buffer));
|
||||
}
|
||||
|
||||
audio::decode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->ref()));
|
||||
}
|
||||
|
||||
void VoiceClient::cancel_replay() {
|
||||
log_trace(category::voice_connection, tr("Cancel replay for client {}"), this->_client_id);
|
||||
this->output_source->clear();
|
||||
this->set_state(state::stopped);
|
||||
}
|
||||
|
||||
void VoiceClient::event_execute(const std::chrono::system_clock::time_point &scheduled) {
|
||||
static auto max_time = chrono::milliseconds(10);
|
||||
|
||||
bool reschedule = false;
|
||||
auto now = chrono::system_clock::now();
|
||||
while(true) {
|
||||
unique_lock buffer_lock(this->audio_decode_queue_lock);
|
||||
if(this->play_premature_packets) {
|
||||
this->play_premature_packets = false;
|
||||
|
||||
for(auto& codec_data : this->codec) {
|
||||
if(!codec_data) continue;
|
||||
|
||||
if(!codec_data->premature_packets.empty()) {
|
||||
size_t play_count = 0;
|
||||
while(!codec_data->premature_packets.empty()) {
|
||||
auto& packet = codec_data->premature_packets.front();
|
||||
|
||||
//Test if we're able to replay stuff again
|
||||
if((uint16_t) (codec_data->last_packet_id + 1) < packet.packet_id && play_count > 0) //Only check for the order if we replayed one already
|
||||
break; //Nothing new
|
||||
|
||||
this->output_source->enqueue_samples(packet.buffer);
|
||||
codec_data->last_packet_id = packet.packet_id;
|
||||
codec_data->premature_packets.pop_front();
|
||||
play_count++;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_PREMATURE_PACKETS
|
||||
if(play_count > 0)
|
||||
log_debug(category::audio, tr("Replayed (buffer underflow) {} premature packets for client {}"), play_count, this->_client_id);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(this->audio_decode_queue.empty())
|
||||
break;
|
||||
|
||||
if(chrono::system_clock::now() - now > max_time) {
|
||||
reschedule = true;
|
||||
break;
|
||||
}
|
||||
|
||||
auto entry = move(this->audio_decode_queue.front());
|
||||
this->audio_decode_queue.pop_front();
|
||||
buffer_lock.unlock();
|
||||
|
||||
//TODO: Drop too old buffers!
|
||||
this->process_encoded_buffer(entry);
|
||||
}
|
||||
|
||||
if(audio_decode_event_dropped.exchange(false) && !reschedule) {
|
||||
//Is not really a warning, it happens all the time and isn't really an issue
|
||||
//log_warn(category::voice_connection, tr("Dropped auto enqueue event execution for client {}. No reschedulling planned, hopefully we processed all buffers."), this->_client_id);
|
||||
}
|
||||
|
||||
if(reschedule) {
|
||||
log_warn(category::voice_connection, tr("Audio data decode will take longer than {} us. Enqueueing for later"), chrono::duration_cast<chrono::microseconds>(max_time).count());
|
||||
audio::decode_event_loop->schedule(static_pointer_cast<event::EventEntry>(this->ref()));
|
||||
}
|
||||
}
|
||||
|
||||
#define MAX_LOST_PACKETS (6)
|
||||
//Note: This function must be executed single threaded
|
||||
void VoiceClient::process_encoded_buffer(const std::unique_ptr<EncodedBuffer> &buffer) {
|
||||
string error;
|
||||
|
||||
auto& codec_data = this->codec[buffer->codec];
|
||||
|
||||
if(!codec_data) {
|
||||
auto info = codec::get_info(buffer->codec);
|
||||
if(!info || !info->supported) {
|
||||
log_warn(category::voice_connection, tr("Received voice packet from client {}, but we dont support it ({})"), this->_client_id, buffer->codec);
|
||||
return;
|
||||
}
|
||||
|
||||
auto instance = make_unique<AudioCodec>();
|
||||
instance->successfully_initialized = false;
|
||||
|
||||
instance->last_packet_id = (uint16_t) (buffer->packet_id - 1); /* could be 0xFFFF */
|
||||
instance->converter = info->new_converter(error);
|
||||
if(!instance->converter) {
|
||||
codec_data = move(instance);
|
||||
|
||||
log_warn(category::voice_connection, tr("Failed to initialize new codec {} for client {}: {}"), buffer->codec, this->_client_id, error);
|
||||
return;
|
||||
}
|
||||
|
||||
instance->resampler = make_shared<audio::AudioResampler>(instance->converter->sample_rate(), this->output_source->sample_rate, instance->converter->channels());
|
||||
if(!instance->resampler->valid()) {
|
||||
codec_data = move(instance);
|
||||
|
||||
log_warn(category::voice_connection, tr("Failed to initialize new codec resampler {} for client {}"), buffer->codec, this->_client_id);
|
||||
return;
|
||||
}
|
||||
instance->successfully_initialized = true;
|
||||
codec_data = move(instance);
|
||||
} else if(!codec_data->successfully_initialized) {
|
||||
return; /* already failed ignore that stuff */
|
||||
}
|
||||
|
||||
uint16_t diff;
|
||||
bool premature = false;
|
||||
|
||||
if(codec_data->last_packet_timestamp + chrono::seconds(1) < buffer->receive_timestamp || this->_state >= state::stopping) {
|
||||
diff = 0xFFFF;
|
||||
} else {
|
||||
if(codec_data->last_packet_id > buffer->packet_id) {
|
||||
auto local_index = (uint16_t) (codec_data->last_packet_id + MAX_LOST_PACKETS);
|
||||
if(local_index < buffer->packet_id)
|
||||
diff = 0xFF; /* we got in a new generation */
|
||||
else {
|
||||
log_warn(category::audio,
|
||||
tr("Received voice packet for client {} with is older than the last we received (Current index: {}, Packet index: {}). Dropping packet."),
|
||||
this->_client_id, buffer->packet_id, codec_data->last_packet_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
diff = buffer->packet_id - codec_data->last_packet_id;
|
||||
}
|
||||
}
|
||||
|
||||
const auto old_packet_id = codec_data->last_packet_id;
|
||||
codec_data->last_packet_timestamp = buffer->receive_timestamp;
|
||||
|
||||
if(buffer->buffer.empty()) {
|
||||
/* lets playback the last samples and we're done */
|
||||
this->set_state(state::stopping);
|
||||
|
||||
/* enqueue all premature packets (list should be already ordered!) */
|
||||
{
|
||||
unique_lock buffer_lock(this->audio_decode_queue_lock);
|
||||
for(const auto& packet : codec_data->premature_packets)
|
||||
this->output_source->enqueue_samples(packet.buffer);
|
||||
codec_data->premature_packets.clear();
|
||||
}
|
||||
|
||||
log_trace(category::voice_connection, tr("Stopping replay for client {}. Empty buffer!"), this->_client_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if(diff == 0) {
|
||||
//Duplicated packets
|
||||
log_warn(category::audio, tr("Received voice packet with the same ID then the last one. Dropping packet."));
|
||||
return;
|
||||
} else
|
||||
diff--; /* because the diff is normally 1 (ofc) */
|
||||
|
||||
if(diff <= MAX_LOST_PACKETS) {
|
||||
if(diff > 0) {
|
||||
/* lets first handle packet as "lost", even thou we're enqueueing it as premature */
|
||||
//auto status = codec_data->converter->decode_lost(error, diff);
|
||||
//if(status < 0)
|
||||
// log_warn(category::voice_connection, tr("Failed to decode (skip) dropped packets. Return code {} => {}"), status, error);
|
||||
premature = !buffer->head && this->state() != state::stopped;
|
||||
|
||||
log_debug(category::voice_connection,
|
||||
tr("Client {} dropped one or more audio packets. Old packet id: {}, New packet id: {}, Diff: {}. Head: {}. Flagging chunk as premature: {}"),
|
||||
this->_client_id, old_packet_id, buffer->packet_id, diff, buffer->head, premature);
|
||||
}
|
||||
} else {
|
||||
log_debug(category::voice_connection, tr("Client {} resetted decoder. Old packet id: {}, New packet id: {}, diff: {}"), this->_client_id, old_packet_id, buffer->packet_id, diff);
|
||||
codec_data->converter->reset_decoder();
|
||||
if(!codec_data->converter) {
|
||||
log_warn(category::voice_connection, tr("Failed to reset codec decoder {} for client {}: {}"), buffer->codec, this->_client_id, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!premature)
|
||||
codec_data->last_packet_id = buffer->packet_id;
|
||||
|
||||
char target_buffer[target_buffer_length];
|
||||
if(target_buffer_length < codec_data->converter->expected_decoded_length(buffer->buffer.data_ptr(), buffer->buffer.length())) {
|
||||
log_warn(category::voice_connection, tr("Failed to decode audio data. Target buffer is smaller then expected bytes ({} < {})"), target_buffer_length, codec_data->converter->expected_decoded_length(buffer->buffer.data_ptr(), buffer->buffer.length()));
|
||||
return;
|
||||
}
|
||||
auto samples = codec_data->converter->decode(error, buffer->buffer.data_ptr(), buffer->buffer.length(), target_buffer);
|
||||
if(samples < 0) {
|
||||
log_warn(category::voice_connection, tr("Failed to decode audio data: {}"), error);
|
||||
return;
|
||||
}
|
||||
if(target_buffer_length < codec_data->resampler->estimated_output_size(samples) * codec_data->resampler->channels() * 4) {
|
||||
log_warn(category::voice_connection, tr("Failed to resample audio data. Target buffer is smaller then expected bytes ({} < {})"), target_buffer_length, (codec_data->resampler->estimated_output_size(samples) * codec_data->resampler->channels() * 4));
|
||||
return;
|
||||
}
|
||||
|
||||
auto resampled_samples = codec_data->resampler->process(target_buffer, target_buffer, samples);
|
||||
if(resampled_samples <= 0) {
|
||||
log_warn(category::voice_connection, tr("Failed to resample audio data. Resampler resulted in {}"), resampled_samples);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!audio::merge::merge_channels_interleaved(target_buffer, this->output_source->channel_count, target_buffer, codec_data->resampler->channels(), resampled_samples)) {
|
||||
log_warn(category::voice_connection, tr("Failed to merge channels to output stream channel count!"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(this->_volume != 1) {
|
||||
auto buf = (float*) target_buffer;
|
||||
auto count = this->output_source->channel_count * resampled_samples;
|
||||
while(count-- > 0)
|
||||
*(buf++) *= this->_volume;
|
||||
}
|
||||
|
||||
if(premature) {
|
||||
auto audio_buffer = audio::SampleBuffer::allocate((uint8_t) this->output_source->channel_count, (uint16_t) resampled_samples);
|
||||
|
||||
audio_buffer->sample_index = 0;
|
||||
memcpy(audio_buffer->sample_data, target_buffer, this->output_source->channel_count * resampled_samples * 4);
|
||||
|
||||
{
|
||||
unique_lock buffer_lock(this->audio_decode_queue_lock);
|
||||
auto it = codec_data->premature_packets.begin();
|
||||
for(; it != codec_data->premature_packets.end(); it++) {
|
||||
if(it->packet_id > buffer->packet_id) {
|
||||
break; /* it is set to the right position */
|
||||
}
|
||||
}
|
||||
codec_data->premature_packets.insert(it, {
|
||||
buffer->packet_id,
|
||||
move(audio_buffer)
|
||||
});
|
||||
std::stable_sort(codec_data->premature_packets.begin(), codec_data->premature_packets.end(), [](const PrematureAudioPacket& a, const PrematureAudioPacket& b) {
|
||||
return a.packet_id < b.packet_id;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
auto enqueued = this->output_source->enqueue_samples(target_buffer, resampled_samples);
|
||||
if(enqueued != resampled_samples)
|
||||
log_warn(category::voice_connection, tr("Failed to enqueue all samples for client {}. Enqueued {} of {}"), this->_client_id, enqueued, resampled_samples);
|
||||
this->set_state(state::playing);
|
||||
|
||||
/* test if any premature got its original place */
|
||||
{
|
||||
unique_lock buffer_lock(this->audio_decode_queue_lock);
|
||||
|
||||
size_t play_count = 0;
|
||||
while(!codec_data->premature_packets.empty()) {
|
||||
auto& packet = codec_data->premature_packets[0];
|
||||
|
||||
//Test if we're able to replay stuff again
|
||||
if((uint16_t) (codec_data->last_packet_id + 1) < packet.packet_id)
|
||||
break; //Nothing new
|
||||
|
||||
this->output_source->enqueue_samples(packet.buffer);
|
||||
codec_data->last_packet_id = packet.packet_id;
|
||||
codec_data->premature_packets.pop_front();
|
||||
play_count++;
|
||||
}
|
||||
#ifdef DEBUG_PREMATURE_PACKETS
|
||||
if(play_count > 0)
|
||||
log_debug(category::audio, tr("Replayed (id match) {} premature packets for client {}"), play_count, this->_client_id);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceClient::event_execute_dropped(const std::chrono::system_clock::time_point &point) {
|
||||
if(audio_decode_event_dropped.exchange(true))
|
||||
//Is not really a warning, it happens all the time and isn't really an issue
|
||||
;//log_warn(category::voice_connection, tr("Dropped auto enqueue event execution two or more times in a row for client {}"), this->_client_id);
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <nan.h>
|
||||
#include <NanEventCallback.h>
|
||||
#include <functional>
|
||||
#include <pipes/buffer.h>
|
||||
#include "../../audio/AudioResampler.h"
|
||||
#include "../../audio/codec/Converter.h"
|
||||
#include "../../audio/AudioOutput.h"
|
||||
#include "../../EventLoop.h"
|
||||
|
||||
namespace tc {
|
||||
namespace connection {
|
||||
class ServerConnection;
|
||||
class VoiceConnection;
|
||||
class VoiceClient;
|
||||
|
||||
namespace codec {
|
||||
enum value {
|
||||
MIN = 0,
|
||||
|
||||
SPEEX_NARROWBAND = 0,
|
||||
SPEEX_WIDEBAND = 1,
|
||||
SPEEX_ULTRA_WIDEBAND = 2,
|
||||
|
||||
CELT_MONO = 3,
|
||||
|
||||
OPUS_VOICE = 4,
|
||||
OPUS_MUSIC = 5,
|
||||
|
||||
MAX = 5,
|
||||
};
|
||||
|
||||
struct condec_info {
|
||||
bool supported;
|
||||
std::string name;
|
||||
std::function<std::shared_ptr<audio::codec::Converter>(std::string&)> new_converter;
|
||||
};
|
||||
|
||||
extern const condec_info info[6];
|
||||
inline const condec_info* get_info(value codec) {
|
||||
if(codec > value::MAX || codec < value::MIN)
|
||||
return nullptr;
|
||||
return &info[codec];
|
||||
}
|
||||
}
|
||||
|
||||
class VoiceClient : private event::EventEntry {
|
||||
friend class VoiceConnection;
|
||||
|
||||
template<typename _Tp, typename _Up>
|
||||
friend inline std::shared_ptr<_Tp> std::static_pointer_cast(const std::shared_ptr<_Up>& __r) noexcept;
|
||||
public:
|
||||
struct state {
|
||||
enum value {
|
||||
buffering, /* this state is never active */
|
||||
playing,
|
||||
stopping,
|
||||
stopped
|
||||
};
|
||||
};
|
||||
VoiceClient(const std::shared_ptr<VoiceConnection>& /* connection */, uint16_t /* client id */);
|
||||
virtual ~VoiceClient();
|
||||
|
||||
inline uint16_t client_id() { return this->_client_id; }
|
||||
|
||||
void initialize_js_object();
|
||||
void finalize_js_object();
|
||||
|
||||
v8::Local<v8::Object> js_handle() {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
return this->_js_handle.Get(Nan::GetCurrentContext()->GetIsolate());
|
||||
}
|
||||
|
||||
inline std::shared_ptr<VoiceClient> ref() { return this->_ref.lock(); }
|
||||
|
||||
void process_packet(uint16_t packet_id, const pipes::buffer_view& /* buffer */, codec::value /* codec */, bool /* head */);
|
||||
|
||||
inline float get_volume() { return this->_volume; }
|
||||
inline void set_volume(float value) { this->_volume = value; }
|
||||
|
||||
inline state::value state() { return this->_state; }
|
||||
|
||||
void cancel_replay();
|
||||
|
||||
std::function<void()> on_state_changed;
|
||||
|
||||
inline std::shared_ptr<audio::AudioOutputSource> output_stream() { return this->output_source; }
|
||||
private:
|
||||
struct PrematureAudioPacket {
|
||||
uint16_t packet_id = 0;
|
||||
std::shared_ptr<tc::audio::SampleBuffer> buffer{};
|
||||
};
|
||||
|
||||
struct AudioCodec {
|
||||
uint16_t last_packet_id = 0;
|
||||
std::chrono::system_clock::time_point last_packet_timestamp;
|
||||
|
||||
bool successfully_initialized;
|
||||
std::shared_ptr<audio::codec::Converter> converter;
|
||||
std::shared_ptr<audio::AudioResampler> resampler;
|
||||
|
||||
std::deque<PrematureAudioPacket> premature_packets;
|
||||
};
|
||||
|
||||
std::array<std::unique_ptr<AudioCodec>, codec::MAX + 1> codec{
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr
|
||||
};
|
||||
std::shared_ptr<audio::AudioOutputSource> output_source;
|
||||
|
||||
std::weak_ptr<VoiceClient> _ref;
|
||||
v8::Persistent<v8::Object> _js_handle;
|
||||
|
||||
uint16_t _client_id;
|
||||
float _volume = 1.f;
|
||||
|
||||
bool play_premature_packets = false;
|
||||
|
||||
std::chrono::system_clock::time_point _last_received_packet;
|
||||
state::value _state = state::stopped;
|
||||
inline void set_state(state::value value) {
|
||||
if(value == this->_state)
|
||||
return;
|
||||
this->_state = value;
|
||||
if(this->on_state_changed)
|
||||
this->on_state_changed();
|
||||
}
|
||||
|
||||
struct EncodedBuffer {
|
||||
bool head;
|
||||
uint16_t packet_id;
|
||||
pipes::buffer buffer;
|
||||
codec::value codec;
|
||||
std::chrono::system_clock::time_point receive_timestamp;
|
||||
};
|
||||
|
||||
std::atomic_bool audio_decode_event_dropped{false};
|
||||
std::mutex audio_decode_queue_lock;
|
||||
std::deque<std::unique_ptr<EncodedBuffer>> audio_decode_queue;
|
||||
void event_execute(const std::chrono::system_clock::time_point &point) override;
|
||||
void event_execute_dropped(const std::chrono::system_clock::time_point &point) override;
|
||||
void process_encoded_buffer(const std::unique_ptr<EncodedBuffer>& /* buffer */);
|
||||
};
|
||||
|
||||
|
||||
class VoiceClientWrap : public Nan::ObjectWrap {
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
VoiceClientWrap(const std::shared_ptr<VoiceClient>&);
|
||||
virtual ~VoiceClientWrap();
|
||||
|
||||
void do_wrap(const v8::Local<v8::Object>&);
|
||||
private:
|
||||
static NAN_METHOD(_get_state);
|
||||
static NAN_METHOD(_get_volume);
|
||||
static NAN_METHOD(_set_volume);
|
||||
static NAN_METHOD(_abort_replay);
|
||||
static NAN_METHOD(_get_stream);
|
||||
|
||||
std::weak_ptr<VoiceClient> _handle;
|
||||
|
||||
bool _currently_playing = false;
|
||||
Nan::callback_t<> call_state_changed;
|
||||
void _call_state_changed();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
#include "VoiceConnection.h"
|
||||
#include "VoiceClient.h"
|
||||
#include "../ServerConnection.h"
|
||||
#include "AudioSender.h"
|
||||
#include "../../audio/js/AudioConsumer.h"
|
||||
#include "../../audio/AudioInput.h"
|
||||
#include "../../logger.h"
|
||||
#include <misc/endianness.h> /* MUST be included as last file */
|
||||
|
||||
using namespace std;
|
||||
using namespace tc;
|
||||
using namespace tc::connection;
|
||||
using namespace ts;
|
||||
using namespace ts::protocol;
|
||||
using namespace audio::recorder;
|
||||
|
||||
VoiceConnectionWrap::VoiceConnectionWrap(const std::shared_ptr<VoiceConnection>& handle) : handle(handle) {}
|
||||
VoiceConnectionWrap::~VoiceConnectionWrap() {
|
||||
if(!this->_voice_recoder_handle.IsEmpty()) {
|
||||
auto old_consumer = this->_voice_recoder_ptr;
|
||||
assert(old_consumer);
|
||||
|
||||
lock_guard read_lock(old_consumer->native_consumer()->on_read_lock);
|
||||
old_consumer->native_consumer()->on_read = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceConnectionWrap::do_wrap(const v8::Local<v8::Object> &object) {
|
||||
this->Wrap(object);
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(VoiceConnectionWrap::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(VoiceConnectionWrap::NewInstance);
|
||||
klass->SetClassName(Nan::New("VoiceConnection").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "decoding_supported", VoiceConnectionWrap::_decoding_supported);
|
||||
Nan::SetPrototypeMethod(klass, "encoding_supported", VoiceConnectionWrap::_encoding_supported);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "register_client", VoiceConnectionWrap::_register_client);
|
||||
Nan::SetPrototypeMethod(klass, "available_clients", VoiceConnectionWrap::_available_clients);
|
||||
Nan::SetPrototypeMethod(klass, "unregister_client", VoiceConnectionWrap::_unregister_client);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "audio_source", VoiceConnectionWrap::_audio_source);
|
||||
Nan::SetPrototypeMethod(klass, "set_audio_source", VoiceConnectionWrap::_set_audio_source);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "get_encoder_codec", VoiceConnectionWrap::_get_encoder_codec);
|
||||
Nan::SetPrototypeMethod(klass, "set_encoder_codec", VoiceConnectionWrap::_set_encoder_codec);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "enable_voice_send", VoiceConnectionWrap::_enable_voice_send);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::NewInstance) {
|
||||
if(!info.IsConstructCall())
|
||||
Nan::ThrowError("invalid invoke!");
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_connected) {
|
||||
info.GetReturnValue().Set(true);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_encoding_supported) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
auto codec = info[0]->Uint32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
|
||||
info.GetReturnValue().Set(codec >= 4 && codec <= 5); /* ignore SPEX currently :/ */
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_decoding_supported) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
auto codec = info[0]->Uint32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
|
||||
info.GetReturnValue().Set(codec >= 4 && codec <= 5); /* ignore SPEX currently :/ */
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_register_client) {
|
||||
return ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->register_client(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::register_client) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
auto id = info[0]->Uint32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
auto handle = this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
auto client = handle->register_client(id);
|
||||
if(!client) {
|
||||
Nan::ThrowError("failed to register client");
|
||||
return;
|
||||
}
|
||||
client->initialize_js_object();
|
||||
info.GetReturnValue().Set(client->js_handle());
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_available_clients) {
|
||||
return ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->available_clients(info);
|
||||
}
|
||||
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::available_clients) {
|
||||
auto handle = this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
auto client = handle->clients();
|
||||
|
||||
v8::Local<v8::Array> result = Nan::New<v8::Array>(client.size());
|
||||
for(size_t index = 0; index < client.size(); index++)
|
||||
Nan::Set(result, index, client[index]->js_handle());
|
||||
|
||||
info.GetReturnValue().Set(result);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_unregister_client) {
|
||||
return ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->unregister_client(info);
|
||||
}
|
||||
NAN_METHOD(VoiceConnectionWrap::unregister_client) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
auto id = info[0]->Uint32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
auto handle = this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
auto client = handle->find_client(id);
|
||||
if(!client) {
|
||||
Nan::ThrowError("missing client");
|
||||
return;
|
||||
}
|
||||
|
||||
client->finalize_js_object();
|
||||
handle->delete_client(client);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_audio_source) {
|
||||
auto client = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
info.GetReturnValue().Set(client->_voice_recoder_handle.Get(info.GetIsolate()));
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_set_audio_source) {
|
||||
ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder())->set_audio_source(info);
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::set_audio_source) {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if(!Nan::New(AudioConsumerWrapper::constructor_template())->HasInstance(info[0]) && !info[0]->IsNullOrUndefined()) {
|
||||
Nan::ThrowError("invalid consumer (Consumer must be native!)");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this->handle.lock()) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
this->release_recorder();
|
||||
if(!info[0]->IsNullOrUndefined()) {
|
||||
this->_voice_recoder_ptr = ObjectWrap::Unwrap<audio::recorder::AudioConsumerWrapper>(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
this->_voice_recoder_handle.Reset(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked());
|
||||
|
||||
auto native_consumer = this->_voice_recoder_ptr->native_consumer();
|
||||
|
||||
weak_ptr weak_handle = this->handle;
|
||||
auto sample_rate = native_consumer->sample_rate;
|
||||
auto channels = native_consumer->channel_count;
|
||||
|
||||
lock_guard read_lock(this->_voice_recoder_ptr->native_read_callback_lock);
|
||||
this->_voice_recoder_ptr->native_read_callback = [weak_handle, sample_rate, channels](const void* buffer, size_t length) {
|
||||
auto handle = weak_handle.lock();
|
||||
if(!handle) {
|
||||
log_warn(category::audio, tr("Missing voice connection handle. Dropping input!"));
|
||||
return;
|
||||
}
|
||||
|
||||
shared_ptr<VoiceSender> sender = handle->voice_sender();
|
||||
if(sender) {
|
||||
if(length > 0 && buffer)
|
||||
sender->send_data(buffer, length, sample_rate, channels);
|
||||
else
|
||||
sender->send_stop();
|
||||
} else {
|
||||
log_warn(category::audio, tr("Missing voice connection audio sender. Dropping input!"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_get_encoder_codec) {
|
||||
auto _this = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = _this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
info.GetReturnValue().Set(handle->get_encoder_codec());
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_set_encoder_codec) {
|
||||
auto _this = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = _this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsNumber()) {
|
||||
Nan::ThrowError("Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
handle->set_encoder_codec((uint8_t) info[0]->NumberValue(Nan::GetCurrentContext()).FromMaybe(0));
|
||||
}
|
||||
|
||||
NAN_METHOD(VoiceConnectionWrap::_enable_voice_send) {
|
||||
auto _this = ObjectWrap::Unwrap<VoiceConnectionWrap>(info.Holder());
|
||||
auto handle = _this->handle.lock();
|
||||
if(!handle) {
|
||||
Nan::ThrowError("handle has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
if(info.Length() != 1 || !info[0]->IsBoolean()) {
|
||||
Nan::ThrowError("Invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
auto sender = handle->voice_sender();
|
||||
if(!sender) {
|
||||
Nan::ThrowError("Voice sender has been deallocated");
|
||||
return;
|
||||
}
|
||||
|
||||
sender->set_voice_send_enabled(info[0]->BooleanValue(info.GetIsolate()));
|
||||
}
|
||||
|
||||
|
||||
void VoiceConnectionWrap::release_recorder() {
|
||||
if(!this->_voice_recoder_handle.IsEmpty()) {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
|
||||
auto old_consumer = this->_voice_recoder_ptr;
|
||||
assert(old_consumer);
|
||||
|
||||
lock_guard read_lock(this->_voice_recoder_ptr->native_read_callback_lock);
|
||||
this->_voice_recoder_ptr->native_read_callback = nullptr;
|
||||
} else {
|
||||
assert(!this->_voice_recoder_ptr);
|
||||
}
|
||||
|
||||
this->_voice_recoder_ptr = nullptr;
|
||||
this->_voice_recoder_handle.Reset();
|
||||
}
|
||||
|
||||
|
||||
VoiceConnection::VoiceConnection(ServerConnection *handle) : _handle(handle) {
|
||||
this->_voice_sender = make_shared<VoiceSender>(this);
|
||||
this->_voice_sender->_ref = this->_voice_sender;
|
||||
this->_voice_sender->set_codec(codec::OPUS_MUSIC);
|
||||
}
|
||||
VoiceConnection::~VoiceConnection() {
|
||||
if(v8::Isolate::GetCurrent())
|
||||
this->finalize_js_object();
|
||||
else {
|
||||
assert(this->_js_handle.IsEmpty());
|
||||
}
|
||||
|
||||
this->_voice_sender->finalize();
|
||||
}
|
||||
|
||||
void VoiceConnection::reset() {
|
||||
lock_guard lock(this->_clients_lock);
|
||||
this->_clients.clear();
|
||||
}
|
||||
|
||||
void VoiceConnection::initialize_js_object() {
|
||||
auto object_wrap = new VoiceConnectionWrap(this->ref());
|
||||
auto object = Nan::NewInstance(Nan::New(VoiceConnectionWrap::constructor()), 0, nullptr).ToLocalChecked();
|
||||
object_wrap->do_wrap(object);
|
||||
|
||||
this->_js_handle.Reset(Nan::GetCurrentContext()->GetIsolate(), object);
|
||||
}
|
||||
|
||||
void VoiceConnection::finalize_js_object() {
|
||||
this->_js_handle.Reset();
|
||||
}
|
||||
|
||||
std::shared_ptr<VoiceClient> VoiceConnection::find_client(uint16_t client_id) {
|
||||
lock_guard lock(this->_clients_lock);
|
||||
for(const auto& client : this->_clients)
|
||||
if(client->client_id() == client_id)
|
||||
return client;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<VoiceClient> VoiceConnection::register_client(uint16_t client_id) {
|
||||
lock_guard lock(this->_clients_lock);
|
||||
auto client = this->find_client(client_id);
|
||||
if(client) return client;
|
||||
|
||||
client = make_shared<VoiceClient>(this->ref(), client_id);
|
||||
client->_ref = client;
|
||||
this->_clients.push_back(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
void VoiceConnection::delete_client(const std::shared_ptr<tc::connection::VoiceClient> &client) {
|
||||
{
|
||||
lock_guard lock(this->_clients_lock);
|
||||
auto it = find(this->_clients.begin(), this->_clients.end(), client);
|
||||
if(it != this->_clients.end()) {
|
||||
this->_clients.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO deinitialize client
|
||||
}
|
||||
|
||||
void VoiceConnection::process_packet(const std::shared_ptr<ts::protocol::ServerPacket> &packet) {
|
||||
if(packet->type() == PacketTypeInfo::Voice) {
|
||||
if(packet->data().length() < 5) {
|
||||
//TODO log invalid voice packet
|
||||
return;
|
||||
}
|
||||
auto packet_id = be2le16(&packet->data()[0]);
|
||||
auto client_id = be2le16(&packet->data()[2]);
|
||||
auto codec_id = (uint8_t) packet->data()[4];
|
||||
auto flag_head = packet->has_flag(PacketFlag::Compressed);
|
||||
//container->voice_data = packet->data().length() > 5 ? packet->data().range(5) : pipes::buffer{};
|
||||
|
||||
//log_info(category::voice_connection, tr("Received voice packet from {}. Packet ID: {}"), client_id, packet_id);
|
||||
auto client = this->find_client(client_id);
|
||||
if(!client) {
|
||||
log_warn(category::voice_connection, tr("Received voice packet from unknown client {}. Dropping packet!"), client_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if(packet->data().length() > 5)
|
||||
client->process_packet(packet_id, packet->data().range(5), (codec::value) codec_id, flag_head);
|
||||
else
|
||||
client->process_packet(packet_id, pipes::buffer_view{nullptr, 0}, (codec::value) codec_id, flag_head);
|
||||
} else {
|
||||
//TODO implement whisper
|
||||
}
|
||||
}
|
||||
|
||||
void VoiceConnection::set_encoder_codec(const uint8_t &codec) {
|
||||
if(codec > codec::MAX) return;
|
||||
|
||||
auto vs = this->_voice_sender;
|
||||
if(vs)
|
||||
vs->set_codec((codec::value) codec);
|
||||
}
|
||||
|
||||
uint8_t VoiceConnection::get_encoder_codec() {
|
||||
auto vs = this->_voice_sender;
|
||||
return vs ? vs->get_codec() : 0;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <v8.h>
|
||||
#include <nan.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <protocol/Packet.h>
|
||||
|
||||
namespace tc {
|
||||
namespace audio {
|
||||
namespace recorder {
|
||||
class AudioConsumerWrapper;
|
||||
}
|
||||
}
|
||||
|
||||
namespace connection {
|
||||
class ServerConnection;
|
||||
class VoiceConnection;
|
||||
class VoiceClient;
|
||||
class VoiceSender;
|
||||
|
||||
class VoiceConnectionWrap : public Nan::ObjectWrap {
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
explicit VoiceConnectionWrap(const std::shared_ptr<VoiceConnection>&);
|
||||
virtual ~VoiceConnectionWrap();
|
||||
|
||||
void do_wrap(const v8::Local<v8::Object>&);
|
||||
private:
|
||||
static NAN_METHOD(_connected);
|
||||
static NAN_METHOD(_encoding_supported);
|
||||
static NAN_METHOD(_decoding_supported);
|
||||
|
||||
static NAN_METHOD(_register_client);
|
||||
NAN_METHOD(register_client);
|
||||
static NAN_METHOD(_available_clients);
|
||||
NAN_METHOD(available_clients);
|
||||
static NAN_METHOD(_unregister_client);
|
||||
NAN_METHOD(unregister_client);
|
||||
|
||||
static NAN_METHOD(_audio_source);
|
||||
static NAN_METHOD(_set_audio_source);
|
||||
NAN_METHOD(set_audio_source);
|
||||
|
||||
static NAN_METHOD(_get_encoder_codec);
|
||||
static NAN_METHOD(_set_encoder_codec);
|
||||
static NAN_METHOD(_enable_voice_send);
|
||||
|
||||
void release_recorder();
|
||||
|
||||
std::function<void(const void* /* buffer */, size_t /* samples */)> _read_callback;
|
||||
audio::recorder::AudioConsumerWrapper* _voice_recoder_ptr = nullptr;
|
||||
Nan::Persistent<v8::Object> _voice_recoder_handle;
|
||||
std::weak_ptr<VoiceConnection> handle;
|
||||
};
|
||||
|
||||
class VoiceConnection {
|
||||
friend class ServerConnection;
|
||||
friend class VoiceConnectionWrap;
|
||||
public:
|
||||
explicit VoiceConnection(ServerConnection*);
|
||||
virtual ~VoiceConnection();
|
||||
|
||||
void reset();
|
||||
|
||||
void initialize_js_object();
|
||||
void finalize_js_object();
|
||||
|
||||
ServerConnection* handle() { return this->_handle; }
|
||||
v8::Local<v8::Object> js_handle() {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
return this->_js_handle.Get(Nan::GetCurrentContext()->GetIsolate());
|
||||
}
|
||||
|
||||
inline std::shared_ptr<VoiceConnection> ref() { return this->_ref.lock(); }
|
||||
inline std::deque<std::shared_ptr<VoiceClient>> clients() {
|
||||
std::lock_guard lock(this->_clients_lock);
|
||||
return this->_clients;
|
||||
}
|
||||
|
||||
inline std::shared_ptr<VoiceSender> voice_sender() { return this->_voice_sender; }
|
||||
|
||||
std::shared_ptr<VoiceClient> find_client(uint16_t /* client id */);
|
||||
std::shared_ptr<VoiceClient> register_client(uint16_t /* client id */);
|
||||
void delete_client(const std::shared_ptr<VoiceClient>&);
|
||||
|
||||
void process_packet(const std::shared_ptr<ts::protocol::ServerPacket>&);
|
||||
|
||||
void set_encoder_codec(const uint8_t& /* codec */);
|
||||
uint8_t get_encoder_codec();
|
||||
private:
|
||||
ServerConnection* _handle;
|
||||
std::weak_ptr<VoiceConnection> _ref;
|
||||
|
||||
v8::Persistent<v8::Object> _js_handle;
|
||||
|
||||
std::recursive_mutex _clients_lock;
|
||||
std::deque<std::shared_ptr<VoiceClient>> _clients;
|
||||
|
||||
std::shared_ptr<VoiceSender> _voice_sender;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,773 +0,0 @@
|
||||
#include "FileTransferManager.h"
|
||||
#include "FileTransferObject.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fcntl.h>
|
||||
#include <iostream>
|
||||
|
||||
#ifndef WIN32
|
||||
#include <unistd.h>
|
||||
#include <misc/net.h>
|
||||
#else
|
||||
#include <ws2tcpip.h>
|
||||
#define SOCK_NONBLOCK (0)
|
||||
#define MSG_DONTWAIT (0)
|
||||
#endif
|
||||
|
||||
using namespace tc;
|
||||
using namespace tc::ft;
|
||||
using namespace std;
|
||||
using namespace std::chrono;
|
||||
|
||||
tc::ft::FileTransferManager* transfer_manager = nullptr;
|
||||
|
||||
Transfer::~Transfer() {
|
||||
log_free("Transfer", this);
|
||||
}
|
||||
|
||||
bool Transfer::initialize(std::string &error) {
|
||||
if(this->_state != state::UNINITIALIZED) {
|
||||
error = tr("invalid state");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!this->_transfer_object->initialize(error)) {
|
||||
error = tr("failed to initialize transfer object: ") + error;
|
||||
return false;
|
||||
}
|
||||
this->_state = state::CONNECTING;
|
||||
|
||||
/* resolve address */
|
||||
{
|
||||
addrinfo hints{}, *result;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
|
||||
if(getaddrinfo(this->_options->remote_address.data(), nullptr, &hints, &result) != 0 || !result) {
|
||||
error = tr("failed to resolve hostname");
|
||||
this->_state = state::UNINITIALIZED;
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(&this->remote_address, result->ai_addr, result->ai_addrlen);
|
||||
freeaddrinfo(result);
|
||||
}
|
||||
switch(this->remote_address.ss_family) {
|
||||
case AF_INET:
|
||||
((sockaddr_in*) &this->remote_address)->sin_port = htons(this->_options->remote_port);
|
||||
case AF_INET6:
|
||||
((sockaddr_in6*) &this->remote_address)->sin6_port = htons(this->_options->remote_port);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
log_info(category::file_transfer, tr("Setting remote port to {}"), this->_options->remote_port);
|
||||
this->_socket = (int) ::socket(this->remote_address.ss_family, SOCK_STREAM | SOCK_NONBLOCK, 0);
|
||||
if(this->_socket < 0) {
|
||||
this->finalize();
|
||||
error = tr("failed to spawn socket");
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
u_long enabled = 0;
|
||||
auto non_block_rs = ioctlsocket(this->_socket, FIONBIO, &enabled);
|
||||
if (non_block_rs != NO_ERROR) {
|
||||
this->finalize();
|
||||
error = "failed to enable non blocking more";
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
{
|
||||
|
||||
lock_guard lock(this->event_lock);
|
||||
this->event_read = event_new(this->event_io, this->_socket, EV_READ | EV_PERSIST, &Transfer::_callback_read, this);
|
||||
this->event_write = event_new(this->event_io, this->_socket, EV_WRITE, &Transfer::_callback_write, this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Transfer::connect() {
|
||||
int result = ::connect(this->_socket, reinterpret_cast<sockaddr *> (&this->remote_address), sizeof(this->remote_address));
|
||||
if (result < 0) {
|
||||
#ifdef WIN32
|
||||
auto error = WSAGetLastError();
|
||||
|
||||
if(error != WSAEWOULDBLOCK) {
|
||||
wchar_t *s = nullptr;
|
||||
FormatMessageW(
|
||||
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr,
|
||||
error,
|
||||
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||
(LPWSTR)&s,
|
||||
0,
|
||||
nullptr
|
||||
);
|
||||
fprintf(stdout, "Connect failed with code %d. Error: %ld/%S\n", result, error, s);
|
||||
LocalFree(s);
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if(errno != EINPROGRESS) {
|
||||
log_error(category::file_transfer, tr("Failed to connect with code: {} => {}/{}"), result, errno, strerror(errno));
|
||||
this->finalize();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
this->_state = state::CONNECTED; /* we're connected */
|
||||
}
|
||||
log_debug(category::file_transfer, tr("Connect result: {} - {}"), result, errno);
|
||||
|
||||
timeval connect_timeout{5, 0};
|
||||
event_add(this->event_write, &connect_timeout); /* enabled if socket is connected */
|
||||
////event_add(this->event_read, &connect_timeout); /* enabled if socket is connected */
|
||||
this->handle()->execute_event_loop();
|
||||
|
||||
if(this->_state == state::CONNECTED)
|
||||
this->handle_connected();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Transfer::finalize(bool blocking) {
|
||||
if(this->_state == state::UNINITIALIZED)
|
||||
return;
|
||||
|
||||
this->_state = state::UNINITIALIZED;
|
||||
|
||||
{
|
||||
unique_lock lock(this->event_lock);
|
||||
auto event_read = this->event_read, event_write = this->event_write;
|
||||
this->event_read = nullptr;
|
||||
this->event_write = nullptr;
|
||||
lock.unlock();
|
||||
if(event_read) {
|
||||
if(blocking)
|
||||
event_del_block(event_read);
|
||||
else
|
||||
event_del_noblock(event_read);
|
||||
event_free(event_read);
|
||||
}
|
||||
if(event_write) {
|
||||
if(blocking)
|
||||
event_del_block(event_write);
|
||||
else
|
||||
event_del_noblock(event_write);
|
||||
event_free(event_write);
|
||||
}
|
||||
}
|
||||
|
||||
if(this->_socket > 0) {
|
||||
#ifdef WIN32
|
||||
closesocket(this->_socket);
|
||||
#else
|
||||
shutdown(this->_socket, SHUT_RDWR);
|
||||
close(this->_socket);
|
||||
#endif
|
||||
this->_socket = 0;
|
||||
}
|
||||
|
||||
this->_transfer_object->finalize();
|
||||
|
||||
this->_handle->remove_transfer(this);
|
||||
}
|
||||
|
||||
void Transfer::_callback_write(evutil_socket_t, short flags, void *_ptr_transfer) {
|
||||
reinterpret_cast<Transfer*>(_ptr_transfer)->callback_write(flags);
|
||||
}
|
||||
|
||||
void Transfer::_callback_read(evutil_socket_t, short flags, void *_ptr_transfer) {
|
||||
reinterpret_cast<Transfer*>(_ptr_transfer)->callback_read(flags);
|
||||
}
|
||||
|
||||
void Transfer::callback_read(short flags) {
|
||||
if(this->_state < state::CONNECTING && this->_state > state::DISCONNECTING)
|
||||
return;
|
||||
|
||||
if(flags & EV_TIMEOUT) {
|
||||
auto target = dynamic_pointer_cast<TransferTarget>(this->_transfer_object);
|
||||
if(target) {
|
||||
if(this->last_target_write.time_since_epoch().count() == 0)
|
||||
this->last_target_write = system_clock::now();
|
||||
else if(system_clock::now() - this->last_target_write > seconds(5)) {
|
||||
this->call_callback_failed("timeout (write)");
|
||||
this->finalize(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if(this->last_source_read.time_since_epoch().count() == 0)
|
||||
this->last_source_read = system_clock::now();
|
||||
else if(system_clock::now() - this->last_source_read > seconds(5)) {
|
||||
this->call_callback_failed("timeout (read)");
|
||||
this->finalize(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->event_lock);
|
||||
if(this->event_read) {
|
||||
event_add(this->event_read, &this->alive_check_timeout);
|
||||
this->handle()->execute_event_loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(flags & EV_READ) {
|
||||
if(this->_state == state::CONNECTING) {
|
||||
log_debug(category::file_transfer, tr("Connected (read event)"));
|
||||
this->handle_connected();
|
||||
}
|
||||
|
||||
int64_t buffer_length = 1024;
|
||||
char buffer[1024];
|
||||
|
||||
buffer_length = recv(this->_socket, buffer, (int) buffer_length, MSG_DONTWAIT);
|
||||
if(buffer_length < 0) {
|
||||
#ifdef WIN32
|
||||
auto error = WSAGetLastError();
|
||||
|
||||
if(error != WSAEWOULDBLOCK)
|
||||
return;
|
||||
#else
|
||||
if(errno == EAGAIN)
|
||||
return;
|
||||
#endif
|
||||
|
||||
log_error(category::file_transfer, tr("Received an error while receivig data: {}/{}"), errno, strerror(errno));
|
||||
//TODO may handle this error message?
|
||||
this->handle_disconnect();
|
||||
return;
|
||||
} else if(buffer_length == 0) {
|
||||
log_info(category::file_transfer, tr("Received an disconnect"));
|
||||
this->handle_disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
auto target = dynamic_pointer_cast<TransferTarget>(this->_transfer_object);
|
||||
if(target) {
|
||||
log_trace(category::file_transfer, tr("Read {} bytes"), buffer_length);
|
||||
string error;
|
||||
auto state = target->write_bytes(error, (uint8_t*) buffer, buffer_length);
|
||||
this->last_target_write = system_clock::now();
|
||||
if(state == error::out_of_space) {
|
||||
log_error(category::file_transfer, tr("Failed to write read data (out of space)"));
|
||||
this->call_callback_failed(tr("out of local space"));
|
||||
this->finalize(true);
|
||||
return;
|
||||
} else if(state == error::custom) {
|
||||
log_error(category::file_transfer, tr("Failed to write read data ({})"), error);
|
||||
this->call_callback_failed(error);
|
||||
this->finalize(true);
|
||||
return;
|
||||
} else if(state == error::custom_recoverable) {
|
||||
log_error(category::file_transfer, tr("Failed to write read data ({})"), error);
|
||||
} else if(state != error::success) {
|
||||
log_error(category::file_transfer, tr("invalid local write return code! ({})"), state);
|
||||
}
|
||||
|
||||
auto stream_index = target->stream_index();
|
||||
auto expected_bytes = target->expected_length();
|
||||
if(stream_index >= expected_bytes) {
|
||||
this->call_callback_finished(false);
|
||||
this->finalize(false);
|
||||
}
|
||||
this->call_callback_process(stream_index, expected_bytes);
|
||||
} else {
|
||||
log_warn(category::file_transfer, tr("Read {} bytes, but we're not in download mode"), buffer_length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Transfer::callback_write(short flags) {
|
||||
if(this->_state < state::CONNECTING && this->_state > state::DISCONNECTING)
|
||||
return;
|
||||
|
||||
if(flags & EV_TIMEOUT) {
|
||||
//we received a timeout! (May just for creating buffers)
|
||||
if(this->_state == state::CONNECTING) {
|
||||
this->call_callback_failed(tr("failed to connect"));
|
||||
this->finalize(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool readd_write = false, readd_write_for_read = false;
|
||||
if(flags & EV_WRITE) {
|
||||
if(this->_state == state::CONNECTING)
|
||||
this->handle_connected();
|
||||
|
||||
pipes::buffer buffer;
|
||||
while(true) {
|
||||
{
|
||||
lock_guard lock(this->queue_lock);
|
||||
auto size = this->write_queue.size();
|
||||
if(size == 0)
|
||||
break;
|
||||
|
||||
buffer = this->write_queue.front();
|
||||
this->write_queue.pop_front();
|
||||
|
||||
readd_write = size > 1;
|
||||
}
|
||||
|
||||
auto written = send(this->_socket, buffer.data_ptr<char>(), buffer.length(), MSG_DONTWAIT);
|
||||
if(written <= 0) {
|
||||
{
|
||||
lock_guard lock(this->queue_lock);
|
||||
this->write_queue.push_front(buffer);
|
||||
readd_write = true;
|
||||
}
|
||||
#ifdef WIN32
|
||||
auto _error = WSAGetLastError();
|
||||
#else
|
||||
auto _error = errno;
|
||||
#define WSAEWOULDBLOCK (0)
|
||||
#define WSAECONNREFUSED (0)
|
||||
#define WSAECONNRESET (0)
|
||||
#define WSAENOTCONN (0)
|
||||
#endif
|
||||
if(_error == EAGAIN || _error == WSAEWOULDBLOCK) {
|
||||
break; /* event will be added with e.g. a timeout */
|
||||
} else if(_error == ECONNREFUSED || _error == WSAECONNREFUSED) {
|
||||
this->call_callback_failed("connection refused");
|
||||
this->finalize(false);
|
||||
} else if(_error == ECONNRESET || _error == WSAECONNRESET) {
|
||||
this->call_callback_failed("connection reset");
|
||||
this->finalize(false);
|
||||
} else if(_error == ENOTCONN || _error == WSAENOTCONN) {
|
||||
this->call_callback_failed("not connected");
|
||||
this->finalize(false);
|
||||
} else if(written == 0) {
|
||||
this->handle_disconnect();
|
||||
} else {
|
||||
log_error(category::file_transfer, "Encountered write error: {}/{}", _error, strerror(_error));
|
||||
this->handle_disconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(written < buffer.length()) {
|
||||
lock_guard lock(this->queue_lock);
|
||||
this->write_queue.push_front(buffer.range(written));
|
||||
readd_write = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(this->_state == state::CONNECTED) {
|
||||
auto source = dynamic_pointer_cast<TransferSource>(this->_transfer_object);
|
||||
if(source) {
|
||||
size_t queue_length = 0;
|
||||
{
|
||||
lock_guard lock(this->queue_lock);
|
||||
queue_length = this->write_queue.size();
|
||||
}
|
||||
|
||||
|
||||
string error;
|
||||
auto total_bytes = source->byte_length();
|
||||
auto bytes_to_write = total_bytes - source->stream_index();
|
||||
|
||||
while(queue_length < 8 && bytes_to_write > 0) {
|
||||
uint64_t buffer_size = 1400; /* best TCP packet size (equal to the MTU) */
|
||||
pipes::buffer buffer{buffer_size};
|
||||
auto read_status = source->read_bytes(error, buffer.data_ptr<uint8_t>(), buffer_size);
|
||||
this->last_source_read = system_clock::now();
|
||||
if(read_status != error::success) {
|
||||
if(read_status == error::would_block) {
|
||||
readd_write_for_read = true;
|
||||
break;
|
||||
} else if(read_status == error::custom) {
|
||||
this->call_callback_failed(tr("failed to read from source: ") + error);
|
||||
this->finalize(false);
|
||||
return;
|
||||
} else if(read_status == error::custom_recoverable) {
|
||||
log_warn(category::file_transfer, tr("Failed to read from source (but its recoverable): {}"), error);
|
||||
break;
|
||||
} else {
|
||||
log_error(category::file_transfer, tr("invalid source read status ({})"), read_status);
|
||||
this->finalize(false);
|
||||
return;
|
||||
}
|
||||
} else if(buffer_size == 0) {
|
||||
log_warn(category::file_transfer, tr("Invalid source read size! ({})"), buffer_size);
|
||||
break;
|
||||
}
|
||||
|
||||
{
|
||||
lock_guard lock(this->queue_lock);
|
||||
this->write_queue.push_back(buffer.range(0, buffer_size));
|
||||
queue_length = this->write_queue.size();
|
||||
}
|
||||
|
||||
bytes_to_write -= buffer_size;
|
||||
}
|
||||
|
||||
this->call_callback_process(total_bytes - bytes_to_write, total_bytes);
|
||||
if(queue_length == 0) {
|
||||
if(source->stream_index() == source->byte_length()) {
|
||||
this->call_callback_finished(false);
|
||||
this->finalize(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
readd_write = queue_length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* we only need write for connect */
|
||||
if(readd_write || readd_write_for_read) {
|
||||
lock_guard lock(this->event_lock);
|
||||
if(this->event_write) {
|
||||
timeval timeout{};
|
||||
if(readd_write) {
|
||||
/* we should be writeable within the next second or we do a keep alive circle */
|
||||
timeout.tv_sec = 1;
|
||||
timeout.tv_usec = 0;
|
||||
} else if(readd_write_for_read) {
|
||||
/* Schedule a next read attempt of our source */
|
||||
timeout.tv_sec = 0;
|
||||
timeout.tv_usec = 50000;
|
||||
}
|
||||
event_add(this->event_write, &timeout);
|
||||
this->handle()->execute_event_loop();
|
||||
}
|
||||
} else {
|
||||
log_debug(category::general, tr("No readd"));
|
||||
}
|
||||
}
|
||||
|
||||
void Transfer::_write_message(const pipes::buffer_view &buffer) {
|
||||
{
|
||||
lock_guard lock(this->queue_lock);
|
||||
this->write_queue.push_back(buffer.own_buffer());
|
||||
}
|
||||
if(this->_state >= state::CONNECTED) {
|
||||
lock_guard lock(this->event_lock);
|
||||
if(this->event_write) {
|
||||
event_add(this->event_write, nullptr);
|
||||
this->handle()->execute_event_loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Transfer::handle_disconnect() {
|
||||
if(this->_state != state::DISCONNECTING) {
|
||||
auto source = dynamic_pointer_cast<TransferSource>(this->_transfer_object);
|
||||
auto target = dynamic_pointer_cast<TransferTarget>(this->_transfer_object);
|
||||
|
||||
if(source && source->stream_index() != source->byte_length()) {
|
||||
this->call_callback_failed("received disconnect while transmitting (" + to_string(target->stream_index()) + "/" + to_string(target->expected_length()) + ")");
|
||||
} else if(target && target->stream_index() != target->expected_length()) {
|
||||
this->call_callback_failed("received disconnect while receiving (" + to_string(target->stream_index()) + "/" + to_string(target->expected_length()) + ")");
|
||||
} else
|
||||
this->call_callback_finished(false);
|
||||
}
|
||||
this->finalize(false);
|
||||
}
|
||||
|
||||
void Transfer::handle_connected() {
|
||||
log_info(category::file_transfer, tr("Transfer connected. Sending key {}"), this->_options->transfer_key);
|
||||
|
||||
this->_state = state::CONNECTED;
|
||||
event_add(this->event_read, &this->alive_check_timeout);
|
||||
this->handle()->execute_event_loop();
|
||||
|
||||
this->_write_message(pipes::buffer_view{this->_options->transfer_key.data(), this->_options->transfer_key.length()});
|
||||
this->call_callback_connected();
|
||||
//We dont have to add a timeout to write for prebuffering because its already done by writing this message
|
||||
}
|
||||
|
||||
void Transfer::call_callback_connected() {
|
||||
if(this->callback_start)
|
||||
this->callback_start();
|
||||
}
|
||||
|
||||
void Transfer::call_callback_failed(const std::string &error) {
|
||||
if(this->callback_failed)
|
||||
this->callback_failed(error);
|
||||
}
|
||||
|
||||
void Transfer::call_callback_finished(bool aborted) {
|
||||
if(this->callback_finished)
|
||||
this->callback_finished(aborted);
|
||||
}
|
||||
|
||||
void Transfer::call_callback_process(size_t current, size_t max) {
|
||||
auto now = system_clock::now();
|
||||
if(now - milliseconds{500} > this->last_process_call)
|
||||
this->last_process_call = now;
|
||||
else
|
||||
return;
|
||||
if(this->callback_process)
|
||||
this->callback_process(current, max);
|
||||
}
|
||||
|
||||
FileTransferManager::FileTransferManager() {}
|
||||
FileTransferManager::~FileTransferManager() {}
|
||||
|
||||
void FileTransferManager::initialize() {
|
||||
this->event_io_canceled = false;
|
||||
this->event_io = event_base_new();
|
||||
this->event_io_thread = std::thread(&FileTransferManager::_execute_event_loop, this);
|
||||
}
|
||||
|
||||
void FileTransferManager::finalize() {
|
||||
this->event_io_canceled = true;
|
||||
this->event_io_condition.notify_all();
|
||||
event_base_loopexit(this->event_io, nullptr);
|
||||
this->event_io_thread.join();
|
||||
|
||||
//TODO drop all file transfers!
|
||||
event_base_free(this->event_io);
|
||||
this->event_io = nullptr;
|
||||
}
|
||||
|
||||
void FileTransferManager::_execute_event_loop() {
|
||||
while(!this->event_io_canceled) {
|
||||
this->event_execute = false;
|
||||
event_base_loop(this->event_io, 0);
|
||||
if(this->running_transfers().size() > 0) {
|
||||
this_thread::sleep_for(milliseconds(50));
|
||||
} else {
|
||||
unique_lock lock(this->event_io_lock);
|
||||
this->event_io_condition.wait_for(lock, minutes(1), [&]{
|
||||
return this->event_io_canceled || this->event_execute;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Transfer> FileTransferManager::register_transfer(std::string& error, const std::shared_ptr<tc::ft::TransferObject> &object, std::unique_ptr<tc::ft::TransferOptions> options) {
|
||||
auto transfer = make_shared<Transfer>(this, object, move(options));
|
||||
|
||||
transfer->event_io = this->event_io;
|
||||
if(!transfer->initialize(error)) {
|
||||
//error = "failed to initialize transfer: " + error;
|
||||
return nullptr;
|
||||
}
|
||||
{
|
||||
lock_guard lock(this->_transfer_lock);
|
||||
this->_running_transfers.push_back(transfer);
|
||||
}
|
||||
|
||||
return transfer;
|
||||
}
|
||||
|
||||
void FileTransferManager::drop_transfer(const std::shared_ptr<Transfer> &transfer) {
|
||||
transfer->finalize(true);
|
||||
{
|
||||
lock_guard lock(this->_transfer_lock);
|
||||
auto it = find(this->_running_transfers.begin(), this->_running_transfers.end(), transfer);
|
||||
if(it != this->_running_transfers.end())
|
||||
this->_running_transfers.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferManager::remove_transfer(tc::ft::Transfer *transfer) {
|
||||
lock_guard lock(this->_transfer_lock);
|
||||
this->_running_transfers.erase(remove_if(this->_running_transfers.begin(), this->_running_transfers.end(), [&](const shared_ptr<Transfer>& _t) {
|
||||
return &*_t == transfer;
|
||||
}), this->_running_transfers.end());
|
||||
}
|
||||
|
||||
#ifdef NODEJS_API
|
||||
|
||||
NAN_MODULE_INIT(JSTransfer::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(JSTransfer::NewInstance);
|
||||
klass->SetClassName(Nan::New("JSTransfer").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
Nan::SetPrototypeMethod(klass, "start", JSTransfer::_start);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(JSTransfer::NewInstance) {
|
||||
if (info.IsConstructCall()) {
|
||||
if(info.Length() != 1 || !info[0]->IsObject()) {
|
||||
Nan::ThrowError("invalid arguments");
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* transfer_key: string;
|
||||
* client_transfer_id: number;
|
||||
* server_transfer_id: number;
|
||||
* object: HandledTransferObject;
|
||||
*/
|
||||
auto options = info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked();
|
||||
v8::Local<v8::String> key = options->Get(Nan::New<v8::String>("transfer_key").ToLocalChecked()).As<v8::String>();
|
||||
v8::Local<v8::Number> client_transfer_id = options->Get(Nan::New<v8::String>("client_transfer_id").ToLocalChecked()).As<v8::Number>();
|
||||
v8::Local<v8::Number> server_transfer_id = options->Get(Nan::New<v8::String>("server_transfer_id").ToLocalChecked()).As<v8::Number>();
|
||||
v8::Local<v8::String> remote_address = options->Get(Nan::New<v8::String>("remote_address").ToLocalChecked()).As<v8::String>();
|
||||
v8::Local<v8::Number> remote_port = options->Get(Nan::New<v8::String>("remote_port").ToLocalChecked()).As<v8::Number>();
|
||||
|
||||
if(
|
||||
key.IsEmpty() || !key->IsString() ||
|
||||
remote_address.IsEmpty() || !remote_address->IsString() ||
|
||||
remote_port.IsEmpty() || !remote_port->IsInt32() ||
|
||||
client_transfer_id.IsEmpty() || !client_transfer_id->IsInt32() ||
|
||||
server_transfer_id.IsEmpty() || !server_transfer_id->IsInt32()
|
||||
) {
|
||||
Nan::ThrowError("invalid argument types");
|
||||
return;
|
||||
}
|
||||
|
||||
auto wrapped_options = options->Get(Nan::New<v8::String>("object").ToLocalChecked()).As<v8::Object>();
|
||||
if(!TransferObjectWrap::is_wrap(wrapped_options)) {
|
||||
Nan::ThrowError("invalid handle");
|
||||
return;
|
||||
}
|
||||
auto transfer_object = ObjectWrap::Unwrap<TransferObjectWrap>(wrapped_options)->target();
|
||||
assert(transfer_object);
|
||||
|
||||
|
||||
auto t_options = make_unique<TransferOptions>();
|
||||
t_options->transfer_key = *Nan::Utf8String(key);
|
||||
t_options->client_transfer_id = client_transfer_id->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
t_options->server_transfer_id = server_transfer_id->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
t_options->remote_address = *Nan::Utf8String(remote_address);
|
||||
t_options->remote_port = remote_port->Int32Value(Nan::GetCurrentContext()).FromMaybe(0);
|
||||
|
||||
string error;
|
||||
auto transfer = transfer_manager->register_transfer(error, transfer_object, move(t_options));
|
||||
if(!transfer) {
|
||||
Nan::ThrowError(Nan::New<v8::String>("failed to create transfer: " + error).ToLocalChecked());
|
||||
return;
|
||||
}
|
||||
|
||||
auto js_instance = new JSTransfer(transfer);
|
||||
js_instance->Wrap(info.This());
|
||||
js_instance->_self_ref = true;
|
||||
js_instance->Ref(); /* increase ref counter because file transfer is running */
|
||||
info.GetReturnValue().Set(info.This());
|
||||
} else {
|
||||
if(info.Length() != 1) {
|
||||
Nan::ThrowError("invalid argument count");
|
||||
return;
|
||||
}
|
||||
|
||||
v8::Local<v8::Function> cons = Nan::New(constructor());
|
||||
v8::Local<v8::Value> argv[1] = {info[0]};
|
||||
|
||||
Nan::TryCatch try_catch;
|
||||
auto result = Nan::NewInstance(cons, info.Length(), argv);
|
||||
if(try_catch.HasCaught()) {
|
||||
try_catch.ReThrow();
|
||||
return;
|
||||
}
|
||||
info.GetReturnValue().Set(result.ToLocalChecked());
|
||||
}
|
||||
}
|
||||
|
||||
JSTransfer::JSTransfer(std::shared_ptr<tc::ft::Transfer> transfer) : _transfer(move(transfer)) {
|
||||
this->call_failed = Nan::async_callback([&](std::string error) {
|
||||
Nan::HandleScope scope;
|
||||
this->callback_failed(error);
|
||||
});
|
||||
this->call_finished = Nan::async_callback([&](bool f) {
|
||||
Nan::HandleScope scope;
|
||||
this->callback_finished(f);
|
||||
});
|
||||
this->call_start = Nan::async_callback([&] {
|
||||
Nan::HandleScope scope;
|
||||
this->callback_start();
|
||||
});
|
||||
this->call_progress = Nan::async_callback([&](uint64_t a, uint64_t b) {
|
||||
Nan::HandleScope scope;
|
||||
this->callback_progress(a, b);
|
||||
});
|
||||
|
||||
this->_transfer->callback_failed = [&](std::string error) { this->call_failed(std::forward<string>(error)); };
|
||||
this->_transfer->callback_finished = [&](bool f) { this->call_finished(std::forward<bool>(f)); };
|
||||
this->_transfer->callback_start = [&] { this->call_start(); };
|
||||
this->_transfer->callback_process = [&](uint64_t a, uint64_t b) { this->call_progress.call_cpy(a, b); };
|
||||
}
|
||||
|
||||
JSTransfer::~JSTransfer() {
|
||||
cout << "JS dealloc" << endl;
|
||||
this->_transfer->callback_failed = NULL;
|
||||
this->_transfer->callback_finished = NULL;
|
||||
this->_transfer->callback_start = NULL;
|
||||
this->_transfer->callback_process = NULL;
|
||||
}
|
||||
|
||||
NAN_METHOD(JSTransfer::destory_transfer) {
|
||||
//TODO!
|
||||
Nan::ThrowError("Not implemented!");
|
||||
}
|
||||
|
||||
NAN_METHOD(JSTransfer::_start) {
|
||||
return ObjectWrap::Unwrap<JSTransfer>(info.Holder())->start(info);
|
||||
}
|
||||
NAN_METHOD(JSTransfer::start) {
|
||||
if(!this->_transfer->connect()) {
|
||||
Nan::ThrowError("failed to connect");
|
||||
return;
|
||||
}
|
||||
|
||||
log_info(category::file_transfer, tr("Connecting to {}:{}"), this->_transfer->options().remote_address, this->_transfer->options().remote_port);
|
||||
}
|
||||
|
||||
NAN_METHOD(JSTransfer::_abort) {
|
||||
return ObjectWrap::Unwrap<JSTransfer>(info.Holder())->abort(info);
|
||||
}
|
||||
NAN_METHOD(JSTransfer::abort) {
|
||||
//TODO!
|
||||
}
|
||||
|
||||
void JSTransfer::callback_finished(bool flag) {
|
||||
if(this->_self_ref) {
|
||||
this->_self_ref = false;
|
||||
this->Unref();
|
||||
}
|
||||
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_finished").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
||||
if(callback.IsEmpty() || !callback->IsFunction())
|
||||
return;
|
||||
|
||||
v8::Local<v8::Value> arguments[1];
|
||||
arguments[0] = Nan::New<v8::Boolean>(flag);
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, arguments);
|
||||
}
|
||||
|
||||
void JSTransfer::callback_start() {
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_start").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
||||
if(callback.IsEmpty() || !callback->IsFunction())
|
||||
return;
|
||||
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 0, nullptr);
|
||||
}
|
||||
|
||||
void JSTransfer::callback_progress(uint64_t a, uint64_t b) {
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_progress").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
||||
if(callback.IsEmpty() || !callback->IsFunction())
|
||||
return;
|
||||
|
||||
v8::Local<v8::Value> arguments[2];
|
||||
arguments[0] = Nan::New<v8::Number>(a);
|
||||
arguments[1] = Nan::New<v8::Number>(b);
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 2, arguments);
|
||||
}
|
||||
|
||||
void JSTransfer::callback_failed(std::string error) {
|
||||
if(this->_self_ref) {
|
||||
this->_self_ref = false;
|
||||
this->Unref();
|
||||
}
|
||||
auto callback = Nan::Get(this->handle(), Nan::New<v8::String>("callback_failed").ToLocalChecked()).ToLocalChecked().As<v8::Function>();
|
||||
if(callback.IsEmpty() || !callback->IsFunction())
|
||||
return;
|
||||
|
||||
v8::Local<v8::Value> arguments[1];
|
||||
arguments[0] = Nan::New<v8::String>(error).ToLocalChecked();
|
||||
callback->Call(Nan::GetCurrentContext(), Nan::Undefined(), 1, arguments);
|
||||
}
|
||||
#endif
|
||||
@@ -1,228 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <functional>
|
||||
#include <condition_variable>
|
||||
|
||||
#include <event.h>
|
||||
#include <pipes/buffer.h>
|
||||
#include <cstring>
|
||||
|
||||
#if NODEJS_API
|
||||
#include <nan.h>
|
||||
#include <NanEventCallback.h>
|
||||
#include "../../logger.h"
|
||||
|
||||
#endif
|
||||
|
||||
namespace tc {
|
||||
namespace ft {
|
||||
namespace error {
|
||||
enum value : int8_t {
|
||||
success = 0,
|
||||
custom = 1,
|
||||
custom_recoverable = 2,
|
||||
would_block = 3,
|
||||
out_of_space = 4
|
||||
};
|
||||
}
|
||||
class TransferObject {
|
||||
public:
|
||||
explicit TransferObject() {}
|
||||
|
||||
virtual std::string name() const = 0;
|
||||
virtual bool initialize(std::string& /* error */) = 0;
|
||||
virtual void finalize() = 0;
|
||||
};
|
||||
|
||||
class TransferSource : public TransferObject {
|
||||
public:
|
||||
virtual uint64_t byte_length() const = 0;
|
||||
virtual uint64_t stream_index() const = 0;
|
||||
|
||||
virtual error::value read_bytes(std::string& /* error */, uint8_t* /* buffer */, uint64_t& /* max length/result length */) = 0;
|
||||
private:
|
||||
};
|
||||
|
||||
class TransferTarget : public TransferObject {
|
||||
public:
|
||||
TransferTarget() {}
|
||||
|
||||
virtual uint64_t expected_length() const = 0;
|
||||
|
||||
virtual uint64_t stream_index() const = 0;
|
||||
virtual error::value write_bytes(std::string& /* error */, uint8_t* /* buffer */, uint64_t /* max length */) = 0;
|
||||
};
|
||||
|
||||
struct TransferOptions {
|
||||
std::string remote_address;
|
||||
uint16_t remote_port = 0;
|
||||
std::string transfer_key{};
|
||||
uint32_t client_transfer_id = 0;
|
||||
uint32_t server_transfer_id = 0;
|
||||
};
|
||||
|
||||
class FileTransferManager;
|
||||
class Transfer {
|
||||
friend class FileTransferManager;
|
||||
public:
|
||||
struct state {
|
||||
enum value {
|
||||
UNINITIALIZED,
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCONNECTING
|
||||
};
|
||||
};
|
||||
typedef std::function<void()> callback_start_t;
|
||||
typedef std::function<void(bool /* aborted */)> callback_finished_t;
|
||||
typedef std::function<void(const std::string& /* error */)> callback_failed_t;
|
||||
typedef std::function<void(uint64_t /* current index */, uint64_t /* max index */)> callback_process_t;
|
||||
|
||||
explicit Transfer(FileTransferManager* handle, std::shared_ptr<TransferObject> transfer_object, std::unique_ptr<TransferOptions> options) :
|
||||
_transfer_object(std::move(transfer_object)),
|
||||
_handle(handle),
|
||||
_options(std::move(options)) {
|
||||
log_allocate("Transfer", this);
|
||||
}
|
||||
~Transfer();
|
||||
|
||||
bool initialize(std::string& /* error */);
|
||||
void finalize(bool /* blocking */ = true);
|
||||
|
||||
bool connect();
|
||||
bool connected() { return this->_state > state::UNINITIALIZED; }
|
||||
|
||||
FileTransferManager* handle() { return this->_handle; }
|
||||
std::shared_ptr<TransferObject> transfer_object() { return this->_transfer_object; }
|
||||
const TransferOptions& options() { return *this->_options; }
|
||||
|
||||
callback_start_t callback_start{nullptr};
|
||||
callback_finished_t callback_finished{nullptr};
|
||||
callback_failed_t callback_failed{nullptr};
|
||||
callback_process_t callback_process{nullptr};
|
||||
private:
|
||||
static void _callback_read(evutil_socket_t, short, void*);
|
||||
static void _callback_write(evutil_socket_t, short, void*);
|
||||
|
||||
sockaddr_storage remote_address{};
|
||||
FileTransferManager* _handle;
|
||||
std::unique_ptr<TransferOptions> _options;
|
||||
state::value _state = state::UNINITIALIZED;
|
||||
std::shared_ptr<TransferObject> _transfer_object;
|
||||
|
||||
std::mutex event_lock;
|
||||
event_base* event_io = nullptr; /* gets assigned by the manager */
|
||||
::event* event_read = nullptr;
|
||||
::event* event_write = nullptr;
|
||||
|
||||
std::chrono::system_clock::time_point last_source_read;
|
||||
std::chrono::system_clock::time_point last_target_write;
|
||||
std::mutex queue_lock;
|
||||
std::deque<pipes::buffer> write_queue;
|
||||
void _write_message(const pipes::buffer_view& /* buffer */);
|
||||
int _socket = 0;
|
||||
|
||||
timeval alive_check_timeout{1, 0};
|
||||
timeval write_timeout{1, 0};
|
||||
|
||||
/*
|
||||
* Upload mode:
|
||||
* Write the buffers left in write_queue, and if the queue length is less then 12 create new buffers.
|
||||
* This event will as well be triggered every second as timeout, to create new buffers if needed
|
||||
*/
|
||||
void callback_write(short /* flags */);
|
||||
void callback_read(short /* flags */);
|
||||
|
||||
/* called within the write/read callback */
|
||||
void handle_disconnect();
|
||||
void handle_connected();
|
||||
|
||||
void call_callback_connected();
|
||||
void call_callback_failed(const std::string& /* reason */);
|
||||
void call_callback_finished(bool /* aborted */);
|
||||
void call_callback_process(size_t /* current */, size_t /* max */);
|
||||
|
||||
std::chrono::system_clock::time_point last_process_call;
|
||||
};
|
||||
|
||||
class FileTransferManager {
|
||||
public:
|
||||
FileTransferManager();
|
||||
~FileTransferManager();
|
||||
|
||||
void initialize();
|
||||
void finalize();
|
||||
|
||||
std::shared_ptr<Transfer> register_transfer(std::string& error, const std::shared_ptr<TransferObject>& /* object */, std::unique_ptr<TransferOptions> /* options */);
|
||||
std::deque<std::shared_ptr<Transfer>> running_transfers() {
|
||||
std::lock_guard lock(this->_transfer_lock);
|
||||
return this->_running_transfers;
|
||||
}
|
||||
void drop_transfer(const std::shared_ptr<Transfer>& /* transfer */);
|
||||
void remove_transfer(Transfer*); /* internal use */
|
||||
inline void execute_event_loop() {
|
||||
this->event_execute = true;
|
||||
this->event_io_condition.notify_all();
|
||||
}
|
||||
private:
|
||||
bool event_execute = false;
|
||||
bool event_io_canceled = false;
|
||||
std::mutex event_io_lock;
|
||||
std::condition_variable event_io_condition;
|
||||
std::thread event_io_thread;
|
||||
event_base* event_io = nullptr;
|
||||
::event* event_cleanup = nullptr;
|
||||
|
||||
std::mutex _transfer_lock;
|
||||
std::deque<std::shared_ptr<Transfer>> _running_transfers;
|
||||
|
||||
void _execute_event_loop();
|
||||
};
|
||||
|
||||
#ifdef NODEJS_API
|
||||
class JSTransfer : public Nan::ObjectWrap {
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
explicit JSTransfer(std::shared_ptr<Transfer> transfer);
|
||||
~JSTransfer();
|
||||
|
||||
NAN_METHOD(start);
|
||||
NAN_METHOD(abort);
|
||||
|
||||
static NAN_METHOD(destory_transfer);
|
||||
private:
|
||||
static NAN_METHOD(_start);
|
||||
static NAN_METHOD(_abort);
|
||||
|
||||
std::shared_ptr<Transfer> _transfer;
|
||||
|
||||
Nan::callback_t<bool> call_finished;
|
||||
Nan::callback_t<> call_start;
|
||||
Nan::callback_t<uint64_t, uint64_t> call_progress;
|
||||
Nan::callback_t<std::string> call_failed;
|
||||
|
||||
void callback_finished(bool);
|
||||
void callback_start();
|
||||
void callback_progress(uint64_t, uint64_t);
|
||||
void callback_failed(std::string);
|
||||
|
||||
bool _self_ref = false;
|
||||
};
|
||||
#endif
|
||||
}
|
||||
}
|
||||
extern tc::ft::FileTransferManager* transfer_manager;
|
||||
@@ -1,290 +0,0 @@
|
||||
#include "FileTransferObject.h"
|
||||
#include "../../logger.h"
|
||||
#include <iostream>
|
||||
|
||||
#include <experimental/filesystem>
|
||||
|
||||
namespace fs = std::experimental::filesystem;
|
||||
using namespace tc;
|
||||
using namespace tc::ft;
|
||||
using namespace std;
|
||||
|
||||
#ifdef NODEJS_API
|
||||
TransferJSBufferTarget::TransferJSBufferTarget() {
|
||||
log_allocate("TransferJSBufferTarget", this);
|
||||
|
||||
if(!this->_js_buffer.IsEmpty()) {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
this->_js_buffer.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
TransferJSBufferTarget::~TransferJSBufferTarget() {
|
||||
log_free("TransferJSBufferTarget", this);
|
||||
}
|
||||
|
||||
bool TransferJSBufferTarget::initialize(std::string &error) {
|
||||
return true; /* we've already have data */
|
||||
}
|
||||
|
||||
void TransferJSBufferTarget::finalize() { }
|
||||
|
||||
uint64_t TransferJSBufferTarget::stream_index() const {
|
||||
return this->_js_buffer_index;
|
||||
}
|
||||
|
||||
error::value TransferJSBufferTarget::write_bytes(std::string &error, uint8_t *source, uint64_t length) {
|
||||
uint64_t write_length = length;
|
||||
if(length > this->_js_buffer_length - this->_js_buffer_index)
|
||||
write_length = this->_js_buffer_length - this->_js_buffer_index;
|
||||
|
||||
if(write_length > 0) {
|
||||
memcpy((char*) this->_js_buffer_source + this->_js_buffer_index, source, write_length);
|
||||
this->_js_buffer_index += write_length;
|
||||
}
|
||||
|
||||
if(write_length == 0)
|
||||
return error::out_of_space;
|
||||
|
||||
return error::success;
|
||||
}
|
||||
|
||||
NAN_METHOD(TransferJSBufferTarget::create_from_buffer) {
|
||||
if(info.Length() != 1 || !info[0]->IsArrayBuffer()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto buffer = info[0].As<v8::ArrayBuffer>();
|
||||
|
||||
auto instance = make_shared<TransferJSBufferTarget>();
|
||||
instance->_js_buffer = v8::Global<v8::ArrayBuffer>(info.GetIsolate(), info[0].As<v8::ArrayBuffer>());
|
||||
|
||||
instance->_js_buffer_source = buffer->GetContents().Data();
|
||||
instance->_js_buffer_length = buffer->GetContents().ByteLength();
|
||||
instance->_js_buffer_index = 0;
|
||||
|
||||
|
||||
auto object_wrap = new TransferObjectWrap(instance);
|
||||
auto object = Nan::NewInstance(Nan::New(TransferObjectWrap::constructor()), 0, nullptr).ToLocalChecked();
|
||||
object_wrap->do_wrap(object);
|
||||
info.GetReturnValue().Set(object);
|
||||
}
|
||||
|
||||
TransferJSBufferSource::~TransferJSBufferSource() {
|
||||
log_free("TransferJSBufferSource", this);
|
||||
|
||||
if(!this->_js_buffer.IsEmpty()) {
|
||||
assert(v8::Isolate::GetCurrent());
|
||||
this->_js_buffer.Reset();
|
||||
}
|
||||
}
|
||||
TransferJSBufferSource::TransferJSBufferSource() {
|
||||
log_allocate("TransferJSBufferSource", this);
|
||||
}
|
||||
|
||||
bool TransferJSBufferSource::initialize(std::string &string) { return true; }
|
||||
|
||||
void TransferJSBufferSource::finalize() { }
|
||||
|
||||
uint64_t TransferJSBufferSource::stream_index() const {
|
||||
return this->_js_buffer_index;
|
||||
}
|
||||
|
||||
uint64_t TransferJSBufferSource::byte_length() const {
|
||||
return this->_js_buffer_length;
|
||||
}
|
||||
|
||||
error::value TransferJSBufferSource::read_bytes(std::string &error, uint8_t *target, uint64_t &length) {
|
||||
auto org_length = length;
|
||||
if(this->_js_buffer_index + length > this->_js_buffer_length)
|
||||
length = this->_js_buffer_length - this->_js_buffer_index;
|
||||
|
||||
|
||||
memcpy(target, (char*) this->_js_buffer_source + this->_js_buffer_index, length);
|
||||
this->_js_buffer_index += length;
|
||||
|
||||
if(org_length != 0 && length == 0)
|
||||
return error::out_of_space;
|
||||
return error::success;
|
||||
}
|
||||
|
||||
NAN_METHOD(TransferJSBufferSource::create_from_buffer) {
|
||||
if(info.Length() != 1 || !info[0]->IsArrayBuffer()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto buffer = info[0].As<v8::ArrayBuffer>();
|
||||
|
||||
auto instance = make_shared<TransferJSBufferSource>();
|
||||
instance->_js_buffer = v8::Global<v8::ArrayBuffer>(info.GetIsolate(), info[0].As<v8::ArrayBuffer>());
|
||||
|
||||
instance->_js_buffer_source = buffer->GetContents().Data();
|
||||
instance->_js_buffer_length = buffer->GetContents().ByteLength();
|
||||
instance->_js_buffer_index = 0;
|
||||
|
||||
|
||||
auto object_wrap = new TransferObjectWrap(instance);
|
||||
auto object = Nan::NewInstance(Nan::New(TransferObjectWrap::constructor()), 0, nullptr).ToLocalChecked();
|
||||
object_wrap->do_wrap(object);
|
||||
info.GetReturnValue().Set(object);
|
||||
}
|
||||
|
||||
|
||||
NAN_MODULE_INIT(TransferObjectWrap::Init) {
|
||||
auto klass = Nan::New<v8::FunctionTemplate>(TransferObjectWrap::NewInstance);
|
||||
klass->SetClassName(Nan::New("TransferObjectWrap").ToLocalChecked());
|
||||
klass->InstanceTemplate()->SetInternalFieldCount(1);
|
||||
|
||||
constructor().Reset(Nan::GetFunction(klass).ToLocalChecked());
|
||||
}
|
||||
|
||||
NAN_METHOD(TransferObjectWrap::NewInstance) {
|
||||
if(!info.IsConstructCall())
|
||||
Nan::ThrowError("invalid invoke!");
|
||||
}
|
||||
|
||||
void TransferObjectWrap::do_wrap(v8::Local<v8::Object> object) {
|
||||
this->Wrap(object);
|
||||
|
||||
auto source = dynamic_pointer_cast<TransferSource>(this->target());
|
||||
auto target = dynamic_pointer_cast<TransferTarget>(this->target());
|
||||
|
||||
auto direction = source ? "upload" : "download";
|
||||
Nan::Set(object,
|
||||
Nan::New<v8::String>("direction").ToLocalChecked(),
|
||||
v8::String::NewFromUtf8(Nan::GetCurrentContext()->GetIsolate(), direction).ToLocalChecked()
|
||||
);
|
||||
Nan::Set(object,
|
||||
Nan::New<v8::String>("name").ToLocalChecked(),
|
||||
v8::String::NewFromUtf8(Nan::GetCurrentContext()->GetIsolate(), this->target()->name().c_str()).ToLocalChecked()
|
||||
);
|
||||
|
||||
if(source) {
|
||||
Nan::Set(object,
|
||||
Nan::New<v8::String>("total_size").ToLocalChecked(),
|
||||
Nan::New<v8::Number>(source->byte_length())
|
||||
);
|
||||
}
|
||||
|
||||
if(target) {
|
||||
Nan::Set(object,
|
||||
Nan::New<v8::String>("expected_size").ToLocalChecked(),
|
||||
Nan::New<v8::Number>(target->expected_length())
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
TransferFileSource::TransferFileSource(std::string path, std::string name) : _path{std::move(path)}, _name{std::move(name)} {
|
||||
if(!this->_path.empty()) {
|
||||
if(this->_path.back() == '/')
|
||||
this->_path.pop_back();
|
||||
#ifdef WIN32
|
||||
if(this->_path.back() == '\\')
|
||||
this->_path.pop_back();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
#define u8path path
|
||||
#endif
|
||||
|
||||
uint64_t TransferFileSource::byte_length() const {
|
||||
if(file_size.has_value())
|
||||
return file_size.value();
|
||||
|
||||
auto file = fs::u8path(this->_path) / fs::u8path(this->_name);
|
||||
error_code error;
|
||||
auto size = fs::file_size(file,error);
|
||||
if(error)
|
||||
size = 0;
|
||||
return (this->file_size = std::make_optional<size_t>(size)).value();
|
||||
}
|
||||
|
||||
bool TransferFileSource::initialize(std::string &error) {
|
||||
auto file = fs::u8path(this->_path) / fs::u8path(this->_name);
|
||||
|
||||
error_code errc;
|
||||
if(!fs::exists(file)) {
|
||||
error = "file not found";
|
||||
return false;
|
||||
}
|
||||
if(errc) {
|
||||
error = "failed to test for file existence: " + to_string(errc.value()) + "/" + errc.message();
|
||||
return false;
|
||||
}
|
||||
if(!fs::is_regular_file(file, errc)) {
|
||||
error = "target file isn't a regular file";
|
||||
return false;
|
||||
}
|
||||
if(errc) {
|
||||
error = "failed to test for file regularity: " + to_string(errc.value()) + "/" + errc.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
this->file_stream = std::ifstream{file, std::ifstream::in | std::ifstream::binary};
|
||||
if(!this->file_stream) {
|
||||
error = "failed to open file";
|
||||
return false;
|
||||
}
|
||||
|
||||
this->file_stream.seekg(0, std::ifstream::end);
|
||||
auto length = this->file_stream.tellg();
|
||||
if(length != this->byte_length()) {
|
||||
error = "file length missmatch";
|
||||
return false;
|
||||
}
|
||||
this->file_stream.seekg(0, std::ifstream::beg);
|
||||
|
||||
this->position = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TransferFileSource::finalize() {
|
||||
if(this->file_stream)
|
||||
this->file_stream.close();
|
||||
|
||||
this->position = 0;
|
||||
}
|
||||
|
||||
error::value TransferFileSource::read_bytes(std::string &error, uint8_t *buffer, uint64_t &length) {
|
||||
auto result = this->file_stream.readsome((char*) buffer, length);
|
||||
if(result > 0) {
|
||||
length = result;
|
||||
this->position += result;
|
||||
return error::success;
|
||||
}
|
||||
|
||||
if(!this->file_stream) {
|
||||
if(this->file_stream.eof())
|
||||
error = "eof reached";
|
||||
else
|
||||
error = "io error. failed to read";
|
||||
} else {
|
||||
error = "read returned " + to_string(result) + "/" + to_string(length);
|
||||
}
|
||||
return error::custom;
|
||||
}
|
||||
|
||||
uint64_t TransferFileSource::stream_index() const {
|
||||
return this->position;
|
||||
}
|
||||
|
||||
#ifdef NODEJS_API
|
||||
NAN_METHOD(TransferFileSource::create) {
|
||||
if(info.Length() != 2 || !info[0]->IsString() || !info[1]->IsString()) {
|
||||
Nan::ThrowError("invalid argument");
|
||||
return;
|
||||
}
|
||||
|
||||
auto instance = make_shared<TransferFileSource>(*Nan::Utf8String{info[0]}, *Nan::Utf8String{info[1]});
|
||||
auto object_wrap = new TransferObjectWrap(instance);
|
||||
auto object = Nan::NewInstance(Nan::New(TransferObjectWrap::constructor()), 0, nullptr).ToLocalChecked();
|
||||
object_wrap->do_wrap(object);
|
||||
info.GetReturnValue().Set(object);
|
||||
}
|
||||
#endif
|
||||
@@ -1,115 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <fstream>
|
||||
#include "FileTransferManager.h"
|
||||
|
||||
namespace tc {
|
||||
namespace ft {
|
||||
class TransferFileSource : public TransferSource {
|
||||
public:
|
||||
TransferFileSource(std::string /* path */, std::string /* name */);
|
||||
|
||||
[[nodiscard]] inline std::string file_path() const { return this->_path; }
|
||||
[[nodiscard]] inline std::string file_name() const { return this->_name; }
|
||||
|
||||
std::string name() const override { return "TransferFileSource"; }
|
||||
bool initialize(std::string &string) override;
|
||||
void finalize() override;
|
||||
|
||||
uint64_t byte_length() const override;
|
||||
uint64_t stream_index() const override;
|
||||
error::value read_bytes(std::string &string, uint8_t *uint8, uint64_t &uint64) override;
|
||||
|
||||
#ifdef NODEJS_API
|
||||
static NAN_METHOD(create);
|
||||
#endif
|
||||
private:
|
||||
std::string _path;
|
||||
std::string _name;
|
||||
|
||||
uint64_t position{0};
|
||||
std::ifstream file_stream{};
|
||||
mutable std::optional<size_t> file_size;
|
||||
};
|
||||
|
||||
#ifdef NODEJS_API
|
||||
class TransferObjectWrap : public Nan::ObjectWrap {
|
||||
public:
|
||||
static NAN_MODULE_INIT(Init);
|
||||
static NAN_METHOD(NewInstance);
|
||||
|
||||
static inline bool is_wrap(const v8::Local<v8::Value>& value) {
|
||||
if(value.As<v8::Object>().IsEmpty())
|
||||
return false;
|
||||
|
||||
return value->InstanceOf(Nan::GetCurrentContext(), Nan::New<v8::Function>(constructor())).FromMaybe(false);
|
||||
}
|
||||
|
||||
static inline Nan::Persistent<v8::Function> & constructor() {
|
||||
static Nan::Persistent<v8::Function> my_constructor;
|
||||
return my_constructor;
|
||||
}
|
||||
|
||||
explicit TransferObjectWrap(std::shared_ptr<TransferObject> object) : _transfer(std::move(object)) {
|
||||
}
|
||||
|
||||
~TransferObjectWrap() = default;
|
||||
|
||||
void do_wrap(v8::Local<v8::Object> object);
|
||||
|
||||
std::shared_ptr<TransferObject> target() { return this->_transfer; }
|
||||
private:
|
||||
std::shared_ptr<TransferObject> _transfer;
|
||||
};
|
||||
|
||||
class TransferJSBufferSource : public TransferSource {
|
||||
public:
|
||||
TransferJSBufferSource();
|
||||
virtual ~TransferJSBufferSource();
|
||||
|
||||
std::string name() const override { return "TransferJSBufferSource"; }
|
||||
bool initialize(std::string &string) override;
|
||||
|
||||
void finalize() override;
|
||||
|
||||
uint64_t stream_index() const override;
|
||||
|
||||
uint64_t byte_length() const override;
|
||||
|
||||
error::value read_bytes(std::string &string, uint8_t *uint8, uint64_t &uint64) override;
|
||||
|
||||
static NAN_METHOD(create_from_buffer);
|
||||
|
||||
private:
|
||||
v8::Global<v8::ArrayBuffer> _js_buffer;
|
||||
void* _js_buffer_source;
|
||||
uint64_t _js_buffer_length;
|
||||
uint64_t _js_buffer_index;
|
||||
};
|
||||
|
||||
class TransferJSBufferTarget : public TransferTarget {
|
||||
public:
|
||||
TransferJSBufferTarget();
|
||||
virtual ~TransferJSBufferTarget();
|
||||
|
||||
std::string name() const override { return "TransferJSBufferTarget"; }
|
||||
bool initialize(std::string &string) override;
|
||||
|
||||
void finalize() override;
|
||||
|
||||
uint64_t stream_index() const override;
|
||||
uint64_t expected_length() const override { return this->_js_buffer_length; }
|
||||
|
||||
error::value write_bytes(std::string &string, uint8_t *uint8, uint64_t uint64) override;
|
||||
|
||||
static NAN_METHOD(create_from_buffer);
|
||||
|
||||
private:
|
||||
v8::Global<v8::ArrayBuffer> _js_buffer;
|
||||
void* _js_buffer_source;
|
||||
uint64_t _js_buffer_length;
|
||||
uint64_t _js_buffer_index;
|
||||
};
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user