Add preliminary captcha controller

This commit is contained in:
Alex Gleason 2024-10-03 19:23:22 -05:00
parent 205b9a77fe
commit f83ad0dbce
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
5 changed files with 83 additions and 1 deletions

BIN
captcha/tj-holowaychuk.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

View file

@ -36,6 +36,7 @@
"@b-fuze/deno-dom": "jsr:@b-fuze/deno-dom@^0.1.47", "@b-fuze/deno-dom": "jsr:@b-fuze/deno-dom@^0.1.47",
"@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4",
"@electric-sql/pglite": "npm:@electric-sql/pglite@^0.2.8", "@electric-sql/pglite": "npm:@electric-sql/pglite@^0.2.8",
"@gfx/canvas-wasm": "jsr:@gfx/canvas-wasm@^0.4.2",
"@hono/hono": "jsr:@hono/hono@^4.4.6", "@hono/hono": "jsr:@hono/hono@^4.4.6",
"@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1", "@isaacs/ttlcache": "npm:@isaacs/ttlcache@^1.4.1",
"@lambdalisue/async": "jsr:@lambdalisue/async@^2.1.1", "@lambdalisue/async": "jsr:@lambdalisue/async@^2.1.1",

12
deno.lock generated
View file

@ -5,6 +5,7 @@
"jsr:@b-fuze/deno-dom@^0.1.47": "jsr:@b-fuze/deno-dom@0.1.48", "jsr:@b-fuze/deno-dom@^0.1.47": "jsr:@b-fuze/deno-dom@0.1.48",
"jsr:@bradenmacdonald/s3-lite-client@^0.7.4": "jsr:@bradenmacdonald/s3-lite-client@0.7.6", "jsr:@bradenmacdonald/s3-lite-client@^0.7.4": "jsr:@bradenmacdonald/s3-lite-client@0.7.6",
"jsr:@denosaurs/plug@1.0.3": "jsr:@denosaurs/plug@1.0.3", "jsr:@denosaurs/plug@1.0.3": "jsr:@denosaurs/plug@1.0.3",
"jsr:@gfx/canvas-wasm@^0.4.2": "jsr:@gfx/canvas-wasm@0.4.2",
"jsr:@gleasonator/policy": "jsr:@gleasonator/policy@0.2.0", "jsr:@gleasonator/policy": "jsr:@gleasonator/policy@0.2.0",
"jsr:@gleasonator/policy@0.2.0": "jsr:@gleasonator/policy@0.2.0", "jsr:@gleasonator/policy@0.2.0": "jsr:@gleasonator/policy@0.2.0",
"jsr:@gleasonator/policy@0.4.0": "jsr:@gleasonator/policy@0.4.0", "jsr:@gleasonator/policy@0.4.0": "jsr:@gleasonator/policy@0.4.0",
@ -53,6 +54,7 @@
"jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0",
"jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.2", "jsr:@std/dotenv@^0.224.0": "jsr:@std/dotenv@0.224.2",
"jsr:@std/encoding@0.213.1": "jsr:@std/encoding@0.213.1", "jsr:@std/encoding@0.213.1": "jsr:@std/encoding@0.213.1",
"jsr:@std/encoding@1.0.5": "jsr:@std/encoding@1.0.5",
"jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.3", "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.3",
"jsr:@std/encoding@^0.224.1": "jsr:@std/encoding@0.224.3", "jsr:@std/encoding@^0.224.1": "jsr:@std/encoding@0.224.3",
"jsr:@std/fmt@0.213.1": "jsr:@std/fmt@0.213.1", "jsr:@std/fmt@0.213.1": "jsr:@std/fmt@0.213.1",
@ -138,6 +140,12 @@
"jsr:@std/path@0.213.1" "jsr:@std/path@0.213.1"
] ]
}, },
"@gfx/canvas-wasm@0.4.2": {
"integrity": "d653be3bd12cb2fa9bbe5d1b1f041a81b91d80b68502761204aaf60e4592532a",
"dependencies": [
"jsr:@std/encoding@1.0.5"
]
},
"@gleasonator/policy@0.2.0": { "@gleasonator/policy@0.2.0": {
"integrity": "3fe58b853ab203b2b67e65b64391dbcf5c07bc1caaf46e97b2f8ed5b14f30fdf", "integrity": "3fe58b853ab203b2b67e65b64391dbcf5c07bc1caaf46e97b2f8ed5b14f30fdf",
"dependencies": [ "dependencies": [
@ -470,6 +478,9 @@
"@std/encoding@0.224.3": { "@std/encoding@0.224.3": {
"integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf" "integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf"
}, },
"@std/encoding@1.0.5": {
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
},
"@std/fmt@0.213.1": { "@std/fmt@0.213.1": {
"integrity": "a06d31777566d874b9c856c10244ac3e6b660bdec4c82506cd46be052a1082c3" "integrity": "a06d31777566d874b9c856c10244ac3e6b660bdec4c82506cd46be052a1082c3"
}, },
@ -2135,6 +2146,7 @@
"dependencies": [ "dependencies": [
"jsr:@b-fuze/deno-dom@^0.1.47", "jsr:@b-fuze/deno-dom@^0.1.47",
"jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "jsr:@bradenmacdonald/s3-lite-client@^0.7.4",
"jsr:@gfx/canvas-wasm@^0.4.2",
"jsr:@hono/hono@^4.4.6", "jsr:@hono/hono@^4.4.6",
"jsr:@lambdalisue/async@^2.1.1", "jsr:@lambdalisue/async@^2.1.1",
"jsr:@nostrify/db@^0.35.0", "jsr:@nostrify/db@^0.35.0",

View file

@ -5,6 +5,8 @@ import { logger } from '@hono/hono/logger';
import { NostrEvent, NostrSigner, NStore, NUploader } from '@nostrify/nostrify'; import { NostrEvent, NostrSigner, NStore, NUploader } from '@nostrify/nostrify';
import Debug from '@soapbox/stickynotes/debug'; import Debug from '@soapbox/stickynotes/debug';
import '@/startup.ts';
import { Time } from '@/utils/time.ts'; import { Time } from '@/utils/time.ts';
import { import {
@ -36,6 +38,7 @@ import {
import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts'; import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts';
import { blocksController } from '@/controllers/api/blocks.ts'; import { blocksController } from '@/controllers/api/blocks.ts';
import { bookmarksController } from '@/controllers/api/bookmarks.ts'; import { bookmarksController } from '@/controllers/api/bookmarks.ts';
import { captchaController } from '@/controllers/api/captcha.ts';
import { import {
adminRelaysController, adminRelaysController,
adminSetRelaysController, adminSetRelaysController,
@ -113,7 +116,6 @@ import { errorHandler } from '@/controllers/error.ts';
import { frontendController } from '@/controllers/frontend.ts'; import { frontendController } from '@/controllers/frontend.ts';
import { metricsController } from '@/controllers/metrics.ts'; import { metricsController } from '@/controllers/metrics.ts';
import { indexController } from '@/controllers/site.ts'; import { indexController } from '@/controllers/site.ts';
import '@/startup.ts';
import { manifestController } from '@/controllers/manifest.ts'; import { manifestController } from '@/controllers/manifest.ts';
import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts';
import { nostrController } from '@/controllers/well-known/nostr.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts';
@ -277,6 +279,8 @@ app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysContro
app.post('/api/v1/ditto/names', requireSigner, nameRequestController); app.post('/api/v1/ditto/names', requireSigner, nameRequestController);
app.get('/api/v1/ditto/names', requireSigner, nameRequestsController); app.get('/api/v1/ditto/names', requireSigner, nameRequestsController);
app.get('/api/v1/ditto/captcha', captchaController);
app.get('/api/v1/ditto/zap_splits', getZapSplitsController); app.get('/api/v1/ditto/zap_splits', getZapSplitsController);
app.get('/api/v1/ditto/:id{[0-9a-f]{64}}/zap_splits', statusZapSplitsController); app.get('/api/v1/ditto/:id{[0-9a-f]{64}}/zap_splits', statusZapSplitsController);

View file

@ -0,0 +1,65 @@
import { createCanvas, loadImage } from '@gfx/canvas-wasm';
import { AppController } from '@/app.ts';
export const captchaController: AppController = async (c) => {
const { puzzle, piece, solution } = await generateCaptcha(
await Deno.readFile(new URL('../../../captcha/tj-holowaychuk.jpg', import.meta.url)),
{
cw: 300,
ch: 300,
pw: 50,
ph: 50,
alpha: 0.6,
},
);
return c.json({
puzzle: puzzle.toDataURL(),
piece: piece.toDataURL(),
});
};
interface Point {
x: number;
y: number;
}
async function generateCaptcha(
from: Uint8Array,
opts: {
pw: number;
ph: number;
cw: number;
ch: number;
alpha: number;
},
) {
const { pw, ph, cw, ch, alpha } = opts;
const puzzle = createCanvas(cw, ch);
const ctx = puzzle.getContext('2d');
const image = await loadImage(from);
ctx.drawImage(image, 0, 0, image.width(), image.height(), 0, 0, cw, ch);
const piece = createCanvas(pw, ph);
const pctx = piece.getContext('2d');
const solution = getPieceCoords(puzzle.width, puzzle.height, pw, ph);
pctx.drawImage(puzzle, solution.x, solution.y, pw, ph, 0, 0, pw, ph);
ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
ctx.fillRect(solution.x, solution.y, pw, ph);
return {
puzzle,
piece,
solution,
};
}
function getPieceCoords(cw: number, ch: number, pw: number, ph: number): Point {
// Random x coordinate such that the piece fits within the canvas horizontally
const x = Math.floor(Math.random() * (cw - pw));
// Random y coordinate such that the piece fits within the canvas vertically
const y = Math.floor(Math.random() * (ch - ph));
return { x, y };
}