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