Added UI for captcha
This commit is contained in:
parent
ef8a4d0e87
commit
5d7536ac45
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/src/server.js"
|
||||
}
|
||||
]
|
||||
}
|
66
dev/mat.json
66
dev/mat.json
|
@ -1,29 +1,65 @@
|
|||
{
|
||||
"zKsAXcOCNd9U": {
|
||||
"mat": [
|
||||
0.0, 0.0, 0.7, 0.7,
|
||||
0.0, 0.0, 0.0, 0.1,
|
||||
0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0
|
||||
0.30000000000000004,
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
0.30000000000000004,
|
||||
0.30000000000000004,
|
||||
0.30000000000000004,
|
||||
0.5,
|
||||
0.10000000000000003,
|
||||
0.10000000000000003,
|
||||
0.10000000000000003,
|
||||
0.10000000000000003,
|
||||
0.10000000000000003,
|
||||
0.10000000000000003,
|
||||
0.10000000000000003,
|
||||
0.10000000000000003
|
||||
],
|
||||
"nums": 2
|
||||
"nums": 6
|
||||
},
|
||||
"eFkYZBMofIVG": {
|
||||
"mat": [
|
||||
0.0, 0.0, 0.8, 0.4,
|
||||
0.0, 0.0, 0.0, 0.4,
|
||||
0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0
|
||||
0.30000000000000004,
|
||||
0.30000000000000004,
|
||||
0.8999999999999999,
|
||||
0.8999999999999999,
|
||||
0.30000000000000004,
|
||||
0.30000000000000004,
|
||||
0.7,
|
||||
1.0999999999999999,
|
||||
-0.09999999999999998,
|
||||
-0.09999999999999998,
|
||||
-0.09999999999999998,
|
||||
-0.09999999999999998,
|
||||
-0.09999999999999998,
|
||||
-0.09999999999999998,
|
||||
-0.09999999999999998,
|
||||
-0.09999999999999998
|
||||
],
|
||||
"nums": 2
|
||||
"nums": 9
|
||||
},
|
||||
"JOAT6FV7VKKi": {
|
||||
"mat": [
|
||||
0.7, 0.6, 0.2, 0.7,
|
||||
1.0, 0.9, 0.5, 0.1,
|
||||
1.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0
|
||||
0.6,
|
||||
0.4,
|
||||
0.20000000000000004,
|
||||
0.9999999999999999,
|
||||
0.6,
|
||||
0.6,
|
||||
0.7999999999999999,
|
||||
0.9999999999999999,
|
||||
-0.19999999999999998,
|
||||
-0.19999999999999998,
|
||||
-0.19999999999999998,
|
||||
-0.19999999999999998,
|
||||
-0.19999999999999998,
|
||||
-0.19999999999999998,
|
||||
-0.19999999999999998,
|
||||
-0.19999999999999998
|
||||
],
|
||||
"nums": 2
|
||||
"nums": 8
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
"main": "src/server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"server": "nodemon src/server.js",
|
||||
"server": "nodemon src/server.js --ignore dev/ --ignore public/ --ignore src/client/",
|
||||
"dev": "NODE_ENV=development; rollup -c -w",
|
||||
"build": "NODE_ENV=production; rollup -c",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
|
|
|
@ -1,29 +1,89 @@
|
|||
import UserSession from '../shared/models/UserSession.js';
|
||||
import initializeSession from './helpers/initializeSession.js';
|
||||
import getImage from './helpers/getImage.js';
|
||||
import verifyImage from './helpers/verifyImage.js';
|
||||
|
||||
/**
|
||||
* Create a uCaptcha box
|
||||
* @param {string} key Website key
|
||||
* @param {string} websiteKey Website key
|
||||
* @return {HTMLElement} uCaptcha box
|
||||
*/
|
||||
function uCaptchaBox(key) {
|
||||
function uCaptchaBox(websiteKey) {
|
||||
const styles = `
|
||||
#ucaptcha-next {
|
||||
border: none;
|
||||
background-color: royalblue;
|
||||
color: #FFF;
|
||||
padding: 10px 20px;
|
||||
text-transform: uppercase;
|
||||
border-radius: 3px;
|
||||
}
|
||||
#ucaptcha-grid {
|
||||
width: 384px;
|
||||
height: 384px;
|
||||
}
|
||||
#ucaptcha-grid td {
|
||||
transition: .1s ease;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#ucaptcha-grid td.selected {
|
||||
padding: 0;
|
||||
border: 10px solid white;
|
||||
}
|
||||
`;
|
||||
const styleTag = createElement('style');
|
||||
styleTag.innerHTML = styles;
|
||||
document.head.appendChild(styleTag);
|
||||
|
||||
const checkbox = createElement('div', {
|
||||
style: 'cursor:pointer;border-radius:3px;border:2px solid #888;width:25px;height:25px;display:inline-block',
|
||||
});
|
||||
checkbox.onclick = function() {
|
||||
fetch(`https://localhost:444/api/init?k=${key}`)
|
||||
.then((r)=>r.text())
|
||||
.then((r)=>r.substr(2))
|
||||
.then((r)=>JSON.parse(r))
|
||||
.then((resp)=>{
|
||||
const session = new UserSession();
|
||||
session.deserialize(resp);
|
||||
|
||||
const captchaBox = createElement('div');
|
||||
captchaBox.appendChild(checkbox);
|
||||
|
||||
const imageContainer = createElement('div', {id: 'ucaptcha-container'});
|
||||
|
||||
const image = createElement('img', {id: 'ucaptcha-img', style: 'position:absolute;display:block;z-index:-999'});
|
||||
imageContainer.appendChild(image);
|
||||
|
||||
const btn = createElement('button', {id: 'ucaptcha-next'});
|
||||
btn.textContent = 'Next';
|
||||
|
||||
captchaBox.appendChild(imageContainer);
|
||||
captchaBox.appendChild(btn);
|
||||
|
||||
checkbox.onclick = async function() {
|
||||
/** @type {import('../shared/models/UserSession.js').UserSession} */
|
||||
const session = await initializeSession(websiteKey);
|
||||
|
||||
const imageGrid = createElement('table', {id: 'ucaptcha-grid'});
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const tr = createElement('tr');
|
||||
for (let ii = 0; ii < 4; ii++) {
|
||||
const td = createElement('td',
|
||||
{'style': 'cursor:pointer'});
|
||||
td.addEventListener('click', (e)=>{
|
||||
e.target.classList.toggle('selected');
|
||||
});
|
||||
tr.appendChild(td);
|
||||
}
|
||||
imageGrid.appendChild(tr);
|
||||
}
|
||||
|
||||
btn.addEventListener('click', (e)=>{
|
||||
verifyImage(session, imageGrid);
|
||||
getImage(session, captchaBox);
|
||||
});
|
||||
|
||||
imageContainer.appendChild(imageGrid);
|
||||
|
||||
getImage(session, captchaBox);
|
||||
|
||||
checkbox.setAttribute('style',
|
||||
checkbox.getAttribute('style') + 'background-color:royalblue;');
|
||||
};
|
||||
const captchaBox = createElement('div');
|
||||
captchaBox.appendChild(checkbox);
|
||||
return captchaBox;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import request from './util/request.js';
|
||||
import UserSession from '../../shared/models/UserSession.js';
|
||||
|
||||
/**
|
||||
* Instantiate the uCaptcha loop
|
||||
* @param {UserSession} session
|
||||
* @param {HTMLElement} captchaBox
|
||||
*/
|
||||
export default function instantiateLoop(session, captchaBox) {
|
||||
console.log(captchaBox);
|
||||
request(`/api/image?s=${session.sessionId}`, {responseType: 'blob'})
|
||||
.then((blob)=>{
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
console.log(imageUrl);
|
||||
captchaBox
|
||||
.querySelector('#ucaptcha-container')
|
||||
.querySelector('#ucaptcha-img')
|
||||
.setAttribute('src', imageUrl);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import request from './util/request.js';
|
||||
import UserSession from '../../shared/models/UserSession.js';
|
||||
|
||||
/**
|
||||
* @param {string} websiteKey
|
||||
* @return {Promise<UserSession>}
|
||||
*/
|
||||
export default function initializeSession(websiteKey) {
|
||||
return new Promise((resolve, reject)=>{
|
||||
request(`/api/init?k=${websiteKey}`)
|
||||
.then((resp)=>{
|
||||
console.log(resp);
|
||||
const session = new UserSession();
|
||||
session.deserialize(resp);
|
||||
resolve(session);
|
||||
})
|
||||
.catch((e)=>{
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* @typedef {Object} RequestOptions
|
||||
* @property {Object<string, any>} [body] Request body
|
||||
* @property {string} [method] Request method
|
||||
* @property {XMLHttpRequestResponseType} [responseType] Return raw response from server
|
||||
*/
|
||||
|
||||
/** @type {RequestOptions} */
|
||||
const defaultOptions = {
|
||||
method: 'GET',
|
||||
responseType: 'text', // Text because we will need to substr it.
|
||||
};
|
||||
|
||||
/**
|
||||
* Make an HTTP request
|
||||
* @param {string} url
|
||||
* @param {RequestOptions} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
export default function(url, options) {
|
||||
console.log(options);
|
||||
options = Object.assign({}, defaultOptions, options);
|
||||
console.log(options);
|
||||
return new Promise((resolve, reject)=>{
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open(options.method, url);
|
||||
|
||||
xhr.responseType = options.responseType;
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status < 400) {
|
||||
if (options.responseType !== 'text') {
|
||||
resolve(this.response);
|
||||
} else {
|
||||
resolve(JSON.parse(this.responseText.substr(2)));
|
||||
}
|
||||
} else {
|
||||
reject(JSON.parse(this.responseText));
|
||||
}
|
||||
};
|
||||
|
||||
if (options.method === 'POST') {
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.send(JSON.stringify(options.body));
|
||||
} else {
|
||||
xhr.send();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import request from './util/request.js';
|
||||
import UserSession from '../../shared/models/UserSession.js';
|
||||
|
||||
/**
|
||||
* Verify the image
|
||||
* @param {UserSession} session
|
||||
* @param {HTMLElement} captchaGrid
|
||||
*/
|
||||
export default async function(session, captchaGrid) {
|
||||
const tds = captchaGrid.querySelectorAll('td');
|
||||
const selectedTds = [];
|
||||
|
||||
const mat = new Array(tds.length);
|
||||
for (let i = 0; i < tds.length; i++) {
|
||||
if (tds[i].classList.contains('selected')) {
|
||||
mat[i] = 1;
|
||||
selectedTds.push(tds[i]);
|
||||
} else {
|
||||
mat[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const body = {
|
||||
s: session.sessionId,
|
||||
mat,
|
||||
};
|
||||
|
||||
await request('/api/verify', {method: 'POST', body});
|
||||
|
||||
selectedTds.map((td)=>{
|
||||
td.classList.remove('selected');
|
||||
});
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
* 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} score How likely is the user to be a human?
|
||||
*/
|
||||
|
|
|
@ -6,19 +6,26 @@ import serveImage from './serveImage.js';
|
|||
import verifyImage from './verifyImage.js';
|
||||
|
||||
import sendJson from '../../helpers/sendJson.js';
|
||||
import {IMAGES_FOLDER} from '../../R.js';
|
||||
import {IMAGES_FOLDER, MAX_SESSION_TIME} from '../../R.js';
|
||||
|
||||
const router = new express.Router();
|
||||
|
||||
router.get('/init', async (req, res)=>{
|
||||
const websiteKey = req.query.k;
|
||||
if (!websiteKey) {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const websiteKey = req.query.k;
|
||||
if (!websiteKey) {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await initializeSession(websiteKey);
|
||||
sendJson(res, result);
|
||||
const session = await initializeSession(websiteKey, req.cookies);
|
||||
res.cookie(websiteKey, session.sessionId,
|
||||
{httpOnly: true, maxAge: MAX_SESSION_TIME * 1000});
|
||||
sendJson(res, session.serialize());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/image', async (req, res)=>{
|
||||
|
@ -40,8 +47,12 @@ router.get('/image', async (req, res)=>{
|
|||
router.post('/verify', async (req, res)=>{
|
||||
const sessionId = req.body.s;
|
||||
const mat = req.body.mat;
|
||||
console.log(req.body);
|
||||
|
||||
await verifyImage(sessionId, mat);
|
||||
sendJson(res, {
|
||||
ok: 1,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -14,19 +14,34 @@ import {MAX_SESSION_TIME} from '../../R.js';
|
|||
|
||||
/**
|
||||
* @param {string} websiteKey
|
||||
* @return {any}
|
||||
* @param {Object} cookies
|
||||
* @return {UserSession}
|
||||
*/
|
||||
export default async function initializeSession(websiteKey) {
|
||||
const randomSessionId = randomBytes(8);
|
||||
export default async function initializeSession(websiteKey, cookies) {
|
||||
let randomSessionId;
|
||||
|
||||
if (cookies[websiteKey]) {
|
||||
randomSessionId = cookies[websiteKey];
|
||||
|
||||
const session = new UserSession();
|
||||
session.sessionId = randomSessionId;
|
||||
session.websiteKey = websiteKey;
|
||||
|
||||
return session;
|
||||
} else {
|
||||
randomSessionId = randomBytes(8);
|
||||
}
|
||||
|
||||
const image = await pickRandomFile();
|
||||
|
||||
const session = new UserSession();
|
||||
session.sessionId = randomSessionId;
|
||||
const userPayload = session.serialize();
|
||||
session.websiteKey = websiteKey;
|
||||
|
||||
/** @type {IDBSession} */
|
||||
const idbPayload = {
|
||||
sessionId: session.sessionId,
|
||||
websiteKey: session.websiteKey,
|
||||
image,
|
||||
score: 0.5,
|
||||
};
|
||||
|
@ -34,5 +49,5 @@ export default async function initializeSession(websiteKey) {
|
|||
client.setex(
|
||||
session.sessionId, MAX_SESSION_TIME, JSON.stringify(idbPayload));
|
||||
|
||||
return userPayload;
|
||||
return session;
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ import {IMAGES_FOLDER} from '../../R.js';
|
|||
* @return {Promise<string>} Image file without extension
|
||||
*/
|
||||
export default () => {
|
||||
return new Promise((reject, resolve)=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
fs.readdir(IMAGES_FOLDER, (err, files)=>{
|
||||
if (err) return reject(err);
|
||||
return resolve(
|
||||
|
|
|
@ -11,6 +11,7 @@ import {client} from '../../helpers/idb.js';
|
|||
export default function(sessionId) {
|
||||
return new Promise((resolve, reject)=>{
|
||||
client.get(sessionId, (err, result)=>{
|
||||
// FIXME: `result` is sometimes null on a perfect query
|
||||
if (err) return reject(err);
|
||||
|
||||
/** @type {IDBSession} */
|
||||
|
|
|
@ -16,20 +16,53 @@ import pickRandomFile from './pickRandomFile.js';
|
|||
* @param {string} filename
|
||||
* @return {FileMat}
|
||||
*/
|
||||
function getMat(filename) {
|
||||
function getMat(image) {
|
||||
const mats = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(PROJECT_ROOT, 'dev', 'mat.json'), {encoding: 'utf8'}));
|
||||
|
||||
return mats[filename];
|
||||
return mats[image];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
updatedMat[i] += delta;
|
||||
} 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>} mat
|
||||
* @param {Array<number>} userMat
|
||||
* @return {Promise}
|
||||
*/
|
||||
export default function(sessionId, mat) {
|
||||
export default function(sessionId, userMat) {
|
||||
return new Promise((resolve, reject)=>{
|
||||
client.get(sessionId, (err, result)=>{
|
||||
if (err) return reject(err);
|
||||
|
@ -38,19 +71,29 @@ export default function(sessionId, mat) {
|
|||
result = JSON.parse(result);
|
||||
|
||||
const imageMat = getMat(result.image);
|
||||
const trueArgmax = argmaxThresh(imageMat.mat, 0.2).join(',');
|
||||
const userArgmax = argmaxThresh(mat, 1).join(',');
|
||||
const trueArgmax = argmaxThresh(imageMat.mat, 0.3).join(',');
|
||||
const userArgmax = argmaxThresh(userMat, 1).join(',');
|
||||
|
||||
updateMat(imageMat, userMat, result.image);
|
||||
|
||||
console.log('trueArgmax', trueArgmax);
|
||||
console.log('userArgmax', userArgmax);
|
||||
|
||||
if (userArgmax !== trueArgmax) {
|
||||
// TODO: Decrease the score based on mat dispersion
|
||||
// High variance = small reduction
|
||||
// Low variance = high reduction
|
||||
result.score -= 0.2;
|
||||
} else {
|
||||
result.score += 0.2;
|
||||
}
|
||||
|
||||
pickRandomFile().then((image)=>{
|
||||
const update = JSON.stringify(
|
||||
Object.assign(result, {image: image}));
|
||||
|
||||
client.setex(sessionId, MAX_SESSION_TIME,
|
||||
Object.assign(result, {image: image}), (err)=>{
|
||||
update, (err)=>{
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* User session model
|
||||
* @type {Object} Session Object.
|
||||
* @property {string} _sessionId The session identifier
|
||||
* @property {string} _websiteKey The website id for session
|
||||
*/
|
||||
export class UserSession {
|
||||
/**
|
||||
|
@ -9,7 +10,6 @@ export class UserSession {
|
|||
* @param {string} id
|
||||
*/
|
||||
set sessionId(id) {
|
||||
if (id.length !== 8) throw Error('ClientID is not 8 characters');
|
||||
this._sessionId = id;
|
||||
}
|
||||
|
||||
|
@ -21,24 +21,40 @@ export class UserSession {
|
|||
return this._sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set session ID
|
||||
* @param {string} id
|
||||
*/
|
||||
set websiteKey(id) {
|
||||
this._websiteKey = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session ID
|
||||
* @type {string}
|
||||
*/
|
||||
get websiteKey() {
|
||||
return this._websiteKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the session into a JSON object
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
serialize() {
|
||||
const {sessionId} = this;
|
||||
if (!sessionId) {
|
||||
throw Error('Unable to serialize because of missing field(s)');
|
||||
const {sessionId, websiteKey} = this;
|
||||
if (!sessionId || !websiteKey) {
|
||||
throw Error();
|
||||
}
|
||||
return [sessionId];
|
||||
return [sessionId, websiteKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize session data from JSON object
|
||||
* @param {Array<any>} payload
|
||||
*/
|
||||
deserialize([sessionId]) {
|
||||
Object.assign(this, {sessionId});
|
||||
deserialize([sessionId, websiteKey]) {
|
||||
Object.assign(this, {sessionId, websiteKey});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue