add module code
This commit is contained in:
parent
8c96ae1782
commit
014ed6f6a8
53
module/gm.js
Normal file
53
module/gm.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
const { spawn } = require("child_process");
|
||||||
|
|
||||||
|
// GM
|
||||||
|
async function callGM(params, stdin) {
|
||||||
|
const out = [];
|
||||||
|
const err = [];
|
||||||
|
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
gm = spawn("gm", params);
|
||||||
|
|
||||||
|
gm.stdout.on("data", (c) => out.push(c));
|
||||||
|
gm.stderr.on("data", (c) => err.push(c));
|
||||||
|
|
||||||
|
gm.on("close", (code) => {
|
||||||
|
if (code !== 0) return reject(Buffer.concat(err).toString());
|
||||||
|
|
||||||
|
resolve(Buffer.concat(out));
|
||||||
|
});
|
||||||
|
gm.stdin.end(stdin);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = (dbg) => console.debug(dbg) || dbg;
|
||||||
|
class GMController {
|
||||||
|
async cropImage(buffer, x, y, offsetX = 0, offsetY = 0) {
|
||||||
|
return await callGM(
|
||||||
|
["convert", "-", "-crop", `${x}x${y}+${offsetX}+${offsetY}`, "png:-"],
|
||||||
|
buffer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For change hash-sum by insignificant changes in pixel colors
|
||||||
|
async repixelise(buffer) {
|
||||||
|
const noiseAmount = Math.random() * 1; // 0..1%
|
||||||
|
const brightness = (98 + Math.random() * 4).toFixed(2); // 98..102%
|
||||||
|
|
||||||
|
return await callGM(
|
||||||
|
["convert", "-", "-noise", ""+noiseAmount, "-modulate", brightness.toString(), "png:-"],
|
||||||
|
buffer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSize (buffer) {
|
||||||
|
const [width, height] =(await callGM(
|
||||||
|
['identify', '-format', '%w %h', '-'],
|
||||||
|
buffer,
|
||||||
|
)).toString().split(/\s{1,}/g).map(s => +s);
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new GMController();
|
||||||
117
module/index.js
Normal file
117
module/index.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const gm = require("./gm");
|
||||||
|
const mime = require("./mimetype");
|
||||||
|
|
||||||
|
function shuffle(array) {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
let j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFixedBytes(pathToFile, size = 16) {
|
||||||
|
const fd = fs.openSync(pathToFile, "r");
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.alloc(size);
|
||||||
|
fs.readSync(fd, buffer, 0, size, 0);
|
||||||
|
return buffer;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
fs.closeSync(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isImage (pathToFile, requiredSize=null) {
|
||||||
|
const contentType = mime.byPath(pathToFile).contentType;
|
||||||
|
const isImageMime = /^image\//m.test(contentType);
|
||||||
|
if (
|
||||||
|
!isImageMime || !requiredSize ||
|
||||||
|
!Array.isArray(requiredSize)
|
||||||
|
) return isImageMime;
|
||||||
|
|
||||||
|
const [width, height] = requiredSize;
|
||||||
|
const { width:ow, height:oh } = await gm.getSize(
|
||||||
|
fs.readFileSync(pathToFile)
|
||||||
|
);
|
||||||
|
|
||||||
|
return width === ow && height === oh;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getImageFromFolderGeneral (imagesFolder) {
|
||||||
|
// Get list of images from folder
|
||||||
|
const entries = fs.readdirSync(imagesFolder, { withFileTypes: true });
|
||||||
|
const images = [];
|
||||||
|
for (let entry of entries) {
|
||||||
|
if (!entry.isDirectory() && !!(await isImage(path.join(imagesFolder, entry.name), [150, 150]))) {
|
||||||
|
images.push(path.join(imagesFolder, entry.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (images.length === 0)
|
||||||
|
throw new Error(imagesFolder, "has not required images (150x150 px)");
|
||||||
|
|
||||||
|
shuffle(images);
|
||||||
|
//console.debug(images);
|
||||||
|
return images[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function genHashGeneral(length = 3) {
|
||||||
|
return Buffer.from(
|
||||||
|
[...new Array(length)].map((x) => Math.round(Math.random() * 255)),
|
||||||
|
).toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateTask (imagesFolder, options = {}) {
|
||||||
|
if (!path.isAbsolute(imagesFolder))
|
||||||
|
imagesFolder = path.join(process.cwd(), imagesFolder);
|
||||||
|
|
||||||
|
// Define tools
|
||||||
|
if (!options.genHash) options.genHash = genHashGeneral;
|
||||||
|
if (!options.getImageFromFolder)
|
||||||
|
options.getImageFromFolder = getImageFromFolderGeneral;
|
||||||
|
|
||||||
|
const choosenImage = fs.readFileSync(await getImageFromFolderGeneral(imagesFolder));
|
||||||
|
// Split the image by 9 shards
|
||||||
|
const fragments = new Object();
|
||||||
|
let answer = "";
|
||||||
|
const addOps = [];
|
||||||
|
for (let row = 0; row < 3; row++)
|
||||||
|
for (let col = 0; col < 3; col++) {
|
||||||
|
let key;
|
||||||
|
do {
|
||||||
|
key = options.genHash();
|
||||||
|
} while (!!fragments[key]);
|
||||||
|
answer += key;
|
||||||
|
const shardBytes = await gm.repixelise(
|
||||||
|
await gm.cropImage(choosenImage, 50, 50, col * 50, row * 50),
|
||||||
|
);
|
||||||
|
addOps.push(
|
||||||
|
() =>
|
||||||
|
(fragments[key] =
|
||||||
|
`data:image/png;base64,${shardBytes.toString("base64")}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
shuffle(addOps);
|
||||||
|
addOps.forEach((f) => f());
|
||||||
|
const successCode = options.genHash(8);
|
||||||
|
|
||||||
|
return {
|
||||||
|
openPuzzleCaptchaAPI: {
|
||||||
|
getTask: {
|
||||||
|
isError: false,
|
||||||
|
mode: "3x3",
|
||||||
|
fragments
|
||||||
|
},
|
||||||
|
taskSolved: {
|
||||||
|
isError: false,
|
||||||
|
isSuccess: true,
|
||||||
|
successCode
|
||||||
|
},
|
||||||
|
},
|
||||||
|
answer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { generateTask };
|
||||||
128
module/mimetype.js
Normal file
128
module/mimetype.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
function readByBuffer(buffer) {
|
||||||
|
if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
|
||||||
|
return { contentType: "application/octet-stream", extension: "" };
|
||||||
|
}
|
||||||
|
buffer = buffer.slice(0, 16);
|
||||||
|
|
||||||
|
// ---------- IMAGES ----------
|
||||||
|
|
||||||
|
// PNG
|
||||||
|
if (
|
||||||
|
buffer.length >= 8 &&
|
||||||
|
buffer[0] === 0x89 &&
|
||||||
|
buffer[1] === 0x50 &&
|
||||||
|
buffer[2] === 0x4e &&
|
||||||
|
buffer[3] === 0x47
|
||||||
|
) {
|
||||||
|
return { contentType: "image/png", extension: "png" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// JPEG
|
||||||
|
if (buffer[0] === 0xff && buffer[1] === 0xd8) {
|
||||||
|
return { contentType: "image/jpeg", extension: "jpg" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// GIF
|
||||||
|
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
|
||||||
|
return { contentType: "image/gif", extension: "gif" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// WEBP
|
||||||
|
if (
|
||||||
|
buffer.length >= 12 &&
|
||||||
|
buffer[0] === 0x52 && // R
|
||||||
|
buffer[1] === 0x49 && // I
|
||||||
|
buffer[2] === 0x46 && // F
|
||||||
|
buffer[3] === 0x46 && // F
|
||||||
|
buffer[8] === 0x57 && // W
|
||||||
|
buffer[9] === 0x45 && // E
|
||||||
|
buffer[10] === 0x42 && // B
|
||||||
|
buffer[11] === 0x50 // P
|
||||||
|
) {
|
||||||
|
return { contentType: "image/webp", extension: "webp" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- VIDEO / AUDIO ----------
|
||||||
|
|
||||||
|
// MP4 / M4V / MOV (ISO BMFF)
|
||||||
|
if (buffer.length >= 12 && buffer.slice(4, 8).toString() === "ftyp") {
|
||||||
|
const brand = buffer.slice(8, 12).toString();
|
||||||
|
|
||||||
|
if (["isom", "mp41", "mp42"].includes(brand)) {
|
||||||
|
return { contentType: "video/mp4", extension: "mp4" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["M4V ", "M4VH"].includes(brand)) {
|
||||||
|
return { contentType: "video/x-m4v", extension: "m4v" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (brand === "qt ") {
|
||||||
|
return { contentType: "video/quicktime", extension: "mov" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MP3
|
||||||
|
if (
|
||||||
|
buffer.slice(0, 3).toString() === "ID3" ||
|
||||||
|
(buffer[0] === 0xff && (buffer[1] & 0xe0) === 0xe0)
|
||||||
|
) {
|
||||||
|
return { contentType: "audio/mpeg", extension: "mp3" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// WAV
|
||||||
|
if (
|
||||||
|
buffer.slice(0, 4).toString() === "RIFF" &&
|
||||||
|
buffer.slice(8, 12).toString() === "WAVE"
|
||||||
|
) {
|
||||||
|
return { contentType: "audio/wav", extension: "wav" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// OGG
|
||||||
|
if (buffer.slice(0, 4).toString() === "OggS") {
|
||||||
|
return { contentType: "audio/ogg", extension: "ogg" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- DOCUMENTS ----------
|
||||||
|
|
||||||
|
// PDF
|
||||||
|
if (buffer.slice(0, 4).toString() === "%PDF") {
|
||||||
|
return { contentType: "application/pdf", extension: "pdf" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZIP (docx, xlsx, apk, etc.)
|
||||||
|
if (buffer[0] === 0x50 && buffer[1] === 0x4b) {
|
||||||
|
return { contentType: "application/zip", extension: "zip" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- TEXT ----------
|
||||||
|
|
||||||
|
// UTF-8 text (heuristic)
|
||||||
|
const text = buffer.toString("utf8");
|
||||||
|
if (/^[\x09\x0A\x0D\x20-\x7E]*$/.test(text)) {
|
||||||
|
return { contentType: "text/plain", extension: "txt" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- FALLBACK ----------
|
||||||
|
return {
|
||||||
|
contentType: "application/octet-stream",
|
||||||
|
extension: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readByPath(pathToFile) {
|
||||||
|
const size = 16;
|
||||||
|
const fd = fs.openSync(pathToFile, "r");
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.alloc(size);
|
||||||
|
fs.readSync(fd, buffer, 0, size, 0);
|
||||||
|
return readByBuffer(buffer);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
fs.closeSync(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { byPath: readByPath, byBuffer: readByBuffer };
|
||||||
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "openpuzzle-captcha",
|
||||||
|
"version": "25.12.0",
|
||||||
|
"description": "Generate of interactive captcha (Puzzles)",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.fullgream.tech/fullgream/openpuzzle-captcha.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"captcha",
|
||||||
|
"puzzle",
|
||||||
|
"interactive",
|
||||||
|
"canvas",
|
||||||
|
"interactive-captcha"
|
||||||
|
],
|
||||||
|
"author": "FullGreaM",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user