using System.Text.RegularExpressions; namespace ClawdDotNet.Tools.SocialMediaManager; /// /// Prüft und normalisiert YouTube-Adressen, bevor sie an yt-dlp übergeben werden. /// /// Hintergrund (S2): Die Eingabe wurde ungeprüft in eine Argument-Zeichenkette /// interpoliert. UseShellExecute = false verhindert zwar Shell-Metazeichen, /// nicht aber Options-Injection: yt-dlp kennt --exec, das beliebige Befehle nach /// dem Download ausführt. Ein Wert wie /// --exec "cmd /c …" https://youtube.com/… führte damit zu Codeausführung. /// /// Kritisch ist das, weil der Agent untrusted Inhalte verarbeitet (Videotitel, Posts, /// Mails) — eine Prompt-Injection darin kann ihn dazu bringen, genau so einen Wert zu /// setzen. /// public static class YouTubeUrl { private static readonly string[] AllowedHosts = [ "youtube.com", "www.youtube.com", "m.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be" ]; /// Handles wie "@ChannelName" — bewusst eng gefasst. private static readonly Regex HandlePattern = new( @"^@?[A-Za-z0-9._-]{1,100}$", RegexOptions.Compiled); /// /// Löst eine Channel-Angabe zu einer vollständigen URL auf. /// Akzeptiert ein Handle ("@Name" oder "Name") oder eine YouTube-URL. /// public static bool TryResolveChannelUrl(string? input, out string url, out string? error) { url = ""; error = null; var value = input?.Trim() ?? ""; if (value.Length == 0) { error = "Channel specification missing."; return false; } // Reject anything that looks like an option immediately if (value.StartsWith('-')) { error = "Channel specification must not start with '-'."; return false; } if (value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { if (!TryValidateUrl(value, out error)) return false; url = value; return true; } if (!HandlePattern.IsMatch(value)) { error = "Invalid channel handle. Allowed are letters, digits, dots, hyphens, and underscores."; return false; } var handle = value.StartsWith('@') ? value : "@" + value; url = $"https://www.youtube.com/{handle}/videos"; return true; } /// Validates a video or channel URL against allowed hosts. public static bool TryValidateUrl(string? input, out string? error) { error = null; var value = input?.Trim() ?? ""; if (value.StartsWith('-')) { error = "URL must not start with '-'."; return false; } if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) { error = "Not a valid URL."; return false; } if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) { error = $"Unsupported scheme '{uri.Scheme}'. Allowed are http and https."; return false; } if (!AllowedHosts.Contains(uri.Host, StringComparer.OrdinalIgnoreCase)) { error = $"Host '{uri.Host}' is not allowed. Only YouTube URLs are permitted."; return false; } return true; } /// /// Baut die Argumentliste, um die neueste Video-Id eines Kanals abzufragen. /// Die Liste wird an ProcessStartInfo.ArgumentList übergeben, wo jedes /// Element als genau ein Argument ankommt. /// public static List BuildLatestVideoIdArgs(string channelUrl) => [ "--print", "%(id)s", "--playlist-end", "1", "--", // beendet die Optionsliste channelUrl ]; /// Baut die Argumentliste für den Audio-Download. public static List BuildAudioDownloadArgs( string outputPath, string videoUrl, string? ffmpegDirectory, bool convertToMp3) { var args = new List(); if (!string.IsNullOrEmpty(ffmpegDirectory)) { args.Add("--ffmpeg-location"); args.Add(ffmpegDirectory); } args.Add("-x"); if (convertToMp3) { args.Add("--audio-format"); args.Add("mp3"); } args.Add("-o"); args.Add(outputPath); args.Add("--"); args.Add(videoUrl); return args; } }