namespace FinalFactory.Rendezvous.Contracts; public enum NatPunchPeerRole { HostPresence = 1, Host = 2, Client = 3, } public sealed class NatPunchRequestToken { public NatPunchPeerRole Role { get; set; } public MediationHandle MediationHandle { get; set; } public string Capability { get; set; } = string.Empty; public override string ToString() => "[NatPunchRequestToken: capability redacted]"; } public static class NatPunchRequestTokenCodec { public const int EncodedLength = ContractLimits.NatPunchRequestTokenCharacters; private const string VersionPrefix = "rv1:"; private const int HandleLength = 32; private const int CapabilityLength = ContractLimits.DerivedCredentialCharacters; private const char Separator = ':'; private const char Padding = '.'; public static string Encode( NatPunchPeerRole role, MediationHandle mediationHandle, string capability) { if (!TryGetRoleCode(role, out char roleCode) || mediationHandle.Value == Guid.Empty || capability is null || capability.Length != CapabilityLength || !ContractValidation.IsCapabilityValid(capability)) { throw new ArgumentException("The NAT punch request token fields are invalid."); } string payload = string.Concat( VersionPrefix, roleCode, Separator, mediationHandle.Value.ToString("N"), Separator, capability); return payload.PadRight(EncodedLength, Padding); } public static bool TryDecode(string? encoded, out NatPunchRequestToken? token) { token = null; if (encoded is null || encoded.Length != EncodedLength || !encoded.StartsWith(VersionPrefix, StringComparison.Ordinal) || !TryParseRole(encoded[VersionPrefix.Length], out NatPunchPeerRole role)) { return false; } int roleSeparator = VersionPrefix.Length + 1; int handleOffset = roleSeparator + 1; int capabilitySeparator = handleOffset + HandleLength; int capabilityOffset = capabilitySeparator + 1; int paddingOffset = capabilityOffset + CapabilityLength; string handleText = encoded.Substring(handleOffset, HandleLength); if (encoded[roleSeparator] != Separator || encoded[capabilitySeparator] != Separator || !Guid.TryParseExact(handleText, "N", out Guid handle) || handle == Guid.Empty || !string.Equals(handleText, handle.ToString("N"), StringComparison.Ordinal) || !ContainsOnlyPadding(encoded, paddingOffset)) { return false; } string capability = encoded.Substring(capabilityOffset, CapabilityLength); if (!ContractValidation.IsCapabilityValid(capability)) { return false; } token = new NatPunchRequestToken { Role = role, MediationHandle = new MediationHandle(handle), Capability = capability, }; return true; } private static bool TryGetRoleCode(NatPunchPeerRole role, out char code) { code = role switch { NatPunchPeerRole.HostPresence => 'p', NatPunchPeerRole.Host => 'h', NatPunchPeerRole.Client => 'c', _ => default, }; return code != default; } private static bool TryParseRole(char code, out NatPunchPeerRole role) { role = code switch { 'p' => NatPunchPeerRole.HostPresence, 'h' => NatPunchPeerRole.Host, 'c' => NatPunchPeerRole.Client, _ => default, }; return role != default; } private static bool ContainsOnlyPadding(string value, int offset) { for (int index = offset; index < value.Length; index++) { if (value[index] != Padding) { return false; } } return true; } }