using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; namespace Shared.Engine { public static class PlayerJsDecoder { private static readonly Regex _reAtobLiteral = new Regex(@"atob\(\s*(['""])(?[A-Za-z0-9+/=]+)\1\s*\)", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex _reJsonParseHelper = new Regex(@"JSON\.parse\(\s*(?[A-Za-z_$][\w$]*)\(\s*(?['""])(?.*?)(\k)\s*\)\s*\)", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); private static readonly Regex _reHelperCall = new Regex(@"^\s*(?[A-Za-z_$][\w$]*)\(\s*(?['""])(?.*?)(\k)\s*\)\s*$", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled); private static readonly Regex _reTrailingComma = new Regex(@",\s*([}\]])", RegexOptions.Compiled); private static readonly UTF8Encoding _utf8Strict = new UTF8Encoding(false, true); private static readonly Encoding _latin1 = Encoding.GetEncoding("ISO-8859-1"); public static PlayerPayload ExtractPlayerPayload(string htmlText) { string cleanHtml = WebUtility.HtmlDecode(htmlText ?? string.Empty); if (string.IsNullOrWhiteSpace(cleanHtml)) return null; var candidates = new List { cleanHtml }; string decodedScript = DecodeOuterPlayerScript(cleanHtml); if (!string.IsNullOrWhiteSpace(decodedScript)) candidates.Insert(0, decodedScript); foreach (string sourceText in candidates) { string objectText = ExtractObjectByBraces(sourceText, "new Playerjs"); if (string.IsNullOrWhiteSpace(objectText)) objectText = ExtractObjectByBraces(sourceText, "Playerjs({"); string searchText = string.IsNullOrWhiteSpace(objectText) ? sourceText : objectText; string fileValue = ExtractJsValue(searchText, "file"); if (fileValue == null && !string.IsNullOrWhiteSpace(objectText)) fileValue = ExtractJsValue(sourceText, "file"); if (fileValue == null) continue; string titleValue = ExtractJsValue(searchText, "title"); object parsedFile = ParsePlayerFileValue(fileValue, sourceText); return new PlayerPayload { Title = Nullish(titleValue), FilePayload = parsedFile }; } return null; } private static object ParsePlayerFileValue(string rawValue, string contextText) { string text = rawValue?.Trim(); if (string.IsNullOrWhiteSpace(text)) return rawValue; if (text.StartsWith("[") || text.StartsWith("{")) { JsonNode loaded = LoadJsonLoose(text); if (loaded != null) return loaded; } var parseMatch = _reJsonParseHelper.Match(text); if (parseMatch.Success) { string decoded = DecodeHelperPayload(parseMatch.Groups["fn"].Value, parseMatch.Groups["payload"].Value, contextText); if (!string.IsNullOrWhiteSpace(decoded)) { JsonNode loaded = LoadJsonLoose(decoded); if (loaded != null) return loaded; } } var helperMatch = _reHelperCall.Match(text); if (helperMatch.Success) { string decoded = DecodeHelperPayload(helperMatch.Groups["fn"].Value, helperMatch.Groups["payload"].Value, contextText); if (!string.IsNullOrWhiteSpace(decoded)) { JsonNode loaded = LoadJsonLoose(decoded); if (loaded != null) return loaded; return decoded; } } return rawValue; } private static string DecodeHelperPayload(string helperName, string payload, string contextText) { if (string.IsNullOrWhiteSpace(helperName)) return null; if (helperName.Equals("atob", StringComparison.OrdinalIgnoreCase)) { byte[] rawBytes = SafeBase64Decode(payload); return rawBytes == null ? null : DecodeBytes(rawBytes); } string helperKey = ExtractHelperKey(contextText, helperName); if (string.IsNullOrWhiteSpace(helperKey)) return null; byte[] keyBytes = Encoding.UTF8.GetBytes(helperKey); if (keyBytes.Length == 0) return null; byte[] payloadBytes = SafeBase64Decode(payload); if (payloadBytes == null) return null; var decoded = new byte[payloadBytes.Length]; for (int index = 0; index < payloadBytes.Length; index++) decoded[index] = (byte)(payloadBytes[index] ^ keyBytes[index % keyBytes.Length]); return DecodeBytes(decoded); } private static string ExtractHelperKey(string contextText, string helperName) { if (string.IsNullOrWhiteSpace(contextText) || string.IsNullOrWhiteSpace(helperName)) return null; string pattern = $@"function\s+{Regex.Escape(helperName)}\s*\([^)]*\)\s*\{{[\s\S]*?var\s+k\s*=\s*(['""])(?.*?)\1"; var match = Regex.Match(contextText, pattern, RegexOptions.IgnoreCase); if (!match.Success) return null; return Nullish(match.Groups["key"].Value); } private static string DecodeOuterPlayerScript(string text) { if (string.IsNullOrWhiteSpace(text)) return null; var match = _reAtobLiteral.Match(text); if (!match.Success) return null; byte[] rawData = SafeBase64Decode(match.Groups["payload"].Value); if (rawData == null || rawData.Length <= 32) return null; byte[] key = rawData.Take(32).ToArray(); byte[] encryptedData = rawData.Skip(32).ToArray(); var decoded = new byte[encryptedData.Length]; for (int index = 0; index < encryptedData.Length; index++) decoded[index] = (byte)(encryptedData[index] ^ key[index % key.Length]); return DecodeBytes(decoded); } private static byte[] SafeBase64Decode(string value) { string text = value?.Trim(); if (string.IsNullOrWhiteSpace(text)) return null; int remainder = text.Length % 4; if (remainder != 0) text += new string('=', 4 - remainder); try { return Convert.FromBase64String(text); } catch { return null; } } private static string DecodeBytes(byte[] data) { if (data == null || data.Length == 0) return null; try { return _utf8Strict.GetString(data); } catch { return _latin1.GetString(data); } } private static string ExtractObjectByBraces(string text, string anchor) { if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(anchor)) return null; int anchorIndex = text.IndexOf(anchor, StringComparison.OrdinalIgnoreCase); if (anchorIndex < 0) return null; int braceIndex = text.IndexOf('{', anchorIndex); if (braceIndex < 0) return null; int depth = 0; bool escaped = false; char? inString = null; for (int index = braceIndex; index < text.Length; index++) { char current = text[index]; if (inString.HasValue) { if (escaped) { escaped = false; continue; } if (current == '\\') { escaped = true; continue; } if (current == inString.Value) inString = null; continue; } if (current == '"' || current == '\'') { inString = current; continue; } if (current == '{') { depth++; continue; } if (current == '}') { depth--; if (depth == 0) return text.Substring(braceIndex + 1, index - braceIndex - 1); } } return null; } private static string ExtractJsValue(string text, string key) { if (string.IsNullOrWhiteSpace(text) || string.IsNullOrWhiteSpace(key)) return null; var match = Regex.Match(text, $@"\b{Regex.Escape(key)}\b\s*:\s*", RegexOptions.IgnoreCase); if (!match.Success) return null; int index = match.Index + match.Length; while (index < text.Length && char.IsWhiteSpace(text[index])) index++; if (index >= text.Length) return null; char token = text[index]; if (token == '"' || token == '\'') { var (value, _) = ReadJsString(text, index); return value; } if (token == '[') { int endIndex = FindMatchingBracket(text, index, '[', ']'); return endIndex >= index ? text.Substring(index, endIndex - index + 1) : null; } if (token == '{') { int endIndex = FindMatchingBracket(text, index, '{', '}'); return endIndex >= index ? text.Substring(index, endIndex - index + 1) : null; } int stopIndex = index; while (stopIndex < text.Length && text[stopIndex] != ',' && text[stopIndex] != '}' && text[stopIndex] != '\n' && text[stopIndex] != '\r') stopIndex++; return text.Substring(index, stopIndex - index).Trim(); } private static (string value, int nextIndex) ReadJsString(string text, int startIndex) { if (string.IsNullOrWhiteSpace(text) || startIndex < 0 || startIndex >= text.Length) return (null, -1); char quote = text[startIndex]; if (quote != '"' && quote != '\'') return (null, -1); var buffer = new StringBuilder(); bool escaped = false; for (int index = startIndex + 1; index < text.Length; index++) { char current = text[index]; if (escaped) { buffer.Append(current); escaped = false; continue; } if (current == '\\') { escaped = true; continue; } if (current == quote) return (buffer.ToString(), index + 1); buffer.Append(current); } return (null, -1); } private static int FindMatchingBracket(string text, int startIndex, char openChar, char closeChar) { if (string.IsNullOrWhiteSpace(text) || startIndex < 0 || startIndex >= text.Length || text[startIndex] != openChar) return -1; int depth = 0; bool escaped = false; char? inString = null; for (int index = startIndex; index < text.Length; index++) { char current = text[index]; if (inString.HasValue) { if (escaped) { escaped = false; continue; } if (current == '\\') { escaped = true; continue; } if (current == inString.Value) inString = null; continue; } if (current == '"' || current == '\'') { inString = current; continue; } if (current == openChar) { depth++; continue; } if (current == closeChar) { depth--; if (depth == 0) return index; } } return -1; } public static JsonNode LoadJsonLoose(string value) { string text = value?.Trim(); if (string.IsNullOrWhiteSpace(text)) return null; string normalized = WebUtility.HtmlDecode(text).Replace("\\/", "/"); string unescapedQuotes = normalized.Replace("\\'", "'").Replace("\\\"", "\""); var candidates = new[] { normalized, unescapedQuotes, RemoveTrailingCommas(normalized), RemoveTrailingCommas(unescapedQuotes) }; foreach (string candidate in candidates.Distinct(StringComparer.Ordinal)) { if (string.IsNullOrWhiteSpace(candidate)) continue; try { return JsonNode.Parse(candidate); } catch { } } return null; } private static string RemoveTrailingCommas(string value) { return string.IsNullOrWhiteSpace(value) ? value : _reTrailingComma.Replace(value, "$1"); } public static string Nullish(string value) { string text = value?.Trim(); if (string.IsNullOrWhiteSpace(text)) return null; if (text.Equals("null", StringComparison.OrdinalIgnoreCase) || text.Equals("none", StringComparison.OrdinalIgnoreCase) || text.Equals("undefined", StringComparison.OrdinalIgnoreCase)) return null; return text; } } public sealed class PlayerPayload { public string Title { get; set; } public object FilePayload { get; set; } } }