/* * file-transfer-chat-message-modifier.cpp * Copyright (C) 2010-2018 Belledonne Communications SARL * * 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 2 * 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. */ #include "linphone/api/c-content.h" #include "address/address.h" #include "bctoolbox/crypto.h" #include "c-wrapper/c-wrapper.h" #include "chat/chat-message/chat-message-p.h" #include "chat/chat-room/chat-room-p.h" #include "content/content-type.h" #include "content/content.h" #include "core/core.h" #include "logger/logger.h" #include "file-transfer-chat-message-modifier.h" // ============================================================================= using namespace std; LINPHONE_BEGIN_NAMESPACE FileTransferChatMessageModifier::FileTransferChatMessageModifier (belle_http_provider_t *prov) : provider(prov) { bgTask.setName("File transfer upload"); } belle_http_request_t *FileTransferChatMessageModifier::getHttpRequest () const { return httpRequest; } void FileTransferChatMessageModifier::setHttpRequest (belle_http_request_t *request) { httpRequest = request; } FileTransferChatMessageModifier::~FileTransferChatMessageModifier () { if (isFileTransferInProgressAndValid()) cancelFileTransfer(); //to avoid body handler to still refference zombie FileTransferChatMessageModifier else releaseHttpRequest(); } ChatMessageModifier::Result FileTransferChatMessageModifier::encode (const shared_ptr &message, int &errorCode) { chatMessage = message; currentFileContentToTransfer = nullptr; // For each FileContent, upload it and create a FileTransferContent for (Content *content : message->getContents()) { if (content->isFile()) { lInfo() << "Found file content, set it for file upload"; FileContent *fileContent = (FileContent *)content; currentFileContentToTransfer = fileContent; break; } } if (!currentFileContentToTransfer) return ChatMessageModifier::Result::Skipped; /* Open a transaction with the server and send an empty request(RCS5.1 section 3.5.4.8.3.1) */ if (uploadFile() == 0) return ChatMessageModifier::Result::Suspended; return ChatMessageModifier::Result::Error; } // ---------------------------------------------------------- static void _chat_message_file_transfer_on_progress ( belle_sip_body_handler_t *bh, belle_sip_message_t *m, void *data, size_t offset, size_t total ) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->fileTransferOnProgress(bh, m, offset, total); } void FileTransferChatMessageModifier::fileTransferOnProgress ( belle_sip_body_handler_t *bh, belle_sip_message_t *m, size_t offset, size_t total ) { if (!isFileTransferInProgressAndValid()) { releaseHttpRequest(); return; } shared_ptr message = chatMessage.lock(); if (!message) return; LinphoneChatMessage *msg = L_GET_C_BACK_PTR(message); LinphoneChatMessageCbs *cbs = linphone_chat_message_get_callbacks(msg); LinphoneContent *content = L_GET_C_BACK_PTR((Content *)currentFileContentToTransfer); if (linphone_chat_message_cbs_get_file_transfer_progress_indication(cbs)) { linphone_chat_message_cbs_get_file_transfer_progress_indication(cbs)(msg, content, offset, total); } else { // Legacy: call back given by application level. linphone_core_notify_file_transfer_progress_indication(message->getCore()->getCCore(), msg, content, offset, total); } } static int _chat_message_on_send_body ( belle_sip_user_body_handler_t *bh, belle_sip_message_t *m, void *data, size_t offset, uint8_t *buffer, size_t *size ) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; return d->onSendBody(bh, m, offset, buffer, size); } int FileTransferChatMessageModifier::onSendBody ( belle_sip_user_body_handler_t *bh, belle_sip_message_t *m, size_t offset, uint8_t *buffer, size_t *size ) { int retval = -1; shared_ptr message = chatMessage.lock(); if (!message) return BELLE_SIP_STOP; LinphoneChatMessage *msg = L_GET_C_BACK_PTR(message); if (!isFileTransferInProgressAndValid()) { if (httpRequest) { releaseHttpRequest(); } return BELLE_SIP_STOP; } // if we've not reach the end of file yet, ask for more data // in case of file body handler, won't be called if (currentFileContentToTransfer->getFilePath().empty() && offset < currentFileContentToTransfer->getFileSize()) { // get data from call back LinphoneChatMessageCbs *cbs = linphone_chat_message_get_callbacks(msg); LinphoneChatMessageCbsFileTransferSendCb file_transfer_send_cb = linphone_chat_message_cbs_get_file_transfer_send(cbs); LinphoneContent *content = L_GET_C_BACK_PTR((Content *)currentFileContentToTransfer); if (file_transfer_send_cb) { LinphoneBuffer *lb = file_transfer_send_cb(msg, content, offset, *size); if (lb) { *size = linphone_buffer_get_size(lb); memcpy(buffer, linphone_buffer_get_content(lb), *size); linphone_buffer_unref(lb); } else { *size = 0; } } else { // Legacy linphone_core_notify_file_transfer_send(message->getCore()->getCCore(), msg, content, (char *)buffer, size); } } LinphoneImEncryptionEngine *imee = linphone_core_get_im_encryption_engine(message->getCore()->getCCore()); if (imee) { LinphoneImEncryptionEngineCbs *imee_cbs = linphone_im_encryption_engine_get_callbacks(imee); LinphoneImEncryptionEngineCbsUploadingFileCb cb_process_uploading_file = linphone_im_encryption_engine_cbs_get_process_uploading_file(imee_cbs); if (cb_process_uploading_file) { size_t max_size = *size; uint8_t *encrypted_buffer = (uint8_t *)ms_malloc0(max_size); retval = cb_process_uploading_file(imee, msg, offset, (const uint8_t *)buffer, size, encrypted_buffer); if (retval == 0) { if (*size > max_size) { lError() << "IM encryption engine process upload file callback returned a size bigger than the size of the buffer, so it will be truncated !"; *size = max_size; } memcpy(buffer, encrypted_buffer, *size); } ms_free(encrypted_buffer); } } return retval <= 0 ? BELLE_SIP_CONTINUE : BELLE_SIP_STOP; } static void _chat_message_on_send_end (belle_sip_user_body_handler_t *bh, void *data) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->onSendEnd(bh); } void FileTransferChatMessageModifier::onSendEnd (belle_sip_user_body_handler_t *bh) { shared_ptr message = chatMessage.lock(); if (!message) return; LinphoneImEncryptionEngine *imee = linphone_core_get_im_encryption_engine(message->getCore()->getCCore()); if (imee) { LinphoneImEncryptionEngineCbs *imee_cbs = linphone_im_encryption_engine_get_callbacks(imee); LinphoneImEncryptionEngineCbsUploadingFileCb cb_process_uploading_file = linphone_im_encryption_engine_cbs_get_process_uploading_file(imee_cbs); if (cb_process_uploading_file) { cb_process_uploading_file(imee, L_GET_C_BACK_PTR(message), 0, nullptr, nullptr, nullptr); } } } static void _chat_message_process_response_from_post_file (void *data, const belle_http_response_event_t *event) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->processResponseFromPostFile(event); } void FileTransferChatMessageModifier::processResponseFromPostFile (const belle_http_response_event_t *event) { if (httpRequest && !isFileTransferInProgressAndValid()) { releaseHttpRequest(); return; } shared_ptr message = chatMessage.lock(); if (!message) return; // check the answer code if (event->response) { int code = belle_http_response_get_status_code(event->response); if (code == 204) { // this is the reply to the first post to the server - an empty msg // start uploading the file belle_sip_multipart_body_handler_t *bh; string first_part_header; belle_sip_body_handler_t *first_part_bh; bool is_file_encryption_enabled = false; LinphoneImEncryptionEngine *imee = linphone_core_get_im_encryption_engine(message->getCore()->getCCore()); if (imee && message->getChatRoom()) { LinphoneImEncryptionEngineCbs *imee_cbs = linphone_im_encryption_engine_get_callbacks(imee); LinphoneImEncryptionEngineCbsIsEncryptionEnabledForFileTransferCb is_encryption_enabled_for_file_transfer_cb = linphone_im_encryption_engine_cbs_get_is_encryption_enabled_for_file_transfer(imee_cbs); if (is_encryption_enabled_for_file_transfer_cb) { is_file_encryption_enabled = is_encryption_enabled_for_file_transfer_cb(imee, L_GET_C_BACK_PTR(message->getChatRoom())); } } FileTransferContent *fileTransferContent = new FileTransferContent(); fileTransferContent->setContentType(ContentType::FileTransfer); fileTransferContent->setFileSize(currentFileContentToTransfer->getFileSize()); // Copy file size information message->getPrivate()->addContent(fileTransferContent); // shall we encrypt the file if (is_file_encryption_enabled && message->getChatRoom()) { // temporary storage for the Content-disposition header value : use a generic filename to not leak it // Actual filename stored in msg->file_transfer_information->name will be set in encrypted msg // sended to the first_part_header = "form-data; name=\"File\"; filename=\"filename.txt\""; LinphoneImEncryptionEngineCbs *imee_cbs = linphone_im_encryption_engine_get_callbacks(imee); LinphoneImEncryptionEngineCbsGenerateFileTransferKeyCb generate_file_transfer_key_cb = linphone_im_encryption_engine_cbs_get_generate_file_transfer_key(imee_cbs); if (generate_file_transfer_key_cb) { generate_file_transfer_key_cb(imee, L_GET_C_BACK_PTR(message->getChatRoom()), L_GET_C_BACK_PTR(message)); } } else { // temporary storage for the Content-disposition header value first_part_header = "form-data; name=\"File\"; filename=\"" + currentFileContentToTransfer->getFileName() + "\""; } // create a user body handler to take care of the file and add the content disposition and content-type headers first_part_bh = (belle_sip_body_handler_t *)belle_sip_user_body_handler_new(currentFileContentToTransfer->getFileSize(), _chat_message_file_transfer_on_progress, nullptr, nullptr, _chat_message_on_send_body, _chat_message_on_send_end, this); if (!currentFileContentToTransfer->getFilePath().empty()) { belle_sip_user_body_handler_t *body_handler = (belle_sip_user_body_handler_t *)first_part_bh; // No need to add again the callback for progression, otherwise it will be called twice first_part_bh = (belle_sip_body_handler_t *)belle_sip_file_body_handler_new(currentFileContentToTransfer->getFilePath().c_str(), nullptr, this); belle_sip_file_body_handler_set_user_body_handler((belle_sip_file_body_handler_t *)first_part_bh, body_handler); // Ensure the file size has been set to the correct value fileTransferContent->setFileSize(belle_sip_file_body_handler_get_file_size((belle_sip_file_body_handler_t *)first_part_bh)); } else if (!currentFileContentToTransfer->isEmpty()) { first_part_bh = (belle_sip_body_handler_t *)belle_sip_memory_body_handler_new_from_buffer( ms_strdup(currentFileContentToTransfer->getBodyAsString().c_str()), currentFileContentToTransfer->getSize(), _chat_message_file_transfer_on_progress, this); } belle_sip_body_handler_add_header(first_part_bh, belle_sip_header_create("Content-disposition", first_part_header.c_str())); belle_sip_body_handler_add_header(first_part_bh, (belle_sip_header_t *)belle_sip_header_content_type_create( currentFileContentToTransfer->getContentType().getType().c_str(), currentFileContentToTransfer->getContentType().getSubType().c_str())); // insert it in a multipart body handler which will manage the boundaries of multipart msg bh = belle_sip_multipart_body_handler_new(_chat_message_file_transfer_on_progress, this, first_part_bh, nullptr); releaseHttpRequest(); fileUploadBeginBackgroundTask(); uploadFile(); belle_sip_message_set_body_handler(BELLE_SIP_MESSAGE(httpRequest), BELLE_SIP_BODY_HANDLER(bh)); } else if (code == 200) { // file has been uploaded correctly, get server reply and send it FileTransferContent *fileTransferContent = nullptr; for (Content *c : message->getPrivate()->getContents()) { if (c->isFileTransfer()) { FileTransferContent *tmpContent = static_cast(c); if (!tmpContent->getFileContent() && tmpContent->getSize() == 0) { // If FileTransferContent doesn't have a FileContent yet and is empty // It's the one we seek, otherwise it may be a previous uploaded FileTransferContent fileTransferContent = tmpContent; break; } } } const char *body = belle_sip_message_get_body((belle_sip_message_t *)event->response); if (body && strlen(body) > 0) { // if we have an encryption key for the file, we must insert it into the msg and restore the correct filename const unsigned char *contentKey = reinterpret_cast(fileTransferContent->getFileKey().data()); size_t contentKeySize = fileTransferContent->getFileKeySize(); if (contentKeySize > 0) { // parse the msg body xmlDocPtr xmlMessageBody = xmlParseDoc((const xmlChar *)body); xmlNodePtr cur = xmlDocGetRootElement(xmlMessageBody); if (cur != nullptr) { cur = cur->xmlChildrenNode; while (cur != nullptr) { // we found a file info node, check it has a type="file" attribute if (!xmlStrcmp(cur->name, (const xmlChar *)"file-info")) { xmlChar *typeAttribute = xmlGetProp(cur, (const xmlChar *)"type"); // this is the node we are looking for : add a file-key children node if (!xmlStrcmp(typeAttribute, (const xmlChar *)"file")) { // need to parse the children node to update the file-name one xmlNodePtr fileInfoNodeChildren = cur->xmlChildrenNode; // convert key to base64 size_t b64Size; bctbx_base64_encode(nullptr, &b64Size, contentKey, contentKeySize); unsigned char *keyb64 = (unsigned char *)ms_malloc0(b64Size + 1); int xmlStringLength; bctbx_base64_encode(keyb64, &b64Size, contentKey, contentKeySize); keyb64[b64Size] = '\0'; // libxml need a null terminated string // add the node containing the key to the file-info node xmlNewTextChild(cur, nullptr, (const xmlChar *)"file-key", (const xmlChar *)keyb64); xmlFree(typeAttribute); ms_free(keyb64); // look for the file-name node and update its content while (fileInfoNodeChildren != nullptr) { // we found a the file-name node, update its content with the real filename if (!xmlStrcmp(fileInfoNodeChildren->name, (const xmlChar *)"file-name")) { // update node content xmlNodeSetContent(fileInfoNodeChildren, (const xmlChar *)(currentFileContentToTransfer->getFileName().c_str())); break; } fileInfoNodeChildren = fileInfoNodeChildren->next; } // dump the xml into msg->message char *buffer; xmlDocDumpFormatMemoryEnc(xmlMessageBody, (xmlChar **)&buffer, &xmlStringLength, "UTF-8", 0); fileTransferContent->setBody(buffer); break; } xmlFree(typeAttribute); } cur = cur->next; } } xmlFreeDoc(xmlMessageBody); } else { // no encryption key, transfer in plain, just copy the msg sent by server fileTransferContent->setBody(body); } FileContent *fileContent = currentFileContentToTransfer; fileTransferContent->setFileContent(fileContent); message->getPrivate()->removeContent(fileContent); message->getPrivate()->setState(ChatMessage::State::FileTransferDone); releaseHttpRequest(); message->getPrivate()->send(); fileUploadEndBackgroundTask(); } else { lWarning() << "Received empty response from server, file transfer failed"; FileTransferContent *fileTransferContent = nullptr; for (Content *c : message->getPrivate()->getContents()) { if (c->isFileTransfer()) { fileTransferContent = static_cast(c); message->getPrivate()->removeContent(fileTransferContent); delete fileTransferContent; break; } } message->getPrivate()->setState(ChatMessage::State::NotDelivered); releaseHttpRequest(); fileUploadEndBackgroundTask(); } } else if (code == 400) { lWarning() << "Received HTTP code response " << code << " for file transfer, probably meaning file is too large"; FileTransferContent *fileTransferContent = nullptr; for (Content *c : message->getPrivate()->getContents()) { if (c->isFileTransfer()) { fileTransferContent = static_cast(c); message->getPrivate()->removeContent(fileTransferContent); delete fileTransferContent; break; } } message->getPrivate()->setState(ChatMessage::State::FileTransferError); releaseHttpRequest(); fileUploadEndBackgroundTask(); } else { lWarning() << "Unhandled HTTP code response " << code << " for file transfer"; FileTransferContent *fileTransferContent = nullptr; for (Content *c : message->getPrivate()->getContents()) { if (c->isFileTransfer()) { fileTransferContent = static_cast(c); message->getPrivate()->removeContent(fileTransferContent); delete fileTransferContent; break; } } message->getPrivate()->setState(ChatMessage::State::NotDelivered); releaseHttpRequest(); fileUploadEndBackgroundTask(); } } } static void _chat_message_process_io_error_upload (void *data, const belle_sip_io_error_event_t *event) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->processIoErrorUpload(event); } void FileTransferChatMessageModifier::processIoErrorUpload (const belle_sip_io_error_event_t *event) { lError() << "I/O Error during file upload of msg [" << this << "]"; shared_ptr message = chatMessage.lock(); if (!message) return; message->getPrivate()->setState(ChatMessage::State::NotDelivered); releaseHttpRequest(); } static void _chat_message_process_auth_requested_upload (void *data, belle_sip_auth_event *event) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->processAuthRequestedUpload(event); } void FileTransferChatMessageModifier::processAuthRequestedUpload (const belle_sip_auth_event *event) { lError() << "Error during file upload: auth requested for msg [" << this << "]"; shared_ptr message = chatMessage.lock(); if (!message) return; message->getPrivate()->setState(ChatMessage::State::NotDelivered); releaseHttpRequest(); } int FileTransferChatMessageModifier::uploadFile () { if (httpRequest) { lError() << "Unable to upload file: there is already an upload in progress."; return -1; } shared_ptr message = chatMessage.lock(); if (!message) return -1; // THIS IS ONLY FOR BACKWARD C API COMPAT if (currentFileContentToTransfer->getFilePath().empty() && !message->getPrivate()->getFileTransferFilepath().empty()) { currentFileContentToTransfer->setFilePath(message->getPrivate()->getFileTransferFilepath()); } belle_http_request_listener_callbacks_t cbs = { 0 }; cbs.process_response = _chat_message_process_response_from_post_file; cbs.process_io_error = _chat_message_process_io_error_upload; cbs.process_auth_requested = _chat_message_process_auth_requested_upload; const char *url = linphone_core_get_file_transfer_server(message->getCore()->getCCore()); return startHttpTransfer(url ? url : "", "POST", &cbs); } int FileTransferChatMessageModifier::startHttpTransfer (const string &url, const string &action, belle_http_request_listener_callbacks_t *cbs) { belle_generic_uri_t *uri = nullptr; shared_ptr message = chatMessage.lock(); if (!message) return -1; if (url.empty()) { lWarning() << "Cannot process file transfer msg [" << this << "]: no file remote URI configured."; goto error; } uri = belle_generic_uri_parse(url.c_str()); if (!uri || !belle_generic_uri_get_host(uri)) { lWarning() << "Cannot process file transfer msg [" << this << "]: incorrect file remote URI configured '" << url << "'."; goto error; } httpRequest = belle_http_request_create( action.c_str(), uri, belle_sip_header_create("User-Agent", linphone_core_get_user_agent(message->getCore()->getCCore())), nullptr ); if (!httpRequest) { lWarning() << "Could not create http request for uri " << url; goto error; } // keep a reference to the http request to be able to cancel it during upload belle_sip_object_ref(httpRequest); // give msg to listener to be able to start the actual file upload when server answer a 204 No content httpListener = belle_http_request_listener_create_from_callbacks(cbs, this); belle_http_provider_send_request(provider, httpRequest, httpListener); return 0; error: if (uri) { belle_sip_object_unref(uri); } return -1; } void FileTransferChatMessageModifier::fileUploadBeginBackgroundTask () { shared_ptr message = chatMessage.lock(); if (!message) return; bgTask.start(message->getCore()); } void FileTransferChatMessageModifier::fileUploadEndBackgroundTask () { bgTask.stop(); } // ---------------------------------------------------------- static void fillFileTransferContentInformationsFromVndGsmaRcsFtHttpXml (FileTransferContent *fileTransferContent) { xmlChar *fileUrl = nullptr; xmlDocPtr xmlMessageBody; xmlNodePtr cur; /* parse the msg body to get all informations from it */ xmlMessageBody = xmlParseDoc((const xmlChar *)fileTransferContent->getBodyAsString().c_str()); cur = xmlDocGetRootElement(xmlMessageBody); if (cur) { cur = cur->xmlChildrenNode; while (cur) { if (!xmlStrcmp(cur->name, (const xmlChar *)"file-info")) { /* we found a file info node, check if it has a type="file" attribute */ xmlChar *typeAttribute = xmlGetProp(cur, (const xmlChar *)"type"); if (!xmlStrcmp(typeAttribute, (const xmlChar *)"file")) { /* this is the node we are looking for */ cur = cur->xmlChildrenNode; /* now loop on the content of the file-info node */ while (cur) { if (!xmlStrcmp(cur->name, (const xmlChar *)"file-size")) { xmlChar *fileSizeString = xmlNodeListGetString(xmlMessageBody, cur->xmlChildrenNode, 1); size_t size = (size_t)strtol((const char *)fileSizeString, nullptr, 10); fileTransferContent->setFileSize(size); xmlFree(fileSizeString); } if (!xmlStrcmp(cur->name, (const xmlChar *)"file-name")) { xmlChar *filename = xmlNodeListGetString(xmlMessageBody, cur->xmlChildrenNode, 1); fileTransferContent->setFileName((char *)filename); xmlFree(filename); } if (!xmlStrcmp(cur->name, (const xmlChar *)"data")) { fileUrl = xmlGetProp(cur, (const xmlChar *)"url"); } if (!xmlStrcmp(cur->name, (const xmlChar *)"file-key")) { // there is a key in the msg: file has been encrypted // convert the key from base 64 xmlChar *keyb64 = xmlNodeListGetString(xmlMessageBody, cur->xmlChildrenNode, 1); size_t keyLength; bctbx_base64_decode(NULL, &keyLength, (unsigned char *)keyb64, strlen((const char *)keyb64)); uint8_t *keyBuffer = (uint8_t *)malloc(keyLength); // decode the key into local key buffer bctbx_base64_decode(keyBuffer, &keyLength, (unsigned char *)keyb64, strlen((const char *)keyb64)); fileTransferContent->setFileKey((const char *)keyBuffer, keyLength); // duplicate key value into the linphone content private structure xmlFree(keyb64); free(keyBuffer); } cur = cur->next; } xmlFree(typeAttribute); break; } xmlFree(typeAttribute); } cur = cur->next; } } xmlFreeDoc(xmlMessageBody); fileTransferContent->setFileUrl(fileUrl ? (const char *)fileUrl : ""); xmlFree(fileUrl); } ChatMessageModifier::Result FileTransferChatMessageModifier::decode (const shared_ptr &message, int &errorCode) { chatMessage = message; Content internalContent = message->getInternalContent(); if (internalContent.getContentType() == ContentType::FileTransfer) { FileTransferContent *fileTransferContent = new FileTransferContent(); fileTransferContent->setContentType(internalContent.getContentType()); fileTransferContent->setBody(internalContent.getBody()); fillFileTransferContentInformationsFromVndGsmaRcsFtHttpXml(fileTransferContent); message->addContent(fileTransferContent); return ChatMessageModifier::Result::Done; } for (Content *content : message->getContents()) { if (content->isFileTransfer()) { FileTransferContent *fileTransferContent = static_cast(content); fillFileTransferContentInformationsFromVndGsmaRcsFtHttpXml(fileTransferContent); } } return ChatMessageModifier::Result::Done; } // ---------------------------------------------------------- static void createFileTransferInformationsFromVndGsmaRcsFtHttpXml (FileTransferContent *fileTransferContent) { xmlChar *fileUrl = nullptr; xmlDocPtr xmlMessageBody; xmlNodePtr cur; /* parse the msg body to get all informations from it */ xmlMessageBody = xmlParseDoc((const xmlChar *)fileTransferContent->getBodyAsString().c_str()); FileContent *fileContent = new FileContent(); cur = xmlDocGetRootElement(xmlMessageBody); if (cur) { cur = cur->xmlChildrenNode; while (cur) { if (!xmlStrcmp(cur->name, (const xmlChar *)"file-info")) { /* we found a file info node, check if it has a type="file" attribute */ xmlChar *typeAttribute = xmlGetProp(cur, (const xmlChar *)"type"); if (!xmlStrcmp(typeAttribute, (const xmlChar *)"file")) { /* this is the node we are looking for */ cur = cur->xmlChildrenNode; /* now loop on the content of the file-info node */ while (cur) { if (!xmlStrcmp(cur->name, (const xmlChar *)"file-size")) { xmlChar *fileSizeString = xmlNodeListGetString(xmlMessageBody, cur->xmlChildrenNode, 1); size_t size = (size_t)strtol((const char *)fileSizeString, nullptr, 10); fileContent->setFileSize(size); xmlFree(fileSizeString); } if (!xmlStrcmp(cur->name, (const xmlChar *)"file-name")) { xmlChar *filename = xmlNodeListGetString(xmlMessageBody, cur->xmlChildrenNode, 1); fileContent->setFileName((char *)filename); xmlFree(filename); } if (!xmlStrcmp(cur->name, (const xmlChar *)"content-type")) { xmlChar *content_type = xmlNodeListGetString(xmlMessageBody, cur->xmlChildrenNode, 1); int contentTypeIndex = 0; char *type; char *subtype; while (content_type[contentTypeIndex] != '/' && content_type[contentTypeIndex] != '\0') { contentTypeIndex++; } type = ms_strndup((char *)content_type, contentTypeIndex); subtype = ms_strdup(((char *)content_type + contentTypeIndex + 1)); ContentType contentType(type, subtype); fileContent->setContentType(contentType); ms_free(subtype); ms_free(type); ms_free(content_type); } if (!xmlStrcmp(cur->name, (const xmlChar *)"data")) { fileUrl = xmlGetProp(cur, (const xmlChar *)"url"); } if (!xmlStrcmp(cur->name, (const xmlChar *)"file-key")) { // there is a key in the msg: file has been encrypted // convert the key from base 64 xmlChar *keyb64 = xmlNodeListGetString(xmlMessageBody, cur->xmlChildrenNode, 1); size_t keyLength; bctbx_base64_decode(NULL, &keyLength, (unsigned char *)keyb64, strlen((const char *)keyb64)); uint8_t *keyBuffer = (uint8_t *)malloc(keyLength); // decode the key into local key buffer bctbx_base64_decode(keyBuffer, &keyLength, (unsigned char *)keyb64, strlen((const char *)keyb64)); fileTransferContent->setFileKey((const char *)keyBuffer, keyLength); // duplicate key value into the linphone content private structure xmlFree(keyb64); free(keyBuffer); } cur = cur->next; } xmlFree(typeAttribute); break; } xmlFree(typeAttribute); } cur = cur->next; } } xmlFreeDoc(xmlMessageBody); fileContent->setFilePath(fileTransferContent->getFilePath()); // Copy file path from file transfer content to file content for file body handler // Link the FileContent to the FileTransferContent fileTransferContent->setFileContent(fileContent); xmlFree(fileUrl); } static void _chat_message_on_recv_body (belle_sip_user_body_handler_t *bh, belle_sip_message_t *m, void *data, size_t offset, uint8_t *buffer, size_t size) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->onRecvBody(bh, m, offset, buffer, size); } void FileTransferChatMessageModifier::onRecvBody (belle_sip_user_body_handler_t *bh, belle_sip_message_t *m, size_t offset, uint8_t *buffer, size_t size) { if (!httpRequest || belle_http_request_is_cancelled(httpRequest)) { lWarning() << "Cancelled request for msg [" << this << "], ignoring " << __FUNCTION__; return; } // first call may be with a zero size, ignore it if (size == 0) { return; } shared_ptr message = chatMessage.lock(); if (!message) return; int retval = -1; LinphoneImEncryptionEngine *imee = linphone_core_get_im_encryption_engine(message->getCore()->getCCore()); if (imee) { LinphoneImEncryptionEngineCbs *imee_cbs = linphone_im_encryption_engine_get_callbacks(imee); LinphoneImEncryptionEngineCbsDownloadingFileCb cb_process_downloading_file = linphone_im_encryption_engine_cbs_get_process_downloading_file(imee_cbs); if (cb_process_downloading_file) { uint8_t *decrypted_buffer = (uint8_t *)ms_malloc0(size); retval = cb_process_downloading_file(imee, L_GET_C_BACK_PTR(message), offset, (const uint8_t *)buffer, size, decrypted_buffer); if (retval == 0) { memcpy(buffer, decrypted_buffer, size); } ms_free(decrypted_buffer); } } if (retval <= 0) { if (currentFileContentToTransfer->getFilePath().empty()) { LinphoneChatMessage *msg = L_GET_C_BACK_PTR(message); LinphoneChatMessageCbs *cbs = linphone_chat_message_get_callbacks(msg); LinphoneContent *content = L_GET_C_BACK_PTR((Content *)currentFileContentToTransfer); if (linphone_chat_message_cbs_get_file_transfer_recv(cbs)) { LinphoneBuffer *lb = linphone_buffer_new_from_data(buffer, size); linphone_chat_message_cbs_get_file_transfer_recv(cbs)(msg, content, lb); linphone_buffer_unref(lb); } else { // Legacy: call back given by application level linphone_core_notify_file_transfer_recv(message->getCore()->getCCore(), msg, content, (const char *)buffer, size); } } } else { lWarning() << "File transfer decrypt failed with code " << (int)retval; message->getPrivate()->setState(ChatMessage::State::FileTransferError); } } static void _chat_message_on_recv_end (belle_sip_user_body_handler_t *bh, void *data) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->onRecvEnd(bh); } void FileTransferChatMessageModifier::onRecvEnd (belle_sip_user_body_handler_t *bh) { shared_ptr message = chatMessage.lock(); if (!message) return; int retval = -1; LinphoneImEncryptionEngine *imee = linphone_core_get_im_encryption_engine(message->getCore()->getCCore()); if (imee) { LinphoneImEncryptionEngineCbs *imee_cbs = linphone_im_encryption_engine_get_callbacks(imee); LinphoneImEncryptionEngineCbsDownloadingFileCb cb_process_downloading_file = linphone_im_encryption_engine_cbs_get_process_downloading_file(imee_cbs); if (cb_process_downloading_file) { retval = cb_process_downloading_file(imee, L_GET_C_BACK_PTR(message), 0, nullptr, 0, nullptr); } } if (retval <= 0) { if (currentFileContentToTransfer->getFilePath().empty()) { LinphoneChatMessage *msg = L_GET_C_BACK_PTR(message); LinphoneChatMessageCbs *cbs = linphone_chat_message_get_callbacks(msg); LinphoneContent *content = L_GET_C_BACK_PTR((Content *)currentFileContentToTransfer); if (linphone_chat_message_cbs_get_file_transfer_recv(cbs)) { LinphoneBuffer *lb = linphone_buffer_new(); linphone_chat_message_cbs_get_file_transfer_recv(cbs)(msg, content, lb); linphone_buffer_unref(lb); } else { // Legacy: call back given by application level linphone_core_notify_file_transfer_recv(message->getCore()->getCCore(), msg, content, nullptr, 0); } } } if (retval <= 0 && message->getState() != ChatMessage::State::FileTransferError) { // Remove the FileTransferContent from the message and store the FileContent FileContent *fileContent = currentFileContentToTransfer; message->getPrivate()->addContent(fileContent); for (Content *content : message->getContents()) { if (content->isFileTransfer()) { FileTransferContent *fileTransferContent = static_cast(content); if (fileTransferContent->getFileContent() == fileContent) { message->getPrivate()->removeContent(content); delete fileTransferContent; break; } } } message->getPrivate()->setState(ChatMessage::State::FileTransferDone); } } static void _chat_process_response_headers_from_get_file (void *data, const belle_http_response_event_t *event) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->processResponseHeadersFromGetFile(event); } static FileContent* createFileTransferInformationFromHeaders (const belle_sip_message_t *m) { FileContent *fileContent = new FileContent(); belle_sip_header_content_length_t *content_length_hdr = BELLE_SIP_HEADER_CONTENT_LENGTH(belle_sip_message_get_header(m, "Content-Length")); belle_sip_header_content_type_t *content_type_hdr = BELLE_SIP_HEADER_CONTENT_TYPE(belle_sip_message_get_header(m, "Content-Type")); if (content_type_hdr) { const char *type = belle_sip_header_content_type_get_type(content_type_hdr); const char *subtype = belle_sip_header_content_type_get_subtype(content_type_hdr); lInfo() << "Extracted content type " << type << " / " << subtype << " from header"; ContentType contentType(type, subtype); fileContent->setContentType(contentType); } if (content_length_hdr) { fileContent->setFileSize(belle_sip_header_content_length_get_content_length(content_length_hdr)); lInfo() << "Extracted content length " << fileContent->getFileSize() << " from header"; } return fileContent; } void FileTransferChatMessageModifier::processResponseHeadersFromGetFile (const belle_http_response_event_t *event) { if (event->response) { // we are receiving a response, set a specific body handler to acquire the response. // if not done, belle-sip will create a memory body handler, the default belle_sip_message_t *response = BELLE_SIP_MESSAGE(event->response); shared_ptr message = chatMessage.lock(); if (!message) return; if (currentFileContentToTransfer) { belle_sip_header_content_length_t *content_length_hdr = BELLE_SIP_HEADER_CONTENT_LENGTH(belle_sip_message_get_header(response, "Content-Length")); currentFileContentToTransfer->setFileSize(belle_sip_header_content_length_get_content_length(content_length_hdr)); lInfo() << "Extracted content length " << currentFileContentToTransfer->getFileSize() << " from header"; } else { lWarning() << "No file transfer information for msg [" << this << "]: creating..."; FileContent *content = createFileTransferInformationFromHeaders(response); message->addContent(content); } size_t body_size = 0; if (currentFileContentToTransfer) body_size = currentFileContentToTransfer->getFileSize(); belle_sip_body_handler_t *body_handler = (belle_sip_body_handler_t *)belle_sip_user_body_handler_new( body_size, _chat_message_file_transfer_on_progress, nullptr, _chat_message_on_recv_body, nullptr, _chat_message_on_recv_end, this ); if (!currentFileContentToTransfer->getFilePath().empty()) { belle_sip_user_body_handler_t *bh = (belle_sip_user_body_handler_t *)body_handler; body_handler = (belle_sip_body_handler_t *)belle_sip_file_body_handler_new(currentFileContentToTransfer->getFilePath().c_str(), _chat_message_file_transfer_on_progress, this); if (belle_sip_body_handler_get_size((belle_sip_body_handler_t *)body_handler) == 0) { // If the size of the body has not been initialized from the file stat, use the one from the // file_transfer_information. belle_sip_body_handler_set_size((belle_sip_body_handler_t *)body_handler, body_size); } belle_sip_file_body_handler_set_user_body_handler((belle_sip_file_body_handler_t *)body_handler, bh); } belle_sip_message_set_body_handler((belle_sip_message_t *)event->response, body_handler); } } static void _chat_message_process_auth_requested_download (void *data, belle_sip_auth_event *event) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->processAuthRequestedDownload(event); } void FileTransferChatMessageModifier::processAuthRequestedDownload (const belle_sip_auth_event *event) { lError() << "Error during file download : auth requested for msg [" << this << "]"; shared_ptr message = chatMessage.lock(); if (!message) return; message->getPrivate()->setState(ChatMessage::State::FileTransferError); releaseHttpRequest(); } static void _chat_message_process_io_error_download (void *data, const belle_sip_io_error_event_t *event) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->processIoErrorDownload(event); } void FileTransferChatMessageModifier::processIoErrorDownload (const belle_sip_io_error_event_t *event) { lError() << "I/O Error during file download msg [" << this << "]"; shared_ptr message = chatMessage.lock(); if (!message) return; message->getPrivate()->setState(ChatMessage::State::FileTransferError); releaseHttpRequest(); } static void _chat_message_process_response_from_get_file (void *data, const belle_http_response_event_t *event) { FileTransferChatMessageModifier *d = (FileTransferChatMessageModifier *)data; d->processResponseFromGetFile(event); } void FileTransferChatMessageModifier::processResponseFromGetFile (const belle_http_response_event_t *event) { // check the answer code if (event->response) { shared_ptr message = chatMessage.lock(); if (!message) return; int code = belle_http_response_get_status_code(event->response); if (code >= 400 && code < 500) { lWarning() << "File transfer failed with code " << code; message->getPrivate()->setState(ChatMessage::State::FileTransferError); } else if (code != 200) { lWarning() << "Unhandled HTTP code response " << code << " for file transfer"; } releaseHttpRequest(); } } bool FileTransferChatMessageModifier::downloadFile ( const shared_ptr &message, FileTransferContent *fileTransferContent ) { chatMessage = message; if (httpRequest) { lError() << "There is already a download in progress."; return false; } if (fileTransferContent->getContentType() != ContentType::FileTransfer) { lError() << "Content type is not a FileTransfer."; return false; } createFileTransferInformationsFromVndGsmaRcsFtHttpXml(fileTransferContent); FileContent *fileContent = fileTransferContent->getFileContent(); currentFileContentToTransfer = fileContent; if (!currentFileContentToTransfer) return false; // THIS IS ONLY FOR BACKWARD C API COMPAT if (currentFileContentToTransfer->getFilePath().empty() && !message->getPrivate()->getFileTransferFilepath().empty()) { currentFileContentToTransfer->setFilePath(message->getPrivate()->getFileTransferFilepath()); } belle_http_request_listener_callbacks_t cbs = { 0 }; cbs.process_response_headers = _chat_process_response_headers_from_get_file; cbs.process_response = _chat_message_process_response_from_get_file; cbs.process_io_error = _chat_message_process_io_error_download; cbs.process_auth_requested = _chat_message_process_auth_requested_download; int err = startHttpTransfer(fileTransferContent->getFileUrl(), "GET", &cbs); // File URL has been set by createFileTransferInformationsFromVndGsmaRcsFtHttpXml if (err == -1) return false; // start the download, status is In Progress message->getPrivate()->setState(ChatMessage::State::InProgress); return true; } // ---------------------------------------------------------- void FileTransferChatMessageModifier::cancelFileTransfer () { if (!httpRequest) { lInfo() << "No existing file transfer - nothing to cancel"; return; } if (!belle_http_request_is_cancelled(httpRequest)) { shared_ptr message = chatMessage.lock(); if (message) { lInfo() << "Canceling file transfer " << ( currentFileContentToTransfer->getFilePath().empty() ? L_C_TO_STRING(linphone_core_get_file_transfer_server(message->getCore()->getCCore())) : currentFileContentToTransfer->getFilePath().c_str() ); } else { lInfo() << "Warning: http request still running for ORPHAN msg: this is a memory leak"; } belle_http_provider_cancel_request(provider, httpRequest); } releaseHttpRequest(); } bool FileTransferChatMessageModifier::isFileTransferInProgressAndValid () { return httpRequest && !belle_http_request_is_cancelled(httpRequest); } void FileTransferChatMessageModifier::releaseHttpRequest () { if (httpRequest) { belle_sip_object_unref(httpRequest); httpRequest = nullptr; if (httpListener) { belle_sip_object_unref(httpListener); httpListener = nullptr; } } } string FileTransferChatMessageModifier::createFakeFileTransferFromUrl (const string &url) { string fileName = url.substr(url.find_last_of("/") + 1); stringstream fakeXml; fakeXml << "\r\n"; fakeXml << "\r\n"; fakeXml << "\r\n"; fakeXml << "" << fileName << "\r\n"; fakeXml << "application/binary\r\n"; fakeXml << "\r\n"; fakeXml << "\r\n"; fakeXml << ""; return fakeXml.str(); } LINPHONE_END_NAMESPACE