Editor/Core/DiscoveryProtocol.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
namespace TeamCreate;
public sealed record DiscoveryReply(string ProjectIdHash8, int SessionPort, string HostName, string SecretHex, string Manifest8);
public static class DiscoveryProtocol
{
public const int Port = 27846;
public const string QueryPrefix = "TC1?";
public const string ReplyPrefix = "TC1!";
public const int MaxMessageBytes = 512;
public const int MaxHostNameLength = 64;
public static string ProjectHash8(string projectId)
{
if (string.IsNullOrEmpty(projectId))
{
throw new ArgumentException("A discovery project hash requires a project id.", "projectId");
}
byte[] array = SHA256.HashData(Encoding.ASCII.GetBytes(projectId));
StringBuilder stringBuilder = new StringBuilder(8);
for (int i = 0; i < 4; i++)
{
stringBuilder.Append(array[i].ToString("x2", CultureInfo.InvariantCulture));
}
return stringBuilder.ToString();
}
public static string BuildQuery(string projectIdHash8)
{
if (!IsHex(projectIdHash8, 8))
{
throw new ArgumentException("A discovery query requires an 8-character lowercase hexadecimal project id hash.", "projectIdHash8");
}
return "TC1?pid=" + projectIdHash8 + "\n";
}
public static string BuildReply(string projectIdHash8, int sessionPort, string hostName, string secretHex, string manifest8)
{
if (!IsHex(projectIdHash8, 8))
{
throw new ArgumentException("A discovery reply requires an 8-character lowercase hexadecimal project id hash.", "projectIdHash8");
}
if (sessionPort < 1 || sessionPort > 65535)
{
throw new ArgumentOutOfRangeException("sessionPort", "A discovery reply requires a TCP session port between 1 and 65535.");
}
if (!IsHex(secretHex, 32))
{
throw new ArgumentException("A discovery reply requires a 32-character lowercase hexadecimal session secret (16 bytes).", "secretHex");
}
if (!IsHex(manifest8, 8))
{
throw new ArgumentException("A discovery reply requires an 8-character lowercase hexadecimal manifest revision.", "manifest8");
}
return "TC1!pid=" + projectIdHash8 + ";port=" + sessionPort.ToString(CultureInfo.InvariantCulture) + ";name=" + SanitizeHostName(hostName) + ";secret=" + secretHex + ";rev=" + manifest8 + "\n";
}
public static bool TryParseQuery(string text, out string projectIdHash8)
{
projectIdHash8 = null;
if (!TryGetPayload(text, "TC1?", out var payload))
{
return false;
}
if (!payload.StartsWith("pid=", StringComparison.Ordinal))
{
return false;
}
string text2 = payload.Substring(4);
if (!IsHex(text2, 8))
{
return false;
}
projectIdHash8 = text2;
return true;
}
public static bool TryParseReply(string text, out DiscoveryReply reply)
{
reply = null;
if (!TryGetPayload(text, "TC1!", out var payload))
{
return false;
}
Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
string[] array = payload.Split(';');
foreach (string text2 in array)
{
int num = text2.IndexOf('=');
if (num <= 0)
{
return false;
}
string key = text2.Substring(0, num);
if (!IsKey(key))
{
return false;
}
if (dictionary.ContainsKey(key))
{
return false;
}
dictionary[key] = text2.Substring(num + 1);
}
if (!dictionary.TryGetValue("pid", out var value) || !IsHex(value, 8))
{
return false;
}
if (!dictionary.TryGetValue("port", out var value2) || !TryParsePort(value2, out var port))
{
return false;
}
if (!dictionary.TryGetValue("name", out var value3) || !IsAcceptableHostName(value3))
{
return false;
}
if (!dictionary.TryGetValue("secret", out var value4) || !IsHex(value4, 32))
{
return false;
}
if (!dictionary.TryGetValue("rev", out var value5) || !IsHex(value5, 8))
{
return false;
}
reply = new DiscoveryReply(value, port, value3, value4, value5);
return true;
}
private static bool TryGetPayload(string text, string prefix, out string payload)
{
payload = null;
if (string.IsNullOrEmpty(text))
{
return false;
}
if (text.Length > 512)
{
return false;
}
string text2 = text;
if (text2.EndsWith("\n", StringComparison.Ordinal))
{
text2 = text2.Substring(0, text2.Length - 1);
}
if (text2.EndsWith("\r", StringComparison.Ordinal))
{
text2 = text2.Substring(0, text2.Length - 1);
}
string text3 = text2;
foreach (char c in text3)
{
if (c < ' ' || c > '~')
{
return false;
}
}
if (!text2.StartsWith(prefix, StringComparison.Ordinal))
{
return false;
}
payload = text2.Substring(prefix.Length);
return true;
}
private static bool IsKey(string key)
{
int length = key.Length;
if ((length < 1 || length > 16) ? true : false)
{
return false;
}
foreach (char c in key)
{
if ((c < 'a' || c > 'z') && (c < '0' || c > '9'))
{
return false;
}
}
return true;
}
private static bool IsHex(string text, int length)
{
if (text == null || text.Length != length)
{
return false;
}
foreach (char c in text)
{
if ((c < '0' || c > '9') && (c < 'a' || c > 'f'))
{
return false;
}
}
return true;
}
private static bool TryParsePort(string text, out int port)
{
port = 0;
bool flag = text == null;
bool flag2 = flag;
int length;
if (!flag2)
{
length = text.Length;
bool flag3 = ((length < 1 || length > 5) ? true : false);
flag2 = flag3;
}
if (flag2)
{
return false;
}
foreach (char c in text)
{
if (c < '0' || c > '9')
{
return false;
}
port = port * 10 + (c - 48);
}
length = port;
return length >= 1 && length <= 65535;
}
private static bool IsAcceptableHostName(string name)
{
return name.Length <= 64 && IsCleanText(name) && !name.Contains(';');
}
private static bool IsCleanText(string text)
{
foreach (char c in text)
{
if (c < ' ' || c > '~')
{
return false;
}
}
return true;
}
private static string SanitizeHostName(string hostName)
{
if (string.IsNullOrEmpty(hostName))
{
return string.Empty;
}
StringBuilder stringBuilder = new StringBuilder(Math.Min(hostName.Length, 64));
foreach (char c in hostName)
{
if (stringBuilder.Length >= 64)
{
break;
}
if (c >= ' ' && c <= '~' && c != ';')
{
stringBuilder.Append(c);
}
}
return stringBuilder.ToString();
}
}