From 487865b3c0eba73cdc2397e5513fd3e50ae2fc92 Mon Sep 17 00:00:00 2001 From: Jon Steinich Date: Mon, 8 Jan 2018 14:41:38 -0600 Subject: [PATCH 1/4] 1st pass at porting ha proxy support --- DotNetty.sln | 7 + .../DotNetty.Codecs.HaProxy.csproj | 42 ++ src/DotNetty.Codecs.HaProxy/HAProxyCommand.cs | 73 +++ .../HAProxyConstants.cs | 49 ++ src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs | 594 ++++++++++++++++++ .../HAProxyMessageDecoder.cs | 452 +++++++++++++ .../HAProxyProtocolException.cs | 33 + .../HAProxyProtocolVersion.cs | 71 +++ .../HAProxyProxiedProtocol.cs | 287 +++++++++ src/DotNetty.Codecs.HaProxy/HAProxySSLTLV.cs | 76 +++ src/DotNetty.Codecs.HaProxy/HAProxyTLV.cs | 102 +++ src/DotNetty.Codecs.HaProxy/NetUtil.cs | 210 +++++++ .../Properties/AssemblyInfo.cs | 8 + .../ProtocolDetectionResult.cs | 73 +++ .../ProtocolDetectionState.cs | 26 + 15 files changed, 2103 insertions(+) create mode 100644 src/DotNetty.Codecs.HaProxy/DotNetty.Codecs.HaProxy.csproj create mode 100644 src/DotNetty.Codecs.HaProxy/HAProxyCommand.cs create mode 100644 src/DotNetty.Codecs.HaProxy/HAProxyConstants.cs create mode 100644 src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs create mode 100644 src/DotNetty.Codecs.HaProxy/HAProxyMessageDecoder.cs create mode 100644 src/DotNetty.Codecs.HaProxy/HAProxyProtocolException.cs create mode 100644 src/DotNetty.Codecs.HaProxy/HAProxyProtocolVersion.cs create mode 100644 src/DotNetty.Codecs.HaProxy/HAProxyProxiedProtocol.cs create mode 100644 src/DotNetty.Codecs.HaProxy/HAProxySSLTLV.cs create mode 100644 src/DotNetty.Codecs.HaProxy/HAProxyTLV.cs create mode 100644 src/DotNetty.Codecs.HaProxy/NetUtil.cs create mode 100644 src/DotNetty.Codecs.HaProxy/Properties/AssemblyInfo.cs create mode 100644 src/DotNetty.Codecs.HaProxy/ProtocolDetectionResult.cs create mode 100644 src/DotNetty.Codecs.HaProxy/ProtocolDetectionState.cs diff --git a/DotNetty.sln b/DotNetty.sln index 7071c3b52..edbdcf4e3 100644 --- a/DotNetty.sln +++ b/DotNetty.sln @@ -39,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.ProtocolBuf EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.Redis", "src\DotNetty.Codecs.Redis\DotNetty.Codecs.Redis.csproj", "{1F442118-A665-4891-B056-FE9E54C5B049}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.HaProxy", "src\DotNetty.Codecs.HaProxy\DotNetty.Codecs.HaProxy.csproj", "{23C52DC6-F4E7-457F-B36E-CEEBFA95D24D}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Buffers.Tests", "test\DotNetty.Buffers.Tests\DotNetty.Buffers.Tests.csproj", "{572E1914-489F-402D-944F-71EE0632E5D8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Tests.Common", "test\DotNetty.Tests.Common\DotNetty.Tests.Common.csproj", "{DA54DBAF-CCDA-4AD1-9FF9-EB6F890D1091}" @@ -249,6 +251,10 @@ Global {7155D1E6-00CE-4081-B922-E6C5524EE600}.Debug|Any CPU.Build.0 = Debug|Any CPU {7155D1E6-00CE-4081-B922-E6C5524EE600}.Release|Any CPU.ActiveCfg = Release|Any CPU {7155D1E6-00CE-4081-B922-E6C5524EE600}.Release|Any CPU.Build.0 = Release|Any CPU + {23C52DC6-F4E7-457F-B36E-CEEBFA95D24D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23C52DC6-F4E7-457F-B36E-CEEBFA95D24D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23C52DC6-F4E7-457F-B36E-CEEBFA95D24D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23C52DC6-F4E7-457F-B36E-CEEBFA95D24D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -263,6 +269,7 @@ Global {D284C2BF-E06E-481B-B301-503A9D477B0E} = {126EA539-4B28-4B07-8B5D-D1D7F794D189} {75A1BCC1-A7F3-4893-99C5-3235F87DB00E} = {126EA539-4B28-4B07-8B5D-D1D7F794D189} {1F442118-A665-4891-B056-FE9E54C5B049} = {126EA539-4B28-4B07-8B5D-D1D7F794D189} + {23C52DC6-F4E7-457F-B36E-CEEBFA95D24D} = {126EA539-4B28-4B07-8B5D-D1D7F794D189} {572E1914-489F-402D-944F-71EE0632E5D8} = {541093F6-616E-43D9-B671-FCD1F9C0A181} {DA54DBAF-CCDA-4AD1-9FF9-EB6F890D1091} = {541093F6-616E-43D9-B671-FCD1F9C0A181} {72C92F76-F804-4300-BFF1-459420D9EF0B} = {541093F6-616E-43D9-B671-FCD1F9C0A181} diff --git a/src/DotNetty.Codecs.HaProxy/DotNetty.Codecs.HaProxy.csproj b/src/DotNetty.Codecs.HaProxy/DotNetty.Codecs.HaProxy.csproj new file mode 100644 index 000000000..a567cb589 --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/DotNetty.Codecs.HaProxy.csproj @@ -0,0 +1,42 @@ + + + netstandard1.3;net45 + true + DotNetty.Codecs.HaProxy + HAProxy codec for DotNetty + Copyright © Microsoft Corporation + DotNetty: HAProxy codec + en-US + 0.4.7 + Microsoft Azure + $(NoWarn);CS1591 + false + true + DotNetty.Codecs.HaProxy + ../../DotNetty.snk + true + true + socket;tcp;protocol;netty;dotnetty;network;HAProxy + https://github.com/Azure/DotNetty/ + https://github.com/Azure/DotNetty/blob/master/LICENSE.txt + git + https://github.com/Azure/DotNetty/ + 1.6.1 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyCommand.cs b/src/DotNetty.Codecs.HaProxy/HAProxyCommand.cs new file mode 100644 index 000000000..924658eaa --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/HAProxyCommand.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs.HaProxy +{ + using System; + using System.Collections.Generic; + + /** + * The command of an HAProxy proxy protocol header + */ + public sealed class HAProxyCommand + { + /** + * The LOCAL command represents a connection that was established on purpose by the proxy + * without being relayed. + */ + public static readonly HAProxyCommand LOCAL = new HAProxyCommand(HAProxyConstants.COMMAND_LOCAL_BYTE); + /** + * The PROXY command represents a connection that was established on behalf of another node, + * and reflects the original connection endpoints. + */ + public static readonly HAProxyCommand PROXY = new HAProxyCommand(HAProxyConstants.COMMAND_PROXY_BYTE); + + public static IEnumerable Values + { + get + { + yield return LOCAL; + yield return PROXY; + } + } + + /** + * Returns the {@link HAProxyCommand} represented by the lowest 4 bits of the specified byte. + * + * @param verCmdByte protocol version and command byte + */ + public static HAProxyCommand ValueOf(byte verCmdByte) + { + int cmd = verCmdByte & COMMAND_MASK; + switch ((byte)cmd) + { + case HAProxyConstants.COMMAND_PROXY_BYTE: + return PROXY; + case HAProxyConstants.COMMAND_LOCAL_BYTE: + return LOCAL; + default: + throw new ArgumentOutOfRangeException(nameof(verCmdByte), "unknown command: " + cmd); + } + } + + /** + * The command is specified in the lowest 4 bits of the protocol version and command byte + */ + const byte COMMAND_MASK = 0x0f; + + readonly byte byteValue; + + HAProxyCommand(byte byteValue) + { + this.byteValue = byteValue; + } + + /** + * Returns the byte value of this command. + */ + public byte ByteValue() + { + return this.byteValue; + } + } +} diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyConstants.cs b/src/DotNetty.Codecs.HaProxy/HAProxyConstants.cs new file mode 100644 index 000000000..22b487118 --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/HAProxyConstants.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs.HaProxy +{ + sealed class HAProxyConstants + { + + /** + * Command byte constants + */ + internal const byte COMMAND_LOCAL_BYTE = 0x00; + internal const byte COMMAND_PROXY_BYTE = 0x01; + + /** + * Version byte constants + */ + internal const byte VERSION_ONE_BYTE = 0x10; + internal const byte VERSION_TWO_BYTE = 0x20; + + /** + * Transport protocol byte constants + */ + internal const byte TRANSPORT_UNSPEC_BYTE = 0x00; + internal const byte TRANSPORT_STREAM_BYTE = 0x01; + internal const byte TRANSPORT_DGRAM_BYTE = 0x02; + + /** + * Address family byte constants + */ + internal const byte AF_UNSPEC_BYTE = 0x00; + internal const byte AF_IPV4_BYTE = 0x10; + internal const byte AF_IPV6_BYTE = 0x20; + internal const byte AF_UNIX_BYTE = 0x30; + + /** + * Transport protocol and address family byte constants + */ + internal const byte TPAF_UNKNOWN_BYTE = 0x00; + internal const byte TPAF_TCP4_BYTE = 0x11; + internal const byte TPAF_TCP6_BYTE = 0x21; + internal const byte TPAF_UDP4_BYTE = 0x12; + internal const byte TPAF_UDP6_BYTE = 0x22; + internal const byte TPAF_UNIX_STREAM_BYTE = 0x31; + internal const byte TPAF_UNIX_DGRAM_BYTE = 0x32; + + private HAProxyConstants() { } + } +} diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs b/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs new file mode 100644 index 000000000..9fe22951f --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs @@ -0,0 +1,594 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs.HaProxy +{ + using System; + using System.Collections.Generic; + using System.Text; + using DotNetty.Buffers; + using DotNetty.Common.Utilities; + + using static DotNetty.Codecs.HaProxy.HAProxyProxiedProtocol; + + /** + * Message container for decoded HAProxy proxy protocol parameters + */ + public sealed class HAProxyMessage + { + + /** + * Version 1 proxy protocol message for 'UNKNOWN' proxied protocols. Per spec, when the proxied protocol is + * 'UNKNOWN' we must discard all other header values. + */ + private static readonly HAProxyMessage V1_UNKNOWN_MSG = new HAProxyMessage( + HAProxyProtocolVersion.V1, HAProxyCommand.PROXY, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0); + + /** + * Version 2 proxy protocol message for 'UNKNOWN' proxied protocols. Per spec, when the proxied protocol is + * 'UNKNOWN' we must discard all other header values. + */ + private static readonly HAProxyMessage V2_UNKNOWN_MSG = new HAProxyMessage( + HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0); + + /** + * Version 2 proxy protocol message for local requests. Per spec, we should use an unspecified protocol and family + * for 'LOCAL' commands. Per spec, when the proxied protocol is 'UNKNOWN' we must discard all other header values. + */ + private static readonly HAProxyMessage V2_LOCAL_MSG = new HAProxyMessage( + HAProxyProtocolVersion.V2, HAProxyCommand.LOCAL, HAProxyProxiedProtocol.UNKNOWN, null, null, 0, 0); + + private readonly HAProxyProtocolVersion protocolVersion; + private readonly HAProxyCommand command; + private readonly HAProxyProxiedProtocol proxiedProtocol; + private readonly string sourceAddress; + private readonly string destinationAddress; + private readonly int sourcePort; + private readonly int destinationPort; + private readonly IList tlvs; + + /** + * Creates a new instance + */ + private HAProxyMessage( + HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol, + string sourceAddress, string destinationAddress, string sourcePort, string destinationPort) + : this( + protocolVersion, command, proxiedProtocol, + sourceAddress, destinationAddress, PortStringToInt(sourcePort), PortStringToInt(destinationPort)) + { + } + + /** + * Creates a new instance + */ + private HAProxyMessage( + HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol, + string sourceAddress, string destinationAddress, int sourcePort, int destinationPort) + : this(protocolVersion, command, proxiedProtocol, + sourceAddress, destinationAddress, sourcePort, destinationPort, new List()) + { + } + + /** + * Creates a new instance + */ + private HAProxyMessage( + HAProxyProtocolVersion protocolVersion, HAProxyCommand command, HAProxyProxiedProtocol proxiedProtocol, + string sourceAddress, string destinationAddress, int sourcePort, int destinationPort, + List tlvs) + { + + if (proxiedProtocol == null) + { + throw new ArgumentNullException(nameof(proxiedProtocol)); + } + AddressFamily addrFamily = proxiedProtocol.AddressFamilyType(); + + CheckAddress(sourceAddress, addrFamily); + CheckAddress(destinationAddress, addrFamily); + CheckPort(sourcePort); + CheckPort(destinationPort); + + this.protocolVersion = protocolVersion; + this.command = command; + this.proxiedProtocol = proxiedProtocol; + this.sourceAddress = sourceAddress; + this.destinationAddress = destinationAddress; + this.sourcePort = sourcePort; + this.destinationPort = destinationPort; + this.tlvs = tlvs.AsReadOnly(); + } + + /** + * Decodes a version 2, binary proxy protocol header. + * + * @param header a version 2 proxy protocol header + * @return {@link HAProxyMessage} instance + * @throws HAProxyProtocolException if any portion of the header is invalid + */ + internal static HAProxyMessage DecodeHeader(IByteBuffer header) + { + if (header == null) + { + throw new ArgumentNullException(nameof(header)); + } + + if (header.ReadableBytes < 16) + { + throw new HAProxyProtocolException( + "incomplete header: " + header.ReadableBytes + " bytes (expected: 16+ bytes)"); + } + + // Per spec, the 13th byte is the protocol version and command byte + header.SkipBytes(12); + byte verCmdByte = header.ReadByte(); + + HAProxyProtocolVersion ver; + try + { + ver = HAProxyProtocolVersion.ValueOf(verCmdByte); + } + catch (ArgumentOutOfRangeException e) + { + throw new HAProxyProtocolException(e); + } + + if (ver != HAProxyProtocolVersion.V2) + { + throw new HAProxyProtocolException("version 1 unsupported: 0x" + verCmdByte.ToString("X")); + } + + HAProxyCommand cmd; + try + { + cmd = HAProxyCommand.ValueOf(verCmdByte); + } + catch (ArgumentOutOfRangeException e) + { + throw new HAProxyProtocolException(e); + } + + if (cmd == HAProxyCommand.LOCAL) + { + return V2_LOCAL_MSG; + } + + // Per spec, the 14th byte is the protocol and address family byte + HAProxyProxiedProtocol protAndFam; + try + { + protAndFam = HAProxyProxiedProtocol.ValueOf(header.ReadByte()); + } + catch (ArgumentOutOfRangeException e) + { + throw new HAProxyProtocolException(e); + } + + if (protAndFam == HAProxyProxiedProtocol.UNKNOWN) + { + return V2_UNKNOWN_MSG; + } + + int addressInfoLen = header.ReadUnsignedShort(); + + string srcAddress; + string dstAddress; + int addressLen; + int srcPort = 0; + int dstPort = 0; + + AddressFamily addressFamily = protAndFam.AddressFamilyType(); + + if (addressFamily == AddressFamily.AF_UNIX) + { + // unix sockets require 216 bytes for address information + if (addressInfoLen < 216 || header.ReadableBytes < 216) + { + throw new HAProxyProtocolException( + "incomplete UNIX socket address information: " + + Math.Min(addressInfoLen, header.ReadableBytes) + " bytes (expected: 216+ bytes)"); + } + int startIdx = header.ReaderIndex; + int addressEnd = header.ForEachByte(startIdx, 108, ByteProcessor.FindNul); + if (addressEnd == -1) + { + addressLen = 108; + } + else + { + addressLen = addressEnd - startIdx; + } + srcAddress = header.ToString(startIdx, addressLen, Encoding.ASCII); + + startIdx += 108; + + addressEnd = header.ForEachByte(startIdx, 108, ByteProcessor.FindNul); + if (addressEnd == -1) + { + addressLen = 108; + } + else + { + addressLen = addressEnd - startIdx; + } + dstAddress = header.ToString(startIdx, addressLen, Encoding.ASCII); + // AF_UNIX defines that exactly 108 bytes are reserved for the address. The previous methods + // did not increase the reader index although we already consumed the information. + header.SetReaderIndex(startIdx + 108); + } + else + { + if (addressFamily == AddressFamily.AF_IPv4) + { + // IPv4 requires 12 bytes for address information + if (addressInfoLen < 12 || header.ReadableBytes < 12) + { + throw new HAProxyProtocolException( + "incomplete IPv4 address information: " + + Math.Min(addressInfoLen, header.ReadableBytes) + " bytes (expected: 12+ bytes)"); + } + addressLen = 4; + } + else if (addressFamily == AddressFamily.AF_IPv6) + { + // IPv6 requires 36 bytes for address information + if (addressInfoLen < 36 || header.ReadableBytes < 36) + { + throw new HAProxyProtocolException( + "incomplete IPv6 address information: " + + Math.Min(addressInfoLen, header.ReadableBytes) + " bytes (expected: 36+ bytes)"); + } + addressLen = 16; + } + else + { + throw new HAProxyProtocolException( + "unable to parse address information (unknown address family: " + addressFamily + ')'); + } + + // Per spec, the src address begins at the 17th byte + srcAddress = IpBytesToString(header, addressLen); + dstAddress = IpBytesToString(header, addressLen); + srcPort = header.ReadUnsignedShort(); + dstPort = header.ReadUnsignedShort(); + } + + List tlvs = ReadTlvs(header); + + return new HAProxyMessage(ver, cmd, protAndFam, srcAddress, dstAddress, srcPort, dstPort, tlvs); + } + + private static List ReadTlvs(IByteBuffer header) + { + HAProxyTLV haProxyTLV = ReadNextTLV(header); + if (haProxyTLV == null) + { + return new List(); + } + // In most cases there are less than 4 TLVs available + List haProxyTLVs = new List(4); + + do + { + haProxyTLVs.Add(haProxyTLV); + var sslProxy = haProxyTLV as HAProxySSLTLV; + if (sslProxy != null) { + haProxyTLVs.AddRange(sslProxy.EncapsulatedTLVs()); + } + } while ((haProxyTLV = ReadNextTLV(header)) != null); + return haProxyTLVs; + } + + private static HAProxyTLV ReadNextTLV(IByteBuffer header) + { + + // We need at least 4 bytes for a TLV + if (header.ReadableBytes < 4) + { + return null; + } + + byte typeAsByte = header.ReadByte(); + HAProxyTLV.Type type = HAProxyTLV.TypeForByteValue(typeAsByte); + + int length = header.ReadUnsignedShort(); + switch (type) + { + case HAProxyTLV.Type.PP2_TYPE_SSL: + IByteBuffer rawContent = header.RetainedSlice(header.ReaderIndex, length); + IByteBuffer byteBuf = header.ReadSlice(length); + byte client = byteBuf.ReadByte(); + int verify = byteBuf.ReadInt(); + + if (byteBuf.ReadableBytes >= 4) + { + + List encapsulatedTlvs = new List(4); + do + { + HAProxyTLV haProxyTLV = ReadNextTLV(byteBuf); + if (haProxyTLV == null) + { + break; + } + encapsulatedTlvs.Add(haProxyTLV); + } while (byteBuf.ReadableBytes >= 4); + + return new HAProxySSLTLV(verify, client, encapsulatedTlvs, rawContent); + } + return new HAProxySSLTLV(verify, client, new List(), rawContent); + // If we're not dealing with a SSL Type, we can use the same mechanism + case HAProxyTLV.Type.PP2_TYPE_ALPN: + case HAProxyTLV.Type.PP2_TYPE_AUTHORITY: + case HAProxyTLV.Type.PP2_TYPE_SSL_VERSION: + case HAProxyTLV.Type.PP2_TYPE_SSL_CN: + case HAProxyTLV.Type.PP2_TYPE_NETNS: + case HAProxyTLV.Type.OTHER: + return new HAProxyTLV(type, typeAsByte, header.ReadRetainedSlice(length)); + default: + return null; + } + } + + /** + * Decodes a version 1, human-readable proxy protocol header. + * + * @param header a version 1 proxy protocol header + * @return {@link HAProxyMessage} instance + * @throws HAProxyProtocolException if any portion of the header is invalid + */ + internal static HAProxyMessage DecodeHeader(string header) + { + if (header == null) + { + throw new HAProxyProtocolException("header"); + } + + string[] parts = header.Split(' '); + int numParts = parts.Length; + + if (numParts < 2) + { + throw new HAProxyProtocolException( + "invalid header: " + header + " (expected: 'PROXY' and proxied protocol values)"); + } + + if (!"PROXY".Equals(parts[0])) + { + throw new HAProxyProtocolException("unknown identifier: " + parts[0]); + } + + HAProxyProxiedProtocol protAndFam; + try + { + protAndFam = HAProxyProxiedProtocol.ValueOf(parts[1]); + } + catch (ArgumentOutOfRangeException e) + { + throw new HAProxyProtocolException(e); + } + + if (protAndFam != HAProxyProxiedProtocol.TCP4 && + protAndFam != HAProxyProxiedProtocol.TCP6 && + protAndFam != HAProxyProxiedProtocol.UNKNOWN) + { + throw new HAProxyProtocolException("unsupported v1 proxied protocol: " + parts[1]); + } + + if (protAndFam == HAProxyProxiedProtocol.UNKNOWN) + { + return V1_UNKNOWN_MSG; + } + + if (numParts != 6) + { + throw new HAProxyProtocolException("invalid TCP4/6 header: " + header + " (expected: 6 parts)"); + } + + return new HAProxyMessage( + HAProxyProtocolVersion.V1, HAProxyCommand.PROXY, + protAndFam, parts[2], parts[3], parts[4], parts[5]); + } + + /** + * Convert ip address bytes to string representation + * + * @param header buffer containing ip address bytes + * @param addressLen number of bytes to read (4 bytes for IPv4, 16 bytes for IPv6) + * @return string representation of the ip address + */ + private static string IpBytesToString(IByteBuffer header, int addressLen) + { + StringBuilder sb = new StringBuilder(); + if (addressLen == 4) + { + sb.Append(header.ReadByte() & 0xff); + sb.Append('.'); + sb.Append(header.ReadByte() & 0xff); + sb.Append('.'); + sb.Append(header.ReadByte() & 0xff); + sb.Append('.'); + sb.Append(header.ReadByte() & 0xff); + } + else + { + sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(':'); + sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(':'); + sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(':'); + sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(':'); + sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(':'); + sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(':'); + sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(':'); + sb.Append(header.ReadUnsignedShort().ToString("X")); + } + return sb.ToString(); + } + + /** + * Convert port to integer + * + * @param value the port + * @return port as an integer + * @throws HAProxyProtocolException if port is not a valid integer + */ + private static int PortStringToInt(string value) + { + int port; + try + { + port = int.Parse(value); + } + catch (FormatException e) + { + throw new HAProxyProtocolException(e); + } + + if (port <= 0 || port > 65535) + { + throw new HAProxyProtocolException("invalid port: " + value + " (expected: 1 ~ 65535)"); + } + + return port; + } + + /** + * Validate an address (IPv4, IPv6, Unix Socket) + * + * @param address human-readable address + * @param addrFamily the {@link AddressFamily} to check the address against + * @throws HAProxyProtocolException if the address is invalid + */ + private static void CheckAddress(string address, AddressFamily addrFamily) + { + if (addrFamily == null) + { + throw new ArgumentNullException(nameof(addrFamily)); + } + + if (addrFamily == AddressFamily.AF_UNIX) + { + return; + } + else if(addrFamily == AddressFamily.AF_UNSPEC) + { + if (address != null) + { + throw new HAProxyProtocolException("unable to validate an AF_UNSPEC address: " + address); + } + return; + } + + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (addrFamily == AddressFamily.AF_IPv4) + { + if (!NetUtil.IsValidIpV4Address(address)) + { + throw new HAProxyProtocolException("invalid IPv4 address: " + address); + } + } + else if (addrFamily == AddressFamily.AF_IPv6) + { + if (!NetUtil.IsValidIpV6Address(address)) + { + throw new HAProxyProtocolException("invalid IPv6 address: " + address); + } + } + else + { + throw new HAProxyProtocolException("Not possible"); + } + } + + /** + * Validate a UDP/TCP port + * + * @param port the UDP/TCP port + * @throws HAProxyProtocolException if the port is out of range (0-65535 inclusive) + */ + private static void CheckPort(int port) + { + if (port < 0 || port > 65535) + { + throw new HAProxyProtocolException("invalid port: " + port + " (expected: 1 ~ 65535)"); + } + } + + /** + * Returns the {@link HAProxyProtocolVersion} of this {@link HAProxyMessage}. + */ + public HAProxyProtocolVersion ProtocolVersion() + { + return this.protocolVersion; + } + + /** + * Returns the {@link HAProxyCommand} of this {@link HAProxyMessage}. + */ + public HAProxyCommand Command() + { + return this.command; + } + + /** + * Returns the {@link HAProxyProxiedProtocol} of this {@link HAProxyMessage}. + */ + public HAProxyProxiedProtocol ProxiedProtocol() + { + return this.proxiedProtocol; + } + + /** + * Returns the human-readable source address of this {@link HAProxyMessage}. + */ + public string SourceAddress() + { + return this.sourceAddress; + } + + /** + * Returns the human-readable destination address of this {@link HAProxyMessage}. + */ + public string DestinationAddress() + { + return this.destinationAddress; + } + + /** + * Returns the UDP/TCP source port of this {@link HAProxyMessage}. + */ + public int SourcePort() + { + return this.sourcePort; + } + + /** + * Returns the UDP/TCP destination port of this {@link HAProxyMessage}. + */ + public int DestinationPort() + { + return this.destinationPort; + } + + /** + * Returns a list of {@link HAProxyTLV} or an empty list if no TLVs are present. + *

+ * TLVs are only available for the Proxy Protocol V2 + */ + public IList Tlvs() + { + return this.tlvs; + } + } +} diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyMessageDecoder.cs b/src/DotNetty.Codecs.HaProxy/HAProxyMessageDecoder.cs new file mode 100644 index 000000000..a4b3e77b8 --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/HAProxyMessageDecoder.cs @@ -0,0 +1,452 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs.HaProxy +{ + using System; + using System.Collections.Generic; + using System.Text; + using DotNetty.Buffers; + using DotNetty.Transport.Channels; + + /** + * Decodes an HAProxy proxy protocol header + * + * @see Proxy Protocol Specification + */ + public class HAProxyMessageDecoder : ByteToMessageDecoder + { + /** + * Maximum possible length of a v1 proxy protocol header per spec + */ + const int V1_MAX_LENGTH = 108; + + /** + * Maximum possible length of a v2 proxy protocol header (fixed 16 bytes + max unsigned short) + */ + const int V2_MAX_LENGTH = 16 + 65535; + + /** + * Minimum possible length of a fully functioning v2 proxy protocol header (fixed 16 bytes + v2 address info space) + */ + const int V2_MIN_LENGTH = 16 + 216; + + /** + * Maximum possible length for v2 additional TLV data (max unsigned short - max v2 address info space) + */ + const int V2_MAX_TLV = 65535 - 216; + + /** + * Version 1 header delimiter is always '\r\n' per spec + */ + const int DELIMITER_LENGTH = 2; + + /** + * Binary header prefix + */ + static readonly byte[] BINARY_PREFIX = { + (byte) 0x0D, + (byte) 0x0A, + (byte) 0x0D, + (byte) 0x0A, + (byte) 0x00, + (byte) 0x0D, + (byte) 0x0A, + (byte) 0x51, + (byte) 0x55, + (byte) 0x49, + (byte) 0x54, + (byte) 0x0A + }; + + static readonly byte[] TEXT_PREFIX = { + (byte) 'P', + (byte) 'R', + (byte) 'O', + (byte) 'X', + (byte) 'Y', + }; + + /** + * Binary header prefix length + */ + static readonly int BINARY_PREFIX_LENGTH = BINARY_PREFIX.Length; + + /** + * {@link ProtocolDetectionResult} for {@link HAProxyProtocolVersion#V1}. + */ + static readonly ProtocolDetectionResult DETECTION_RESULT_V1 = ProtocolDetectionResult.Detected(HAProxyProtocolVersion.V1); + + /** + * {@link ProtocolDetectionResult} for {@link HAProxyProtocolVersion#V2}. + */ + static readonly ProtocolDetectionResult DETECTION_RESULT_V2 = ProtocolDetectionResult.Detected(HAProxyProtocolVersion.V2); + + /** + * {@code true} if we're discarding input because we're already over maxLength + */ + bool discarding; + + /** + * Number of discarded bytes + */ + int discardedBytes; + + /** + * {@code true} if we're finished decoding the proxy protocol header + */ + bool finished; + + /** + * Protocol specification version + */ + int version = -1; + + /** + * The latest v2 spec (2014/05/18) allows for additional data to be sent in the proxy protocol header beyond the + * address information block so now we need a configurable max header size + */ + readonly int v2MaxHeaderSize; + + /** + * Creates a new decoder with no additional data (TLV) restrictions + */ + public HAProxyMessageDecoder() + { + v2MaxHeaderSize = V2_MAX_LENGTH; + SingleDecode = true; + } + + /** + * Creates a new decoder with restricted additional data (TLV) size + *

+ * Note: limiting TLV size only affects processing of v2, binary headers. Also, as allowed by the 1.5 spec + * TLV data is currently ignored. For maximum performance it would be best to configure your upstream proxy host to + * NOT send TLV data and instantiate with a max TLV size of {@code 0}. + *

+ * + * @param maxTlvSize maximum number of bytes allowed for additional data (Type-Length-Value vectors) in a v2 header + */ + public HAProxyMessageDecoder(int maxTlvSize) + { + SingleDecode = true; + if (maxTlvSize < 1) + { + v2MaxHeaderSize = V2_MIN_LENGTH; + } + else if (maxTlvSize > V2_MAX_TLV) + { + v2MaxHeaderSize = V2_MAX_LENGTH; + } + else + { + int calcMax = maxTlvSize + V2_MIN_LENGTH; + if (calcMax > V2_MAX_LENGTH) + { + v2MaxHeaderSize = V2_MAX_LENGTH; + } + else + { + v2MaxHeaderSize = calcMax; + } + } + } + + /** + * Returns the proxy protocol specification version in the buffer if the version is found. + * Returns -1 if no version was found in the buffer. + */ + private static int FindVersion(IByteBuffer buffer) + { + int n = buffer.ReadableBytes; + // per spec, the version number is found in the 13th byte + if (n < 13) + { + return -1; + } + + int idx = buffer.ReaderIndex; + return Match(BINARY_PREFIX, buffer, idx) ? buffer.GetByte(idx + BINARY_PREFIX_LENGTH) : 1; + } + + /** + * Returns the index in the buffer of the end of header if found. + * Returns -1 if no end of header was found in the buffer. + */ + private static int FindEndOfHeader(IByteBuffer buffer) + { + int n = buffer.ReadableBytes; + + // per spec, the 15th and 16th bytes contain the address length in bytes + if (n < 16) + { + return -1; + } + + int offset = buffer.ReaderIndex + 14; + + // the total header length will be a fixed 16 byte sequence + the dynamic address information block + int totalHeaderBytes = 16 + buffer.GetUnsignedShort(offset); + + // ensure we actually have the full header available + if (n >= totalHeaderBytes) + { + return totalHeaderBytes; + } + else + { + return -1; + } + } + + /** + * Returns the index in the buffer of the end of line found. + * Returns -1 if no end of line was found in the buffer. + */ + private static int FindEndOfLine(IByteBuffer buffer) + { + int n = buffer.WriterIndex; + for (int i = buffer.ReaderIndex; i < n; i++) + { + byte b = buffer.GetByte(i); + if (b == '\r' && i < n - 1 && buffer.GetByte(i + 1) == '\n') + { + return i; // \r\n + } + } + return -1; // Not found. + } + + public override void ChannelRead(IChannelHandlerContext context, object message) + { + base.ChannelRead(context, message); + if (finished) { + context.Channel.Pipeline.Remove(this); + } + } + + protected override sealed void Decode(IChannelHandlerContext context, IByteBuffer input, List output) + { + // determine the specification version + if (version == -1 && (version = FindVersion(input)) == -1) + { + return; + } + + IByteBuffer decoded; + + if (version == 1) + { + decoded = DecodeLine(context, input); + } + else + { + decoded = DecodeStruct(context, input); + } + + if (decoded != null) + { + finished = true; + try + { + if (version == 1) + { + output.Add(HAProxyMessage.DecodeHeader(decoded.ToString(Encoding.ASCII))); + } + else + { + output.Add(HAProxyMessage.DecodeHeader(decoded)); + } + } + catch (HAProxyProtocolException e) + { + Fail(context, null, e); + } + } + } + + /** + * Create a frame out of the {@link ByteBuf} and return it. + * Based on code from {@link LineBasedFrameDecoder#decode(ChannelHandlerContext, ByteBuf)}. + * + * @param ctx the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to + * @param buffer the {@link ByteBuf} from which to read data + * @return frame the {@link ByteBuf} which represent the frame or {@code null} if no frame could + * be created + */ + private IByteBuffer DecodeStruct(IChannelHandlerContext ctx, IByteBuffer buffer) + { + int eoh = FindEndOfHeader(buffer); + if (!discarding) + { + if (eoh >= 0) + { + int length = eoh - buffer.ReaderIndex; + if (length > v2MaxHeaderSize) + { + buffer.SetReaderIndex(eoh); + FailOverLimit(ctx, length); + return null; + } + return buffer.ReadSlice(length); + } + else + { + int length = buffer.ReadableBytes; + if (length > v2MaxHeaderSize) + { + discardedBytes = length; + buffer.SkipBytes(length); + discarding = true; + FailOverLimit(ctx, "over " + discardedBytes); + } + return null; + } + } + else + { + if (eoh >= 0) + { + buffer.SetReaderIndex(eoh); + discardedBytes = 0; + discarding = false; + } + else + { + discardedBytes = buffer.ReadableBytes; + buffer.SkipBytes(discardedBytes); + } + return null; + } + } + + /** + * Create a frame out of the {@link ByteBuf} and return it. + * Based on code from {@link LineBasedFrameDecoder#decode(ChannelHandlerContext, ByteBuf)}. + * + * @param ctx the {@link ChannelHandlerContext} which this {@link HAProxyMessageDecoder} belongs to + * @param buffer the {@link ByteBuf} from which to read data + * @return frame the {@link ByteBuf} which represent the frame or {@code null} if no frame could + * be created + */ + private IByteBuffer DecodeLine(IChannelHandlerContext ctx, IByteBuffer buffer) + { + int eol = FindEndOfLine(buffer); + if (!discarding) + { + if (eol >= 0) + { + int length = eol - buffer.ReaderIndex; + if (length > V1_MAX_LENGTH) + { + buffer.SetReaderIndex(eol + DELIMITER_LENGTH); + FailOverLimit(ctx, length); + return null; + } + IByteBuffer frame = buffer.ReadSlice(length); + buffer.SkipBytes(DELIMITER_LENGTH); + return frame; + } + else + { + int length = buffer.ReadableBytes; + if (length > V1_MAX_LENGTH) + { + discardedBytes = length; + buffer.SkipBytes(length); + discarding = true; + FailOverLimit(ctx, "over " + discardedBytes); + } + return null; + } + } + else + { + if (eol >= 0) + { + int delimLength = buffer.GetByte(eol) == '\r' ? 2 : 1; + buffer.SetReaderIndex(eol + delimLength); + discardedBytes = 0; + discarding = false; + } + else + { + discardedBytes = buffer.ReadableBytes; + buffer.SkipBytes(discardedBytes); + } + return null; + } + } + + private void FailOverLimit(IChannelHandlerContext ctx, int length) + { + FailOverLimit(ctx, length.ToString()); + } + + private void FailOverLimit(IChannelHandlerContext ctx, string length) + { + int maxLength = version == 1 ? V1_MAX_LENGTH : v2MaxHeaderSize; + Fail(ctx, "header length (" + length + ") exceeds the allowed maximum (" + maxLength + ')', null); + } + + private void Fail(IChannelHandlerContext ctx, string errMsg, Exception e) + { + finished = true; + ctx.CloseAsync().RunSynchronously(); // drop connection immediately per spec + HAProxyProtocolException ppex; + if (errMsg != null && e != null) + { + ppex = new HAProxyProtocolException(errMsg, e); + } + else if (errMsg != null) + { + ppex = new HAProxyProtocolException(errMsg); + } + else if (e != null) + { + ppex = new HAProxyProtocolException(e); + } + else + { + ppex = new HAProxyProtocolException(); + } + throw ppex; + } + + /** + * Returns the {@link ProtocolDetectionResult} for the given {@link ByteBuf}. + */ + public static ProtocolDetectionResult DetectProtocol(IByteBuffer buffer) + { + if (buffer.ReadableBytes < 12) + { + return ProtocolDetectionResult.NeedsMoreData(); + } + + int idx = buffer.ReaderIndex; + + if (Match(BINARY_PREFIX, buffer, idx)) + { + return DETECTION_RESULT_V2; + } + if (Match(TEXT_PREFIX, buffer, idx)) + { + return DETECTION_RESULT_V1; + } + return ProtocolDetectionResult.Invalid(); + } + + private static bool Match(byte[] prefix, IByteBuffer buffer, int idx) + { + for (int i = 0; i < prefix.Length; i++) + { + byte b = buffer.GetByte(idx + i); + if (b != prefix[i]) + { + return false; + } + } + return true; + } + } +} diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyProtocolException.cs b/src/DotNetty.Codecs.HaProxy/HAProxyProtocolException.cs new file mode 100644 index 000000000..1cb339a57 --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/HAProxyProtocolException.cs @@ -0,0 +1,33 @@ +namespace DotNetty.Codecs.HaProxy +{ + using System; + + /** + * A {@link DecoderException} which is thrown when an invalid HAProxy proxy protocol header is encountered + */ + public class HAProxyProtocolException : DecoderException + { + + public HAProxyProtocolException() + : base("") + { + + } + + public HAProxyProtocolException(string message) + : base(message) + { + } + + public HAProxyProtocolException(Exception cause) + : base(cause) + { + } + + public HAProxyProtocolException(string message, Exception cause) + : base(cause) + { + } + + } +} diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyProtocolVersion.cs b/src/DotNetty.Codecs.HaProxy/HAProxyProtocolVersion.cs new file mode 100644 index 000000000..d40feaa9d --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/HAProxyProtocolVersion.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs.HaProxy +{ + using System; + using System.Collections.Generic; + + /** + * The HAProxy proxy protocol specification version. + */ + public sealed class HAProxyProtocolVersion + { + /** + * The ONE proxy protocol version represents a version 1 (human-readable) header. + */ + public static readonly HAProxyProtocolVersion V1 = new HAProxyProtocolVersion(HAProxyConstants.VERSION_ONE_BYTE); + /** + * The TWO proxy protocol version represents a version 2 (binary) header. + */ + public static readonly HAProxyProtocolVersion V2 = new HAProxyProtocolVersion(HAProxyConstants.VERSION_TWO_BYTE); + + public static IEnumerable Values + { + get + { + yield return V1; + yield return V2; + } + } + + /** + * Returns the {@link HAProxyProtocolVersion} represented by the highest 4 bits of the specified byte. + * + * @param verCmdByte protocol version and command byte + */ + public static HAProxyProtocolVersion ValueOf(byte verCmdByte) + { + int version = verCmdByte & VERSION_MASK; + switch ((byte)version) + { + case HAProxyConstants.VERSION_TWO_BYTE: + return V2; + case HAProxyConstants.VERSION_ONE_BYTE: + return V1; + default: + throw new ArgumentOutOfRangeException(nameof(verCmdByte), "unknown version: " + version); + } + } + + /** + * The highest 4 bits of the protocol version and command byte contain the version + */ + const byte VERSION_MASK = (byte)0xf0; + + readonly byte byteValue; + + HAProxyProtocolVersion(byte byteValue) + { + this.byteValue = byteValue; + } + + /** + * Returns the byte value of this command. + */ + public byte ByteValue() + { + return this.byteValue; + } + } +} diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyProxiedProtocol.cs b/src/DotNetty.Codecs.HaProxy/HAProxyProxiedProtocol.cs new file mode 100644 index 000000000..123d42b9b --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/HAProxyProxiedProtocol.cs @@ -0,0 +1,287 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs.HaProxy +{ + using System; + using System.Collections.Generic; + + /** + * A protocol proxied by HAProxy which is represented by its transport protocol and address family. + */ + public sealed class HAProxyProxiedProtocol + { + /** + * The UNKNOWN represents a connection which was forwarded for an unknown protocol and an unknown address family. + */ + public static readonly HAProxyProxiedProtocol UNKNOWN = new HAProxyProxiedProtocol("UNKNOWN", HAProxyConstants.TPAF_UNKNOWN_BYTE, AddressFamily.AF_UNSPEC, TransportProtocol.UNSPEC); + /** + * The TCP4 represents a connection which was forwarded for an IPv4 client over TCP. + */ + public static readonly HAProxyProxiedProtocol TCP4 = new HAProxyProxiedProtocol("TCP4", HAProxyConstants.TPAF_TCP4_BYTE, AddressFamily.AF_IPv4, TransportProtocol.STREAM); + /** + * The TCP6 represents a connection which was forwarded for an IPv6 client over TCP. + */ + public static readonly HAProxyProxiedProtocol TCP6 = new HAProxyProxiedProtocol("TCP6", HAProxyConstants.TPAF_TCP6_BYTE, AddressFamily.AF_IPv6, TransportProtocol.STREAM); + /** + * The UDP4 represents a connection which was forwarded for an IPv4 client over UDP. + */ + public static readonly HAProxyProxiedProtocol UDP4 = new HAProxyProxiedProtocol("UDP4", HAProxyConstants.TPAF_UDP4_BYTE, AddressFamily.AF_IPv4, TransportProtocol.DGRAM); + /** + * The UDP6 represents a connection which was forwarded for an IPv6 client over UDP. + */ + public static readonly HAProxyProxiedProtocol UDP6 = new HAProxyProxiedProtocol("UDP6", HAProxyConstants.TPAF_UDP6_BYTE, AddressFamily.AF_IPv6, TransportProtocol.DGRAM); + /** + * The UNIX_STREAM represents a connection which was forwarded for a UNIX stream socket. + */ + public static readonly HAProxyProxiedProtocol UNIX_STREAM = new HAProxyProxiedProtocol("UNIX_STREAM", HAProxyConstants.TPAF_UNIX_STREAM_BYTE, AddressFamily.AF_UNIX, TransportProtocol.STREAM); + /** + * The UNIX_DGRAM represents a connection which was forwarded for a UNIX datagram socket. + */ + public static readonly HAProxyProxiedProtocol UNIX_DGRAM = new HAProxyProxiedProtocol("UNIX_DGRAM", HAProxyConstants.TPAF_UNIX_DGRAM_BYTE, AddressFamily.AF_UNIX, TransportProtocol.DGRAM); + + public static IEnumerable Values + { + get + { + yield return UNKNOWN; + yield return TCP4; + yield return TCP6; + yield return UDP4; + yield return UDP6; + yield return UNIX_STREAM; + yield return UNIX_DGRAM; + } + } + + static readonly Dictionary NAME_LOOKUP = new Dictionary(); + + readonly byte byteValue; + readonly AddressFamily addressFamily; + readonly TransportProtocol transportProtocol; + + HAProxyProxiedProtocol(string name, byte byteValue, AddressFamily addressFamily, TransportProtocol transportProtocol) + { + NAME_LOOKUP.Add(name, this); + this.byteValue = byteValue; + this.addressFamily = addressFamily; + this.transportProtocol = transportProtocol; + } + + /** + * Returns the {@link HAProxyProxiedProtocol} represented by the specified byte. + * + * @param tpafByte transport protocol and address family byte + */ + public static HAProxyProxiedProtocol ValueOf(byte tpafByte) + { + switch (tpafByte) + { + case HAProxyConstants.TPAF_TCP4_BYTE: + return TCP4; + case HAProxyConstants.TPAF_TCP6_BYTE: + return TCP6; + case HAProxyConstants.TPAF_UNKNOWN_BYTE: + return UNKNOWN; + case HAProxyConstants.TPAF_UDP4_BYTE: + return UDP4; + case HAProxyConstants.TPAF_UDP6_BYTE: + return UDP6; + case HAProxyConstants.TPAF_UNIX_STREAM_BYTE: + return UNIX_STREAM; + case HAProxyConstants.TPAF_UNIX_DGRAM_BYTE: + return UNIX_DGRAM; + default: + throw new ArgumentOutOfRangeException(nameof(tpafByte), "unknown transport protocol + address family: " + (tpafByte & 0xFF)); + } + } + + public static HAProxyProxiedProtocol ValueOf(string value) + { + HAProxyProxiedProtocol protocol; + NAME_LOOKUP.TryGetValue(value, out protocol); + if (protocol != null) + { + return protocol; + } + throw new ArgumentOutOfRangeException(nameof(value)); + } + + /** + * Returns the byte value of this protocol and address family. + */ + public byte ByteValue() + { + return this.byteValue; + } + + /** + * Returns the {@link AddressFamily} of this protocol and address family. + */ + public AddressFamily AddressFamilyType() + { + return this.addressFamily; + } + + /** + * Returns the {@link TransportProtocol} of this protocol and address family. + */ + public TransportProtocol TransportProtocolType() + { + return this.transportProtocol; + } + + /** + * The address family of an HAProxy proxy protocol header. + */ + public sealed class AddressFamily + { + /** + * The UNSPECIFIED address family represents a connection which was forwarded for an unknown protocol. + */ + public static readonly AddressFamily AF_UNSPEC = new AddressFamily(HAProxyConstants.AF_UNSPEC_BYTE); + /** + * The IPV4 address family represents a connection which was forwarded for an IPV4 client. + */ + public static readonly AddressFamily AF_IPv4 = new AddressFamily(HAProxyConstants.AF_IPV4_BYTE); + /** + * The IPV6 address family represents a connection which was forwarded for an IPV6 client. + */ + public static readonly AddressFamily AF_IPv6 = new AddressFamily(HAProxyConstants.AF_IPV6_BYTE); + /** + * The UNIX address family represents a connection which was forwarded for a unix socket. + */ + public static readonly AddressFamily AF_UNIX = new AddressFamily(HAProxyConstants.AF_UNIX_BYTE); + + public static IEnumerable Values + { + get + { + yield return AF_UNSPEC; + yield return AF_IPv4; + yield return AF_IPv6; + yield return AF_UNIX; + } + } + + /** + * The highest 4 bits of the transport protocol and address family byte contain the address family + */ + const byte FAMILY_MASK = (byte)0xf0; + + readonly byte byteValue; + + /** + * Creates a new instance + */ + AddressFamily(byte byteValue) + { + this.byteValue = byteValue; + } + + /** + * Returns the {@link AddressFamily} represented by the highest 4 bits of the specified byte. + * + * @param tpafByte transport protocol and address family byte + */ + public static AddressFamily ValueOf(byte tpafByte) + { + int addressFamily = tpafByte & FAMILY_MASK; + switch ((byte)addressFamily) + { + case HAProxyConstants.AF_IPV4_BYTE: + return AF_IPv4; + case HAProxyConstants.AF_IPV6_BYTE: + return AF_IPv6; + case HAProxyConstants.AF_UNSPEC_BYTE: + return AF_UNSPEC; + case HAProxyConstants.AF_UNIX_BYTE: + return AF_UNIX; + default: + throw new ArgumentOutOfRangeException(nameof(tpafByte), "unknown address family: " + addressFamily); + } + } + + /** + * Returns the byte value of this address family. + */ + public byte ByteValue() + { + return this.byteValue; + } + } + + /** + * The transport protocol of an HAProxy proxy protocol header + */ + public sealed class TransportProtocol + { + /** + * The UNSPEC transport protocol represents a connection which was forwarded for an unknown protocol. + */ + public static readonly TransportProtocol UNSPEC = new TransportProtocol(HAProxyConstants.TRANSPORT_UNSPEC_BYTE); + /** + * The STREAM transport protocol represents a connection which was forwarded for a TCP connection. + */ + public static readonly TransportProtocol STREAM = new TransportProtocol(HAProxyConstants.TRANSPORT_STREAM_BYTE); + /** + * The DGRAM transport protocol represents a connection which was forwarded for a UDP connection. + */ + public static readonly TransportProtocol DGRAM = new TransportProtocol(HAProxyConstants.TRANSPORT_DGRAM_BYTE); + + public static IEnumerable Values + { + get + { + yield return UNSPEC; + yield return STREAM; + yield return DGRAM; + } + } + + /** + * The transport protocol is specified in the lowest 4 bits of the transport protocol and address family byte + */ + const byte TRANSPORT_MASK = 0x0f; + + readonly byte transportByte; + + /** + * Creates a new instance. + */ + TransportProtocol(byte transportByte) + { + this.transportByte = transportByte; + } + + /** + * Returns the {@link TransportProtocol} represented by the lowest 4 bits of the specified byte. + * + * @param tpafByte transport protocol and address family byte + */ + public static TransportProtocol ValueOf(byte tpafByte) + { + int transportProtocol = tpafByte & TRANSPORT_MASK; + switch ((byte)transportProtocol) + { + case HAProxyConstants.TRANSPORT_STREAM_BYTE: + return STREAM; + case HAProxyConstants.TRANSPORT_UNSPEC_BYTE: + return UNSPEC; + case HAProxyConstants.TRANSPORT_DGRAM_BYTE: + return DGRAM; + default: + throw new ArgumentOutOfRangeException(nameof(tpafByte), "unknown transport protocol: " + transportProtocol); + } + } + + /** + * Returns the byte value of this transport protocol. + */ + public byte ByteValue() + { + return this.transportByte; + } + } + } +} diff --git a/src/DotNetty.Codecs.HaProxy/HAProxySSLTLV.cs b/src/DotNetty.Codecs.HaProxy/HAProxySSLTLV.cs new file mode 100644 index 000000000..d4d96bd9b --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/HAProxySSLTLV.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs.HaProxy +{ + using System.Collections.Generic; + using DotNetty.Buffers; + + /** + * Represents a {@link HAProxyTLV} of the type {@link HAProxyTLV.Type#PP2_TYPE_SSL}. + * This TLV encapsulates other TLVs and has additional information like verification information and a client bitfield. + */ + public sealed class HAProxySSLTLV : HAProxyTLV + { + + readonly int verify; + readonly IList tlvs; + readonly byte clientBitField; + + /** + * Creates a new HAProxySSLTLV + * + * @param verify the verification result as defined in the specification for the pp2_tlv_ssl struct (see + * http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt) + * @param clientBitField the bitfield with client information + * @param tlvs the encapsulated {@link HAProxyTLV}s + * @param rawContent the raw TLV content + */ + internal HAProxySSLTLV(int verify, byte clientBitField, List tlvs, IByteBuffer rawContent) : base(Type.PP2_TYPE_SSL, (byte)0x20, rawContent) + { + this.verify = verify; + this.tlvs = tlvs.AsReadOnly(); + this.clientBitField = clientBitField; + } + + /** + * Returns {@code true} if the bit field for PP2_CLIENT_CERT_CONN was set + */ + public bool IsPP2ClientCertConn() + { + return (this.clientBitField & 0x2) != 0; + } + + /** + * Returns {@code true} if the bit field for PP2_CLIENT_SSL was set + */ + public bool IsPP2ClientSSL() + { + return (this.clientBitField & 0x1) != 0; + } + + /** + * Returns {@code true} if the bit field for PP2_CLIENT_CERT_SESS was set + */ + public bool IsPP2ClientCertSess() + { + return (this.clientBitField & 0x4) != 0; + } + + /** + * Returns the verification result + */ + public int Verify() + { + return this.verify; + } + + /** + * Returns an unmodifiable Set of encapsulated {@link HAProxyTLV}s. + */ + public IList EncapsulatedTLVs() + { + return this.tlvs; + } + } +} diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyTLV.cs b/src/DotNetty.Codecs.HaProxy/HAProxyTLV.cs new file mode 100644 index 000000000..3f4bdc071 --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/HAProxyTLV.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs.HaProxy +{ + using DotNetty.Buffers; + + /** + * A Type-Length Value (TLV vector) that can be added to the PROXY protocol + * to include additional information like SSL information. + * + * @see HAProxySSLTLV + */ + public class HAProxyTLV : DefaultByteBufferHolder + { + + readonly Type type; + readonly byte typeByteValue; + + /** + * The registered types a TLV can have regarding the PROXY protocol 1.5 spec + */ + public enum Type + { + PP2_TYPE_ALPN, + PP2_TYPE_AUTHORITY, + PP2_TYPE_SSL, + PP2_TYPE_SSL_VERSION, + PP2_TYPE_SSL_CN, + PP2_TYPE_NETNS, + /** + * A TLV type that is not officially defined in the spec. May be used for nonstandard TLVs + */ + OTHER + } + + /** + * Returns the the {@link Type} for a specific byte value as defined in the PROXY protocol 1.5 spec + *

+ * If the byte value is not an official one, it will return {@link Type#OTHER}. + * + * @param byteValue the byte for a type + * + * @return the {@link Type} of a TLV + */ + public static Type TypeForByteValue(byte byteValue) + { + switch (byteValue) + { + case 0x01: + return Type.PP2_TYPE_ALPN; + case 0x02: + return Type.PP2_TYPE_AUTHORITY; + case 0x20: + return Type.PP2_TYPE_SSL; + case 0x21: + return Type.PP2_TYPE_SSL_VERSION; + case 0x22: + return Type.PP2_TYPE_SSL_CN; + case 0x30: + return Type.PP2_TYPE_NETNS; + default: + return Type.OTHER; + } + } + + /** + * Creates a new HAProxyTLV + * + * @param type the {@link Type} of the TLV + * @param typeByteValue the byteValue of the TLV. This is especially important if non-standard TLVs are used + * @param content the raw content of the TLV + */ + internal HAProxyTLV(Type type, byte typeByteValue, IByteBuffer content) : base(content) + { + this.type = type; + this.typeByteValue = typeByteValue; + } + + /** + * Returns the {@link Type} of this TLV + */ + public Type TLVType() + { + return this.type; + } + + /** + * Returns the type of the TLV as byte + */ + public byte TypeByteValue() + { + return this.typeByteValue; + } + + public override IByteBufferHolder Replace(IByteBuffer content) + { + return new HAProxyTLV(type, typeByteValue, content); + } + + } +} diff --git a/src/DotNetty.Codecs.HaProxy/NetUtil.cs b/src/DotNetty.Codecs.HaProxy/NetUtil.cs new file mode 100644 index 000000000..22ae9a45e --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/NetUtil.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Util +{ + class NetUtil + { + + /** + * Takes a string and parses it to see if it is a valid IPV4 address. + * + * @return true, if the string represents an IPV4 address in dotted + * notation, false otherwise + */ + public static bool IsValidIpV4Address(string ip) + { + return IsValidIpV4Address(ip, 0, ip.Length); + } + + private static bool IsValidIpV4Address(string ip, int from, int toExcluded) + { + int len = toExcluded - from; + int i; + return len <= 15 && len >= 7 && + (i = ip.IndexOf('.', from + 1)) > 0 && IsValidIpV4Word(ip, from, i) && + (i = ip.IndexOf('.', from = i + 2)) > 0 && IsValidIpV4Word(ip, from - 1, i) && + (i = ip.IndexOf('.', from = i + 2)) > 0 && IsValidIpV4Word(ip, from - 1, i) && + IsValidIpV4Word(ip, i + 1, toExcluded); + } + + private static bool IsValidIpV4Word(string word, int from, int toExclusive) + { + int len = toExclusive - from; + char c0, c1, c2; + if (len < 1 || len > 3 || (c0 = word[from]) < '0') + { + return false; + } + if (len == 3) + { + return (c1 = word[from + 1]) >= '0' && + (c2 = word[from + 2]) >= '0' && + (c0 <= '1' && c1 <= '9' && c2 <= '9' || + c0 == '2' && c1 <= '5' && (c2 <= '5' || c1 < '5' && c2 <= '9')); + } + return c0 <= '9' && (len == 1 || IsValidNumericChar(word[from + 1])); + } + + private static bool IsValidNumericChar(char c) + { + return c >= '0' && c <= '9'; + } + + public static bool IsValidIpV6Address(string ip) + { + int end = ip.Length; + if (end < 2) + { + return false; + } + + // strip "[]" + int start; + char c = ip[0]; + if (c == '[') + { + end--; + if (ip[end] != ']') + { + // must have a close ] + return false; + } + start = 1; + c = ip[1]; + } + else + { + start = 0; + } + + int colons; + int compressBegin; + if (c == ':') + { + // an IPv6 address can start with "::" or with a number + if (ip[start + 1] != ':') + { + return false; + } + colons = 2; + compressBegin = start; + start += 2; + } + else + { + colons = 0; + compressBegin = -1; + } + + int wordLen = 0; + for (int i = start; i < end; i++) + { + c = ip[i]; + if (IsValidHexChar(c)) + { + if (wordLen < 4) + { + wordLen++; + continue; + } + return false; + } + + switch (c) + { + case ':': + if (colons > 7) + { + return false; + } + if (ip[i - 1] == ':') + { + if (compressBegin >= 0) + { + return false; + } + compressBegin = i - 1; + } + else + { + wordLen = 0; + } + colons++; + break; + case '.': + // case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d + + // check a normal case (6 single colons) + if (compressBegin < 0 && colons != 6 || + // a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an + // IPv4 ending, otherwise 7 :'s is bad + (colons == 7 && compressBegin >= start || colons > 7)) + { + return false; + } + + // Verify this address is of the correct structure to contain an IPv4 address. + // It must be IPv4-Mapped or IPv4-Compatible + // (see https://tools.ietf.org/html/rfc4291#section-2.5.5). + int ipv4Start = i - wordLen; + int j = ipv4Start - 2; // index of character before the previous ':'. + if (IsValidIPv4MappedChar(ip[j])) + { + if (!IsValidIPv4MappedChar(ip[j - 1]) || + !IsValidIPv4MappedChar(ip[j - 2]) || + !IsValidIPv4MappedChar(ip[j - 3])) + { + return false; + } + j -= 5; + } + + for (; j >= start; --j) + { + char tmpChar = ip[j]; + if (tmpChar != '0' && tmpChar != ':') + { + return false; + } + } + + // 7 - is minimum IPv4 address length + int ipv4End = ip.IndexOf('%', ipv4Start + 7); + if (ipv4End < 0) + { + ipv4End = end; + } + return IsValidIpV4Address(ip, ipv4Start, ipv4End); + case '%': + // strip the interface name/index after the percent sign + end = i; + goto loopEnd; + default: + return false; + } + } + loopEnd: + + // normal case without compression + if (compressBegin < 0) + { + return colons == 7 && wordLen > 0; + } + + return compressBegin + 2 == end || + // 8 colons is valid only if compression in start or end + wordLen > 0 && (colons < 8 || compressBegin <= start); + } + + private static bool IsValidHexChar(char c) + { + return c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c >= 'a' && c <= 'f'; + } + + private static bool IsValidIPv4MappedChar(char c) + { + return c == 'f' || c == 'F'; + } + } +} diff --git a/src/DotNetty.Codecs.HaProxy/Properties/AssemblyInfo.cs b/src/DotNetty.Codecs.HaProxy/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..bea1740fb --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using System.Resources; + +[assembly: NeutralResourcesLanguage("en-US")] +[assembly: AssemblyMetadata("Serviceable", "True")] \ No newline at end of file diff --git a/src/DotNetty.Codecs.HaProxy/ProtocolDetectionResult.cs b/src/DotNetty.Codecs.HaProxy/ProtocolDetectionResult.cs new file mode 100644 index 000000000..29a01825e --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/ProtocolDetectionResult.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs +{ + using System; + + /** + * Result of detecting a protocol. + * + * @param the type of the protocol + */ + public sealed class ProtocolDetectionResult + { + + static readonly ProtocolDetectionResult NEEDS_MORE_DATE = new ProtocolDetectionResult(ProtocolDetectionState.NEEDS_MORE_DATA, default(T)); + static readonly ProtocolDetectionResult INVALID = new ProtocolDetectionResult(ProtocolDetectionState.INVALID, default(T)); + + readonly ProtocolDetectionState state; + readonly T result; + + /** + * Returns a {@link ProtocolDetectionResult} that signals that more data is needed to detect the protocol. + */ + public static ProtocolDetectionResult NeedsMoreData() + { + return NEEDS_MORE_DATE; + } + + /** + * Returns a {@link ProtocolDetectionResult} that signals the data was invalid for the protocol. + */ + public static ProtocolDetectionResult Invalid() + { + return INVALID; + } + + /** + * Returns a {@link ProtocolDetectionResult} which holds the detected protocol. + */ + public static ProtocolDetectionResult Detected(T protocol) + { + if (protocol == null) + { + throw new ArgumentNullException(nameof(protocol)); + } + return new ProtocolDetectionResult(ProtocolDetectionState.DETECTED, protocol); + } + + private ProtocolDetectionResult(ProtocolDetectionState state, T result) + { + this.state = state; + this.result = result; + } + + /** + * Return the {@link ProtocolDetectionState}. If the state is {@link ProtocolDetectionState#DETECTED} you + * can retrieve the protocol via {@link #detectedProtocol()}. + */ + public ProtocolDetectionState State() + { + return this.state; + } + + /** + * Returns the protocol if {@link #state()} returns {@link ProtocolDetectionState#DETECTED}, otherwise {@code null}. + */ + public T DetectedProtocol() + { + return this.result; + } + } +} diff --git a/src/DotNetty.Codecs.HaProxy/ProtocolDetectionState.cs b/src/DotNetty.Codecs.HaProxy/ProtocolDetectionState.cs new file mode 100644 index 000000000..a172227ef --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/ProtocolDetectionState.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs +{ + /** + * The state of the current detection. + */ + public enum ProtocolDetectionState + { + /** + * Need more data to detect the protocol. + */ + NEEDS_MORE_DATA, + + /** + * The data was invalid. + */ + INVALID, + + /** + * Protocol was detected, + */ + DETECTED + } +} From 2b38dabd7f88888b3700dd5675a9817829f9d25a Mon Sep 17 00:00:00 2001 From: Jon Steinich Date: Tue, 9 Jan 2018 09:05:45 -0600 Subject: [PATCH 2/4] 1st pass at porting ha proxy test cases Fixed a couple issues that tests caught --- DotNetty.sln | 7 + src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs | 19 +- .../HAProxyMessageDecoder.cs | 2 +- .../HAProxyProxiedProtocol.cs | 4 +- .../Properties/Friends.cs | 7 + .../DotNetty.Codecs.HaProxy.Tests.csproj | 37 + .../HAProxyMessageDecoderTest.cs | 1051 +++++++++++++++++ .../HAProxySSLTLVTest.cs | 51 + .../Properties/AssemblyInfo.cs | 19 + 9 files changed, 1185 insertions(+), 12 deletions(-) create mode 100644 src/DotNetty.Codecs.HaProxy/Properties/Friends.cs create mode 100644 test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj create mode 100644 test/DotNetty.Codecs.HaProxy.Tests/HAProxyMessageDecoderTest.cs create mode 100644 test/DotNetty.Codecs.HaProxy.Tests/HAProxySSLTLVTest.cs create mode 100644 test/DotNetty.Codecs.HaProxy.Tests/Properties/AssemblyInfo.cs diff --git a/DotNetty.sln b/DotNetty.sln index edbdcf4e3..53afeb074 100644 --- a/DotNetty.sln +++ b/DotNetty.sln @@ -53,6 +53,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.Protobuf.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.ProtocolBuffers.Tests", "test\DotNetty.Codecs.ProtocolBuffers.Tests\DotNetty.Codecs.ProtocolBuffers.Tests.csproj", "{A7FC497E-790A-4980-B57C-32AF4AD4AB9D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.HaProxy.Tests", "test\DotNetty.Codecs.HaProxy.Tests\DotNetty.Codecs.HaProxy.Tests.csproj", "{34DBC7D4-A9AE-4BD6-96CD-DD7CDD4B5D5D}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.Redis.Tests", "test\DotNetty.Codecs.Redis.Tests\DotNetty.Codecs.Redis.Tests.csproj", "{08C19033-23B2-47D7-8332-86273AE287BC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetty.Codecs.Tests", "test\DotNetty.Codecs.Tests\DotNetty.Codecs.Tests.csproj", "{CE97D2EC-3EA9-4FEC-B304-F57646DB54FD}" @@ -159,6 +161,10 @@ Global {3AF8A62F-F5CE-4C2D-B356-8B9FDFA51668}.Debug|Any CPU.Build.0 = Debug|Any CPU {3AF8A62F-F5CE-4C2D-B356-8B9FDFA51668}.Release|Any CPU.ActiveCfg = Release|Any CPU {3AF8A62F-F5CE-4C2D-B356-8B9FDFA51668}.Release|Any CPU.Build.0 = Release|Any CPU + {34DBC7D4-A9AE-4BD6-96CD-DD7CDD4B5D5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34DBC7D4-A9AE-4BD6-96CD-DD7CDD4B5D5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34DBC7D4-A9AE-4BD6-96CD-DD7CDD4B5D5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34DBC7D4-A9AE-4BD6-96CD-DD7CDD4B5D5D}.Release|Any CPU.Build.0 = Release|Any CPU {A7FC497E-790A-4980-B57C-32AF4AD4AB9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A7FC497E-790A-4980-B57C-32AF4AD4AB9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {A7FC497E-790A-4980-B57C-32AF4AD4AB9D}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -275,6 +281,7 @@ Global {72C92F76-F804-4300-BFF1-459420D9EF0B} = {541093F6-616E-43D9-B671-FCD1F9C0A181} {E31A5D46-71B7-4B7E-A9F8-3F011822F28A} = {541093F6-616E-43D9-B671-FCD1F9C0A181} {3AF8A62F-F5CE-4C2D-B356-8B9FDFA51668} = {541093F6-616E-43D9-B671-FCD1F9C0A181} + {34DBC7D4-A9AE-4BD6-96CD-DD7CDD4B5D5D} = {541093F6-616E-43D9-B671-FCD1F9C0A181} {A7FC497E-790A-4980-B57C-32AF4AD4AB9D} = {541093F6-616E-43D9-B671-FCD1F9C0A181} {08C19033-23B2-47D7-8332-86273AE287BC} = {541093F6-616E-43D9-B671-FCD1F9C0A181} {CE97D2EC-3EA9-4FEC-B304-F57646DB54FD} = {541093F6-616E-43D9-B671-FCD1F9C0A181} diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs b/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs index 9fe22951f..c7393c68f 100644 --- a/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs +++ b/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs @@ -8,6 +8,7 @@ namespace DotNetty.Codecs.HaProxy using System.Text; using DotNetty.Buffers; using DotNetty.Common.Utilities; + using DotNetty.Util; using static DotNetty.Codecs.HaProxy.HAProxyProxiedProtocol; @@ -136,7 +137,7 @@ internal static HAProxyMessage DecodeHeader(IByteBuffer header) if (ver != HAProxyProtocolVersion.V2) { - throw new HAProxyProtocolException("version 1 unsupported: 0x" + verCmdByte.ToString("X")); + throw new HAProxyProtocolException("version 1 unsupported: 0x" + verCmdByte.ToString("x")); } HAProxyCommand cmd; @@ -413,21 +414,21 @@ private static string IpBytesToString(IByteBuffer header, int addressLen) } else { - sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(header.ReadUnsignedShort().ToString("x")); sb.Append(':'); - sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(header.ReadUnsignedShort().ToString("x")); sb.Append(':'); - sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(header.ReadUnsignedShort().ToString("x")); sb.Append(':'); - sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(header.ReadUnsignedShort().ToString("x")); sb.Append(':'); - sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(header.ReadUnsignedShort().ToString("x")); sb.Append(':'); - sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(header.ReadUnsignedShort().ToString("x")); sb.Append(':'); - sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(header.ReadUnsignedShort().ToString("x")); sb.Append(':'); - sb.Append(header.ReadUnsignedShort().ToString("X")); + sb.Append(header.ReadUnsignedShort().ToString("x")); } return sb.ToString(); } diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyMessageDecoder.cs b/src/DotNetty.Codecs.HaProxy/HAProxyMessageDecoder.cs index a4b3e77b8..b80f92532 100644 --- a/src/DotNetty.Codecs.HaProxy/HAProxyMessageDecoder.cs +++ b/src/DotNetty.Codecs.HaProxy/HAProxyMessageDecoder.cs @@ -392,7 +392,7 @@ private void FailOverLimit(IChannelHandlerContext ctx, string length) private void Fail(IChannelHandlerContext ctx, string errMsg, Exception e) { finished = true; - ctx.CloseAsync().RunSynchronously(); // drop connection immediately per spec + ctx.CloseAsync().Wait(); // drop connection immediately per spec HAProxyProtocolException ppex; if (errMsg != null && e != null) { diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyProxiedProtocol.cs b/src/DotNetty.Codecs.HaProxy/HAProxyProxiedProtocol.cs index 123d42b9b..cc4b27d9b 100644 --- a/src/DotNetty.Codecs.HaProxy/HAProxyProxiedProtocol.cs +++ b/src/DotNetty.Codecs.HaProxy/HAProxyProxiedProtocol.cs @@ -11,6 +11,8 @@ namespace DotNetty.Codecs.HaProxy */ public sealed class HAProxyProxiedProtocol { + static readonly Dictionary NAME_LOOKUP = new Dictionary(); + /** * The UNKNOWN represents a connection which was forwarded for an unknown protocol and an unknown address family. */ @@ -54,8 +56,6 @@ public static IEnumerable Values } } - static readonly Dictionary NAME_LOOKUP = new Dictionary(); - readonly byte byteValue; readonly AddressFamily addressFamily; readonly TransportProtocol transportProtocol; diff --git a/src/DotNetty.Codecs.HaProxy/Properties/Friends.cs b/src/DotNetty.Codecs.HaProxy/Properties/Friends.cs new file mode 100644 index 000000000..b0b76c274 --- /dev/null +++ b/src/DotNetty.Codecs.HaProxy/Properties/Friends.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DotNetty.Codecs.HaProxy.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d9782d5a0b850f230f71e06de2e101d8441d83e15eef715837eee38fdbf5cb369b41ec36e6e7668c18cbb09e5419c179360461e740c1cce6ffbdcf81f245e1e705482797fe42aff2d31ecd72ea87362ded3c14066746fbab4a8e1896f8b982323c84e2c1b08407c0de18b7feef1535fb972a3b26181f5a304ebd181795a46d8f")] + diff --git a/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj b/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj new file mode 100644 index 000000000..d13d30860 --- /dev/null +++ b/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj @@ -0,0 +1,37 @@ + + + true + netcoreapp1.1;net452 + false + ../../DotNetty.snk + true + + + win-x64 + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/DotNetty.Codecs.HaProxy.Tests/HAProxyMessageDecoderTest.cs b/test/DotNetty.Codecs.HaProxy.Tests/HAProxyMessageDecoderTest.cs new file mode 100644 index 000000000..6d3748bff --- /dev/null +++ b/test/DotNetty.Codecs.HaProxy.Tests/HAProxyMessageDecoderTest.cs @@ -0,0 +1,1051 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs.HaProxy.Tests +{ + using System.Collections.Generic; + using System.Text; + using DotNetty.Buffers; + using DotNetty.Transport.Channels; + using DotNetty.Transport.Channels.Embedded; + using Xunit; + using static DotNetty.Codecs.HaProxy.HAProxyProxiedProtocol; + + public static class DecoderTestHelper + { + public static int Count(this IEnumerator enumerator) + { + int count = enumerator.Current == null ? 0 : 1; + while (enumerator.MoveNext()) + { + count++; + } + return count; + } + } + + public class HAProxyMessageDecoderTest + { + + EmbeddedChannel ch; + + public HAProxyMessageDecoderTest() + { + ch = new EmbeddedChannel(new HAProxyMessageDecoder()); + } + + private static IByteBuffer CopiedBuffer(string value, Encoding encoding) + { + return Unpooled.CopiedBuffer(value.ToCharArray(), 0, value.Length, encoding); + } + + [Fact] + public void TestIPV4Decode() + { + int startChannels = ch.Pipeline.GetEnumerator().Count(); + string header = "PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n"; + ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII)); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + Assert.True(msgObj is HAProxyMessage); + HAProxyMessage msg = (HAProxyMessage)msgObj; + Assert.Equal(HAProxyProtocolVersion.V1, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.PROXY, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.TCP4, msg.ProxiedProtocol()); + Assert.Equal("192.168.0.1", msg.SourceAddress()); + Assert.Equal("192.168.0.11", msg.DestinationAddress()); + Assert.Equal(56324, msg.SourcePort()); + Assert.Equal(443, msg.DestinationPort()); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestIPV6Decode() + { + int startChannels = ch.Pipeline.GetEnumerator().Count(); + string header = "PROXY TCP6 2001:0db8:85a3:0000:0000:8a2e:0370:7334 1050:0:0:0:5:600:300c:326b 56324 443\r\n"; + ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII)); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + Assert.True(msgObj is HAProxyMessage); + HAProxyMessage msg = (HAProxyMessage)msgObj; + Assert.Equal(HAProxyProtocolVersion.V1, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.PROXY, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.TCP6, msg.ProxiedProtocol()); + Assert.Equal("2001:0db8:85a3:0000:0000:8a2e:0370:7334", msg.SourceAddress()); + Assert.Equal("1050:0:0:0:5:600:300c:326b", msg.DestinationAddress()); + Assert.Equal(56324, msg.SourcePort()); + Assert.Equal(443, msg.DestinationPort()); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestUnknownProtocolDecode() + { + int startChannels = ch.Pipeline.GetEnumerator().Count(); + string header = "PROXY UNKNOWN 192.168.0.1 192.168.0.11 56324 443\r\n"; + ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII)); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + Assert.True(msgObj is HAProxyMessage); + HAProxyMessage msg = (HAProxyMessage)msgObj; + Assert.Equal(HAProxyProtocolVersion.V1, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.PROXY, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.UNKNOWN, msg.ProxiedProtocol()); + Assert.Null(msg.SourceAddress()); + Assert.Null(msg.DestinationAddress()); + Assert.Equal(0, msg.SourcePort()); + Assert.Equal(0, msg.DestinationPort()); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestV1NoUDP() + { + string header = "PROXY UDP4 192.168.0.1 192.168.0.11 56324 443\r\n"; + Assert.Throws(() => ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII))); + } + + [Fact] + public void TestInvalidPort() + { + string header = "PROXY TCP4 192.168.0.1 192.168.0.11 80000 443\r\n"; + Assert.Throws(() => ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII))); + } + + [Fact] + public void TestInvalidIPV4Address() + { + string header = "PROXY TCP4 299.168.0.1 192.168.0.11 56324 443\r\n"; + Assert.Throws(() => ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII))); + } + + [Fact] + public void TestInvalidIPV6Address() + { + string header = "PROXY TCP6 r001:0db8:85a3:0000:0000:8a2e:0370:7334 1050:0:0:0:5:600:300c:326b 56324 443\r\n"; + Assert.Throws(() => ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII))); + } + + [Fact] + public void TestInvalidProtocol() + { + string header = "PROXY TCP7 192.168.0.1 192.168.0.11 56324 443\r\n"; + Assert.Throws(() => ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII))); + } + + [Fact] + public void TestMissingParams() + { + string header = "PROXY TCP4 192.168.0.1 192.168.0.11 56324\r\n"; + Assert.Throws(() => ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII))); + } + + [Fact] + public void TestTooManyParams() + { + string header = "PROXY TCP4 192.168.0.1 192.168.0.11 56324 443 123\r\n"; + Assert.Throws(() => ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII))); + } + + [Fact] + public void TestInvalidCommand() + { + string header = "PING TCP4 192.168.0.1 192.168.0.11 56324 443\r\n"; + Assert.Throws(() => ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII))); + } + + [Fact] + public void TestInvalidEOL() + { + string header = "PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\nGET / HTTP/1.1\r\n"; + Assert.Throws(() => ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII))); + } + + [Fact] + public void TestHeaderTooLong() + { + string header = "PROXY TCP4 192.168.0.1 192.168.0.11 56324 " + + "00000000000000000000000000000000000000000000000000000000000000000443\r\n"; + Assert.Throws(() => ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII))); + } + + [Fact] + public void TestIncompleteHeader() + { + string header = "PROXY TCP4 192.168.0.1 192.168.0.11 56324"; + ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII)); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestCloseOnInvalid() + { + string header = "GET / HTTP/1.1\r\n"; + try + { + ch.WriteInbound(CopiedBuffer(header, Encoding.ASCII)); + } + catch (HAProxyProtocolException ppex) + { + // swallow this exception since we're just testing to be sure the channel was closed + } + bool isComplete = ch.CloseCompletion.Wait(5000); + if (!isComplete || !ch.CloseCompletion.IsCompleted || ch.CloseCompletion.IsFaulted) + { + Assert.True(false, "Expected channel close"); + } + } + + [Fact] + public void TestTransportProtocolAndAddressFamily() + { + byte unknown = HAProxyProxiedProtocol.UNKNOWN.ByteValue(); + byte tcp4 = HAProxyProxiedProtocol.TCP4.ByteValue(); + byte tcp6 = HAProxyProxiedProtocol.TCP6.ByteValue(); + byte udp4 = HAProxyProxiedProtocol.UDP4.ByteValue(); + byte udp6 = HAProxyProxiedProtocol.UDP6.ByteValue(); + byte unix_stream = HAProxyProxiedProtocol.UNIX_STREAM.ByteValue(); + byte unix_dgram = HAProxyProxiedProtocol.UNIX_DGRAM.ByteValue(); + + Assert.Equal(TransportProtocol.UNSPEC, TransportProtocol.ValueOf(unknown)); + Assert.Equal(TransportProtocol.STREAM, TransportProtocol.ValueOf(tcp4)); + Assert.Equal(TransportProtocol.STREAM, TransportProtocol.ValueOf(tcp6)); + Assert.Equal(TransportProtocol.STREAM, TransportProtocol.ValueOf(unix_stream)); + Assert.Equal(TransportProtocol.DGRAM, TransportProtocol.ValueOf(udp4)); + Assert.Equal(TransportProtocol.DGRAM, TransportProtocol.ValueOf(udp6)); + Assert.Equal(TransportProtocol.DGRAM, TransportProtocol.ValueOf(unix_dgram)); + + Assert.Equal(AddressFamily.AF_UNSPEC, AddressFamily.ValueOf(unknown)); + Assert.Equal(AddressFamily.AF_IPv4, AddressFamily.ValueOf(tcp4)); + Assert.Equal(AddressFamily.AF_IPv4, AddressFamily.ValueOf(udp4)); + Assert.Equal(AddressFamily.AF_IPv6, AddressFamily.ValueOf(tcp6)); + Assert.Equal(AddressFamily.AF_IPv6, AddressFamily.ValueOf(udp6)); + Assert.Equal(AddressFamily.AF_UNIX, AddressFamily.ValueOf(unix_stream)); + Assert.Equal(AddressFamily.AF_UNIX, AddressFamily.ValueOf(unix_dgram)); + } + + [Fact] + public void TestV2IPV4Decode() + { + byte[] header = new byte[28]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x21; // v2, cmd=PROXY + header[13] = 0x11; // TCP over IPv4 + + header[14] = 0x00; // Remaining Bytes + header[15] = 0x0c; // ----- + + header[16] = (byte)0xc0; // Source Address + header[17] = (byte)0xa8; // ----- + header[18] = 0x00; // ----- + header[19] = 0x01; // ----- + + header[20] = (byte)0xc0; // Destination Address + header[21] = (byte)0xa8; // ----- + header[22] = 0x00; // ----- + header[23] = 0x0b; // ----- + + header[24] = (byte)0xdc; // Source Port + header[25] = 0x04; // ----- + + header[26] = 0x01; // Destination Port + header[27] = (byte)0xbb; // ----- + + int startChannels = ch.Pipeline.GetEnumerator().Count(); + ch.WriteInbound(Unpooled.CopiedBuffer(header)); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + Assert.True(msgObj is HAProxyMessage); + HAProxyMessage msg = (HAProxyMessage)msgObj; + Assert.Equal(HAProxyProtocolVersion.V2, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.PROXY, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.TCP4, msg.ProxiedProtocol()); + Assert.Equal("192.168.0.1", msg.SourceAddress()); + Assert.Equal("192.168.0.11", msg.DestinationAddress()); + Assert.Equal(56324, msg.SourcePort()); + Assert.Equal(443, msg.DestinationPort()); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestV2UDPDecode() + { + byte[] header = new byte[28]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x21; // v2, cmd=PROXY + header[13] = 0x12; // UDP over IPv4 + + header[14] = 0x00; // Remaining Bytes + header[15] = 0x0c; // ----- + + header[16] = (byte)0xc0; // Source Address + header[17] = (byte)0xa8; // ----- + header[18] = 0x00; // ----- + header[19] = 0x01; // ----- + + header[20] = (byte)0xc0; // Destination Address + header[21] = (byte)0xa8; // ----- + header[22] = 0x00; // ----- + header[23] = 0x0b; // ----- + + header[24] = (byte)0xdc; // Source Port + header[25] = 0x04; // ----- + + header[26] = 0x01; // Destination Port + header[27] = (byte)0xbb; // ----- + + int startChannels = ch.Pipeline.GetEnumerator().Count(); + ch.WriteInbound(Unpooled.CopiedBuffer(header)); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + Assert.True(msgObj is HAProxyMessage); + HAProxyMessage msg = (HAProxyMessage)msgObj; + Assert.Equal(HAProxyProtocolVersion.V2, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.PROXY, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.UDP4, msg.ProxiedProtocol()); + Assert.Equal("192.168.0.1", msg.SourceAddress()); + Assert.Equal("192.168.0.11", msg.DestinationAddress()); + Assert.Equal(56324, msg.SourcePort()); + Assert.Equal(443, msg.DestinationPort()); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void Testv2IPV6Decode() + { + byte[] header = new byte[52]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x21; // v2, cmd=PROXY + header[13] = 0x21; // TCP over IPv6 + + header[14] = 0x00; // Remaining Bytes + header[15] = 0x24; // ----- + + header[16] = 0x20; // Source Address + header[17] = 0x01; // ----- + header[18] = 0x0d; // ----- + header[19] = (byte)0xb8; // ----- + header[20] = (byte)0x85; // ----- + header[21] = (byte)0xa3; // ----- + header[22] = 0x00; // ----- + header[23] = 0x00; // ----- + header[24] = 0x00; // ----- + header[25] = 0x00; // ----- + header[26] = (byte)0x8a; // ----- + header[27] = 0x2e; // ----- + header[28] = 0x03; // ----- + header[29] = 0x70; // ----- + header[30] = 0x73; // ----- + header[31] = 0x34; // ----- + + header[32] = 0x10; // Destination Address + header[33] = 0x50; // ----- + header[34] = 0x00; // ----- + header[35] = 0x00; // ----- + header[36] = 0x00; // ----- + header[37] = 0x00; // ----- + header[38] = 0x00; // ----- + header[39] = 0x00; // ----- + header[40] = 0x00; // ----- + header[41] = 0x05; // ----- + header[42] = 0x06; // ----- + header[43] = 0x00; // ----- + header[44] = 0x30; // ----- + header[45] = 0x0c; // ----- + header[46] = 0x32; // ----- + header[47] = 0x6b; // ----- + + header[48] = (byte)0xdc; // Source Port + header[49] = 0x04; // ----- + + header[50] = 0x01; // Destination Port + header[51] = (byte)0xbb; // ----- + + int startChannels = ch.Pipeline.GetEnumerator().Count(); + ch.WriteInbound(Unpooled.CopiedBuffer(header)); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + Assert.True(msgObj is HAProxyMessage); + HAProxyMessage msg = (HAProxyMessage)msgObj; + Assert.Equal(HAProxyProtocolVersion.V2, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.PROXY, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.TCP6, msg.ProxiedProtocol()); + Assert.Equal("2001:db8:85a3:0:0:8a2e:370:7334", msg.SourceAddress()); + Assert.Equal("1050:0:0:0:5:600:300c:326b", msg.DestinationAddress()); + Assert.Equal(56324, msg.SourcePort()); + Assert.Equal(443, msg.DestinationPort()); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void Testv2UnixDecode() + { + byte[] header = new byte[232]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x21; // v2, cmd=PROXY + header[13] = 0x31; // UNIX_STREAM + + header[14] = 0x00; // Remaining Bytes + header[15] = (byte)0xd8; // ----- + + header[16] = 0x2f; // Source Address + header[17] = 0x76; // ----- + header[18] = 0x61; // ----- + header[19] = 0x72; // ----- + header[20] = 0x2f; // ----- + header[21] = 0x72; // ----- + header[22] = 0x75; // ----- + header[23] = 0x6e; // ----- + header[24] = 0x2f; // ----- + header[25] = 0x73; // ----- + header[26] = 0x72; // ----- + header[27] = 0x63; // ----- + header[28] = 0x2e; // ----- + header[29] = 0x73; // ----- + header[30] = 0x6f; // ----- + header[31] = 0x63; // ----- + header[32] = 0x6b; // ----- + header[33] = 0x00; // ----- + + header[124] = 0x2f; // Destination Address + header[125] = 0x76; // ----- + header[126] = 0x61; // ----- + header[127] = 0x72; // ----- + header[128] = 0x2f; // ----- + header[129] = 0x72; // ----- + header[130] = 0x75; // ----- + header[131] = 0x6e; // ----- + header[132] = 0x2f; // ----- + header[133] = 0x64; // ----- + header[134] = 0x65; // ----- + header[135] = 0x73; // ----- + header[136] = 0x74; // ----- + header[137] = 0x2e; // ----- + header[138] = 0x73; // ----- + header[139] = 0x6f; // ----- + header[140] = 0x63; // ----- + header[141] = 0x6b; // ----- + header[142] = 0x00; // ----- + + int startChannels = ch.Pipeline.GetEnumerator().Count(); + ch.WriteInbound(Unpooled.CopiedBuffer(header)); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + Assert.True(msgObj is HAProxyMessage); + HAProxyMessage msg = (HAProxyMessage)msgObj; + Assert.Equal(HAProxyProtocolVersion.V2, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.PROXY, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.UNIX_STREAM, msg.ProxiedProtocol()); + Assert.Equal("/var/run/src.sock", msg.SourceAddress()); + Assert.Equal("/var/run/dest.sock", msg.DestinationAddress()); + Assert.Equal(0, msg.SourcePort()); + Assert.Equal(0, msg.DestinationPort()); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestV2LocalProtocolDecode() + { + byte[] header = new byte[28]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x20; // v2, cmd=LOCAL + header[13] = 0x00; // Unspecified transport protocol and address family + + header[14] = 0x00; // Remaining Bytes + header[15] = 0x0c; // ----- + + header[16] = (byte)0xc0; // Source Address + header[17] = (byte)0xa8; // ----- + header[18] = 0x00; // ----- + header[19] = 0x01; // ----- + + header[20] = (byte)0xc0; // Destination Address + header[21] = (byte)0xa8; // ----- + header[22] = 0x00; // ----- + header[23] = 0x0b; // ----- + + header[24] = (byte)0xdc; // Source Port + header[25] = 0x04; // ----- + + header[26] = 0x01; // Destination Port + header[27] = (byte)0xbb; // ----- + + int startChannels = ch.Pipeline.GetEnumerator().Count(); + ch.WriteInbound(Unpooled.CopiedBuffer(header)); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + Assert.True(msgObj is HAProxyMessage); + HAProxyMessage msg = (HAProxyMessage)msgObj; + Assert.Equal(HAProxyProtocolVersion.V2, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.LOCAL, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.UNKNOWN, msg.ProxiedProtocol()); + Assert.Null(msg.SourceAddress()); + Assert.Null(msg.DestinationAddress()); + Assert.Equal(0, msg.SourcePort()); + Assert.Equal(0, msg.DestinationPort()); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestV2UnknownProtocolDecode() + { + byte[] header = new byte[28]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x21; // v2, cmd=PROXY + header[13] = 0x00; // Unspecified transport protocol and address family + + header[14] = 0x00; // Remaining Bytes + header[15] = 0x0c; // ----- + + header[16] = (byte)0xc0; // Source Address + header[17] = (byte)0xa8; // ----- + header[18] = 0x00; // ----- + header[19] = 0x01; // ----- + + header[20] = (byte)0xc0; // Destination Address + header[21] = (byte)0xa8; // ----- + header[22] = 0x00; // ----- + header[23] = 0x0b; // ----- + + header[24] = (byte)0xdc; // Source Port + header[25] = 0x04; // ----- + + header[26] = 0x01; // Destination Port + header[27] = (byte)0xbb; // ----- + + int startChannels = ch.Pipeline.GetEnumerator().Count(); + ch.WriteInbound(Unpooled.CopiedBuffer(header)); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + Assert.True(msgObj is HAProxyMessage); + HAProxyMessage msg = (HAProxyMessage)msgObj; + Assert.Equal(HAProxyProtocolVersion.V2, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.PROXY, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.UNKNOWN, msg.ProxiedProtocol()); + Assert.Null(msg.SourceAddress()); + Assert.Null(msg.DestinationAddress()); + Assert.Equal(0, msg.SourcePort()); + Assert.Equal(0, msg.DestinationPort()); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestV2WithSslTLVs() + { + ch = new EmbeddedChannel(new HAProxyMessageDecoder()); + + byte[] bytes = { + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 17, 0, 35, 127, 0, 0, 1, 127, 0, 0, 1, + 201, 166, 7, 89, 32, 0, 20, 5, 0, 0, 0, 0, 33, 0, 5, 84, 76, 83, 118, 49, 34, 0, 4, 76, 69, 65, 70 + };//-55, -90 + + int startChannels = ch.Pipeline.GetEnumerator().Count(); + Assert.True(ch.WriteInbound(Unpooled.CopiedBuffer(bytes))); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + HAProxyMessage msg = (HAProxyMessage)msgObj; + + Assert.Equal(HAProxyProtocolVersion.V2, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.PROXY, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.TCP4, msg.ProxiedProtocol()); + Assert.Equal("127.0.0.1", msg.SourceAddress()); + Assert.Equal("127.0.0.1", msg.DestinationAddress()); + Assert.Equal(51622, msg.SourcePort()); + Assert.Equal(1881, msg.DestinationPort()); + IList tlvs = msg.Tlvs(); + + Assert.Equal(3, tlvs.Count); + HAProxyTLV firstTlv = tlvs[0]; + Assert.Equal(HAProxyTLV.Type.PP2_TYPE_SSL, firstTlv.TLVType()); + HAProxySSLTLV sslTlv = (HAProxySSLTLV) firstTlv; + Assert.Equal(0, sslTlv.Verify()); + Assert.True(sslTlv.IsPP2ClientSSL()); + Assert.True(sslTlv.IsPP2ClientCertSess()); + Assert.False(sslTlv.IsPP2ClientCertConn()); + + HAProxyTLV secondTlv = tlvs[1]; + + Assert.Equal(HAProxyTLV.Type.PP2_TYPE_SSL_VERSION, secondTlv.TLVType()); + IByteBuffer secondContentBuf = secondTlv.Content; + byte[] secondContent = new byte[secondContentBuf.ReadableBytes]; + secondContentBuf.ReadBytes(secondContent); + Assert.Equal(Encoding.ASCII.GetBytes("TLSv1"), secondContent); + + HAProxyTLV thirdTLV = tlvs[2]; + Assert.Equal(HAProxyTLV.Type.PP2_TYPE_SSL_CN, thirdTLV.TLVType()); + IByteBuffer thirdContentBuf = thirdTLV.Content; + byte[] thirdContent = new byte[thirdContentBuf.ReadableBytes]; + thirdContentBuf.ReadBytes(thirdContent); + Assert.Equal(Encoding.ASCII.GetBytes("LEAF"), thirdContent); + + Assert.True(sslTlv.EncapsulatedTLVs().Contains(secondTlv)); + Assert.True(sslTlv.EncapsulatedTLVs().Contains(thirdTLV)); + + Assert.True(0 < firstTlv.ReferenceCount); + Assert.True(0 < secondTlv.ReferenceCount); + Assert.True(0 < thirdTLV.ReferenceCount); + Assert.False(thirdTLV.Release()); + Assert.False(secondTlv.Release()); + Assert.True(firstTlv.Release()); + Assert.Equal(0, firstTlv.ReferenceCount); + Assert.Equal(0, secondTlv.ReferenceCount); + Assert.Equal(0, thirdTLV.ReferenceCount); + + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestV2WithTLV() + { + ch = new EmbeddedChannel(new HAProxyMessageDecoder(4)); + + byte[] header = new byte[236]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x21; // v2, cmd=PROXY + header[13] = 0x31; // UNIX_STREAM + + header[14] = 0x00; // Remaining Bytes + header[15] = (byte)0xdc; // ----- + + header[16] = 0x2f; // Source Address + header[17] = 0x76; // ----- + header[18] = 0x61; // ----- + header[19] = 0x72; // ----- + header[20] = 0x2f; // ----- + header[21] = 0x72; // ----- + header[22] = 0x75; // ----- + header[23] = 0x6e; // ----- + header[24] = 0x2f; // ----- + header[25] = 0x73; // ----- + header[26] = 0x72; // ----- + header[27] = 0x63; // ----- + header[28] = 0x2e; // ----- + header[29] = 0x73; // ----- + header[30] = 0x6f; // ----- + header[31] = 0x63; // ----- + header[32] = 0x6b; // ----- + header[33] = 0x00; // ----- + + header[124] = 0x2f; // Destination Address + header[125] = 0x76; // ----- + header[126] = 0x61; // ----- + header[127] = 0x72; // ----- + header[128] = 0x2f; // ----- + header[129] = 0x72; // ----- + header[130] = 0x75; // ----- + header[131] = 0x6e; // ----- + header[132] = 0x2f; // ----- + header[133] = 0x64; // ----- + header[134] = 0x65; // ----- + header[135] = 0x73; // ----- + header[136] = 0x74; // ----- + header[137] = 0x2e; // ----- + header[138] = 0x73; // ----- + header[139] = 0x6f; // ----- + header[140] = 0x63; // ----- + header[141] = 0x6b; // ----- + header[142] = 0x00; // ----- + + // ---- Additional data (TLV) ---- \\ + + header[232] = 0x01; // Type + header[233] = 0x00; // Remaining bytes + header[234] = 0x01; // ----- + header[235] = 0x01; // Payload + + int startChannels = ch.Pipeline.GetEnumerator().Count(); + ch.WriteInbound(Unpooled.CopiedBuffer(header)); + object msgObj = ch.ReadInbound(); + Assert.Equal(startChannels - 1, ch.Pipeline.GetEnumerator().Count()); + Assert.True(msgObj is HAProxyMessage); + HAProxyMessage msg = (HAProxyMessage)msgObj; + Assert.Equal(HAProxyProtocolVersion.V2, msg.ProtocolVersion()); + Assert.Equal(HAProxyCommand.PROXY, msg.Command()); + Assert.Equal(HAProxyProxiedProtocol.UNIX_STREAM, msg.ProxiedProtocol()); + Assert.Equal("/var/run/src.sock", msg.SourceAddress()); + Assert.Equal("/var/run/dest.sock", msg.DestinationAddress()); + Assert.Equal(0, msg.SourcePort()); + Assert.Equal(0, msg.DestinationPort()); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestV2InvalidProtocol() + { + byte[] header = new byte[28]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x21; // v2, cmd=PROXY + header[13] = 0x41; // Bogus transport protocol + + header[14] = 0x00; // Remaining Bytes + header[15] = 0x0c; // ----- + + header[16] = (byte)0xc0; // Source Address + header[17] = (byte)0xa8; // ----- + header[18] = 0x00; // ----- + header[19] = 0x01; // ----- + + header[20] = (byte)0xc0; // Destination Address + header[21] = (byte)0xa8; // ----- + header[22] = 0x00; // ----- + header[23] = 0x0b; // ----- + + header[24] = (byte)0xdc; // Source Port + header[25] = 0x04; // ----- + + header[26] = 0x01; // Destination Port + header[27] = (byte)0xbb; // ----- + + Assert.Throws(() => ch.WriteInbound(Unpooled.CopiedBuffer(header))); + } + + [Fact] + public void TestV2MissingParams() + { + byte[] header = new byte[26]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x21; // v2, cmd=PROXY + header[13] = 0x11; // TCP over IPv4 + + header[14] = 0x00; // Remaining Bytes + header[15] = 0x0a; // ----- + + header[16] = (byte)0xc0; // Source Address + header[17] = (byte)0xa8; // ----- + header[18] = 0x00; // ----- + header[19] = 0x01; // ----- + + header[20] = (byte)0xc0; // Destination Address + header[21] = (byte)0xa8; // ----- + header[22] = 0x00; // ----- + header[23] = 0x0b; // ----- + + header[24] = (byte)0xdc; // Source Port + header[25] = 0x04; // ----- + + Assert.Throws(() => ch.WriteInbound(Unpooled.CopiedBuffer(header))); + } + + [Fact] + public void TestV2InvalidCommand() + { + byte[] header = new byte[28]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x22; // v2, Bogus command + header[13] = 0x11; // TCP over IPv4 + + header[14] = 0x00; // Remaining Bytes + header[15] = 0x0c; // ----- + + header[16] = (byte)0xc0; // Source Address + header[17] = (byte)0xa8; // ----- + header[18] = 0x00; // ----- + header[19] = 0x01; // ----- + + header[20] = (byte)0xc0; // Destination Address + header[21] = (byte)0xa8; // ----- + header[22] = 0x00; // ----- + header[23] = 0x0b; // ----- + + header[24] = (byte)0xdc; // Source Port + header[25] = 0x04; // ----- + + header[26] = 0x01; // Destination Port + header[27] = (byte)0xbb; // ----- + + Assert.Throws(() => ch.WriteInbound(Unpooled.CopiedBuffer(header))); + } + + [Fact] + public void TestV2InvalidVersion() + { + byte[] header = new byte[28]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x31; // Bogus version, cmd=PROXY + header[13] = 0x11; // TCP over IPv4 + + header[14] = 0x00; // Remaining Bytes + header[15] = 0x0c; // ----- + + header[16] = (byte)0xc0; // Source Address + header[17] = (byte)0xa8; // ----- + header[18] = 0x00; // ----- + header[19] = 0x01; // ----- + + header[20] = (byte)0xc0; // Destination Address + header[21] = (byte)0xa8; // ----- + header[22] = 0x00; // ----- + header[23] = 0x0b; // ----- + + header[24] = (byte)0xdc; // Source Port + header[25] = 0x04; // ----- + + header[26] = 0x01; // Destination Port + header[27] = (byte)0xbb; // ----- + + Assert.Throws(() => ch.WriteInbound(Unpooled.CopiedBuffer(header))); + } + + [Fact] + public void TestV2HeaderTooLong() + { + ch = new EmbeddedChannel(new HAProxyMessageDecoder(0)); + + byte[] header = new byte[248]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x21; // v2, cmd=PROXY + header[13] = 0x11; // TCP over IPv4 + + header[14] = 0x00; // Remaining Bytes + header[15] = (byte)0xe8; // ----- + + header[16] = (byte)0xc0; // Source Address + header[17] = (byte)0xa8; // ----- + header[18] = 0x00; // ----- + header[19] = 0x01; // ----- + + header[20] = (byte)0xc0; // Destination Address + header[21] = (byte)0xa8; // ----- + header[22] = 0x00; // ----- + header[23] = 0x0b; // ----- + + header[24] = (byte)0xdc; // Source Port + header[25] = 0x04; // ----- + + header[26] = 0x01; // Destination Port + header[27] = (byte)0xbb; // ----- + + Assert.Throws(() => ch.WriteInbound(Unpooled.CopiedBuffer(header))); + } + + [Fact] + public void TestV2IncompleteHeader() + { + byte[] header = new byte[13]; + header[0] = 0x0D; // Binary Prefix + header[1] = 0x0A; // ----- + header[2] = 0x0D; // ----- + header[3] = 0x0A; // ----- + header[4] = 0x00; // ----- + header[5] = 0x0D; // ----- + header[6] = 0x0A; // ----- + header[7] = 0x51; // ----- + header[8] = 0x55; // ----- + header[9] = 0x49; // ----- + header[10] = 0x54; // ----- + header[11] = 0x0A; // ----- + + header[12] = 0x21; // v2, cmd=PROXY + + ch.WriteInbound(Unpooled.CopiedBuffer(header)); + Assert.Null(ch.ReadInbound()); + Assert.False(ch.Finish()); + } + + [Fact] + public void TestDetectProtocol() + { + IByteBuffer validHeaderV1 = CopiedBuffer("PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n", + Encoding.ASCII); + ProtocolDetectionResult result = HAProxyMessageDecoder.DetectProtocol(validHeaderV1); + Assert.Equal(ProtocolDetectionState.DETECTED, result.State()); + Assert.Equal(HAProxyProtocolVersion.V1, result.DetectedProtocol()); + validHeaderV1.Release(); + + IByteBuffer invalidHeader = CopiedBuffer("Invalid header", Encoding.ASCII); + result = HAProxyMessageDecoder.DetectProtocol(invalidHeader); + Assert.Equal(ProtocolDetectionState.INVALID, result.State()); + Assert.Null(result.DetectedProtocol()); + invalidHeader.Release(); + + IByteBuffer validHeaderV2 = Unpooled.Buffer(); + validHeaderV2.WriteByte(0x0D); + validHeaderV2.WriteByte(0x0A); + validHeaderV2.WriteByte(0x0D); + validHeaderV2.WriteByte(0x0A); + validHeaderV2.WriteByte(0x00); + validHeaderV2.WriteByte(0x0D); + validHeaderV2.WriteByte(0x0A); + validHeaderV2.WriteByte(0x51); + validHeaderV2.WriteByte(0x55); + validHeaderV2.WriteByte(0x49); + validHeaderV2.WriteByte(0x54); + validHeaderV2.WriteByte(0x0A); + result = HAProxyMessageDecoder.DetectProtocol(validHeaderV2); + Assert.Equal(ProtocolDetectionState.DETECTED, result.State()); + Assert.Equal(HAProxyProtocolVersion.V2, result.DetectedProtocol()); + validHeaderV2.Release(); + + IByteBuffer incompleteHeader = Unpooled.Buffer(); + incompleteHeader.WriteByte(0x0D); + incompleteHeader.WriteByte(0x0A); + incompleteHeader.WriteByte(0x0D); + incompleteHeader.WriteByte(0x0A); + incompleteHeader.WriteByte(0x00); + incompleteHeader.WriteByte(0x0D); + incompleteHeader.WriteByte(0x0A); + result = HAProxyMessageDecoder.DetectProtocol(incompleteHeader); + Assert.Equal(ProtocolDetectionState.NEEDS_MORE_DATA, result.State()); + Assert.Null(result.DetectedProtocol()); + incompleteHeader.Release(); + } + + } +} diff --git a/test/DotNetty.Codecs.HaProxy.Tests/HAProxySSLTLVTest.cs b/test/DotNetty.Codecs.HaProxy.Tests/HAProxySSLTLVTest.cs new file mode 100644 index 000000000..fdfeccf46 --- /dev/null +++ b/test/DotNetty.Codecs.HaProxy.Tests/HAProxySSLTLVTest.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DotNetty.Codecs.HaProxy.Tests +{ + using System.Collections.Generic; + using DotNetty.Buffers; + using Xunit; + + public class HAProxySSLTLVTest + { + + [Fact] + public void TestClientBitmask() + { + // 0b0000_0111 + byte allClientsEnabled = 0x7; + HAProxySSLTLV allClientsEnabledTLV = + new HAProxySSLTLV(0, allClientsEnabled, new List(), Unpooled.Buffer()); + + Assert.True(allClientsEnabledTLV.IsPP2ClientCertConn()); + Assert.True(allClientsEnabledTLV.IsPP2ClientSSL()); + Assert.True(allClientsEnabledTLV.IsPP2ClientCertSess()); + + Assert.True(allClientsEnabledTLV.Release()); + + // 0b0000_0101 + byte clientSSLandClientCertSessEnabled = 0x5; + + HAProxySSLTLV clientSSLandClientCertSessTLV = + new HAProxySSLTLV(0, clientSSLandClientCertSessEnabled, new List(), Unpooled.Buffer()); + + Assert.False(clientSSLandClientCertSessTLV.IsPP2ClientCertConn()); + Assert.True(clientSSLandClientCertSessTLV.IsPP2ClientSSL()); + Assert.True(clientSSLandClientCertSessTLV.IsPP2ClientCertSess()); + + Assert.True(clientSSLandClientCertSessTLV.Release()); + // 0b0000_0000 + byte noClientEnabled = 0x0; + + HAProxySSLTLV noClientTlv = + new HAProxySSLTLV(0, noClientEnabled, new List(), Unpooled.Buffer()); + + Assert.False(noClientTlv.IsPP2ClientCertConn()); + Assert.False(noClientTlv.IsPP2ClientSSL()); + Assert.False(noClientTlv.IsPP2ClientCertSess()); + + Assert.True(noClientTlv.Release()); + } + } +} diff --git a/test/DotNetty.Codecs.HaProxy.Tests/Properties/AssemblyInfo.cs b/test/DotNetty.Codecs.HaProxy.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..83795376c --- /dev/null +++ b/test/DotNetty.Codecs.HaProxy.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DotNetty.Codecs.HaProxy.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("57ab7999-1705-4f12-a05d-89fec4eaa305")] From bf894f78addf579b062b9fb6360d7e5de9f3770c Mon Sep 17 00:00:00 2001 From: Jon Steinich Date: Thu, 28 Feb 2019 08:59:58 -0600 Subject: [PATCH 3/4] Pull in some upstream changes --- src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs | 1 - .../DotNetty.Codecs.HaProxy.Tests.csproj | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs b/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs index c7393c68f..3c92c7c7f 100644 --- a/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs +++ b/src/DotNetty.Codecs.HaProxy/HAProxyMessage.cs @@ -8,7 +8,6 @@ namespace DotNetty.Codecs.HaProxy using System.Text; using DotNetty.Buffers; using DotNetty.Common.Utilities; - using DotNetty.Util; using static DotNetty.Codecs.HaProxy.HAProxyProxiedProtocol; diff --git a/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj b/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj index d13d30860..23d889ff9 100644 --- a/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj +++ b/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj @@ -14,14 +14,14 @@ - - - + + + - - + + From 9ae08469d829fad36a73185ae6b5ac25fb708c2b Mon Sep 17 00:00:00 2001 From: Jon Steinich Date: Thu, 28 Feb 2019 09:11:06 -0600 Subject: [PATCH 4/4] pull in more upstream fixes --- src/DotNetty.Common/Utilities/NetUtil.cs | 5 +++++ .../DotNetty.Codecs.HaProxy.Tests.csproj | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/DotNetty.Common/Utilities/NetUtil.cs b/src/DotNetty.Common/Utilities/NetUtil.cs index b3995909d..37cb04173 100644 --- a/src/DotNetty.Common/Utilities/NetUtil.cs +++ b/src/DotNetty.Common/Utilities/NetUtil.cs @@ -198,6 +198,11 @@ public static bool IsValidIpV6Address(string ip) // 8 colons is valid only if compression in start or end wordLen > 0 && (colons < 8 || compressBegin <= start); } + + public static bool IsValidIpV4Address(string ip) + { + return IsValidIpV4Address(ip, 0, ip.Length); + } static bool IsValidIpV4Address(string ip, int from, int toExcluded) { diff --git a/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj b/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj index 23d889ff9..1f83ad2ea 100644 --- a/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj +++ b/test/DotNetty.Codecs.HaProxy.Tests/DotNetty.Codecs.HaProxy.Tests.csproj @@ -1,7 +1,7 @@  true - netcoreapp1.1;net452 + netcoreapp2.0;net452 false ../../DotNetty.snk true @@ -12,7 +12,7 @@ - +