Files
natsnet/dotnet/src/ZB.MOM.NatsNet.Server/NatsMessageHeaders.cs
Joseph Doherty 11b387e442 feat: port session 08 — Client Connection & PROXY Protocol
- ClientConnection: full connection lifecycle, string/identity helpers,
  SplitSubjectQueue, KindString, MsgParts, SetHeader, message header
  manipulation (GenHeader, RemoveHeader, SliceHeader, GetHeader)
- ClientTypes: ClientConnectionType, ClientProtocol, ClientFlags,
  ReadCacheFlags, ClosedState, PmrFlags, DenyType, ClientOptions,
  ClientInfo, NbPool, RouteTarget, ClientKindHelpers
- NatsMessageHeaders: complete header utility class (GenHeader,
  RemoveHeaderIfPrefixPresent, RemoveHeaderIfPresent, SliceHeader,
  GetHeader, SetHeader, GetHeaderKeyIndex)
- ProxyProtocol: PROXY protocol v1/v2 parser (ReadV1Header,
  ParseV2Header, ReadProxyProtoHeader sync entry point)
- ServerErrors: add ErrAuthorization sentinel
- Tests: 32 standalone unit tests (proxy protocol: IDs 159-168,
  171-178, 180-181; client: IDs 200-201, 247-256)
- DB: 195 features → complete (387-581); 32 tests → complete;
  81 server-dependent tests → n/a

Features: 667 complete, 274 unit tests complete (17.2% overall)
2026-02-26 13:50:38 -05:00

390 lines
14 KiB
C#

// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/client.go (header utility functions) in the NATS server Go source.
using System.Text;
namespace ZB.MOM.NatsNet.Server;
/// <summary>
/// Wire-level NATS message header constants.
/// </summary>
public static class NatsHeaderConstants
{
/// <summary>NATS header status line: <c>"NATS/1.0\r\n"</c>. Mirrors Go <c>hdrLine</c>.</summary>
public const string HdrLine = "NATS/1.0\r\n";
/// <summary>Empty header block with blank line terminator. Mirrors Go <c>emptyHdrLine</c>.</summary>
public const string EmptyHdrLine = "NATS/1.0\r\n\r\n";
// JetStream expected-sequence headers (defined in server/stream.go, used by header utilities).
public const string JsExpectedStream = "Nats-Expected-Stream";
public const string JsExpectedLastSeq = "Nats-Expected-Last-Sequence";
public const string JsExpectedLastSubjSeq = "Nats-Expected-Last-Subject-Sequence";
public const string JsExpectedLastSubjSeqSubj = "Nats-Expected-Last-Subject-Sequence-Subject";
public const string JsExpectedLastMsgId = "Nats-Expected-Last-Msg-Id";
// Other commonly used headers.
public const string JsMsgId = "Nats-Msg-Id";
public const string JsMsgRollup = "Nats-Rollup";
}
/// <summary>
/// Low-level NATS message header manipulation utilities.
/// Mirrors the package-level functions in server/client.go:
/// <c>genHeader</c>, <c>removeHeaderIfPresent</c>, <c>removeHeaderIfPrefixPresent</c>,
/// <c>getHeader</c>, <c>sliceHeader</c>, <c>getHeaderKeyIndex</c>, <c>setHeader</c>.
/// </summary>
public static class NatsMessageHeaders
{
private static readonly byte[] CrLfBytes = "\r\n"u8.ToArray();
// -------------------------------------------------------------------------
// genHeader (feature 506)
// -------------------------------------------------------------------------
/// <summary>
/// Generates a header buffer by appending <c>key: value\r\n</c> to an existing header,
/// or starting a fresh <c>NATS/1.0\r\n</c> block if <paramref name="hdr"/> is empty/null.
/// Mirrors Go <c>genHeader</c>.
/// </summary>
/// <param name="hdr">Existing header bytes, or <c>null</c> to start fresh.</param>
/// <param name="key">Header key.</param>
/// <param name="value">Header value.</param>
public static byte[] GenHeader(byte[]? hdr, string key, string value)
{
var sb = new StringBuilder();
// Strip trailing CRLF from existing header to reopen for appending,
// or start fresh with the header status line.
const int LenCrLf = 2;
if (hdr is { Length: > LenCrLf })
{
// Write all but the trailing "\r\n"
sb.Append(Encoding.ASCII.GetString(hdr, 0, hdr.Length - LenCrLf));
}
else
{
sb.Append(NatsHeaderConstants.HdrLine);
}
// Append "key: value\r\n\r\n" (HTTP header format).
sb.Append(key);
sb.Append(": ");
sb.Append(value);
sb.Append("\r\n\r\n");
return Encoding.ASCII.GetBytes(sb.ToString());
}
// -------------------------------------------------------------------------
// removeHeaderIfPresent (feature 504)
// -------------------------------------------------------------------------
/// <summary>
/// Removes the first occurrence of <paramref name="key"/> header from <paramref name="hdr"/>.
/// Returns <c>null</c> if the result would be an empty header block.
/// Mirrors Go <c>removeHeaderIfPresent</c>.
/// </summary>
public static byte[]? RemoveHeaderIfPresent(byte[] hdr, string key)
{
int start = GetHeaderKeyIndex(key, hdr);
// Key must exist and be preceded by '\n' (not at position 0).
if (start < 1 || hdr[start - 1] != '\n')
return hdr;
int index = start + key.Length;
if (index >= hdr.Length || hdr[index] != ':')
return hdr;
// Find CRLF following this header line.
int crlfIdx = IndexOfCrLf(hdr, start);
if (crlfIdx < 0)
return hdr;
// Remove from 'start' through end of CRLF.
int removeEnd = start + crlfIdx + 2; // +2 for "\r\n"
var result = new byte[hdr.Length - (removeEnd - start)];
Buffer.BlockCopy(hdr, 0, result, 0, start);
Buffer.BlockCopy(hdr, removeEnd, result, start, hdr.Length - removeEnd);
// If nothing meaningful remains, return null.
if (result.Length <= NatsHeaderConstants.EmptyHdrLine.Length)
return null;
return result;
}
// -------------------------------------------------------------------------
// removeHeaderIfPrefixPresent (feature 505)
// -------------------------------------------------------------------------
/// <summary>
/// Removes all headers whose names start with <paramref name="prefix"/>.
/// Returns <c>null</c> if the result would be an empty header block.
/// Mirrors Go <c>removeHeaderIfPrefixPresent</c>.
/// </summary>
public static byte[]? RemoveHeaderIfPrefixPresent(byte[] hdr, string prefix)
{
var prefixBytes = Encoding.ASCII.GetBytes(prefix);
var working = hdr.ToList(); // work on a list for easy splicing
int index = 0;
while (index < working.Count)
{
// Look for prefix starting at current index.
int found = IndexOf(working, prefixBytes, index);
if (found < 0)
break;
// Must be preceded by '\n'.
if (found < 1 || working[found - 1] != '\n')
break;
// Find CRLF after this prefix's key:value line.
int crlfIdx = IndexOfCrLf(working, found + prefix.Length);
if (crlfIdx < 0)
break;
int removeEnd = found + prefix.Length + crlfIdx + 2;
working.RemoveRange(found, removeEnd - found);
// Don't advance index — there may be more headers at same position.
if (working.Count <= NatsHeaderConstants.EmptyHdrLine.Length)
return null;
}
return working.ToArray();
}
// -------------------------------------------------------------------------
// getHeaderKeyIndex (feature 510)
// -------------------------------------------------------------------------
/// <summary>
/// Returns the byte offset of <paramref name="key"/> in <paramref name="hdr"/>,
/// or <c>-1</c> if not found.
/// The key must be preceded by <c>\r\n</c> and followed by <c>:</c>.
/// Mirrors Go <c>getHeaderKeyIndex</c>.
/// </summary>
public static int GetHeaderKeyIndex(string key, byte[] hdr)
{
if (hdr.Length == 0) return -1;
var bkey = Encoding.ASCII.GetBytes(key);
int keyLen = bkey.Length;
int hdrLen = hdr.Length;
int offset = 0;
while (true)
{
int index = IndexOf(hdr, bkey, offset);
// Need index >= 2 (room for preceding \r\n) and enough space for trailing colon.
if (index < 2) return -1;
// Preceded by \r\n ?
if (hdr[index - 1] != '\n' || hdr[index - 2] != '\r')
{
offset = index + keyLen;
continue;
}
// Immediately followed by ':' ?
if (index + keyLen >= hdrLen)
return -1;
if (hdr[index + keyLen] != ':')
{
offset = index + keyLen;
continue;
}
return index;
}
}
// -------------------------------------------------------------------------
// sliceHeader (feature 509)
// -------------------------------------------------------------------------
/// <summary>
/// Returns a slice of <paramref name="hdr"/> containing the value of <paramref name="key"/>,
/// or <c>null</c> if not found.
/// The returned slice shares memory with <paramref name="hdr"/>.
/// Mirrors Go <c>sliceHeader</c>.
/// </summary>
public static ReadOnlyMemory<byte>? SliceHeader(string key, byte[] hdr)
{
if (hdr.Length == 0) return null;
int index = GetHeaderKeyIndex(key, hdr);
if (index == -1) return null;
// Skip over key + ':' separator.
index += key.Length + 1;
int hdrLen = hdr.Length;
// Skip leading whitespace.
while (index < hdrLen && hdr[index] == ' ')
index++;
int start = index;
// Collect until CRLF.
while (index < hdrLen)
{
if (hdr[index] == '\r' && index + 1 < hdrLen && hdr[index + 1] == '\n')
break;
index++;
}
// Return a slice with capped length == value length (no extra capacity).
return new ReadOnlyMemory<byte>(hdr, start, index - start);
}
// -------------------------------------------------------------------------
// getHeader (feature 508)
// -------------------------------------------------------------------------
/// <summary>
/// Returns a copy of the value for the header named <paramref name="key"/>,
/// or <c>null</c> if not found.
/// Mirrors Go <c>getHeader</c>.
/// </summary>
public static byte[]? GetHeader(string key, byte[] hdr)
{
var slice = SliceHeader(key, hdr);
if (slice is null) return null;
// Return a fresh copy.
return slice.Value.ToArray();
}
// -------------------------------------------------------------------------
// setHeader (feature 511)
// -------------------------------------------------------------------------
/// <summary>
/// Replaces the value of the first existing <paramref name="key"/> header in
/// <paramref name="hdr"/>, or appends a new header if the key is absent.
/// Returns a new buffer when the new value is larger; modifies in-place otherwise.
/// Mirrors Go <c>setHeader</c>.
/// </summary>
public static byte[] SetHeader(string key, string val, byte[] hdr)
{
int start = GetHeaderKeyIndex(key, hdr);
if (start >= 0)
{
int valStart = start + key.Length + 1; // skip past ':'
int hdrLen = hdr.Length;
// Preserve a single leading space if present.
if (valStart < hdrLen && hdr[valStart] == ' ')
valStart++;
// Find the CR before the CRLF.
int crIdx = IndexOf(hdr, [(byte)'\r'], valStart);
if (crIdx < 0) return hdr; // malformed
int valEnd = crIdx;
int oldValLen = valEnd - valStart;
var valBytes = Encoding.ASCII.GetBytes(val);
int extra = valBytes.Length - oldValLen;
if (extra > 0)
{
// New value is larger — must allocate a new buffer.
var newHdr = new byte[hdrLen + extra];
Buffer.BlockCopy(hdr, 0, newHdr, 0, valStart);
Buffer.BlockCopy(valBytes, 0, newHdr, valStart, valBytes.Length);
Buffer.BlockCopy(hdr, valEnd, newHdr, valStart + valBytes.Length, hdrLen - valEnd);
return newHdr;
}
// Write in place (new value fits).
int n = valBytes.Length;
Buffer.BlockCopy(valBytes, 0, hdr, valStart, n);
// Shift remainder left.
Buffer.BlockCopy(hdr, valEnd, hdr, valStart + n, hdrLen - valEnd);
return hdr[..(valStart + n + hdrLen - valEnd)];
}
// Key not present — append.
bool hasTrailingCrLf = hdr.Length >= 2
&& hdr[^2] == '\r'
&& hdr[^1] == '\n';
byte[] suffix;
if (hasTrailingCrLf)
{
// Strip trailing CRLF, append "key: val\r\n\r\n".
suffix = Encoding.ASCII.GetBytes($"{key}: {val}\r\n");
var result = new byte[hdr.Length - 2 + suffix.Length + 2];
Buffer.BlockCopy(hdr, 0, result, 0, hdr.Length - 2);
Buffer.BlockCopy(suffix, 0, result, hdr.Length - 2, suffix.Length);
result[^2] = (byte)'\r';
result[^1] = (byte)'\n';
return result;
}
suffix = Encoding.ASCII.GetBytes($"{key}: {val}\r\n");
var newBuf = new byte[hdr.Length + suffix.Length];
Buffer.BlockCopy(hdr, 0, newBuf, 0, hdr.Length);
Buffer.BlockCopy(suffix, 0, newBuf, hdr.Length, suffix.Length);
return newBuf;
}
// -------------------------------------------------------------------------
// Internal helpers
// -------------------------------------------------------------------------
private static int IndexOf(byte[] haystack, byte[] needle, int offset)
{
var span = haystack.AsSpan(offset);
int idx = span.IndexOf(needle);
return idx < 0 ? -1 : offset + idx;
}
private static int IndexOf(List<byte> haystack, byte[] needle, int offset)
{
for (int i = offset; i <= haystack.Count - needle.Length; i++)
{
bool match = true;
for (int j = 0; j < needle.Length; j++)
{
if (haystack[i + j] != needle[j]) { match = false; break; }
}
if (match) return i;
}
return -1;
}
/// <summary>Returns the offset of the first \r\n in <paramref name="hdr"/> at or after <paramref name="offset"/>.</summary>
private static int IndexOfCrLf(byte[] hdr, int offset)
{
var span = hdr.AsSpan(offset);
int idx = span.IndexOf(CrLfBytes);
return idx; // relative to offset
}
private static int IndexOfCrLf(List<byte> hdr, int offset)
{
for (int i = offset; i < hdr.Count - 1; i++)
{
if (hdr[i] == '\r' && hdr[i + 1] == '\n')
return i - offset;
}
return -1;
}
}