487 lines
20 KiB
C++
487 lines
20 KiB
C++
#include "mumlib/CryptState.hpp"
|
|
#include "mumlib/VarInt.hpp"
|
|
#include "mumlib/enums.hpp"
|
|
#include "mumlib/Transport.hpp"
|
|
#include "mumlib/Audio.hpp"
|
|
|
|
#include "mumlib.hpp"
|
|
|
|
#include <boost/asio.hpp>
|
|
#include <boost/bind.hpp>
|
|
#include <log4cpp/Category.hh>
|
|
|
|
#include <Mumble.pb.h>
|
|
|
|
using namespace std;
|
|
using namespace boost::asio;
|
|
|
|
using namespace mumlib;
|
|
|
|
namespace mumlib {
|
|
struct _Mumlib_Private : boost::noncopyable {
|
|
log4cpp::Category &logger = log4cpp::Category::getInstance("mumlib.Mumlib");
|
|
|
|
bool externalIoService;
|
|
io_service &ioService;
|
|
|
|
Callback &callback;
|
|
|
|
Transport transport;
|
|
|
|
Audio audio;
|
|
|
|
int sessionId = 0;
|
|
int channelId = 0;
|
|
|
|
_Mumlib_Private(Callback &callback, MumlibConfiguration &configuration)
|
|
: _Mumlib_Private(callback, *(new io_service()), configuration) {
|
|
externalIoService = false;
|
|
}
|
|
|
|
_Mumlib_Private(Callback &callback, io_service &ioService, MumlibConfiguration &configuration)
|
|
: callback(callback),
|
|
ioService(ioService),
|
|
externalIoService(true),
|
|
transport(ioService, boost::bind(&_Mumlib_Private::processIncomingTcpMessage, this, _1, _2, _3),
|
|
boost::bind(&_Mumlib_Private::processAudioPacket, this, _1, _2, _3)),
|
|
audio(configuration.opusSampleRate, configuration.opusEncoderBitrate) {
|
|
|
|
audio.setOpusEncoderBitrate(configuration.opusEncoderBitrate);
|
|
}
|
|
|
|
virtual ~_Mumlib_Private() {
|
|
if (not externalIoService) {
|
|
delete &ioService;
|
|
}
|
|
}
|
|
|
|
bool processAudioPacket(AudioPacketType type, uint8_t *buffer, int length) {
|
|
logger.info("Got %d B of encoded audio data.", length);
|
|
try {
|
|
auto incomingAudioPacket = audio.decodeIncomingAudioPacket(buffer, length);
|
|
|
|
if (type == AudioPacketType::OPUS) {
|
|
int16_t pcmData[5000];
|
|
auto status = audio.decodeOpusPayload(incomingAudioPacket.audioPayload,
|
|
incomingAudioPacket.audioPayloadLength,
|
|
pcmData,
|
|
5000);
|
|
|
|
callback.audio(incomingAudioPacket.target,
|
|
incomingAudioPacket.sessionId,
|
|
incomingAudioPacket.sequenceNumber,
|
|
pcmData,
|
|
status.first);
|
|
} else {
|
|
logger.warn("Incoming audio packet doesn't contain Opus data, calling unsupportedAudio callback.");
|
|
callback.unsupportedAudio(incomingAudioPacket.target,
|
|
incomingAudioPacket.sessionId,
|
|
incomingAudioPacket.sequenceNumber,
|
|
incomingAudioPacket.audioPayload,
|
|
incomingAudioPacket.audioPayloadLength);
|
|
}
|
|
|
|
} catch (mumlib::AudioException &exp) {
|
|
logger.error("Audio decode error: %s.", exp.what());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
|
|
bool processIncomingTcpMessage(MessageType messageType, uint8_t *buffer, int length) {
|
|
logger.debug("Process incoming message: type %d, length: %d.", messageType, length);
|
|
|
|
switch (messageType) {
|
|
case MessageType::VERSION: {
|
|
MumbleProto::Version version;
|
|
version.ParseFromArray(buffer, length);
|
|
callback.version(
|
|
version.version() >> 16,
|
|
version.version() >> 8 & 0xff,
|
|
version.version() & 0xff,
|
|
version.release(),
|
|
version.os(),
|
|
version.os_version());
|
|
}
|
|
break;
|
|
case MessageType::SERVERSYNC: {
|
|
MumbleProto::ServerSync serverSync;
|
|
serverSync.ParseFromArray(buffer, length);
|
|
|
|
sessionId = serverSync.session();
|
|
|
|
callback.serverSync(
|
|
serverSync.welcome_text(),
|
|
serverSync.session(),
|
|
serverSync.max_bandwidth(),
|
|
serverSync.permissions()
|
|
);
|
|
}
|
|
break;
|
|
case MessageType::CHANNELREMOVE: {
|
|
MumbleProto::ChannelRemove channelRemove;
|
|
channelRemove.ParseFromArray(buffer, length);
|
|
callback.channelRemove(channelRemove.channel_id());
|
|
}
|
|
break;
|
|
case MessageType::CHANNELSTATE: {
|
|
MumbleProto::ChannelState channelState;
|
|
channelState.ParseFromArray(buffer, length);
|
|
|
|
int32_t channel_id = channelState.has_channel_id() ? channelState.channel_id() : -1;
|
|
int32_t parent = channelState.has_parent() ? channelState.parent() : -1;
|
|
|
|
|
|
bool temporary = channelState.has_temporary() ? channelState.temporary()
|
|
: false; //todo make sure it's correct to assume it's false
|
|
int position = channelState.has_position() ? channelState.position() : 0;
|
|
|
|
vector<uint32_t> links;
|
|
for (int i = 0; i < channelState.links_size(); ++i) {
|
|
links.push_back(channelState.links(i));
|
|
}
|
|
|
|
vector<uint32_t> links_add;
|
|
for (int i = 0; i < channelState.links_add_size(); ++i) {
|
|
links_add.push_back(channelState.links_add(i));
|
|
}
|
|
|
|
vector<uint32_t> links_remove;
|
|
for (int i = 0; i < channelState.links_remove_size(); ++i) {
|
|
links_remove.push_back(channelState.links_remove(i));
|
|
}
|
|
|
|
// this->channelId = channel_id;
|
|
|
|
callback.channelState(
|
|
channelState.name(),
|
|
channel_id,
|
|
parent,
|
|
channelState.description(),
|
|
links,
|
|
links_add,
|
|
links_remove,
|
|
temporary,
|
|
position
|
|
);
|
|
}
|
|
break;
|
|
case MessageType::USERREMOVE: {
|
|
MumbleProto::UserRemove user_remove;
|
|
user_remove.ParseFromArray(buffer, length);
|
|
|
|
int32_t actor = user_remove.has_actor() ? user_remove.actor() : -1;
|
|
bool ban = user_remove.has_ban() ? user_remove.ban()
|
|
: false; //todo make sure it's correct to assume it's false
|
|
|
|
callback.userRemove(
|
|
user_remove.session(),
|
|
actor,
|
|
user_remove.reason(),
|
|
ban
|
|
);
|
|
}
|
|
break;
|
|
case MessageType::USERSTATE: {
|
|
MumbleProto::UserState userState;
|
|
userState.ParseFromArray(buffer, length);
|
|
|
|
// There are far too many things in this structure. Culling to the ones that are probably important
|
|
int32_t session = userState.has_session() ? userState.session() : -1;
|
|
int32_t actor = userState.has_actor() ? userState.actor() : -1;
|
|
int32_t user_id = userState.has_user_id() ? userState.user_id() : -1;
|
|
int32_t channel_id = userState.has_channel_id() ? userState.channel_id() : -1;
|
|
int32_t mute = userState.has_mute() ? userState.mute() : -1;
|
|
int32_t deaf = userState.has_deaf() ? userState.deaf() : -1;
|
|
int32_t suppress = userState.has_suppress() ? userState.suppress() : -1;
|
|
int32_t self_mute = userState.has_self_mute() ? userState.self_mute() : -1;
|
|
int32_t self_deaf = userState.has_self_deaf() ? userState.self_deaf() : -1;
|
|
int32_t priority_speaker = userState.has_priority_speaker() ? userState.priority_speaker() : -1;
|
|
int32_t recording = userState.has_recording() ? userState.recording() : -1;
|
|
|
|
callback.userState(session,
|
|
actor,
|
|
userState.name(),
|
|
user_id,
|
|
channel_id,
|
|
mute,
|
|
deaf,
|
|
suppress,
|
|
self_mute,
|
|
self_deaf,
|
|
userState.comment(),
|
|
priority_speaker,
|
|
recording);
|
|
}
|
|
break;
|
|
case MessageType::BANLIST: {
|
|
MumbleProto::BanList ban_list;
|
|
ban_list.ParseFromArray(buffer, length);
|
|
for (int i = 0; i < ban_list.bans_size(); i++) {
|
|
auto ban = ban_list.bans(i);
|
|
|
|
const uint8_t *ip_data = reinterpret_cast<const uint8_t *>(ban.address().c_str());
|
|
uint32_t ip_data_size = ban.address().size();
|
|
int32_t duration = ban.has_duration() ? ban.duration() : -1;
|
|
|
|
callback.banList(
|
|
ip_data,
|
|
ip_data_size,
|
|
ban.mask(),
|
|
ban.name(),
|
|
ban.hash(),
|
|
ban.reason(),
|
|
ban.start(),
|
|
duration);
|
|
}
|
|
}
|
|
break;
|
|
case MessageType::TEXTMESSAGE: {
|
|
MumbleProto::TextMessage text_message;
|
|
text_message.ParseFromArray(buffer, length);
|
|
|
|
int32_t actor = text_message.has_actor() ? text_message.actor() : -1;
|
|
|
|
vector<uint32_t> sessions;
|
|
for (int i = 0; i < text_message.session_size(); ++i) {
|
|
sessions.push_back(text_message.session(i));
|
|
}
|
|
|
|
vector<uint32_t> channel_ids;
|
|
for (int i = 0; i < text_message.channel_id_size(); ++i) {
|
|
channel_ids.push_back(text_message.channel_id(i));
|
|
}
|
|
|
|
vector<uint32_t> tree_ids;
|
|
for (int i = 0; i < text_message.tree_id_size(); ++i) {
|
|
tree_ids.push_back(text_message.tree_id(i));
|
|
}
|
|
|
|
callback.textMessage(actor, sessions, channel_ids, tree_ids, text_message.message());
|
|
}
|
|
break;
|
|
case MessageType::PERMISSIONDENIED: // 12
|
|
logger.warn("PermissionDenied Message: support not implemented yet");
|
|
break;
|
|
case MessageType::ACL: // 13
|
|
logger.warn("ACL Message: support not implemented yet.");
|
|
break;
|
|
case MessageType::QUERYUSERS: // 14
|
|
logger.warn("QueryUsers Message: support not implemented yet");
|
|
break;
|
|
case MessageType::CONTEXTACTIONMODIFY: // 16
|
|
logger.warn("ContextActionModify Message: support not implemented yet");
|
|
break;
|
|
case MessageType::CONTEXTACTION: // 17
|
|
logger.warn("ContextAction Message: support not implemented yet");
|
|
break;
|
|
case MessageType::USERLIST: // 18
|
|
logger.warn("UserList Message: support not implemented yet");
|
|
break;
|
|
case MessageType::VOICETARGET:
|
|
logger.warn("VoiceTarget Message: I don't think the server ever sends this structure.");
|
|
break;
|
|
case MessageType::PERMISSIONQUERY: {
|
|
MumbleProto::PermissionQuery permissionQuery;
|
|
permissionQuery.ParseFromArray(buffer, length);
|
|
|
|
int32_t channel_id = permissionQuery.has_channel_id() ? permissionQuery.channel_id() : -1;
|
|
uint32_t permissions = permissionQuery.has_permissions() ? permissionQuery.permissions() : 0;
|
|
uint32_t flush = permissionQuery.has_flush() ? permissionQuery.flush() : -1;
|
|
|
|
callback.permissionQuery(channel_id, permissions, flush);
|
|
}
|
|
break;
|
|
case MessageType::CODECVERSION: {
|
|
MumbleProto::CodecVersion codecVersion;
|
|
codecVersion.ParseFromArray(buffer, length);
|
|
|
|
int32_t alpha = codecVersion.alpha();
|
|
int32_t beta = codecVersion.beta();
|
|
uint32_t prefer_alpha = codecVersion.prefer_alpha();
|
|
int32_t opus = codecVersion.has_opus() ? codecVersion.opus() : 0;
|
|
|
|
callback.codecVersion(alpha, beta, prefer_alpha, opus);
|
|
}
|
|
break;
|
|
case MessageType::USERSTATS:
|
|
logger.warn("UserStats Message: support not implemented yet");
|
|
break;
|
|
case MessageType::REQUESTBLOB: // 23
|
|
logger.warn("RequestBlob Message: I don't think this is sent by the server.");
|
|
break;
|
|
case MessageType::SERVERCONFIG: {
|
|
MumbleProto::ServerConfig serverConfig;
|
|
serverConfig.ParseFromArray(buffer, length);
|
|
|
|
uint32_t max_bandwidth = serverConfig.has_max_bandwidth() ? serverConfig.max_bandwidth() : 0;
|
|
uint32_t allow_html = serverConfig.has_allow_html() ? serverConfig.allow_html() : 0;
|
|
uint32_t message_length = serverConfig.has_message_length() ? serverConfig.message_length() : 0;
|
|
uint32_t image_message_length = serverConfig.has_image_message_length()
|
|
? serverConfig.image_message_length() : 0;
|
|
|
|
callback.serverConfig(max_bandwidth, serverConfig.welcome_text(), allow_html, message_length,
|
|
image_message_length);
|
|
}
|
|
break;
|
|
case MessageType::SUGGESTCONFIG: // 25
|
|
logger.warn("SuggestConfig Message: support not implemented yet");
|
|
break;
|
|
default:
|
|
throw MumlibException("unknown message type: " + to_string(static_cast<int>(messageType)));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
};
|
|
|
|
Mumlib::Mumlib(Callback &callback) {
|
|
MumlibConfiguration conf;
|
|
impl = new _Mumlib_Private(callback, conf);
|
|
}
|
|
|
|
Mumlib::Mumlib(Callback &callback, io_service &ioService) {
|
|
MumlibConfiguration conf;
|
|
impl = new _Mumlib_Private(callback, ioService, conf);
|
|
}
|
|
|
|
Mumlib::Mumlib(Callback &callback, MumlibConfiguration &configuration)
|
|
: impl(new _Mumlib_Private(callback, configuration)) { }
|
|
|
|
Mumlib::Mumlib(Callback &callback, io_service &ioService, MumlibConfiguration &configuration)
|
|
: impl(new _Mumlib_Private(callback, ioService, configuration)) { }
|
|
|
|
Mumlib::~Mumlib() {
|
|
disconnect();
|
|
|
|
delete impl;
|
|
}
|
|
|
|
ConnectionState Mumlib::getConnectionState() {
|
|
return impl->transport.getConnectionState();
|
|
}
|
|
|
|
int Mumlib::getChannelId() {
|
|
return impl->channelId;
|
|
}
|
|
|
|
void Mumlib::connect(string host, int port, string user, string password) {
|
|
impl->transport.connect(host, port, user, password);
|
|
}
|
|
|
|
void Mumlib::disconnect() {
|
|
if (not impl->externalIoService) {
|
|
impl->ioService.stop();
|
|
}
|
|
if (impl->transport.getConnectionState() != ConnectionState::NOT_CONNECTED) {
|
|
impl->transport.disconnect();
|
|
}
|
|
}
|
|
|
|
void Mumlib::reconnect() {
|
|
if (not impl->externalIoService) {
|
|
impl->ioService.reset();
|
|
}
|
|
if (impl->transport.getConnectionState() != ConnectionState::NOT_CONNECTED) {
|
|
impl->transport.disconnect();
|
|
}
|
|
}
|
|
|
|
void Mumlib::run() {
|
|
if (impl->externalIoService) {
|
|
throw MumlibException("can't call run() when using external io_service");
|
|
}
|
|
|
|
impl->ioService.run();
|
|
}
|
|
|
|
void Mumlib::sendAudioData(int16_t *pcmData, int pcmLength) {
|
|
uint8_t encodedData[5000];
|
|
int length = impl->audio.encodeAudioPacket(0, pcmData, pcmLength, encodedData, 5000);
|
|
impl->transport.sendEncodedAudioPacket(encodedData, length);
|
|
}
|
|
|
|
void Mumlib::sendAudioDataTarget(int targetId, int16_t *pcmData, int pcmLength) {
|
|
uint8_t encodedData[5000];
|
|
int length = impl->audio.encodeAudioPacket(targetId, pcmData, pcmLength, encodedData, 5000);
|
|
impl->transport.sendEncodedAudioPacket(encodedData, length);
|
|
}
|
|
|
|
void Mumlib::sendTextMessage(string message) {
|
|
MumbleProto::TextMessage textMessage;
|
|
textMessage.set_actor(impl->sessionId);
|
|
textMessage.add_channel_id(impl->channelId);
|
|
textMessage.set_message(message);
|
|
impl->transport.sendControlMessage(MessageType::TEXTMESSAGE, textMessage);
|
|
}
|
|
|
|
void Mumlib::joinChannel(int channelId) {
|
|
MumbleProto::UserState userState;
|
|
userState.set_channel_id(channelId);
|
|
impl->transport.sendControlMessage(MessageType::USERSTATE, userState);
|
|
impl->channelId = channelId;
|
|
}
|
|
|
|
void Mumlib::sendVoiceTarget(int targetId, int channelId) {
|
|
MumbleProto::VoiceTarget voiceTarget;
|
|
MumbleProto::VoiceTarget_Target voiceTargetTarget;
|
|
voiceTargetTarget.set_channel_id(channelId);
|
|
voiceTargetTarget.set_children(true);
|
|
voiceTarget.set_id(targetId);
|
|
voiceTarget.add_targets()->CopyFrom(voiceTargetTarget);
|
|
impl->transport.sendControlMessage(MessageType::VOICETARGET, voiceTarget);
|
|
}
|
|
|
|
void Mumlib::sendUserState(mumlib::UserState field, bool val) {
|
|
MumbleProto::UserState userState;
|
|
|
|
switch (field) {
|
|
case UserState::MUTE:
|
|
userState.set_mute(val);
|
|
break;
|
|
case UserState::DEAF:
|
|
userState.set_deaf(val);
|
|
break;
|
|
case UserState::SUPPRESS:
|
|
userState.set_suppress(val);
|
|
break;
|
|
case UserState::SELF_MUTE:
|
|
userState.set_self_mute(val);
|
|
break;
|
|
case UserState::SELF_DEAF:
|
|
userState.set_self_deaf(val);
|
|
break;
|
|
case UserState::PRIORITY_SPEAKER:
|
|
userState.set_priority_speaker(val);
|
|
break;
|
|
case UserState::RECORDING:
|
|
userState.set_recording(val);
|
|
break;
|
|
default:
|
|
// in any other case, just ignore the command
|
|
return;
|
|
}
|
|
|
|
impl->transport.sendControlMessage(MessageType::USERSTATE, userState);
|
|
}
|
|
|
|
void Mumlib::sendUserState(mumlib::UserState field, std::string val) {
|
|
MumbleProto::UserState userState;
|
|
|
|
switch (field) {
|
|
case UserState::COMMENT:
|
|
// TODO: if comment longer than 128 bytes, we need to set the SHA1 hash
|
|
userState.set_comment(val);
|
|
break;
|
|
default:
|
|
// in any other case, just ignore the command
|
|
return;
|
|
}
|
|
|
|
impl->transport.sendControlMessage(MessageType::USERSTATE, userState);
|
|
}
|
|
}
|