2025-08-19 10:29:48 +02:00

808 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Events;
using Microsoft.Extensions.Logging;
namespace WsTelemetry;
[MinimumApiVersion(175)]
public class WebSocketTelemetryPlugin : BasePlugin
{
public override string ModuleName => "WS Telemetry";
public override string ModuleVersion => "1.2.0";
public override string ModuleAuthor => "you + ChatGPT";
public override string ModuleDescription => "WS(S)-Server: broadcastet Spieler-Position/ViewAngles + Nade-Events";
// --- Konfiguration ---
private volatile bool _enabled = false;
private volatile int _sendHz = 10;
private volatile string _mapName = "";
// Bind-Info: ws://host:port/path oder wss://host:port/path
private volatile string _bindHost = "0.0.0.0";
private volatile int _bindPort = 8081;
private volatile string _bindPath = "/telemetry";
private volatile bool _useTls = false;
// TLS Zertifikat (PFX)
private volatile string _certPath = "";
private volatile string _certPassword = "";
private X509Certificate2? _serverCert;
// --- Server / Clients ---
private TcpListener? _listener;
private CancellationTokenSource? _serverCts;
private Task? _acceptTask;
private volatile bool _serverRunning = false;
private class ClientState
{
public required TcpClient Tcp;
public required Stream Stream; // NetworkStream oder SslStream
public readonly object SendLock = new();
public readonly CancellationTokenSource Cts = new();
}
private readonly ConcurrentDictionary<int, ClientState> _clients = new();
private int _clientSeq = 0;
// --- Outgoing Queue (für Events) ---
private readonly ConcurrentQueue<string> _outbox = new();
private readonly AutoResetEvent _sendSignal = new(false);
// --- Tick / Sampling ---
private double _accumulator = 0.0;
private const double MaxFrameDt = 0.25;
private DateTime _lastTick = DateTime.UtcNow;
public override void Load(bool hotReload)
{
Logger.LogInformation("[WS] Plugin geladen. Kommandos: css_ws_enable, css_ws_url, css_ws_rate, css_ws_cert, css_ws_certpwd");
RegisterListener<Listeners.OnTick>(OnTick);
// Mapname initial erfassen und auf Mapwechsel reagieren
_mapName = Server.MapName ?? "";
RegisterListener<Listeners.OnMapStart>(OnMapStart);
}
public override void Unload(bool hotReload)
{
StopWebSocket();
}
private void OnMapStart(string newMap)
{
_mapName = newMap ?? Server.MapName ?? "";
var payload = JsonSerializer.Serialize(new
{
type = "map",
name = _mapName,
t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
Broadcast(payload);
Logger.LogInformation($"[WS] Map gewechselt: '{_mapName}' an Clients gesendet.");
}
// =========================
// Konsolen-Kommandos
// =========================
[ConsoleCommand("css_ws_enable", "Aktiviert/Deaktiviert den integrierten WS(S)-Server: css_ws_enable 1|0")]
[CommandHelper(minArgs: 1, usage: "<1|0>")]
public void CmdEnable(CCSPlayerController? caller, CommandInfo cmd)
{
var val = cmd.GetArg(1);
bool enable = val == "1" || val.Equals("true", StringComparison.OrdinalIgnoreCase);
if (enable == _enabled)
{
cmd.ReplyToCommand($"[WS] Bereits {_enabled}.");
return;
}
_enabled = enable;
cmd.ReplyToCommand($"[WS] Enabled = {_enabled}");
if (_enabled) StartWebSocket();
else StopWebSocket();
}
[ConsoleCommand("css_ws_sendmap", "Sendet die aktuelle Karte an alle verbundenen Clients")]
public void CmdSendMap(CCSPlayerController? caller, CommandInfo cmd)
{
var payload = JsonSerializer.Serialize(new
{
type = "map",
name = _mapName,
t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
Broadcast(payload);
cmd.ReplyToCommand($"[WS] Map '{_mapName}' an Clients gesendet.");
}
[ConsoleCommand("css_ws_url", "Setzt Bind-Host/Port/Pfad als ws[s]://host:port/path")]
[CommandHelper(minArgs: 1, usage: "<ws[s]://host:port/path>")]
public void CmdUrl(CCSPlayerController? caller, CommandInfo cmd)
{
var url = cmd.GetArg(1);
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || (uri.Scheme != "ws" && uri.Scheme != "wss"))
{
cmd.ReplyToCommand($"[WS] Ungültige URL: {url}");
return;
}
_useTls = uri.Scheme == "wss";
_bindHost = string.IsNullOrWhiteSpace(uri.Host) ? "0.0.0.0" : uri.Host;
_bindPort = uri.IsDefaultPort ? (_useTls ? 443 : 80) : uri.Port;
_bindPath = string.IsNullOrEmpty(uri.AbsolutePath) ? "/" : uri.AbsolutePath;
if (_bindHost == "127.0.0.1") _bindHost = "0.0.0.0";
var scheme = _useTls ? "wss" : "ws";
cmd.ReplyToCommand($"[WS] Bind = {scheme}://{_bindHost}:{_bindPort}{_bindPath}");
if (_enabled) { StopWebSocket(); StartWebSocket(); }
}
[ConsoleCommand("css_ws_rate", "Sendefrequenz in Hz (Standard 10)")]
[CommandHelper(minArgs: 1, usage: "<hz>")]
public void CmdRate(CCSPlayerController? caller, CommandInfo cmd)
{
if (int.TryParse(cmd.GetArg(1), out var hz) && hz > 0 && hz <= 128)
{
_sendHz = hz;
cmd.ReplyToCommand($"[WS] Sendefrequenz = {_sendHz} Hz");
}
else cmd.ReplyToCommand("[WS] Ungültig. Bereich: 1..128");
}
[ConsoleCommand("css_ws_cert", "Setzt das TLS-Zertifikat (PFX-Datei)")]
[CommandHelper(minArgs: 1, usage: "<pfad-zum.pfx>")]
public void CmdCert(CCSPlayerController? caller, CommandInfo cmd)
{
var input = cmd.GetArg(1);
_certPath = Path.IsPathRooted(input) ? input : Path.Combine(ModuleDirectory, input);
_serverCert = null; // neu laden beim Start
cmd.ReplyToCommand($"[WS] Zertifikatspfad gesetzt: '{_certPath}'");
if (_enabled && _useTls) { StopWebSocket(); StartWebSocket(); }
}
[ConsoleCommand("css_ws_certpwd", "Setzt das Passwort für das PFX-Zertifikat (oder '-' zum Leeren)")]
[CommandHelper(minArgs: 1, usage: "<passwort|->")]
public void CmdCertPwd(CCSPlayerController? caller, CommandInfo cmd)
{
var pwd = cmd.GetArg(1);
_certPassword = pwd == "-" ? "" : pwd;
_serverCert = null; // neu laden beim Start
cmd.ReplyToCommand($"[WS] Zertifikatspasswort {(string.IsNullOrEmpty(_certPassword) ? "gelöscht" : "gesetzt")}.");
if (_enabled && _useTls) { StopWebSocket(); StartWebSocket(); }
}
// =========================
// Tick / Spieler-Snapshot
// =========================
private void OnTick()
{
if (!_enabled || !_serverRunning) return;
var now = DateTime.UtcNow;
var dt = (now - _lastTick).TotalSeconds;
_lastTick = now;
if (dt > MaxFrameDt) dt = MaxFrameDt;
_accumulator += dt;
var targetDt = 1.0 / Math.Max(1, _sendHz);
if (_accumulator < targetDt) return;
_accumulator = 0;
var list = new System.Collections.Generic.List<object>();
foreach (var p in Utilities.GetPlayers())
{
try
{
if (p == null || !p.IsValid || p.IsBot || p.IsHLTV) continue;
var pawnHandle = p.PlayerPawn;
if (pawnHandle == null || !pawnHandle.IsValid) continue;
var pawn = pawnHandle.Value;
if (pawn == null) continue;
var pos = pawn.AbsOrigin;
var ang = pawn.EyeAngles;
list.Add(new
{
steamId = p.AuthorizedSteamID?.SteamId64 ?? 0UL,
name = p.PlayerName,
team = p.TeamNum,
pos = new { x = pos.X, y = pos.Y, z = pos.Z },
view = new { pitch = ang.X, yaw = ang.Y, roll = ang.Z }
});
}
catch { }
}
if (list.Count == 0) return;
var payload = new
{
type = "tick",
t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
players = list
};
Broadcast(JsonSerializer.Serialize(payload));
}
// =========================
// Game-Events: Granaten
// =========================
[GameEventHandler]
public HookResult OnGrenadeThrown(EventGrenadeThrown ev, GameEventInfo info)
{
try
{
var p = ev.Userid;
Enqueue(new
{
type = "nade",
sub = "thrown",
t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
steamId = p?.AuthorizedSteamID?.SteamId64 ?? 0UL,
name = p?.PlayerName ?? "",
weapon = ev.Weapon
});
}
catch { }
return HookResult.Continue;
}
[GameEventHandler] public HookResult OnHeDetonate (EventHegrenadeDetonate ev, GameEventInfo info) => EnqueueNadeDet(ev, "he");
[GameEventHandler] public HookResult OnFlashDetonate (EventFlashbangDetonate ev, GameEventInfo info) => EnqueueNadeDet(ev, "flash");
[GameEventHandler] public HookResult OnSmokeDetonate (EventSmokegrenadeDetonate ev, GameEventInfo info) => EnqueueNadeDet(ev, "smoke");
[GameEventHandler] public HookResult OnDecoyDetonate (EventDecoyDetonate ev, GameEventInfo info) => EnqueueNadeDet(ev, "decoy");
[GameEventHandler] public HookResult OnMolotovDetonate (EventMolotovDetonate ev, GameEventInfo info) => EnqueueNadeDet(ev, "molotov");
private HookResult EnqueueNadeDet(GameEvent ev, string kind)
{
try
{
dynamic d = ev;
var p = d.Userid as CCSPlayerController;
Enqueue(new
{
type = "nade",
sub = "detonate",
nade = kind,
t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
steamId = p?.AuthorizedSteamID?.SteamId64 ?? 0UL,
name = p?.PlayerName ?? "",
pos = new { x = (float)d.X, y = (float)d.Y, z = (float)d.Z }
});
}
catch { }
return HookResult.Continue;
}
private HookResult Enqueue(object obj)
{
if (!_enabled || !_serverRunning) return HookResult.Continue;
_outbox.Enqueue(JsonSerializer.Serialize(obj));
_sendSignal.Set();
return HookResult.Continue;
}
private bool TryLoadCertificate(out string usedPath)
{
usedPath = _certPath;
try
{
string pluginDir = ModuleDirectory; // CSS stellt das bereit
// Wenn kein Pfad konfiguriert: im Plugin-Ordner suchen
if (string.IsNullOrWhiteSpace(usedPath))
{
var def = Path.Combine(pluginDir, "cert.pfx");
if (File.Exists(def))
{
usedPath = def;
}
else
{
var files = Directory.GetFiles(pluginDir, "*.pfx", SearchOption.TopDirectoryOnly);
if (files.Length > 0)
usedPath = files[0];
}
}
else
{
// Relativen Pfad relativ zum Plugin-Ordner interpretieren
if (!Path.IsPathRooted(usedPath))
usedPath = Path.Combine(pluginDir, usedPath);
}
if (string.IsNullOrWhiteSpace(usedPath) || !File.Exists(usedPath))
{
Logger.LogWarning($"[WS] Kein PFX gefunden im Plugin-Ordner ({pluginDir}). " +
"Lege z.B. 'cert.pfx' dort ab oder setze mit css_ws_cert <pfad>.");
return false;
}
_serverCert = new X509Certificate2(
usedPath,
string.IsNullOrEmpty(_certPassword) ? null : _certPassword,
X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.Exportable
);
// Erfolgs-Hinweis
try
{
Logger.LogInformation($"[WS] TLS-Zertifikat geladen: {Path.GetFileName(usedPath)} | " +
$"Subject: {_serverCert.Subject} | Gültig bis: {_serverCert.NotAfter:u}");
}
catch
{
Logger.LogInformation($"[WS] TLS-Zertifikat geladen: {Path.GetFileName(usedPath)}");
}
return true;
}
catch (Exception ex)
{
Logger.LogError($"[WS] Zertifikat konnte nicht geladen werden: {ex.Message}");
_serverCert = null;
return false;
}
}
// =========================
// WS(S)-Server / Broadcast
// =========================
private void StartWebSocket()
{
StopWebSocket();
try
{
if (_useTls)
{
if (!TryLoadCertificate(out var usedPath))
throw new Exception("TLS aktiv, aber kein funktionsfähiges PFX gefunden.");
}
IPAddress ip;
if (!IPAddress.TryParse(_bindHost, out ip))
ip = IPAddress.Any;
_listener = new TcpListener(ip, _bindPort);
_listener.Start();
_serverCts = new CancellationTokenSource();
_serverRunning = true;
var scheme = _useTls ? "wss" : "ws";
Logger.LogInformation($"[WS] Server lauscht auf {scheme}://{_bindHost}:{_bindPort}{_bindPath}");
// beim Start aktuelle Map an bereits verbundene (falls Hot-Reload) senden
_mapName = string.IsNullOrEmpty(Server.MapName) ? _mapName : Server.MapName!;
var startPayload = JsonSerializer.Serialize(new
{
type = "map",
name = _mapName,
t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
Broadcast(startPayload);
_acceptTask = Task.Run(async () =>
{
var ct = _serverCts!.Token;
while (!ct.IsCancellationRequested)
{
TcpClient? tcp = null;
try
{
tcp = await _listener!.AcceptTcpClientAsync(ct);
_ = HandleClientAsync(tcp, ct);
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
Logger.LogWarning($"[WS] Accept-Fehler: {ex.Message}");
tcp?.Close();
await Task.Delay(250, ct);
}
}
});
// Sender, der die Outbox entleert
_ = Task.Run(async () =>
{
var ct = _serverCts!.Token;
while (!ct.IsCancellationRequested)
{
if (_outbox.IsEmpty) _sendSignal.WaitOne(200);
while (_outbox.TryDequeue(out var msg))
Broadcast(msg);
await Task.Delay(1, ct);
}
});
}
catch (Exception ex)
{
Logger.LogError($"[WS] Start fehlgeschlagen: {ex.Message}");
StopWebSocket();
}
}
private void StopWebSocket()
{
_serverRunning = false;
try { _serverCts?.Cancel(); } catch { }
try { _listener?.Stop(); } catch { }
_listener = null;
foreach (var kv in _clients)
{
try { kv.Value.Cts.Cancel(); } catch { }
try { kv.Value.Stream.Close(); } catch { }
try { kv.Value.Tcp.Close(); } catch { }
}
_clients.Clear();
_serverCts = null;
_acceptTask = null;
}
private void Broadcast(string text)
{
foreach (var kv in _clients)
{
var id = kv.Key;
var c = kv.Value;
try
{
SendTextFrame(c, text);
}
catch
{
_clients.TryRemove(id, out _);
try { c.Cts.Cancel(); } catch { }
try { c.Stream.Close(); } catch { }
try { c.Tcp.Close(); } catch { }
}
}
}
private async Task HandleClientAsync(TcpClient tcp, CancellationToken serverCt)
{
var id = Interlocked.Increment(ref _clientSeq);
tcp.NoDelay = true;
// Basis-Stream
var baseStream = tcp.GetStream();
baseStream.ReadTimeout = 10000;
baseStream.WriteTimeout = 10000;
// Optional TLS
Stream stream = baseStream;
SslStream? ssl = null;
try
{
if (_useTls)
{
ssl = new SslStream(baseStream, leaveInnerStreamOpen: false);
await ssl.AuthenticateAsServerAsync(
_serverCert!,
clientCertificateRequired: false,
enabledSslProtocols: SslProtocols.Tls13 | SslProtocols.Tls12,
checkCertificateRevocation: false
);
stream = ssl;
}
if (!await DoHandshakeAsync(stream, serverCt))
{
tcp.Close();
return;
}
var state = new ClientState { Tcp = tcp, Stream = stream };
_clients[id] = state;
Logger.LogInformation($"[WS] Client #{id} verbunden. Aktive: {_clients.Count}");
// Dem neuen Client die aktuelle Map sofort schicken
try
{
var mapPayload = JsonSerializer.Serialize(new
{
type = "map",
name = _mapName,
t = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
SendTextFrame(state, mapPayload);
}
catch { /* wenn der Client beim Connect gleich schließt, ignorieren */ }
await ReceiveLoop(state, serverCt);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Logger.LogWarning($"[WS] Client #{id} Fehler: {ex.Message}");
}
finally
{
_clients.TryRemove(id, out _);
try { stream.Close(); } catch { }
try { ssl?.Dispose(); } catch { }
try { baseStream.Close(); } catch { }
try { tcp.Close(); } catch { }
Logger.LogInformation($"[WS] Client #{id} getrennt. Aktive: {_clients.Count}");
}
}
// --- Minimaler WebSocket-Server: Handshake + Frames ---
private static async Task<string> ReadHeadersAsync(Stream s, CancellationToken ct)
{
var buf = new byte[8192];
using var ms = new MemoryStream();
while (true)
{
int n = await s.ReadAsync(buf.AsMemory(0, buf.Length), ct);
if (n <= 0) break;
ms.Write(buf, 0, n);
if (ms.Length >= 4)
{
var b = ms.GetBuffer();
for (int i = 3; i < ms.Length; i++)
{
if (b[i - 3] == '\r' && b[i - 2] == '\n' && b[i - 1] == '\r' && b[i] == '\n')
{
return Encoding.ASCII.GetString(b, 0, i + 1);
}
}
}
if (ms.Length > 65536) throw new Exception("Header zu groß");
}
return Encoding.ASCII.GetString(ms.ToArray());
}
private async Task<bool> DoHandshakeAsync(Stream stream, CancellationToken ct)
{
var header = await ReadHeadersAsync(stream, ct);
if (!header.StartsWith("GET ", StringComparison.OrdinalIgnoreCase))
return false;
var firstLineEnd = header.IndexOf("\r\n", StringComparison.Ordinal);
var firstLine = firstLineEnd > 0 ? header[..firstLineEnd] : header;
var parts = firstLine.Split(' ');
if (parts.Length < 2) return false;
var path = parts[1];
if (!path.StartsWith(_bindPath, StringComparison.Ordinal))
{
var notFound = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n";
var bytes = Encoding.ASCII.GetBytes(notFound);
await stream.WriteAsync(bytes, ct);
await stream.FlushAsync(ct);
return false;
}
string? wsKey = null;
foreach (var line in header.Split("\r\n"))
{
var idx = line.IndexOf(':');
if (idx <= 0) continue;
var name = line[..idx].Trim();
var value = line[(idx + 1)..].Trim();
if (name.Equals("Sec-WebSocket-Key", StringComparison.OrdinalIgnoreCase))
wsKey = value;
}
if (string.IsNullOrEmpty(wsKey))
return false;
var accept = ComputeWebSocketAccept(wsKey);
var resp =
"HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
$"Sec-WebSocket-Accept: {accept}\r\n" +
"\r\n";
var respBytes = Encoding.ASCII.GetBytes(resp);
await stream.WriteAsync(respBytes, ct);
await stream.FlushAsync(ct);
return true;
}
private static string ComputeWebSocketAccept(string key)
{
const string guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
var bytes = Encoding.ASCII.GetBytes(key + guid);
var hash = SHA1.HashData(bytes);
return Convert.ToBase64String(hash);
}
private async Task ReceiveLoop(ClientState state, CancellationToken serverCt)
{
var s = state.Stream;
var buf2 = new byte[2];
while (!serverCt.IsCancellationRequested && !state.Cts.IsCancellationRequested)
{
int r = await ReadExactAsync(s, buf2, 0, 2, serverCt);
if (r == 0) break;
byte b0 = buf2[0]; // FIN + opcode
byte b1 = buf2[1]; // MASK + payload len
byte opcode = (byte)(b0 & 0x0F);
bool masked = (b1 & 0x80) != 0;
ulong len = (ulong)(b1 & 0x7F);
if (len == 126)
{
r = await ReadExactAsync(s, buf2, 0, 2, serverCt);
if (r == 0) break;
len = (ulong)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(buf2, 0));
}
else if (len == 127)
{
var b8 = new byte[8];
r = await ReadExactAsync(s, b8, 0, 8, serverCt);
if (r == 0) break;
if (BitConverter.IsLittleEndian) Array.Reverse(b8);
len = BitConverter.ToUInt64(b8, 0);
}
byte[] mask = Array.Empty<byte>();
if (masked)
{
mask = new byte[4];
r = await ReadExactAsync(s, mask, 0, 4, serverCt);
if (r == 0) break;
}
byte[] payload = Array.Empty<byte>();
if (len > 0)
{
payload = new byte[len];
r = await ReadExactAsync(s, payload, 0, (int)len, serverCt);
if (r == 0) break;
if (masked)
for (int i = 0; i < payload.Length; i++)
payload[i] = (byte)(payload[i] ^ mask[i % 4]);
}
if (opcode == 0x8) // Close
{
await SendCloseFrame(state);
break;
}
else if (opcode == 0x9) // Ping -> Pong
{
await SendPongFrame(state, payload);
}
// Textframes (0x1) werden ignoriert
}
}
private static async Task<int> ReadExactAsync(Stream s, byte[] buf, int off, int len, CancellationToken ct)
{
int got = 0;
while (got < len)
{
int n = await s.ReadAsync(buf.AsMemory(off + got, len - got), ct);
if (n <= 0) return got;
got += n;
}
return got;
}
private void SendTextFrame(ClientState c, string text)
{
var payload = Encoding.UTF8.GetBytes(text);
using var ms = new MemoryStream(capacity: 2 + payload.Length + 10);
ms.WriteByte(0x81); // FIN + Text
if (payload.Length <= 125)
{
ms.WriteByte((byte)payload.Length);
}
else if (payload.Length <= ushort.MaxValue)
{
ms.WriteByte(126);
var lenBytes = BitConverter.GetBytes((ushort)payload.Length);
if (BitConverter.IsLittleEndian) Array.Reverse(lenBytes);
ms.Write(lenBytes, 0, 2);
}
else
{
ms.WriteByte(127);
var lenBytes = BitConverter.GetBytes((ulong)payload.Length);
if (BitConverter.IsLittleEndian) Array.Reverse(lenBytes);
ms.Write(lenBytes, 0, 8);
}
ms.Write(payload, 0, payload.Length);
lock (c.SendLock)
{
var buf = ms.GetBuffer();
c.Stream.Write(buf, 0, (int)ms.Length);
c.Stream.Flush();
}
}
private static Task SendPongFrame(ClientState c, byte[] pingPayload)
{
var header = new MemoryStream(2 + pingPayload.Length);
header.WriteByte(0x8A); // FIN + Pong
if (pingPayload.Length <= 125)
{
header.WriteByte((byte)pingPayload.Length);
}
else if (pingPayload.Length <= ushort.MaxValue)
{
header.WriteByte(126);
var lenBytes = BitConverter.GetBytes((ushort)pingPayload.Length);
if (BitConverter.IsLittleEndian) Array.Reverse(lenBytes);
header.Write(lenBytes, 0, 2);
}
else
{
header.WriteByte(127);
var lenBytes = BitConverter.GetBytes((ulong)pingPayload.Length);
if (BitConverter.IsLittleEndian) Array.Reverse(lenBytes);
header.Write(lenBytes, 0, 8);
}
var buf = header.ToArray();
lock (c.SendLock)
{
c.Stream.Write(buf, 0, buf.Length);
if (pingPayload.Length > 0) c.Stream.Write(pingPayload, 0, pingPayload.Length);
c.Stream.Flush();
}
return Task.CompletedTask;
}
private static Task SendCloseFrame(ClientState c)
{
var frame = new byte[] { 0x88, 0x00 }; // Close, no payload
lock (c.SendLock)
{
c.Stream.Write(frame, 0, frame.Length);
c.Stream.Flush();
}
return Task.CompletedTask;
}
}