Can embed any file in a PNG/WebM/GIF/JPEG and upload it to a third-party host through 4chan
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

267 lines
8.6 KiB

/* eslint-disable */
import { Buffer } from "buffer";
import { fileTypeFromBuffer } from 'file-type';
import { concatAB, PNGDecoder, PNGEncoder } from "./png";
const IDAT = Buffer.from("IDAT");
const IEND = Buffer.from("IEND");
const tEXt = Buffer.from("tEXt");
const CUM0 = Buffer.from("CUM\0" + "0");
let extractEmbedded = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
let magic = false;
let sneed = new PNGDecoder(reader);
try {
let lastIDAT: Buffer | null = null;
for await (let [name, chunk, crc, offset] of sneed.chunks()) {
switch (name) {
case 'tEXt': // should exist at the beginning of file to signal decoders if the file indeed has an embedded chunk
if (chunk.slice(4, 4 + CUM0.length).equals(CUM0))
magic = true;
break;
case 'IDAT':
if (magic) {
lastIDAT = chunk;
break;
}
case 'IEND':
if (!magic)
throw "Didn't find tExt Chunk";
default:
break;
}
}
if (lastIDAT) {
let data = (lastIDAT as Buffer).slice(4);
let fnsize = data.readUInt32LE(0);
let fn = data.slice(4, 4 + fnsize).toString();
// Todo: xor the buffer to prevent scanning for file signatures (4chan embedded file detection)?
data = data.slice(4 + fnsize);
return { filename: fn, data };
}
} catch (e) {
console.error(e);
} finally {
reader.releaseLock();
}
}
let processImage = async (src: string) => {
if (!src.match(/\.png$/))
return;
let resp = await GM_fetch(src);
let reader = (await resp.blob()).stream();
if (!reader)
return;
return await extractEmbedded(reader.getReader());
};
/* Used for debugging */
let processImage2 = async (src: string) => {
if (!src.match(/\.png$/))
return;
let resp = await GM_fetch(src);
let reader = resp.body!.getReader();
if (!reader)
return;
let data = Buffer.alloc(0);
let chunk;
while ((chunk = await reader.read()) && !chunk.done) {
data = concatAB(data, Buffer.from(chunk.value));
}
return {
filename: 'aaaa',
data
};
};
let processPost = async (post: HTMLDivElement) => {
let thumb = post.querySelector(".fileThumb") as HTMLAnchorElement;
if (!thumb)
return;
console.log("Processing post", post)
let res = await processImage(thumb.href);
if (!res)
return;
// add buttons
let fi = post.querySelector(".file-info")!;
let a = document.createElement('a');
a.className = "fa fa-eye";
let type = await fileTypeFromBuffer(res.data);
let cont: HTMLImageElement | HTMLVideoElement;
let w: number, h: number;
if (type?.mime.startsWith("image")) {
cont = document.createElement("img");
} else if (type?.mime.startsWith("video")) {
cont = document.createElement("video");
} else
return; // TODO: handle new file types??? Or direct "download"?
cont.src = URL.createObjectURL(new Blob([res.data]));
await new Promise(res => {
cont.onload = res;
});
if (cont instanceof HTMLImageElement) {
w = cont.naturalWidth;
h = cont.naturalHeight;
}
if (cont instanceof HTMLVideoElement) {
w = cont.width;
h = cont.height;
}
let contract = () => {
cont.style.width = "auto";
cont.style.height = "auto";
cont.style.maxWidth = "125px";
cont.style.maxHeight = "125px";
}
let expand = () => {
cont.style.width = `${w}px`;
cont.style.height = `${h}px`;
cont.style.maxWidth = "unset";
cont.style.maxHeight = "unset";
}
let imgcont = document.createElement('div');
let p = thumb.parentElement!;
p.removeChild(thumb);
imgcont.appendChild(thumb);
p.appendChild(imgcont);
thumb.style.display = "flex";
thumb.style.gap = "5px";
thumb.style.flexDirection = "column";
a.classList.toggle("disabled");
let contracted = true;
contract();
cont.onclick = (e) => {
contracted = !contracted;
(contracted) ? contract() : expand();
e.stopPropagation();
}
let visible = false;
a.onclick = () => {
visible = !visible;
if (visible) {
imgcont.appendChild(cont)
} else {
imgcont.removeChild(cont);
}
a.classList.toggle("disabled");
}
fi.children[1].insertAdjacentElement('afterend', a);
}
let buildChunk = (tag: string, data: Buffer) => {
let ret = Buffer.alloc(data.byteLength + 4);
ret.write(tag.substr(0, 4), 0);
data.copy(ret, 4);
return ret;
}
let BufferWriteStream = () => {
let b = Buffer.from([])
let ret = new WritableStream<Buffer>({
write(chunk) {
b = concatAB(b, chunk);
}
});
return [ret, () => b] as [WritableStream<Buffer>, () => Buffer];
}
let buildInjection = async (container: File, inj: File) => {
let [writestream, extract] = BufferWriteStream();
let encoder = new PNGEncoder(writestream);
let decoder = new PNGDecoder(container.stream().getReader());
let magic = false;
for await (let [name, chunk, crc, offset] of decoder.chunks()) {
if (magic && name != "IDAT")
break;
if (!magic && name == "IDAT") {
await encoder.insertchunk(["tEXt", buildChunk("tEXt", CUM0), 0, 0]);
magic = true;
}
await encoder.insertchunk([name, chunk, crc, offset]);
}
let injb = Buffer.alloc(4 + inj.name.length + inj.size);
injb.writeInt32LE(inj.name.length, 0);
injb.write(inj.name, 4);
Buffer.from(await inj.arrayBuffer()).copy(injb, 4 + inj.name.length);
await encoder.insertchunk(["IDAT", buildChunk("IDAT", injb), 0, 0]);
await encoder.insertchunk(["IEND", buildChunk("IEND", Buffer.from([])), 0, 0]);
return { file: new Blob([extract()]), name: container.name };
}
const startup = async () => {
await Promise.all([...document.querySelectorAll('.postContainer')].map(e => processPost(e as any)));
//await Promise.all([...document.querySelectorAll('.postContainer')].filter(e => e.textContent?.includes("191 KB")).map(e => processPost(e as any)));
document.addEventListener('PostsInserted', <any>(async (e: CustomEvent<string>) => {
processPost(e.target as any);
}));
let getSelectedFile = () => {
return new Promise<File>(res => {
document.addEventListener('QRFile', e => res((e as any).detail), { once: true });
document.dispatchEvent(new CustomEvent('QRGetFile'));
})
}
let injected = false;
document.addEventListener('QRDialogCreation', <any>((e: CustomEvent<string>) => {
if (injected)
return;
injected = true;
let target = e.target as HTMLDivElement;
let bts = target.querySelector('#qr-filename-container')
let i = document.createElement('i');
i.className = "fa fa-magnet";
let a = document.createElement('a')
a.appendChild(i);
a.title = "Embed File (Select a file before...)";
bts?.appendChild(a);
a.onclick = async (e) => {
let file = await getSelectedFile();
if (!file)
return;
let input = document.createElement('input') as HTMLInputElement;
input.setAttribute("type", "file");
input.onchange = (async ev => {
if (input.files)
document.dispatchEvent(new CustomEvent('QRSetFile', { detail: await buildInjection(file, input.files[0]) }))
})
input.click();
}
}));
};
document.addEventListener('4chanXInitFinished', startup);
// onload = () => {
// let container = document.getElementById("container") as HTMLInputElement;
// let injection = document.getElementById("injection") as HTMLInputElement;
// container.onchange = injection.onchange = async () => {
// if (container.files?.length && injection.files?.length) {
// let res = await buildInjection(container.files[0], injection.files[0]);
// let result = document.getElementById("result") as HTMLImageElement;
// let extracted = document.getElementById("extracted") as HTMLImageElement;
// result.src = URL.createObjectURL(res.file);
// let embedded = await extractEmbedded(res.file.stream().getReader());
// extracted.src = URL.createObjectURL(new Blob([embedded?.data!]));
// }
// }
// }