Files
RobustToolbox/Robust.Shared/Network/NetManager.ClientConnect.cs
Paul Ritter 80f9f24243 Serialization v3 aka constant suffering (#1606)
* oops

* fixes serialization il

* copytest

* typo & misc fixes

* 139 moment

* boxing

* mesa dum

* stuff

* goodbye bad friend

* last commit before the big (4) rewrite

* adds datanodes

* kills yamlobjserializer in favor of the new system

* adds more serializers, actually implements them & removes most of the last of the old system

* changed yamlfieldattribute namespace

* adds back iselfserialize

* refactors consts&flags

* renames everything to data(field/definition)

* adds afterserialization

* help

* dataclassgen

* fuggen help me mannen

* Fix most errors on content

* Fix engine errors except map loader

* maploader & misc fix

* misc fixes

* thing

* help

* refactors datanodes

* help me mannen

* Separate ITypeSerializer into reader and writer

* Convert all type serializers

* priority

* adds alot

* il fixes

* adds robustgen

* argh

* adds array & enum serialization

* fixes dataclasses

* adds vec2i / misc fixes

* fixes inheritance

* a very notcursed todo

* fixes some custom dataclasses

* push dis

* Remove data classes

* boutta box

* yes

* Add angle and regex serializer tests

* Make TypeSerializerTest abstract

* sets up ioc etc

* remove pushinheritance

* fixes

* Merge fixes, fix yaml hot reloading

* General fixes2

* Make enum serialization ignore case

* Fix the tag not being copied in data nodes

* Fix not properly serializing flag enums

* Fix component serialization on startup

* Implement ValueDataNode ToString

* Serialization IL fixes, fix return and string equality

* Remove async from prototype manager

* Make serializing unsupported node as enum exception more descriptive

* Fix serv3 tryread casting to serializer instead of reader

* Add constructor for invalid node type exception

* Temporary fix for SERV3: Turn populate delegate into regular code

* Fix not copying the data of non primitive types

* Fix not using the data definition found in copying

* Make ISerializationHooks require explicit implementations

* Add test for serialization inheritance

* Improve IsOverridenIn method

* Fix error message when a data definition is null

* Add method to cast a read value in Serv3Manager

* Rename IServ3Manager to ISerializationManager

* Rename usages of serv3manager, add generic copy method

* Fix IL copy method lookup

* Rename old usages of serv3manager

* Add ITypeCopier

* resistance is futile

* we will conquer this codebase

* Add copy method to all serializers

* Make primitive mismatch error message more descriptive

* bing bong im going to freacking heck

* oopsie moment

* hello are you interested in my wares

* does generic serializers under new architecture

* Convert every non generic serializer to the new format, general fixes

* Update usgaes of generic serializers, cleanup

* does some pushinheritance logic

* finishes pushinheritance FRAMEWORK

* shed

* Add box2, color and component registry serializer tests

* Create more deserialized types and store prototypes with their deserialized results

* Fixes and serializer updates

* Add serialization manager extensions

* adds pushinheritance

* Update all prototypes to have a parent and have consistent id/parent properties

* Fix grammar component serialization

* Add generic serializer tests

* thonk

* Add array serializer test

* Replace logger warning calls with exceptions

* fixes

* Move redundant methods to serialization manager extensions, cleanup

* Add array serialization

* fixes context

* more fixes

* argh

* inheritance

* this should do it

* fixes

* adds copiers & fixes some stuff

* copiers use context v1

* finishing copy context

* more context fixes

* Test fixes

* funky maps

* Fix server user interface component serialization

* Fix value tuple serialization

* Add copying for value types and arrays. Fix copy internal for primitives, enums and strings

* fixes

* fixes more stuff

* yes

* Make abstract/interface skips debugs instead of warnings

* Fix typo

* Make some dictionaries readonly

* Add checks for the serialization manager initializing and already being initialized

* Add base type required and usage for MeansDataDefinition and ImplicitDataDefinitionForInheritorsAttribute

* copy by ref

* Fix exception wording

* Update data field required summary with the new forbidden docs

* Use extension in map loader

* wanna erp

* Change serializing to not use il temporarily

* Make writing work with nullable types

* pushing

* check

* cuddling slaps HARD

* Add serialization priority test

* important fix

* a serialization thing

* serializer moment

* Add validation for some type serializers

* adds context

* moar context

* fixes

* Do the thing for appearance

* yoo lmao

* push haha pp

* Temporarily make copy delegate regular c# code

* Create deserialized component registry to handle not inheriting conflicting references

* YAML LINTER BABY

* ayes

* Fix sprite component norot not being default true like in latest master

* Remove redundant todos

* Add summary doc to every ISerializationManager method

* icon fixes

* Add skip hook argument to readers and copiers

* Merge fixes

* Fix ordering of arguments in read and copy reflection call

* Fix user interface components deserialization

* pew pew

* i am going to HECK

* Add MustUseReturnValue to copy-over methods

* Make serialization log calls use the same sawmill

* gamin

* Fix doc errors in ISerializationManager.cs

* goodbye brave soldier

* fixes

* WIP merge fixes and entity serialization

* aaaaaaaaaaaaaaa

* aaaaaaaaaaaaaaa

* adds inheritancebehaviour

* test/datafield fixes

* forgot that one

* adds more verbose validation

* This fixes the YAML hot reloading

* Replace yield break with Enumerable.Empty

* adds copiers

* aaaaaaaaaaaaa

* array fix
priority fix
misc fixes

* fix(?)

* fix.

* funny map serialization (wip)

* funny map serialization (wip)

* Add TODO

* adds proper info the validation

* Make yaml linter 5 times faster (~80% less execution time)

* Improves the error message for missing fields in the linter

* Include component name in unknown component type error node

* adds alwaysrelevant usa

* fixes mapsaving

* moved surpressor to analyzers proj

* warning cleanup & moves surpressor

* removes old msbuild targets

* Revert "Make yaml linter 5 times faster (~80% less execution time)"

This reverts commit 2ee4cc2c26.

* Add serialization to RobustServerSimulation and mock reflection methods
Fixes container tests

* Fix nullability warnings

* Improve yaml linter message feedback

* oops moment

* Add IEquatable, IComparable, ToString and operators to DataPosition
Rename it to NodeMark
Make it a readonly struct

* Remove try catch from enum parsing

* Make dependency management in serialization less bad

* Make dependencies an argument instead of a property on the serialization manager

* Clean up type serializers

* Improve validation messages and resourc epath checking

* Fix sprite error message

* reached perfection

Co-authored-by: Paul <ritter.paul1+git@googlemail.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Vera Aguilera Puerto <zddm@outlook.es>
2021-03-04 15:59:14 -08:00

515 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Lidgren.Network;
using Newtonsoft.Json;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Shared.Network.Messages.Handshake;
using Robust.Shared.Utility;
namespace Robust.Shared.Network
{
public partial class NetManager
{
private CancellationTokenSource? _cancelConnectTokenSource;
private ClientConnectionState _clientConnectState;
public ClientConnectionState ClientConnectState
{
get => _clientConnectState;
private set
{
_clientConnectState = value;
ClientConnectStateChanged?.Invoke(value);
}
}
public event Action<ClientConnectionState>? ClientConnectStateChanged;
private readonly
Dictionary<NetConnection, (CancellationTokenRegistration reg, TaskCompletionSource<string> tcs)>
_awaitingStatusChange
= new();
private readonly
Dictionary<NetConnection, (CancellationTokenRegistration, TaskCompletionSource<NetIncomingMessage>)>
_awaitingData =
new();
/// <inheritdoc />
public async void ClientConnect(string host, int port, string userNameRequest)
{
DebugTools.Assert(!IsServer, "Should never be called on the server.");
if (ClientConnectState == ClientConnectionState.Connected)
{
throw new InvalidOperationException("The client is already connected to a server.");
}
if (ClientConnectState != ClientConnectionState.NotConnecting)
{
throw new InvalidOperationException("A connect attempt is already in progress. Cancel it first.");
}
_cancelConnectTokenSource = new CancellationTokenSource();
var mainCancelToken = _cancelConnectTokenSource.Token;
ClientConnectState = ClientConnectionState.ResolvingHost;
Logger.DebugS("net", "Attempting to connect to {0} port {1}", host, port);
var resolveResult = await CCResolveHost(host, mainCancelToken);
if (resolveResult == null)
{
ClientConnectState = ClientConnectionState.NotConnecting;
return;
}
var (first, second) = resolveResult.Value;
ClientConnectState = ClientConnectionState.EstablishingConnection;
Logger.DebugS("net", "First attempt IP address is {0}, second attempt {1}", first, second);
var result = await CCHappyEyeballs(port, first, second, mainCancelToken);
if (result == null)
{
ClientConnectState = ClientConnectionState.NotConnecting;
return;
}
var (winningPeer, winningConnection) = result.Value;
ClientConnectState = ClientConnectionState.Handshake;
// We're connected start handshaking.
try
{
await CCDoHandshake(winningPeer, winningConnection, userNameRequest, mainCancelToken);
}
catch (TaskCanceledException)
{
winningPeer.Peer.Shutdown("Cancelled");
_toCleanNetPeers.Add(winningPeer.Peer);
ClientConnectState = ClientConnectionState.NotConnecting;
return;
}
catch (Exception e)
{
OnConnectFailed(e.Message);
Logger.ErrorS("net", "Exception during handshake: {0}", e);
winningPeer.Peer.Shutdown("Something happened.");
_toCleanNetPeers.Add(winningPeer.Peer);
ClientConnectState = ClientConnectionState.NotConnecting;
return;
}
ClientConnectState = ClientConnectionState.Connected;
Logger.DebugS("net", "Handshake completed, connection established.");
}
private async Task CCDoHandshake(NetPeerData peer, NetConnection connection, string userNameRequest,
CancellationToken cancel)
{
var authToken = _authManager.Token;
var pubKey = _authManager.PubKey;
var authServer = _authManager.Server;
var userId = _authManager.UserId;
var hasPubKey = !string.IsNullOrEmpty(pubKey);
var authenticate = !string.IsNullOrEmpty(authToken);
var msgLogin = new MsgLoginStart
{
UserName = userNameRequest,
CanAuth = authenticate,
NeedPubKey = !hasPubKey
};
var outLoginMsg = peer.Peer.CreateMessage();
msgLogin.WriteToBuffer(outLoginMsg);
peer.Peer.SendMessage(outLoginMsg, connection, NetDeliveryMethod.ReliableOrdered);
NetEncryption? encryption = null;
var response = await AwaitData(connection, cancel);
var loginSuccess = response.ReadBoolean();
response.ReadPadBits();
if (!loginSuccess)
{
// Need to authenticate, packet is MsgEncryptionRequest
var encRequest = new MsgEncryptionRequest();
encRequest.ReadFromBuffer(response);
var sharedSecret = new byte[AesKeyLength];
RandomNumberGenerator.Fill(sharedSecret);
encryption = new NetAESEncryption(peer.Peer, sharedSecret, 0, sharedSecret.Length);
byte[] keyBytes;
if (hasPubKey)
{
// public key provided by launcher.
keyBytes = Convert.FromBase64String(pubKey!);
}
else
{
// public key is gotten from handshake.
keyBytes = encRequest.PublicKey;
}
var rsaKey = RSA.Create();
rsaKey.ImportRSAPublicKey(keyBytes, out _);
var encryptedSecret = rsaKey.Encrypt(sharedSecret, RSAEncryptionPadding.OaepSHA256);
var encryptedVerifyToken = rsaKey.Encrypt(encRequest.VerifyToken, RSAEncryptionPadding.OaepSHA256);
var authHashBytes = MakeAuthHash(sharedSecret, keyBytes);
var authHash = Convert.ToBase64String(authHashBytes);
var joinReq = new JoinRequest {Hash = authHash};
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("SS14Auth", authToken);
var joinJson = JsonConvert.SerializeObject(joinReq);
var joinResp = await httpClient.PostAsync(authServer + "api/session/join",
new StringContent(joinJson, EncodingHelpers.UTF8, MediaTypeNames.Application.Json), cancel);
joinResp.EnsureSuccessStatusCode();
var encryptionResponse = new MsgEncryptionResponse
{
SharedSecret = encryptedSecret,
VerifyToken = encryptedVerifyToken,
UserId = userId!.Value.UserId
};
var outEncRespMsg = peer.Peer.CreateMessage();
encryptionResponse.WriteToBuffer(outEncRespMsg);
peer.Peer.SendMessage(outEncRespMsg, connection, NetDeliveryMethod.ReliableOrdered);
// Expect login success here.
response = await AwaitData(connection, cancel);
encryption.Decrypt(response);
}
var msgSuc = new MsgLoginSuccess();
msgSuc.ReadFromBuffer(response);
var channel = new NetChannel(this, connection, msgSuc.UserData, msgSuc.Type);
_channels.Add(connection, channel);
peer.AddChannel(channel);
_clientEncryption = encryption;
}
private static byte[] MakeAuthHash(byte[] sharedSecret, byte[] pkBytes)
{
Logger.DebugS("auth", "shared: {0}, pk: {1}", Convert.ToBase64String(sharedSecret), Convert.ToBase64String(pkBytes));
var incHash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
incHash.AppendData(sharedSecret);
incHash.AppendData(pkBytes);
return incHash.GetHashAndReset();
}
private async Task<(IPAddress first, IPAddress? second)?>
CCResolveHost(string host, CancellationToken mainCancelToken)
{
// Get list of potential IP addresses for the domain.
var endPoints = await ResolveDnsAsync(host);
if (mainCancelToken.IsCancellationRequested)
{
return null;
}
if (endPoints == null)
{
OnConnectFailed($"Unable to resolve domain '{host}'");
return null;
}
// Try to get an IPv6 and IPv4 address.
var ipv6 = endPoints.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetworkV6);
var ipv4 = endPoints.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork);
if (ipv4 == null && ipv6 == null)
{
OnConnectFailed($"Domain '{host}' has no associated IP addresses");
return null;
}
IPAddress first;
IPAddress? second = null;
if (ipv6 != null)
{
// If there's an IPv6 address try it first then the IPv4.
first = ipv6;
second = ipv4;
}
else
{
first = ipv4!;
}
return (first, second);
}
private async Task<(NetPeerData winningPeer, NetConnection winningConnection)?>
CCHappyEyeballs(int port, IPAddress first, IPAddress? second, CancellationToken mainCancelToken)
{
NetPeerData CreatePeerForIp(IPAddress address)
{
var config = _getBaseNetPeerConfig();
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
config.LocalAddress = IPAddress.IPv6Any;
}
else
{
config.LocalAddress = IPAddress.Any;
}
var peer = new NetPeer(config);
peer.Start();
var data = new NetPeerData(peer);
_netPeers.Add(data);
return data;
}
// Create first peer.
var firstPeer = CreatePeerForIp(first);
var firstConnection = firstPeer.Peer.Connect(new IPEndPoint(first, port));
NetPeerData? secondPeer = null;
NetConnection? secondConnection = null;
string? secondReason = null;
async Task<string> AwaitNonInitStatusChange(NetConnection connection, CancellationToken cancellationToken)
{
NetConnectionStatus status;
string reason;
do
{
reason = await AwaitStatusChange(connection, cancellationToken);
status = connection.Status;
} while (status == NetConnectionStatus.InitiatedConnect);
return reason;
}
async Task ConnectSecondDelayed(CancellationToken cancellationToken)
{
DebugTools.AssertNotNull(second);
// Connecting via second peer is delayed by 25ms to give an advantage to IPv6, if it works.
await Task.Delay(25, cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
secondPeer = CreatePeerForIp(second);
secondConnection = secondPeer.Peer.Connect(new IPEndPoint(second, port));
secondReason = await AwaitNonInitStatusChange(secondConnection, cancellationToken);
}
NetPeerData? winningPeer;
NetConnection? winningConnection;
string? firstReason = null;
try
{
if (second != null)
{
// We have two addresses to try.
var cancellation = CancellationTokenSource.CreateLinkedTokenSource(mainCancelToken);
var firstPeerChanged = AwaitNonInitStatusChange(firstConnection, cancellation.Token);
var secondPeerChanged = ConnectSecondDelayed(cancellation.Token);
var firstChange = await Task.WhenAny(firstPeerChanged, secondPeerChanged);
if (firstChange == firstPeerChanged)
{
Logger.DebugS("net", "First peer status changed.");
// First peer responded first.
if (firstConnection.Status == NetConnectionStatus.Connected)
{
// First peer won!
Logger.DebugS("net", "First peer succeeded.");
cancellation.Cancel();
if (secondPeer != null)
{
secondPeer.Peer.Shutdown("First connection attempt won.");
_toCleanNetPeers.Add(secondPeer.Peer);
}
winningPeer = firstPeer;
winningConnection = firstConnection;
}
else
{
// First peer failed, try the second one I guess.
Logger.DebugS("net", "First peer failed.");
firstPeer.Peer.Shutdown("You failed.");
_toCleanNetPeers.Add(firstPeer.Peer);
firstReason = firstPeerChanged.Result;
await secondPeerChanged;
winningPeer = secondPeer;
winningConnection = secondConnection;
}
}
else
{
if (secondConnection!.Status == NetConnectionStatus.Connected)
{
// Second peer won!
Logger.DebugS("net", "Second peer succeeded.");
cancellation.Cancel();
firstPeer.Peer.Shutdown("Second connection attempt won.");
_toCleanNetPeers.Add(firstPeer.Peer);
winningPeer = secondPeer;
winningConnection = secondConnection;
}
else
{
// First peer failed, try the second one I guess.
Logger.DebugS("net", "Second peer failed.");
secondPeer!.Peer.Shutdown("You failed.");
_toCleanNetPeers.Add(secondPeer.Peer);
firstReason = await firstPeerChanged;
winningPeer = firstPeer;
winningConnection = firstConnection;
}
}
}
else
{
// Only one address to try. Pretty straight forward.
firstReason = await AwaitNonInitStatusChange(firstConnection, mainCancelToken);
winningPeer = firstPeer;
winningConnection = firstConnection;
}
}
catch (TaskCanceledException)
{
firstPeer.Peer.Shutdown("Cancelled");
_toCleanNetPeers.Add(firstPeer.Peer);
if (secondPeer != null)
{
// ReSharper disable once PossibleNullReferenceException
secondPeer.Peer.Shutdown("Cancelled");
_toCleanNetPeers.Add(secondPeer.Peer);
}
return null;
}
// winningPeer can still be failed at this point.
// If it is, neither succeeded. RIP.
if (winningConnection!.Status != NetConnectionStatus.Connected)
{
winningPeer!.Peer.Shutdown("You failed");
_toCleanNetPeers.Add(winningPeer.Peer);
OnConnectFailed((secondReason ?? firstReason)!);
return null;
}
return (winningPeer!, winningConnection);
}
private Task<string> AwaitStatusChange(NetConnection connection, CancellationToken cancellationToken = default)
{
if (_awaitingStatusChange.ContainsKey(connection))
{
throw new InvalidOperationException();
}
var tcs = new TaskCompletionSource<string>();
CancellationTokenRegistration reg = default;
if (cancellationToken != default)
{
reg = cancellationToken.Register(() =>
{
_awaitingStatusChange.Remove(connection);
tcs.TrySetCanceled();
});
}
_awaitingStatusChange.Add(connection, (reg, tcs));
return tcs.Task;
}
private Task<NetIncomingMessage> AwaitData(NetConnection connection,
CancellationToken cancellationToken = default)
{
if (_awaitingData.ContainsKey(connection))
{
throw new InvalidOperationException("Cannot await data twice.");
}
DebugTools.Assert(!_channels.ContainsKey(connection),
"AwaitData cannot be used once a proper channel for the connection has been constructed, as it does not support encryption.");
var tcs = new TaskCompletionSource<NetIncomingMessage>();
CancellationTokenRegistration reg = default;
if (cancellationToken != default)
{
reg = cancellationToken.Register(() =>
{
_awaitingData.Remove(connection);
tcs.TrySetCanceled();
});
}
_awaitingData.Add(connection, (reg, tcs));
return tcs.Task;
}
public static async Task<IPAddress[]?> ResolveDnsAsync(string ipOrHost)
{
if (string.IsNullOrEmpty(ipOrHost))
{
throw new ArgumentException("Supplied string must not be empty", nameof(ipOrHost));
}
ipOrHost = ipOrHost.Trim();
if (IPAddress.TryParse(ipOrHost, out var ipAddress))
{
if (ipAddress.AddressFamily == AddressFamily.InterNetwork
|| ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
return new[] {ipAddress};
}
throw new ArgumentException("This method will not currently resolve other than IPv4 or IPv6 addresses");
}
try
{
var entry = await Dns.GetHostEntryAsync(ipOrHost);
return entry.AddressList;
}
catch (SocketException)
{
return null;
}
}
private sealed class JoinRequest
{
public string Hash = default!;
}
}
}