Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 8 additions & 18 deletions plugins/vietnamese/NguonC/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;

Expand Down Expand Up @@ -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 ----------
Expand All @@ -318,23 +320,11 @@ class NguonCPlugin implements Plugin.PluginBase {
`<meta name="lnreader-video-url" content="${encodeHtmlEntities(opts.embed)}">`,
);
} else {
/*
if (opts.m3u8) {
// không phải m3u8 của nguonc nên chưa rõ cách bypass
metas.push('<meta name="lnreader-video-mode" content="direct">', '<meta name="lnreader-video-type" content="m3u8">');
metas.push(
`<meta name="lnreader-video-url" content="${encodeHtmlEntities(opts.m3u8)}">`,
);
} else {
*/
metas.push('<meta name="lnreader-video-mode" content="lazy">');
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(`<div ${attrs.join(' ')} style="display:none;"></div>`);
}

Expand Down
256 changes: 112 additions & 144 deletions plugins/vietnamese/NguonC/webview/index.ts
Original file line number Diff line number Diff line change
@@ -1,165 +1,133 @@
/* eslint-disable */
/// <reference types="webview" />

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<string | null> {
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);
}
})();
Loading