/******************************************************************************\ * Copyright (c) 2024-2026 * * Author(s): * Tony Mountifield * ****************************************************************************** * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * \******************************************************************************/ #include "protocol.h" #include "server.h" #ifndef SERVER_ONLY # include "client.h" #endif #include "channel.h" #ifndef SERVER_ONLY // TCP connection used by client CTcpConnection::CTcpConnection ( QTcpSocket* pTcpSocket, const CHostAddress& tcpAddress, CClient* pClient, CChannel* pChannel, bool bIsSession ) : pTcpSocket ( pTcpSocket ), tcpAddress ( tcpAddress ), pServer ( nullptr ), pChannel ( pChannel ), bIsSession ( bIsSession ) { vecbyRecBuf.Init ( MAX_SIZE_BYTES_NETW_BUF ); iPos = 0; iPayloadRemain = 0; connect ( pTcpSocket, &QTcpSocket::disconnected, this, &CTcpConnection::OnDisconnected ); connect ( pTcpSocket, &QTcpSocket::readyRead, this, &CTcpConnection::OnReadyRead ); connect ( this, &CTcpConnection::ProtocolCLMessageReceived, pChannel, &CChannel::OnProtocolCLMessageReceived ); if ( bIsSession ) { // set up keepalive CLM_EMPTY_MESSAGE over TCP session connection connect ( this, &CTcpConnection::CLSendEmptyMes, pClient, &CClient::OnCLSendEmptyMes ); connect ( &TimerKeepalive, &QTimer::timeout, this, &CTcpConnection::OnTimerKeepalive ); TimerKeepalive.start ( TCP_KEEPALIVE_INTERVAL_MS ); } } #endif // TCP connection used by server CTcpConnection::CTcpConnection ( QTcpSocket* pTcpSocket, const CHostAddress& tcpAddress, CServer* pServer ) : pTcpSocket ( pTcpSocket ), tcpAddress ( tcpAddress ), pServer ( pServer ), pChannel ( nullptr ), bIsSession ( false ) { vecbyRecBuf.Init ( MAX_SIZE_BYTES_NETW_BUF ); iPos = 0; iPayloadRemain = 0; connect ( pTcpSocket, &QTcpSocket::disconnected, this, &CTcpConnection::OnDisconnected ); connect ( pTcpSocket, &QTcpSocket::readyRead, this, &CTcpConnection::OnReadyRead ); connect ( this, &CTcpConnection::ProtocolCLMessageReceived, pServer, &CServer::OnProtocolCLMessageReceived ); // setup an idle timer on the server side only connect ( &TimerIdleTimeout, &QTimer::timeout, this, &CTcpConnection::OnTimerIdleTimeout ); TimerIdleTimeout.setSingleShot ( true ); TimerIdleTimeout.start ( TCP_IDLE_TIMEOUT_MS ); } void CTcpConnection::OnDisconnected() { qInfo() << "- Jamulus-TCP: disconnected from:" << tcpAddress.toString(); TimerKeepalive.stop(); TimerIdleTimeout.stop(); pTcpSocket->deleteLater(); if ( pChannel && pChannel->GetTcpConnection() == this ) { pChannel->SetTcpConnection ( nullptr ); // unlink from channel } deleteLater(); // delete this object in the next event loop } void CTcpConnection::OnReadyRead() { long iBytesAvail = pTcpSocket->bytesAvailable(); qDebug() << "- readyRead(), bytesAvailable() =" << iBytesAvail; while ( iBytesAvail > 0 ) { if ( iPos < MESS_HEADER_LENGTH_BYTE ) { // reading message header long iNumBytesRead = pTcpSocket->read ( (char*) &vecbyRecBuf[iPos], MESS_HEADER_LENGTH_BYTE - iPos ); if ( iNumBytesRead == -1 ) { return; } qDebug() << "-- (hdr) iNumBytesRead =" << iNumBytesRead; iPos += iNumBytesRead; iBytesAvail -= iNumBytesRead; if ( iPos >= MESS_HEADER_LENGTH_BYTE ) { // now have a complete header iPayloadRemain = CProtocol::GetBodyLength ( vecbyRecBuf ); // check for valid packet length on the wire if ( iPayloadRemain < 0 || iPayloadRemain > MAX_SIZE_BYTES_NETW_BUF - MESS_HEADER_LENGTH_BYTE ) { qWarning() << "- Jamulus-TCP: invalid frame length" << iPayloadRemain << "- dropping connection"; disconnectFromHost(); return; } iPayloadRemain -= iPos - MESS_HEADER_LENGTH_BYTE; } } else { // reading message body long iNumBytesRead = pTcpSocket->read ( (char*) &vecbyRecBuf[iPos], iPayloadRemain ); if ( iNumBytesRead == -1 ) { return; } qDebug() << "-- (body) iNumBytesRead =" << iNumBytesRead; iPos += iNumBytesRead; iPayloadRemain -= iNumBytesRead; iBytesAvail -= iNumBytesRead; Q_ASSERT ( iPayloadRemain >= 0 ); if ( iPayloadRemain == 0 ) { // have a complete payload qDebug() << "- Jamulus-TCP: received protocol message of length" << iPos; // check if this is a protocol message int iRecCounter; int iRecID; CVector vecbyMesBodyData; if ( !CProtocol::ParseMessageFrame ( vecbyRecBuf, iPos, vecbyMesBodyData, iRecCounter, iRecID ) ) { qDebug() << "- Jamulus-TCP: message parsed OK, ID =" << iRecID; // this is a protocol message, check the type of the message if ( CProtocol::IsConnectionLessMessageID ( iRecID ) ) { //### TODO: BEGIN ###// // a copy of the vector is used -> avoid malloc in real-time routine emit ProtocolCLMessageReceived ( iRecID, vecbyMesBodyData, tcpAddress, this ); //### TODO: END ###// // disconnect if it's not a client session connection if ( !pServer && !bIsSession ) { pTcpSocket->disconnectFromHost(); } } else { //### TODO: BEGIN ###// // a copy of the vector is used -> avoid malloc in real-time routine // emit ProtocolMessageReceived ( iRecCounter, iRecID, vecbyMesBodyData, pTcpConnection->tcpAddress, pTcpConnection ); //### TODO: END ###// } } else { qWarning() << "- Jamulus-TCP: failed to parse frame"; } iPos = 0; // ready for next message, if any } } } qDebug() << "- end of readyRead(), bytesAvailable() =" << pTcpSocket->bytesAvailable(); if ( pServer ) { // restart server idle timer allowing for keepalive interval TimerIdleTimeout.start ( TCP_KEEPALIVE_INTERVAL_MS + TCP_IDLE_TIMEOUT_MS ); } } void CTcpConnection::OnTimerKeepalive() { // qDebug() << "- Keepalive timer" << this << "to TCP" << tcpAddress.toString(); emit CLSendEmptyMes ( tcpAddress, this ); } void CTcpConnection::OnTimerIdleTimeout() { // qDebug() << "- ConnTimeout timer" << this << "from TCP" << tcpAddress.toString(); qWarning() << "- Jamulus-TCP: idle timeout - disconnecting"; disconnectFromHost(); } qint64 CTcpConnection::write ( const char* data, qint64 maxSize ) { if ( !pTcpSocket ) { return -1; } return pTcpSocket->write ( data, maxSize ); } void CTcpConnection::disconnectFromHost() { if ( pTcpSocket ) { pTcpSocket->disconnectFromHost(); } }