feat(batch24): complete leaf nodes implementation and verification
This commit is contained in:
177
dotnet/src/ZB.MOM.NatsNet.Server/LeafNode/LeafNodeHandler.cs
Normal file
177
dotnet/src/ZB.MOM.NatsNet.Server/LeafNode/LeafNodeHandler.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright 2019-2026 The NATS Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using ZB.MOM.NatsNet.Server.Auth;
|
||||
using ZB.MOM.NatsNet.Server.Internal;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
internal static class LeafNodeHandler
|
||||
{
|
||||
internal static readonly TimeSpan LeafNodeReconnectDelayAfterLoopDetected = TimeSpan.FromSeconds(30);
|
||||
internal static readonly TimeSpan LeafNodeReconnectAfterPermViolation = TimeSpan.FromSeconds(30);
|
||||
internal static readonly TimeSpan LeafNodeReconnectDelayAfterClusterNameSame = TimeSpan.FromSeconds(30);
|
||||
|
||||
internal const string LeafNodeLoopDetectionSubjectPrefix = "$LDS.";
|
||||
|
||||
public static Exception? ValidateLeafNode(ServerOptions options)
|
||||
{
|
||||
var authErr = ValidateLeafNodeAuthOptions(options);
|
||||
if (authErr != null)
|
||||
return authErr;
|
||||
|
||||
foreach (var remote in options.LeafNode.Remotes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(remote.LocalAccount))
|
||||
remote.LocalAccount = ServerConstants.DefaultGlobalAccount;
|
||||
|
||||
var (warnings, proxyErr) = ValidateLeafNodeProxyOptions(remote);
|
||||
_ = warnings;
|
||||
if (proxyErr != null)
|
||||
return proxyErr;
|
||||
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.LeafNode.MinVersion))
|
||||
{
|
||||
var minVersionErr = CheckLeafMinVersionConfig(options.LeafNode.MinVersion);
|
||||
if (minVersionErr != null)
|
||||
return minVersionErr;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Exception? CheckLeafMinVersionConfig(string minVersion)
|
||||
{
|
||||
var (ok, err) = ServerUtilities.VersionAtLeastCheckError(minVersion, 2, 8, 0);
|
||||
if (err != null)
|
||||
return new InvalidOperationException($"invalid leafnode minimum version: {err.Message}", err);
|
||||
|
||||
if (!ok)
|
||||
return new InvalidOperationException("the minimum version should be at least 2.8.0");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Exception? ValidateLeafNodeAuthOptions(ServerOptions options)
|
||||
{
|
||||
var users = options.LeafNode.Users;
|
||||
if (users is null || users.Count == 0)
|
||||
return null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.LeafNode.Username))
|
||||
return new InvalidOperationException("can not have a single user/pass and a users array");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(options.LeafNode.Nkey))
|
||||
return new InvalidOperationException("can not have a single nkey and a users array");
|
||||
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var user in users)
|
||||
{
|
||||
if (!seen.Add(user.Username))
|
||||
return new InvalidOperationException($"duplicate user {user.Username} detected in leafnode authorization");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static (List<string> Warnings, Exception? Error) ValidateLeafNodeProxyOptions(RemoteLeafOpts remote)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(remote.Proxy.Url))
|
||||
return (warnings, null);
|
||||
|
||||
if (!Uri.TryCreate(remote.Proxy.Url, UriKind.Absolute, out var proxyUri))
|
||||
return (warnings, new InvalidOperationException("invalid proxy URL"));
|
||||
|
||||
if (!string.Equals(proxyUri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(proxyUri.Scheme, "https", StringComparison.OrdinalIgnoreCase))
|
||||
return (warnings, new InvalidOperationException($"proxy URL scheme must be http or https, got: {proxyUri.Scheme}"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(proxyUri.Host))
|
||||
return (warnings, new InvalidOperationException("proxy URL must specify a host"));
|
||||
|
||||
if (remote.Proxy.Timeout < TimeSpan.Zero)
|
||||
return (warnings, new InvalidOperationException("proxy timeout must be >= 0"));
|
||||
|
||||
var userPresent = !string.IsNullOrWhiteSpace(remote.Proxy.Username);
|
||||
var passPresent = !string.IsNullOrWhiteSpace(remote.Proxy.Password);
|
||||
if (userPresent != passPresent)
|
||||
return (warnings, new InvalidOperationException("proxy username and password must both be specified or both be empty"));
|
||||
|
||||
return (warnings, null);
|
||||
}
|
||||
|
||||
public static LeafNodeCfg NewLeafNodeCfg(RemoteLeafOpts remote)
|
||||
{
|
||||
var cfg = new LeafNodeCfg
|
||||
{
|
||||
RemoteOpts = remote,
|
||||
Urls = [.. remote.Urls],
|
||||
CurUrl = remote.Urls.Count > 0 ? remote.Urls[0] : null,
|
||||
Perms = new Permissions
|
||||
{
|
||||
Publish = new SubjectPermission { Deny = [.. remote.DenyImports] },
|
||||
Subscribe = new SubjectPermission { Deny = [.. remote.DenyExports] },
|
||||
},
|
||||
};
|
||||
|
||||
cfg.SaveUserPassword(cfg.CurUrl);
|
||||
cfg.SaveTLSHostname(cfg.CurUrl);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
public static Exception? EstablishHTTPProxyTunnel(Stream stream, Uri target, RemoteLeafProxyOpts proxy, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
if (stream is null)
|
||||
return new InvalidOperationException("proxy tunnel requires an open stream");
|
||||
if (target is null)
|
||||
return new InvalidOperationException("proxy tunnel requires a target URL");
|
||||
|
||||
var hostPort = target.IsDefaultPort ? target.Host : $"{target.Host}:{target.Port}";
|
||||
var builder = new StringBuilder();
|
||||
builder.Append("CONNECT ").Append(hostPort).Append(" HTTP/1.1\r\n");
|
||||
builder.Append("Host: ").Append(hostPort).Append("\r\n");
|
||||
if (!string.IsNullOrWhiteSpace(proxy.Username) || !string.IsNullOrWhiteSpace(proxy.Password))
|
||||
{
|
||||
var raw = Encoding.UTF8.GetBytes($"{proxy.Username}:{proxy.Password}");
|
||||
builder.Append("Proxy-Authorization: Basic ")
|
||||
.Append(Convert.ToBase64String(raw))
|
||||
.Append("\r\n");
|
||||
}
|
||||
|
||||
builder.Append("\r\n");
|
||||
var data = Encoding.ASCII.GetBytes(builder.ToString());
|
||||
stream.Write(data, 0, data.Length);
|
||||
stream.Flush();
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string KeyFromSub(Internal.Subscription sub)
|
||||
{
|
||||
var subject = Encoding.ASCII.GetString(sub.Subject);
|
||||
if (sub.Queue is not { Length: > 0 })
|
||||
return subject;
|
||||
|
||||
return $"{subject} {Encoding.ASCII.GetString(sub.Queue)}";
|
||||
}
|
||||
|
||||
public static string KeyFromSubWithOrigin(Internal.Subscription sub, string? origin = null)
|
||||
{
|
||||
var subject = Encoding.ASCII.GetString(sub.Subject);
|
||||
var queue = sub.Queue is { Length: > 0 } q ? Encoding.ASCII.GetString(q) : string.Empty;
|
||||
var hasOrigin = !string.IsNullOrWhiteSpace(origin);
|
||||
|
||||
return (hasOrigin, queue.Length > 0) switch
|
||||
{
|
||||
(false, false) => $"R {subject}",
|
||||
(false, true) => $"R {subject} {queue}",
|
||||
(true, false) => $"L {subject} {origin}",
|
||||
(true, true) => $"L {subject} {queue} {origin}",
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user