Ctrl K
ComunidadGuías…

src/ServerSettingsSanitizer.cs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Script.Serialization;

namespace Clipman
{
    internal static class ServerSettingsSanitizer
    {
        private static readonly Regex UrlRegex = new Regex(@"(?:https?|clipman)://[^\s""'<>]+", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private static readonly Regex JsonAuthTokenRegex = new Regex(@"""(?:AuthToken|token)""\s*:\s*""([^""]+)""", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private static readonly Regex JsonPortRegex = new Regex(@"""Port""\s*:\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private static readonly Regex JsonHostRegex = new Regex(@"""Host""\s*:\s*""([^""]+)""", RegexOptions.IgnoreCase | RegexOptions.Compiled);

        public static string CleanUrl(string value)
        {
            var text = (value ?? string.Empty).Trim();
            if (text.Length == 0) return string.Empty;

            var directUrl = UrlRegex.Match(text);
            if (directUrl.Success)
            {
                return NormalizeDisplayUrl(TrimUrlPunctuation(directUrl.Value));
            }

            var host = MatchGroup(JsonHostRegex, text);
            var port = MatchGroup(JsonPortRegex, text);
            if (host.Length > 0 && port.Length > 0)
            {
                if (string.Equals(host, "0.0.0.0", StringComparison.OrdinalIgnoreCase))
                {
                    host = "localhost";
                }
                return "clipman://" + host + ":" + port;
            }

            var cleaned = TrimUrlPunctuation(text);
            if (cleaned.Length > 0 &&
                cleaned.IndexOf("://", StringComparison.Ordinal) < 0 &&
                cleaned.IndexOfAny(new[] { ' ', '\t', '\r', '\n' }) < 0)
            {
                cleaned = "clipman://" + cleaned;
            }
            return NormalizeDisplayUrl(cleaned);
        }

        public static string CleanTransportUrl(string value)
        {
            var cleaned = CleanUrl(value);
            if (cleaned.StartsWith("clipman://", StringComparison.OrdinalIgnoreCase))
            {
                return "http://" + cleaned.Substring("clipman://".Length);
            }
            return cleaned;
        }

        public static string CleanToken(string value)
        {
            var text = (value ?? string.Empty).Trim();
            if (text.Length == 0) return string.Empty;

            var jsonToken = MatchGroup(JsonAuthTokenRegex, text);
            if (jsonToken.Length > 0)
            {
                return jsonToken.Trim();
            }

            try
            {
                var serializer = new JavaScriptSerializer();
                var parsed = serializer.Deserialize<ServerSettingsTokenProbe>(text);
                if (parsed != null && !string.IsNullOrWhiteSpace(parsed.AuthToken))
                {
                    return parsed.AuthToken.Trim();
                }
            }
            catch
            {
            }

            return text.Trim().Trim('"', '\'', ',', ';');
        }

        public static bool TryParseConnectionConfig(string value, out ServerConnectionDetails details, out string error)
        {
            details = null;
            error = string.Empty;
            try
            {
                var serializer = new JavaScriptSerializer();
                var parsed = serializer.DeserializeObject(value ?? string.Empty) as Dictionary<string, object>;
                if (parsed == null || !string.Equals(GetString(parsed, "clipman"), "server-connection", StringComparison.Ordinal))
                {
                    error = "This is not a Clipman Server connection file.";
                    return false;
                }

                int version;
                if (!int.TryParse(GetString(parsed, "version"), out version) || version != 1)
                {
                    error = "This Clipman Server connection-file version is not supported.";
                    return false;
                }

                var address = CleanUrl(GetString(parsed, "address"));
                if (address.Length == 0)
                {
                    var host = GetString(parsed, "host");
                    var port = GetString(parsed, "port");
                    if (host.Length > 0 && port.Length > 0)
                    {
                        address = CleanUrl(host + ":" + port);
                    }
                }
                var token = CleanToken(GetString(parsed, "token"));
                if (address.Length == 0 || token.Length == 0)
                {
                    error = "The connection file does not contain both a server address and token.";
                    return false;
                }

                ServerCertificateAuthority authority;
                if (!TryParseCertificateAuthority(GetString(parsed, "ca_cert_pem"), address, out authority, out error))
                {
                    return false;
                }

                details = new ServerConnectionDetails(address, token, authority);
                return true;
            }
            catch (Exception ex)
            {
                error = "Clipman could not read this connection file: " + ex.Message;
                return false;
            }
        }

        public static bool TryCreateConnectionConfig(string addressValue, string tokenValue, out string json, out string error)
        {
            return TryCreateConnectionConfig(addressValue, tokenValue, string.Empty, string.Empty, out json, out error);
        }

        public static bool TryCreateConnectionConfig(
            string addressValue,
            string tokenValue,
            string caCertPem,
            string caHost,
            out string json,
            out string error)
        {
            json = string.Empty;
            error = string.Empty;
            var address = CleanUrl(addressValue);
            var token = CleanToken(tokenValue);
            if (address.Length == 0 || token.Length == 0)
            {
                error = "Enter both a Clipman Server address and token before exporting the connection file.";
                return false;
            }

            Uri uri;
            var transport = CleanTransportUrl(address);
            if (!Uri.TryCreate(transport, UriKind.Absolute, out uri) || string.IsNullOrWhiteSpace(uri.Host))
            {
                error = "The Clipman Server address is not valid.";
                return false;
            }

            ServerCertificateAuthority authority;
            if (!TryParseCertificateAuthority(caCertPem, address, out authority, out error))
            {
                return false;
            }
            if (authority != null &&
                !string.IsNullOrWhiteSpace(caHost) &&
                !string.Equals(authority.Host, caHost.Trim(), StringComparison.OrdinalIgnoreCase))
            {
                error = "The private certificate authority is configured for a different server host.";
                return false;
            }

            var values = new Dictionary<string, object>
            {
                { "clipman", "server-connection" },
                { "version", 1 },
                { "address", address },
                { "host", uri.Host },
                { "port", uri.IsDefaultPort ? (string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase) ? 443 : 80) : uri.Port },
                { "token", token }
            };
            if (authority != null)
            {
                values["ca_cert_pem"] = authority.Pem;
            }
            json = new JavaScriptSerializer().Serialize(values);
            return true;
        }

        public static bool TryParseCertificateAuthority(
            string pemValue,
            string addressValue,
            out ServerCertificateAuthority authority,
            out string error)
        {
            authority = null;
            error = string.Empty;
            var pem = (pemValue ?? string.Empty).Trim();
            if (pem.Length == 0) return true;
            if (Encoding.UTF8.GetByteCount(pem) > 32 * 1024)
            {
                error = "The private certificate authority exceeds the 32 KiB limit.";
                return false;
            }
            if (pem.IndexOf("PRIVATE KEY", StringComparison.OrdinalIgnoreCase) >= 0)
            {
                error = "The private certificate authority must not contain private key material.";
                return false;
            }

            var match = Regex.Match(
                pem,
                @"\A\s*-----BEGIN CERTIFICATE-----\s*(?<data>[A-Za-z0-9+/=\r\n]+?)\s*-----END CERTIFICATE-----\s*\z",
                RegexOptions.CultureInvariant);
            if (!match.Success)
            {
                error = "The private certificate authority must contain exactly one PEM CERTIFICATE block.";
                return false;
            }

            byte[] raw;
            X509Certificate2 certificate;
            try
            {
                raw = Convert.FromBase64String(Regex.Replace(match.Groups["data"].Value, @"\s+", string.Empty));
                certificate = new X509Certificate2(raw);
            }
            catch (Exception ex)
            {
                error = "Clipman could not parse the private certificate authority: " + ex.Message;
                return false;
            }

            var isAuthority = false;
            var canSign = true;
            foreach (X509Extension extension in certificate.Extensions)
            {
                if (extension.Oid != null && extension.Oid.Value == "2.5.29.19")
                {
                    var constraints = new X509BasicConstraintsExtension(extension, extension.Critical);
                    isAuthority = constraints.CertificateAuthority;
                }
                else if (extension.Oid != null && extension.Oid.Value == "2.5.29.15")
                {
                    var usage = new X509KeyUsageExtension(extension, extension.Critical);
                    canSign = (usage.KeyUsages & X509KeyUsageFlags.KeyCertSign) != 0;
                }
            }
            if (!isAuthority)
            {
                error = "The configured certificate is not marked as a certificate authority.";
                return false;
            }
            if (!canSign)
            {
                error = "The configured certificate authority cannot sign certificates.";
                return false;
            }
            var now = DateTime.Now;
            if (now < certificate.NotBefore)
            {
                error = "The configured certificate authority is not valid yet.";
                return false;
            }
            if (now > certificate.NotAfter)
            {
                error = "The configured certificate authority has expired.";
                return false;
            }

            Uri uri;
            var displayAddress = CleanUrl(addressValue);
            var transportAddress = CleanTransportUrl(displayAddress);
            if (!Uri.TryCreate(transportAddress, UriKind.Absolute, out uri) ||
                !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) ||
                string.IsNullOrWhiteSpace(uri.Host))
            {
                error = "A private certificate authority can be used only with an HTTPS server address.";
                return false;
            }

            var canonicalPem = "-----BEGIN CERTIFICATE-----\n" +
                Convert.ToBase64String(raw, Base64FormattingOptions.InsertLineBreaks).Replace("\r\n", "\n") +
                "\n-----END CERTIFICATE-----\n";
            authority = new ServerCertificateAuthority(
                canonicalPem,
                uri.Host,
                raw,
                certificate.Subject,
                certificate.NotAfter,
                Sha256Fingerprint(raw));
            return true;
        }

        private static string Sha256Fingerprint(byte[] raw)
        {
            using (var sha = SHA256.Create())
            {
                return BitConverter.ToString(sha.ComputeHash(raw)).Replace("-", ":");
            }
        }

        private static string GetString(Dictionary<string, object> values, string key)
        {
            object value;
            return values.TryGetValue(key, out value) && value != null
                ? Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture).Trim()
                : string.Empty;
        }

        private static string MatchGroup(Regex regex, string text)
        {
            var match = regex.Match(text);
            return match.Success && match.Groups.Count > 1 ? match.Groups[1].Value.Trim() : string.Empty;
        }

        private static string TrimUrlPunctuation(string value)
        {
            return (value ?? string.Empty).Trim().Trim('"', '\'', ',', ';', '.', ')', ']', '}');
        }

        private static string NormalizeDisplayUrl(string value)
        {
            var cleaned = TrimUrlPunctuation(value);
            if (cleaned.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
            {
                cleaned = "clipman://" + cleaned.Substring("http://".Length);
            }
            return cleaned;
        }

        private sealed class ServerSettingsTokenProbe
        {
            public string AuthToken { get; set; }
        }
    }

    internal sealed class ServerConnectionDetails
    {
        public ServerConnectionDetails(string address, string token, ServerCertificateAuthority authority)
        {
            Address = address;
            Token = token;
            Authority = authority;
        }

        public string Address { get; private set; }
        public string Token { get; private set; }
        public ServerCertificateAuthority Authority { get; private set; }
    }

    internal sealed class ServerCertificateAuthority
    {
        public ServerCertificateAuthority(
            string pem,
            string host,
            byte[] rawData,
            string subject,
            DateTime expires,
            string fingerprint)
        {
            Pem = pem ?? string.Empty;
            Host = host ?? string.Empty;
            RawData = rawData ?? new byte[0];
            Subject = subject ?? string.Empty;
            Expires = expires;
            Fingerprint = fingerprint ?? string.Empty;
        }

        public string Pem { get; private set; }
        public string Host { get; private set; }
        public byte[] RawData { get; private set; }
        public string Subject { get; private set; }
        public DateTime Expires { get; private set; }
        public string Fingerprint { get; private set; }
    }
}