blob: b47264aa8451ea2436703974ee92dcadfcd57982 [file] [log] [blame]
/*
* Copyright (C) 2021-2022 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#import "Adapter.h"
#import "Utils.h"
#import "jamiNotificationExtension-Swift.h"
#import "jami/jami.h"
#import "jami/configurationmanager_interface.h"
#import "jami/callmanager_interface.h"
#import "jami/conversation_interface.h"
#import "jami/datatransfer_interface.h"
#define MSGPACK_DISABLE_LEGACY_NIL
#import "opendht/crypto.h"
#import "opendht/default_types.h"
#import "yaml-cpp/yaml.h"
#import "json/json.h"
#import "fstream"
#import "charconv"
@implementation Adapter
static id<AdapterDelegate> _delegate;
using namespace libjami;
struct PeerConnectionRequest : public dht::EncryptedValue<PeerConnectionRequest>
{
static const constexpr dht::ValueType& TYPE = dht::ValueType::USER_DATA;
static constexpr const char* key_prefix = "peer:";
dht::Value::Id id = dht::Value::INVALID_ID;
std::string ice_msg {};
bool isAnswer {false};
std::string connType {};
MSGPACK_DEFINE_MAP(id, ice_msg, isAnswer, connType)
};
// Constants
const std::string fileSeparator = "/";
NSString* const certificates = @"certificates";
NSString* const crls = @"crls";
NSString* const ocsp = @"ocsp";
NSString* const nameCache = @"namecache";
NSString* const defaultNameServer = @"ns.jami.net";
std::string const nameServerConfiguration = "RingNS.uri";
NSString* const accountConfig = @"config.yml";
std::map<std::string, std::shared_ptr<CallbackWrapperBase>> confHandlers;
std::map<std::string, std::string> cachedNames;
std::map<std::string, std::string> nameServers;
#pragma mark Callbacks registration
- (void)registerSignals
{
confHandlers.insert(exportable_callback<ConfigurationSignal::GetAppDataPath>(
[](const std::string& name, std::vector<std::string>* ret) {
if (name == "cache") {
auto path = [Constants cachesPath];
ret->push_back(std::string([path.path UTF8String]));
} else {
auto path = [Constants documentsPath];
ret->push_back(std::string([path.path UTF8String]));
}
}));
confHandlers.insert(exportable_callback<ConversationSignal::MessageReceived>(
[weakDelegate = Adapter.delegate](const std::string& accountId,
const std::string& conversationId,
std::map<std::string, std::string> message) {
id<AdapterDelegate> delegate = weakDelegate;
if (delegate) {
NSString* convId = [NSString stringWithUTF8String:conversationId.c_str()];
NSString* account = [NSString stringWithUTF8String:accountId.c_str()];
NSMutableDictionary* interaction = [Utils mapToDictionnary:message];
[delegate newInteractionWithConversationId:convId
accountId:account
message:interaction];
}
}));
confHandlers.insert(exportable_callback<DataTransferSignal::DataTransferEvent>(
[weakDelegate = Adapter.delegate](const std::string& account_id,
const std::string& conversation_id,
const std::string& interaction_id,
const std::string& file_id,
int eventCode) {
id<AdapterDelegate> delegate = weakDelegate;
if (delegate) {
NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
NSString* conversationId = [NSString stringWithUTF8String:conversation_id.c_str()];
NSString* fileId = [NSString stringWithUTF8String:file_id.c_str()];
NSString* interactionId = [NSString stringWithUTF8String:interaction_id.c_str()];
[delegate dataTransferEventWithFileId:fileId
withEventCode:eventCode
accountId:accountId
conversationId:conversationId
interactionId:interactionId];
}
}));
confHandlers.insert(exportable_callback<ConversationSignal::ConversationSyncFinished>(
[weakDelegate = Adapter.delegate](const std::string& account_id) {
id<AdapterDelegate> delegate = weakDelegate;
if (delegate) {
NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
[delegate conversationSyncCompletedWithAccountId:accountId];
}
}));
confHandlers.insert(exportable_callback<ConversationSignal::ConversationCloned>(
[weakDelegate = Adapter.delegate](const std::string& account_id) {
id<AdapterDelegate> delegate = weakDelegate;
if (delegate) {
NSString* accountId = [NSString stringWithUTF8String:account_id.c_str()];
[delegate conversationClonedWithAccountId:accountId];
}
}));
confHandlers.insert(exportable_callback<ConversationSignal::ConversationRequestReceived>([weakDelegate = Adapter.delegate](const std::string& accountId, const std::string& conversationId, std::map<std::string, std::string> metadata) {
id<AdapterDelegate> delegate = weakDelegate;
if (delegate) {
NSString* accountIdStr = [NSString stringWithUTF8String:accountId.c_str()];
NSString* convIdStr = [NSString stringWithUTF8String:conversationId.c_str()];
NSMutableDictionary* info = [Utils mapToDictionnary: metadata];
[delegate receivedConversationRequestWithAccountId: accountIdStr conversationId: convIdStr metadata:info];
}
}));
registerSignalHandlers(confHandlers);
}
#pragma mark AdapterDelegate
+ (id<AdapterDelegate>)delegate
{
return _delegate;
}
+ (void)setDelegate:(id<AdapterDelegate>)delegate
{
_delegate = delegate;
}
- (bool)downloadFileWithFileId:(NSString*)fileId
accountId:(NSString*)accountId
conversationId:(NSString*)conversationId
interactionId:(NSString*)interactionId
withFilePath:(NSString*)filePath
{
return downloadFile(std::string([accountId UTF8String]),
std::string([conversationId UTF8String]),
std::string([interactionId UTF8String]),
std::string([fileId UTF8String]),
std::string([filePath UTF8String]));
}
- (BOOL)start:(NSString*)accountId
{
[self registerSignals];
if (initialized() == true) {
reloadConversationsAndRequests(std::string([accountId UTF8String]));
setAccountActive(std::string([accountId UTF8String]), true);
return true;
}
#if DEBUG
int flag = LIBJAMI_FLAG_CONSOLE_LOG | LIBJAMI_FLAG_DEBUG | LIBJAMI_FLAG_IOS_EXTENSION | LIBJAMI_FLAG_NO_AUTOSYNC | LIBJAMI_FLAG_NO_LOCAL_AUDIO;
#else
int flag = LIBJAMI_FLAG_IOS_EXTENSION | LIBJAMI_FLAG_NO_AUTOSYNC | LIBJAMI_FLAG_NO_LOCAL_AUDIO;
#endif
if (![[NSThread currentThread] isMainThread]) {
__block bool success;
dispatch_sync(dispatch_get_main_queue(), ^{
if (init(static_cast<InitFlag>(flag))) {
success = start({});
} else {
success = false;
}
});
return success;
} else {
if (init(static_cast<InitFlag>(flag))) {
return start({});
}
return false;
}
}
- (void)stop
{
unregisterSignalHandlers();
confHandlers.clear();
[self setAccountsActive:false];
}
- (void)setAccountsActive:(BOOL)active
{
auto accounts = getAccountList();
for (auto account : accounts) {
if (active) {
reloadConversationsAndRequests(account);
}
setAccountActive(account, active, true);
}
}
- (NSDictionary<NSString*, NSString*>*)decrypt:(NSString*)keyPath
accountId:(NSString*)accountId
treated:(NSString*)treatedMessagesPath
value:(NSDictionary*)value
{
if (![[NSFileManager defaultManager] fileExistsAtPath:keyPath]) {
return {};
}
NSData* data = [[NSFileManager defaultManager] contentsAtPath:keyPath];
const uint8_t* bytes = (const uint8_t*) [data bytes];
dht::crypto::PrivateKey dhtKey(bytes, [data length], "");
Json::Value jsonValue = toJson(value);
dht::Value dhtValue(jsonValue);
if (!dhtValue.isEncrypted()) {
return {};
}
try {
dht::Sp<dht::Value> decrypted = dhtValue.decrypt(dhtKey);
auto unpacked = msgpack::unpack((const char*) decrypted->data.data(), decrypted->data.size());
auto peerCR = unpacked.get().as<PeerConnectionRequest>();
if (peerCR.connType.empty()) {
// this value is not a PeerConnectionRequest
// check if it a TrustRequest
auto conversationRequest = unpacked.get().as<dht::TrustRequest>();
if (conversationRequest.confirm) {
// request confirmation. We need to wait for conversation to clone
return @{@"": @"application/clone"};
}
if (!conversationRequest.conversationId.empty()) {
if (conversationRequest.service == "cx.ring") {
// return git message type to start daemon
return @{@"": @"application/im-gitmessage-id"};
}
}
return {};
}
if (isMessageTreated(peerCR.id, [treatedMessagesPath UTF8String])) {
return {};
}
std::string peerId = "";
if (peerCR.connType == "videoCall" || peerCR.connType == "audioCall") {
auto certPath = [[[Constants documentsPath] URLByAppendingPathComponent:accountId] URLByAppendingPathComponent:certificates].path.UTF8String;
auto crlPath = [[[Constants documentsPath] URLByAppendingPathComponent:accountId] URLByAppendingPathComponent:crls].path.UTF8String;
auto ocspPath = [[[Constants documentsPath] URLByAppendingPathComponent:accountId] URLByAppendingPathComponent:ocsp].path.UTF8String;
peerId = getPeerId(decrypted->owner->getId().toString(),
certPath,
crlPath,
ocspPath);
}
return @{@(peerId.c_str()): @(peerCR.connType.c_str())};
} catch (std::runtime_error error) {
}
return {};
}
-(NSString*)getNameFor:(NSString*)address accountId:(NSString*)accountId {
return @(getName(std::string([address UTF8String]), std::string([accountId UTF8String])).c_str());
}
-(NSString*)nameServerForAccountId:(NSString*)accountId; {
auto nameServer = getNameServer(std::string([accountId UTF8String]));
return nameServer.empty() ? defaultNameServer : @(nameServer.c_str());
}
Json::Value
toJson(NSDictionary* value)
{
Json::Value val;
for (NSString* key in value.allKeys) {
if ([[value objectForKey:key] isKindOfClass:[NSString class]]) {
NSString* stringValue = [value objectForKey:key];
val[key.UTF8String] = stringValue.UTF8String;
} else if ([[value objectForKey:key] isKindOfClass:[NSNumber class]]) {
NSNumber* number = [value objectForKey:key];
if ([key isEqualToString:@"id"]) {
unsigned long long int intValue = [number unsignedLongLongValue];
val[key.UTF8String] = intValue;
} else {
int intValue = [number intValue];
val[key.UTF8String] = intValue;
}
}
}
return val;
}
std::string getName(std::string addres, std::string accountId)
{
auto name = cachedNames.find(addres);
if (name != cachedNames.end()) {
return name->second;
}
auto ns = getNameServer(accountId);
NSURL *url = [NSURL URLWithString: @(ns.c_str())];
NSString* host = [url host];
NSString* nameServer = host.length == 0 ? defaultNameServer : host;
std::string namesPath = [[[Constants cachesPath] URLByAppendingPathComponent: nameCache] URLByAppendingPathComponent: nameServer].path.UTF8String;
msgpack::unpacker pac;
// read file
std::ifstream file = std::ifstream(namesPath, std::ios_base::in);
if (!file.is_open()) {
return "";
}
std::string line;
while (std::getline(file, line)) {
pac.reserve_buffer(line.size());
memcpy(pac.buffer(), line.data(), line.size());
pac.buffer_consumed(line.size());
}
// load values
msgpack::object_handle oh;
if (pac.next(oh))
oh.get().convert(cachedNames);
auto cacheRes = cachedNames.find(addres);
return cacheRes != cachedNames.end() ? cacheRes->second : std::string {};
}
std::string getNameServer(std::string accountId) {
auto it = nameServers.find(accountId);
if (it != nameServers.end()) {
return it->second;
}
std::string nameServer {};
auto accountConfigPath = [[[Constants documentsPath] URLByAppendingPathComponent: @(accountId.c_str())] URLByAppendingPathComponent: accountConfig].path.UTF8String;
try {
std::ifstream file = std::ifstream(accountConfigPath, std::ios_base::in);
YAML::Node node = YAML::Load(file);
file.close();
nameServer = node[nameServerConfiguration].as<std::string>();
if (!nameServer.empty()) {
nameServers.insert(std::pair<std::string, std::string>(accountId, nameServer));
}
} catch (const std::exception& e) {}
return nameServer;
}
#pragma mark functions copied from the daemon
#define LIKELY(expr) (expr)
#define UNLIKELY(expr) (expr)
/*
* Check whether a Unicode (5.2) char is in a valid range.
*
* The first check comes from the Unicode guarantee to never encode
* a point above 0x0010ffff, since UTF-16 couldn't represent it.
*
* The second check covers surrogate pairs (category Cs).
*
* @param Char the character
*/
#define UNICODE_VALID(Char) ((Char) < 0x110000 && (((Char) &0xFFFFF800) != 0xD800))
#define CONTINUATION_CHAR \
if ((*(unsigned char*) p & 0xc0) != 0x80) /* 10xxxxxx */ \
goto error; \
val <<= 6; \
val |= (*(unsigned char*) p) & 0x3f;
template<typename ID = dht::Value::Id>
bool
isMessageTreated(ID messageId, const std::string& path)
{
std::ifstream file = std::ifstream(path, std::ios_base::in);
if (!file.is_open()) {
return false;
}
std::set<ID, std::less<>> treatedMessages;
std::string line;
while (std::getline(file, line)) {
if constexpr (std::is_same<ID, std::string>::value) {
treatedMessages.emplace(std::move(line));
} else if constexpr (std::is_integral<ID>::value) {
ID vid;
if (auto [p, ec] = std::from_chars(line.data(), line.data() + line.size(), vid, 16);
ec == std::errc()) {
treatedMessages.emplace(vid);
}
}
}
return treatedMessages.find(messageId) != treatedMessages.end();
}
std::string
getPeerId(const std::string& key,
const std::string& certPath,
const std::string& crlPath,
const std::string& ocspPath)
{
std::map<std::string, std::shared_ptr<dht::crypto::Certificate>> certs;
auto dir_content = readDirectory(certPath);
unsigned n = 0;
for (const auto& f : dir_content) {
try {
auto crt = std::make_shared<dht::crypto::Certificate>(
loadFile(certPath + fileSeparator + f));
auto id = crt->getId().toString();
auto longId = crt->getLongId().toString();
if (id != f && longId != f)
throw std::logic_error("Certificate id mismatch");
while (crt) {
id = crt->getId().toString();
longId = crt->getLongId().toString();
certs.emplace(std::move(id), crt);
certs.emplace(std::move(longId), crt);
loadRevocations(*crt, crlPath, ocspPath);
crt = crt->issuer;
++n;
}
} catch (const std::exception& e) {
}
}
auto cit = certs.find(key);
if (cit == certs.cend()) {
return {};
}
dht::InfoHash peer_account_id;
if (not foundPeerDevice(cit->second, peer_account_id)) {
return {};
}
return peer_account_id.toString();
}
void
loadRevocations(dht::crypto::Certificate& crt,
const std::string& crlPath,
const std::string& ocspPath)
{
auto dir = crlPath + fileSeparator + crt.getId().toString();
for (const auto& crl : readDirectory(dir)) {
try {
crt.addRevocationList(
std::make_shared<dht::crypto::RevocationList>(loadFile(dir + fileSeparator + crl)));
} catch (const std::exception& e) {
}
}
auto ocsp_dir = ocspPath + fileSeparator + crt.getId().toString();
for (const auto& ocsp : readDirectory(ocsp_dir)) {
try {
std::string ocsp_filepath = ocsp_dir + fileSeparator + ocsp;
auto serial = crt.getSerialNumber();
if (dht::toHex(serial.data(), serial.size()) != ocsp)
continue;
dht::Blob ocspBlob = loadFile(ocsp_filepath);
crt.ocspResponse = std::make_shared<dht::crypto::OcspResponse>(ocspBlob.data(),
ocspBlob.size());
} catch (const std::exception& e) {
}
}
}
bool
foundPeerDevice(const std::shared_ptr<dht::crypto::Certificate>& crt, dht::InfoHash& account_id)
{
if (not crt)
return false;
auto top_issuer = crt;
while (top_issuer->issuer)
top_issuer = top_issuer->issuer;
if (top_issuer == crt) {
return false;
}
dht::crypto::TrustList peer_trust;
peer_trust.add(*top_issuer);
if (not peer_trust.verify(*crt)) {
return false;
}
if (crt->ocspResponse and crt->ocspResponse->getCertificateStatus() != GNUTLS_OCSP_CERT_GOOD) {
return false;
}
account_id = crt->issuer->getId();
return true;
}
std::vector<std::string>
readDirectory(const std::string& dir)
{
NSError* error;
NSFileManager* fileMgr = [NSFileManager defaultManager];
NSArray* files = [fileMgr contentsOfDirectoryAtPath:@(dir.c_str()) error:&error];
std::vector<std::string> vector;
for (NSString* fileName in files) {
vector.push_back([fileName UTF8String]);
}
return vector;
}
std::vector<uint8_t>
loadFile(const std::string& path)
{
if (![[NSFileManager defaultManager] fileExistsAtPath:@(path.c_str())]) {
return {};
}
NSData* data = [[NSFileManager defaultManager] contentsAtPath:@(path.c_str())];
return [Utils vectorOfUInt8FromData:data];
}
std::string
utf8_make_valid(const std::string& name)
{
ssize_t remaining_bytes = name.size();
ssize_t valid_bytes;
const char* remainder = name.c_str();
const char* invalid;
char* str = NULL;
char* pos = nullptr;
while (remaining_bytes != 0) {
if (utf8_validate_c_str(remainder, remaining_bytes, &invalid))
break;
valid_bytes = invalid - remainder;
if (str == NULL)
// If every byte is replaced by U+FFFD, max(strlen(string)) == 3 * name.size()
str = new char[3 * remaining_bytes];
pos = str;
strncpy(pos, remainder, valid_bytes);
pos += valid_bytes;
/* append U+FFFD REPLACEMENT CHARACTER */
pos[0] = '\357';
pos[1] = '\277';
pos[2] = '\275';
pos += 3;
remaining_bytes -= valid_bytes + 1;
remainder = invalid + 1;
}
if (str == NULL)
return std::string(name);
strncpy(pos, remainder, remaining_bytes);
pos += remaining_bytes;
std::string answer(str, pos - str);
assert(utf8_validate_c_str(answer.c_str(), -1, NULL));
delete[] str;
return answer;
}
bool
utf8_validate_c_str(const char* str, ssize_t max_len, const char** end)
{
const char* p;
if (max_len < 0)
p = fast_validate(str);
else
p = fast_validate_len(str, max_len);
if (end)
*end = p;
if ((max_len >= 0 && p != str + max_len) || (max_len < 0 && *p != '\0'))
return false;
else
return true;
}
static const char*
fast_validate(const char* str)
{
char32_t val = 0;
char32_t min = 0;
const char* p;
for (p = str; *p; p++) {
if (*(unsigned char*) p < 128)
/* done */;
else {
const char* last;
last = p;
if ((*(unsigned char*) p & 0xe0) == 0xc0) { /* 110xxxxx */
if (UNLIKELY((*(unsigned char*) p & 0x1e) == 0))
goto error;
p++;
if (UNLIKELY((*(unsigned char*) p & 0xc0) != 0x80)) /* 10xxxxxx */
goto error;
} else {
if ((*(unsigned char*) p & 0xf0) == 0xe0) { /* 1110xxxx */
min = (1 << 11);
val = *(unsigned char*) p & 0x0f;
goto TWO_REMAINING;
} else if ((*(unsigned char*) p & 0xf8) == 0xf0) { /* 11110xxx */
min = (1 << 16);
val = *(unsigned char*) p & 0x07;
} else
goto error;
p++;
CONTINUATION_CHAR;
TWO_REMAINING:
p++;
CONTINUATION_CHAR;
p++;
CONTINUATION_CHAR;
if (UNLIKELY(val < min))
goto error;
if (UNLIKELY(!UNICODE_VALID(val)))
goto error;
}
continue;
error:
return last;
}
}
return p;
}
static const char*
fast_validate_len(const char* str, ssize_t max_len)
{
char32_t val = 0;
char32_t min = 0;
const char* p;
assert(max_len >= 0);
for (p = str; ((p - str) < max_len) && *p; p++) {
if (*(unsigned char*) p < 128)
/* done */;
else {
const char* last;
last = p;
if ((*(unsigned char*) p & 0xe0) == 0xc0) { /* 110xxxxx */
if (UNLIKELY(max_len - (p - str) < 2))
goto error;
if (UNLIKELY((*(unsigned char*) p & 0x1e) == 0))
goto error;
p++;
if (UNLIKELY((*(unsigned char*) p & 0xc0) != 0x80)) /* 10xxxxxx */
goto error;
} else {
if ((*(unsigned char*) p & 0xf0) == 0xe0) { /* 1110xxxx */
if (UNLIKELY(max_len - (p - str) < 3))
goto error;
min = (1 << 11);
val = *(unsigned char*) p & 0x0f;
goto TWO_REMAINING;
} else if ((*(unsigned char*) p & 0xf8) == 0xf0) { /* 11110xxx */
if (UNLIKELY(max_len - (p - str) < 4))
goto error;
min = (1 << 16);
val = *(unsigned char*) p & 0x07;
} else
goto error;
p++;
CONTINUATION_CHAR;
TWO_REMAINING:
p++;
CONTINUATION_CHAR;
p++;
CONTINUATION_CHAR;
if (UNLIKELY(val < min))
goto error;
if (UNLIKELY(!UNICODE_VALID(val)))
goto error;
}
continue;
error:
return last;
}
}
return p;
}
@end