Compare commits

...

11 Commits

Author SHA1 Message Date
7ab9d70d47 more flexible 2025-06-27 20:31:44 +03:00
044f4094f0 added alphabet setting 2025-05-11 14:15:32 +03:00
ceb8642365 make /api/captchas/all return hashes as well 2025-05-03 23:57:06 +03:00
9c2fe1c856 added endpoints to fetch captchas 2025-05-03 23:53:09 +03:00
86898fa117 cleanup 2025-05-01 18:24:18 +03:00
e18d26f6f9 added statistics, autofocus field on reload 2025-05-01 18:17:35 +03:00
3d5fa825c4 scripts 2025-05-01 17:34:28 +03:00
e6d14f48bd migration in deployment 2025-04-30 18:31:21 +03:00
b0b1c7a2fc update docker volumes 2025-04-30 18:05:28 +03:00
92fd491194 relay requesting (and maybe verifying) to the user 2025-04-30 18:03:56 +03:00
991a4f29a6 unsuccessful(unstable and dirty) proxy solution 2025-04-29 19:53:50 +03:00
34 changed files with 1918 additions and 225 deletions

View File

@@ -1,8 +1,12 @@
FROM node:22-bullseye FROM node:22-bullseye
ARG APP_PORT ARG APP_PORT
ENV APP_PORT $APP_PORT ENV APP_PORT $APP_PORT
COPY . . WORKDIR /app
COPY migrations migrations
COPY public public
COPY views views
COPY src src
COPY entrypoint.sh migrate.sh package.json package-lock.json .
RUN npm i RUN npm i
EXPOSE $APP_PORT EXPOSE $APP_PORT
ENTRYPOINT ["node", "./src/index.js"] ENTRYPOINT ["bash", "./entrypoint.sh"]

View File

@@ -1,2 +1 @@
# captcha_agregator # captcha_agregator

5
TODO
View File

@@ -1,5 +0,0 @@
[ ] Fully implement and test proxies
[ ] Add a counter of tries for a captcha
[ ] Make new setting "max tries per captcha"
[ ] Unclaim proxy and delete captcha on max tries reached
[ ] Add retry to fronted when captcha is entered incorrectly

View File

@@ -1,18 +0,0 @@
CREATE TABLE IF NOT EXISTS captchas (
id SERIAL PRIMARY KEY,
proxy_id INTEGER REFERENCES proxies(id),
hash CHAR(32),
solution CHAR(6)
);
CREATE TYPE IF NOT EXISTS protocol_type AS ENUM ('http', 'https');
CREATE TABLE IF NOT EXISTS proxies (
id SERIAL PRIMARY KEY,
proto protocol_type,
claimed BOOLEAN DEFAULT FALSE,
host INET NOT NULL,
port SMALLINT NOT NULL,
username VARCHAR(16),
password VARCHAR(32)
)

View File

@@ -5,18 +5,18 @@ services:
ports: ports:
- ${APP_PORT}:${APP_PORT} - ${APP_PORT}:${APP_PORT}
depends_on: depends_on:
- database database:
condition: service_healthy
restart: on-failure restart: on-failure
volumes: volumes:
- ./data/temp:/temp - ./data/uploads:${DATA_DIR}
- ./data/uploads:/uploads - ./.env:/app/.env:ro
database: database:
hostname: database
image: 'postgres:15' image: 'postgres:15'
volumes: volumes:
- ./data/db:/var/lib/postgresql/data - ./data/db:/var/lib/postgresql/data
env_file: .env env_file: .env
ports:
- 5432:5432
environment: environment:
POSTGRES_USER: ${DBUSER} POSTGRES_USER: ${DBUSER}
POSTGRES_PASSWORD: ${DBPASS} POSTGRES_PASSWORD: ${DBPASS}

4
entrypoint.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
npm run migrate up
node src/index.js

3
migrate.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
source .env
DATABASE_URL=postgres://$DBUSER:$DBPASS@$DBHOST:$DBPORT npx node-pg-migrate $*

View File

@@ -0,0 +1,23 @@
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export function up(pgm) {
pgm.createTable('captchas', {
id: 'id',
hash: {type: 'char(32)'},
solution: {type: 'char(6)'}
})
}
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export function down(pgm) {
pgm.dropTable('captchas')
}

View File

@@ -0,0 +1,33 @@
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export function up(pgm) {
pgm.createType('protocol_type', ['http', 'https']);
pgm.createTable('proxies', {
id: 'id',
proto: {type: 'protocol_type', notNull: true},
claimed: {type: 'boolean', default: false},
host: {type: 'inet', notNull: true},
port: {type: 'smallint', notNull: true},
username: {type: 'varchar(16)'},
password: {type: 'varchar(32)'}
});
pgm.addColumn('captchas', {proxy_id: {type: 'integer', references: 'proxies(id)'}});
}
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export function down(pgm) {
pgm.dropType('protocol_type');
pgm.dropTable('proxies');
pgm.dropColumn('captchas', 'proxy_id');
}

View File

@@ -0,0 +1,46 @@
export const shorthands = undefined;
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export function up(pgm) {
pgm.createTable('users', {
id: 'id',
username: {type:"varchar(32)", notNull: true},
password: {type:"char(60)", notNull: true}
})
pgm.addColumn('captchas', {
submitter: {type: 'integer', references: 'users(id)'}
});
pgm.dropType('protocol_type', {cascade: true});
pgm.dropTable('proxies', {cascade: true});
pgm.dropColumn('captchas', 'proxy_id', {cascade: true});
}
/**
* @param pgm {import('node-pg-migrate').MigrationBuilder}
* @param run {() => void | undefined}
* @returns {Promise<void> | void}
*/
export function down(pgm) {
pgm.dropTable('users');
pgm.dropColumn('captchas', 'submitter');
pgm.createType('protocol_type', ['http', 'https']);
pgm.createTable('proxies', {
id: 'id',
proto: {type: 'protocol_type', notNull: true},
claimed: {type: 'boolean', default: false},
host: {type: 'inet', notNull: true},
port: {type: 'smallint', notNull: true},
username: {type: 'varchar(16)'},
password: {type: 'varchar(32)'}
});
pgm.addColumn('captchas', {proxy_id: {type: 'integer', references: 'proxies(id)'}});
}

3
new_captcha_user.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env bash
curl -X POST https://captcha.foxarmy.org/api/user/register -H 'Content-Type: application/json' -H "Authorization: Bearer $1" --data-raw "{\"username\":\"$2\", \"password\":\"$3\"}"

1415
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,15 +11,21 @@
"type": "module", "type": "module",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"start": "node src/index.js" "start": "node src/index.js",
"migrate": "bash migrate.sh"
}, },
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.7",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0", "express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"pg": "^8.15.5", "pg": "^8.15.5",
"pug": "^3.0.3" "pug": "^3.0.3",
"undici": "^7.8.0"
}, },
"devDependencies": { "devDependencies": {
"node-pg-migrate": "^7.9.1",
"nodemon": "^3.1.10" "nodemon": "^3.1.10"
} }
} }

View File

@@ -20,3 +20,13 @@ body {
margin-top: 1%; margin-top: 1%;
margin-bottom: 1%; margin-bottom: 1%;
} }
.topSolvers {
margin-left: auto;
margin-right: 5%;
max-width: max-content;
float:right;
display: inline;
margin-top:1%;
padding-top: 0%;
}

View File

@@ -1,21 +1,107 @@
window.onload = async () => { const check_solution = async (solution) => {
let id = (await (await fetch("/api/captcha", {method: "POST"})).json()).id const body = {
console.log(id); "TotalSum": "78278",
fetch(`/api/captcha/${id}`).then(response => response.blob()) "FnNumber": "9960440301173139",
.then(blob => { "ReceiptOperationType": "1",
const url = URL.createObjectURL(blob); "DocNumber": "35704",
document.getElementById("captcha_image").src = url; "DocFiscalSign": "4149689833",
"Captcha": solution,
"DocDateTime": "2022-09-21T20:28:00.000Z"
} }
); const result = await fetch("https://check.ofd.ru/Document/FetchReceiptFromFns", {
const form = document.getElementById("captchaForm"); method: "POST",
headers: {
"Content-Type": "application/json;charset=utf-8",
// "Origin": "https://check.ofd.ru"
},
body: JSON.stringify(body),
});
return result.status != 400;
}
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 blobToBase64 = (blob) => {
return new Promise((resolve, _) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(blob);
});
}
const show_stats = async () => {
const response = await fetch("/api/user/stats", {
method: "GET",
headers: {
'Content-Type': 'application/json'
}
});
const stats = (await response.json()).stats;
const statsText = document.getElementById("topSolversText");
statsText.innerHTML += '<br/>'
stats.top_five.forEach(stat => {
statsText.innerHTML += `<b>${stat.username}: ${stat.count}</b><br/>`;
});
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"); const inputField = document.getElementById("captcha");
inputField.focus();
// intentionally do not wait for it
show_stats();
if (!document.cookie.includes('JWT')) {
document.location.href = "/login";
}
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) => { form.addEventListener('submit', async (e) => {
e.preventDefault(); 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/${id}`, {method: "PATCH",headers: {'Content-Type': 'application/json'}, body: JSON.stringify({"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) { if (response.status == 200) {
inputField.value = ""; inputField.value = "";
window.location.reload(); window.location.reload();
}; } else {
}) response_json = await response.json()
alert(response_json.message)
}
});
}; };

48
public/js/login.js Normal file
View File

@@ -0,0 +1,48 @@
const set_cookie = (name, value, days) => {
const expires = new Date(Date.now() + days * 86400 * 1000).toUTCString();
document.cookie = `${name}=${value}; expires=${expires}; path=/;`;
}
window.onload = async () => {
alert("This service requests a captcha from https://ofd.ru and sends an example receipt to it to check the correctness of the captcha. If you are not okay with making such requests, please leave the site immediately");
if (document.cookie.includes('JWT')) {
document.location = '/';
}
const form = document.getElementById("loginForm");
const username = document.getElementById("username");
const password = document.getElementById("password");
form.addEventListener('submit', async (e) => {
e.preventDefault();
const response = await fetch(
`/api/user/login`, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"username": username.value,
"password": password.value
})
});
switch (response.status) {
case 403:
alert("Incorrect password");
password.value = "";
break;
case 200:
response_json = await response.json()
set_cookie("JWT", response_json["token"], 365);
window.location.href = '/';
break;
case 404:
alert("No such user exists");
username.value = "";
break;
default:
alert("Unknown server error. Please, conact the developer");
console.log(response);
}
});
};

View File

@@ -9,3 +9,8 @@ DBPASS=GENERATE_A_STRONG_PASSWORD_HERE
APP_PORT=3000 APP_PORT=3000
CAPTCHA_SOURCE_URL=https://example.com CAPTCHA_SOURCE_URL=https://example.com
DATA_DIR=/opt/captcha_aggregator 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]"
CAPTCHA_LENGTH=6

View File

@@ -2,7 +2,7 @@ import dotenv from 'dotenv';
dotenv.config({ path: ".env" }); dotenv.config({ path: ".env" });
const getBoolean = value => { return value === 'true'? true : false } const getBoolean = value => { return value === 'true' ? true : false }
const config = { const config = {
dbuser: process.env.DBUSER, dbuser: process.env.DBUSER,
@@ -14,7 +14,11 @@ const config = {
app_port: Number.parseInt(process.env.APP_PORT), app_port: Number.parseInt(process.env.APP_PORT),
captcha_source_url: process.env.CAPTCHA_SOURCE_URL, captcha_source_url: process.env.CAPTCHA_SOURCE_URL,
data_dir: process.env.DATA_DIR data_dir: process.env.DATA_DIR,
admin_token: process.env.ADMIN_TOKEN,
secret: process.env.SECRET,
alphabet: process.env.ALPHABET,
captcha_length: Number.parseInt(process.env.CAPTCHA_LENGTH)
} }
export default config; export default config;

View File

@@ -1,55 +1,48 @@
import CaptchaService from "../services/captcha.js"; import CaptchaService from "../services/captcha.js";
import ProxyService from "../services/proxy.js"; import jwt from 'jsonwebtoken';
import config from '../config.js'; import config from '../config.js';
import fs from 'fs/promises';
class CaptchaController { class CaptchaController {
async new(req, res) { async submit(req, res) {
try { const { image, solution } = req.body;
const proxy = await ProxyService.take(); if (!image) return res.status(400).send({ "message": "You must send image blob" });
const id = await CaptchaService.new(proxy);
return res.status(200).send({"id": id}); if (!solution || solution.length != config.captcha_length) return res.status(400).send({ "message": "You must send a valid solution" });
} catch (e) { for (let i = 0; i < solution.length; i++) {
console.log(e) let char = solution[i];
return res.status(500).send({"message": "Unknown server error"}); if (!char.match(config.alphabet)) {
console.log("Illegal symbol: " + char);
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(200).send({ "message": "Success" });
}
async get(req, res) { async get(req, res) {
try { try {
const id = req.params.id; const { id } = req.params;
const captcha = await CaptchaService.get(id);
const hash = await CaptchaService.get(id); if (captcha == undefined) return res.status(404).send({ "message": "no such captcha found" });
if (hash == undefined) { return res.status(200).send({ "message": "success", "captcha": captcha })
return res.status(404).send({"message": "captcha not found"});
}
const image = await fs.readFile(`${config.data_dir}/${hash}.jpeg`);
return res.status(200).send(image)
} catch (e) { } catch (e) {
console.log(e) console.log(`Error upon requesting one captcha: ${e}`)
return res.status(500).send({"message": "Unknown server error"}); 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 solve (req, res) { async get_all(req, res) {
try { try {
const id = req.params.id; return res.status(200).send({ "message": "success", "captchas": await CaptchaService.get_all() });
const solution = req.body["solution"];
if (solution == undefined || solution.length != 6) {
return res.status(400).send({"message": 'please, send a valid solution. Example: {"solution":"123456"}'});
}
if (!await CaptchaService.check_and_save_solution(id, solution))
return res.status(409).send({"message": "Solution is not correct"});
let proxy_id = await CaptchaService.get_assigned_proxy(id);
await ProxyService.give_back(id);
return res.status(200).send({"message": "Successful"});
} catch (e) { } catch (e) {
console.log(e) console.log(`Error upon requesting all captchas: ${e}`)
return res.status(500).send({"message": "Unknown server error"}); return res.status(500).send({ "message": "Unknown server error. Please, contact the developer." })
} }
} }
} }

View File

@@ -1,31 +0,0 @@
import ProxyService from '../services/proxy.js';
class CaptchaController {
async add(req, res) {
try {
const { host, port, user: username, password } = req.body;
if (!host || !port) return res.status(400).send({"message":"You must specify host and port!"});
const id = await ProxyService.add(host, port, username, password);
return res.status(200).send({"id": id});
} catch (e) {
console.log(e)
return res.status(500).send({"message": "Unknown server error"});
}
}
async delete (req, res) {
try {
const id = req.params.id;
await ProxyService.delete(id)
return res.status(200).send({"message": "Successful"});
} catch (e) {
console.log(e)
return res.status(500).send({"message": "Unknown server error"});
}
}
}
export default new CaptchaController();

49
src/controllers/user.js Normal file
View File

@@ -0,0 +1,49 @@
import UserService from '../services/user.js';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import config from '../config.js';
class UserController {
async register(req, res) {
try {
const { username, password } = req.body;
if (!username) return res.status(400).send({"message":"You must specify username"});
if (!password) return res.status(400).send({"message":"You must specify password"});
if (await UserService.get_by_username(username)) return res.status(409).send({"message":"Such user already exists!"})
const id = await UserService.add(username, bcrypt.hashSync(password, 10));
const token = jwt.sign({"username": username, "id": id}, config.secret);
return res.status(200).send({"messae": "Success", "token": token});
} catch (e) {
console.log(e)
return res.status(500).send({"message": "Unknown server error. Please, contact the developer"});
}
}
async login (req, res) {
try {
const { username, password } = req.body;
const user = await UserService.get_by_username(username);
if (!user) return res.status(404).send({"message":"No such user exists"});
const hashed_password = user.password;
if (!bcrypt.compareSync(password, hashed_password)) return res.status(403).send({"message":"Passwords dont match"});
const token = jwt.sign({"username": username, "id": user.id}, config.secret);
return res.status(200).send({"message": "Success", "token": token});
} catch (e) {
console.log(e)
return res.status(500).send({"message": "Unknown server error"});
}
}
async stats (req, res) {
try {
const user_id = jwt.decode(req.token).id;
const stats = await UserService.get_stats(user_id);
return res.status(200).send({"message": "Success", "stats": stats});
} catch (e) {
console.log(e)
return res.status(500).send({"message": "Unknown server error"});
}
}
}
export default new UserController();

View File

@@ -15,7 +15,4 @@ const pool = new Pool({
port: config.dbport port: config.dbport
}); });
pool.query(fs.readFileSync('./db_schema.psql').toString());
export default pool; export default pool;

View File

@@ -1,20 +1,34 @@
import express from 'express'; import express from 'express';
import path from 'path'; import path from 'path';
import cookieParser from 'cookie-parser';
import CaptchaRouter from './routers/captcha.js'; import CaptchaRouter from './routers/captcha.js';
import FrontendRouter from './routers/frontend.js'; import FrontendRouter from './routers/frontend.js';
import config from './config.js'; import config from './config.js';
import UserRouter from './routers/user.js';
const app = express(); const app = express();
app.use(express.static(path.join('./public'))); app.use(express.static(path.join('./public')));
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
app.use(express.json()); 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.set('view engine', 'pug');
app.use('/api', CaptchaRouter); app.use('/api', CaptchaRouter);
app.use('/api', UserRouter);
app.use('/', FrontendRouter); app.use('/', FrontendRouter);
const server = app.listen(config.app_port, () => { const server = app.listen(config.app_port, () => {

20
src/middlewares/auth.js Normal file
View File

@@ -0,0 +1,20 @@
import jwt from 'jsonwebtoken';
import config from '../config.js';
const verify_admin_token = async (req, res, next) => {
const header = req.headers['authorization'];
if (!header) return res.status(400).send({"message": "No admin token supplied"});
if (config.admin_token != header.split(' ')[1]) return res.status(403).send({"message":"Admin token is incorrect"});
next();
}
const verify_user_jwt = async (req, res, next) => {
const header = req.headers['authorization'];
let token = req.cookies.JWT? req.cookies.JWT : (header? header.split(' ')[1] : undefined)
if (!token) return res.status(400).send({"message": "No authorization token supplied"});
req.token = token;
if (!jwt.verify(req.token, config.secret)) return res.status(403).send({"message":"Could not verify authorization token"});
next();
}
export default {verify_admin_token, verify_user_jwt};

View File

@@ -1,11 +1,12 @@
import { Router } from 'express'; import { Router } from 'express';
import CaptchaController from '../controllers/captcha.js'; import CaptchaController from '../controllers/captcha.js';
import auth from '../middlewares/auth.js';
const CaptchaRouter = new Router(); const CaptchaRouter = new Router();
CaptchaRouter.post('/captcha', CaptchaController.new); CaptchaRouter.post('/captcha/submit', auth.verify_user_jwt, CaptchaController.submit);
CaptchaRouter.get('/captcha/all', CaptchaController.get_all);
CaptchaRouter.get('/captcha/:id', CaptchaController.get); CaptchaRouter.get('/captcha/:id', CaptchaController.get);
CaptchaRouter.patch('/captcha/:id', CaptchaController.solve);
export default CaptchaRouter; export default CaptchaRouter;

View File

@@ -6,4 +6,8 @@ FrontendRouter.get('/', async (req, res) => {
return res.render("index.pug"); return res.render("index.pug");
}); });
FrontendRouter.get('/login', async (req, res) => {
return res.render("login.pug");
});
export default FrontendRouter; export default FrontendRouter;

View File

@@ -1,11 +0,0 @@
import { Router } from 'express';
import ProxyController from '../controllers/proxy.js';
const ProxyRouter = new Router();
ProxyRouter.post('/proxy', ProxyController.add);
ProxyRouter.get('/proxy/:id', ProxyController.get);
ProxyRouter.patch('/captcha/:id', ProxyController.solve);
export default ProxyRouter;

11
src/routers/user.js Normal file
View File

@@ -0,0 +1,11 @@
import { Router } from 'express';
import UserController from '../controllers/user.js';
import auth from '../middlewares/auth.js';
const UserRouter = new Router();
UserRouter.post('/user/register', auth.verify_admin_token, UserController.register);
UserRouter.post('/user/login', UserController.login);
UserRouter.get('/user/stats', auth.verify_user_jwt, UserController.stats);
export default UserRouter;

View File

@@ -1,72 +1,40 @@
import db from '../db.js'; import db from '../db.js';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { ProxyAgent } from 'undici';
import config from '../config.js'; import config from '../config.js';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import captcha from '../controllers/captcha.js';
function base64ToArrayBuffer(base64) {
var binaryString = atob(base64);
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
class CaptchaService { class CaptchaService {
async new(proxy) { async new(image, solution, submitter) {
try { const b64 = image.replace(/^.*,/, "");
let dispatcher; const arrayBuffer = base64ToArrayBuffer(b64);
if (proxy.username) { const buffer = Buffer.from(arrayBuffer);
dispatcher = new ProxyAgent(`${proxy.proto}://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port}`)
} else {
dispatcher = new ProxyAgent(`${proxy.proto}://${proxy.host}:${proxy.port}`)
}
const captcha = await (await fetch(config.captcha_source_url, dispatcher)).blob();
const buffer = Buffer.from(await captcha.arrayBuffer());
const hash = createHash('md5').update(buffer).digest('hex'); const hash = createHash('md5').update(buffer).digest('hex');
await fs.writeFile(`${config.data_dir}/${hash}.jpeg`, buffer); await fs.writeFile(`${config.data_dir}/${hash}.jpeg`, b64, 'base64');
await db.query("INSERT INTO captchas (hash, solution, submitter) VALUES ($1, $2, $3)", [hash, solution, submitter]);
const id = (await db.query("INSERT INTO captchas (hash, proxy_id) VALUES ($1, $2) RETURNING id", [hash, proxy.id])).rows[0].id;
return id
} catch(e) {
console.log(e);
}
} }
async check_solution(solution) { async get(id) {
const body = { const captcha = (await db.query("SELECT hash, solution FROM captchas WHERE id = $1", [id])).rows[0];
"TotalSum": "78278", if (captcha == undefined) return undefined;
"FnNumber": "9960440301173139",
"ReceiptOperationType": "1", const path = `${config.data_dir}/${captcha.hash}.jpeg`;
"DocNumber": "35704",
"DocFiscalSign": "4149689833", const image = Buffer.from(await fs.readFile(path)).toString('base64');
"Captcha": solution, return { "image": image, "solution": captcha.solution, "hash": captcha.hash };
"DocDateTime": "2022-09-21T20:28:00.000Z"
}
const result = await fetch("https://check.ofd.ru/Document/FetchReceiptFromFns", {
"headers": {
"Content-Type": "application/json;charset=utf-8"
},
"body": JSON.stringify(body),
"method": "POST",
});
return result.status != 400;
} }
async get (id) { async get_all() {
let result = await db.query("SELECT hash FROM captchas WHERE id = $1", [id]) return (await db.query("SELECT id, hash FROM captchas")).rows
if (result.rows[0] == undefined) {
return undefined
}
return result.rows[0].hash;
}
async check_and_save_solution (id, solution) {
if (!await this.check_solution(solution)) {
return false;
}
await db.query("UPDATE captchas SET solution = $1 WHERE id = $2", [solution, id]);
return true;
}
async get_assigned_proxy(id) {
return (await db.query("SELECT proxy_id FROM captchas WHERE id = $1", [id])).rows[0].proxy_id;
} }
} }

View File

@@ -1,23 +0,0 @@
import db from '../db.js';
class ProxyService {
async add(proto, host, port, username, password) {
await db.query("INSERT INTO proxies (proto, host, port, username, password) VALUES ($1, $2, $3, $4, $5)", [proto, host, port, username, password]);
}
async take() {
let proxy = (await db.query("UPDATE proxies SET claimed = TRUE WHERE id = ( SELECT id FROM proxies WHERE claimed = FALSE ORDER BY id LIMIT 1 ) RETURNING proto, host, port, username, password")).rows[0]
if (proxy == undefined) return undefined;
return proxy;
}
async give_back(id) {
await db.query("UPDATE proxies SET claimed = false WHERE id = $1", [id]);
}
async delete(id) {
await db.query("DELETE FRM proxies WHERE id = $1", [id]);
}
}
export default new ProxyService();

20
src/services/user.js Normal file
View File

@@ -0,0 +1,20 @@
import db from '../db.js';
class UserService {
async add(username, password) {
return (await db.query("INSERT INTO users (username, password) VALUES ($1, $2) RETURNING id", [username, password])).rows[0].id;
}
async get_by_username(username) {
return (await db.query("SELECT * FROM users WHERE username = $1", [username])).rows[0];
}
async get_stats(user_id) {
return {
my_count: (await db.query("SELECT COUNT(*) FROM captchas WHERE submitter = $1", [user_id])).rows[0].count,
top_five: (await db.query("select username, (select count(*) from captchas where submitter = users.id) from users JOIN captchas on submitter = users.id GROUP BY users.id ORDER BY count DESC LIMIT 5")).rows
};
}
}
export default new UserService();

1
stats.psql Normal file
View File

@@ -0,0 +1 @@
select users.id, username, (select count(*) from captchas where submitter = users.id) AS amount_solved from users JOIN captchas on submitter = users.id GROUP BY users.id ORDER BY amount_solved DESC;

View File

@@ -6,8 +6,12 @@ html
meta(name="description" content="") meta(name="description" content="")
link(href="css/index.css" rel="stylesheet") link(href="css/index.css" rel="stylesheet")
body body
div(id="tsparticles") div(id="main")
div(class="topSolvers")
label(id="topSolversText") Top-5 solvers:
main(class="box") main(class="box")
h2 Captcha Aggregator h2 Captcha Aggregator
form(id="captchaForm") form(id="captchaForm")

22
views/login.pug Normal file
View File

@@ -0,0 +1,22 @@
html
head
title Captcha Aggregator Login
meta(charset="utf-8")
meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no")
meta(name="description" content="")
link(href="css/index.css" rel="stylesheet")
body
div(id="main")
main(class="box")
h2 Captcha Aggregator
form(id="loginForm")
div(class="inputBox")
label(for="username") Username
input(id="username" placeholder="username" required=true)
div(class="inputBox")
label(for="password") Password
input(type="password" name="password" id="password" placeholder="password" required=true)
div
button(type="submit" name="" style="float: center;") Log in
script(type="text/javascript" src="js/login.js")