mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-04-19 13:59:22 -04:00
When a Dresser component is set to `ClothingSource.OwnerConnection`, it will verify the owner connection actually owns the items they are attempting to wear using the Steam API. This means users can't just add items they never owned to their avatar forcefully.
501 lines
13 KiB
C#
501 lines
13 KiB
C#
using Sandbox.Engine;
|
|
using Sandbox.Internal;
|
|
|
|
namespace Sandbox.Network;
|
|
|
|
/// <summary>
|
|
/// A network system is a bunch of connections that people can send messages
|
|
/// over. Right now it can be a dedicated server, a listen server, a pure client,
|
|
/// or a p2p system.
|
|
/// </summary>
|
|
internal partial class NetworkSystem
|
|
{
|
|
void InstallHandshakeMessages()
|
|
{
|
|
AddHandler<ServerInfo>( On_Handshake_ServerInfo );
|
|
AddHandler<UserInfo>( On_Handshake_ClientInfo );
|
|
AddHandler<Welcome>( On_Handshake_Welcome );
|
|
AddHandler<RequestInitialSnapshot>( On_Handshake_RequestSnapshot );
|
|
AddHandler<InitialSnapshotResponse>( On_Handshake_Snapshot );
|
|
AddHandler<MountedVPKsResponse>( On_Handshake_MountedVPKs );
|
|
AddHandler<RequestMountedVPKs>( On_Handshake_RequestMountedVPKs );
|
|
AddHandler<ClientReady>( On_Handshake_ClientReady );
|
|
AddHandler<Activate>( On_Handshake_Activate );
|
|
AddHandler<RestartHandshakeMsg>( On_Handshake_Restart );
|
|
AddHandler<KickMsg>( On_Kick );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server says hello to the client. It tells the client some basic information about itself.
|
|
/// The client can determine here whether they still want to join or not.
|
|
/// </summary>
|
|
async Task On_Handshake_ServerInfo( ServerInfo msg, Connection source, Guid msgId )
|
|
{
|
|
if ( !source.IsHost )
|
|
return;
|
|
|
|
IsDeveloperHost = msg.IsDeveloperHost;
|
|
|
|
source.UpdateFrom( msg.Host );
|
|
source.State = Connection.ChannelState.LoadingServerInformation;
|
|
|
|
Connection.Local = new LocalConnection( msg.Assigned.Id )
|
|
{
|
|
HandshakeId = msg.HandshakeId,
|
|
State = Connection.ChannelState.LoadingServerInformation
|
|
};
|
|
|
|
//
|
|
// Tell the server all about ourselves
|
|
//
|
|
var output = UserInfo.Local;
|
|
|
|
if ( !source.OnReceiveServerInfo( ref output, msg ) )
|
|
{
|
|
IGameInstanceDll.Current.Disconnect();
|
|
return;
|
|
}
|
|
|
|
log.Trace( $"Server Id is {source.Id}" );
|
|
log.Trace( $"Map Name is {msg.MapName}" );
|
|
log.Trace( $"Server Name is {msg.ServerName}" );
|
|
log.Trace( $"Engine version is {msg.EngineVersion}" );
|
|
log.Trace( $"Game Package is {msg.GamePackage}" );
|
|
log.Trace( $"My Client ID is {Connection.Local.Id}" );
|
|
|
|
// This is a bit of a mess, it needs a good cleaning up. If they have a menu package, then load it first.
|
|
if ( !string.IsNullOrEmpty( msg.GamePackage ) )
|
|
{
|
|
UpdateLoading( $"Loading {msg.GamePackage}" );
|
|
|
|
log.Trace( $"Loading menu package.. {msg.GamePackage}" );
|
|
|
|
var flags = GameLoadingFlags.Remote | GameLoadingFlags.Reload;
|
|
if ( IsDeveloperHost ) flags |= GameLoadingFlags.Developer;
|
|
|
|
if ( !Application.IsStandalone )
|
|
{
|
|
LaunchArguments.Map = msg.MapPackage;
|
|
|
|
// Fetch the package info so we can show the game's custom loading screen media
|
|
var packageInfo = await Package.FetchAsync( msg.GamePackage, false );
|
|
if ( packageInfo is not null )
|
|
{
|
|
LoadingScreen.Media = packageInfo.LoadingScreen.MediaUrl;
|
|
}
|
|
|
|
await IGameInstanceDll.Current.LoadGamePackageAsync( msg.GamePackage, flags, default );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.Trace( $"No game package - must be a developer" );
|
|
}
|
|
|
|
if ( IGameInstanceDll.Current is not null )
|
|
{
|
|
// TypeLibrary was probably rebuilt, keep it up to date
|
|
TypeLibrary = IGameInstanceDll.Current.TypeLibrary;
|
|
}
|
|
|
|
foreach ( var (k, v) in msg.ServerData )
|
|
{
|
|
Networking.SetData( k, v );
|
|
}
|
|
|
|
Networking.MaxPlayers = msg.MaxPlayers;
|
|
Networking.ServerName = msg.ServerName;
|
|
Networking.MapName = msg.MapName;
|
|
|
|
InstallStringTables();
|
|
log.Trace( $"Fetching Server Data.." );
|
|
|
|
//
|
|
// Tell me what I need
|
|
//
|
|
UpdateLoading( $"Fetching Server Data" );
|
|
|
|
source.SendMessage( output with
|
|
{
|
|
HandshakeId = msg.HandshakeId
|
|
} );
|
|
}
|
|
|
|
async Task On_Handshake_ClientInfo( UserInfo msg, Connection source, Guid msgId )
|
|
{
|
|
if ( source.IsHost )
|
|
return;
|
|
|
|
if ( msg.HandshakeId != source.HandshakeId )
|
|
return;
|
|
|
|
if ( source.State != Connection.ChannelState.LoadingServerInformation )
|
|
{
|
|
source.Kick( $"Invalid Handshake State {source.State}" );
|
|
Log.Info( $"Kicking {source.DisplayName} [{source.SteamId}] Invalid Handshake State {source.State}" );
|
|
return;
|
|
}
|
|
|
|
if ( !await source.OnReceiveUserInfo( msg ) )
|
|
return;
|
|
|
|
//
|
|
// Lobbies and steam network connections are trusted, so we can take the display name and Steam Id from them,
|
|
// we shouldn't trust any other type of connection... but local TCP we can let slide.
|
|
//
|
|
if ( source is SteamLobbyConnection slob )
|
|
{
|
|
var friend = new Friend( slob.Friend.Id );
|
|
msg.SteamId = slob.Friend.Id;
|
|
msg.DisplayName = friend.Name;
|
|
}
|
|
|
|
Log.Info( $"{msg.DisplayName} [{msg.SteamId}] is connecting" );
|
|
|
|
//
|
|
// If the lobby is set to FriendsOnly, only allow players who are Steam friends with the host.
|
|
//
|
|
if ( !Application.IsDedicatedServer && Config.Privacy == LobbyPrivacy.FriendsOnly )
|
|
{
|
|
var hostSteamId = Utility.Steam.SteamId;
|
|
|
|
// Host is always allowed
|
|
if ( msg.SteamId != hostSteamId.Value && !new Friend( msg.SteamId ).IsFriend )
|
|
{
|
|
Log.Info( $"Kicked {msg.DisplayName} [{msg.SteamId}] - not friends with host [{hostSteamId}]" );
|
|
source.Kick( "This lobby is Friends Only." );
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
var denialReason = "";
|
|
|
|
source.PreInfo = new ConnectionInfo( null )
|
|
{
|
|
ConnectionId = source.Id,
|
|
State = source.State
|
|
};
|
|
|
|
source.PreInfo.Update( msg );
|
|
|
|
if ( GameSystem is not null && !GameSystem.AcceptConnection( source, ref denialReason ) )
|
|
{
|
|
Log.Info( $"Kicking {msg.DisplayName} [{msg.SteamId}] - {denialReason}" );
|
|
source.Kick( denialReason );
|
|
return;
|
|
}
|
|
|
|
source.PreInfo = null;
|
|
source.State = Connection.ChannelState.Welcome;
|
|
|
|
//log.Info( $"Client Name is {data.DisplayName}" );
|
|
//log.Info( $"Client SteamId is {data.SteamId}" );
|
|
//log.Info( $"Client EngineVersion is {data.EngineVersion}" );
|
|
|
|
var output = new Welcome();
|
|
output.HandshakeId = msg.HandshakeId;
|
|
|
|
foreach ( var table in tables )
|
|
{
|
|
table.SendSnapshot( source );
|
|
}
|
|
|
|
//
|
|
// They're connected now dummy
|
|
//
|
|
msg.ConnectionTime = DateTime.UtcNow;
|
|
|
|
//
|
|
// Make their name unique
|
|
//
|
|
var displayName = msg.DisplayName;
|
|
var index = 2;
|
|
while ( ConnectionInfo.All.Values.Any( x => string.Equals( x.DisplayName, displayName, StringComparison.OrdinalIgnoreCase ) ) )
|
|
{
|
|
displayName = $"{msg.DisplayName} ({index})";
|
|
index++;
|
|
}
|
|
msg.DisplayName = displayName;
|
|
|
|
//
|
|
// Add player info to the manager. This will get sent to all the other players, so this
|
|
// player is part of the game now.
|
|
//
|
|
{
|
|
AddConnection( source, msg );
|
|
}
|
|
|
|
//
|
|
// Tell game this guy has connected. This happens after ConnectionInfo so that
|
|
// everyone can look up their name etc.
|
|
//
|
|
GameSystem?.OnConnected( source );
|
|
|
|
source.SendMessage( output );
|
|
}
|
|
|
|
async Task On_Handshake_Welcome( Welcome msg, Connection source, Guid msgId )
|
|
{
|
|
if ( !source.IsHost )
|
|
return;
|
|
|
|
if ( Connection.Local is null )
|
|
throw new UnauthorizedAccessException();
|
|
|
|
if ( msg.HandshakeId != Connection.Local.HandshakeId )
|
|
return;
|
|
|
|
if ( Connection.Local.State != Connection.ChannelState.LoadingServerInformation )
|
|
throw new UnauthorizedAccessException();
|
|
|
|
Connection.Local.State = Connection.ChannelState.Welcome;
|
|
|
|
log.Trace( $"Welcome!" );
|
|
|
|
UpdateLoading( $"Loading Network Tables" );
|
|
|
|
await IGameInstanceDll.Current?.LoadNetworkTables( this );
|
|
|
|
UpdateLoading( $"Init Game System" );
|
|
|
|
InitializeGameSystem();
|
|
|
|
log.Trace( $"Game Network System: {GameSystem}" );
|
|
|
|
//
|
|
// Here would be a goodish place to send a bunch of CRC's of the loaded state, so
|
|
// the server can compare and reject if we're loading assemblies wrong (cheater)
|
|
//
|
|
UpdateLoading( "Fetching Snapshot" );
|
|
|
|
var output = new RequestMountedVPKs { HandshakeId = msg.HandshakeId };
|
|
source.SendMessage( output );
|
|
}
|
|
|
|
Task On_Handshake_RequestMountedVPKs( RequestMountedVPKs msg, Connection source, Guid msgId )
|
|
{
|
|
if ( source.IsHost )
|
|
return Task.CompletedTask;
|
|
|
|
if ( msg.HandshakeId != source.HandshakeId )
|
|
return Task.CompletedTask;
|
|
|
|
if ( source.State != Connection.ChannelState.Welcome )
|
|
{
|
|
source.Kick( $"Invalid Handshake State {source.State}" );
|
|
Log.Info( $"Kicking {source.DisplayName} [{source.SteamId}] Invalid Handshake State {source.State}" );
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
var output = new MountedVPKsResponse { HandshakeId = msg.HandshakeId };
|
|
|
|
GameSystem?.GetMountedVPKs( source, ref output );
|
|
|
|
source.State = Connection.ChannelState.MountVPKs;
|
|
source.SendMessage( output );
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
async Task On_Handshake_MountedVPKs( MountedVPKsResponse msg, Connection source, Guid msgId )
|
|
{
|
|
if ( !source.IsHost )
|
|
return;
|
|
|
|
if ( Connection.Local is null )
|
|
throw new UnauthorizedAccessException();
|
|
|
|
if ( msg.HandshakeId != Connection.Local.HandshakeId )
|
|
return;
|
|
|
|
if ( Connection.Local.State != Connection.ChannelState.Welcome )
|
|
throw new UnauthorizedAccessException();
|
|
|
|
Connection.Local.State = Connection.ChannelState.MountVPKs;
|
|
|
|
await GameSystem?.MountVPKs( source, msg );
|
|
|
|
var userInfo = UserInfo.Local;
|
|
var output = new RequestInitialSnapshot();
|
|
|
|
output.HandshakeId = msg.HandshakeId;
|
|
output.UserData = userInfo.UserData;
|
|
|
|
source.SendMessage( output );
|
|
}
|
|
|
|
Task On_Handshake_RequestSnapshot( RequestInitialSnapshot msg, Connection source, Guid msgId )
|
|
{
|
|
if ( source.IsHost )
|
|
return Task.CompletedTask;
|
|
|
|
if ( msg.HandshakeId != source.HandshakeId )
|
|
return Task.CompletedTask;
|
|
|
|
if ( source.State != Connection.ChannelState.MountVPKs )
|
|
{
|
|
source.Kick( $"Invalid Handshake State {source.State}" );
|
|
Log.Info( $"Kicking {source.DisplayName} [{source.SteamId}] Invalid Handshake State {source.State}" );
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
if ( msg.UserData is not null )
|
|
{
|
|
source.UpdateUserData( msg.UserData );
|
|
}
|
|
|
|
source.State = Connection.ChannelState.Snapshot;
|
|
|
|
log.Trace( $"[{this}] Requesting a snapshot" );
|
|
|
|
var snapshot = new SnapshotMsg
|
|
{
|
|
GameObjectSystems = [],
|
|
NetworkObjects = new( 64 )
|
|
};
|
|
|
|
GameSystem?.GetSnapshot( source, ref snapshot );
|
|
|
|
var output = new InitialSnapshotResponse
|
|
{
|
|
HandshakeId = source.HandshakeId,
|
|
Snapshot = snapshot
|
|
};
|
|
|
|
source.SendMessage( output );
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
async Task On_Handshake_Snapshot( InitialSnapshotResponse msg, Connection source, Guid msgId )
|
|
{
|
|
NetworkDebugSystem.Current?.Record( NetworkDebugSystem.MessageType.Snapshot, msg );
|
|
|
|
if ( !source.IsHost )
|
|
return;
|
|
|
|
if ( Connection.Local is null )
|
|
throw new UnauthorizedAccessException();
|
|
|
|
if ( msg.HandshakeId != Connection.Local.HandshakeId )
|
|
return;
|
|
|
|
if ( Connection.Local.State != Connection.ChannelState.MountVPKs )
|
|
throw new UnauthorizedAccessException();
|
|
|
|
Connection.Local.State = Connection.ChannelState.Snapshot;
|
|
|
|
UpdateLoading( "Loading Snapshot" );
|
|
Log.Trace( $"[{this}] Got a snapshot" );
|
|
|
|
//
|
|
// Spawn the scene, which could also lead to calling OnStart, OnEnable on components
|
|
// which might create new network instances. So from this point we should be considered
|
|
// live in the game.
|
|
//
|
|
if ( GameSystem is not null )
|
|
{
|
|
try
|
|
{
|
|
await GameSystem.SetSnapshotAsync( msg.Snapshot );
|
|
}
|
|
catch ( Exception e )
|
|
{
|
|
IGameInstanceDll.Current.Disconnect();
|
|
IMenuSystem.ShowServerError( "Disconnected", "Error Deserializing Snapshot" );
|
|
Log.Error( e );
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
Log.Trace( $"[{this}] Finished loading snapshot" );
|
|
|
|
var output = new ClientReady
|
|
{
|
|
HandshakeId = msg.HandshakeId
|
|
};
|
|
|
|
source.SendMessage( output );
|
|
}
|
|
|
|
Task On_Handshake_ClientReady( ClientReady msg, Connection source, Guid msgId )
|
|
{
|
|
if ( source.IsHost )
|
|
return Task.CompletedTask;
|
|
|
|
if ( msg.HandshakeId != source.HandshakeId )
|
|
return Task.CompletedTask;
|
|
|
|
Log.Trace( $"[{this}] Client is ready" );
|
|
|
|
if ( source.State != Connection.ChannelState.Snapshot )
|
|
{
|
|
source.Kick( $"Invalid Handshake State {source.State}" );
|
|
Log.Info( $"Kicking {source.DisplayName} [{source.SteamId}] Invalid Handshake State {source.State}" );
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
source.State = Connection.ChannelState.Connected;
|
|
|
|
//
|
|
// Tell game this guy is fully active.
|
|
//
|
|
GameSystem?.OnJoined( source );
|
|
|
|
var output = new Activate();
|
|
output.HandshakeId = msg.HandshakeId;
|
|
|
|
source.SendMessage( output );
|
|
|
|
Log.Info( $"{source.DisplayName} [{source.SteamId}] is connected" );
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
Task On_Handshake_Restart( RestartHandshakeMsg msg, Connection source, Guid msgId )
|
|
{
|
|
if ( source.IsHost )
|
|
return Task.CompletedTask;
|
|
|
|
StartHandshake( source );
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
Task On_Kick( KickMsg msg, Connection source, Guid msgId )
|
|
{
|
|
if ( !source.IsHost )
|
|
{
|
|
// Conna: only the host can kick us.
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
IGameInstanceDll.Current.Disconnect();
|
|
IMenuSystem.ShowServerError( "Disconnected", msg.Reason );
|
|
Log.Warning( $"Disconnecting - {msg.Reason}" );
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
Task On_Handshake_Activate( Activate msg, Connection source, Guid msgId )
|
|
{
|
|
if ( !source.IsHost )
|
|
return Task.CompletedTask;
|
|
|
|
if ( Connection.Local is null )
|
|
throw new UnauthorizedAccessException();
|
|
|
|
if ( msg.HandshakeId != Connection.Local.HandshakeId )
|
|
return Task.CompletedTask;
|
|
|
|
Log.Trace( $"[{this}] I am spawning into the game!" );
|
|
LoadingScreen.IsVisible = false;
|
|
|
|
Connection.Local.State = Connection.ChannelState.Connected;
|
|
source.State = Connection.ChannelState.Connected;
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|