From 7ab9d70d47fbd7c7a0d577b2ced231d340f2c9a7 Mon Sep 17 00:00:00 2001 From: leca Date: Fri, 27 Jun 2025 20:31:44 +0300 Subject: [PATCH] more flexible --- Dockerfile | 1 + README.md | 1 - docker-compose.yml | 6 +++--- public/js/index.js | 42 +++++++++++++++++++++++++++----------- sample.env | 3 ++- src/config.js | 5 +++-- src/controllers/captcha.js | 27 ++++++++++++------------ src/index.js | 10 +++++++++ src/services/captcha.js | 4 ++-- 9 files changed, 65 insertions(+), 34 deletions(-) diff --git a/Dockerfile b/Dockerfile index bcf239d..27e1557 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM node:22-bullseye ARG APP_PORT ENV APP_PORT $APP_PORT +WORKDIR /app COPY migrations migrations COPY public public COPY views views diff --git a/README.md b/README.md index 24803a6..b13a64e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ # captcha_agregator - diff --git a/docker-compose.yml b/docker-compose.yml index 66af735..5a383b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,18 +5,18 @@ services: ports: - ${APP_PORT}:${APP_PORT} depends_on: - - database + database: + condition: service_healthy restart: on-failure volumes: - ./data/uploads:${DATA_DIR} + - ./.env:/app/.env:ro database: hostname: database image: 'postgres:15' volumes: - ./data/db:/var/lib/postgresql/data env_file: .env - ports: - - 5432:5432 environment: POSTGRES_USER: ${DBUSER} POSTGRES_PASSWORD: ${DBPASS} diff --git a/public/js/index.js b/public/js/index.js index 4248e40..e94f485 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -23,20 +23,20 @@ const check_solution = async (solution) => { const get_cookie = (name) => { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i].trim(); - if (cookie.startsWith(name + '=')) { - const value = cookie.substring(name.length + 1); - } + const cookie = cookies[i].trim(); + if (cookie.startsWith(name + '=')) { + const value = cookie.substring(name.length + 1); + } } } const blobToBase64 = (blob) => { return new Promise((resolve, _) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.readAsDataURL(blob); + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(blob); }); - } +} const show_stats = async () => { const response = await fetch("/api/user/stats", { @@ -54,6 +54,19 @@ const show_stats = async () => { statsText.innerHTML += `You have solved ${stats.my_count} captcha(s)` } +const validate_solution = (settings, solution) => { + if (solution.length != settings.captcha_length) return false; + for (let i = 0; i < solution.length; i++) { + let char = solution[i]; + if (!char.match(settings.captcha_regex)) { + console.log("Illegal symbol: " + char); + alert(`Illegal symbol ${char} at position ${i + 1}`); + return false + } + } + return true; +} + window.onload = async () => { const inputField = document.getElementById("captcha"); inputField.focus(); @@ -62,22 +75,27 @@ window.onload = async () => { if (!document.cookie.includes('JWT')) { document.location.href = "/login"; } - - const response = await fetch("https://check.ofd.ru/api/captcha/common/img"); + const settings = await (await fetch("/api/settings")).json(); + const captcha_source_url = settings.captcha_source_url; + const response = await fetch(captcha_source_url); captcha = await response.blob(); const url = URL.createObjectURL(captcha); document.getElementById("captcha_image").src = url; const form = document.getElementById("captchaForm"); - + form.addEventListener('submit', async (e) => { e.preventDefault(); + if (!validate_solution(settings, inputField.value)) { + alert("You must specify valid solution!") + return; + } // if (!await check_solution(inputField.value)) { // alert("Капча решена неверно") // returnl // } - const response = await fetch(`/api/captcha/submit`, {method: "POST", headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ "image": await blobToBase64(captcha), "solution": inputField.value})}); + const response = await fetch(`/api/captcha/submit`, { method: "POST", headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ "image": await blobToBase64(captcha), "solution": inputField.value }) }); if (response.status == 200) { inputField.value = ""; window.location.reload(); diff --git a/sample.env b/sample.env index 54e4023..51822bd 100644 --- a/sample.env +++ b/sample.env @@ -12,4 +12,5 @@ DATA_DIR=/opt/captcha_aggregator ADMIN_TOKEN=GENERATE_A_STRONG_TOKEN_HERE SECRET=GENERATE_A_STRONG_SECRET_HERE #Allowed symbols. Must be a regex -ALPHABET="[123456789ABCDEFGHIJKLMNPQRSTUVWXZY]" \ No newline at end of file +ALPHABET="[123456789ABCDEFGHIJKLMNPQRSTUVWXZY]" +CAPTCHA_LENGTH=6 \ No newline at end of file diff --git a/src/config.js b/src/config.js index f96e301..537e37a 100644 --- a/src/config.js +++ b/src/config.js @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; dotenv.config({ path: ".env" }); -const getBoolean = value => { return value === 'true'? true : false } +const getBoolean = value => { return value === 'true' ? true : false } const config = { dbuser: process.env.DBUSER, @@ -17,7 +17,8 @@ const config = { data_dir: process.env.DATA_DIR, admin_token: process.env.ADMIN_TOKEN, secret: process.env.SECRET, - alphabet: process.env.ALPHABET + alphabet: process.env.ALPHABET, + captcha_length: Number.parseInt(process.env.CAPTCHA_LENGTH) } export default config; \ No newline at end of file diff --git a/src/controllers/captcha.js b/src/controllers/captcha.js index 0659bd5..2a639ff 100644 --- a/src/controllers/captcha.js +++ b/src/controllers/captcha.js @@ -4,44 +4,45 @@ import config from '../config.js'; class CaptchaController { async submit(req, res) { - const {image, solution} = req.body; - if (!image) return res.status(400).send({"message":"You must send image blob"}); - if (!solution || solution.length != 6) return res.status(400).send({"message":"You must send a valid solution"}); - for (let i = 0; i < solution.length; i ++) { + const { image, solution } = req.body; + if (!image) return res.status(400).send({ "message": "You must send image blob" }); + + if (!solution || solution.length != config.captcha_length) return res.status(400).send({ "message": "You must send a valid solution" }); + for (let i = 0; i < solution.length; i++) { let char = solution[i]; if (!char.match(config.alphabet)) { console.log("Illegal symbol: " + char); - return res.status(400).send({"message": `Illegal symbol ${char} at position ${i + 1}`}); + return res.status(400).send({ "message": `Illegal symbol ${char} at position ${i + 1}` }); } } try { await CaptchaService.new(image, solution, jwt.decode(req.token).id); } catch (e) { console.log(`Error upon submitting: ${e}`); - return res.status(500).send({"message": "Unknown server error. Please, contact the developer."}); + return res.status(500).send({ "message": "Unknown server error. Please, contact the developer." }); } - return res.status(200).send({"message": "Success"}); + return res.status(200).send({ "message": "Success" }); } async get(req, res) { try { const { id } = req.params; const captcha = await CaptchaService.get(id); - if (captcha == undefined) return res.status(404).send({"message":"no such captcha found"}); - return res.status(200).send({"message":"success", "captcha": captcha}) + if (captcha == undefined) return res.status(404).send({ "message": "no such captcha found" }); + return res.status(200).send({ "message": "success", "captcha": captcha }) } catch (e) { console.log(`Error upon requesting one captcha: ${e}`) - if (e.code == 'ENOENT') return res.status(404).send({"message": "The ID exists in the DB but I can't find an actual image. Please, contact the developer."}) - return res.status(500).send({"message": "Unknown server error. Please, contact the developer."}) + if (e.code == 'ENOENT') return res.status(404).send({ "message": "The ID exists in the DB but I can't find an actual image. Please, contact the developer." }) + return res.status(500).send({ "message": "Unknown server error. Please, contact the developer." }) } } async get_all(req, res) { try { - return res.status(200).send({"message":"success", "captchas": await CaptchaService.get_all()}); + return res.status(200).send({ "message": "success", "captchas": await CaptchaService.get_all() }); } catch (e) { console.log(`Error upon requesting all captchas: ${e}`) - return res.status(500).send({"message": "Unknown server error. Please, contact the developer."}) + return res.status(500).send({ "message": "Unknown server error. Please, contact the developer." }) } } } diff --git a/src/index.js b/src/index.js index d648984..e2dd1d7 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,16 @@ app.use(express.urlencoded({ extended: false })); app.use(express.json()); app.use(cookieParser()); +app.get('/api/settings', (req, res) => { + return res.status(200).send( + { + "captcha_source_url": config.captcha_source_url, + "captcha_regex": config.captcha_regex, + "captcha_length": config.captcha_length + } + ) +}) + app.set('view engine', 'pug'); app.use('/api', CaptchaRouter); diff --git a/src/services/captcha.js b/src/services/captcha.js index 006ee3c..b1aadb2 100644 --- a/src/services/captcha.js +++ b/src/services/captcha.js @@ -15,7 +15,7 @@ function base64ToArrayBuffer(base64) { class CaptchaService { async new(image, solution, submitter) { - const b64 = image.replace(/^data:image\/jpeg;base64,/, ""); + const b64 = image.replace(/^.*,/, ""); const arrayBuffer = base64ToArrayBuffer(b64); const buffer = Buffer.from(arrayBuffer); const hash = createHash('md5').update(buffer).digest('hex'); @@ -30,7 +30,7 @@ class CaptchaService { const path = `${config.data_dir}/${captcha.hash}.jpeg`; const image = Buffer.from(await fs.readFile(path)).toString('base64'); - return {"image": image, "solution": captcha.solution, "hash": captcha.hash}; + return { "image": image, "solution": captcha.solution, "hash": captcha.hash }; } async get_all() {