diff --git a/.travis/build-cmake.sh b/.travis/build-cmake.sh index 718aefc..1b5c35e 100755 --- a/.travis/build-cmake.sh +++ b/.travis/build-cmake.sh @@ -99,21 +99,23 @@ if [[ $BUILD_SANITIZERS -ne 0 ]]; then fi # Run basic tests -build-cmake-ref/tests/test_crypto -[[ $BUILD_LIBSODIUM -ne 0 ]] && build-cmake-sodium/tests/test_crypto -build-cmake-sodium25519/tests/test_crypto -build-cmake/tests/test_crypto -build-cmake/tests/test_connection +build-cmake-ref/bin/test_crypto +[[ $BUILD_LIBSODIUM -ne 0 ]] && build-cmake-sodium/bin/test_crypto +build-cmake-sodium25519/bin/test_crypto +build-cmake/bin/test_crypto +build-cmake/bin/test_connection # Run sanitized builds if [[ $BUILD_SANITIZERS -ne 0 ]]; then for SANITIZER in asan ubsan tsan; do [[ -d build-${SANITIZER} ]] || continue - build-${SANITIZER}/tests/test_crypto - build-${SANITIZER}/tests/test_connection + build-${SANITIZER}/bin/test_crypto + build-${SANITIZER}/bin/test_connection done fi +# FIXME Run P2P tests? + set +x exit 0 diff --git a/.travis/build-meson.sh b/.travis/build-meson.sh deleted file mode 100755 index 66bf0c6..0000000 --- a/.travis/build-meson.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash -# -# Meson build test -# - -set -e - -cleanup() { - echo "Cleaning up Meson build directories" >&2 - rm -rf build-meson{,-ref,-sodium{,25519}} -} - -trap cleanup EXIT -cleanup - -MESON_ARGS=( - -Dlight_tests=true - -DWerror=true -) - -BUILD_LIBSODIUM=1 - -# libsodium's AES implementation only works on x86_64 -[[ $(uname -m) != x86_64 ]] && BUILD_LIBSODIUM=0 - -set -x - -# Build lightweight test builds -meson . build-meson ${MESON_ARGS[@]} --buildtype debugoptimized -meson . build-meson-ref ${MESON_ARGS[@]} --buildtype debugoptimized -Duse_crypto25519=Reference -[[ $BUILD_LIBSODIUM -ne 0 ]] && meson . build-meson-sodium ${MESON_ARGS[@]} --buildtype debugoptimized -Duse_crypto=libsodium -Duse_crypto25519=libsodium -meson . build-meson-sodium25519 ${MESON_ARGS[@]} --buildtype debugoptimized -Duse_crypto25519=libsodium - -# Build all targets -ninja -v -C build-meson -[[ $BUILD_LIBSODIUM -ne 0 ]] && ninja -v -C build-meson-sodium -ninja -v -C build-meson-sodium25519 -ninja -v -C build-meson-ref - -# Run basic tests -build-meson/tests/test_crypto -[[ $BUILD_LIBSODIUM -ne 0 ]] && build-meson-sodium/tests/test_crypto -build-meson-sodium25519/tests/test_crypto -build-meson-ref/tests/test_crypto - -set +x - -exit 0 diff --git a/.travis/build.sh b/.travis/build.sh index 3e9ab50..8602789 100755 --- a/.travis/build.sh +++ b/.travis/build.sh @@ -52,8 +52,8 @@ fi msg "Image is $IMAGE:$IMAGE_TAG, sanitizers enabled: $BUILD_SANITIZERS" -# We need at least one build system available -has cmake || has meson || die "No build system available" +# Make sure cmake is installed +has cmake || die "cmake required" # We also need at least one compiler has_clang || has_gcc || die "No compiler available" @@ -63,16 +63,16 @@ ccache -M4G if has_clang; then msg "Beginning build tests with Clang" - export CC="ccache clang" CXX="ccache clang++" - has meson && bash .travis/build-meson.sh + #export CC="ccache clang" CXX="ccache clang++" + #has meson && bash .travis/build-meson.sh export CC=clang CXX=clang++ has cmake && bash .travis/build-cmake.sh fi if has_gcc; then msg "Beginning build tests with GCC" - export CC="ccache gcc" CXX="ccache g++" - has meson && bash .travis/build-meson.sh + #export CC="ccache gcc" CXX="ccache g++" + #has meson && bash .travis/build-meson.sh export CC=gcc CXX=g++ has cmake && bash .travis/build-cmake.sh fi diff --git a/.travis/install-alpine.sh b/.travis/install-alpine.sh index f1672bb..1cd9c48 100755 --- a/.travis/install-alpine.sh +++ b/.travis/install-alpine.sh @@ -18,6 +18,7 @@ PACKAGES=( pkgconf git linux-headers + go ) PACKAGES+=(protobuf-dev) diff --git a/.travis/install-archlinux.sh b/.travis/install-archlinux.sh index f0302d2..b25900e 100755 --- a/.travis/install-archlinux.sh +++ b/.travis/install-archlinux.sh @@ -12,6 +12,7 @@ PACKAGES=( cmake meson git + golang ) PACKAGES+=(protobuf) diff --git a/.travis/install-fedora.sh b/.travis/install-fedora.sh index aa3749c..fcf46bb 100755 --- a/.travis/install-fedora.sh +++ b/.travis/install-fedora.sh @@ -16,6 +16,7 @@ PACKAGES=( pkgconf-pkg-config git make + golang ) PACKAGES+=(protobuf-compiler protobuf-devel) diff --git a/.travis/install-ubuntu.sh b/.travis/install-ubuntu.sh index b5a6731..063d31c 100755 --- a/.travis/install-ubuntu.sh +++ b/.travis/install-ubuntu.sh @@ -10,7 +10,7 @@ apt-get update apt-get install -y locales locale-gen en_US.UTF-8 -PACKAGES=(build-essential pkg-config ccache cmake meson clang git) +PACKAGES=(build-essential pkg-config ccache cmake meson clang git golang) PACKAGES+=(libprotobuf-dev protobuf-compiler) PACKAGES+=(libssl-dev) diff --git a/BUILDING.md b/BUILDING.md index baae286..cabb493 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -3,7 +3,8 @@ Building ## Dependencies -* CMake or Meson, and build tool like Ninja, GNU Make or Visual Studio +* CMake 3.10 or later +* A build tool like Ninja, GNU Make or Visual Studio * A C++11-compliant compiler, such as: * GCC 7.3 or later * Clang 3.3 or later @@ -13,8 +14,12 @@ Building * OpenSSL 1.1.x, plus ed25519-donna and curve25519-donna. (We've made some minor changes, so the source is included in this project.) * libsodium - * [bcrypt](https://docs.microsoft.com/en-us/windows/desktop/api/bcrypt/) (windows only) + * [bcrypt](https://docs.microsoft.com/en-us/windows/desktop/api/bcrypt/) + (windows only) * Google protobuf 2.6.1+ +* Google [webrtc](https://opensource.google/projects/webrtc) is used for + NAT piercing (ICE) for P2P connections. The relevant code is linked in as a + git submodule. You'll need to initialize that submodule to compile. ## Linux @@ -38,14 +43,7 @@ Arch Linux: ### Building -Using Meson: - -``` -$ meson . build -$ ninja -C build -``` - -Or CMake: +Using CMake (preferred): ``` $ mkdir build @@ -56,8 +54,7 @@ $ ninja ## Windows / Visual Studio -On Windows, you can use the [vcpkg](https://github.com/microsoft/vcpkg/) package manager. Or setup the -dependencies by hand, which is a bit of an arduous gauntlet. +On Windows, you can use the [vcpkg](https://github.com/microsoft/vcpkg/) package manager. ### vcpkg @@ -77,6 +74,8 @@ To use libsodium as the crypto backend rather than OpenSSL, install `gamenetwork ### Manual setup +Setting up the dependencies by hand is a bit of an arduous gauntlet. + #### OpenSSL You can install the [OpenSSL binaries](https://slproweb.com/products/Win32OpenSSL.html) @@ -224,7 +223,6 @@ a 32-bit build, install the i686 versions of these packages): $ pacman -S \ git \ mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-meson \ mingw-w64-x86_64-openssl \ mingw-w64-x86_64-pkg-config \ mingw-w64-x86_64-protobuf @@ -235,8 +233,10 @@ And finally, clone the repository and build it: ``` $ git clone https://github.com/ValveSoftware/GameNetworkingSockets.git $ cd GameNetworkingSockets -$ meson . build -$ ninja -C build +$ mkdir build +$ cd build +$ cmake -G Ninja .. +$ ninja ``` **NOTE:** When building with MSYS2, be sure you launch the correct version of @@ -262,9 +262,3 @@ This extension allows for configuring the CMake project and building it from within the Visual Studio Code IDE. VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=vector-of-bool.cmake-tools - -### Meson by Ali Sabil -This extension comes in handy if you're editing the Meson build files. - -VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=asabil.meson - diff --git a/CMakeLists.txt b/CMakeLists.txt index 38e1176..68974c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,11 @@ include(CMakeDependentOption) include(CMakePushCheckState) include(CheckSymbolExists) +# Put all the output from all projects into the same folder +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + project(GameNetworkingSockets C CXX) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) @@ -140,9 +145,7 @@ endif() add_subdirectory(src) -message(STATUS "---------------------------------------------------------") +#message(STATUS "---------------------------------------------------------") message(STATUS "Crypto library for AES/SHA256: ${USE_CRYPTO}") message(STATUS "Crypto library for ed25519/curve25519: ${USE_CRYPTO25519}") -message(STATUS "---------------------------------------------------------") - -# vim: set ts=4 sts=4 sw=4 noet: +#message(STATUS "---------------------------------------------------------") diff --git a/README.md b/README.md index eda44c1..ec50a57 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,11 @@ to facilitate binding to other languages. Third party language bindings: -* C#: -* Go: +* C#: + * + * +* Go: + * ## Roadmap diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index a798ec9..196f16b 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,6 +1,29 @@ cmake_minimum_required(VERSION 3.5) project(gns_example C CXX) +find_library(golang REQUIRED) + +# +# Trivial signaling server, written in go +# +set(SIGNAL_SERVER_TARGET trivial_signaling_server) +set(SIGNAL_SERVER_OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/trivial_signaling_server) +set(SIGNAL_SERVER_SRCS + trivial_signaling_server.go +) +add_custom_command( + OUTPUT ${SIGNAL_SERVER_OUTPUT} + DEPENDS ${SIGNAL_SERVER_SRCS} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Building GO Trivial signaling server" + COMMAND env go build -o "${SIGNAL_SERVER_OUTPUT}" ${CMAKE_GO_FLAGS} ${SIGNAL_SERVER_SRCS} +) +add_custom_target(${SIGNAL_SERVER_TARGET} ALL DEPENDS ${SIGNAL_SERVER_OUTPUT}) + +# +# Really simple client/server chat +# + add_executable( example_chat example_chat.cpp) @@ -15,5 +38,3 @@ target_link_libraries(example_chat GameNetworkingSockets::GameNetworkingSockets) if(COMMAND add_sanitizers) add_sanitizers(example_chat) endif() - -# vim: set ts=4 sts=4 sw=4 noet: diff --git a/examples/meson.build b/examples/meson.build deleted file mode 100644 index 5409767..0000000 --- a/examples/meson.build +++ /dev/null @@ -1,12 +0,0 @@ -cxx_compiler = meson.get_compiler('cpp') - -dependencies = [ - dep_threads, - dep_GameNetworkingSockets_so # Declared in the other project -] - -executable('example_chat', - 'example_chat.cpp', - dependencies: dependencies, -) - diff --git a/examples/trivial_signaling_client.cpp b/examples/trivial_signaling_client.cpp new file mode 100644 index 0000000..8ddc171 --- /dev/null +++ b/examples/trivial_signaling_client.cpp @@ -0,0 +1,363 @@ +// Client of our dummy trivial signaling server service. +// Serves as an example of you how to hook up signaling server +// to SteamNetworkingSockets P2P connections + +#include +#include +#include + +#include "trivial_signaling_client.h" +#include +#include + +#include "../tests/test_common.h" + +#ifdef POSIX + #include + #include + #include + #include + #include + typedef int SOCKET; + constexpr SOCKET INVALID_SOCKET = -1; + inline void closesocket( SOCKET s ) { close(s); } + inline int GetSocketError() { return errno; } +#endif +#ifdef _WIN32 +#endif + +inline int HexDigitVal( char c ) +{ + if ( '0' <= c && c <= '9' ) + return c - '0'; + if ( 'a' <= c && c <= 'f' ) + return c - 'a' + 0xa; + if ( 'A' <= c && c <= 'F' ) + return c - 'A' + 0xa; + return -1; +} + +/// Implementation of ITrivialSignalingClient +class CTrivialSignalingClient : public ITrivialSignalingClient +{ + + // This is the thing we'll actually create to send signals for a particular + // connection. + struct ConnectionSignaling : ISteamNetworkingConnectionCustomSignaling + { + CTrivialSignalingClient *const m_pOwner; + std::string const m_sPeerIdentity; // Save off the string encoding of the identity we're talking to + + ConnectionSignaling( CTrivialSignalingClient *owner, const char *pszPeerIdentity ) + : m_pOwner( owner ) + , m_sPeerIdentity( pszPeerIdentity ) + { + } + + // + // Implements ISteamNetworkingConnectionCustomSignaling + // + + // This is called from SteamNetworkingSockets to send a signal. This could be called from any thread, + // so we need to be threadsafe, and avoid duoing slow stuff or calling back into SteamNetworkingSockets + virtual bool SendSignal( HSteamNetConnection hConn, const SteamNetConnectionInfo_t &info, const void *pMsg, int cbMsg ) override + { + + // We'll use a dumb hex encoding. + std::string signal; + signal.reserve( m_sPeerIdentity.length() + cbMsg*2 + 4 ); + signal.append( m_sPeerIdentity ); + signal.push_back( ' ' ); + for ( const uint8_t *p = (const uint8_t *)pMsg ; cbMsg > 0 ; --cbMsg, ++p ) + { + static const char hexdigit[] = "0123456789abcdef"; + signal.push_back( hexdigit[ *p >> 4U ] ); + signal.push_back( hexdigit[ *p & 0xf ] ); + } + signal.push_back('\n'); + + m_pOwner->Send( signal ); + } + + // Self destruct. This will be called by SteamNetworkingSockets when it's done with us. + virtual void Release() override + { + delete this; + } + }; + + sockaddr_storage m_adrServer; + size_t const m_adrServerSize; + ISteamNetworkingSockets *const m_pSteamNetworkingSockets; + std::string m_sGreeting; + + std::mutex sockMutex; + SOCKET m_sock; + std::string m_sBufferedData; + + void CloseSocket() + { + if ( m_sock != INVALID_SOCKET ) + { + closesocket( m_sock ); + m_sock = INVALID_SOCKET; + } + m_sBufferedData.clear(); + } + + void Connect() + { + CloseSocket(); + + int sockType = SOCK_STREAM; + #ifdef LINUX + sockType |= SOCK_CLOEXEC; + #endif + #if !defined( _WIN32 ) + sockType |= SOCK_NONBLOCK; + #endif + m_sock = socket( m_adrServer.ss_family, sockType, IPPROTO_TCP ); + if ( m_sock == INVALID_SOCKET ) + { + TEST_Printf( "socket() failed, error=%d\n", GetSocketError() ); + return; + } + #ifdef _WIN32 + opt = 1; + if ( ioctlsocket( sock, FIONBIO, (unsigned long*)&opt ) == -1 ) + { + CloseSocket(); + TEST_Printf( "ioctlsocket() failed, error=%d\n", GetSocketError() ); + return; + } + #endif + + connect( m_sock, (const sockaddr *)&m_adrServer, (socklen_t )m_adrServerSize ); + } + +public: + CTrivialSignalingClient( const sockaddr *adrServer, size_t adrServerSize, ISteamNetworkingSockets *pSteamNetworkingSockets ) + : m_adrServerSize( adrServerSize ), m_pSteamNetworkingSockets( pSteamNetworkingSockets ) + { + memcpy( &m_adrServer, adrServer, adrServerSize ); + m_sock = INVALID_SOCKET; + + // Save off our identity + SteamNetworkingIdentity identitySelf; identitySelf.Clear(); + pSteamNetworkingSockets->GetIdentity( &identitySelf ); + assert( !identitySelf.IsInvalid() ); + assert( !identitySelf.IsLocalHost() ); // We need something more specific than that + m_sGreeting = SteamNetworkingIdentityRender( identitySelf ).c_str(); + assert( strchr( m_sGreeting.c_str(), ' ' ) == nullptr ); // Our protocol is dumb and doesn't support this + m_sGreeting.push_back( '\n' ); + + // Begin connecting immediately + Connect(); + } + + // Send the signal. + void Send( const std::string &s ) + { + assert( s.length() > 0 && s[ s.length()-1 ] == '\n' ); // All of our signals are '\n'-terminated + + sockMutex.lock(); + if ( m_sock != INVALID_SOCKET ) + { + int l = s.length(); + int r = ::send( m_sock, s.c_str(), s.length(), 0 ); + if ( r != l && r != 0 ) + { + // Socket hosed, or we sent a partial signal. + // We need to restart connection + TEST_Printf( "Failed to send %d bytes to trivial signaling server. send() returned %d, errno=%d. Closing and restarting connection.\n", + l, r, GetSocketError() ); + CloseSocket(); + } + } + sockMutex.unlock(); + } + + // + // Implements ISteamNetworkingCustomSignalingService + // + virtual ISteamNetworkingConnectionCustomSignaling *CreateSignalingForConnection( + const SteamNetworkingIdentity &identityPeer, + const char *pszRoutingInfo, + SteamNetworkingErrMsg &errMsg + ) override { + SteamNetworkingIdentityRender sIdentityPeer( identityPeer ); + + // FIXME - here we really ouight to confirm that the string version of the + // identity does not have spaces, since our protocol doesn't permit it. + + return new ConnectionSignaling( this, sIdentityPeer.c_str() ); + } + + virtual void Poll() override + { + // Drain the socket into the buffer, and check for reconnecting + sockMutex.lock(); + if ( m_sock == INVALID_SOCKET ) + { + Connect(); + } + else + { + for (;;) + { + char buf[256]; + int r = recv( m_sock, buf, sizeof(buf), 0 ); + if ( r == 0 ) + break; + if ( r < 0 ) + { + int e = GetSocketError(); + if ( e != EAGAIN && e != EWOULDBLOCK ) + { + TEST_Printf( "Failed to recv from trivial signaling server. recv() returned %d, errno=%d. Closing and restarting connection\n", r, e ); + CloseSocket(); + } + break; + } + + m_sBufferedData.append( buf, r ); + } + } + + // Release the lock now. See the notes below about why it's very important + // to release the lock early and not hold it while we try to dispatch the + // received callbacks. + sockMutex.unlock(); + + // Now dispatch any buffered signals + for (;;) + { + + // Find end of line. Do we have a complete signal? + size_t l = m_sBufferedData.find( '\n' ); + if ( l == std::string::npos ) + break; + + // Locate the space that seperates [from] [payload] + size_t spc = m_sBufferedData.find( ' ' ); + if ( spc != std::string::npos && spc < l ) + { + + // Hex decode the payload. As it turns out, we actually don't + // need the sender's identity. The payload has everything needed + // to process the message. Maybe we should remove it from our + // dummy signaling protocol? It might be useful for debugging, tho. + std::string data; data.reserve( ( l - spc ) / 2 ); + for ( size_t i = spc+1 ; i+2 <= l ; i += 2 ) + { + int h = HexDigitVal( m_sBufferedData[i] ); + int l = HexDigitVal( m_sBufferedData[i+1] ); + if ( ( h | l ) & ~0xf ) + { + // Failed hex decode. Not a bug in our code here, but this is just example code, so we'll handle it this way + assert( !"Failed hex decode from signaling server?!" ); + goto next_message; + } + data.push_back( (char)(h<<4 | l ) ); + } + + // Setup a context object that can respond if this signal is a connection request. + struct Context : ISteamNetworkingCustomSignalingRecvContext + { + CTrivialSignalingClient *m_pOwner; + + virtual ISteamNetworkingConnectionCustomSignaling *OnConnectRequest( + HSteamNetConnection hConn, + const SteamNetworkingIdentity &identityPeer, + int nLocalVirtualPort + ) override { + + // We will just always handle requests thorugh the usual listen socket state + // machine. See the docuemntation for this function for other behaviour we + // might take. + + // Also, note that if there was routing/session info, it should have been in + // our envelope that we know how to parse, and we should save it off in this + // context object. + SteamNetworkingErrMsg ignoreErrMsg; + return m_pOwner->CreateSignalingForConnection( identityPeer, nullptr, ignoreErrMsg ); + } + + virtual void SendRejectionSignal( + const SteamNetworkingIdentity &identityPeer, + const void *pMsg, int cbMsg + ) override { + + // We'll just silently ignore all failures. This is actually the more secure + // Way to handle it in many cases. Actively returning failure might allow + // an attacker to just scrape random peers to see who is online. If you know + // the peer has a good reason for trying to connect, sending an active failure + // can improve error handling and the UX, instead of relying on timeout. But + // just consider the security implications. + } + }; + Context context; + context.m_pOwner = this; + + // Dispatch. + // Remember: From inside this function, our context object might get callbacks. + // And we might get asked to send signals, either now, or really at any time + // from any thread! If possible, avoid calling this function while holding locks. + // To process this call, SteamnetworkingSockets will need take its own internal lock. + // That lock may be held by another thread that is asking you to send a signal! So + // be warned that deadlocks are a possibility here. + m_pSteamNetworkingSockets->ReceivedP2PCustomSignal( data.c_str(), data.length(), &context ); + } + +next_message: + m_sBufferedData.erase( 0, l ); + } + } + + virtual void Release() + { + // NOTE: Here we are assuming that the calling code has already cleaned + // up all the connections, to keep the example simple. + CloseSocket(); + } +}; + +// Start connecting to the signaling server. +ITrivialSignalingClient *CreateTrivialSignalingClient( + const char *pszServerAddress, // Address of the server. + ISteamNetworkingSockets *pSteamNetworkingSockets, // Where should we send signals when we get them? + SteamNetworkingErrMsg &errMsg // Error message is retjrned here if we fail +) { + + std::string sAddress( pszServerAddress ); + std::string sService; + size_t colon = sAddress.find( ':' ); + if ( colon == std::string::npos ) + { + sService = "10000"; // Default port + } + else + { + sService = sAddress.substr( colon+1 ); + sAddress.erase( colon ); + } + + // Resolve name synchronously + addrinfo *pAddrInfo = nullptr; + int r = getaddrinfo( sAddress.c_str(), sService.c_str(), nullptr, &pAddrInfo ); + if ( r != 0 || pAddrInfo == nullptr ) + { + sprintf( errMsg, "Invalid/unknown server address. getaddrinfo returned %d", r ); + return nullptr; + } + + auto *pClient = new CTrivialSignalingClient( pAddrInfo->ai_addr, pAddrInfo->ai_addrlen, pSteamNetworkingSockets ); + + freeaddrinfo( pAddrInfo ); + + return pClient; +} + + + + diff --git a/examples/trivial_signaling_client.h b/examples/trivial_signaling_client.h new file mode 100644 index 0000000..af4737e --- /dev/null +++ b/examples/trivial_signaling_client.h @@ -0,0 +1,57 @@ +// Client of our dummy trivial signaling server service. +// Serves as an example of you how to hook up signaling server +// to SteamNetworkingSockets P2P connections + +#pragma once + +#include +class ISteamNetworkingSockets; + +// FIXME - Eventually I intend to add a mechanism to set the default +// signaling service, so that SteamnetworkingSockets can +// initiate creation of signaling sessions. This will be the +// interface used for that. +/// Interface used to create signaling sessions to particular peers. +/// Typically this represents a connection to some service. +class ISteamNetworkingCustomSignalingService +{ +public: + + /// Setup a session for sending signals for a particular connection. + /// The signals will always be sent to the same peer. + /// + /// pszSessionRoutingInfo is reserved for future use, it will always + /// be NULL right now. + /// + /// On failure, return NULL + virtual ISteamNetworkingConnectionCustomSignaling *CreateSignalingForConnection( + const SteamNetworkingIdentity &identityPeer, + const char *pszRoutingInfo, + SteamNetworkingErrMsg &errMsg + ) = 0; +}; + +/// Interface to our client. +class ITrivialSignalingClient : public ISteamNetworkingCustomSignalingService +{ +public: + + /// Poll the server for incoming signals and dispatch them. + /// We use polling in this example just to keep it simple. + /// You could use a service thread. + virtual void Poll() = 0; + + /// Disconnect from the server and close down our polling thread. + virtual void Release() = 0; +}; + +// Start connecting to the signaling server. +ITrivialSignalingClient *CreateTrivialSignalingClient( + const char *address, // Address:port + ISteamNetworkingSockets *pSteamNetworkingSockets, // Where should we send signals when we get them? + SteamNetworkingErrMsg &errMsg // Error message is retjrned here if we fail +); + + + + diff --git a/examples/trivial_signaling_server.go b/examples/trivial_signaling_server.go new file mode 100644 index 0000000..005f006 --- /dev/null +++ b/examples/trivial_signaling_server.go @@ -0,0 +1,164 @@ +// Really simple P2P signaling server. +// +// When establishing peer-to-peer connections, the peers +// need some sort of pre-arranged side channels that they +// can use to exchange messages. This channel is assumed +// to be relatively low bandwidth and high latency. This +// service is often called "signaling". +// +// This server has the following really simple protocol: +// It listens on a particular TCP port. Clients connect +// raw TCP. The protocol is text-based and line oriented, +// so it is easy to test using telnet. When a client +// connects, it should send its identity on the first line. +// Afterwards, clients can send a message to a peer by +// sending a line formatted as follows: +// +// DESTINATION_IDENTITY PAYLOAD +// +// Identites may not contain spaces, and the payload +// should be plain ASCII text. (Hex or base64 encode it). +// +// If there is a client with that destination identity, +// then the server will forward the message on. Otherwise +// it is discarded. +// +// Forwarded messages have basically the same format and +// are the only type of message the server ever sends to the +// client. The only difference is that the identity is the +// identity of the sender. +// +// This is just an example code to illustrate what a +// signaling service is. A real production server would +// probably need to be able to scale across multiple +// processes, and provide authentication and rate +// limiting. +// +// Note that SteamNetworkingSockets use of signaling +// service does NOT assume guaranteed delivery. + +package main + +import ( + "strings" + "fmt" + "net" + "flag" + "bufio" + "log" +) + +const DEFAULT_LISTEN_PORT = 10000 + +// Current list of client connections +var g_mapClientConnections = make(map[string]net.Conn) + +// Goroutine to service a client connection +func ServiceConnection(conn net.Conn) { + + // Save off address + addr := conn.RemoteAddr().String() + + // Attach a Reader object to the connection, so we can read from it easily + in := bufio.NewReader(conn) + + // In our trivial protocol, the first line contains the client identity + // on a line by itself + intro,err := in.ReadString('\n') + if err != nil { + log.Printf( "[%s] Aborting connection before we ever received client identity", addr ) + conn.Close() + } + identity := strings.TrimSpace(intro) + + // Amnnnnnd that's it. No authentication. + + // Locate existing connection, if any. + existingConn := g_mapClientConnections[identity] + + // Add us to map or replace existing entry + g_mapClientConnections[identity] = conn + + // Now handle existing entry + if existingConn != nil { + log.Printf( "[%s@%s] Closing connection to make room for new connection from '%s'", identity, existingConn.RemoteAddr().String(), addr ) + existingConn.Close() + } + log.Printf( "[%s@%s] Added connection", identity, addr ) + + // Keep reading until connection is closed + for { + line,err := in.ReadString('\n') + if err != nil { + conn.Close() + + // Are we stil in the map? + if g_mapClientConnections[identity] == conn { + log.Printf( "[%s@%s] Connecton closed. %s", identity, addr, err ) + delete(g_mapClientConnections, identity) + } else { + // Assume it's because we got replaced by another connection. + // The other connection already logged, so don't do anything here + } + break; + } + + // Our protocol is just [destination peer identity] [payload] + // And everything is in text. + dest_and_msg := strings.SplitN(line, " ", 2) + if len(dest_and_msg) != 2 { + log.Printf("[%s@%s] Ignoring weird input '%s' (maybe truncated?)", identity, addr, line) + continue + } + dest_identity := strings.TrimSpace(dest_and_msg[0]); + payload := dest_and_msg[1] + + // Locate the destination peer's connection. + dest_conn := g_mapClientConnections[dest_identity] + if dest_conn == nil { + log.Printf("[%s@%s] Ignoring, destination peer '%s' not found", identity, addr, dest_identity) + continue + } + + // Format new message, putting the sender's identity in front. + msg := identity + " " + payload + + // Send to the peer + dest_conn.Write([]byte(msg)) + + // Lawg + log.Printf("[%s@%s] -> %s (%d chars)", identity, addr, dest_identity, len(payload)) + } + +} + +// Main entry point +func main() { + + // Parse command line flags + port := flag.Int("port", DEFAULT_LISTEN_PORT, "Port to listen on") + flag.Parse() + listen_addr := fmt.Sprintf("0.0.0.0:%d", *port) + + // Start listening + listener, err := net.Listen("tcp", listen_addr) + if err != nil { + log.Panic(err) + } + log.Printf("Listening at %s", listen_addr) + + // Main loop + for { + + // Wait for the next incoming connection + conn,err := listener.Accept() + if err != nil { + log.Panic(err) + } + + // Start goroutine to service it + go ServiceConnection(conn) + } + +} + diff --git a/include/steam/isteamnetworkingsockets.h b/include/steam/isteamnetworkingsockets.h index f97e5b0..a66739c 100644 --- a/include/steam/isteamnetworkingsockets.h +++ b/include/steam/isteamnetworkingsockets.h @@ -557,9 +557,9 @@ public: /// NOTE: The routing blob returned here is not encrypted. Send it to your backend /// and don't share it directly with clients. virtual EResult GetGameCoordinatorServerLogin( SteamDatagramGameCoordinatorServerLogin *pLoginInfo, int *pcbSignedBlob, void *pBlob ) = 0; - #endif // #ifndef STEAMNETWORKINGSOCKETS_ENABLE_SDR + // // Relayed connections using custom signaling protocol // @@ -661,87 +661,15 @@ protected: }; #define STEAMNETWORKINGSOCKETS_INTERFACE_VERSION "SteamNetworkingSockets009" -/// Interface used to send signaling messages for a particular connection. -/// You will need to construct one of these per connection. -/// -/// - For connections initiated locally, you will construct it and pass -/// it to ISteamNetworkingSockets::ConnectP2PCustomSignaling. -/// - For connections initiated remotely and "accepted" locally, you -/// will return it from ISteamNetworkingCustomSignalingRecvContext::OnConnectRequest -class ISteamNetworkingConnectionCustomSignaling -{ -public: - /// Called to send a rendezvous message to the remote peer. This may be called - /// from any thread, at any time, so you need to be thread-safe! Don't take - /// any locks that might hold while calling into SteamNetworkingSockets functions, - /// because this could lead to deadlocks. - /// - /// Note that when initiating a connection, we may not know the identity - /// of the peer, if you did not specify it in ConnectP2PCustomSignaling. - /// - /// Return true if a best-effort attempt was made to deliver the message. - /// If you return false, it is assumed that the situation is fatal; - /// the connection will be closed, and Release() will be called - /// eventually. - /// - /// Signaling objects will not be shared between connections. - /// You can assume that the same value of hConn will be used - /// every time. - virtual bool SendSignal( HSteamNetConnection hConn, const SteamNetConnectionInfo_t &info, const void *pMsg, int cbMsg ) = 0; - - /// Called when the connection no longer needs to send signals. - /// Note that this happens eventually (but not immediately) after - /// the connection is closed. Signals may need to be sent for a brief - /// time after the connection is closed, to clean up the connection. - virtual void Release() = 0; -}; - -/// Interface used when a custom signal is received. -/// See ISteamNetworkingSockets::ReceivedP2PCustomSignal -class ISteamNetworkingCustomSignalingRecvContext -{ -public: - - /// Called when the signal represents a request for a new connection. - /// - /// If you want to ignore the request, just return NULL. In this case, - /// the peer will NOT receive any reply. You should consider ignoring - /// requests rather than actively rejecting them, as a security measure. - /// If you actively reject requests, then this makes it possible to detect - /// if a user is online or not, just by sending them a request. - /// - /// If you wish to send back a rejection, then use - /// ISteamNetworkingSockets::CloseConnection() and then return NULL. - /// We will marshal a properly formatted rejection signal and - /// call SendRejectionSignal() so you can send it to them. - /// - /// If you return a signaling object, the connection is NOT immediately - /// accepted by default. Instead, it stays in the "connecting" state, - /// and the usual callback is posted, and your app can accept the - /// connection using ISteamNetworkingSockets::AcceptConnection. This - /// may be useful so that these sorts of connections can be more similar - /// to your application code as other types of connections accepted on - /// a listen socket. If this is not useful and you want to skip this - /// callback process and immediately accept the connection, call - /// ISteamNetworkingSockets::AcceptConnection before returning the - /// signaling object. - /// - /// After accepting a connection (through either means), the connection - /// will transition into the "finding route" state. - virtual ISteamNetworkingConnectionCustomSignaling *OnConnectRequest( HSteamNetConnection hConn, const SteamNetworkingIdentity &identityPeer, int nLocalVirtualPort ) = 0; - - /// This is called actively communication rejection or failure - /// to the incoming message. If you intend to ignore all incoming requests - /// that you do not wish to accept, then it's not strictly necessary to - /// implement this. - virtual void SendRejectionSignal( const SteamNetworkingIdentity &identityPeer, const void *pMsg, int cbMsg ) = 0; -}; // Global accessor. #if defined( STEAMNETWORKINGSOCKETS_PARTNER ) - // Standalone lib - STEAMNETWORKINGSOCKETS_INTERFACE ISteamNetworkingSockets *SteamNetworkingSockets(); - STEAMNETWORKINGSOCKETS_INTERFACE ISteamNetworkingSockets *SteamGameServerNetworkingSockets(); + // Standalone lib. Use different symbol name, so that we can dynamically switch between steamclient.dll + // and the standalone lib + STEAMNETWORKINGSOCKETS_INTERFACE ISteamNetworkingSockets *SteamNetworkingSockets_Lib(); + STEAMNETWORKINGSOCKETS_INTERFACE ISteamNetworkingSockets *SteamGameServerNetworkingSockets_Lib(); + inline ISteamNetworkingSockets *SteamNetworkingSockets() { return SteamNetworkingSockets_Lib(); } + inline ISteamNetworkingSockets *SteamGameServerNetworkingSockets() { return SteamGameServerNetworkingSockets_Lib(); } #elif defined( STEAMNETWORKINGSOCKETS_OPENSOURCE ) || defined( STEAMNETWORKINGSOCKETS_STREAMINGCLIENT ) diff --git a/include/steam/steamnetworkingcustomsignaling.h b/include/steam/steamnetworkingcustomsignaling.h new file mode 100644 index 0000000..e95be93 --- /dev/null +++ b/include/steam/steamnetworkingcustomsignaling.h @@ -0,0 +1,92 @@ +//====== Copyright Valve Corporation, All rights reserved. ==================== +// +// Interfaces needed to implement your own P2P signaling service. If you +// aren't using P2P connections, or you can use the default service provided +// by the platform (e.g. a typical Steam game), then you don't need anything +// in this file. +// +//============================================================================= + +#ifndef STEAMNETWORKINGCUSTOMSIGNALING +#define STEAMNETWORKINGCUSTOMSIGNALING +#pragma once + +#include "steamnetworkingtypes.h" + +/// Interface used to send signaling messages for a particular connection. +/// +/// - For connections initiated locally, you will construct it and pass +/// it to ISteamNetworkingSockets::ConnectP2PCustomSignaling. +/// - For connections initiated remotely and "accepted" locally, you +/// will return it from ISteamNetworkingCustomSignalingRecvContext::OnConnectRequest +class ISteamNetworkingConnectionCustomSignaling +{ +public: + /// Called to send a rendezvous message to the remote peer. This may be called + /// from any thread, at any time, so you need to be thread-safe! Don't take + /// any locks that might hold while calling into SteamNetworkingSockets functions, + /// because this could lead to deadlocks. + /// + /// Note that when initiating a connection, we may not know the identity + /// of the peer, if you did not specify it in ConnectP2PCustomSignaling. + /// + /// Return true if a best-effort attempt was made to deliver the message. + /// If you return false, it is assumed that the situation is fatal; + /// the connection will be closed, and Release() will be called + /// eventually. + /// + /// Signaling objects will not be shared between connections. + /// You can assume that the same value of hConn will be used + /// every time. + virtual bool SendSignal( HSteamNetConnection hConn, const SteamNetConnectionInfo_t &info, const void *pMsg, int cbMsg ) = 0; + + /// Called when the connection no longer needs to send signals. + /// Note that this happens eventually (but not immediately) after + /// the connection is closed. Signals may need to be sent for a brief + /// time after the connection is closed, to clean up the connection. + virtual void Release() = 0; +}; + +/// Interface used when a custom signal is received. +/// See ISteamNetworkingSockets::ReceivedP2PCustomSignal +class ISteamNetworkingCustomSignalingRecvContext +{ +public: + + /// Called when the signal represents a request for a new connection. + /// + /// If you want to ignore the request, just return NULL. In this case, + /// the peer will NOT receive any reply. You should consider ignoring + /// requests rather than actively rejecting them, as a security measure. + /// If you actively reject requests, then this makes it possible to detect + /// if a user is online or not, just by sending them a request. + /// + /// If you wish to send back a rejection, then use + /// ISteamNetworkingSockets::CloseConnection() and then return NULL. + /// We will marshal a properly formatted rejection signal and + /// call SendRejectionSignal() so you can send it to them. + /// + /// If you return a signaling object, the connection is NOT immediately + /// accepted by default. Instead, it stays in the "connecting" state, + /// and the usual callback is posted, and your app can accept the + /// connection using ISteamNetworkingSockets::AcceptConnection. This + /// may be useful so that these sorts of connections can be more similar + /// to your application code as other types of connections accepted on + /// a listen socket. If this is not useful and you want to skip this + /// callback process and immediately accept the connection, call + /// ISteamNetworkingSockets::AcceptConnection before returning the + /// signaling object. + /// + /// After accepting a connection (through either means), the connection + /// will transition into the "finding route" state. + virtual ISteamNetworkingConnectionCustomSignaling *OnConnectRequest( HSteamNetConnection hConn, const SteamNetworkingIdentity &identityPeer, int nLocalVirtualPort ) = 0; + + /// This is called to actively communicate rejection or failure + /// to the incoming message. If you intend to ignore all incoming requests + /// that you do not wish to accept, then it's not strictly necessary to + /// implement this. + virtual void SendRejectionSignal( const SteamNetworkingIdentity &identityPeer, const void *pMsg, int cbMsg ) = 0; +}; + +#endif // STEAMNETWORKINGCUSTOMSIGNALING + diff --git a/meson.build b/meson.build deleted file mode 100644 index 1a5e4d3..0000000 --- a/meson.build +++ /dev/null @@ -1,121 +0,0 @@ -project('GameNetworkingSockets', 'cpp', 'c', - default_options: [ - 'buildtype=debugoptimized', - ], - license: 'BSD' -) - -flags_common = [ - '-fno-strict-aliasing', - '-fvisibility=hidden', -] - -target_os = target_machine.system() -if target_os == 'windows' - flags_common += [ - '-fno-stack-protector', - ] -else - flags_common += [ - '-fstack-protector-strong', - '-fstack-clash-protection', - ] -endif - -flags_cxx = [ - '-fno-rtti', - '-fno-exceptions', - '-fvisibility-inlines-hidden', -] - -warn_flags_common = [ - '-Wall', - '-Wextra', - - # Explicitly disable noisy warnings - '-Wno-documentation', - '-Wno-implicit-fallthrough', - '-Wno-padded', - '-Wno-sign-compare', - '-Wno-sign-conversion', - '-Wno-signed-enum-bitfield', - '-Wno-unknown-pragmas', - '-Wno-unused-const-variable', - '-Wno-unused-local-typedef', - '-Wno-unused-parameter', - '-Wno-nested-anon-types', - '-Wno-format-truncation' -] - -werror = get_option('Werror') -if werror - warn_flags_common += [ '-Werror' ] -endif - -use_crypto_25519 = get_option('use_crypto25519') -if use_crypto_25519 == 'Reference' - warn_flags_common += [ '-Wno-unused-function' ] -endif - -warn_flags_c = [ - '-Wimplicit', - '-Wstrict-prototypes', - '-Wno-missing-prototypes', -] - -warn_flags_cxx = [ - '-Wno-c++98-compat', - '-Wno-c++98-compat-pedantic', - '-Wno-exit-time-destructors', - '-Wno-global-constructors', - '-Wno-non-virtual-dtor', - '-Wno-old-style-cast', - '-Wno-reorder', - - # These ones are prolific but don't really matter. Most are in generated - # protobuf code. - '-Wno-zero-as-null-pointer-constant', - '-Wno-missing-variable-declarations', -] - -c_compiler = meson.get_compiler('c') -cxx_compiler = meson.get_compiler('cpp') - -c_flags = [] -c_flags += c_compiler.first_supported_argument(['-std=c11', '-std=gnu99']) -foreach arg : flags_common + warn_flags_common + warn_flags_c - if c_compiler.has_argument(arg) - c_flags += [ arg ] - endif -endforeach -add_project_arguments(c_flags, language: 'c') - -cxx_flags = [] -cxx_flags += cxx_compiler.first_supported_argument(['-std=c++11', '-std=c++0x']) -foreach arg : flags_common + warn_flags_common + flags_cxx + warn_flags_cxx - if cxx_compiler.has_argument(arg) - cxx_flags += [ arg ] - endif -endforeach -add_project_arguments(cxx_flags, language: 'cpp') - -# Preprocessor defines for platform and compiler -cpp_flags = [] -if target_os == 'linux' - cpp_flags += ['-DPOSIX', '-DLINUX'] -elif target_os == 'darwin' - cpp_flags += ['-DPOSIX', '-DOSX'] -elif target_os == 'windows' - cpp_flags += ['-D_WIN32', '-DWIN32', '-D__STDC_FORMAT_MACROS=1', '-D__USE_MINGW_ANSI_STDIO=0'] -else - error('Could not identify your target operating system') -endif - -add_project_arguments(cpp_flags, language: 'c') -add_project_arguments(cpp_flags, language: 'cpp') - -dep_threads = dependency('threads') - -subdir('src') -subdir('tests') -subdir('examples') diff --git a/meson_options.txt b/meson_options.txt deleted file mode 100644 index b48e26a..0000000 --- a/meson_options.txt +++ /dev/null @@ -1,21 +0,0 @@ -option('light_tests', - type: 'boolean', - value: true, - description: 'Use smaller/shorter tests for simple integration testing (e.g. Travis)') - -option('Werror', - type: 'boolean', - value: false, - description: 'Enable -Werror flag when compiling') - -option('use_crypto', - type: 'combo', - value: 'OpenSSL', - description: 'Crypto library to use for AES/SHA256', - choices: ['OpenSSL', 'libsodium', 'BCrypt']) - -option('use_crypto25519', - type: 'combo', - value: 'OpenSSL', - description: 'Crypto library to use for ed25519/curve25519', - choices: ['OpenSSL', 'libsodium', 'Reference']) diff --git a/src/meson.build b/src/meson.build deleted file mode 100644 index a042db2..0000000 --- a/src/meson.build +++ /dev/null @@ -1,165 +0,0 @@ -dependencies = [] - -c_compiler = meson.get_compiler('c') - -protoc_bin = find_program('protoc') -protoc = generator(protoc_bin, - output : ['@BASENAME@.pb.cc', '@BASENAME@.pb.h'], - arguments : ['-I@CURRENT_SOURCE_DIR@/common', '--proto_path=@CURRENT_SOURCE_DIR@', '--cpp_out=@BUILD_DIR@', '@INPUT@']) - -use_crypto = get_option('use_crypto') -use_crypto25519 = get_option('use_crypto25519') - -if use_crypto == 'OpenSSL' or use_crypto25519 == 'OpenSSL' - min_libcrypto = dependency('libcrypto', version: '>=1.1.0') - good_libcrypto = dependency('libcrypto', version: '>=1.1.1', required: false) - - if good_libcrypto.found() - dependencies += [ good_libcrypto ] - else - dependencies += [ min_libcrypto ] - endif - - if use_crypto25519 == 'OpenSSL' and not good_libcrypto.found() - error('This version of OpenSSL does not support ed25519/curve25519. Please use -Duse_crypto25519=Reference or upgrade OpenSSL to 1.1.1 or later.') - endif - - code = '''#include - int main(int argc, char **argv) { - EVP_MD_CTX_free(NULL); - return 0; - } - ''' - result = c_compiler.links(code, dependencies: dependencies, name: 'EVP API') - if not result - error('Your OpenSSL version appears to be too old. Check that you\'re using OpenSSL 1.1.0 or later.') - endif -endif - -if use_crypto == 'libsodium' or use_crypto25519 == 'libsodium' - dependencies += [ dependency('libsodium') ] -endif - -dependencies += [ - dep_threads, - dependency('protobuf', version: '>=3.0.0'), -] - -incdirs = include_directories('.', '../include', 'common', 'public') - -cpp_flags = [ - '-DSTEAMNETWORKINGSOCKETS_FOREXPORT', - '-DVALVE_CRYPTO_ENABLE_25519', - '-DCRYPTO_DISABLE_ENCRYPT_WITH_PASSWORD', - '-DGOOGLE_PROTOBUF_NO_RTTI', -] - -target_os = target_machine.system() - -if target_os == 'windows' - dependencies += [ - c_compiler.find_library('ws2_32'), - ] -endif - -protobuf_sources = [ - 'common/steamnetworkingsockets_messages_certs.proto', - 'common/steamnetworkingsockets_messages.proto', - 'common/steamnetworkingsockets_messages_udp.proto', -] - -sources = [ - 'common/crypto_textencode.cpp', - 'common/keypair.cpp', - 'common/steamid.cpp', - 'steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp', - 'steamnetworkingsockets/clientlib/steamnetworkingsockets_flat.cpp', - 'steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp', - 'steamnetworkingsockets/clientlib/steamnetworkingsockets_lowlevel.cpp', - 'steamnetworkingsockets/clientlib/steamnetworkingsockets_p2p.cpp', - 'steamnetworkingsockets/clientlib/steamnetworkingsockets_snp.cpp', - 'steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.cpp', - 'steamnetworkingsockets/steamnetworkingsockets_certs.cpp', - 'steamnetworkingsockets/steamnetworkingsockets_certstore.cpp', - 'steamnetworkingsockets/steamnetworkingsockets_shared.cpp', - 'steamnetworkingsockets/steamnetworkingsockets_stats.cpp', - 'steamnetworkingsockets/steamnetworkingsockets_thinker.cpp', - 'tier0/dbg.cpp', - 'tier0/platformtime.cpp', - 'tier1/bitstring.cpp', - 'tier1/ipv6text.c', - 'tier1/netadr.cpp', - 'tier1/utlbuffer.cpp', - 'tier1/utlmemory.cpp', - 'vstdlib/strtools.cpp', -] - -if use_crypto == 'OpenSSL' - cpp_flags += [ '-DSTEAMNETWORKINGSOCKETS_CRYPTO_VALVEOPENSSL' ] - sources += [ - 'common/opensslwrapper.cpp', - 'common/crypto_openssl.cpp', - ] -endif - -if use_crypto == 'BCrypt' - dependencies += [ c_compiler.find_library('bcrypt') ] - cpp_flags += [ '-DED25519_HASH_BCRYPT', '-DSTEAMNETWORKINGSOCKETS_CRYPTO_BCRYPT' ] - sources += [ 'common/crypto_bcrypt.cpp' ] -endif - -if use_crypto == 'libsodium' - if target_machine.cpu_family() != 'x86' and target_machine.cpu_family() != 'x86_64' - error('-Duse_crypto=libsodium invalid, libsodium AES implementation only works on x86/x86_64 CPUs') - endif - cpp_flags += [ '-DSTEAMNETWORKINGSOCKETS_CRYPTO_LIBSODIUM' ] - sources += [ 'common/crypto_libsodium.cpp' ] -endif - -if use_crypto25519 == 'OpenSSL' - cpp_flags += [ '-DSTEAMNETWORKINGSOCKETS_CRYPTO_25519_OPENSSL' ] - sources += [ 'common/crypto_25519_openssl.cpp' ] -endif - -if use_crypto25519 == 'libsodium' - cpp_flags += [ '-DSTEAMNETWORKINGSOCKETS_CRYPTO_25519_LIBSODIUM' ] - sources += [ 'common/crypto_25519_libsodium.cpp' ] -endif - -if use_crypto25519 == 'Reference' - cpp_flags += [ '-DVALVE_CRYPTO_25519_DONNA' ] - sources += [ - 'common/crypto_25519_donna.cpp', - 'external/curve25519-donna/curve25519.c', - 'external/curve25519-donna/curve25519_VALVE_sse2.c', - 'external/ed25519-donna/ed25519_VALVE.c', - 'external/ed25519-donna/ed25519_VALVE_sse2.c', - ] -endif - -protobufs = protoc.process(protobuf_sources) - -GameNetworkingSockets_static = static_library('GameNetworkingSockets', - sources, protobufs, - c_args: cpp_flags, - cpp_args: cpp_flags, - include_directories: incdirs, - dependencies: dependencies) - -dep_GameNetworkingSockets_static = declare_dependency( - include_directories: include_directories( '../include', '.' ), - compile_args: ['-DSTEAMNETWORKINGSOCKETS_STATIC_LINK'], - link_with: GameNetworkingSockets_static ) - -GameNetworkingSockets_so = library('GameNetworkingSockets', - sources, protobufs, - c_args: cpp_flags, - cpp_args: cpp_flags, - include_directories: incdirs, - dependencies: dependencies) - -dep_GameNetworkingSockets_so = declare_dependency( - include_directories: include_directories( '../include', '.' ), - link_with: GameNetworkingSockets_so ) - -# vim: set ts=2 sts=2 sw=2 et: diff --git a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_p2p.cpp b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_p2p.cpp index d6c192a..8bd5ccd 100644 --- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_p2p.cpp +++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_p2p.cpp @@ -4,6 +4,10 @@ #include "csteamnetworkingsockets.h" #include "crypto.h" +#ifdef POSIX + #include +#endif + #ifdef STEAMNETWORKINGSOCKETS_ENABLE_SDR #include "steamnetworkingsockets_sdr_p2p.h" #endif diff --git a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_p2p.h b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_p2p.h index efec08f..1749695 100644 --- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_p2p.h +++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_p2p.h @@ -4,6 +4,7 @@ #define STEAMNETWORKINGSOCKETS_P2P_H #pragma once +#include #include "steamnetworkingsockets_connections.h" #include "csteamnetworkingsockets.h" diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0953f00..eea2395 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,11 +7,19 @@ endif() add_executable( test_connection + test_common.cpp test_connection.cpp) +add_executable( + test_p2p + test_common.cpp + test_p2p.cpp + ../examples/trivial_signaling_client.cpp) if(SANITIZE) target_link_libraries(test_connection GameNetworkingSockets_s) + target_link_libraries(test_p2p GameNetworkingSockets_s) else() target_link_libraries(test_connection GameNetworkingSockets) + target_link_libraries(test_p2p GameNetworkingSockets) endif() add_sanitizers(test_connection) @@ -23,6 +31,8 @@ target_include_directories(test_crypto PRIVATE ../src ../src/public ../src/commo target_link_libraries(test_crypto GameNetworkingSockets_s) add_sanitizers(test_crypto) -file(COPY aesgcmtestvectors DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +file(COPY aesgcmtestvectors DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) -# vim: set ts=4 sts=4 sw=4 noet: +# NOTE: This happens when we generate the projects, and really we'd like to actually +# make a build rule to copy it. I don't want to do that work right now. +file(COPY test_p2p.py DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) diff --git a/tests/meson.build b/tests/meson.build deleted file mode 100644 index 7369e65..0000000 --- a/tests/meson.build +++ /dev/null @@ -1,39 +0,0 @@ -common_deps = [ dep_threads ] -cppflags = [ - '-DVALVE_CRYPTO_ENABLE_25519' -] - -light_tests= get_option('light_tests') -if light_tests - cppflags += ['-DLIGHT_TESTS'] -endif - -executable('test_connection', - 'test_connection.cpp', - dependencies: common_deps + [ dep_GameNetworkingSockets_so ], - cpp_args: cppflags, -) - -executable('test_crypto', - 'test_crypto.cpp', - dependencies: common_deps + [ dep_GameNetworkingSockets_static ], - cpp_args: cppflags + ['-DTEST_VECTOR_DIR="' + join_paths(meson.source_root(), 'tests/aesgcmtestvectors') + '/"'], - include_directories: include_directories('../src', '../src/public', '../src/common') -) - -# !FIXME! Ug cannot link with the static lib, because we need to #define the hardcoded key. -# So we'll need the crypto and protobuf dependencies, and those are pretty complicated. -# We need to refactor these files to get that organized. -#executable('test_pki', -# 'test_pki.cpp', -# dependencies: common_deps, -# cpp_args: cppflags + ['-DSTEAMNETWORKINGSOCKETS_HARDCODED_ROOT_CA_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMPsJA3FUaQSsHkyYhWvcYt9v3XbwRBcWuVuayWGdK1"'], -# include_directories: include_directories('../src', '../include', '../src/public', '../src/common') -#) - -#incdirs = include_directories('../include') -#executable('test_flat', -# 'test_flat.c', -# dependence: dependencies, -# include_directories: incdirs -#) diff --git a/tests/test_common.cpp b/tests/test_common.cpp new file mode 100644 index 0000000..a2f52bf --- /dev/null +++ b/tests/test_common.cpp @@ -0,0 +1,120 @@ +// Misc stuff used in the tests + +#include "test_common.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifndef STEAMNETWORKINGSOCKETS_OPENSOURCE +#include +#endif + +static FILE *g_fpLog = nullptr; +static SteamNetworkingMicroseconds g_logTimeZero; + +static void DebugOutput( ESteamNetworkingSocketsDebugOutputType eType, const char *pszMsg ) +{ + SteamNetworkingMicroseconds time = SteamNetworkingUtils()->GetLocalTimestamp() - g_logTimeZero; + if ( g_fpLog ) + fprintf( g_fpLog, "%10.6f %s\n", time*1e-6, pszMsg ); + if ( eType <= k_ESteamNetworkingSocketsDebugOutputType_Msg ) + { + printf( "%10.6f %s\n", time*1e-6, pszMsg ); + fflush(stdout); + } + if ( eType == k_ESteamNetworkingSocketsDebugOutputType_Bug ) + { + fflush(stdout); + fflush(stderr); + if ( g_fpLog ) + fflush( g_fpLog ); + + // !KLUDGE! Our logging (which is done while we hold the lock) + // is occasionally triggering this assert. Just ignroe that one + // error for now. + // Yes, this is a kludge. + if ( strstr( pszMsg, "SteamDatagramTransportLock held for" ) ) + return; + + assert( !"TEST FAILED" ); + } +} + +void TEST_Printf( const char *fmt, ... ) +{ + char text[ 2048 ]; + va_list ap; + va_start( ap, fmt ); + vsprintf( text, fmt, ap ); + va_end(ap); + char *nl = strchr( text, '\0' ) - 1; + if ( nl >= text && *nl == '\n' ) + *nl = '\0'; + DebugOutput( k_ESteamNetworkingSocketsDebugOutputType_Msg, text ); +} + +void TEST_Fatal( const char *fmt, ... ) +{ + fflush(stdout); + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + fflush(stderr); + exit(1); +} + +void TEST_Init( const SteamNetworkingIdentity *pIdentity ) +{ + g_fpLog = fopen( "log.txt", "wt" ); + g_logTimeZero = SteamNetworkingUtils()->GetLocalTimestamp(); + + SteamNetworkingUtils()->SetDebugOutputFunction( k_ESteamNetworkingSocketsDebugOutputType_Debug, DebugOutput ); + //SteamNetworkingUtils()->SetDebugOutputFunction( k_ESteamNetworkingSocketsDebugOutputType_Verbose, DebugOutput ); + //SteamNetworkingUtils()->SetDebugOutputFunction( k_ESteamNetworkingSocketsDebugOutputType_Msg, DebugOutput ); + + #ifdef STEAMNETWORKINGSOCKETS_OPENSOURCE + SteamDatagramErrMsg errMsg; + if ( !GameNetworkingSockets_Init( pIdentity, errMsg ) ) + { + fprintf( stderr, "GameNetworkingSockets_Init failed. %s", errMsg ); + exit(1); + } + #else + //SteamAPI_Init(); + + SteamDatagramClient_SetAppID( 570 ); // Just set something, doesn't matter what + //SteamDatagramClient_SetUniverse( k_EUniverseDev ); + + SteamDatagramErrMsg errMsg; + if ( !SteamDatagramClient_Init( true, errMsg ) ) + { + fprintf( stderr, "SteamDatagramClient_Init failed. %s", errMsg ); + exit(1); + } + #endif +} + +void TEST_Kill() +{ + #ifdef STEAMNETWORKINGSOCKETS_OPENSOURCE + GameNetworkingSockets_Kill(); + #else + SteamDatagramClient_Kill(); + #endif +} + +void TEST_PumpCallbacks() +{ + SteamNetworkingSockets()->RunCallbacks(); + std::this_thread::sleep_for( std::chrono::milliseconds( 2 ) ); +} + diff --git a/tests/test_common.h b/tests/test_common.h new file mode 100644 index 0000000..e36094a --- /dev/null +++ b/tests/test_common.h @@ -0,0 +1,11 @@ +// Misc stuff used in the tests +#pragma once + +struct SteamNetworkingIdentity; + +extern void TEST_Init(); +extern void TEST_Printf( const char *fmt, ... ); +extern void TEST_Fatal( const char *fmt, ... ); +extern void TEST_Init( const SteamNetworkingIdentity *pIdentity ); +extern void TEST_Kill(); +extern void TEST_PumpCallbacks(); diff --git a/tests/test_connection.cpp b/tests/test_connection.cpp index 5c06038..42f287b 100644 --- a/tests/test_connection.cpp +++ b/tests/test_connection.cpp @@ -8,6 +8,8 @@ #include #include +#include "test_common.h" + #include #include #ifndef STEAMNETWORKINGSOCKETS_OPENSOURCE @@ -19,93 +21,6 @@ static std::default_random_engine g_rand; static SteamNetworkingMicroseconds g_usecTestElapsed; -FILE *g_fpLog = nullptr; -SteamNetworkingMicroseconds g_logTimeZero; - -void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t *pInfo ); - -static void DebugOutput( ESteamNetworkingSocketsDebugOutputType eType, const char *pszMsg ) -{ - SteamNetworkingMicroseconds time = SteamNetworkingUtils()->GetLocalTimestamp() - g_logTimeZero; - if ( g_fpLog ) - fprintf( g_fpLog, "%10.6f %s\n", time*1e-6, pszMsg ); - if ( eType <= k_ESteamNetworkingSocketsDebugOutputType_Msg ) - { - printf( "%10.6f %s\n", time*1e-6, pszMsg ); - fflush(stdout); - } - if ( eType == k_ESteamNetworkingSocketsDebugOutputType_Bug ) - { - fflush(stdout); - fflush(stderr); - if ( g_fpLog ) - fflush( g_fpLog ); - - // !KLUDGE! Our logging (which is done while we hold the lock) - // is occasionally triggering this assert. Just ignroe that one - // error for now. - // Yes, this is a kludge. - if ( strstr( pszMsg, "SteamDatagramTransportLock held for" ) ) - return; - - assert( !"TEST FAILED" ); - } -} - -static void Printf( const char *fmt, ... ) -{ - char text[ 2048 ]; - va_list ap; - va_start( ap, fmt ); - vsprintf( text, fmt, ap ); - va_end(ap); - char *nl = strchr( text, '\0' ) - 1; - if ( nl >= text && *nl == '\n' ) - *nl = '\0'; - DebugOutput( k_ESteamNetworkingSocketsDebugOutputType_Msg, text ); -} - -static void InitSteamDatagramConnectionSockets() -{ - g_fpLog = fopen( "log.txt", "wt" ); - g_logTimeZero = SteamNetworkingUtils()->GetLocalTimestamp(); - - SteamNetworkingUtils()->SetDebugOutputFunction( k_ESteamNetworkingSocketsDebugOutputType_Debug, DebugOutput ); - //SteamNetworkingUtils()->SetDebugOutputFunction( k_ESteamNetworkingSocketsDebugOutputType_Verbose, DebugOutput ); - //SteamNetworkingUtils()->SetDebugOutputFunction( k_ESteamNetworkingSocketsDebugOutputType_Msg, DebugOutput ); - SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged( OnSteamNetConnectionStatusChanged ); - - #ifdef STEAMNETWORKINGSOCKETS_OPENSOURCE - SteamDatagramErrMsg errMsg; - if ( !GameNetworkingSockets_Init( nullptr, errMsg ) ) - { - fprintf( stderr, "GameNetworkingSockets_Init failed. %s", errMsg ); - exit(1); - } - #else - //SteamAPI_Init(); - - SteamDatagramClient_SetAppID( 570 ); // Just set something, doesn't matter what - //SteamDatagramClient_SetUniverse( k_EUniverseDev ); - - SteamDatagramErrMsg errMsg; - if ( !SteamDatagramClient_Init( true, errMsg ) ) - { - fprintf( stderr, "SteamDatagramClient_Init failed. %s", errMsg ); - exit(1); - } - #endif -} - -static void ShutdownSteamDatagramConnectionSockets() -{ - #ifdef STEAMNETWORKINGSOCKETS_OPENSOURCE - GameNetworkingSockets_Kill(); - #else - SteamDatagramClient_Kill(); - #endif -} - static HSteamListenSocket g_hSteamListenSocket = k_HSteamListenSocket_Invalid; struct TestMsg @@ -186,7 +101,7 @@ struct SFakePeer if ( result != k_EResultOK ) { - Printf( "***ERROR ON Send: %s %.3f %s message %lld, %d bytes (pending %d bytes)\n", + TEST_Printf( "***ERROR ON Send: %s %.3f %s message %lld, %d bytes (pending %d bytes)\n", m_sName.c_str(), g_usecTestElapsed*1e-6, msg.m_bReliable ? "reliable" : "unreliable", @@ -197,7 +112,7 @@ struct SFakePeer } #if 0 else - Printf( "Send: %s %.3f %s message %lld, %d bytes (pending %d bytes)\n", + TEST_Printf( "Send: %s %.3f %s message %lld, %d bytes (pending %d bytes)\n", connection.m_sName.c_str(), g_usecTestElapsed*1e-6, msg.m_bReliable ? "reliable" : "unreliable", @@ -244,7 +159,7 @@ static void Recv( ISteamNetworkingSockets *pSteamSocketNetworking ) { // Print that it happened. - Printf( + TEST_Printf( "Recv: %s, %s MISMATCH NUM wanted %lld got %lld\n", pConnection->m_sName.c_str(), pTestMsg->m_bReliable ? "RELIABLE" : "UNRELIABLE", @@ -278,7 +193,7 @@ void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_ { case k_ESteamNetworkingConnectionState_ClosedByPeer: case k_ESteamNetworkingConnectionState_ProblemDetectedLocally: - Printf( "Steam Net connection %x %s, reason %d: %s\n", + TEST_Printf( "Steam Net connection %x %s, reason %d: %s\n", pInfo->m_hConn, ( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer ? "closed by peer" : "problem detected locally" ), pInfo->m_info.m_eEndReason, @@ -301,7 +216,7 @@ void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_ /* case k_ESteamNetworkingConnectionState_None: - Printf( "No steam Net connection %x (%s)\n", pInfo->m_hConn, pInfo->m_info.m_steamIDRemote.Render() ); + TEST_Printf( "No steam Net connection %x (%s)\n", pInfo->m_hConn, pInfo->m_info.m_steamIDRemote.Render() ); if ( g_hSteamNetConnection == pInfo->m_hConn ) { @@ -317,7 +232,7 @@ void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_ if ( g_hSteamListenSocket != k_HSteamListenSocket_Invalid && pInfo->m_info.m_hListenSocket == g_hSteamListenSocket ) { // Somebody's knocking - Printf( "[%s] Accepting\n", pInfo->m_info.m_szConnectionDescription ); + TEST_Printf( "[%s] Accepting\n", pInfo->m_info.m_szConnectionDescription ); g_peerServer.m_hSteamNetConnection = pInfo->m_hConn; g_peerServer.m_bIsConnected = true; SteamNetworkingSockets()->AcceptConnection( pInfo->m_hConn ); @@ -331,7 +246,7 @@ void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_ { g_peerClient.m_bIsConnected = true; } - Printf( "[%s] connected\n", pInfo->m_info.m_szConnectionDescription ); + TEST_Printf( "[%s] connected\n", pInfo->m_info.m_szConnectionDescription ); break; @@ -341,15 +256,9 @@ void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_ } } -static void PumpCallbacks() -{ - SteamNetworkingSockets()->RunCallbacks(); - std::this_thread::sleep_for( std::chrono::milliseconds( 2 ) ); -} - static void PumpCallbacksAndMakeSureStillConnected() { - PumpCallbacks(); + TEST_PumpCallbacks(); assert( g_peerClient.m_bIsConnected ); assert( g_peerServer.m_bIsConnected ); assert( g_peerServer.m_hSteamNetConnection != k_HSteamNetConnection_Invalid ); @@ -368,35 +277,35 @@ static void PrintStatus( const SFakePeer &p1, const SFakePeer &p2 ) { const SteamNetworkingQuickConnectionStatus &info1 = p1.m_info; const SteamNetworkingQuickConnectionStatus &info2 = p2.m_info; - Printf( "\n" ); - Printf( "%12s %12s\n", p1.m_sName.c_str(), p2.m_sName.c_str() ); - Printf( "%10dms %10dms Ping\n", info1.m_nPing, info2.m_nPing ); - Printf( "%12s %12s Quality\n", FormatQuality( info1.m_flConnectionQualityLocal ).c_str(), FormatQuality( info2.m_flConnectionQualityLocal ).c_str() ); - Printf( "%11.1fK %11.1fK Send buffer\n", ( info1.m_cbPendingReliable+info1.m_cbPendingUnreliable )/1024.0f, ( info2.m_cbPendingReliable+info2.m_cbPendingUnreliable )/1024.0f ); - Printf( "%11.1fK %11.1fK Send rate (app)\n", p1.m_flSendRate/1024.0f, p2.m_flSendRate/1024.0f ); - Printf( "%11.1fK %11.1fK Send rate (wire)\n", info1.m_flOutBytesPerSec/1024.0f, info2.m_flOutBytesPerSec/1024.0f ); - Printf( "%12.1f %12.1f Send pkts/sec (wire)\n", info1.m_flOutPacketsPerSec, info2.m_flOutPacketsPerSec ); - Printf( "%11.1fK %11.1fK Send bandwidth (estimate)\n", info1.m_nSendRateBytesPerSecond/1024.0f, info2.m_nSendRateBytesPerSecond/1024.0f ); - Printf( "%11.1fK %11.1fK Recv rate (app)\n", p1.m_flRecvRate/1024.0f, p2.m_flRecvRate/1024.0f ); - Printf( "%11.1fK %11.1fK Recv rate (wire)\n", info1.m_flInBytesPerSec/1024.0f, info2.m_flInBytesPerSec/1024.0f ); - Printf( "%12.1f %12.1f Recv pkts/sec (wire)\n", info1.m_flInPacketsPerSec, info2.m_flInPacketsPerSec ); - Printf( "%10.1fms %10.1fms Send buffer drain time, based on bandwidth\n", ( info1.m_cbPendingReliable+info1.m_cbPendingUnreliable )*1000.0f/info1.m_nSendRateBytesPerSecond, ( info2.m_cbPendingReliable+info2.m_cbPendingUnreliable )*1000.0f/info2.m_nSendRateBytesPerSecond ); - Printf( "%10.1fms %10.1fms App RTT (reliable)\n", p1.m_flReliableMsgDelay*1e3, p2.m_flReliableMsgDelay*1e3 ); - Printf( "%10.1fms %10.1fms App RTT (unreliable)\n", p1.m_flUnreliableMsgDelay*1e3, p2.m_flUnreliableMsgDelay*1e3 ); + TEST_Printf( "\n" ); + TEST_Printf( "%12s %12s\n", p1.m_sName.c_str(), p2.m_sName.c_str() ); + TEST_Printf( "%10dms %10dms Ping\n", info1.m_nPing, info2.m_nPing ); + TEST_Printf( "%12s %12s Quality\n", FormatQuality( info1.m_flConnectionQualityLocal ).c_str(), FormatQuality( info2.m_flConnectionQualityLocal ).c_str() ); + TEST_Printf( "%11.1fK %11.1fK Send buffer\n", ( info1.m_cbPendingReliable+info1.m_cbPendingUnreliable )/1024.0f, ( info2.m_cbPendingReliable+info2.m_cbPendingUnreliable )/1024.0f ); + TEST_Printf( "%11.1fK %11.1fK Send rate (app)\n", p1.m_flSendRate/1024.0f, p2.m_flSendRate/1024.0f ); + TEST_Printf( "%11.1fK %11.1fK Send rate (wire)\n", info1.m_flOutBytesPerSec/1024.0f, info2.m_flOutBytesPerSec/1024.0f ); + TEST_Printf( "%12.1f %12.1f Send pkts/sec (wire)\n", info1.m_flOutPacketsPerSec, info2.m_flOutPacketsPerSec ); + TEST_Printf( "%11.1fK %11.1fK Send bandwidth (estimate)\n", info1.m_nSendRateBytesPerSecond/1024.0f, info2.m_nSendRateBytesPerSecond/1024.0f ); + TEST_Printf( "%11.1fK %11.1fK Recv rate (app)\n", p1.m_flRecvRate/1024.0f, p2.m_flRecvRate/1024.0f ); + TEST_Printf( "%11.1fK %11.1fK Recv rate (wire)\n", info1.m_flInBytesPerSec/1024.0f, info2.m_flInBytesPerSec/1024.0f ); + TEST_Printf( "%12.1f %12.1f Recv pkts/sec (wire)\n", info1.m_flInPacketsPerSec, info2.m_flInPacketsPerSec ); + TEST_Printf( "%10.1fms %10.1fms Send buffer drain time, based on bandwidth\n", ( info1.m_cbPendingReliable+info1.m_cbPendingUnreliable )*1000.0f/info1.m_nSendRateBytesPerSecond, ( info2.m_cbPendingReliable+info2.m_cbPendingUnreliable )*1000.0f/info2.m_nSendRateBytesPerSecond ); + TEST_Printf( "%10.1fms %10.1fms App RTT (reliable)\n", p1.m_flReliableMsgDelay*1e3, p2.m_flReliableMsgDelay*1e3 ); + TEST_Printf( "%10.1fms %10.1fms App RTT (unreliable)\n", p1.m_flUnreliableMsgDelay*1e3, p2.m_flUnreliableMsgDelay*1e3 ); } static void TestNetworkConditions( int rate, float loss, int lag, float reorderPct, int reorderLag, bool bActLikeGame ) { ISteamNetworkingSockets *pSteamSocketNetworking = SteamNetworkingSockets(); - Printf( "---------------------------------------------------\n" ); - Printf( "NETWORK CONDITIONS\n" ); - Printf( "Rate . . . . . . : %d Bps\n", rate ); - Printf( "Loss . . . . . . : %g%%\n", loss ); - Printf( "Ping . . . . . . : %d\n", lag*2 ); - Printf( "Reorder. . . . . : %g%% @ %dms\n", reorderPct, reorderLag ); - Printf( "Act like game. . : %d\n", (int)bActLikeGame ); - Printf( "---------------------------------------------------\n" ); + TEST_Printf( "---------------------------------------------------\n" ); + TEST_Printf( "NETWORK CONDITIONS\n" ); + TEST_Printf( "Rate . . . . . . : %d Bps\n", rate ); + TEST_Printf( "Loss . . . . . . : %g%%\n", loss ); + TEST_Printf( "Ping . . . . . . : %d\n", lag*2 ); + TEST_Printf( "Reorder. . . . . : %g%% @ %dms\n", reorderPct, reorderLag ); + TEST_Printf( "Act like game. . : %d\n", (int)bActLikeGame ); + TEST_Printf( "---------------------------------------------------\n" ); SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_SendRateMin, rate ); SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_SendRateMax, rate ); @@ -470,14 +379,14 @@ static void TestNetworkConditions( int rate, float loss, int lag, float reorderP usecWhenStateEnd = g_usecTestElapsed + ( bQuiet ? usecQuietDuration : usecActiveDuration ); if ( nIterations-- <= 0 ) break; - Printf( "Entering active time (sending enabled)\n" ); + TEST_Printf( "Entering active time (sending enabled)\n" ); } } else { bQuiet = true; usecWhenStateEnd = g_usecTestElapsed + usecQuietDuration; - Printf( "Entering quiet time (no sending) to see how fast queues drain\n" ); + TEST_Printf( "Entering quiet time (no sending) to see how fast queues drain\n" ); } } @@ -553,7 +462,7 @@ static void RunSteamDatagramConnectionTest() // Wait for connection to complete while ( !g_peerClient.m_bIsConnected || !g_peerServer.m_bIsConnected ) - PumpCallbacks(); + TEST_PumpCallbacks(); auto Test = []( int rate, float loss, int lag, float reorderPct, int reorderLag ) { @@ -626,12 +535,13 @@ int main( ) TestSteamNetworkingIdentity(); // Create client and server sockets - InitSteamDatagramConnectionSockets(); + TEST_Init( nullptr ); + SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged( OnSteamNetConnectionStatusChanged ); // Run the test RunSteamDatagramConnectionTest(); - ShutdownSteamDatagramConnectionSockets(); + TEST_Kill(); return 0; } diff --git a/tests/test_p2p.cpp b/tests/test_p2p.cpp new file mode 100644 index 0000000..dfb5d33 --- /dev/null +++ b/tests/test_p2p.cpp @@ -0,0 +1,243 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_common.h" + +#include +#include +#include "../examples/trivial_signaling_client.h" + +HSteamListenSocket g_hListenSock; +HSteamNetConnection g_hConnection; +enum ETestRole +{ + k_ETestRole_Undefined, + k_ETestRole_Server, + k_ETestRole_Client, + k_ETestRole_Symmetric, +}; +ETestRole g_eTestRole = k_ETestRole_Undefined; + +int g_nVirtualPortLocal = 0; // Used when listening, and when connecting +int g_nVirtualPortRemote = 0; // Only used when connecting + +void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t *pInfo ) +{ + // What's the state of the connection? + switch ( pInfo->m_info.m_eState ) + { + case k_ESteamNetworkingConnectionState_ClosedByPeer: + case k_ESteamNetworkingConnectionState_ProblemDetectedLocally: + + TEST_Printf( "[%s] %s, reason %d: %s\n", + pInfo->m_info.m_szConnectionDescription, + ( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer ? "closed by peer" : "problem detected locally" ), + pInfo->m_info.m_eEndReason, + pInfo->m_info.m_szEndDebug + ); + + // Close our end + SteamNetworkingSockets()->CloseConnection( pInfo->m_hConn, 0, nullptr, false ); + + if ( g_hConnection == pInfo->m_hConn ) + { + g_hConnection = k_HSteamNetConnection_Invalid; + } + else + { + // Why are we hearing about any another connection? + assert( false ); + } + + break; + + case k_ESteamNetworkingConnectionState_None: + // Notification that a connection was destroyed. (By us, presumably.) + // We don't need this, so ignore it. + break; + + case k_ESteamNetworkingConnectionState_Connecting: + + // Is this a connection we initiated, or one that we are receiving? + if ( g_hListenSock != k_HSteamListenSocket_Invalid && pInfo->m_info.m_hListenSocket == g_hListenSock ) + { + // Somebody's knocking + TEST_Printf( "[%s] Accepting\n", pInfo->m_info.m_szConnectionDescription ); + g_hConnection = pInfo->m_hConn; + SteamNetworkingSockets()->AcceptConnection( pInfo->m_hConn ); + } + else + { + // Note that we will get notification when our own connection that + // we initiate enters this state. + } + break; + + case k_ESteamNetworkingConnectionState_FindingRoute: + // P2P connections will spend a bried time here where they swap addresses + // and try to find a route. + TEST_Printf( "[%s] finding route\n", pInfo->m_info.m_szConnectionDescription ); + break; + + case k_ESteamNetworkingConnectionState_Connected: + // We got fully connected + assert( pInfo->m_hConn == g_hConnection ); // We don't initiate or accept any other connections, so this should be out own connection + TEST_Printf( "[%s] connected\n", pInfo->m_info.m_szConnectionDescription ); + break; + + default: + assert( false ); + break; + } +} + + +int main( int argc, const char **argv ) +{ + SteamNetworkingIdentity identityLocal; identityLocal.Clear(); + SteamNetworkingIdentity identityRemote; identityRemote.Clear(); + const char *pszTrivialSignalingService = "localhost:10000"; + + // Parse the command line + for ( int idxArg = 1 ; idxArg < argc ; ++idxArg ) + { + const char *pszSwitch = argv[idxArg]; + + auto GetArg = [&]() -> const char * { + if ( idxArg + 1 >= argc ) + TEST_Fatal( "Expected argument after %s", pszSwitch ); + return argv[++idxArg]; + }; + auto ParseIdentity = [&]( SteamNetworkingIdentity &x ) { + const char *pszArg = GetArg(); + if ( !x.ParseString( pszArg ) ) + TEST_Fatal( "'%s' is not a valid identity string", pszArg ); + }; + + if ( !strcmp( pszSwitch, "--identity-local" ) ) + ParseIdentity( identityLocal ); + else if ( !strcmp( pszSwitch, "--identity-remote" ) ) + ParseIdentity( identityRemote ); + else if ( !strcmp( pszSwitch, "--signaling-server" ) ) + pszTrivialSignalingService = GetArg(); + else if ( !strcmp( pszSwitch, "--client" ) ) + g_eTestRole = k_ETestRole_Client; + else if ( !strcmp( pszSwitch, "--server" ) ) + g_eTestRole = k_ETestRole_Server; + else if ( !strcmp( pszSwitch, "--symmetric" ) ) + g_eTestRole = k_ETestRole_Symmetric; + else + TEST_Fatal( "Unexpected command line argument '%s'", pszSwitch ); + } + + if ( g_eTestRole == k_ETestRole_Undefined ) + TEST_Fatal( "Must specify test role (--server, --client, or --symmetric" ); + if ( identityLocal.IsInvalid() ) + TEST_Fatal( "Must specify local identity using --identity-local" ); + if ( identityRemote.IsInvalid() && g_eTestRole != k_ETestRole_Server ) + TEST_Fatal( "Must specify remote identity using --identity-remote" ); + + // Initialize library, with the desired local identity + TEST_Init( &identityLocal ); + + // Create the signaling service + SteamNetworkingErrMsg errMsg; + ITrivialSignalingClient *pSignaling = CreateTrivialSignalingClient( pszTrivialSignalingService, SteamNetworkingSockets(), errMsg ); + if ( pSignaling == nullptr ) + TEST_Fatal( "Failed to initializing signaling client. %s", errMsg ); + + SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged( OnSteamNetConnectionStatusChanged ); + + // Create listen socket to receive connections on, unless we are the client + if ( g_eTestRole == k_ETestRole_Server ) + { + TEST_Printf( "Creating listen socket, local virtual port %d\n", g_nVirtualPortLocal ); + g_hListenSock = SteamNetworkingSockets()->CreateListenSocketP2P( g_nVirtualPortLocal, 0, nullptr ); + assert( g_hListenSock != k_HSteamListemSocket_Invalid ); + } + else if ( g_eTestRole == k_ETestRole_Symmetric ) + { + + // Currently you must create a listen socket to use symmetric mode, + // even if you know that you will always create connections "both ways". + // In the future we might try to remove this requirement. It is a bit + // less efficient, since it always triggered the race condition case + // where both sides create their own connections, and then one side + // decides to their theirs away. If we have a listen socket, then + // it can be the case that one peer will receive the incoming connection + // from the other peer, and since he has a listen socket, can save + // the connection, and then implicitly accept it when he initiates his + // own connection. Without the listen socket, if an incoming connection + // request arrives before we have started connecting out, then we are forced + // to ignore it, as the app has given no indication that it desires to + // receive inbound connections at all. + TEST_Printf( "Creating listen socket in symmetric mode, local virtual port %d\n", g_nVirtualPortLocal ); + SteamNetworkingConfigValue_t opt; + opt.SetInt32( k_ESteamNetworkingConfig_SymmetricConnect, 1 ); // << Note we set symmetric mode on the listen socket + g_hListenSock = SteamNetworkingSockets()->CreateListenSocketP2P( g_nVirtualPortLocal, 1, &opt ); + assert( g_hListenSock != k_HSteamListemSocket_Invalid ); + } + + // Begin connecting to peer, unless we are the server + if ( g_eTestRole != k_ETestRole_Server ) + { + std::vector< SteamNetworkingConfigValue_t > vecOpts; + + // If we want the local and virtual port to differ, we must set + // an option. This is a pretty rare use case, and usually not needed. + // The local virtual port is only usually relevant for symmetric + // connections, and then, it almost always matches. Here we are + // just showing in this example code how you could handle this if you + // needed them to differ. + if ( g_nVirtualPortRemote != g_nVirtualPortLocal ) + { + SteamNetworkingConfigValue_t opt; + opt.SetInt32( k_ESteamNetworkingConfig_LocalVirtualPort, g_nVirtualPortLocal ); + vecOpts.push_back( opt ); + } + + // Symmetric mode? Noce that since we created a listen socket on this local + // virtual port and tagged it for symmetric connect mode, any connections + // we create that use the same local virtual port will automatically inherit + // this setting. However, this is really not recommended. It is best to be + // explicit. + if ( g_eTestRole == k_ETestRole_Symmetric ) + { + SteamNetworkingConfigValue_t opt; + opt.SetInt32( k_ESteamNetworkingConfig_SymmetricConnect, 1 ); + vecOpts.push_back( opt ); + TEST_Printf( "Connecting to '%s' in symmetric mode, virtual port %d, from local virtual port %d.\n", + SteamNetworkingIdentityRender( identityRemote ).c_str(), g_nVirtualPortRemote, + g_nVirtualPortLocal ); + } + else + { + TEST_Printf( "Connecting to '%s', virtual port %d, from local virtual port %d.\n", + SteamNetworkingIdentityRender( identityRemote ).c_str(), g_nVirtualPortRemote, + g_nVirtualPortLocal ); + } + + g_hConnection = SteamNetworkingSockets()->ConnectP2P( identityRemote, g_nVirtualPortRemote, (int)vecOpts.size(), vecOpts.data() ); + } + + // Main test loop + for (;;) + { + // Check for incoming signals, and dispatch them + pSignaling->Poll(); + + // Check callbacks + TEST_PumpCallbacks(); + } + + + TEST_Kill(); + return 0; +} diff --git a/tests/test_p2p.py b/tests/test_p2p.py new file mode 100755 index 0000000..5fa372a --- /dev/null +++ b/tests/test_p2p.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +# Test script to run the p2p test. This runs three processes: the two clients, +# and the dummy signaling service. +# +# NOTE: You usually won't run this script from its original location. The +# makefiles will copy it into the same location as the tests and examples + +import subprocess +import threading + +def RunProcess( tag, cmdline, **popen_kwargs ): + + with open( tag + ".log", "wt" ) as log: + + def WriteLn( ln ): + print( "%s> %s" % (tag, ln ) ) + log.write( "%s\n" % ln ) + + WriteLn( "Executing: " + ' '.join( cmdline ) ) + process = subprocess.Popen( cmdline, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, **popen_kwargs ) + process.stdin.close() + while True: + sOutput = process.stdout.readline() + if sOutput: + sOutput = str(sOutput, 'utf-8', 'ignore') + WriteLn( sOutput.rstrip() ) + elif process.poll() is not None: + break + process.wait() + WriteLn( "Exitted with %d" % process.returncode ) + +def StartProcessInThread( tag, cmdline, **popen_kwargs ): + def ThreadProc(): + RunProcess( tag, cmdline, **popen_kwargs ) + thread = threading.Thread( target=ThreadProc, name=tag ) + thread.start() + return thread + +def StartClientInThread( role, local, remote ): + cmdline = [ + "./test_p2p", + "--" + role, + "--identity-local", "str:"+local, + "--identity-remote", "str:"+remote, + "--signaling-server", "localhost:10000" + ] + return StartProcessInThread( local, cmdline ); + +signaling = StartProcessInThread( "signaling", [ './trivial_signaling_server' ], cwd="../examples" ) + +client1 = StartClientInThread( "server", "peer_server", "peer_client" ) +client2 = StartClientInThread( "client", "peer_client", "peer_server" ) + +client1.join() +client2.join() +signaling.join() +