mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Break captcha package into smaller modules
This commit is contained in:
parent
a2732642a5
commit
5f617b2d1a
6 changed files with 86 additions and 84 deletions
36
packages/captcha/assets.ts
Normal file
36
packages/captcha/assets.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { type Image, loadImage } from '@gfx/canvas-wasm';
|
||||||
|
|
||||||
|
export interface CaptchaImages {
|
||||||
|
bgImages: Image[];
|
||||||
|
puzzleMask: Image;
|
||||||
|
puzzleHole: Image;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCaptchaImages(): Promise<CaptchaImages> {
|
||||||
|
const bgImages = await getBackgroundImages();
|
||||||
|
|
||||||
|
const puzzleMask = await loadImage(
|
||||||
|
await Deno.readFile(new URL('./assets/puzzle/puzzle-mask.png', import.meta.url)),
|
||||||
|
);
|
||||||
|
const puzzleHole = await loadImage(
|
||||||
|
await Deno.readFile(new URL('./assets/puzzle/puzzle-hole.png', import.meta.url)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { bgImages, puzzleMask, puzzleHole };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBackgroundImages(): Promise<Image[]> {
|
||||||
|
const path = new URL('./assets/bg/', import.meta.url);
|
||||||
|
|
||||||
|
const images: Image[] = [];
|
||||||
|
|
||||||
|
for await (const dirEntry of Deno.readDir(path)) {
|
||||||
|
if (dirEntry.isFile && dirEntry.name.endsWith('.jpg')) {
|
||||||
|
const file = await Deno.readFile(new URL(dirEntry.name, path));
|
||||||
|
const image = await loadImage(file);
|
||||||
|
images.push(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return images;
|
||||||
|
}
|
||||||
21
packages/captcha/canvas.ts
Normal file
21
packages/captcha/canvas.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { CanvasRenderingContext2D } from '@gfx/canvas-wasm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a small amount of noise to the image.
|
||||||
|
* This protects against an attacker pregenerating every possible solution and then doing a reverse-lookup.
|
||||||
|
*/
|
||||||
|
export function addNoise(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
||||||
|
const imageData = ctx.getImageData(0, 0, width, height);
|
||||||
|
|
||||||
|
// Loop over every pixel.
|
||||||
|
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||||
|
// Add/subtract a small amount from each color channel.
|
||||||
|
// We skip i+3 because that's the alpha channel, which we don't want to modify.
|
||||||
|
for (let j = 0; j < 3; j++) {
|
||||||
|
const alteration = Math.floor(Math.random() * 11) - 5; // Vary between -5 and +5
|
||||||
|
imageData.data[i + j] = Math.min(Math.max(imageData.data[i + j] + alteration, 0), 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
|
@ -1,57 +1,9 @@
|
||||||
import {
|
import { createCanvas, type EmulatedCanvas2D } from '@gfx/canvas-wasm';
|
||||||
type CanvasRenderingContext2D,
|
|
||||||
createCanvas,
|
|
||||||
type EmulatedCanvas2D,
|
|
||||||
type Image,
|
|
||||||
loadImage,
|
|
||||||
} from '@gfx/canvas-wasm';
|
|
||||||
|
|
||||||
export interface CaptchaImages {
|
import { addNoise } from './canvas.ts';
|
||||||
bgImages: Image[];
|
import { areIntersecting, type Dimensions, type Point } from './geometry.ts';
|
||||||
puzzleMask: Image;
|
|
||||||
puzzleHole: Image;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Point {
|
import type { CaptchaImages } from './assets.ts';
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Dimensions {
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Rectangle = Point & Dimensions;
|
|
||||||
|
|
||||||
export async function getCaptchaImages(): Promise<CaptchaImages> {
|
|
||||||
const bgImages = await getBackgroundImages();
|
|
||||||
|
|
||||||
const puzzleMask = await loadImage(
|
|
||||||
await Deno.readFile(new URL('./assets/puzzle/puzzle-mask.png', import.meta.url)),
|
|
||||||
);
|
|
||||||
const puzzleHole = await loadImage(
|
|
||||||
await Deno.readFile(new URL('./assets/puzzle/puzzle-hole.png', import.meta.url)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { bgImages, puzzleMask, puzzleHole };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getBackgroundImages(): Promise<Image[]> {
|
|
||||||
const path = new URL('./assets/bg/', import.meta.url);
|
|
||||||
|
|
||||||
const images: Image[] = [];
|
|
||||||
|
|
||||||
for await (const dirEntry of Deno.readDir(path)) {
|
|
||||||
if (dirEntry.isFile && dirEntry.name.endsWith('.jpg')) {
|
|
||||||
const file = await Deno.readFile(new URL(dirEntry.name, path));
|
|
||||||
const image = await loadImage(file);
|
|
||||||
images.push(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return images;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Generate a puzzle captcha, returning canvases for the board and piece. */
|
/** Generate a puzzle captcha, returning canvases for the board and piece. */
|
||||||
export function generateCaptcha(
|
export function generateCaptcha(
|
||||||
|
|
@ -92,26 +44,6 @@ export function generateCaptcha(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a small amount of noise to the image.
|
|
||||||
* This protects against an attacker pregenerating every possible solution and then doing a reverse-lookup.
|
|
||||||
*/
|
|
||||||
function addNoise(ctx: CanvasRenderingContext2D, width: number, height: number): void {
|
|
||||||
const imageData = ctx.getImageData(0, 0, width, height);
|
|
||||||
|
|
||||||
// Loop over every pixel.
|
|
||||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
|
||||||
// Add/subtract a small amount from each color channel.
|
|
||||||
// We skip i+3 because that's the alpha channel, which we don't want to modify.
|
|
||||||
for (let j = 0; j < 3; j++) {
|
|
||||||
const alteration = Math.floor(Math.random() * 11) - 5; // Vary between -5 and +5
|
|
||||||
imageData.data[i + j] = Math.min(Math.max(imageData.data[i + j] + alteration, 0), 255);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.putImageData(imageData, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyCaptchaSolution(puzzleSize: Dimensions, point: Point, solution: Point): boolean {
|
export function verifyCaptchaSolution(puzzleSize: Dimensions, point: Point, solution: Point): boolean {
|
||||||
return areIntersecting(
|
return areIntersecting(
|
||||||
{ ...point, ...puzzleSize },
|
{ ...point, ...puzzleSize },
|
||||||
|
|
@ -119,17 +51,6 @@ export function verifyCaptchaSolution(puzzleSize: Dimensions, point: Point, solu
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function areIntersecting(rect1: Rectangle, rect2: Rectangle, threshold = 0.5): boolean {
|
|
||||||
const r1cx = rect1.x + rect1.w / 2;
|
|
||||||
const r2cx = rect2.x + rect2.w / 2;
|
|
||||||
const r1cy = rect1.y + rect1.h / 2;
|
|
||||||
const r2cy = rect2.y + rect2.h / 2;
|
|
||||||
const dist = Math.sqrt((r2cx - r1cx) ** 2 + (r2cy - r1cy) ** 2);
|
|
||||||
const e1 = Math.sqrt(rect1.h ** 2 + rect1.w ** 2) / 2;
|
|
||||||
const e2 = Math.sqrt(rect2.h ** 2 + rect2.w ** 2) / 2;
|
|
||||||
return dist < (e1 + e2) * threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Random coordinates such that the piece fits within the canvas. */
|
/** Random coordinates such that the piece fits within the canvas. */
|
||||||
function generateSolution(bgSize: Dimensions, puzzleSize: Dimensions): Point {
|
function generateSolution(bgSize: Dimensions, puzzleSize: Dimensions): Point {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@
|
||||||
"name": "@ditto/captcha",
|
"name": "@ditto/captcha",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./captcha.ts"
|
".": "./mod.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
packages/captcha/geometry.ts
Normal file
22
packages/captcha/geometry.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
export interface Point {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Dimensions {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Rectangle = Point & Dimensions;
|
||||||
|
|
||||||
|
export function areIntersecting(rect1: Rectangle, rect2: Rectangle, threshold = 0.5): boolean {
|
||||||
|
const r1cx = rect1.x + rect1.w / 2;
|
||||||
|
const r2cx = rect2.x + rect2.w / 2;
|
||||||
|
const r1cy = rect1.y + rect1.h / 2;
|
||||||
|
const r2cy = rect2.y + rect2.h / 2;
|
||||||
|
const dist = Math.sqrt((r2cx - r1cx) ** 2 + (r2cy - r1cy) ** 2);
|
||||||
|
const e1 = Math.sqrt(rect1.h ** 2 + rect1.w ** 2) / 2;
|
||||||
|
const e2 = Math.sqrt(rect2.h ** 2 + rect2.w ** 2) / 2;
|
||||||
|
return dist < (e1 + e2) * threshold;
|
||||||
|
}
|
||||||
2
packages/captcha/mod.ts
Normal file
2
packages/captcha/mod.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { getCaptchaImages } from './assets.ts';
|
||||||
|
export { generateCaptcha, verifyCaptchaSolution } from './captcha.ts';
|
||||||
Loading…
Add table
Reference in a new issue