sharp-chat/SharpChat.SockChat/SharpChatWebSocketServer.cs

172 lines
6.8 KiB
C#
Raw Permalink Normal View History

#nullable disable
2025-04-25 18:18:13 +00:00
using Fleck;
2025-04-28 12:29:11 +00:00
using Microsoft.Extensions.Logging;
2022-08-30 17:00:58 +02:00
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
2025-04-28 12:29:11 +00:00
using ZLogger;
2022-08-30 17:00:58 +02:00
// Near direct reimplementation of Fleck's WebSocketServer with address reusing
// Fleck's Socket wrapper doesn't provide any way to do this with the normally provided APIs
// https://github.com/statianzo/Fleck/blob/1.1.0/src/Fleck/WebSocketServer.cs
namespace SharpChat.SockChat;
2022-08-30 17:00:58 +02:00
public class SharpChatWebSocketServer : IWebSocketServer {
2025-04-28 12:29:11 +00:00
private readonly ILogger Logger;
private readonly string _scheme;
private readonly IPAddress _locationIP;
private Action<IWebSocketConnection> _config;
2022-08-30 17:00:58 +02:00
2025-04-28 12:29:11 +00:00
public SharpChatWebSocketServer(ILogger logger, string location, bool supportDualStack = true) {
Logger = logger;
Uri uri = new(location);
2022-08-30 17:00:58 +02:00
Port = uri.Port;
Location = location;
SupportDualStack = supportDualStack;
2022-08-30 17:00:58 +02:00
_locationIP = ParseIPAddress(uri);
_scheme = uri.Scheme;
Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP);
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
2022-08-30 17:00:58 +02:00
if(SupportDualStack && Type.GetType("Mono.Runtime") == null && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
2022-08-30 17:00:58 +02:00
}
ListenerSocket = new SocketWrapper(socket);
SupportedSubProtocols = [];
}
public ISocket ListenerSocket { get; set; }
public string Location { get; private set; }
public bool SupportDualStack { get; }
public int Port { get; private set; }
public X509Certificate2 Certificate { get; set; }
public SslProtocols EnabledSslProtocols { get; set; }
public IEnumerable<string> SupportedSubProtocols { get; set; }
public bool RestartAfterListenError { get; set; }
public bool IsSecure {
get { return _scheme == "wss" && Certificate != null; }
}
public void Dispose() {
ListenerSocket.Dispose();
GC.SuppressFinalize(this);
}
2022-08-30 17:00:58 +02:00
private static IPAddress ParseIPAddress(Uri uri) {
string ipStr = uri.Host;
if(ipStr == "0.0.0.0") {
return IPAddress.Any;
} else if(ipStr == "[0000:0000:0000:0000:0000:0000:0000:0000]") {
return IPAddress.IPv6Any;
} else {
try {
return IPAddress.Parse(ipStr);
} catch(Exception ex) {
throw new FormatException("Failed to parse the IP address part of the location. Please make sure you specify a valid IP address. Use 0.0.0.0 or [::] to listen on all interfaces.", ex);
}
2022-08-30 17:00:58 +02:00
}
}
2022-08-30 17:00:58 +02:00
public void Start(Action<IWebSocketConnection> config) {
IPEndPoint ipLocal = new(_locationIP, Port);
ListenerSocket.Bind(ipLocal);
ListenerSocket.Listen(100);
Port = ((IPEndPoint)ListenerSocket.LocalEndPoint).Port;
2025-04-28 12:29:11 +00:00
Logger.ZLogInformation($"Server started at {Location} (actual port {Port})");
if(_scheme == "wss") {
if(Certificate == null) {
2025-04-28 12:29:11 +00:00
Logger.ZLogError($"Scheme cannot be 'wss' without a Certificate");
return;
}
2022-08-30 17:00:58 +02:00
// makes dotnet shut up, TLS is handled by NGINX anyway
// if(EnabledSslProtocols == SslProtocols.None) {
// EnabledSslProtocols = SslProtocols.Tls;
2025-04-28 12:29:11 +00:00
// Logger.ZLogDebug($"Using default TLS 1.0 security protocol.");
// }
}
ListenForClients();
_config = config;
}
private void ListenForClients() {
ListenerSocket.Accept(OnClientConnect, e => {
2025-04-28 12:29:11 +00:00
Logger.ZLogError($"Listener socket is closed: {e}");
if(RestartAfterListenError) {
2025-04-28 12:29:11 +00:00
Logger.ZLogInformation($"Listener socket restarting");
2022-08-30 17:00:58 +02:00
try {
ListenerSocket.Dispose();
Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP);
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
ListenerSocket = new SocketWrapper(socket);
Start(_config);
2025-04-28 12:29:11 +00:00
Logger.ZLogInformation($"Listener socket restarted");
2023-02-07 16:01:56 +01:00
} catch(Exception ex) {
2025-04-28 12:29:11 +00:00
Logger.ZLogError($"Listener could not be restarted: {ex}");
2022-08-30 17:00:58 +02:00
}
}
});
}
2022-08-30 17:00:58 +02:00
private void OnClientConnect(ISocket clientSocket) {
if(clientSocket == null) return; // socket closed
2022-08-30 17:00:58 +02:00
2025-04-28 12:29:11 +00:00
Logger.ZLogDebug($"Client connected from {clientSocket.RemoteIpAddress}:{clientSocket.RemotePort}");
ListenForClients();
2022-08-30 17:00:58 +02:00
WebSocketConnection connection = null;
2022-08-30 17:00:58 +02:00
connection = new WebSocketConnection(
clientSocket,
_config,
bytes => RequestParser.Parse(bytes, _scheme),
r => {
try {
return HandlerFactory.BuildHandler(
r, s => connection.OnMessage(s), connection.Close, b => connection.OnBinary(b),
b => connection.OnPing(b), b => connection.OnPong(b)
);
} catch(WebSocketException) {
const string responseMsg = "HTTP/1.1 200 OK\r\n"
+ "Date: {0}\r\n"
+ "Server: SharpChat\r\n"
+ "Content-Length: {1}\r\n"
+ "Content-Type: text/html; charset=utf-8\r\n"
+ "Connection: close\r\n"
+ "\r\n"
+ "{2}";
string responseBody = File.Exists("http-motd.txt") ? File.ReadAllText("http-motd.txt") : "SharpChat";
clientSocket.Stream.Write(Encoding.UTF8.GetBytes(string.Format(
responseMsg, DateTimeOffset.Now.ToString("r"), responseBody.CountUtf8Bytes(), responseBody
)));
clientSocket.Close();
return null;
}
},
s => SubProtocolNegotiator.Negotiate(SupportedSubProtocols, s));
if(IsSecure) {
2025-04-28 12:29:11 +00:00
Logger.ZLogDebug($"Authenticating Secure Connection");
clientSocket
.Authenticate(Certificate,
EnabledSslProtocols,
connection.StartReceiving,
2025-04-28 12:29:11 +00:00
e => Logger.ZLogWarning($"Failed to Authenticate: {e}"));
} else {
connection.StartReceiving();
2022-08-30 17:00:58 +02:00
}
}
}