Overwrite
Complete Overwrite of the Folder with the free shard. ServUO 57.3 has been added.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
#region Header
|
||||
// _,-'/-'/
|
||||
// . __,-; ,'( '/
|
||||
// \. `-.__`-._`:_,-._ _ , . ``
|
||||
// `:-._,------' ` _,`--` -: `_ , ` ,' :
|
||||
// `---..__,,--' (C) 2023 ` -'. -'
|
||||
// # Vita-Nex [http://core.vita-nex.com] #
|
||||
// {o)xxx|===============- # -===============|xxx(o}
|
||||
// # #
|
||||
#endregion
|
||||
|
||||
#region References
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
|
||||
using Server;
|
||||
using Server.Network;
|
||||
|
||||
using VitaNex.Crypto;
|
||||
#endregion
|
||||
|
||||
namespace VitaNex.Modules.WebSockets
|
||||
{
|
||||
public sealed class WebSocketsClientKey : CryptoHashCode
|
||||
{
|
||||
public override string Value => base.Value.Replace("-", String.Empty);
|
||||
|
||||
public WebSocketsClientKey(string key)
|
||||
: base(CryptoHashType.SHA1, String.Concat(key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
|
||||
{ }
|
||||
|
||||
public WebSocketsClientKey(GenericReader reader)
|
||||
: base(reader)
|
||||
{ }
|
||||
|
||||
public override void Serialize(GenericWriter writer)
|
||||
{
|
||||
base.Serialize(writer);
|
||||
|
||||
writer.SetVersion(0);
|
||||
}
|
||||
|
||||
public override void Deserialize(GenericReader reader)
|
||||
{
|
||||
base.Deserialize(reader);
|
||||
|
||||
reader.GetVersion();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class WebSocketsClient : NetState
|
||||
{
|
||||
public WebSocketsClientKey Key { get; private set; }
|
||||
|
||||
public TcpClient TcpClient { get; private set; }
|
||||
|
||||
public bool Connected => TcpClient != null && TcpClient.Connected;
|
||||
|
||||
public WebSocketsClient(TcpClient client, MessagePump p)
|
||||
: base(client.Client, p)
|
||||
{
|
||||
TcpClient = client;
|
||||
}
|
||||
|
||||
public WebSocketsClientKey ResolveKey(string key)
|
||||
{
|
||||
return Key ?? (Key = new WebSocketsClientKey(key));
|
||||
}
|
||||
|
||||
/*
|
||||
public override void Dispose(bool flush)
|
||||
{
|
||||
base.Dispose(flush);
|
||||
}
|
||||
|
||||
public override void Send(Packet p)
|
||||
{
|
||||
base.Send(p);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.ToString();
|
||||
}*/
|
||||
/*
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return base.GetHashCode();
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return base.Equals(obj);
|
||||
}*/
|
||||
/*
|
||||
public void Send(Packet p)
|
||||
{
|
||||
if (Connected)
|
||||
{
|
||||
NetState.Send(p);
|
||||
}
|
||||
}
|
||||
|
||||
public void Flush()
|
||||
{
|
||||
if (Connected)
|
||||
{
|
||||
NetState.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
VitaNexCore.TryCatch(() =>
|
||||
{
|
||||
if (!Connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WebSockets.Disconnected(this);
|
||||
|
||||
NetState.Dispose();
|
||||
NetState = null;
|
||||
}, e =>
|
||||
{
|
||||
lock (WebSockets.Clients)
|
||||
{
|
||||
WebSockets.Clients.Remove(this);
|
||||
}
|
||||
|
||||
WebSockets.CMOptions.ToConsole(e);
|
||||
});
|
||||
}*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#region Header
|
||||
// _,-'/-'/
|
||||
// . __,-; ,'( '/
|
||||
// \. `-.__`-._`:_,-._ _ , . ``
|
||||
// `:-._,------' ` _,`--` -: `_ , ` ,' :
|
||||
// `---..__,,--' (C) 2023 ` -'. -'
|
||||
// # Vita-Nex [http://core.vita-nex.com] #
|
||||
// {o)xxx|===============- # -===============|xxx(o}
|
||||
// # #
|
||||
#endregion
|
||||
|
||||
#region References
|
||||
using Server;
|
||||
#endregion
|
||||
|
||||
namespace VitaNex.Modules.WebSockets
|
||||
{
|
||||
public class WebSocketsOptions : CoreModuleOptions
|
||||
{
|
||||
[CommandProperty(WebSockets.Access)]
|
||||
public int Port { get; set; }
|
||||
|
||||
[CommandProperty(WebSockets.Access)]
|
||||
public int MaxConnections { get; set; }
|
||||
|
||||
public WebSocketsOptions()
|
||||
: base(typeof(WebSockets))
|
||||
{
|
||||
Port = 2594;
|
||||
MaxConnections = 1000;
|
||||
}
|
||||
|
||||
public WebSocketsOptions(GenericReader reader)
|
||||
: base(reader)
|
||||
{ }
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
|
||||
Port = 2594;
|
||||
MaxConnections = 1000;
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
|
||||
Port = 2594;
|
||||
MaxConnections = 1000;
|
||||
}
|
||||
|
||||
public override void Serialize(GenericWriter writer)
|
||||
{
|
||||
base.Serialize(writer);
|
||||
|
||||
var version = writer.SetVersion(0);
|
||||
|
||||
switch (version)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
writer.Write(Port);
|
||||
writer.Write(MaxConnections);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Deserialize(GenericReader reader)
|
||||
{
|
||||
base.Deserialize(reader);
|
||||
|
||||
var version = reader.GetVersion();
|
||||
|
||||
switch (version)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
Port = reader.ReadInt();
|
||||
MaxConnections = reader.ReadInt();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
452
Scripts/SubSystem/VitaNex/Core/Modules/WebSockets/WebSockets.cs
Normal file
452
Scripts/SubSystem/VitaNex/Core/Modules/WebSockets/WebSockets.cs
Normal file
@@ -0,0 +1,452 @@
|
||||
#region Header
|
||||
// _,-'/-'/
|
||||
// . __,-; ,'( '/
|
||||
// \. `-.__`-._`:_,-._ _ , . ``
|
||||
// `:-._,------' ` _,`--` -: `_ , ` ,' :
|
||||
// `---..__,,--' (C) 2023 ` -'. -'
|
||||
// # Vita-Nex [http://core.vita-nex.com] #
|
||||
// {o)xxx|===============- # -===============|xxx(o}
|
||||
// # #
|
||||
#endregion
|
||||
|
||||
#region References
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
using Server;
|
||||
using Server.Misc;
|
||||
#endregion
|
||||
|
||||
namespace VitaNex.Modules.WebSockets
|
||||
{
|
||||
public static partial class WebSockets
|
||||
{
|
||||
public const AccessLevel Access = AccessLevel.Administrator;
|
||||
|
||||
private static bool _Started;
|
||||
|
||||
private static readonly PollTimer _ActivityTimer;
|
||||
|
||||
public static TcpListener Listener { get; private set; }
|
||||
public static List<WebSocketsClient> Clients { get; private set; }
|
||||
|
||||
public static event Action<WebSocketsClient> OnConnected;
|
||||
public static event Action<WebSocketsClient> OnDisconnected;
|
||||
|
||||
private static readonly MethodInfo _IsPrivateNetwork = //
|
||||
typeof(ServerList).GetMethod("IsPrivateNetwork", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
|
||||
private static void AcquireListener()
|
||||
{
|
||||
if (!CMOptions.ModuleEnabled)
|
||||
{
|
||||
ReleaseListener();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Listener != null && ((IPEndPoint)Listener.LocalEndpoint).Port != CMOptions.Port)
|
||||
{
|
||||
ReleaseListener();
|
||||
}
|
||||
|
||||
if (Listener == null)
|
||||
{
|
||||
var address = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Select(adapter => adapter.GetIPProperties())
|
||||
.Select(
|
||||
properties => properties.UnicastAddresses.Select(unicast => unicast.Address)
|
||||
.FirstOrDefault(
|
||||
ip => !IPAddress.IsLoopback(ip) && ip.AddressFamily != AddressFamily.InterNetworkV6 &&
|
||||
(_IsPrivateNetwork == null || (bool)_IsPrivateNetwork.Invoke(null, new object[] { ip }))))
|
||||
.FirstOrDefault() ?? IPAddress.Any;
|
||||
|
||||
Listener = new TcpListener(address, CMOptions.Port);
|
||||
}
|
||||
|
||||
if (!Listener.Server.IsBound)
|
||||
{
|
||||
Listener.Start(CMOptions.MaxConnections);
|
||||
|
||||
CMOptions.ToConsole("Listening: {0}", Listener.LocalEndpoint);
|
||||
}
|
||||
|
||||
_Listening = true;
|
||||
}
|
||||
|
||||
private static void ReleaseListener()
|
||||
{
|
||||
if (Listener == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VitaNexCore.TryCatch(
|
||||
() =>
|
||||
{
|
||||
if (Listener.Server.IsBound)
|
||||
{
|
||||
Listener.Server.Disconnect(true);
|
||||
}
|
||||
});
|
||||
|
||||
VitaNexCore.TryCatch(Listener.Stop);
|
||||
|
||||
Listener = null;
|
||||
|
||||
_Listening = false;
|
||||
}
|
||||
|
||||
private static bool _Listening;
|
||||
|
||||
private static void ListenAsync()
|
||||
{
|
||||
AcquireListener();
|
||||
|
||||
if (Listener == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VitaNexCore.TryCatch(
|
||||
() => Listener.BeginAcceptTcpClient(
|
||||
r =>
|
||||
{
|
||||
var client = VitaNexCore.TryCatchGet(() => Listener.EndAcceptTcpClient(r), CMOptions.ToConsole);
|
||||
|
||||
if (client != null && client.Connected)
|
||||
{
|
||||
VitaNexCore.TryCatch(() => Connected(client), CMOptions.ToConsole);
|
||||
}
|
||||
|
||||
ListenAsync();
|
||||
},
|
||||
null),
|
||||
e =>
|
||||
{
|
||||
_Listening = false;
|
||||
CMOptions.ToConsole(e);
|
||||
});
|
||||
}
|
||||
|
||||
private static void Connected(TcpClient tcp)
|
||||
{
|
||||
if (tcp == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VitaNexCore.TryCatch(
|
||||
() =>
|
||||
{
|
||||
if (Listener != null && _Started)
|
||||
{
|
||||
Connected(new WebSocketsClient(tcp, Core.MessagePump));
|
||||
}
|
||||
else
|
||||
{
|
||||
tcp.Close();
|
||||
}
|
||||
},
|
||||
CMOptions.ToConsole);
|
||||
}
|
||||
|
||||
private static void Connected(WebSocketsClient client)
|
||||
{
|
||||
lock (Clients)
|
||||
{
|
||||
if (!Clients.Contains(client))
|
||||
{
|
||||
Clients.Add(client);
|
||||
}
|
||||
}
|
||||
|
||||
CMOptions.ToConsole("[{0}] Client connected: {1}", Clients.Count, client.Address);
|
||||
|
||||
if (OnConnected != null)
|
||||
{
|
||||
VitaNexCore.TryCatch(
|
||||
() => OnConnected(client),
|
||||
e =>
|
||||
{
|
||||
CMOptions.ToConsole(e);
|
||||
|
||||
client.Dispose();
|
||||
Disconnected(client);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void Disconnected(WebSocketsClient client)
|
||||
{
|
||||
if (OnDisconnected != null)
|
||||
{
|
||||
VitaNexCore.TryCatch(() => OnDisconnected(client), CMOptions.ToConsole);
|
||||
}
|
||||
|
||||
lock (Clients)
|
||||
{
|
||||
Clients.Remove(client);
|
||||
}
|
||||
|
||||
CMOptions.ToConsole("[{0}] Client disconnected: {1}", Clients.Count, client.Address);
|
||||
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
private static void Encode(string data, out byte[] buffer, out int length)
|
||||
{
|
||||
length = Encoding.UTF8.GetByteCount(data);
|
||||
buffer = new byte[length];
|
||||
|
||||
Encoding.UTF8.GetBytes(data, 0, data.Length, buffer, 0);
|
||||
}
|
||||
|
||||
private static void Decode(byte[] src, out string data)
|
||||
{
|
||||
data = Encoding.UTF8.GetString(src);
|
||||
}
|
||||
|
||||
private static void Compress(ref byte[] buffer, ref int length)
|
||||
{
|
||||
using (MemoryStream inS = new MemoryStream(buffer.Take(length).ToArray()), outS = new MemoryStream())
|
||||
{
|
||||
using (var ds = new DeflateStream(outS, CompressionMode.Compress))
|
||||
{
|
||||
inS.CopyTo(ds);
|
||||
|
||||
outS.Position = 0;
|
||||
}
|
||||
|
||||
buffer = outS.ToArray();
|
||||
length = buffer.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Decompress(ref byte[] buffer, ref int length)
|
||||
{
|
||||
using (MemoryStream inS = new MemoryStream(buffer.Take(length).ToArray()), outS = new MemoryStream())
|
||||
{
|
||||
using (var ds = new DeflateStream(inS, CompressionMode.Decompress))
|
||||
{
|
||||
ds.CopyTo(outS);
|
||||
|
||||
outS.Position = 0;
|
||||
}
|
||||
|
||||
buffer = outS.ToArray();
|
||||
length = buffer.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Send(
|
||||
WebSocketsClient client,
|
||||
string data,
|
||||
bool encode,
|
||||
bool compress,
|
||||
Action<WebSocketsClient, byte[]> callback)
|
||||
{
|
||||
VitaNexCore.TryCatch(
|
||||
() =>
|
||||
{
|
||||
int len;
|
||||
byte[] buffer;
|
||||
|
||||
if (encode)
|
||||
{
|
||||
Encode(data, out buffer, out len);
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer = data.Select(c => (byte)c).ToArray();
|
||||
len = buffer.Length;
|
||||
}
|
||||
|
||||
Send(client, buffer, len, compress, callback);
|
||||
},
|
||||
CMOptions.ToConsole);
|
||||
}
|
||||
|
||||
private static void Send(
|
||||
WebSocketsClient client,
|
||||
byte[] buffer,
|
||||
int len,
|
||||
bool compress,
|
||||
Action<WebSocketsClient, byte[]> callback)
|
||||
{
|
||||
var stream = client.TcpClient.GetStream();
|
||||
|
||||
if (compress)
|
||||
{
|
||||
Compress(ref buffer, ref len);
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
|
||||
while (count < len)
|
||||
{
|
||||
var block = buffer.Skip(count).Take(client.TcpClient.SendBufferSize).ToArray();
|
||||
|
||||
stream.Write(block, 0, block.Length);
|
||||
|
||||
count += block.Length;
|
||||
}
|
||||
|
||||
if (callback != null)
|
||||
{
|
||||
callback(client, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Receive(
|
||||
WebSocketsClient client,
|
||||
bool decompress,
|
||||
bool decode,
|
||||
Action<WebSocketsClient, string, byte[]> callback)
|
||||
{
|
||||
VitaNexCore.TryCatch(
|
||||
() =>
|
||||
{
|
||||
var stream = client.TcpClient.GetStream();
|
||||
|
||||
var buffer = new byte[client.TcpClient.ReceiveBufferSize];
|
||||
var len = buffer.Length;
|
||||
|
||||
stream.Read(buffer, 0, buffer.Length);
|
||||
|
||||
if (decompress)
|
||||
{
|
||||
Decompress(ref buffer, ref len);
|
||||
}
|
||||
|
||||
string data;
|
||||
|
||||
if (decode)
|
||||
{
|
||||
Decode(buffer, out data);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = new string(buffer.Select(b => (char)b).ToArray());
|
||||
}
|
||||
|
||||
if (callback != null)
|
||||
{
|
||||
callback(client, data, buffer);
|
||||
}
|
||||
},
|
||||
CMOptions.ToConsole);
|
||||
}
|
||||
|
||||
private static void HandleConnection(WebSocketsClient client)
|
||||
{
|
||||
VitaNexCore.TryCatch(
|
||||
() =>
|
||||
{
|
||||
if (client.Seeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
|
||||
Receive(
|
||||
client,
|
||||
false,
|
||||
true,
|
||||
(c, d, b) =>
|
||||
{
|
||||
if (d.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = d.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
lines = lines.Take(lines.Length - 1).ToArray();
|
||||
|
||||
if (CMOptions.ModuleDebug)
|
||||
{
|
||||
CMOptions.ToConsole(lines.Not(String.IsNullOrWhiteSpace).ToArray());
|
||||
}
|
||||
|
||||
lines.ForEach(
|
||||
line =>
|
||||
{
|
||||
line = line.Trim();
|
||||
|
||||
var header = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (header.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hk = header[0].Replace(":", String.Empty);
|
||||
|
||||
if (String.IsNullOrWhiteSpace(hk))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hv = header.Length > 1 ? String.Join(" ", header.Skip(1)) : String.Empty;
|
||||
|
||||
if (!headers.ContainsKey(hk))
|
||||
{
|
||||
headers.Add(hk, hv);
|
||||
}
|
||||
else
|
||||
{
|
||||
headers[hk] = hv;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (headers.Count > 0)
|
||||
{
|
||||
HandleHttpRequest(client, headers);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("No headers defined for WebSockets client handshake.", new SocketException());
|
||||
}
|
||||
},
|
||||
CMOptions.ToConsole);
|
||||
}
|
||||
|
||||
private static void HandleHttpRequest(WebSocketsClient client, Dictionary<string, string> headers)
|
||||
{
|
||||
//var uri = headers["GET"];
|
||||
//var origin = headers["Origin"];
|
||||
|
||||
var key = client.ResolveKey(headers["Sec-WebSocket-Key"]);
|
||||
|
||||
var answer = Convert.ToBase64String(Encoding.ASCII.GetBytes(key.Value));
|
||||
|
||||
var sendHeaders = new List<string>
|
||||
{
|
||||
"HTTP/1.1 101 Switching Protocols", //
|
||||
"Connection: Upgrade", //
|
||||
"Sec-WebSocket-Accept: " + answer, //
|
||||
"Upgrade: websocket" //
|
||||
};
|
||||
|
||||
Send(client, String.Join("\r\n", sendHeaders) + "\r\n\r\n", false, false, (c, d) => client.Start());
|
||||
|
||||
if (!CMOptions.ModuleDebug)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CMOptions.ToConsole("HEADERS>>>\n");
|
||||
CMOptions.ToConsole(sendHeaders.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#region Header
|
||||
// _,-'/-'/
|
||||
// . __,-; ,'( '/
|
||||
// \. `-.__`-._`:_,-._ _ , . ``
|
||||
// `:-._,------' ` _,`--` -: `_ , ` ,' :
|
||||
// `---..__,,--' (C) 2023 ` -'. -'
|
||||
// # Vita-Nex [http://core.vita-nex.com] #
|
||||
// {o)xxx|===============- # -===============|xxx(o}
|
||||
// # #
|
||||
#endregion
|
||||
|
||||
#region References
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Server;
|
||||
using Server.Network;
|
||||
#endregion
|
||||
|
||||
namespace VitaNex.Modules.WebSockets
|
||||
{
|
||||
[CoreModule("Web Sockets", "1.0.0.1")]
|
||||
public static partial class WebSockets
|
||||
{
|
||||
public static WebSocketsOptions CMOptions { get; private set; }
|
||||
|
||||
static WebSockets()
|
||||
{
|
||||
CMOptions = new WebSocketsOptions();
|
||||
|
||||
EventSink.ServerStarted += () => _Started = true;
|
||||
|
||||
Clients = new List<WebSocketsClient>();
|
||||
|
||||
OnConnected += HandleConnection;
|
||||
|
||||
_ActivityTimer = PollTimer.FromSeconds(
|
||||
60.0,
|
||||
() =>
|
||||
{
|
||||
if (!_Listening || Listener == null || Listener.Server == null || !Listener.Server.IsBound)
|
||||
{
|
||||
_Listening = false;
|
||||
ListenAsync();
|
||||
}
|
||||
|
||||
Clients.RemoveAll(c => !c.Connected);
|
||||
},
|
||||
() => CMOptions.ModuleEnabled && Clients.Count > 0);
|
||||
|
||||
NetState.CreatedCallback += ns =>
|
||||
{
|
||||
if (ns is WebSocketsClient)
|
||||
{
|
||||
var client = (WebSocketsClient)ns;
|
||||
|
||||
client.CompressionEnabled = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static void CMInvoke()
|
||||
{
|
||||
ListenAsync();
|
||||
}
|
||||
|
||||
private static void CMEnabled()
|
||||
{
|
||||
ListenAsync();
|
||||
}
|
||||
|
||||
private static void CMDisabled()
|
||||
{
|
||||
ReleaseListener();
|
||||
}
|
||||
|
||||
private static void CMSave()
|
||||
{ }
|
||||
|
||||
private static void CMLoad()
|
||||
{ }
|
||||
|
||||
private static void CMDisposed()
|
||||
{
|
||||
if (Listener == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Listener.Stop();
|
||||
Listener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user