From 285fc5d0689176a840f12c190e33e7c2448c6981 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 21:47:56 -0500 Subject: [PATCH] feat(batch26): implement websocket upgrade/options/server feature group B --- .../NatsServer.WebSocket.cs | 25 ++++++++++++++++ .../WebSocket/WebSocketHandler.cs | 27 ++++++++++++++++++ .../WebSocket/WebSocketTypes.cs | 25 ++++++++++++++++ porting.db | Bin 6742016 -> 6742016 bytes 4 files changed, 77 insertions(+) diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.WebSocket.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.WebSocket.cs index 5d0406c..6200289 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.WebSocket.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.WebSocket.cs @@ -82,6 +82,15 @@ public sealed partial class NatsServer internal static Exception WsReturnHTTPError(int statusCode, string message) => new WsHttpError(statusCode, message); + private static bool wsHeaderContains(NameValueCollection headers, string key, string expected) => + WsHeaderContains(headers, key, expected); + + private static (bool supported, bool noContext) wsPMCExtensionSupport(NameValueCollection headers, bool checkNoContextTakeOver) => + WsPMCExtensionSupport(headers, checkNoContextTakeOver); + + private static Exception wsReturnHTTPError(int statusCode, string message) => + WsReturnHTTPError(statusCode, message); + internal static string WsGetHostAndPort(string hostPort, out int port) { port = 0; @@ -103,6 +112,9 @@ public sealed partial class NatsServer return hostPort; } + private static string wsGetHostAndPort(string hostPort, out int port) => + WsGetHostAndPort(hostPort, out port); + internal static byte[] WsMakeChallengeKey(string key) { ArgumentNullException.ThrowIfNull(key); @@ -116,6 +128,10 @@ public sealed partial class NatsServer return Convert.ToBase64String(digest); } + private static byte[] wsMakeChallengeKey(string key) => WsMakeChallengeKey(key); + + private static string wsAcceptKey(string key) => WsAcceptKey(key); + internal static Exception? ValidateWebsocketOptions(WebsocketOpts options) { if (options.Port < 0 || options.Port > 65535) @@ -127,6 +143,8 @@ public sealed partial class NatsServer return null; } + private static Exception? validateWebsocketOptions(WebsocketOpts options) => ValidateWebsocketOptions(options); + private void WsSetOriginOptions() { lock (_websocket.Mu) @@ -304,4 +322,11 @@ public sealed partial class NatsServer return false; return uri.Scheme.Equals(WsConstants.SchemePrefixTls, StringComparison.OrdinalIgnoreCase); } + + private static bool isWSURL(string url) => IsWSURL(url); + + private static bool isWSSURL(string url) => IsWSSURL(url); + + private System.Net.Security.SslServerAuthenticationOptions? wsGetTLSConfig() => + GetOpts().Websocket.TlsConfig; } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/WebSocket/WebSocketHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/WebSocket/WebSocketHandler.cs index 6930ca0..d09d4e8 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/WebSocket/WebSocketHandler.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/WebSocket/WebSocketHandler.cs @@ -25,4 +25,31 @@ internal sealed class WebSocketHandler public static byte[] WsCreateCloseMessage(int status, string body) => WebSocketHelpers.WsCreateCloseMessage(status, body); + + public static bool WsHeaderContains(System.Collections.Specialized.NameValueCollection headers, string key, string expected) => + NatsServer.WsHeaderContains(headers, key, expected); + + public static (bool supported, bool noContext) WsPMCExtensionSupport(System.Collections.Specialized.NameValueCollection headers, bool checkNoContextTakeOver) => + NatsServer.WsPMCExtensionSupport(headers, checkNoContextTakeOver); + + public static Exception WsReturnHTTPError(int statusCode, string message) => + NatsServer.WsReturnHTTPError(statusCode, message); + + public static string WsGetHostAndPort(string hostPort, out int port) => + NatsServer.WsGetHostAndPort(hostPort, out port); + + public static string WsAcceptKey(string key) => + NatsServer.WsAcceptKey(key); + + public static byte[] WsMakeChallengeKey(string key) => + NatsServer.WsMakeChallengeKey(key); + + public static Exception? ValidateWebsocketOptions(WebsocketOpts options) => + NatsServer.ValidateWebsocketOptions(options); + + public static bool IsWSURL(string url) => + NatsServer.IsWSURL(url); + + public static bool IsWSSURL(string url) => + NatsServer.IsWSSURL(url); } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/WebSocket/WebSocketTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/WebSocket/WebSocketTypes.cs index 7d41f5d..6fd4a64 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/WebSocket/WebSocketTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/WebSocket/WebSocketTypes.cs @@ -203,6 +203,31 @@ internal sealed class SrvWebsocket public bool Compression { get; set; } public string Host { get; set; } = string.Empty; public int Port { get; set; } + + public Exception? checkOrigin(string requestHost, string? origin) + { + if (!SameOrigin && AllowedOrigins.Count == 0) + return null; + if (string.IsNullOrWhiteSpace(origin)) + return new InvalidOperationException("origin header missing"); + if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri)) + return new InvalidOperationException("invalid origin"); + if (SameOrigin && !string.Equals(uri.Host, requestHost, StringComparison.OrdinalIgnoreCase)) + return new InvalidOperationException("origin not same as host"); + if (AllowedOrigins.Count == 0) + return null; + if (!AllowedOrigins.TryGetValue(uri.Host, out var allowed)) + return new InvalidOperationException("origin host not allowed"); + + var port = uri.IsDefaultPort + ? uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) ? "443" : "80" + : uri.Port.ToString(); + if (!string.Equals(allowed.Scheme, uri.Scheme, StringComparison.OrdinalIgnoreCase)) + return new InvalidOperationException("origin scheme not allowed"); + if (!string.Equals(allowed.Port, port, StringComparison.Ordinal)) + return new InvalidOperationException("origin port not allowed"); + return null; + } } internal readonly record struct AllowedOrigin(string Scheme, string Port); diff --git a/porting.db b/porting.db index dd42a6dd82d66e8c4c306cace044e38a9d48ad57..baa47b0a45c9b998ae09c73ee31f95899b60eebb 100644 GIT binary patch delta 1460 zcmY+>OH30{6vpw+ZBdbymbR$S_DWmITScpa_{J9~YJpN}TPq+X5Em*Y#<&nv3?YFS z*&LKjVMQVfA%n{raLSn(wS#MF$?SRLV&)L@9oN zC9h>etWQ(I9w9a9;)ufx(scr0Pq_Lr{0hjq!QFDrVqo`R$%_wSG zQB#VVRMh#`ihQN02}O-7YD`h1ih8N27p>Qw21o!Sn7|B)unyM421pXGJCh&(qTY*Y z8!gx^W;$oL*vONlkJaof*p^3ac1wXY&0QIYHiyM3(B&)YbgFV%(w8RszmK$}b(b4e zLf)MwH{H9bHi?5y%YZ}VZvS-=W5am2;V*);G(ok2f6JZEL!?Sk%7 z>~puc-BfLgF^Q$6{Fp#B9Xy?$mT}k0KKfF|yQs{EPr*T28Z75ovgzk?-b{rZrsZj; zmuJeRqh5ZS5`M6yYcSBY3NCw78mizP>YURoPu)JAEqnHYk3S=$Udw4Z#1kk`$sMw3 zTO}_chg$CkbHhqmk>0!21ATo4cx3Y49)6G=QMCdMdXF$mEZgI+!( zm+HZg$U%v5F2sUy1yKZz9E^Iw@tj*7 z^u%X@1UG+z`YXg})rw|LI_2gYIBD$$zY<{3s?2<6S9e!$Z&#=Goj1|@+kE(d;`tjz zT2`bbMS87Bi;DD0kzOj&f+D@pmi)igyka<~NY54NnIb*)rqSt1FBPak0AKI}e+Ym; z&_EDqK_^X5>WfdRzo^dmP{AEOmQrIxH?J;}R%1jp7br{!*M&<$zi^y%1$-zCwu;FV zWfl8q@rS6VRkhy`-TEm;kmf#LI67hzVG9_*2*D5np%4b)5Fw4&A}4;6p(wyipDdG%96w%V@ZoaK57_C- zkjf}c*-UpiT3Ze?(_ow_L+%{)#F-!p%#tUL8B?h8aR|CvG91-oGn6u@5C2m9dw6v9C$f?_Cv zLr@B3a2Sq2IaGiHDxnIh!3i}`3w2Nr4bTWp&