1 #include "test_common.h"
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <stdarg.h>
5 #include <string.h>
6 #include <string>
7 #include <random>
8 #include <chrono>
9 #include <thread>
10 
11 
12 #include <steam/steamnetworkingsockets.h>
13 #include <steam/isteamnetworkingutils.h>
14 #include "../examples/trivial_signaling_client.h"
15 
16 HSteamListenSocket g_hListenSock;
17 HSteamNetConnection g_hConnection;
18 enum ETestRole
19 {
20 	k_ETestRole_Undefined,
21 	k_ETestRole_Server,
22 	k_ETestRole_Client,
23 	k_ETestRole_Symmetric,
24 };
25 ETestRole g_eTestRole = k_ETestRole_Undefined;
26 
27 int g_nVirtualPortLocal = 0; // Used when listening, and when connecting
28 int g_nVirtualPortRemote = 0; // Only used when connecting
29 
Quit(int rc)30 void Quit( int rc )
31 {
32 	if ( rc == 0 )
33 	{
34 		// OK, we cannot just exit the process, because we need to give
35 		// the connection time to actually send the last message and clean up.
36 		// If this were a TCP connection, we could just bail, because the OS
37 		// would handle it.  But this is an application protocol over UDP.
38 		// So give a little bit of time for good cleanup.  (Also note that
39 		// we really ought to continue pumping the signaling service, but
40 		// in this exampple we'll assume that no more signals need to be
41 		// exchanged, since we've gotten this far.)  If we just terminated
42 		// the program here, our peer could very likely timeout.  (Although
43 		// it's possible that the cleanup packets have already been placed
44 		// on the wire, and if they don't drop, things will get cleaned up
45 		// properly.)
46 		TEST_Printf( "Waiting for any last cleanup packets.\n" );
47 		std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) );
48 	}
49 
50 	TEST_Kill();
51 	exit(rc);
52 }
53 
54 // Send a simple string message to out peer, using reliable transport.
SendMessageToPeer(const char * pszMsg)55 void SendMessageToPeer( const char *pszMsg )
56 {
57 	TEST_Printf( "Sending msg '%s'\n", pszMsg );
58 	EResult r = SteamNetworkingSockets()->SendMessageToConnection(
59 		g_hConnection, pszMsg, (int)strlen(pszMsg)+1, k_nSteamNetworkingSend_Reliable, nullptr );
60 	assert( r == k_EResultOK );
61 }
62 
63 // Called when a connection undergoes a state transition.
OnSteamNetConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t * pInfo)64 void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t *pInfo )
65 {
66 	// What's the state of the connection?
67 	switch ( pInfo->m_info.m_eState )
68 	{
69 	case k_ESteamNetworkingConnectionState_ClosedByPeer:
70 	case k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
71 
72 		TEST_Printf( "[%s] %s, reason %d: %s\n",
73 			pInfo->m_info.m_szConnectionDescription,
74 			( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer ? "closed by peer" : "problem detected locally" ),
75 			pInfo->m_info.m_eEndReason,
76 			pInfo->m_info.m_szEndDebug
77 		);
78 
79 		// Close our end
80 		SteamNetworkingSockets()->CloseConnection( pInfo->m_hConn, 0, nullptr, false );
81 
82 		if ( g_hConnection == pInfo->m_hConn )
83 		{
84 			g_hConnection = k_HSteamNetConnection_Invalid;
85 
86 			// In this example, we will bail the test whenever this happens.
87 			// Was this a normal termination?
88 			int rc = 0;
89 			if ( rc == k_ESteamNetworkingConnectionState_ProblemDetectedLocally || pInfo->m_info.m_eEndReason != k_ESteamNetConnectionEnd_App_Generic )
90 				rc = 1; // failure
91 			Quit( rc );
92 		}
93 		else
94 		{
95 			// Why are we hearing about any another connection?
96 			assert( false );
97 		}
98 
99 		break;
100 
101 	case k_ESteamNetworkingConnectionState_None:
102 		// Notification that a connection was destroyed.  (By us, presumably.)
103 		// We don't need this, so ignore it.
104 		break;
105 
106 	case k_ESteamNetworkingConnectionState_Connecting:
107 
108 		// Is this a connection we initiated, or one that we are receiving?
109 		if ( g_hListenSock != k_HSteamListenSocket_Invalid && pInfo->m_info.m_hListenSocket == g_hListenSock )
110 		{
111 			// Somebody's knocking
112 			// Note that we assume we will only ever receive a single connection
113 			assert( g_hConnection == k_HSteamNetConnection_Invalid ); // not really a bug in this code, but a bug in the test
114 
115 			TEST_Printf( "[%s] Accepting\n", pInfo->m_info.m_szConnectionDescription );
116 			g_hConnection = pInfo->m_hConn;
117 			SteamNetworkingSockets()->AcceptConnection( pInfo->m_hConn );
118 		}
119 		else
120 		{
121 			// Note that we will get notification when our own connection that
122 			// we initiate enters this state.
123 			assert( g_hConnection == pInfo->m_hConn );
124 			TEST_Printf( "[%s] Entered connecting state\n", pInfo->m_info.m_szConnectionDescription );
125 		}
126 		break;
127 
128 	case k_ESteamNetworkingConnectionState_FindingRoute:
129 		// P2P connections will spend a bried time here where they swap addresses
130 		// and try to find a route.
131 		TEST_Printf( "[%s] finding route\n", pInfo->m_info.m_szConnectionDescription );
132 		break;
133 
134 	case k_ESteamNetworkingConnectionState_Connected:
135 		// We got fully connected
136 		assert( pInfo->m_hConn == g_hConnection ); // We don't initiate or accept any other connections, so this should be out own connection
137 		TEST_Printf( "[%s] connected\n", pInfo->m_info.m_szConnectionDescription );
138 		break;
139 
140 	default:
141 		assert( false );
142 		break;
143 	}
144 }
145 
main(int argc,const char ** argv)146 int main( int argc, const char **argv )
147 {
148 	SteamNetworkingIdentity identityLocal; identityLocal.Clear();
149 	SteamNetworkingIdentity identityRemote; identityRemote.Clear();
150 	const char *pszTrivialSignalingService = "localhost:10000";
151 
152 	// Parse the command line
153 	for ( int idxArg = 1 ; idxArg < argc ; ++idxArg )
154 	{
155 		const char *pszSwitch = argv[idxArg];
156 
157 		auto GetArg = [&]() -> const char * {
158 			if ( idxArg + 1 >= argc )
159 				TEST_Fatal( "Expected argument after %s", pszSwitch );
160 			return argv[++idxArg];
161 		};
162 		auto ParseIdentity = [&]( SteamNetworkingIdentity &x ) {
163 			const char *pszArg = GetArg();
164 			if ( !x.ParseString( pszArg ) )
165 				TEST_Fatal( "'%s' is not a valid identity string", pszArg );
166 		};
167 
168 		if ( !strcmp( pszSwitch, "--identity-local" ) )
169 			ParseIdentity( identityLocal );
170 		else if ( !strcmp( pszSwitch, "--identity-remote" ) )
171 			ParseIdentity( identityRemote );
172 		else if ( !strcmp( pszSwitch, "--signaling-server" ) )
173 			pszTrivialSignalingService = GetArg();
174 		else if ( !strcmp( pszSwitch, "--client" ) )
175 			g_eTestRole = k_ETestRole_Client;
176 		else if ( !strcmp( pszSwitch, "--server" ) )
177 			g_eTestRole = k_ETestRole_Server;
178 		else if ( !strcmp( pszSwitch, "--symmetric" ) )
179 			g_eTestRole = k_ETestRole_Symmetric;
180 		else
181 			TEST_Fatal( "Unexpected command line argument '%s'", pszSwitch );
182 	}
183 
184 	if ( g_eTestRole == k_ETestRole_Undefined )
185 		TEST_Fatal( "Must specify test role (--server, --client, or --symmetric" );
186 	if ( identityLocal.IsInvalid() )
187 		TEST_Fatal( "Must specify local identity using --identity-local" );
188 	if ( identityRemote.IsInvalid() && g_eTestRole != k_ETestRole_Server )
189 		TEST_Fatal( "Must specify remote identity using --identity-remote" );
190 
191 	// Initialize library, with the desired local identity
192 	TEST_Init( &identityLocal );
193 
194 	// Hardcode STUN servers
195 	SteamNetworkingUtils()->SetGlobalConfigValueString( k_ESteamNetworkingConfig_P2P_STUN_ServerList, "stun.l.google.com:19302" );
196 
197 	// Allow sharing of any kind of ICE address.
198 	// We don't have any method of relaying (TURN) in this example, so we are essentially
199 	// forced to disclose our public address if we want to pierce NAT.  But if we
200 	// had relay fallback, or if we only wanted to connect on the LAN, we could restrict
201 	// to only sharing private addresses.
202 	SteamNetworkingUtils()->SetGlobalConfigValueInt32(k_ESteamNetworkingConfig_P2P_Transport_ICE_Enable, k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_All );
203 
204 	// Create the signaling service
205 	SteamNetworkingErrMsg errMsg;
206 	ITrivialSignalingClient *pSignaling = CreateTrivialSignalingClient( pszTrivialSignalingService, SteamNetworkingSockets(), errMsg );
207 	if ( pSignaling == nullptr )
208 		TEST_Fatal( "Failed to initializing signaling client.  %s", errMsg );
209 
210 	SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged( OnSteamNetConnectionStatusChanged );
211 
212 	// Comment this line in for more detailed spew about signals, route finding, ICE, etc
213 	//SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_LogLevel_P2PRendezvous, k_ESteamNetworkingSocketsDebugOutputType_Verbose );
214 
215 	// Create listen socket to receive connections on, unless we are the client
216 	if ( g_eTestRole == k_ETestRole_Server )
217 	{
218 		TEST_Printf( "Creating listen socket, local virtual port %d\n", g_nVirtualPortLocal );
219 		g_hListenSock = SteamNetworkingSockets()->CreateListenSocketP2P( g_nVirtualPortLocal, 0, nullptr );
220 		assert( g_hListenSock != k_HSteamListenSocket_Invalid  );
221 	}
222 	else if ( g_eTestRole == k_ETestRole_Symmetric )
223 	{
224 
225 		// Currently you must create a listen socket to use symmetric mode,
226 		// even if you know that you will always create connections "both ways".
227 		// In the future we might try to remove this requirement.  It is a bit
228 		// less efficient, since it always triggered the race condition case
229 		// where both sides create their own connections, and then one side
230 		// decides to their theirs away.  If we have a listen socket, then
231 		// it can be the case that one peer will receive the incoming connection
232 		// from the other peer, and since he has a listen socket, can save
233 		// the connection, and then implicitly accept it when he initiates his
234 		// own connection.  Without the listen socket, if an incoming connection
235 		// request arrives before we have started connecting out, then we are forced
236 		// to ignore it, as the app has given no indication that it desires to
237 		// receive inbound connections at all.
238 		TEST_Printf( "Creating listen socket in symmetric mode, local virtual port %d\n", g_nVirtualPortLocal );
239 		SteamNetworkingConfigValue_t opt;
240 		opt.SetInt32( k_ESteamNetworkingConfig_SymmetricConnect, 1 ); // << Note we set symmetric mode on the listen socket
241 		g_hListenSock = SteamNetworkingSockets()->CreateListenSocketP2P( g_nVirtualPortLocal, 1, &opt );
242 		assert( g_hListenSock != k_HSteamListenSocket_Invalid  );
243 	}
244 
245 	// Begin connecting to peer, unless we are the server
246 	if ( g_eTestRole != k_ETestRole_Server )
247 	{
248 		std::vector< SteamNetworkingConfigValue_t > vecOpts;
249 
250 		// If we want the local and virtual port to differ, we must set
251 		// an option.  This is a pretty rare use case, and usually not needed.
252 		// The local virtual port is only usually relevant for symmetric
253 		// connections, and then, it almost always matches.  Here we are
254 		// just showing in this example code how you could handle this if you
255 		// needed them to differ.
256 		if ( g_nVirtualPortRemote != g_nVirtualPortLocal )
257 		{
258 			SteamNetworkingConfigValue_t opt;
259 			opt.SetInt32( k_ESteamNetworkingConfig_LocalVirtualPort, g_nVirtualPortLocal );
260 			vecOpts.push_back( opt );
261 		}
262 
263 		// Symmetric mode?  Noce that since we created a listen socket on this local
264 		// virtual port and tagged it for symmetric connect mode, any connections
265 		// we create that use the same local virtual port will automatically inherit
266 		// this setting.  However, this is really not recommended.  It is best to be
267 		// explicit.
268 		if ( g_eTestRole == k_ETestRole_Symmetric )
269 		{
270 			SteamNetworkingConfigValue_t opt;
271 			opt.SetInt32( k_ESteamNetworkingConfig_SymmetricConnect, 1 );
272 			vecOpts.push_back( opt );
273 			TEST_Printf( "Connecting to '%s' in symmetric mode, virtual port %d, from local virtual port %d.\n",
274 				SteamNetworkingIdentityRender( identityRemote ).c_str(), g_nVirtualPortRemote,
275 				g_nVirtualPortLocal );
276 		}
277 		else
278 		{
279 			TEST_Printf( "Connecting to '%s', virtual port %d, from local virtual port %d.\n",
280 				SteamNetworkingIdentityRender( identityRemote ).c_str(), g_nVirtualPortRemote,
281 				g_nVirtualPortLocal );
282 		}
283 
284 		// Connect using the "custom signaling" path.  Note that when
285 		// you are using this path, the identity is actually optional,
286 		// since we don't need it.  (Your signaling object already
287 		// knows how to talk to the peer) and then the peer identity
288 		// will be confirmed via rendezvous.
289 		SteamNetworkingErrMsg errMsg;
290 		ISteamNetworkingConnectionSignaling *pConnSignaling = pSignaling->CreateSignalingForConnection(
291 			identityRemote,
292 			errMsg
293 		);
294 		assert( pConnSignaling );
295 		g_hConnection = SteamNetworkingSockets()->ConnectP2PCustomSignaling( pConnSignaling, &identityRemote, g_nVirtualPortRemote, (int)vecOpts.size(), vecOpts.data() );
296 		assert( g_hConnection != k_HSteamNetConnection_Invalid );
297 
298 		// Go ahead and send a message now.  The message will be queued until route finding
299 		// completes.
300 		SendMessageToPeer( "Greetings!" );
301 	}
302 
303 	// Main test loop
304 	for (;;)
305 	{
306 		// Check for incoming signals, and dispatch them
307 		pSignaling->Poll();
308 
309 		// Check callbacks
310 		TEST_PumpCallbacks();
311 
312 		// If we have a connection, then poll it for messages
313 		if ( g_hConnection != k_HSteamNetConnection_Invalid )
314 		{
315 			SteamNetworkingMessage_t *pMessage;
316 			int r = SteamNetworkingSockets()->ReceiveMessagesOnConnection( g_hConnection, &pMessage, 1 );
317 			assert( r == 0 || r == 1 ); // <0 indicates an error
318 			if ( r == 1 )
319 			{
320 				// In this example code we will assume all messages are '\0'-terminated strings.
321 				// Obviously, this is not secure.
322 				TEST_Printf( "Received message '%s'\n", pMessage->GetData() );
323 
324 				// Free message struct and buffer.
325 				pMessage->Release();
326 
327 				// If we're the client, go ahead and shut down.  In this example we just
328 				// wanted to establish a connection and exchange a message, and we've done that.
329 				// Note that we use "linger" functionality.  This flushes out any remaining
330 				// messages that we have queued.  Essentially to us, the connection is closed,
331 				// but on thew wire, we will not actually close it until all reliable messages
332 				// have been confirmed as received by the client.  (Or the connection is closed
333 				// by the peer or drops.)  If we are the "client" role, then we know that no such
334 				// messages are in the pipeline in this test.  But in symmetric mode, it is
335 				// possible that we need to flush out our message that we sent.
336 				if ( g_eTestRole != k_ETestRole_Server )
337 				{
338 					TEST_Printf( "Closing connection and shutting down.\n" );
339 					SteamNetworkingSockets()->CloseConnection( g_hConnection, 0, "Test completed OK", true );
340 					Quit(0);
341 				}
342 
343 				// We're the server.  Send a reply.
344 				SendMessageToPeer( "I got your message" );
345 			}
346 		}
347 	}
348 
349 	return 0;
350 }
351