/******************************************************************************\
* 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();
}
}