diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3b08124..0a0107a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,6 +183,11 @@ jobs: - name: Build AASDK for ${{ matrix.arch }} on ${{ matrix.distro }} run: | echo "Building AASDK for ${{ matrix.arch }} architecture" + RUN_TESTS=false + if [ "${{ github.event_name }}" = "pull_request" ] || [ "${{ github.ref }}" = "refs/heads/main" ] || [ "${{ github.ref }}" = "refs/heads/develop" ]; then + RUN_TESTS=true + fi + echo "RUN_TESTS=${RUN_TESTS} for this build" if [ "${{ github.ref }}" = "refs/heads/main" ]; then BUILD_TYPE="release" echo "Building release packages for main branch" @@ -196,6 +201,7 @@ jobs: --build-arg TARGET_ARCH=${{ matrix.arch }} \ --build-arg DEBIAN_VERSION=${{ matrix.distro }} \ --build-arg BUILD_TYPE=${BUILD_TYPE} \ + --build-arg RUN_TESTS=${RUN_TESTS} \ --tag aasdk-build:${{ matrix.arch }} \ --load \ . diff --git a/CMakeLists.txt b/CMakeLists.txt index a2b83f2b..e54693ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -153,7 +153,7 @@ endif() # Paths set(sources_directory ${CMAKE_CURRENT_SOURCE_DIR}/src) set(include_directory ${CMAKE_CURRENT_SOURCE_DIR}/include) -set(include_ut_directory ${CMAKE_CURRENT_SOURCE_DIR}/include_ut) +set(include_ut_directory ${CMAKE_CURRENT_SOURCE_DIR}/unit_test) # Set output directories to build directory for proper packaging set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/lib) @@ -344,12 +344,18 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/include/aasdk/Version.hpp" ) # Install SSL certificate and key files -install(FILES - "${CMAKE_CURRENT_SOURCE_DIR}/cert/headunit.crt" - "${CMAKE_CURRENT_SOURCE_DIR}/cert/headunit.key" - DESTINATION /etc/openauto - PERMISSIONS OWNER_READ GROUP_READ - COMPONENT runtime +install(FILES + "${CMAKE_CURRENT_SOURCE_DIR}/cert/headunit.crt" + DESTINATION /etc/aasdk + PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ + COMPONENT runtime +) + +install(FILES + "${CMAKE_CURRENT_SOURCE_DIR}/cert/headunit.key" + DESTINATION /etc/aasdk + PERMISSIONS OWNER_READ OWNER_WRITE + COMPONENT runtime ) # Export the targets to a script @@ -390,6 +396,8 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/aasdk.pc" COMPONENT development) if(AASDK_TEST) + enable_testing() + add_executable(aasdk_ut ${tests_source_files} ${tests_include_files}) @@ -400,6 +408,8 @@ if(AASDK_TEST) gtest_main gmock_main) + add_test(NAME aasdk_ut COMMAND aasdk_ut) + if(AASDK_CODE_COVERAGE) include(CodeCoverage) append_coverage_compiler_flags() diff --git a/Dockerfile b/Dockerfile index a26f76cb..583cc136 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,7 @@ FROM debian:${DEBIAN_VERSION}-slim # Build arguments ARG TARGET_ARCH=amd64 ARG BUILD_TYPE=release +ARG RUN_TESTS=false ARG DEBIAN_FRONTEND=noninteractive ARG GIT_COMMIT_ID=unknown ARG GIT_BRANCH=unknown @@ -102,8 +103,11 @@ RUN echo "=== Git environment variables before build ===" && \ echo " GIT_DESCRIBE=$GIT_DESCRIBE" && \ echo " GIT_COMMIT_TIMESTAMP=$GIT_COMMIT_TIMESTAMP" && \ echo " GIT_DIRTY=$GIT_DIRTY" && \ + echo " RUN_TESTS=$RUN_TESTS" && \ echo "=============================================" && \ export TARGET_ARCH=$(dpkg-architecture -qDEB_HOST_ARCH) && \ + TEST_ARG="" && \ + if [ "$RUN_TESTS" = "true" ]; then TEST_ARG="test"; fi && \ echo "Building AASDK for architecture: $TARGET_ARCH (native compilation)" && \ # Compute distro-specific release suffix for DEB packages DISTRO_DEB_RELEASE=$(bash /src/scripts/distro_release.sh) && \ @@ -112,7 +116,7 @@ RUN echo "=== Git environment variables before build ===" && \ # Pass through to CMake via build.sh using CMAKE_ARGS env DISTRO_DEB_RELEASE="$CPACK_DEB_RELEASE" CMAKE_ARGS="$CMAKE_ARGS -DCPACK_DEBIAN_PACKAGE_RELEASE=$CPACK_DEB_RELEASE -DCPACK_PROJECT_CONFIG_FILE=/src/cmake_modules/CPackProjectConfig.cmake" \ CROSS_COMPILE=${CROSS_COMPILE} \ - ./build.sh ${BUILD_TYPE} clean package --skip-protobuf --skip-absl && \ + ./build.sh ${BUILD_TYPE} clean ${TEST_ARG} package --skip-protobuf --skip-absl && \ if [ -d "packages" ]; then \ cp packages/*.deb /output/ 2>/dev/null || true && \ echo "Packages built:" && \ diff --git a/QUICK_REFERENCE.md b/QUICK_REFERENCE.md index 591d9b89..ce24661d 100644 --- a/QUICK_REFERENCE.md +++ b/QUICK_REFERENCE.md @@ -135,11 +135,38 @@ export TARGET_ARCH=arm64 # Cross-compilation target export CMAKE_BUILD_PARALLEL_LEVEL=4 # Parallel build jobs export AASDK_LOG_LEVEL=DEBUG # Logging level +# Runtime tracing (no rebuild required) +export AASDK_TRACE_CRYPTOR=1 # Enable Cryptor decrypt trace +export AASDK_TRACE_CRYPTOR_SAMPLE_EVERY=8 # Log every Nth decrypt sample (1-1000) +export AASDK_TRACE_MESSAGE=1 # Enable MessageInStream trace +export AASDK_TRACE_MESSAGE_VIDEO_ONLY=1 # Restrict message trace to video channel +export AASDK_TRACE_MESSAGE_SAMPLE_EVERY=2 # Log every Nth message sample (1-1000) + # Library paths export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH" export LD_LIBRARY_PATH="/usr/local/lib:$LD_LIBRARY_PATH" ``` +## Runtime Trace Toggles + +Use these toggles to inspect encrypted frame flow at runtime without rebuilding aasdk. + +```bash +# Example: focused projection-path diagnostics +export AASDK_TRACE_CRYPTOR=1 +export AASDK_TRACE_CRYPTOR_SAMPLE_EVERY=8 +export AASDK_TRACE_MESSAGE=1 +export AASDK_TRACE_MESSAGE_VIDEO_ONLY=1 +export AASDK_TRACE_MESSAGE_SAMPLE_EVERY=2 +``` + +Expected tags in logs: +- `[CryptorTrace] decrypt-read` +- `[CryptorTrace] decrypt-drained` +- `[MessageTrace] encrypted-pass-through` +- `[MessageTrace] decrypt` +- `[MessageTrace] resolve` + ## 📦 Package Information ### Version Format diff --git a/debian/postinst b/debian/postinst index 6048ad9b..7d94bacc 100755 --- a/debian/postinst +++ b/debian/postinst @@ -6,6 +6,46 @@ export DEBIAN_FRONTEND=noninteractive case "$1" in configure) + cert_dir="/etc/aasdk" + legacy_dir="/etc/openauto" + cert_file="$cert_dir/headunit.crt" + key_file="$cert_dir/headunit.key" + + # Ensure target cert directory exists. + mkdir -p "$cert_dir" + chmod 755 "$cert_dir" || true + + # Migrate from the old location if needed. + if [ ! -f "$cert_file" ] && [ -f "$legacy_dir/headunit.crt" ]; then + cp -f "$legacy_dir/headunit.crt" "$cert_file" + fi + + if [ ! -f "$key_file" ] && [ -f "$legacy_dir/headunit.key" ]; then + cp -f "$legacy_dir/headunit.key" "$key_file" + fi + + # Prefer a dedicated service group when present. + cert_group="root" + if getent group aasdk >/dev/null 2>&1; then + cert_group="aasdk" + fi + + if [ -f "$cert_file" ]; then + chown root:"$cert_group" "$cert_file" || true + chmod 644 "$cert_file" || true + fi + + if [ -f "$key_file" ]; then + chown root:root "$key_file" || true + chmod 600 "$key_file" || true + + # Optional compatibility mode for non-root runtimes in the aasdk group. + if [ "$cert_group" = "aasdk" ]; then + chown root:aasdk "$key_file" || true + chmod 640 "$key_file" || true + fi + fi + # Update the dynamic linker cache if [ -x /sbin/ldconfig ]; then /sbin/ldconfig || true diff --git a/src/Channel/Control/ControlServiceChannel.cpp b/src/Channel/Control/ControlServiceChannel.cpp index e4d67aa8..31103ff2 100644 --- a/src/Channel/Control/ControlServiceChannel.cpp +++ b/src/Channel/Control/ControlServiceChannel.cpp @@ -37,6 +37,8 @@ namespace aasdk { void ControlServiceChannel::sendVersionRequest(SendPromise::Pointer promise) { AASDK_LOG_CHANNEL_CONTROL(debug, "sendVersionRequest()"); + AASDK_LOG(info) << "[ControlServiceChannel] sendVersionRequest major=" << AASDK_MAJOR + << " minor=" << AASDK_MINOR; auto message(std::make_shared(channelId_, messenger::EncryptionType::PLAIN, messenger::MessageType::SPECIFIC)); @@ -53,6 +55,7 @@ namespace aasdk { void ControlServiceChannel::sendHandshake(common::Data handshakeBuffer, SendPromise::Pointer promise) { AASDK_LOG_CHANNEL_CONTROL(debug, "sendHandshake()"); + AASDK_LOG(info) << "[ControlServiceChannel] sendHandshake bytes=" << handshakeBuffer.size(); auto message(std::make_shared(channelId_, messenger::EncryptionType::PLAIN, messenger::MessageType::SPECIFIC)); message->insertPayload( @@ -193,7 +196,7 @@ namespace aasdk { messenger::MessageId messageId(message->getPayload()); common::DataConstBuffer payload(message->getPayload(), messageId.getSizeOf()); - AASDK_LOG(debug) << "[ControlServiceChannel] MessageId: " << messageId.getId(); + AASDK_LOG(info) << "[ControlServiceChannel] MessageId: " << messageId.getId(); switch (messageId.getId()) { case aap_protobuf::service::control::message::ControlMessageType::MESSAGE_VERSION_RESPONSE: diff --git a/src/Messenger/Cryptor.cpp b/src/Messenger/Cryptor.cpp index b40e86ba..5a2e0bfa 100644 --- a/src/Messenger/Cryptor.cpp +++ b/src/Messenger/Cryptor.cpp @@ -18,6 +18,9 @@ // along with aasdk. If not, see . #include +#include +#include +#include #include #include #include @@ -29,6 +32,82 @@ namespace aasdk { namespace messenger { + namespace { + + struct RuntimeTraceConfig { + bool enabled{false}; + int sampleEvery{1}; + }; + + static auto parseEnvBool(const char* value, bool defaultValue) -> bool { + if (value == nullptr) { + return defaultValue; + } + + const std::string token(value); + if (token.empty()) { + return defaultValue; + } + + if (token == "1" || token == "true" || token == "TRUE" || token == "on" || + token == "ON" || token == "yes" || token == "YES") { + return true; + } + + if (token == "0" || token == "false" || token == "FALSE" || token == "off" || + token == "OFF" || token == "no" || token == "NO") { + return false; + } + + return defaultValue; + } + + static auto parseEnvInt(const char* value, int defaultValue, int minValue, int maxValue) -> int { + if (value == nullptr) { + return defaultValue; + } + + try { + const int parsed = std::stoi(value); + return std::max(minValue, std::min(maxValue, parsed)); + } catch (...) { + return defaultValue; + } + } + + static auto readRuntimeTraceConfig() -> RuntimeTraceConfig { + RuntimeTraceConfig cfg; + cfg.enabled = parseEnvBool(std::getenv("AASDK_TRACE_CRYPTOR"), false); + cfg.sampleEvery = parseEnvInt(std::getenv("AASDK_TRACE_CRYPTOR_SAMPLE_EVERY"), 1, 1, 1000); + return cfg; + } + + static auto getRuntimeTraceConfig() -> RuntimeTraceConfig { + static RuntimeTraceConfig cached = readRuntimeTraceConfig(); + static auto lastRefresh = std::chrono::steady_clock::now(); + + const auto now = std::chrono::steady_clock::now(); + if (now - lastRefresh > std::chrono::seconds(1)) { + cached = readRuntimeTraceConfig(); + lastRefresh = now; + } + + return cached; + } + + static auto shouldEmitTraceSample() -> bool { + static std::atomic counter{0}; + const RuntimeTraceConfig cfg = getRuntimeTraceConfig(); + if (!cfg.enabled) { + return false; + } + + const uint64_t current = ++counter; + return (current % static_cast(cfg.sampleEvery)) == 0; + } + + } // namespace + // Embedded certificate constants static const std::string cCertificate = "-----BEGIN CERTIFICATE-----\n\ MIIDKjCCAhICARswDQYJKoZIhvcNAQELBQAwWzELMAkGA1UEBhMCVVMxEzARBgNV\n\ @@ -95,7 +174,8 @@ KAwp3tIHPoJOQiKNQ3/qks5km/9dujUGU2ARiU3qmxLMdgegFz8e\n\ static std::string loadCertificate() { // Try paths in order of preference std::vector certPaths = { - "/etc/openauto/headunit.crt", // Installed system path + "/etc/aasdk/headunit.crt", // Installed system path + "/etc/openauto/headunit.crt", // Legacy path (backward compatibility) "/usr/share/aasdk/cert/headunit.crt", // Alternative system path "./cert/headunit.crt", // Development path "../cert/headunit.crt" // Alternative development path @@ -117,7 +197,8 @@ KAwp3tIHPoJOQiKNQ3/qks5km/9dujUGU2ARiU3qmxLMdgegFz8e\n\ static std::string loadPrivateKey() { // Try paths in order of preference std::vector keyPaths = { - "/etc/openauto/headunit.key", // Installed system path + "/etc/aasdk/headunit.key", // Installed system path + "/etc/openauto/headunit.key", // Legacy path (backward compatibility) "/usr/share/aasdk/cert/headunit.key", // Alternative system path "./cert/headunit.key", // Development path "../cert/headunit.key" // Alternative development path @@ -261,37 +342,64 @@ KAwp3tIHPoJOQiKNQ3/qks5km/9dujUGU2ARiU3qmxLMdgegFz8e\n\ } size_t Cryptor::decrypt(common::Data &output, const common::DataConstBuffer &buffer, int frameLength) { - int overhead = 29; - int length = frameLength - overhead; std::lock_guard lock(mutex_); + const bool traceSample = shouldEmitTraceSample(); + const int pendingBeforeWrite = sslWrapper_->getAvailableBytes(ssl_); + this->write(buffer); const size_t beginOffset = output.size(); - size_t totalReadSize = 0; // Initialise - size_t availableBytes = length; - size_t readBytes = (availableBytes - totalReadSize) > 2048 ? 2048 : availableBytes - - totalReadSize; // Calculate How many Bytes to Read - output.resize(output.size() + - readBytes); // Resize Output to match the bytes we want to read - - // We try to be a bit more explicit here, using the frame length from the frame itself rather than just blindly reading from the SSL buffer. + size_t totalReadSize = 0; + while (true) { + const size_t readBytes = 2048; + output.resize(beginOffset + totalReadSize + readBytes); - while (readBytes > 0) { const auto ¤tBuffer = common::DataBuffer(output, totalReadSize + beginOffset); - auto readSize = sslWrapper_->sslRead(ssl_, currentBuffer.data, currentBuffer.size); + const auto readSize = sslWrapper_->sslRead(ssl_, currentBuffer.data, currentBuffer.size); if (readSize <= 0) { - throw error::Error(error::ErrorCode::SSL_READ, sslWrapper_->getError(ssl_, readSize)); + const auto nativeError = sslWrapper_->getError(ssl_, readSize); + const int pendingAfterRead = sslWrapper_->getAvailableBytes(ssl_); + + if (nativeError == SSL_ERROR_WANT_READ || nativeError == SSL_ERROR_WANT_WRITE) { + if (traceSample) { + AASDK_LOG(info) << "[CryptorTrace] decrypt-drained" + << " frameLength=" << frameLength + << " encryptedBytesIn=" << buffer.size + << " totalReadSize=" << totalReadSize + << " requestedReadBytes=" << readBytes + << " sslError=" << nativeError + << " pendingBeforeWrite=" << pendingBeforeWrite + << " pendingAfterRead=" << pendingAfterRead; + } + AASDK_LOG(debug) << "[Cryptor] SSL decrypt drained" + << " frameLength=" << frameLength + << " totalReadSize=" << totalReadSize + << " requestedReadBytes=" << readBytes + << " sslError=" << nativeError; + output.resize(beginOffset + totalReadSize); + return totalReadSize; + } + + const std::string info = "decrypt sslRead<=0" + " frameLength=" + std::to_string(frameLength) + + " totalReadSize=" + std::to_string(totalReadSize) + + " requestedReadBytes=" + std::to_string(readBytes) + + " returnCode=" + std::to_string(readSize); + throw error::Error(error::ErrorCode::SSL_READ, nativeError, info); } - totalReadSize += readSize; - availableBytes = sslWrapper_->getAvailableBytes(ssl_); - readBytes = (length - totalReadSize) > 2048 ? 2048 : length - totalReadSize; - output.resize(output.size() + readBytes); + totalReadSize += static_cast(readSize); + if (traceSample) { + AASDK_LOG(info) << "[CryptorTrace] decrypt-read" + << " frameLength=" << frameLength + << " encryptedBytesIn=" << buffer.size + << " readSize=" << readSize + << " totalReadSize=" << totalReadSize + << " pendingBeforeWrite=" << pendingBeforeWrite; + } } - - return totalReadSize; } common::Data Cryptor::readHandshakeBuffer() { @@ -320,7 +428,13 @@ KAwp3tIHPoJOQiKNQ3/qks5km/9dujUGU2ARiU3qmxLMdgegFz8e\n\ const auto readSize = sslWrapper_->bioRead(bIOs_.second, currentBuffer.data, currentBuffer.size); if (readSize <= 0) { - throw error::Error(error::ErrorCode::SSL_BIO_READ, sslWrapper_->getError(ssl_, readSize)); + const auto nativeError = sslWrapper_->getError(ssl_, readSize); + const std::string info = "read bioRead<=0" + " pendingSize=" + std::to_string(pendingSize) + + " totalReadSize=" + std::to_string(totalReadSize) + + " currentBufferSize=" + std::to_string(currentBuffer.size) + + " returnCode=" + std::to_string(readSize); + throw error::Error(error::ErrorCode::SSL_BIO_READ, nativeError, info); } totalReadSize += readSize; diff --git a/src/Messenger/MessageInStream.cpp b/src/Messenger/MessageInStream.cpp index b55a37c9..7949fd2e 100644 --- a/src/Messenger/MessageInStream.cpp +++ b/src/Messenger/MessageInStream.cpp @@ -17,14 +17,99 @@ // along with aasdk. If not, see . #include +#include #include #include #include +#include +#include +#include +#include +#include #include +#include namespace aasdk::messenger { + namespace { + + struct MessageTraceConfig { + bool enabled{false}; + bool videoOnly{true}; + int sampleEvery{1}; + }; + + static auto parseEnvBool(const char* value, bool defaultValue) -> bool { + if (value == nullptr) { + return defaultValue; + } + + const std::string token(value); + if (token == "1" || token == "true" || token == "TRUE" || token == "on" || + token == "ON" || token == "yes" || token == "YES") { + return true; + } + + if (token == "0" || token == "false" || token == "FALSE" || token == "off" || + token == "OFF" || token == "no" || token == "NO") { + return false; + } + + return defaultValue; + } + + static auto parseEnvInt(const char* value, int defaultValue, int minValue, int maxValue) -> int { + if (value == nullptr) { + return defaultValue; + } + + try { + const int parsed = std::stoi(value); + return std::max(minValue, std::min(maxValue, parsed)); + } catch (...) { + return defaultValue; + } + } + + static auto readMessageTraceConfig() -> MessageTraceConfig { + MessageTraceConfig cfg; + cfg.enabled = parseEnvBool(std::getenv("AASDK_TRACE_MESSAGE"), false); + cfg.videoOnly = parseEnvBool(std::getenv("AASDK_TRACE_MESSAGE_VIDEO_ONLY"), true); + cfg.sampleEvery = parseEnvInt(std::getenv("AASDK_TRACE_MESSAGE_SAMPLE_EVERY"), 1, 1, 1000); + return cfg; + } + + static auto getMessageTraceConfig() -> MessageTraceConfig { + static MessageTraceConfig cached = readMessageTraceConfig(); + static auto lastRefresh = std::chrono::steady_clock::now(); + + const auto now = std::chrono::steady_clock::now(); + if (now - lastRefresh > std::chrono::seconds(1)) { + cached = readMessageTraceConfig(); + lastRefresh = now; + } + + return cached; + } + + static auto shouldTraceMessage(ChannelId channelId) -> bool { + static std::atomic counter{0}; + const MessageTraceConfig cfg = getMessageTraceConfig(); + if (!cfg.enabled) { + return false; + } + + if (cfg.videoOnly && channelId != ChannelId::MEDIA_SINK_VIDEO) { + return false; + } + + const uint64_t current = ++counter; + return (current % static_cast(cfg.sampleEvery)) == 0; + } + + } // namespace + MessageInStream::MessageInStream(boost::asio::io_service &ioService, transport::ITransport::Pointer transport, ICryptor::Pointer cryptor) : strand_(ioService), transport_(std::move(transport)), cryptor_(std::move(cryptor)) { @@ -61,7 +146,12 @@ namespace aasdk::messenger { AASDK_LOG(debug) << "[MessageInStream] Processing Frame Header: Ch " << channelIdToString(frameHeader.getChannelId()) << " Fr " - << frameTypeToString(frameHeader.getType()); + << frameTypeToString(frameHeader.getType()) + << " Enc " << (frameHeader.getEncryptionType() == EncryptionType::ENCRYPTED ? "ENCRYPTED" : "PLAIN") + << " Msg " << (frameHeader.getMessageType() == MessageType::CONTROL ? "CONTROL" : "SPECIFIC") + << " Raw[0]=0x" << std::hex << static_cast(buffer.cdata[0]) + << " Raw[1]=0x" << static_cast(buffer.cdata[1]) + << std::dec; isValidFrame_ = true; @@ -125,20 +215,66 @@ namespace aasdk::messenger { FrameSize frameSize(buffer); frameSize_ = (int) frameSize.getFrameSize(); + AASDK_LOG(debug) << "[MessageInStream] Frame size parsed: frameSize=" << frameSize.getFrameSize() + << " totalSize=" << frameSize.getTotalSize(); transport_->receive(frameSize.getFrameSize(), std::move(transportPromise)); } void MessageInStream::receiveFramePayloadHandler(const common::DataConstBuffer &buffer) { + const ChannelId channelId = message_->getChannelId(); + const bool traceMessage = shouldTraceMessage(channelId); + const size_t payloadSizeBefore = message_->getPayload().size(); + + AASDK_LOG(debug) << "[MessageInStream] Payload handler: ch=" << channelIdToString(message_->getChannelId()) + << " enc=" << (message_->getEncryptionType() == EncryptionType::ENCRYPTED ? "ENCRYPTED" : "PLAIN") + << " msg=" << (message_->getType() == MessageType::CONTROL ? "CONTROL" : "SPECIFIC") + << " frameType=" << frameTypeToString(thisFrameType_) + << " frameSize=" << frameSize_ + << " payloadBytes=" << buffer.size + << " cryptorActive=" << (cryptor_->isActive() ? "true" : "false"); + if (message_->getEncryptionType() == EncryptionType::ENCRYPTED) { - try { - cryptor_->decrypt(message_->getPayload(), buffer, frameSize_); - } - catch (const error::Error &e) { - AASDK_LOG_MESSENGER(debug, "Rejecting message."); - message_.reset(); - promise_->reject(e); - promise_.reset(); - return; + if (!cryptor_->isActive()) { + // Some devices deliver raw TLS records on control before cryptor activation. + // Only synthesize ENCAPSULATED_SSL for TLS-looking records to avoid + // misclassifying regular control payloads (e.g. version responses). + const bool looksLikeTlsRecord = + (buffer.size >= 2) && + (buffer.cdata[0] >= 0x14 && buffer.cdata[0] <= 0x17) && + (buffer.cdata[1] == 0x03); + + if (message_->getChannelId() == ChannelId::CONTROL && looksLikeTlsRecord) { + message_->insertPayload(messenger::MessageId( + aap_protobuf::service::control::message::ControlMessageType::MESSAGE_ENCAPSULATED_SSL).getData()); + } + + message_->insertPayload(buffer); + if (traceMessage) { + AASDK_LOG(debug) << "[MessageTrace] encrypted-pass-through" + << " ch=" << channelIdToString(channelId) + << " payloadBytes=" << buffer.size + << " payloadSizeAfter=" << message_->getPayload().size(); + } + } else { + try { + const size_t decryptedBytes = cryptor_->decrypt(message_->getPayload(), buffer, frameSize_); + if (traceMessage) { + AASDK_LOG(debug) << "[MessageTrace] decrypt" + << " ch=" << channelIdToString(channelId) + << " frameSize=" << frameSize_ + << " encryptedBytes=" << buffer.size + << " decryptedBytes=" << decryptedBytes + << " payloadSizeBefore=" << payloadSizeBefore + << " payloadSizeAfter=" << message_->getPayload().size(); + } + } + catch (const error::Error &e) { + AASDK_LOG_MESSENGER(debug, "Rejecting message."); + message_.reset(); + promise_->reject(e); + promise_.reset(); + return; + } } } else { message_->insertPayload(buffer); @@ -149,6 +285,12 @@ namespace aasdk::messenger { // If this is the LAST frame or a BULK frame... if ((thisFrameType_ == FrameType::BULK || thisFrameType_ == FrameType::LAST) && isValidFrame_) { AASDK_LOG_MESSENGER(debug, "Resolving message."); + if (traceMessage) { + AASDK_LOG(debug) << "[MessageTrace] resolve" + << " ch=" << channelIdToString(channelId) + << " frameType=" << frameTypeToString(thisFrameType_) + << " totalPayloadBytes=" << message_->getPayload().size(); + } promise_->resolve(std::move(message_)); promise_.reset(); isResolved = true; diff --git a/src/Messenger/MessageInStream.ut.cpp b/src/Messenger/MessageInStream.ut.cpp index 71f1d54f..bc90c864 100644 --- a/src/Messenger/MessageInStream.ut.cpp +++ b/src/Messenger/MessageInStream.ut.cpp @@ -17,6 +17,8 @@ */ #include +#include +#include #include #include #include @@ -134,6 +136,7 @@ TEST_F(MessageInStreamUnitTest, MessageInStream_ReceiveEncryptedMessage) EXPECT_CALL(transportMock_, receive(framePayload.size(), _)).WillOnce(SaveArg<1>(&framePayloadTransportPromise)); common::Data decryptedPayload(500, 0x5F); + ON_CALL(cryptorMock_, isActive()).WillByDefault(Return(true)); EXPECT_CALL(cryptorMock_, decrypt(_, _, _)).WillOnce(DoAll(SetArgReferee<0>(decryptedPayload), Return(decryptedPayload.size()))); frameSizeTransportPromise->resolve(frameSize.getData()); @@ -181,6 +184,7 @@ TEST_F(MessageInStreamUnitTest, MessageInStream_MessageDecryptionFailed) EXPECT_CALL(transportMock_, receive(framePayload.size(), _)).WillOnce(SaveArg<1>(&framePayloadTransportPromise)); common::Data decryptedPayload(500, 0x5F); + ON_CALL(cryptorMock_, isActive()).WillByDefault(Return(true)); EXPECT_CALL(cryptorMock_, decrypt(_, _, _)).WillOnce(ThrowSSLReadException()); frameSizeTransportPromise->resolve(frameSize.getData()); @@ -350,13 +354,15 @@ TEST_F(MessageInStreamUnitTest, MessageInStream_ReceiveSplittedMessage) EXPECT_THAT(payload, testing::ContainerEq(expectedPayload)); } -TEST_F(MessageInStreamUnitTest, MessageInStream_IntertwinedChannels) +TEST_F(MessageInStreamUnitTest, MessageInStream_IntertwinedChannelsContinueReceivingWithoutResolving) { MessageInStream::Pointer messageInStream(std::make_shared(ioService_, transport_, cryptor_)); FrameHeader frame1Header(ChannelId::BLUETOOTH, FrameType::FIRST, EncryptionType::PLAIN, MessageType::SPECIFIC); transport::ITransport::ReceivePromise::Pointer frameHeaderTransportPromise; - EXPECT_CALL(transportMock_, receive(FrameHeader::getSizeOf(), _)).Times(2).WillRepeatedly(SaveArg<1>(&frameHeaderTransportPromise)); + // FrameHeader::getSizeOf() == FrameSize::getSizeOf(SHORT) == 2, so all receive(2,_) calls + // (3 header reads + 1 SHORT frame-size read) must share a single unified expectation. + EXPECT_CALL(transportMock_, receive(FrameHeader::getSizeOf(), _)).Times(4).WillRepeatedly(SaveArg<1>(&frameHeaderTransportPromise)); messageInStream->startReceive(std::move(receivePromise_)); @@ -387,12 +393,153 @@ TEST_F(MessageInStreamUnitTest, MessageInStream_IntertwinedChannels) ioService_.reset(); FrameHeader frame2Header(ChannelId::MEDIA_SINK_VIDEO, FrameType::LAST, EncryptionType::PLAIN, MessageType::SPECIFIC); + // Resolving frame2Header causes receiveFrameHeaderHandler to request the SHORT frame-size (2 bytes). + // frameHeaderTransportPromise is reused — after ioService_.run() it will point to the size promise. + frameHeaderTransportPromise->resolve(frame2Header.getData()); + + ioService_.run(); + ioService_.reset(); - EXPECT_CALL(receivePromiseHandlerMock_, onReject(error::Error(error::ErrorCode::MESSENGER_INTERTWINED_CHANNELS))); + transport::ITransport::ReceivePromise::Pointer frame2PayloadTransportPromise; + EXPECT_CALL(transportMock_, receive(frame2Payload.size(), _)).WillOnce(SaveArg<1>(&frame2PayloadTransportPromise)); + FrameSize frame2Size(frame2Payload.size()); + // frameHeaderTransportPromise now holds the SHORT frame-size promise (saved by WillRepeatedly above). + frameHeaderTransportPromise->resolve(frame2Size.getData()); + + ioService_.run(); + ioService_.reset(); + + EXPECT_CALL(receivePromiseHandlerMock_, onReject(_)).Times(0); EXPECT_CALL(receivePromiseHandlerMock_, onResolve(_)).Times(0); - frameHeaderTransportPromise->resolve(frame2Header.getData()); + frame2PayloadTransportPromise->resolve(frame2Payload); + + ioService_.run(); +} + +TEST_F(MessageInStreamUnitTest, MessageInStream_InactiveCryptorTlsControlPayloadGetsEncapsulatedSslPrefix) +{ + MessageInStream::Pointer messageInStream(std::make_shared(ioService_, transport_, cryptor_)); + + FrameHeader frameHeader(ChannelId::CONTROL, FrameType::BULK, EncryptionType::ENCRYPTED, MessageType::CONTROL); + transport::ITransport::ReceivePromise::Pointer frameHeaderTransportPromise; + EXPECT_CALL(transportMock_, receive(FrameHeader::getSizeOf(), _)).WillOnce(SaveArg<1>(&frameHeaderTransportPromise)); + + messageInStream->startReceive(std::move(receivePromise_)); + + ioService_.run(); + ioService_.reset(); + + common::Data framePayload{0x17, 0x03, 0x03, 0x00, 0x2A}; + FrameSize frameSize(framePayload.size()); + transport::ITransport::ReceivePromise::Pointer frameSizeTransportPromise; + EXPECT_CALL(transportMock_, receive(FrameSize::getSizeOf(FrameSizeType::SHORT), _)).WillOnce(SaveArg<1>(&frameSizeTransportPromise)); + frameHeaderTransportPromise->resolve(frameHeader.getData()); + + ioService_.run(); + ioService_.reset(); + + transport::ITransport::ReceivePromise::Pointer framePayloadTransportPromise; + EXPECT_CALL(transportMock_, receive(framePayload.size(), _)).WillOnce(SaveArg<1>(&framePayloadTransportPromise)); + ON_CALL(cryptorMock_, isActive()).WillByDefault(Return(false)); + EXPECT_CALL(cryptorMock_, decrypt(_, _, _)).Times(0); + frameSizeTransportPromise->resolve(frameSize.getData()); + + ioService_.run(); + ioService_.reset(); + + Message::Pointer message; + EXPECT_CALL(receivePromiseHandlerMock_, onReject(_)).Times(0); + EXPECT_CALL(receivePromiseHandlerMock_, onResolve(_)).WillOnce(SaveArg<0>(&message)); + framePayloadTransportPromise->resolve(framePayload); + + ioService_.run(); + + common::Data expectedPayload = MessageId( + aap_protobuf::service::control::message::ControlMessageType::MESSAGE_ENCAPSULATED_SSL).getData(); + expectedPayload.insert(expectedPayload.end(), framePayload.begin(), framePayload.end()); + EXPECT_THAT(message->getPayload(), testing::ContainerEq(expectedPayload)); +} + +TEST_F(MessageInStreamUnitTest, MessageInStream_InactiveCryptorNonTlsControlPayloadPassesThroughRaw) +{ + MessageInStream::Pointer messageInStream(std::make_shared(ioService_, transport_, cryptor_)); + + FrameHeader frameHeader(ChannelId::CONTROL, FrameType::BULK, EncryptionType::ENCRYPTED, MessageType::CONTROL); + transport::ITransport::ReceivePromise::Pointer frameHeaderTransportPromise; + EXPECT_CALL(transportMock_, receive(FrameHeader::getSizeOf(), _)).WillOnce(SaveArg<1>(&frameHeaderTransportPromise)); + + messageInStream->startReceive(std::move(receivePromise_)); + + ioService_.run(); + ioService_.reset(); + + common::Data framePayload{0x01, 0x02, 0x03, 0x04}; + FrameSize frameSize(framePayload.size()); + transport::ITransport::ReceivePromise::Pointer frameSizeTransportPromise; + EXPECT_CALL(transportMock_, receive(FrameSize::getSizeOf(FrameSizeType::SHORT), _)).WillOnce(SaveArg<1>(&frameSizeTransportPromise)); + frameHeaderTransportPromise->resolve(frameHeader.getData()); + + ioService_.run(); + ioService_.reset(); + + transport::ITransport::ReceivePromise::Pointer framePayloadTransportPromise; + EXPECT_CALL(transportMock_, receive(framePayload.size(), _)).WillOnce(SaveArg<1>(&framePayloadTransportPromise)); + ON_CALL(cryptorMock_, isActive()).WillByDefault(Return(false)); + EXPECT_CALL(cryptorMock_, decrypt(_, _, _)).Times(0); + frameSizeTransportPromise->resolve(frameSize.getData()); + + ioService_.run(); + ioService_.reset(); + + Message::Pointer message; + EXPECT_CALL(receivePromiseHandlerMock_, onReject(_)).Times(0); + EXPECT_CALL(receivePromiseHandlerMock_, onResolve(_)).WillOnce(SaveArg<0>(&message)); + framePayloadTransportPromise->resolve(framePayload); + + ioService_.run(); + + EXPECT_THAT(message->getPayload(), testing::ContainerEq(framePayload)); +} + +TEST_F(MessageInStreamUnitTest, MessageInStream_InactiveCryptorTlsVideoPayloadDoesNotSynthesizeControlMessageId) +{ + MessageInStream::Pointer messageInStream(std::make_shared(ioService_, transport_, cryptor_)); + + FrameHeader frameHeader(ChannelId::MEDIA_SINK_VIDEO, FrameType::BULK, EncryptionType::ENCRYPTED, MessageType::SPECIFIC); + transport::ITransport::ReceivePromise::Pointer frameHeaderTransportPromise; + EXPECT_CALL(transportMock_, receive(FrameHeader::getSizeOf(), _)).WillOnce(SaveArg<1>(&frameHeaderTransportPromise)); + + messageInStream->startReceive(std::move(receivePromise_)); + + ioService_.run(); + ioService_.reset(); + + common::Data framePayload{0x17, 0x03, 0x03, 0x00, 0x10}; + FrameSize frameSize(framePayload.size()); + transport::ITransport::ReceivePromise::Pointer frameSizeTransportPromise; + EXPECT_CALL(transportMock_, receive(FrameSize::getSizeOf(FrameSizeType::SHORT), _)).WillOnce(SaveArg<1>(&frameSizeTransportPromise)); + frameHeaderTransportPromise->resolve(frameHeader.getData()); + + ioService_.run(); + ioService_.reset(); + + transport::ITransport::ReceivePromise::Pointer framePayloadTransportPromise; + EXPECT_CALL(transportMock_, receive(framePayload.size(), _)).WillOnce(SaveArg<1>(&framePayloadTransportPromise)); + ON_CALL(cryptorMock_, isActive()).WillByDefault(Return(false)); + EXPECT_CALL(cryptorMock_, decrypt(_, _, _)).Times(0); + frameSizeTransportPromise->resolve(frameSize.getData()); ioService_.run(); + ioService_.reset(); + + Message::Pointer message; + EXPECT_CALL(receivePromiseHandlerMock_, onReject(_)).Times(0); + EXPECT_CALL(receivePromiseHandlerMock_, onResolve(_)).WillOnce(SaveArg<0>(&message)); + framePayloadTransportPromise->resolve(framePayload); + + ioService_.run(); + + EXPECT_THAT(message->getPayload(), testing::ContainerEq(framePayload)); } TEST_F(MessageInStreamUnitTest, MessageInStream_RejectWhenInProgress) diff --git a/src/Transport/SSLWrapper.cpp b/src/Transport/SSLWrapper.cpp index 7d345c5e..769588cc 100644 --- a/src/Transport/SSLWrapper.cpp +++ b/src/Transport/SSLWrapper.cpp @@ -18,6 +18,8 @@ // along with aasdk. If not, see . #include +#include +#include #if defined(__has_include) && __has_include() && !defined(OPENSSL_NO_ENGINE) #include #define HAVE_ENGINE_H @@ -32,6 +34,35 @@ namespace aasdk { namespace transport { + static auto sslErrorToString(int sslErrorCode) -> const char* { + switch (sslErrorCode) { + case SSL_ERROR_NONE: + return "SSL_ERROR_NONE"; + case SSL_ERROR_SSL: + return "SSL_ERROR_SSL"; + case SSL_ERROR_WANT_READ: + return "SSL_ERROR_WANT_READ"; + case SSL_ERROR_WANT_WRITE: + return "SSL_ERROR_WANT_WRITE"; + case SSL_ERROR_WANT_X509_LOOKUP: + return "SSL_ERROR_WANT_X509_LOOKUP"; + case SSL_ERROR_SYSCALL: + return "SSL_ERROR_SYSCALL"; + case SSL_ERROR_ZERO_RETURN: + return "SSL_ERROR_ZERO_RETURN"; +#if defined(SSL_ERROR_WANT_CONNECT) + case SSL_ERROR_WANT_CONNECT: + return "SSL_ERROR_WANT_CONNECT"; +#endif +#if defined(SSL_ERROR_WANT_ACCEPT) + case SSL_ERROR_WANT_ACCEPT: + return "SSL_ERROR_WANT_ACCEPT"; +#endif + default: + return "SSL_ERROR_UNKNOWN"; + } + } + SSLWrapper::SSLWrapper() { SSL_library_init(); SSL_load_error_strings(); // Optional: Can also be removed if not needed. @@ -187,10 +218,38 @@ namespace aasdk { } int SSLWrapper::getError(SSL *ssl, int returnCode) { - while (auto err = ERR_get_error()) { - AASDK_LOG(error) << "[SSLWrapper] SSL Error " << ERR_error_string(err, NULL); + const int sslErrorCode = SSL_get_error(ssl, returnCode); + const int savedErrno = errno; + const bool fatalError = + sslErrorCode != SSL_ERROR_NONE && + sslErrorCode != SSL_ERROR_WANT_READ && + sslErrorCode != SSL_ERROR_WANT_WRITE && + sslErrorCode != SSL_ERROR_WANT_X509_LOOKUP; + + if (fatalError) { + AASDK_LOG(error) << "[SSLWrapper] getError returnCode=" << returnCode + << " ssl_error=" << sslErrorCode + << "(" << sslErrorToString(sslErrorCode) << ")" + << " errno=" << savedErrno + << "(" << std::strerror(savedErrno) << ")" + << " state=" + << (ssl ? SSL_state_string_long(ssl) : ""); + } else { + AASDK_LOG(debug) << "[SSLWrapper] getError returnCode=" << returnCode + << " ssl_error=" << sslErrorCode + << "(" << sslErrorToString(sslErrorCode) << ")" + << " errno=" << savedErrno + << "(" << std::strerror(savedErrno) << ")" + << " state=" + << (ssl ? SSL_state_string_long(ssl) : ""); + } + + if (fatalError) { + while (auto err = ERR_get_error()) { + AASDK_LOG(error) << "[SSLWrapper] SSL Error " << ERR_error_string(err, NULL); + } } - return SSL_get_error(ssl, returnCode); + return sslErrorCode; } } diff --git a/src/Transport/USBTransport.cpp b/src/Transport/USBTransport.cpp index 8e1ceb66..bc15370f 100644 --- a/src/Transport/USBTransport.cpp +++ b/src/Transport/USBTransport.cpp @@ -17,6 +17,7 @@ // along with aasdk. If not, see . #include +#include namespace aasdk { @@ -26,11 +27,24 @@ namespace aasdk { : Transport(ioService), aoapDevice_(std::move(aoapDevice)) {} void USBTransport::enqueueReceive(common::DataBuffer buffer) { + const auto inEndpoint = aoapDevice_->getInEndpoint().getAddress(); + AASDK_LOG(debug) << "[USBTransport] enqueueReceive endpoint=0x" << std::hex + << static_cast(inEndpoint) << std::dec + << " requestedBytes=" << buffer.size; + auto usbEndpointPromise = usb::IUSBEndpoint::Promise::defer(receiveStrand_); - usbEndpointPromise->then([this, self = this->shared_from_this()](auto bytesTransferred) { + usbEndpointPromise->then([this, self = this->shared_from_this(), inEndpoint](auto bytesTransferred) { + AASDK_LOG(debug) << "[USBTransport] receiveComplete endpoint=0x" + << std::hex << static_cast(inEndpoint) + << std::dec << " bytesTransferred=" << bytesTransferred; this->receiveHandler(bytesTransferred); }, - [this, self = this->shared_from_this()](auto e) { + [this, self = this->shared_from_this(), inEndpoint](auto e) { + AASDK_LOG(warning) << "[USBTransport] receiveError endpoint=0x" + << std::hex << static_cast(inEndpoint) + << std::dec << " code=" << static_cast(e.getCode()) + << " native=" << e.getNativeCode() + << " what=" << e.what(); this->rejectReceivePromises(e); }); @@ -42,12 +56,32 @@ namespace aasdk { } void USBTransport::doSend(SendQueue::iterator queueElement, common::Data::size_type offset) { + const auto outEndpoint = aoapDevice_->getOutEndpoint().getAddress(); + const auto remainingBytes = queueElement->first.size() - offset; + AASDK_LOG(debug) << "[USBTransport] doSend endpoint=0x" << std::hex + << static_cast(outEndpoint) << std::dec + << " offset=" << offset + << " remainingBytes=" << remainingBytes + << " totalMessageBytes=" << queueElement->first.size(); + auto usbEndpointPromise = usb::IUSBEndpoint::Promise::defer(sendStrand_); usbEndpointPromise->then( - [this, self = this->shared_from_this(), queueElement, offset](size_t bytesTransferred) mutable { + [this, self = this->shared_from_this(), queueElement, offset, outEndpoint](size_t bytesTransferred) mutable { + AASDK_LOG(debug) << "[USBTransport] sendComplete endpoint=0x" << std::hex + << static_cast(outEndpoint) << std::dec + << " offset=" << offset + << " bytesTransferred=" << bytesTransferred + << " totalMessageBytes=" << queueElement->first.size(); this->sendHandler(queueElement, offset, bytesTransferred); }, - [this, self = this->shared_from_this(), queueElement](const error::Error &e) mutable { + [this, self = this->shared_from_this(), queueElement, offset, outEndpoint](const error::Error &e) mutable { + AASDK_LOG(warning) << "[USBTransport] sendError endpoint=0x" << std::hex + << static_cast(outEndpoint) << std::dec + << " offset=" << offset + << " totalMessageBytes=" << queueElement->first.size() + << " code=" << static_cast(e.getCode()) + << " native=" << e.getNativeCode() + << " what=" << e.what(); queueElement->second->reject(e); sendQueue_.erase(queueElement); diff --git a/src/USB/AOAPDevice.cpp b/src/USB/AOAPDevice.cpp index 6c9607df..e424bd41 100644 --- a/src/USB/AOAPDevice.cpp +++ b/src/USB/AOAPDevice.cpp @@ -55,6 +55,7 @@ #include #include #include +#include namespace aasdk { @@ -102,6 +103,15 @@ namespace aasdk { auto result = usbWrapper.claimInterface(handle, interfaceDescriptor->bInterfaceNumber); + // Recovery path for stale interface ownership after abrupt transport teardown. + if (result == LIBUSB_ERROR_BUSY) { + AASDK_LOG(warning) << "[AOAPDevice] claimInterface busy on iface=" + << static_cast(interfaceDescriptor->bInterfaceNumber) + << ", attempting release+retry"; + usbWrapper.releaseInterface(handle, interfaceDescriptor->bInterfaceNumber); + result = usbWrapper.claimInterface(handle, interfaceDescriptor->bInterfaceNumber); + } + if (result != 0) { throw error::Error(error::ErrorCode::USB_CLAIM_INTERFACE, result); }