mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19: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 {
|
||||
type CanvasRenderingContext2D,
|
||||
createCanvas,
|
||||
type EmulatedCanvas2D,
|
||||
type Image,
|
||||
loadImage,
|
||||
} from '@gfx/canvas-wasm';
|
||||
import { createCanvas, type EmulatedCanvas2D } from '@gfx/canvas-wasm';
|
||||
|
||||
export interface CaptchaImages {
|
||||
bgImages: Image[];
|
||||
puzzleMask: Image;
|
||||
puzzleHole: Image;
|
||||
}
|
||||
import { addNoise } from './canvas.ts';
|
||||
import { areIntersecting, type Dimensions, type Point } from './geometry.ts';
|
||||
|
||||
interface Point {
|
||||
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;
|
||||
}
|
||||
import type { CaptchaImages } from './assets.ts';
|
||||
|
||||
/** Generate a puzzle captcha, returning canvases for the board and piece. */
|
||||
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 {
|
||||
return areIntersecting(
|
||||
{ ...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. */
|
||||
function generateSolution(bgSize: Dimensions, puzzleSize: Dimensions): Point {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
"name": "@ditto/captcha",
|
||||
"version": "1.0.0",
|
||||
"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