178 lines
6.8 KiB
C#
178 lines
6.8 KiB
C#
// 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}",
|
|
};
|
|
}
|
|
}
|