Compare commits
	
		
			11 Commits
		
	
	
		
			c257844fa2
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7ab9d70d47 | |||
| 044f4094f0 | |||
| ceb8642365 | |||
| 9c2fe1c856 | |||
| 86898fa117 | |||
| e18d26f6f9 | |||
| 3d5fa825c4 | |||
| e6d14f48bd | |||
| b0b1c7a2fc | |||
| 92fd491194 | |||
| 991a4f29a6 | 
							
								
								
									
										10
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -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"]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								TODO
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								TODO
									
									
									
									
									
								
							@@ -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
 | 
					 | 
				
			||||||
@@ -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)
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@@ -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
									
								
							
							
						
						
									
										4
									
								
								entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					npm run migrate up
 | 
				
			||||||
 | 
					node src/index.js
 | 
				
			||||||
							
								
								
									
										3
									
								
								migrate.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								migrate.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env bash
 | 
				
			||||||
 | 
					source .env
 | 
				
			||||||
 | 
					DATABASE_URL=postgres://$DBUSER:$DBPASS@$DBHOST:$DBPORT npx node-pg-migrate $*
 | 
				
			||||||
							
								
								
									
										23
									
								
								migrations/1745667860234_captchas.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								migrations/1745667860234_captchas.js
									
									
									
									
									
										Normal 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')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										33
									
								
								migrations/1745668334604_proxies.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								migrations/1745668334604_proxies.js
									
									
									
									
									
										Normal 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');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										46
									
								
								migrations/1745958234333_users.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								migrations/1745958234333_users.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										3
									
								
								new_captcha_user.sh
									
									
									
									
									
										Executable 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
									
									
									
								
							
							
						
						
									
										1415
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							@@ -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"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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
									
								
							
							
						
						
									
										48
									
								
								public/js/login.js
									
									
									
									
									
										Normal 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);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -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
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
@@ -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." })
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
									
								
							
							
						
						
									
										49
									
								
								src/controllers/user.js
									
									
									
									
									
										Normal 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();
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/index.js
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								src/index.js
									
									
									
									
									
								
							@@ -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
									
								
							
							
						
						
									
										20
									
								
								src/middlewares/auth.js
									
									
									
									
									
										Normal 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};
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
									
								
							
							
						
						
									
										11
									
								
								src/routers/user.js
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
@@ -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;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
									
								
							
							
						
						
									
										20
									
								
								src/services/user.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1
									
								
								stats.psql
									
									
									
									
									
										Normal 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;
 | 
				
			||||||
@@ -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
									
								
							
							
						
						
									
										22
									
								
								views/login.pug
									
									
									
									
									
										Normal 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")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
		Reference in New Issue
	
	Block a user