2026-05-11 23:06:39 +03:00
/*
* InfiniPaint
* Copyright ( C ) 2025 - 2026 Yousef Khadadeh
*
* 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 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 General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < https : //www.gnu.org/licenses/>.
*/
2025-07-06 20:43:53 +03:00
# include "NetLibrary.hpp"
# include <nlohmann/json.hpp>
# include <rtc/peerconnection.hpp>
# include "../Logger.hpp"
# include <rtc/rtc.hpp>
# include <variant>
# include <vector>
# include <Helpers/Random.hpp>
2026-02-24 21:13:34 +03:00
# include "Helpers/StringHelpers.hpp"
2025-07-06 20:43:53 +03:00
# include "NetClient.hpp"
# include "NetServer.hpp"
# include <bit>
2026-02-24 21:13:34 +03:00
# include <SDL3/SDL_iostream.h>
2025-07-06 20:43:53 +03:00
std : : atomic < bool > NetLibrary : : alreadyInitialized = false ;
std : : string NetLibrary : : signalingAddr ;
std : : shared_ptr < rtc : : WebSocket > NetLibrary : : ws ;
std : : unordered_map < std : : string , std : : shared_ptr < NetLibrary : : PeerData > > NetLibrary : : peers ;
std : : string NetLibrary : : globalID ;
rtc : : Configuration NetLibrary : : config ;
std : : mutex NetLibrary : : serverListMutex ;
std : : mutex NetLibrary : : clientListMutex ;
std : : mutex NetLibrary : : peerListMutex ;
std : : vector < std : : weak_ptr < NetClient > > NetLibrary : : clients ;
std : : vector < std : : weak_ptr < NetServer > > NetLibrary : : servers ;
std : : promise < void > NetLibrary : : wsPromise ;
2025-10-27 20:19:47 +03:00
NetLibrary : : LoadP2PSettingsInPathResult NetLibrary : : load_p2p_settings_in_path ( const std : : filesystem : : path & p2pConfigPath ) {
config . iceServers . clear ( ) ;
signalingAddr . clear ( ) ;
2025-07-06 20:43:53 +03:00
2026-02-24 21:13:34 +03:00
std : : string fileData ;
2025-07-06 20:43:53 +03:00
2026-02-24 21:13:34 +03:00
try {
fileData = read_file_to_string ( p2pConfigPath ) ;
}
catch ( . . . ) {
return LoadP2PSettingsInPathResult : : FAILED_TO_OPEN ;
}
2025-07-06 20:43:53 +03:00
2026-02-24 21:13:34 +03:00
try {
nlohmann : : json j = nlohmann : : json : : parse ( fileData ) ;
2025-07-06 20:43:53 +03:00
2026-02-24 21:13:34 +03:00
j . at ( " signalingServer " ) . get_to < std : : string > ( signalingAddr ) ;
std : : vector < std : : string > stunList = j . at ( " stunList " ) . get < std : : vector < std : : string > > ( ) ;
std : : vector < nlohmann : : json > turnList = j . at ( " turnList " ) ;
for ( std : : string & s : stunList ) {
if ( s . substr ( 0 , 5 ) . compare ( " stun: " ) )
s . insert ( 0 , " stun: " ) ;
config . iceServers . emplace_back ( s ) ;
2025-07-06 20:43:53 +03:00
}
2026-02-24 21:13:34 +03:00
for ( const nlohmann : : json & turnServer : turnList )
config . iceServers . emplace_back ( turnServer [ " url " ] . get < std : : string > ( ) , turnServer [ " port " ] . get < uint16_t > ( ) , turnServer [ " username " ] . get < std : : string > ( ) , turnServer [ " credential " ] . get < std : : string > ( ) ) ;
}
catch ( . . . ) {
return LoadP2PSettingsInPathResult : : FAILED_TO_READ ;
2025-07-06 20:43:53 +03:00
}
2025-10-27 20:19:47 +03:00
return LoadP2PSettingsInPathResult : : SUCCESS ;
}
2026-05-14 11:53:36 +03:00
void NetLibrary : : init_config ( const std : : filesystem : : path & p2pConfigPath ) {
2025-10-27 20:19:47 +03:00
rtc : : InitLogger ( rtc : : LogLevel : : Info , [ ] ( rtc : : LogLevel level , std : : string message ) {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " [LibDataChannel Log Level " + std : : to_string ( static_cast < int > ( level ) ) + " ] " + message ) ;
2025-10-27 20:19:47 +03:00
} ) ;
switch ( load_p2p_settings_in_path ( p2pConfigPath ) ) {
case LoadP2PSettingsInPathResult : : SUCCESS :
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Successfully loaded local P2P configuration " ) ;
2025-10-27 20:19:47 +03:00
break ;
case LoadP2PSettingsInPathResult : : FAILED_TO_READ :
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : DESKTOP_USERINFO , " Invalid custom P2P config, using defaults " ) ;
2025-10-27 20:19:47 +03:00
case LoadP2PSettingsInPathResult : : FAILED_TO_OPEN :
switch ( load_p2p_settings_in_path ( " data/config/default_p2p.json " ) ) {
case LoadP2PSettingsInPathResult : : SUCCESS :
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Successfully loaded default P2P configuration " ) ;
2025-10-27 20:19:47 +03:00
break ;
case LoadP2PSettingsInPathResult : : FAILED_TO_READ :
throw std : : runtime_error ( " [NetLibrary::init] Failed to read default_p2p.json " ) ;
case LoadP2PSettingsInPathResult : : FAILED_TO_OPEN :
throw std : : runtime_error ( " [NetLibrary::init] Failed to open default_p2p.json " ) ;
}
break ;
}
2026-05-14 11:53:36 +03:00
}
2025-07-06 20:43:53 +03:00
2026-05-14 11:53:36 +03:00
void NetLibrary : : init_websocket ( ) {
if ( alreadyInitialized )
return ;
2026-05-16 18:59:21 +03:00
// TLS verification fails on mac and android due to incorrect cert location
// Should be fixed later
2026-02-24 21:13:34 +03:00
# if defined(__APPLE__) || defined(__ANDROID__)
2025-07-06 20:43:53 +03:00
rtc : : WebSocket : : Configuration wsConfig ;
wsConfig . disableTlsVerification = true ;
ws = std : : make_shared < rtc : : WebSocket > ( wsConfig ) ;
# else
ws = std : : make_shared < rtc : : WebSocket > ( ) ;
# endif
auto wsFuture = wsPromise . get_future ( ) ;
ws - > onOpen ( [ ] ( ) {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Websocket connected, signaling ready " ) ;
2025-07-06 20:43:53 +03:00
wsPromise . set_value ( ) ;
} ) ;
ws - > onError ( [ ] ( std : : string s ) {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Websocket error: " + s ) ;
2025-07-06 20:43:53 +03:00
wsPromise . set_exception ( std : : make_exception_ptr ( std : : runtime_error ( s ) ) ) ;
} ) ;
ws - > onClosed ( [ ] ( ) {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Websocket closed " ) ;
2026-05-14 11:53:36 +03:00
destroy_websocket ( ) ;
2025-07-06 20:43:53 +03:00
} ) ;
ws - > onMessage ( [ wws = make_weak_ptr ( ws ) ] ( auto data ) {
if ( ! std : : holds_alternative < std : : string > ( data ) )
return ;
2025-10-27 20:19:47 +03:00
nlohmann : : json message = nlohmann : : json : : parse ( std : : get < std : : string > ( data ) ) ;
2025-07-06 20:43:53 +03:00
auto itID = message . find ( " id " ) ;
if ( itID = = message . end ( ) )
return ;
auto id = itID - > get < std : : string > ( ) ;
auto itType = message . find ( " type " ) ;
auto type = itType - > get < std : : string > ( ) ;
std : : shared_ptr < rtc : : PeerConnection > peerConnection ;
{
std : : scoped_lock peerListLock ( peerListMutex ) ;
auto peerIt = peers . find ( id ) ;
if ( peerIt ! = peers . end ( ) )
peerConnection = peerIt - > second - > connection ;
else if ( type = = " offer " ) {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Websocket answering to " + id ) ;
2025-07-06 20:43:53 +03:00
auto peer = setup_peer_connection ( config , wws , id ) ;
peers [ id ] = peer ;
peerConnection = peer - > connection ;
}
}
if ( ! peerConnection )
return ;
if ( type = = " offer " | | type = = " answer " ) {
auto sdp = message [ " description " ] . get < std : : string > ( ) ;
peerConnection - > setRemoteDescription ( rtc : : Description ( sdp , type ) ) ;
}
else if ( type = = " candidate " ) {
auto sdp = message [ " candidate " ] . get < std : : string > ( ) ;
auto mid = message [ " mid " ] . get < std : : string > ( ) ;
peerConnection - > addRemoteCandidate ( rtc : : Candidate ( sdp , mid ) ) ;
}
} ) ;
get_global_id ( ) ;
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " NetLibrary global id: " + globalID ) ;
2025-07-06 20:43:53 +03:00
const std : : string wsPrefix = signalingAddr . find ( " :// " ) = = std : : string : : npos ? " ws:// " : " " ;
const std : : string url = wsPrefix + signalingAddr + " / " + globalID ;
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Websocket URL is: " + url ) ;
2025-07-06 20:43:53 +03:00
ws - > open ( url ) ;
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " NetLibrary waiting for signaling to be connected... " ) ;
2025-07-06 20:43:53 +03:00
//wsFuture.get();
alreadyInitialized = true ;
}
2025-10-27 20:19:47 +03:00
void NetLibrary : : copy_default_p2p_config_to_path ( const std : : filesystem : : path & newP2PConfigPath ) {
2026-04-23 20:56:11 +03:00
try {
std : : string readLocalP2PConfig = read_file_to_string ( newP2PConfigPath ) ; // If it doesnt fail, then file exists, so dont copy p2p config
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Local P2P configuration already exists " ) ;
2025-10-27 20:19:47 +03:00
return ;
2026-04-23 20:56:11 +03:00
} catch ( . . . ) { }
try {
nlohmann : : json j ( nlohmann : : json : : parse ( read_file_to_string ( " data/config/default_p2p.json " ) ) ) ;
std : : stringstream newFileSS ;
newFileSS < < std : : setw ( 4 ) < < j ;
if ( SDL_SaveFile ( newP2PConfigPath . string ( ) . c_str ( ) , newFileSS . view ( ) . data ( ) , newFileSS . view ( ) . size ( ) ) )
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " New P2P configuration created " ) ;
2025-10-27 20:19:47 +03:00
else
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " [NetLibrary::copy_default_p2p_config_to_path] Could not open new p2p.json to write to " ) ;
2026-04-23 20:56:11 +03:00
} catch ( . . . ) {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " [NetLibrary::copy_default_p2p_config_to_path] Could not open default_p2p.json " ) ;
2026-04-23 20:56:11 +03:00
}
2025-10-27 20:19:47 +03:00
}
2025-07-06 20:43:53 +03:00
std : : string NetLibrary : : get_random_server_local_id ( ) {
return Random : : get ( ) . alphanumeric_str ( LOCALID_LEN ) ;
}
const std : : string & NetLibrary : : get_global_id ( ) {
if ( globalID . empty ( ) )
globalID = Random : : get ( ) . alphanumeric_str ( GLOBALID_LEN ) ;
return globalID ;
}
void NetLibrary : : register_server ( std : : shared_ptr < NetServer > server ) {
std : : scoped_lock serverListLock ( serverListMutex ) ;
servers . emplace_back ( server ) ;
}
void NetLibrary : : register_client ( std : : shared_ptr < NetClient > client ) {
client - > localID = Random : : get ( ) . alphanumeric_str ( LOCALID_LEN ) ;
2026-05-14 11:53:36 +03:00
std : : scoped_lock clientListLock ( clientListMutex ) ;
2025-07-06 20:43:53 +03:00
clients . emplace_back ( client ) ;
}
void NetLibrary : : finish_client_registration ( std : : shared_ptr < NetClient > client ) {
try {
std : : string serverGlobalID = client - > serverFullID . substr ( 0 , GLOBALID_LEN ) ;
std : : string serverLocalID = client - > serverFullID . substr ( GLOBALID_LEN ) ;
std : : scoped_lock peerListLock ( peerListMutex ) ;
if ( ! peers . contains ( serverGlobalID ) ) {
auto peer = setup_peer_connection ( config , ws , serverGlobalID ) ;
peers [ serverGlobalID ] = peer ;
peer - > channel = peer - > connection - > createDataChannel ( MAIN_NETLIBRARY_CHANNEL ) ;
setup_main_data_channel ( peer , serverGlobalID ) ;
}
peers [ serverGlobalID ] - > messageQueue . emplace ( client - > localID + serverLocalID ) ;
client - > fullyRegistered = true ;
}
catch ( const std : : exception & e ) {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Invalid client registration: " + std : : string ( e . what ( ) ) ) ;
2025-07-06 20:43:53 +03:00
client - > isDisconnected = true ;
}
}
void NetLibrary : : update ( ) {
{
std : : scoped_lock clientListLock ( clientListMutex ) ;
std : : erase_if ( clients , [ ] ( auto & cWeak ) {
auto cLock = cWeak . lock ( ) ;
if ( ! cLock )
return true ;
if ( ! cLock - > fullyRegistered & & ws - > isOpen ( ) )
finish_client_registration ( cLock ) ;
return false ;
} ) ;
}
{
std : : scoped_lock serverListLock ( serverListMutex ) ;
std : : erase_if ( servers , [ ] ( auto & sWeak ) {
return ! sWeak . lock ( ) ;
} ) ;
}
{
std : : scoped_lock peerListLock ( peerListMutex ) ;
std : : erase_if ( peers , [ ] ( auto & a ) {
auto & p = a . second ;
rtc : : PeerConnection : : State s = p - > connection - > state ( ) ;
if ( s = = rtc : : PeerConnection : : State : : Disconnected | | s = = rtc : : PeerConnection : : State : : Closed | | s = = rtc : : PeerConnection : : State : : Failed )
return true ;
if ( p - > channel ) {
if ( p - > channel - > isOpen ( ) ) {
while ( ! p - > messageQueue . empty ( ) ) {
p - > channel - > send ( p - > messageQueue . front ( ) ) ;
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Sent main channel message: " + p - > messageQueue . front ( ) ) ;
2025-07-06 20:43:53 +03:00
p - > messageQueue . pop ( ) ;
}
}
}
return false ;
} ) ;
}
}
std : : shared_ptr < NetLibrary : : PeerData > NetLibrary : : setup_peer_connection ( const rtc : : Configuration & config , std : : weak_ptr < rtc : : WebSocket > wws , std : : string id ) {
using json = nlohmann : : json ;
auto peer = std : : make_shared < PeerData > ( ) ;
peer - > connection = std : : make_shared < rtc : : PeerConnection > ( config ) ;
peer - > connection - > onLocalDescription ( [ wws , id ] ( rtc : : Description description ) {
json message = { { " id " , id } ,
{ " type " , description . typeString ( ) } ,
{ " description " , std : : string ( description ) } } ;
if ( auto ws = wws . lock ( ) )
ws - > send ( message . dump ( ) ) ;
} ) ;
peer - > connection - > onLocalCandidate ( [ wws , id ] ( rtc : : Candidate candidate ) {
json message = { { " id " , id } ,
{ " type " , " candidate " } ,
{ " candidate " , std : : string ( candidate ) } ,
{ " mid " , candidate . mid ( ) } } ;
if ( auto ws = wws . lock ( ) )
ws - > send ( message . dump ( ) ) ;
} ) ;
peer - > connection - > onDataChannel ( [ id , peerWeak = make_weak_ptr ( peer ) ] ( std : : shared_ptr < rtc : : DataChannel > dc ) {
auto peer = peerWeak . lock ( ) ;
if ( peer ) {
if ( ! peer - > channel ) {
if ( dc - > label ( ) = = MAIN_NETLIBRARY_CHANNEL ) {
peer - > channel = dc ;
setup_main_data_channel ( peer , id ) ;
}
else {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Client connected with incorrect first channel name: " + dc - > label ( ) + " . Could be from a different version of the program " ) ;
2025-07-06 20:43:53 +03:00
dc - > send ( " REFUSE " ) ;
peer - > connection - > close ( ) ;
}
}
else
channel_created_in_client ( dc ) ;
}
} ) ;
return peer ;
}
void NetLibrary : : setup_main_data_channel ( std : : shared_ptr < PeerData > peer , const std : : string & id ) {
peer - > channel - > onOpen ( [ id ] ( ) {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Main data channel has opened on " + id ) ;
2025-07-06 20:43:53 +03:00
} ) ;
peer - > channel - > onClosed ( [ id ] ( ) {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Main data channel has closed on " + id ) ;
2025-07-06 20:43:53 +03:00
} ) ;
peer - > channel - > onMessage ( [ id , pConWeak = make_weak_ptr ( peer - > connection ) ] ( auto data ) {
if ( ! std : : holds_alternative < std : : string > ( data ) ) { // Main channel messages will only carry text
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Main data channel received binary message, which is not expected " ) ;
2025-07-06 20:43:53 +03:00
return ;
}
std : : string message = std : : get < std : : string > ( data ) ;
if ( message = = " REFUSE " ) {
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : DESKTOP_USERINFO , " Refused connection. Could be on a different version " ) ;
2025-07-06 20:43:53 +03:00
auto pConLock = pConWeak . lock ( ) ;
if ( pConLock )
pConLock - > close ( ) ;
}
else {
std : : string clientLocalID = message . substr ( 0 , LOCALID_LEN ) ;
std : : string serverLocalID = message . substr ( LOCALID_LEN ) ;
2026-05-16 21:56:58 +03:00
Logger : : get ( ) . log ( Logger : : LogType : : INFO , " Main data channel received message " + message ) ;
2025-07-06 20:43:53 +03:00
auto pConLock = pConWeak . lock ( ) ;
if ( pConLock )
assign_client_connection_to_server ( serverLocalID , clientLocalID , pConLock ) ;
}
} ) ;
}
void NetLibrary : : assign_client_connection_to_server ( const std : : string & serverLocalID , const std : : string & clientLocalID , std : : shared_ptr < rtc : : PeerConnection > connection ) {
std : : scoped_lock serverListLock ( serverListMutex ) ;
auto serverIt = std : : find_if ( servers . begin ( ) , servers . end ( ) , [ & serverLocalID ] ( auto & s ) {
auto sLock = s . lock ( ) ;
if ( ! sLock )
return false ;
return sLock - > localID = = serverLocalID ;
} ) ;
if ( serverIt ! = servers . end ( ) ) {
auto sLock = serverIt - > lock ( ) ;
if ( sLock )
sLock - > client_connected ( connection , clientLocalID ) ;
}
}
void NetLibrary : : channel_created_in_client ( std : : shared_ptr < rtc : : DataChannel > channel ) {
std : : string clientID = channel - > label ( ) . substr ( 0 , LOCALID_LEN ) ;
std : : string channelName = channel - > label ( ) . substr ( LOCALID_LEN ) ;
std : : scoped_lock clientListLock ( clientListMutex ) ;
auto clientIt = std : : find_if ( clients . begin ( ) , clients . end ( ) , [ & clientID ] ( auto & c ) {
auto cLock = c . lock ( ) ;
if ( ! cLock )
return false ;
return cLock - > localID = = clientID ;
} ) ;
if ( clientIt ! = clients . end ( ) ) {
auto cLock = clientIt - > lock ( ) ;
if ( cLock )
cLock - > init_channel ( channelName , channel ) ;
}
}
2025-10-02 19:10:58 +03:00
bool NetLibrary : : is_ordered_channel ( std : : string_view channelName ) {
return channelName = = RELIABLE_COMMAND_CHANNEL | | channelName = = UNRELIABLE_COMMAND_CHANNEL ;
}
MessageOrder NetLibrary : : calc_order_for_queued_message ( std : : string_view channelName , MessageOrder & mOrder ) {
return is_ordered_channel ( channelName ) ? mOrder + + : 0 ;
}
std : : string NetLibrary : : attach_message_order ( MessageOrder order , const std : : string & message ) {
2025-07-06 20:43:53 +03:00
union {
MessageOrder o ;
char b [ sizeof ( MessageOrder ) ] ;
} u ;
u . o = ( std : : endian : : native = = std : : endian : : little ) ? std : : byteswap ( order ) : order ;
return std : : string ( u . b , sizeof ( MessageOrder ) ) + message ;
}
MessageOrder NetLibrary : : get_message_order ( rtc : : binary & message ) {
union {
MessageOrder o ;
char b [ sizeof ( MessageOrder ) ] ;
} u ;
std : : memcpy ( u . b , message . data ( ) , sizeof ( MessageOrder ) ) ;
if ( std : : endian : : native = = std : : endian : : little )
u . o = std : : byteswap ( u . o ) ;
return u . o ;
}
2026-05-14 11:53:36 +03:00
bool NetLibrary : : is_initialized ( ) {
return alreadyInitialized ;
}
void NetLibrary : : destroy_websocket ( ) {
if ( ! alreadyInitialized )
return ;
2025-07-06 20:43:53 +03:00
ws = nullptr ;
{
std : : scoped_lock peerListLock ( peerListMutex ) ;
peers . clear ( ) ;
}
{
std : : scoped_lock clientListLock ( clientListMutex ) ;
for ( auto & c : clients ) {
auto cLock = c . lock ( ) ;
if ( cLock )
cLock - > isDisconnected = true ;
}
clients . clear ( ) ;
}
{
std : : scoped_lock serverListLock ( serverListMutex ) ;
for ( auto & s : servers ) {
auto sLock = s . lock ( ) ;
if ( sLock )
sLock - > isDisconnected = true ;
}
servers . clear ( ) ;
}
wsPromise = std : : promise < void > ( ) ;
alreadyInitialized = false ;
2026-05-14 11:53:36 +03:00
//globalID.clear(); // Do we need to do this? (Causes problems when global ID changes from the user's side after they copy a lobby address
2025-07-06 20:43:53 +03:00
}