File: Macros\GeneratePortNumberConfig.cs
Web Access
Project: src\sdk\src\TemplateEngine\Microsoft.TemplateEngine.Orchestrator.RunnableProjects\Microsoft.TemplateEngine.Orchestrator.RunnableProjects.csproj (Microsoft.TemplateEngine.Orchestrator.RunnableProjects)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Abstractions;

namespace Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros
{
    internal class GeneratePortNumberConfig : BaseMacroConfig<GeneratePortNumberMacro, GeneratePortNumberConfig>
    {
        internal const int LowPortDefault = 1024;
        internal const int HighPortDefault = 65535;

        // sources of unsafe ports:
        //   * chrome:  https://chromium.googlesource.com/chromium/src.git/+/refs/heads/master/net/base/port_util.cc#27
        //   * firefox: https://www-archive.mozilla.org/projects/netlib/portbanning#portlist
        //   * safari:  https://github.com/WebKit/WebKit/blob/42f5a93823a7f087a800cd65c6bc0551dbeb55d3/Source/WTF/wtf/URL.cpp#L969
        private static readonly HashSet<int> UnsafePorts = new HashSet<int>()
        {
            1719, // H323 (RAS)
            1720, // H323 (Q931)
            1723, // H323 (H245)
            2049, // NFS
            3659, // apple-sasl / PasswordServer [Apple addition]
            4045, // lockd
            4190, // ManageSieve [Apple addition]
            5060, // SIP
            5061, // SIPS
            6000, // X11
            6566, // SANE
            6665, // Alternate IRC [Apple addition]
            6666, // Alternate IRC [Apple addition]
            6667, // Standard IRC [Apple addition]
            6668, // Alternate IRC [Apple addition]
            6669, // Alternate IRC [Apple addition]
            6679, // Alternate IRC SSL [Apple addition]
            6697, // IRC+SSL [Apple addition]
            10080, // amanda
        };

        internal GeneratePortNumberConfig(GeneratePortNumberMacro macro, string variableName, string? dataType, int fallback, int low, int high)
             : base(macro, variableName, dataType)
        {
            if (low < LowPortDefault)
            {
                throw new ArgumentException($"{nameof(low)} should be greater than {LowPortDefault}.", nameof(low));
            }

            if (high > HighPortDefault)
            {
                throw new ArgumentException($"{nameof(high)} should be less than {HighPortDefault}.", nameof(high));
            }

            if (low > high)
            {
                throw new ArgumentException($"{nameof(low)} should be greater than {nameof(high)}.", nameof(low));
            }

            Fallback = fallback;
            Low = low;
            High = high;
            Port = AllocatePort(low, high, fallback);
        }

        internal GeneratePortNumberConfig(ILogger logger, GeneratePortNumberMacro macro, IGeneratedSymbolConfig generatedSymbolConfig)
            : base(macro, generatedSymbolConfig.VariableName, generatedSymbolConfig.DataType)
        {
            int low = GetOptionalParameterValue(generatedSymbolConfig, "low", ConvertJTokenToInt, LowPortDefault);
            int high = GetOptionalParameterValue(generatedSymbolConfig, "high", ConvertJTokenToInt, HighPortDefault);
            if (low < LowPortDefault)
            {
                logger.LogWarning(LocalizableStrings.GeneratePortNumberConfig_Warning_InvalidLowBound, low, LowPortDefault);
                low = LowPortDefault;
            }

            if (high > HighPortDefault)
            {
                logger.LogWarning(LocalizableStrings.GeneratePortNumberConfig_Warning_InvalidHighBound, high, HighPortDefault);
                high = HighPortDefault;
            }

            if (low > high)
            {
                logger.LogWarning(LocalizableStrings.GeneratePortNumberConfig_Warning_InvalidLowHighBound, low, high, LowPortDefault, HighPortDefault);
                low = LowPortDefault;
                high = HighPortDefault;
            }

            int fallback = GetOptionalParameterValue(generatedSymbolConfig, "fallback", ConvertJTokenToInt, 0);

            Fallback = fallback;
            Low = low;
            High = high;
            Port = AllocatePort(low, high, fallback);
        }

        internal int Port { get; }

        internal int Low { get; }

        internal int High { get; }

        internal int Fallback { get; }

        private static ConcurrentDictionary<int, int> UnavailablePorts { get; } = new(UnsafePorts.ToDictionary(p => p));

        private static int AllocatePort(int low, int high, int fallback = 0)
        {
            int startPort = CryptoRandom.NextInt(low, high);

            for (int testPort = startPort; testPort <= high; testPort++)
            {
                if (TryAllocatePort(testPort))
                {
                    return testPort;
                }
            }

            for (int testPort = low; testPort < startPort; testPort++)
            {
                if (TryAllocatePort(testPort))
                {
                    return testPort;
                }
            }
            return fallback;
        }

        private static bool TryAllocatePort(int testPort)
        {
            Socket? testSocket = null;
            if (!UnavailablePorts.TryAdd(testPort, testPort))
            {
                return false;
            }
            try
            {
                if (Socket.OSSupportsIPv4)
                {
                    testSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                }
                else if (Socket.OSSupportsIPv6)
                {
                    testSocket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
                }

                if (testSocket is null)
                {
                    return false;
                }
                IPEndPoint endPoint = new(testSocket.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any, testPort);
                testSocket.Bind(endPoint);
                return true;
            }
            catch
            {
                return false;
            }
            finally
            {
                testSocket?.Dispose();
            }
        }
    }
}