mirror of
1
2
Fork 0
This commit is contained in:
Frank Lemanschik 2020-03-25 13:21:23 +01:00
parent bdae28a825
commit 639f493972
19 changed files with 331 additions and 407 deletions

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "ucaptcha",
"version": "0.0.0-pre2",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,9 +1,13 @@
import path from 'path';
import {fileURLToPath} from 'url';
const __dirname = fileURLToPath(import.meta.url);
export const __dirname = fileURLToPath(import.meta.url);
export const dirname = meta => fileURLToPath(import.meta.url);
export const PROJECT_ROOT = process.cwd();
export const TAGS = [
null,
'vehicle',
'house',
]
/**
* A path string relative to the project folder.
* @typedef {String} IMAGES_FOLDER
@ -12,8 +16,3 @@ export const IMAGES_FOLDER = path.join(PROJECT_ROOT, 'public', 'images');
export const MAX_SESSION_TIME = 60 * 30;
export const TAGS = [
null,
'vehicle',
'house',
]

View File

@ -0,0 +1,14 @@
// Reduces errors till express is fixed
import express from 'express';
/**
* @type {express.Router}
* @property {function} get
*/
export class Router {
/**
*
*/
constructor() {
return express.Router();
}
}

View File

@ -1,5 +1,5 @@
import fs from 'fs';
import {IMAGES_FOLDER} from '../../R.js';
import {IMAGES_FOLDER} from '../R.js';
/**
* @return {Promise<string>} Image file without extension

View File

@ -0,0 +1,37 @@
/**
* TODO: this code should maybe be the constructor of a session model
*/
import pickRandomFile from './pickRandomFile.js';
import {randomBytes} from './utils.js';
import {client} from '../idb.js';
import {MAX_SESSION_TIME, TAGS} from '../R.js';
import {Session} from '../models/Session.js';
/**
* @param {string} websiteKey
* @param {Object} cookies
* @return {Promise<Session>}
*/
export default async function initializeSession(websiteKey, cookies) {
const image = await pickRandomFile();
const imageTagId = 1; // TODO: Change later to be dynamic based on `image`
const sessionId = cookies[websiteKey] ? cookies[websiteKey] : randomBytes(8);
client.setex(sessionId, MAX_SESSION_TIME, JSON.stringify({
sessionId,
websiteKey,
image,
obj: imageTagId,
score: 0.5,
}));
const session = new Session({
sessionId,
websiteKey,
imageTagId: TAGS[imageTagId],
});
return session;
};

View File

@ -1,6 +1,7 @@
console.log(import.meta.url, ' is deprecated please use res.json() directly')
/**
* @param {Response} res
* @param {object} json
* @param {import('express').Response} res
* @param {JSON} json
*/
export default (res, json) => {
res.setHeader('Content-Type', 'application/json');

View File

@ -1,9 +0,0 @@
/**
* Session storage in IDB
* @typedef {Object} IDBSession
* @property {string} sessionId User session ID
* @property {string} websiteKey Website ID
* @property {string} image Image URL for challenge
* @property {number} obj Image object ID
* @property {number} score How likely is the user to be a human?
*/

19
src/models/Session.js Normal file
View File

@ -0,0 +1,19 @@
/**
* Session storage in IDB
* @type {Session} Session
* @property {string} [sessionId] User session ID
* @property {string} [websiteKey] Website ID
* @property {string} [image] Image URL for challenge
* @property {number} [obj] Image object ID
* @property {number} [score] How likely is the user to be a human?
*/
export class Session {
/**
*
* @param {*} obj
*/
constructor(obj={}) {
Object.assign(this,obj)
}
}
export default Session

View File

@ -1,95 +0,0 @@
/**
* User session
*/
export default function UserSession() {
/** @private @type {string | undefined} */
this._sessionId = undefined;
/** @private @type {string | undefined} */
this._websiteKey = undefined;
/** @private @type {string | undefined} */
this._imageTag = undefined;
/**
* Get session identifier
* @return {string}
*/
this.getSessionId = function() {
return this._sessionId;
};
/**
* Set session identifier
* @param {string} sessionId
*/
this.setSessionId = function(sessionId) {
this._sessionId = sessionId;
};
/**
* Get website key
* @return {string}
*/
this.getWebsiteKey = function() {
return this._websiteKey;
};
/**
* Set website key
* @param {string} websiteKey
*/
this.setWebsiteKey = function(websiteKey) {
this._websiteKey = websiteKey;
};
/**
* Get image tag as text
* @return {string}
*/
this.getImageTag = function() {
return this._imageTag;
};
/**
* Set image tag as text
* @param {string} imageObject
*/
this.setImageTag = function(imageObject) {
this._imageTag = imageObject;
};
/**
* Serialize the session into a JSON object
* @return {Array<any>}
*/
this.serialize = function() {
const sessionId = this.getSessionId();
const websiteKey = this.getWebsiteKey();
const imageTag = this.getImageTag();
return [
sessionId,
websiteKey,
imageTag,
];
};
/**
* Deserialize session data from JSON object
* @param {Array<any>} payload
*/
this.deserialize = function([
sessionId,
websiteKey,
imageTag,
]) {
this.setSessionId(sessionId);
this.setWebsiteKey(websiteKey);
this.setImageTag(imageTag);
};
}

20
src/routes/api/image.js Normal file
View File

@ -0,0 +1,20 @@
import {Router} from '../../helpers/express-router.js';
import {serveImage} from './verify.js';
import path from 'path';
import {IMAGES_FOLDER} from '../../R.js';
const router = new Router();
export default router.get('/image', async (req, res) => {
const sessionId = req.query.s;
if (!sessionId) {
res.end();
return;
}
const [suspicious, filename] = await serveImage(sessionId);
if (suspicious) {
res.end();
return;
}
res.sendFile(path.join(IMAGES_FOLDER, filename));
});

View File

@ -1,56 +0,0 @@
import express from 'express';
import path from 'path';
import reloadSession from './reloadSession.js';
import serveImage from './serveImage.js';
import verifyImage from './verifyImage.js';
import sendJson from '../../helpers/sendJson.js';
import {IMAGES_FOLDER, MAX_SESSION_TIME} from '../../R.js';
const router = new express.Router();
router.get('/reload', async (req, res)=>{
try {
const websiteKey = req.query.k;
if (!websiteKey) {
res.end();
return;
}
const session = await reloadSession(websiteKey, req.cookies);
res.cookie(websiteKey, session.getSessionId(),
{httpOnly: true, maxAge: MAX_SESSION_TIME * 1000});
sendJson(res, session.serialize());
} catch (e) {
console.error(e);
res.end();
}
});
router.get('/image', async (req, res)=>{
const sessionId = req.query.s;
if (!sessionId) {
res.end();
return;
}
const [suspicious, filename] = await serveImage(sessionId);
if (suspicious) {
res.end();
return;
}
res.sendFile(path.join(IMAGES_FOLDER, filename));
});
router.post('/verify', async (req, res)=>{
const sessionId = req.body.s;
const mat = req.body.mat;
console.log(req.body);
const session = await verifyImage(sessionId, mat);
sendJson(res, session.serialize());
});
export default router;

34
src/routes/api/reload.js Normal file
View File

@ -0,0 +1,34 @@
import express from 'express';
import sendJson from '../../helpers/sendJson.js';
import {MAX_SESSION_TIME} from '../../R.js';
import reloadSession from '../../helpers/reloadSession.js';
const router = express.Router();
/**
*
* @param {express.Request} req
* @param {express.Response} res
*/
const reloadHandler = async (req, res) => {
try {
const websiteKey = req.query.k;
if (!websiteKey) {
res.end();
return;
}
const session = await reloadSession(websiteKey, req.cookies);
const {sessionId} = session;
res.cookie(
websiteKey, sessionId,
{httpOnly: true, maxAge: MAX_SESSION_TIME * 1000},
);
res.json(session);
} catch (e) {
console.error(e);
res.end();
}
};
export default router.get('/reload', reloadHandler);

View File

@ -1,51 +0,0 @@
/**
* TODO: this code should maybe be the constructor of a session model
*/
import pickRandomFile from './pickRandomFile.js';
import UserSession from '../../models/UserSession.js';
import {randomBytes} from '../../helpers/utils.js';
import {client} from '../../helpers/idb.js';
import {MAX_SESSION_TIME, TAGS} from '../../R.js';
// /**
// * @typedef {import('../../models/IDBSession.js')} IDBSession
// */
/**
* @param {string} websiteKey
* @param {Object} cookies
* @return {Promise<UserSession>}
*/
export default async function initializeSession(websiteKey, cookies) {
let randomSessionId;
if (cookies[websiteKey]) {
randomSessionId = cookies[websiteKey];
} else {
randomSessionId = randomBytes(8);
}
const image = await pickRandomFile();
const imageTagId = 1; // TODO: Change later to be dynamic based on `image`
const session = new UserSession();
session.setSessionId(randomSessionId);
session.setWebsiteKey(websiteKey);
session.setImageTag(TAGS[imageTagId]);
/** @type {import('../../models/IDBSession.js')} */
const idbPayload = {
sessionId: session.getSessionId(),
websiteKey: session.getWebsiteKey(),
image,
obj: imageTagId,
score: 0.5,
};
client.setex(
session.getSessionId(), MAX_SESSION_TIME, JSON.stringify(idbPayload));
return session;
};

View File

@ -1,45 +0,0 @@
import {client} from '../../helpers/idb.js';
/**
* @typedef {import('../../models/IDBSession.js').IDBSession} IDBSession
*/
/**
* @param {string} sessionId
* @return {Promise}
*/
export default function(sessionId) {
return new Promise((resolve, reject)=>{
client.get(sessionId, (err, result)=>{
if (err) return reject(err);
if (!result) return reject(new Error('Session expired'));
/** @type {IDBSession} */
const sessionData = JSON.parse(result);
console.log(sessionData);
const {
// sessionId,
image,
// score,
} = sessionData;
// if (score < 0.1) {
// client.del(sessionId, (err)=>{
// if (err) return reject(err);
// return resolve([
// true,
// null,
// ]);
// });
// }
return resolve([
null,
image + '.jpg',
]);
});
});
}

189
src/routes/api/verify.js Normal file
View File

@ -0,0 +1,189 @@
import {promisify} from 'util';
import fs from 'fs';
import path from 'path';
import {client} from '../../idb.js';
import {PROJECT_ROOT, MAX_SESSION_TIME, TAGS} from '../../R.js';
import {argmaxThresh} from '../../helpers/utils.js';
import pickRandomFile from '../../helpers/pickRandomFile.js';
import Session from '../../models/Session.js';
import express from 'express';
const router = express.Router();
/**
* @typedef {Object} FileMat
* @property {Array<number>} mat Matrix of floats
* @property {number} nums Number of users that have seen this challenge
* @property {number} obj The object in the image
*/
/** @type {FileMat} */
const defaultMat = {
mat: Array(16).fill(0.5),
nums: 0,
obj: 0,
};
/**
* Get file matrix
* @param {string} image
* @return {FileMat}
*/
function getMat(image) {
const mats = JSON.parse(
fs.readFileSync(
path.join(PROJECT_ROOT, 'dev', 'mat.json'), {encoding: 'utf8'}));
return mats[image] || defaultMat;
}
/**
* Update the image mat
* @param {FileMat} imageMat
* @param {Array<number>} userMat
* @param {string} image
*/
function updateMat(imageMat, userMat, image) {
const updatedMat = [...imageMat.mat];
for (let i = 0; i < imageMat.mat.length; i++) {
const delta = 0.1;
if (userMat[i] === 1) {
if (updatedMat[i] + delta > 1) {
updatedMat[i] = 1.0;
} else {
updatedMat[i] += delta;
}
} else {
if (updatedMat[i] < delta) {
updatedMat[i] = 0.0;
} else {
updatedMat[i] -= delta;
}
}
}
console.log('OLD MAT: ', imageMat.mat);
console.log('NEW MAT: ', updatedMat);
imageMat.nums += 1;
imageMat.mat = updatedMat;
const mats = JSON.parse(
fs.readFileSync(
path.join(PROJECT_ROOT, 'dev', 'mat.json'), {encoding: 'utf8'}));
mats[image] = imageMat;
fs.writeFileSync(
path.join(PROJECT_ROOT, 'dev', 'mat.json'),
JSON.stringify(mats, null, 2), {encoding: 'utf8'});
}
/**
* @param {string} sessionId
* @param {Array<number>} userMat
* @return {Promise}
*/
async function verify(sessionId, userMat) {
const redisGet = promisify(client.get);
const session = await redisGet(sessionId)
.then((resp) => {
if (!resp) {
throw new Error('Session expired');
}
/** @type {Session} */
const result = JSON.parse(resp);
let {image, score, websiteKey} = result;
const imageMat = getMat(image);
const trueArgmax = argmaxThresh(imageMat.mat, 0.6).join(',');
const userArgmax = argmaxThresh(userMat, 1).join(',');
updateMat(imageMat, userMat, image);
console.log('trueArgmax', trueArgmax);
console.log('userArgmax', userArgmax);
const delta = imageMat.nums * 0.05;
if (userArgmax !== trueArgmax) {
// TODO: Decrease the score based on mat dispersion
// High variance = small reduction
// Low variance = high reduction
score -= delta;
} else {
score += delta;
}
return pickRandomFile().then((image)=>{
// TODO: Make me dynamic
const newImageTag = 1;
const update = JSON.stringify(
Object.assign(result, {
image: image,
obj: newImageTag,
}));
return promisify(client.setex)(sessionId, MAX_SESSION_TIME, update).then(()=>new Session({
sessionId, websiteKey, imageTag: TAGS[newImageTag],
}));
});
});
}
/**
* @typedef {import('../../models/Session.js').Session} IDBSession
*/
/**
* serveImage
* @param {string} sessionId
* @return {Promise}
*/
export function serveImage(sessionId) {
const redisGet = promisify(client.get);
return redisGet(sessionId)
.then((result) => {
if (!result) {
throw new Error('Session expired');
}
return result;
})
.then((result) => {
/** @type {Session} */
const sessionData = JSON.parse(result);
console.log(sessionData);
const {
// sessionId,
image,
// score,
} = sessionData;
// if (score < 0.1) {
// client.del(sessionId, (err)=>{
// if (err) return reject(err);
// return resolve([
// true,
// null,
// ]);
// });
// }
return [
null,
image + '.jpg',
];
});
}
export default router.post('/verify', async (req, res)=>{
const sessionId = req.body.s;
const mat = req.body.mat;
console.log(req.body);
const session = await verify(sessionId, mat);
res.json(session);
});

View File

@ -1,136 +0,0 @@
import fs from 'fs';
import path from 'path';
import {client} from '../../helpers/idb.js';
import {PROJECT_ROOT, MAX_SESSION_TIME, TAGS} from '../../R.js';
import {argmaxThresh} from '../../helpers/utils.js';
import pickRandomFile from './pickRandomFile.js';
import UserSession from '../../models/UserSession.js';
/** @typedef {import('../../models/IDBSession.js')} IDBSession */
/**
* @typedef {Object} FileMat
* @property {Array<number>} mat Matrix of floats
* @property {number} nums Number of users that have seen this challenge
* @property {number} obj The object in the image
*/
/** @type {FileMat} */
const defaultMat = {
mat: Array(16).fill(0.5),
nums: 0,
obj: 0,
};
/**
* Get file matrix
* @param {string} image
* @return {FileMat}
*/
function getMat(image) {
const mats = JSON.parse(
fs.readFileSync(
path.join(PROJECT_ROOT, 'dev', 'mat.json'), {encoding: 'utf8'}));
return mats[image] || defaultMat;
}
/**
* Update the image mat
* @param {FileMat} imageMat
* @param {Array<number>} userMat
* @param {string} image
*/
function updateMat(imageMat, userMat, image) {
const updatedMat = Object.assign([], imageMat.mat);
for (let i = 0; i < imageMat.mat.length; i++) {
const delta = 0.1;
if (userMat[i] === 1) {
if (updatedMat[i] + delta > 1) {
updatedMat[i] = 1.0;
} else {
updatedMat[i] += delta;
}
} else {
if (updatedMat[i] < delta) {
updatedMat[i] = 0.0;
} else {
updatedMat[i] -= delta;
}
}
}
console.log('OLD MAT: ', imageMat.mat);
console.log('NEW MAT: ', updatedMat);
imageMat.nums += 1;
imageMat.mat = updatedMat;
const mats = JSON.parse(
fs.readFileSync(
path.join(PROJECT_ROOT, 'dev', 'mat.json'), {encoding: 'utf8'}));
mats[image] = imageMat;
fs.writeFileSync(
path.join(PROJECT_ROOT, 'dev', 'mat.json'),
JSON.stringify(mats, null, 2), {encoding: 'utf8'});
}
/**
* @param {string} sessionId
* @param {Array<number>} userMat
* @return {Promise}
*/
export default function(sessionId, userMat) {
return new Promise((resolve, reject)=>{
client.get(sessionId, (err, resp)=>{
if (err) return reject(err);
if (!resp) return reject(new Error('Session expired'));
/** @type {IDBSession} */
const result = JSON.parse(resp);
const imageMat = getMat(result.image);
const trueArgmax = argmaxThresh(imageMat.mat, 0.6).join(',');
const userArgmax = argmaxThresh(userMat, 1).join(',');
updateMat(imageMat, userMat, result.image);
console.log('trueArgmax', trueArgmax);
console.log('userArgmax', userArgmax);
const delta = imageMat.nums * 0.05;
if (userArgmax !== trueArgmax) {
// TODO: Decrease the score based on mat dispersion
// High variance = small reduction
// Low variance = high reduction
result.score -= delta;
} else {
result.score += delta;
}
pickRandomFile().then((image)=>{
// TODO: Make me dynamic
const newImageTag = 1;
const update = JSON.stringify(
Object.assign(result, {
image: image,
obj: newImageTag,
}));
client.setex(sessionId, MAX_SESSION_TIME,
update, (err)=>{
if (err) return reject(err);
const session = new UserSession();
session.setSessionId(sessionId);
session.setWebsiteKey(result.websiteKey);
session.setImageTag(TAGS[newImageTag]);
resolve(session);
});
});
});
});
}

View File

@ -20,7 +20,10 @@ if (process.env.NODE_ENV === 'development') {
});
}
import apis from './routes/api/index.js';
app.use('/api', apis);
//import apis from './routes/api/index.js';
import apiImage from './routes/api/image.js';
import apiVerify from './routes/api/verify.js';
import apiReload from './routes/api/reload.js';
app.use('/api', [apiImage,apiVerify,apiReload]);
export {app};

View File

@ -1,4 +1,4 @@
import {connect as connectToIdb} from './helpers/idb.js';
import {connect as connectToIdb} from './idb.js';
import {app} from './server.js';
import {fetchImagesJob} from './helpers/fetchImagesJob.js';