From 72707a02de26fb477a369c211a134a876d81915d Mon Sep 17 00:00:00 2001 From: Fioren <102145692+FiorenMas@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:18:42 +0700 Subject: [PATCH 1/2] up --- plugins/vietnamese/NguonC/index.ts | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/plugins/vietnamese/NguonC/index.ts b/plugins/vietnamese/NguonC/index.ts index f9899c79..a3a80f5f 100644 --- a/plugins/vietnamese/NguonC/index.ts +++ b/plugins/vietnamese/NguonC/index.ts @@ -8,6 +8,7 @@ import { ContentType } from '@libs/pluginMetadata'; import { storage } from '@libs/storage'; import { load } from 'cheerio'; + const SITE = 'https://phim.nguonc.com'; const API_BASE = SITE + '/api'; @@ -16,7 +17,7 @@ class NguonCPlugin implements Plugin.PluginBase { name = 'NguonC'; icon = 'src/vi/nguonc/icon.png'; site = SITE; - version = '1.0.9'; + version = '1.0.10'; customJS = 'src/vi/nguonc/player.js'; contentType = ContentType.VIDEO; @@ -286,14 +287,15 @@ class NguonCPlugin implements Plugin.PluginBase { const html = await fetchText(url); const $ = load(html); const div = $('#player'); + const dataObf = div.attr('data-obf')!; const obf = JSON.parse( - Buffer.from(div.attr('data-obf')!, 'base64').toString(), + Buffer.from(dataObf, 'base64').toString(), ) as { sUb: string; hD: string; - kX: string; + kX?: string; }; - return obf; + return { hD: obf.hD, dataObf }; } // ---------- buildPlayerHtml ---------- @@ -318,23 +320,11 @@ class NguonCPlugin implements Plugin.PluginBase { ``, ); } else { - /* - if (opts.m3u8) { - // không phải m3u8 của nguonc nên chưa rõ cách bypass - metas.push('', ''); - metas.push( - ``, - ); - } else { - */ metas.push(''); + const iframeObf = await this.getM3u8DataFromEmbed(opts.embed!); const attrs: string[] = ['id="nguonc-player-container"']; attrs.push(`data-iframe="${encodeHtmlEntities(opts.embed)}"`); - // trying fetch iframe - const iframeObf = await this.getM3u8DataFromEmbed(opts.embed!); - attrs.push(`data-s="${encodeHtmlEntities(iframeObf?.sUb)}"`); - attrs.push(`data-h="${encodeHtmlEntities(iframeObf?.hD)}"`); - attrs.push(`data-k="${encodeHtmlEntities(iframeObf?.kX)}"`); + attrs.push(`data-obf="${encodeHtmlEntities(iframeObf?.dataObf || '')}"`); metas.push(`
`); } From e0ee36bee14f2cee311aed421f67d85506e2ae31 Mon Sep 17 00:00:00 2001 From: Fioren <102145692+FiorenMas@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:19:06 +0700 Subject: [PATCH 2/2] up --- plugins/vietnamese/NguonC/webview/index.ts | 256 +++++++++------------ 1 file changed, 112 insertions(+), 144 deletions(-) diff --git a/plugins/vietnamese/NguonC/webview/index.ts b/plugins/vietnamese/NguonC/webview/index.ts index 8bea5a88..e9f8f1b9 100644 --- a/plugins/vietnamese/NguonC/webview/index.ts +++ b/plugins/vietnamese/NguonC/webview/index.ts @@ -1,165 +1,133 @@ /* eslint-disable */ /// -function hexToBytes(hexString: string) { - const bytes = new Uint8Array(hexString.length / 2); - for (let i = 0; i < hexString.length; i += 2) { - bytes[i / 2] = parseInt(hexString.substr(i, 2), 16); +/** + * NguonC - WebView Video Player + * + * Decryption: HMAC-SHA256(key="stream-derive-v1", data=videoHash)[0:32] → AES-GCM key + * Then AES-GCM decrypt with that key + IV from the #ENC-AESGCM header. + * Fallback: iframe if decryption fails. + */ + +const hexToBytes = (hex: string) => { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); } return bytes; -} -function base64ToBytes(base64String: string) { - const binaryString = atob(base64String); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); +}; + +const b64Decode = (b64: string) => { + const bin = atob(b64); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) { + bytes[i] = bin.charCodeAt(i); } return bytes; -} - -async function initPlayer() { - const container = document.getElementById('nguonc-player-container'); - if (!container || !window.LNReaderPlayer) return; - const iframeUrl = container.getAttribute('data-iframe'); - const s = container.getAttribute('data-s'); - const h = container.getAttribute('data-h'); - const k = container.getAttribute('data-k'); - if (!iframeUrl) return; - const urlObj = new URL(iframeUrl); - const req = await window.reader.fetch(`${urlObj.origin}/${s}.m3u8`, { - method: 'GET', - headers: { - Referer: urlObj.origin, - }, - referrer: urlObj.origin, - }); - const m3u8Content = await req.text(); - const lines = m3u8Content.split('\n'); - let ivHex = ''; - let encryptedData = ''; +}; - for (let line of lines) { - line = line.trim(); - if (line.startsWith('#ENC-AESGCM')) { - const ivMatch = line.match(/iv=([a-fA-F0-9]+)/); - if (ivMatch) ivHex = ivMatch[1]; - } else if (line && !line.startsWith('#')) { - encryptedData = line; - } +async function decryptM3u8( + encryptedBytes: Uint8Array, + ivBytes: Uint8Array, + videoHash: string, +): Promise { + try { + const enc = new TextEncoder(); + const hmacKey = await crypto.subtle.importKey( + 'raw', + enc.encode('stream-derive-v1'), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'], + ); + const hmacResult = await crypto.subtle.sign( + 'HMAC', + hmacKey, + enc.encode(videoHash), + ); + const aesKey = await crypto.subtle.importKey( + 'raw', + new Uint8Array(hmacResult).slice(0, 32), + { name: 'AES-GCM' }, + false, + ['decrypt'], + ); + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: ivBytes }, + aesKey, + encryptedBytes, + ); + return new TextDecoder().decode(decrypted); + } catch { + return null; } +} - if (!ivHex || !encryptedData) { - throw new Error('Định dạng tệp mã hóa không hợp lệ.'); - } +function createProxyFragLoader(origin: string) { + return class { + stats = { aborted: false, loaded: 0, retry: 0, total: 0, chunkCount: 0, bwEstimate: 0, loading: { start: 0, first: 0, end: 0 }, parsing: { start: 0, end: 0 }, buffering: { start: 0, first: 0, end: 0 } }; + constructor() {} + destroy() {} + abort() {} + load(ctx: any, _cfg: any, cbs: any) { + this.stats.loading.start = performance.now(); + window.reader.fetch(ctx.url, { method: 'GET', headers: { Referer: origin }, referrer: origin }) + .then((resp: any) => { if (!resp.ok) throw new Error('HTTP ' + resp.status); this.stats.loading.first = performance.now(); return resp.arrayBuffer(); }) + .then((buf: any) => { this.stats.loading.end = performance.now(); this.stats.loaded = buf.byteLength; this.stats.total = buf.byteLength; cbs.onSuccess({ data: buf }, this.stats, ctx, null); }) + .catch((err: any) => { this.stats.loading.end = performance.now(); cbs.onError({ code: 0, text: err.message }, ctx, null, this.stats); }); + } + }; +} - const ivBytes = hexToBytes(ivHex); - const encryptedBytes = base64ToBytes(encryptedData); +(async () => { + const player = window.LNReaderPlayer; + if (!player) return; - const ciphertext = encryptedBytes.slice(0, -16); - const authTag = encryptedBytes.slice(-16); + const container = document.getElementById('nguonc-player-container'); + const embedUrl = container?.getAttribute('data-iframe') || ''; + const dataObf = container?.getAttribute('data-obf') || ''; - const encryptionKey = k; - if (!encryptionKey) { - throw new Error('Thiếu khóa xác thực mã hóa.'); + if (!embedUrl || !dataObf) { + player.playIframe(embedUrl); + return; } - const textEncoder = new TextEncoder(); - const keyBytes = textEncoder.encode(encryptionKey).slice(0, 32); - const cryptoKey = await crypto.subtle.importKey( - 'raw', - keyBytes, - { - name: 'AES-GCM', - }, - false, - ['decrypt'], - ); - - const encryptedBuffer = new Uint8Array(ciphertext.length + authTag.length); - encryptedBuffer.set(ciphertext); - encryptedBuffer.set(authTag, ciphertext.length); + try { + const embedOrigin = new URL(embedUrl).origin; + const streamData = JSON.parse(atob(dataObf)); + const m3u8Url = `${embedOrigin}/${streamData.sUb}`; - const algorithm = { - name: 'AES-GCM', - iv: ivBytes, - tagLength: 128, - }; + player.log('[NGC] Fetching m3u8...'); + const resp = await window.reader.fetch(m3u8Url, { method: 'GET' }); + const encryptedText = await resp.text(); - const decryptedBuffer = await crypto.subtle.decrypt( - algorithm, - cryptoKey, - encryptedBuffer, - ); - const textDecoder = new TextDecoder(); - const m3u8 = textDecoder.decode(decryptedBuffer); - const blob = new Blob([m3u8], { - type: 'application/vnd.apple.mpegurl', - }); - const url = URL.createObjectURL(blob); - let ProxyFragLoader = function (config: any) { - // @ts-ignore - this._config = config; - // @ts-ignore - this.stats = { - aborted: false, - loaded: 0, - retry: 0, - total: 0, - chunkCount: 0, - bwEstimate: 0, - loading: { start: 0, first: 0, end: 0 }, - parsing: { start: 0, end: 0 }, - buffering: { start: 0, first: 0, end: 0 }, - }; - // @ts-ignore - this.context = null; - // @ts-ignore - this._controller = null; - }; - ProxyFragLoader.prototype.destroy = function () { - this.abort(); - }; - ProxyFragLoader.prototype.abort = function () { - if (this._controller) { - this._controller.abort(); - this._controller = null; + let ivHex = ''; + let encryptedData = ''; + for (const line of encryptedText.split('\n')) { + const trimmed = line.trim(); + if (trimmed.startsWith('#ENC-AESGCM')) { + const m = trimmed.match(/iv=([a-fA-F0-9]+)/); + if (m) ivHex = m[1]; + } else if (trimmed && !trimmed.startsWith('#')) { + encryptedData = trimmed; + } } - }; - // @ts-ignore - ProxyFragLoader.prototype.load = function (ctx, cfg, cbs) { - this.context = ctx; - var self = this; - self.stats.loading.start = performance.now(); - window.reader - .fetch(ctx.url, { - method: 'GET', - headers: { - Referer: urlObj.origin, - }, - referrer: urlObj.origin, - }) - .then(function (resp) { - if (!resp.ok) throw new Error('HTTP ' + resp.status); - self.stats.loading.first = performance.now(); - return resp.arrayBuffer(); - }) - .then(function (buf) { - self.stats.loading.end = performance.now(); - self.stats.loaded = buf.byteLength; - self.stats.total = buf.byteLength; - cbs.onSuccess({ data: buf }, self.stats, ctx, null); - }) - .catch(function (err) { - if (err.name === 'AbortError') return; - self.stats.loading.end = performance.now(); - cbs.onError({ code: 0, text: err.message }, ctx, null, self.stats); - }); - }; + if (!ivHex || !encryptedData) throw new Error('Invalid m3u8 format'); - window.LNReaderPlayer.playHls(url, { - fLoader: ProxyFragLoader, - }); -} + const m3u8Text = await decryptM3u8( + b64Decode(encryptedData), + hexToBytes(ivHex), + streamData.hD, + ); -initPlayer(); + if (!m3u8Text) throw new Error('Decryption failed'); + + player.log('[NGC] Playing decrypted m3u8 (' + m3u8Text.length + ' chars)'); + const blob = new Blob([m3u8Text], { type: 'application/vnd.apple.mpegurl' }); + player.playHls(URL.createObjectURL(blob), { fLoader: createProxyFragLoader(embedOrigin) }); + } catch (err: any) { + player.log('[NGC] Error: ' + (err?.message || err)); + player.playIframe(embedUrl); + } +})();