2025-05-03 02:49:51 +00:00
#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
2025-05-03 02:49:51 +00:00
namespace SharpChat.SockChat ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
public class SharpChatWebSocketServer : IWebSocketServer {
2025-04-28 12:29:11 +00:00
private readonly ILogger Logger ;
2025-04-26 23:15:54 +00:00
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 ;
2025-04-26 23:15:54 +00:00
Uri uri = new ( location ) ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
Port = uri . Port ;
Location = location ;
SupportDualStack = supportDualStack ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00: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
2025-04-26 23:15:54 +00: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
}
2025-04-26 23:15:54 +00: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
2025-04-26 23:15:54 +00: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
}
2025-04-26 23:15:54 +00:00
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00: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})" ) ;
2025-04-26 23:15:54 +00:00
if ( _scheme = = "wss" ) {
if ( Certificate = = null ) {
2025-04-28 12:29:11 +00:00
Logger . ZLogError ( $"Scheme cannot be 'wss' without a Certificate" ) ;
2025-04-26 23:15:54 +00:00
return ;
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00: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.");
2025-04-26 23:15:54 +00:00
// }
}
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}" ) ;
2025-04-26 23:15:54 +00:00
if ( RestartAfterListenError ) {
2025-04-28 12:29:11 +00:00
Logger . ZLogInformation ( $"Listener socket restarting" ) ;
2022-08-30 17:00:58 +02:00
try {
2025-04-26 23:15:54 +00:00
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
}
}
2025-04-26 23:15:54 +00:00
} ) ;
}
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00: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}" ) ;
2025-04-26 23:15:54 +00:00
ListenForClients ( ) ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00:00
WebSocketConnection connection = null ;
2022-08-30 17:00:58 +02:00
2025-04-26 23:15:54 +00: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 (
2025-05-03 02:49:51 +00:00
responseMsg , DateTimeOffset . Now . ToString ( "r" ) , responseBody . CountUtf8Bytes ( ) , responseBody
2025-04-26 23:15:54 +00:00
) ) ) ;
clientSocket . Close ( ) ;
return null ;
}
} ,
s = > SubProtocolNegotiator . Negotiate ( SupportedSubProtocols , s ) ) ;
if ( IsSecure ) {
2025-04-28 12:29:11 +00:00
Logger . ZLogDebug ( $"Authenticating Secure Connection" ) ;
2025-04-26 23:15:54 +00:00
clientSocket
. Authenticate ( Certificate ,
EnabledSslProtocols ,
connection . StartReceiving ,
2025-04-28 12:29:11 +00:00
e = > Logger . ZLogWarning ( $"Failed to Authenticate: {e}" ) ) ;
2025-04-26 23:15:54 +00:00
} else {
connection . StartReceiving ( ) ;
2022-08-30 17:00:58 +02:00
}
}
}