// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. // For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md #include "service-fota/FotaMessages.hpp" #include "service-fota/FotaServiceAPI.hpp" #include "service-fota/ServiceFota.hpp" #include "FotaUrcHandler.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for exception #include // for find_if, remove, transform #include // for tolower #include // for _Bind_helper<>::type, _Placeholder, bind, _1, _2 #include // for accumulate #include // for operator<<, basic_ostream, ostringstream, basic_ostream::operator<<, char_traits, basic_istream, istringstream, basic_ostream<>::__ostream_type #include // for string, basic_string, stoi, getline, operator<<, operator==, operator+, operator!= #include // for unordered_map<>::iterator, _Node_iterator, operator==, _Map_base<>::mapped_type, _Node_iterator_base, unordered_map<>::mapped_type #include // for move, pair #include // for vector namespace FotaService { const TickType_t defaultTimer = 1000; const uint32_t QIDEACTTimeout = 40000; const uint32_t QIACTTimeout = 150000; const char *httpErrorCode200 = "200"; const uint32_t httpErrorCodeValue200 = 200; Service::Service() : sys::Service(service::name::fota) { LOG_INFO("[ServiceFota] Initializing"); bus.channels.push_back(sys::BusChannel::ServiceFotaNotifications); bus.channels.push_back(sys::BusChannel::ServiceCellularNotifications); connectionTimer = sys::TimerFactory::createPeriodicTimer( this, "Fota", std::chrono::milliseconds{defaultTimer}, [this](sys::Timer &) { std::shared_ptr msg = std::make_shared(MessageType::CellularListCurrentCalls); bus.sendUnicast(msg, service::name::fota); }); registerMessageHandlers(); } Service::~Service() { LOG_INFO("[ServiceFota] Cleaning resources"); } sys::ReturnCodes Service::InitHandler() { return sys::ReturnCodes::Success; } sys::ReturnCodes Service::DeinitHandler() { return sys::ReturnCodes::Success; } void Service::ProcessCloseReason(sys::CloseReason closeReason) { sendCloseReadyMessage(this); } void Service::registerMessageHandlers() { LOG_DEBUG("Registring Handlers for Fota::Service:"); using std::placeholders::_1; connect(CellularGetChannelResponseMessage(), std::bind(&Service::handleCellularGetChannelResponseMessage, this, _1)); connect(cellular::StateChange(), std::bind(&Service::handleServiceCellularNotifications, this, _1)); connect(ConfigureAPNMessage(), std::bind(&Service::handleConfigureAPN, this, _1)); connect(ConnectMessage(), std::bind(&Service::handleConnect, this, _1)); connect(HTTPRequestMessage(), std::bind(&Service::handleHttpGet, this, _1)); connect(FOTAStart(), std::bind(&Service::handleFotaStart, this, _1)); connect(FOTARawProgress(), std::bind(&Service::handleRawProgress, this, _1)); } sys::MessagePointer Service::handleCellularGetChannelResponseMessage(sys::Message *req) { LOG_DEBUG("Handling channel response"); auto responseMessage = static_cast(req); LOG_DEBUG("channel ptr: %p", responseMessage->dataChannelPtr); dataChannel = responseMessage->dataChannelPtr; dataChannel->setCallback(std::bind(&Service::handleChannelNotifications, this, std::placeholders::_1)); getApnConfiguration(); setState(); stopActiveContext(); return sys::MessageNone{}; } sys::MessagePointer Service::handleServiceCellularNotifications(sys::Message *req) { if (auto msg = dynamic_cast(req)) { LOG_DEBUG("cellular::StageChange: %s", cellular::State::c_str(msg->request)); if (msg->request == cellular::State::ST::Ready) { LOG_DEBUG("Modem is \"Ready\""); if (dataChannel == nullptr) { LOG_DEBUG("Requesting channel"); CellularServiceAPI::GetDataChannel(this); } else { LOG_DEBUG("Channel already present: %p", dataChannel); } } } return std::make_shared(); } sys::MessagePointer Service::handleConfigureAPN(sys::Message *req) { if (dataChannel != nullptr) { if (auto msg = dynamic_cast(req)) { LOG_DEBUG("Configure APN: %s", msg->apnConfig.toString().c_str()); auto config = std::find_if(contextMap.begin(), contextMap.end(), [&msg](const APN::ContextPair &pair) -> bool { return pair.second.contextId == msg->apnConfig.contextId; }); currentApnContext = msg->apnConfig.contextId; at::Result atResult; if (config == contextMap.end()) { LOG_DEBUG("Configuring new APN: %s", msg->apnConfig.toString().c_str()); atResult = sendAndLogError(prepareQICSGPcmd(msg->apnConfig)); } else { if (config->second.contextId == msg->apnConfig.contextId) { if (config->second.activated && (config->second.apn != msg->apnConfig.apn)) { atResult = sendAndLogError(prepareQIDEACT(config->second.contextId), QIDEACTTimeout); } atResult = sendAndLogError(prepareQICSGPcmd(msg->apnConfig)); } else { atResult.code = at::Result::Code::OK; } } if (atResult) { bus.sendMulticast(std::make_shared(NotificationMessage::Type::Configured), sys::BusChannel::ServiceFotaNotifications); LOG_DEBUG("Internet Cofiguration OK"); } return std::make_shared(true); } } return std::make_shared(); } sys::MessagePointer Service::handleConnect(sys::Message * /*req*/) { if (dataChannel) { LOG_DEBUG("ConnectMessage"); getApnConfiguration(); if (!contextMap[currentApnContext].activated) { if (!sendAndLogError(prepareQIACT(currentApnContext), QIACTTimeout)) { return std::make_shared(false); } } LOG_DEBUG("InternetConnect OK"); bus.sendMulticast(std::make_shared( static_cast(NotificationMessage::Type::Connected)), sys::BusChannel::ServiceFotaNotifications); return std::make_shared(true); } return std::make_shared(false); } sys::MessagePointer Service::handleHttpGet(sys::Message *req) { std::shared_ptr responseMsg; if (auto msg = dynamic_cast(req)) { normalizeUrl(msg->url); url = msg->url; receiverServiceName = msg->sender; LOG_DEBUG("HTTP Get : '%s' (%d)", msg->url.c_str(), static_cast(msg->url.size())); LOG_DEBUG("HTTP Method: %s", (msg->method == HTTPMethod::GET ? "GET" : "POST")); // setup http context std::vector httpGetCommands; httpGetCommands.push_back("AT+QHTTPCFG=\"contextid\",1"); httpGetCommands.push_back("AT+QHTTPCFG=\"responseheader\",1"); /// left for debuging - DNS configuration // httpGetCommands.push_back("AT+QIDNSCFG=?\r"); // httpGetCommands.push_back("AT+QIDNSCFG=1\r"); for (auto &currCmd : httpGetCommands) { if (!sendAndLogError(currCmd)) { return std::make_shared(false); } } if (isHTTPS(msg->url)) { setupSSLContext(); } if (!openURL(msg->url)) { return std::make_shared(false); } switch (msg->method) { case HTTPMethod::GET: { LOG_DEBUG("GET"); if (!sendAndLogError(prepareQHTTPGET())) { return std::make_shared(false); } break; } case HTTPMethod::POST: LOG_DEBUG("POST - not supported yet"); break; } return std::make_shared(true); } return std::make_shared(false); } void Service::handleChannelNotifications(std::string &data) { const std::string QHTTPGET("+QHTTPGET:"); std::string response = data; LOG_DEBUG("URC: %s", response.c_str()); if (auto begin = response.find(QHTTPGET) != std::string::npos) { auto end = response.find("\r", begin); unsigned int dataPosition; dataPosition = begin + QHTTPGET.size(); std::istringstream msg( response.substr(dataPosition, (end != std::string::npos ? end : response.size()) - dataPosition)); std::string tag; std::vector tags; while (std::getline(msg, tag, ',')) { tags.push_back(tag); } if (tags[1] == httpErrorCode200) { LOG_DEBUG("HTTP Response 200 - reading data"); // potentialy critical logs, will be removed with FOTA Window commits // int timeout = 10; // dataChannel->cmd("AT+QHTTPREAD=" + std::to_string(timeout) + "\r" /*, timeout * 1000*/); state = State::QHTTPREAD; file.clear(); dataChannel->cmd(prepareQHTTPREAD()); } } if (state == State::QHTTPREAD) { LOG_DEBUG("QHTTPREAD"); if (response.find("+QHTTPREAD: 0") != std::string::npos) { LOG_DEBUG("QHTTPREAD:finished"); state = State::Connected; parseResponse(); } else { LOG_DEBUG("QHTTPREAD: append: |%s|", response.c_str()); file += response; } } parseQIND(response); } sys::MessagePointer Service::handleFotaStart(sys::Message *req) { LOG_DEBUG("handle Fota Start"); if (auto msg = dynamic_cast(req)) { LOG_DEBUG("Starting fota update: %s", msg->url.c_str()); receiverServiceName = msg->sender; state = State::FOTAUpdate; auto result = dataChannel->cmd(prepareQFOTADLcmd(msg->url)); if (!result) { LOG_WARN("Starting fota error: %s", std::accumulate(result.response.begin(), result.response.end(), std::string("\n")).c_str()); } } return std::make_shared(true); } sys::MessagePointer Service::handleRawProgress(sys::Message *req) { LOG_DEBUG("Handle Fota RawProgress message"); if (auto msg = dynamic_cast(req)) { parseQIND(msg->qindRaw); } return std::make_shared(true); } void Service::getApnConfiguration() { getActiveCotext(); getConfig(); for (auto &[contextId, apn] : contextMap) { LOG_DEBUG("%d: %s", contextId, apn.toString().c_str()); } } void Service::getConfig() { if (dataChannel) { for (auto &[contextId, apn] : contextMap) { if (auto data = sendAndLogError(prepareQICSGPquery(apn))) { const std::string QICSGP_prefix("+QICSGP:"); for (auto &line : data.response) { if (line.find(QICSGP_prefix) != std::string::npos) { std::istringstream raw_data(line.substr(QICSGP_prefix.size())); std::vector data; std::string subitem; while (std::getline(raw_data, subitem, ',')) { LOG_DEBUG(" %s: '%s'", QICSGP_prefix.c_str(), subitem.c_str()); data.push_back(subitem); } try { APN::ContextType configuredType = static_cast(std::stoi(data[0])); if (apn.type != configuredType) { LOG_INFO("Different content type active vs configured-> %d vs %d", static_cast(apn.type), static_cast(configuredType)); } apn.apn = data[1].substr(1, data[1].size() - 2); apn.username = data[2].substr(1, data[2].size() - 2); apn.password = data[3].substr(1, data[3].size() - 2); apn.authMethod = static_cast(std::stoi(data[4])); } catch (...) { LOG_DEBUG("Throw during cofniguration parsing."); } } } } } } } void Service::getActiveCotext() { if (dataChannel) { auto availableContext = sendAndLogError("AT+QIACT?", 150000); if (availableContext) { parseQIACT(availableContext); } } } void Service::stopActiveContext() { for (auto &context : contextMap) { if (context.second.activated) { sendAndLogError(prepareQIDEACT(context.second.contextId)); } } } void Service::setState() { for (auto &[contextId, apn] : contextMap) { if (apn.activated) { state = State::Connected; } } } void Service::parseQIACT(const at::Result &availableContext) { const std::string QIACT_prefix("+QIACT:"); for (auto &line : availableContext.response) { LOG_DEBUG("context: %s", line.c_str()); if (line.find(QIACT_prefix) != std::string::npos) { APN::Config apnConfig; std::vector data; std::istringstream raw_data(line.substr(QIACT_prefix.size())); std::string subItem; while (std::getline(raw_data, subItem, ',')) { LOG_DEBUG("QIACT-: %s", subItem.c_str()); data.push_back(subItem); } try { apnConfig.contextId = static_cast(std::stoi(data[0])); } catch (...) { LOG_DEBUG("Wront context id: %s - setting default: 1", data[0].c_str()); apnConfig.contextId = 1; } apnConfig.activated = (data[1] == "1" ? true : false); int contextTypeRaw = 0; try { contextTypeRaw = std::stoi(data[2]); } catch (const std::exception &e) { LOG_ERROR("Conversion error of %s, taking default value %d", data[2].c_str(), contextTypeRaw); } apnConfig.type = static_cast(contextTypeRaw); apnConfig.ip = data[3].substr(1, data[3].size() - 1); LOG_DEBUG("Warking APN: %s", apnConfig.toString().c_str()); contextMap[apnConfig.contextId] = apnConfig; } } } bool Service::isHTTPS(const std::string &url) const { return url.find("https://") != std::string::npos; } void Service::normalizeUrl(std::string &url) const { std::transform(url.begin(), url.end(), url.begin(), [](unsigned char chr) { return std::tolower(chr); }); } std::string Service::prepareQIACT(unsigned char contextId) { std::ostringstream cmd; cmd << "AT+QIACT=" << static_cast(contextId); return cmd.str(); } std::string Service::prepareQICSGPcmd(const APN::Config &apn) { std::ostringstream cmd; cmd << "AT+QICSGP=" << static_cast(apn.contextId) << "," << static_cast(apn.type) << ",\"" << apn.apn << "\",\"" << apn.username << "\",\"" << apn.password << "\"," << static_cast(apn.authMethod); return cmd.str(); } std::string Service::prepareQICSGPquery(const APN::Config &apn) { std::ostringstream cmd; cmd << "AT+QICSGP=" << static_cast(apn.contextId); return cmd.str(); } std::string Service::prepareQIDEACT(unsigned char contextId) { std::ostringstream cmd; cmd << "AT+QIDEACT=" << static_cast(contextId); return cmd.str(); } std::string Service::prepareQFOTADLcmd(const std::string &url) { std::string fotaCmd("AT+QFOTADL=\""); fotaCmd += url; fotaCmd += "\""; return fotaCmd; } std::string Service::prepareQHTTPGET(unsigned int timeout) { std::ostringstream cmd; cmd << "AT+QHTTPGET=" << timeout; return cmd.str(); } std::string Service::prepareQHTTPREAD(unsigned int timeout) { std::ostringstream cmd; cmd << "AT+QHTTPREAD=" << timeout; return cmd.str(); } std::string Service::prepareQHTTPURL(const std::string &url) { std::ostringstream cmd; cmd << "AT+QHTTPURL=" << url.size(); return cmd.str(); } void Service::setupSSLContext() { std::vector sslConfigCommands; sslConfigCommands.push_back("AT+QHTTPCFG=\"sslctxid\",1"); sslConfigCommands.push_back("AT+QSSLCFG=\"sslversion\",1,5"); sslConfigCommands.push_back("AT+QSSLCFG=\"ciphersuite\",1,0xFFFF"); sslConfigCommands.push_back("AT+QSSLCFG=\"seclevel\",1,0"); for (auto &currCmd : sslConfigCommands) { sendAndLogError(currCmd); } } bool Service::openURL(const std::string &url) { auto response = dataChannel->cmd(prepareQHTTPURL(url)); if (response.response[0] == "CONNECT") { response = dataChannel->cmd(url.c_str()); logIfError(response, url); if (!response) { return false; } } else { return false; } return true; } at::Result Service::sendAndLogError(const std::string &msg) const { at::Result result = dataChannel->cmd(msg); logIfError(result, msg); return result; } at::Result Service::sendAndLogError(const std::string &msg, uint32_t timeout) const { at::Result result = dataChannel->cmd(msg, std::chrono::milliseconds(timeout)); logIfError(result, msg); return result; } void Service::logIfError(const at::Result &result, const std::string &cmdString) const { if (!result) { auto results = dataChannel->cmd(at::AT::QIGETERROR); LOG_WARN("error cmd:%s", cmdString.c_str()); LOG_WARN("Error str:%s", utils::removeNewLines( std::accumulate(results.response.begin(), results.response.end(), std::string("\n"))) .c_str()); } } void Service::parseResponse() { LOG_DEBUG("Got full fille: \n%s", file.c_str()); std::istringstream input(file); enum class HTTPResponsePart { URC, Headers, Body } part; part = HTTPResponsePart::URC; std::vector headers; std::string body; std::string line; while (std::getline(input, line, '\n')) { line.erase(std::remove(line.begin(), line.end(), '\r'), line.end()); LOG_DEBUG("line -> '%s'", line.c_str()); switch (part) { case HTTPResponsePart::URC: if (line.find("CONNECT") != std::string::npos) { part = HTTPResponsePart::Headers; } continue; case HTTPResponsePart::Headers: if (line.empty()) { part = HTTPResponsePart::Body; LOG_DEBUG("=== body part! ==="); } else { headers.push_back(line); } break; case HTTPResponsePart::Body: body += line + '\n'; break; } } std::shared_ptr msg = std::make_shared(); msg->url = url; msg->httpError = HTTPErrors::OK; msg->httpServerResponseError = httpErrorCodeValue200; msg->responseHeaders = std::move(headers); msg->body = std::move(body); bus.sendUnicast(msg, receiverServiceName); } void Service::parseQIND(const std::string &message) { auto urc = at::urc::UrcFactory::Create(message); auto urcHandler = FotaUrcHandler(*this); urc->Handle(urcHandler); } void Service::sendProgress(unsigned int progress, const std::string &receiver) { auto progressMsg = std::make_shared(); progressMsg->progress = progress; bus.sendUnicast(progressMsg, receiver); } void Service::sendFotaFinshed(const std::string &receiver) { auto msg = std::make_shared(); bus.sendUnicast(std::move(msg), receiver); } sys::MessagePointer Service::DataReceivedHandler(sys::DataMessage *msgl, sys::ResponseMessage * /*resp*/) { std::shared_ptr responseMsg; LOG_DEBUG("%s: DataRecieve: bus:%d | message:%d", service::name::fota, static_cast(msgl->channel), static_cast(msgl->messageType)); return (responseMsg ? responseMsg : std::make_shared()); } } // namespace FotaService