diff --git a/package-lock.json b/package-lock.json index 2404b80..470954f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ucaptcha", - "version": "0.0.0-pre2", + "version": "0.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/R.js b/src/R.js index bc4c431..243337e 100644 --- a/src/R.js +++ b/src/R.js @@ -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', -] diff --git a/src/helpers/express-router.js b/src/helpers/express-router.js new file mode 100644 index 0000000..fd78d75 --- /dev/null +++ b/src/helpers/express-router.js @@ -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(); + } +} diff --git a/src/routes/api/pickRandomFile.js b/src/helpers/pickRandomFile.js similarity index 89% rename from src/routes/api/pickRandomFile.js rename to src/helpers/pickRandomFile.js index 96127b0..b2de4a7 100644 --- a/src/routes/api/pickRandomFile.js +++ b/src/helpers/pickRandomFile.js @@ -1,5 +1,5 @@ import fs from 'fs'; -import {IMAGES_FOLDER} from '../../R.js'; +import {IMAGES_FOLDER} from '../R.js'; /** * @return {Promise} Image file without extension diff --git a/src/helpers/reloadSession.js b/src/helpers/reloadSession.js new file mode 100644 index 0000000..15ee5c3 --- /dev/null +++ b/src/helpers/reloadSession.js @@ -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} + */ +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; +}; diff --git a/src/helpers/sendJson.js b/src/helpers/sendJson.js index eca3d0d..364aaac 100644 --- a/src/helpers/sendJson.js +++ b/src/helpers/sendJson.js @@ -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'); diff --git a/src/helpers/idb.js b/src/idb.js similarity index 100% rename from src/helpers/idb.js rename to src/idb.js diff --git a/src/models/IDBSession.js b/src/models/IDBSession.js deleted file mode 100644 index 38db360..0000000 --- a/src/models/IDBSession.js +++ /dev/null @@ -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? - */ diff --git a/src/models/Session.js b/src/models/Session.js new file mode 100644 index 0000000..1c317a6 --- /dev/null +++ b/src/models/Session.js @@ -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 \ No newline at end of file diff --git a/src/models/UserSession.js b/src/models/UserSession.js deleted file mode 100644 index 08eed5f..0000000 --- a/src/models/UserSession.js +++ /dev/null @@ -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} - */ - 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} payload - */ - this.deserialize = function([ - sessionId, - websiteKey, - imageTag, - ]) { - this.setSessionId(sessionId); - this.setWebsiteKey(websiteKey); - this.setImageTag(imageTag); - }; -} diff --git a/src/routes/api/image.js b/src/routes/api/image.js new file mode 100644 index 0000000..01fb713 --- /dev/null +++ b/src/routes/api/image.js @@ -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)); +}); diff --git a/src/routes/api/index.js b/src/routes/api/index.js deleted file mode 100644 index 8305748..0000000 --- a/src/routes/api/index.js +++ /dev/null @@ -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; diff --git a/src/routes/api/reload.js b/src/routes/api/reload.js new file mode 100644 index 0000000..3905c73 --- /dev/null +++ b/src/routes/api/reload.js @@ -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); diff --git a/src/routes/api/reloadSession.js b/src/routes/api/reloadSession.js deleted file mode 100644 index c89cd96..0000000 --- a/src/routes/api/reloadSession.js +++ /dev/null @@ -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} - */ -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; -}; diff --git a/src/routes/api/serveImage.js b/src/routes/api/serveImage.js deleted file mode 100644 index 15cbab2..0000000 --- a/src/routes/api/serveImage.js +++ /dev/null @@ -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', - ]); - }); - }); -} diff --git a/src/routes/api/verify.js b/src/routes/api/verify.js new file mode 100644 index 0000000..4de5698 --- /dev/null +++ b/src/routes/api/verify.js @@ -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} 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} 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} 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); +}); diff --git a/src/routes/api/verifyImage.js b/src/routes/api/verifyImage.js deleted file mode 100644 index f589762..0000000 --- a/src/routes/api/verifyImage.js +++ /dev/null @@ -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} 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} 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} 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); - }); - }); - }); - }); -} diff --git a/src/server.js b/src/server.js index d427bfe..8a7dd33 100644 --- a/src/server.js +++ b/src/server.js @@ -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}; diff --git a/src/start.js b/src/start.js index 9f3d85b..1589bd8 100644 --- a/src/start.js +++ b/src/start.js @@ -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';