2020-03-24 14:36:22 +00:00
using System ;
using System.Collections ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Linq ;
using System.Reactive.Linq ;
2020-03-24 21:51:38 +00:00
using System.Security.Cryptography ;
2020-03-24 14:36:22 +00:00
using System.Text.RegularExpressions ;
using Isopoh.Cryptography.Argon2 ;
using LiteNetLib ;
using MessagePack ;
using Microsoft.Extensions.Logging ;
public class MasterServer
{
2020-03-27 01:53:03 +00:00
public ILogger Logger = null ;
2020-03-24 14:36:22 +00:00
private const string EmailPattern =
@"^([0-9a-zA-Z]" + //Start with a digit or alphabetical
@"([\+\-_\.][0-9a-zA-Z]+)*" + // No continuous or ending +-_. chars in email
@")+" +
@"@(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]*\.)+[a-zA-Z0-9]{2,17})$" ;
private const string UsernamePattern = @"^[A-Za-z0-9]+(?:[ _-][A-Za-z0-9]+)*$" ;
2020-03-26 02:49:09 +00:00
private readonly Dictionary < Type , NotAnActionCollection > _messageCallbacks = new Dictionary < Type , NotAnActionCollection > ( ) ;
private readonly Dictionary < long , User > _users = new Dictionary < long , User > ( ) ;
private readonly Dictionary < Guid , Session > _sessions = new Dictionary < Guid , Session > ( ) ;
2020-03-24 14:36:22 +00:00
private List < User > _readyPlayers = new List < User > ( ) ;
2020-03-26 02:49:09 +00:00
private NetManager _netManager ;
2020-03-24 14:36:22 +00:00
private Stopwatch _timer ;
private Random _random = new Random ( ) ;
2020-03-26 02:49:09 +00:00
private readonly DatabaseCache _database ;
2020-03-24 21:51:38 +00:00
private static readonly RandomNumberGenerator Rng = RandomNumberGenerator . Create ( ) ;
2020-03-24 14:36:22 +00:00
private float Time = > ( float ) _timer . Elapsed . TotalSeconds ;
public bool IsValidEmail ( string email ) = > Regex . IsMatch ( email , EmailPattern ) ;
public bool IsValidUsername ( string name ) = > Regex . IsMatch ( name , UsernamePattern ) ;
public MasterServer ( DatabaseCache cache )
{
2020-03-24 21:51:38 +00:00
_database = cache ;
2020-03-24 14:36:22 +00:00
}
public void ClearMessageListeners ( )
{
_messageCallbacks . Clear ( ) ;
}
public void AddMessageListener < T > ( Action < T > callback ) where T : Message
{
if ( ! _messageCallbacks . ContainsKey ( typeof ( T ) ) )
_messageCallbacks [ typeof ( T ) ] = new ActionCollection < T > ( ) ;
( ( ActionCollection < T > ) _messageCallbacks [ typeof ( T ) ] ) . Add ( callback ) ;
}
public void Stop ( )
{
2020-03-26 02:49:09 +00:00
_netManager . Stop ( ) ;
2020-03-24 14:36:22 +00:00
}
public void Start ( )
{
_timer = new Stopwatch ( ) ;
_timer . Start ( ) ;
EventBasedNetListener listener = new EventBasedNetListener ( ) ;
2020-03-26 02:49:09 +00:00
_netManager = new NetManager ( listener )
2020-03-24 14:36:22 +00:00
{
2020-03-27 17:26:26 +00:00
UnsyncedEvents = true ,
NatPunchEnabled = true ,
2020-03-24 14:36:22 +00:00
} ;
2020-03-26 02:49:09 +00:00
_netManager . Start ( 3075 ) ;
2020-03-24 14:36:22 +00:00
listener . NetworkErrorEvent + = ( point , code ) = > Logger . Log ( LogLevel . Debug , $"{point.Address}: Error {code}" ) ;
2020-03-27 17:39:09 +00:00
listener . ConnectionRequestEvent + = request = > request . AcceptIfKey ( "aetheria-cc65a44d" ) ;
2020-03-24 14:36:22 +00:00
listener . PeerConnectedEvent + = peer = >
{
Logger . Log ( LogLevel . Debug , $"User Connected: {peer.EndPoint}" ) ; // Show peer ip
_users [ peer . Id ] = new User { Peer = peer } ;
} ;
listener . PeerDisconnectedEvent + = ( peer , info ) = >
{
Logger . Log ( LogLevel . Debug , $"User Disconnected: {peer.EndPoint}" ) ; // Show peer ip
// foreach (var verifiedUser in _users.Values.Where(IsVerified))
// verifiedUser.Peer.Send("PlayerLeft", SessionData(verifiedUser.Peer).Username);
_users . Remove ( peer . Id ) ;
} ;
listener . NetworkLatencyUpdateEvent + = ( peer , latency ) = >
{
// Logger($"Received Ping: {latency}");
_users [ peer . Id ] . Latency = latency ;
} ;
listener . NetworkReceiveEvent + = ( peer , reader , method ) = >
{
2020-03-27 17:58:37 +00:00
var bytes = reader . GetRemainingBytes ( ) ;
var message = MessagePackSerializer . Deserialize < Message > ( bytes ) ;
2020-03-28 03:43:07 +00:00
Logger . Log ( LogLevel . Information , $"Received message: {MessagePackSerializer.ConvertToJson(new ReadOnlyMemory<byte>(bytes))}" ) ;
if ( message = = null )
return ;
2020-03-24 14:36:22 +00:00
message . Peer = peer ;
var user = _users [ peer . Id ] ;
var type = message . GetType ( ) ;
if ( type = = typeof ( LoginMessage ) | | type = = typeof ( RegisterMessage ) | | type = = typeof ( VerifyMessage ) )
{
if ( IsVerified ( user ) )
{
peer . Send ( new LoginSuccessMessage { Session = _users [ peer . Id ] . SessionGuid } ) ;
return ;
}
2020-03-24 21:51:38 +00:00
2020-03-28 03:59:24 +00:00
Guid sessionGuid ;
2020-03-24 21:51:38 +00:00
switch ( message )
2020-03-24 14:36:22 +00:00
{
2020-03-24 21:51:38 +00:00
case RegisterMessage register when ! IsValidUsername ( register . Name ) :
2020-03-24 14:36:22 +00:00
peer . Send ( new ErrorMessage { Error = "Username Invalid" } ) ;
return ;
2020-03-24 21:51:38 +00:00
case RegisterMessage register when ! IsValidEmail ( register . Email ) :
2020-03-24 14:36:22 +00:00
peer . Send ( new ErrorMessage { Error = "Email Invalid" } ) ;
return ;
2020-03-24 21:51:38 +00:00
case RegisterMessage register :
{
2020-03-28 03:59:24 +00:00
sessionGuid = Guid . NewGuid ( ) ;
peer . Send ( new LoginSuccessMessage { Session = sessionGuid } ) ;
2020-03-24 14:36:22 +00:00
2020-03-24 21:51:38 +00:00
var newUserData = new Player
{
2020-03-28 03:59:24 +00:00
ID = Guid . NewGuid ( ) ,
2020-03-24 21:51:38 +00:00
Email = register . Email ,
2020-03-26 02:49:09 +00:00
Password = Argon2 . Hash ( register . Password , null , memoryCost : 16384 ) ,
2020-03-24 21:51:38 +00:00
Username = register . Name
} ;
_database . Add ( newUserData ) ;
2020-03-24 14:36:22 +00:00
2020-03-28 03:59:24 +00:00
_sessions [ sessionGuid ] = new Session { Data = newUserData , LastUpdate = DateTime . Now } ;
2020-03-30 03:11:57 +01:00
if ( ! _users . ContainsKey ( peer . Id ) )
return ;
2020-03-28 03:59:24 +00:00
_users [ peer . Id ] . SessionGuid = sessionGuid ;
2020-03-24 21:51:38 +00:00
break ;
}
case VerifyMessage verify when _sessions . ContainsKey ( verify . Session ) :
2020-03-24 14:36:22 +00:00
_users [ peer . Id ] . SessionGuid = verify . Session ;
peer . Send ( new LoginSuccessMessage { Session = verify . Session } ) ;
return ;
2020-03-24 21:51:38 +00:00
case LoginMessage login :
{
2020-03-26 02:49:09 +00:00
var isEmail = IsValidEmail ( login . Auth ) ;
var userData = _database . GetAll < Player > ( ) . FirstOrDefault ( x = >
( isEmail ? x . Email : x . Username ) = = login . Auth ) ;
2020-03-24 14:36:22 +00:00
2020-03-24 21:51:38 +00:00
if ( userData = = null )
{
2020-03-26 02:49:09 +00:00
peer . Send ( new ErrorMessage { Error = isEmail ? "Email Not Found" : "Username Not Found" } ) ;
2020-03-24 21:51:38 +00:00
return ;
}
2020-03-24 14:36:22 +00:00
2020-03-26 02:49:09 +00:00
if ( ! Argon2 . Verify ( userData . Password , login . Password ) )
2020-03-24 21:51:38 +00:00
{
2020-03-27 01:53:03 +00:00
peer . Send ( new ErrorMessage { Error = "Password Incorrect" } ) ;
2020-03-24 21:51:38 +00:00
return ;
}
2020-03-24 14:36:22 +00:00
2020-03-28 03:59:24 +00:00
sessionGuid = Guid . NewGuid ( ) ;
peer . Send ( new LoginSuccessMessage { Session = sessionGuid } ) ;
2020-03-24 14:36:22 +00:00
2020-03-28 03:59:24 +00:00
_sessions . Add ( sessionGuid , new Session { Data = userData , LastUpdate = DateTime . Now } ) ;
2020-03-30 03:11:57 +01:00
// TODO: Intermittent: users getting disconnected before getting here, check that key exists!
if ( ! _users . ContainsKey ( peer . Id ) )
return ;
2020-03-28 03:59:24 +00:00
_users [ peer . Id ] . SessionGuid = sessionGuid ;
2020-03-24 21:51:38 +00:00
break ;
}
2020-03-24 14:36:22 +00:00
}
}
else
{
if ( IsVerified ( user ) )
{
if ( _messageCallbacks . ContainsKey ( type ) )
typeof ( ActionCollection < > ) . MakeGenericType ( new [ ] { type } ) . GetMethod ( "Invoke" )
. Invoke ( _messageCallbacks [ type ] , new object [ ] { message } ) ;
2020-03-27 17:58:37 +00:00
else Logger . Log ( LogLevel . Warning , $"Received {type.Name} message but no one is listening for it so I'll just leave it here ¯\\_(ツ)_/¯\n{MessagePackSerializer.ConvertToJson(new ReadOnlyMemory<byte>(bytes))}" ) ;
2020-03-24 14:36:22 +00:00
_sessions [ _users [ peer . Id ] . SessionGuid ] . LastUpdate = DateTime . Now ;
}
else
peer . Send ( new ErrorMessage { Error = "User Not Verified" } ) ;
}
} ;
AddMessageListener < ChatMessage > ( message = >
{
foreach ( var verifiedUser in _users . Values . Where ( IsVerified ) )
verifiedUser . Peer . Send ( new ChatBroadcastMessage { User = SessionData ( message . Peer ) . Username , Text = message . Text } ) ;
} ) ;
AddMessageListener < ChangeNameMessage > ( message = >
{
SessionData ( message . Peer ) . Username = message . Name ;
_database . Add ( SessionData ( message . Peer ) ) ;
} ) ;
2020-03-29 04:19:24 +01:00
// Observable.Timer(DateTimeOffset.Now, TimeSpan.FromSeconds(30)).Subscribe(_ =>
// {
// foreach (var s in _sessions.ToArray())
// {
// if (DateTime.Now.Subtract(s.Value.LastUpdate).TotalSeconds > s.Value.DurationSeconds)
// _sessions.Remove(s.Key);
// }
// });
2020-03-27 17:58:37 +00:00
Logger . LogInformation ( "LiteNetLib is now open to new connections. Please be gentle." ) ;
2020-03-24 14:36:22 +00:00
}
private bool IsVerified ( User u ) = > _sessions . ContainsKey ( u . SessionGuid ) ;
public Player SessionData ( NetPeer peer ) = > IsVerified ( _users [ peer . Id ] ) ? _sessions [ _users [ peer . Id ] . SessionGuid ] . Data : null ;
}
public class Session
{
public DateTime LastUpdate ;
public Player Data ;
public float DurationSeconds ;
}
public class User
{
public NetPeer Peer ;
public int Latency ;
public Guid SessionGuid ;
public float Tardiness ;
}