aboutsummaryrefslogtreecommitdiffstats
path: root/src/scripts/common/login/captcha.ts
blob: a63aa24850b7670da7a124b87ee6d7c23bbba553 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { observeDOM } from "@/shared/observe-dom";
import { OCRRequest, OCRResponse } from "@/shared/ocr-request";

/**
 * Pre-processes a CAPTCHA image on a canvas by removing grayish
 * background noise while preserving dark pixels, producing a clean
 * binary image suitable for OCR.
 *
 * @param ctx    - The 2D rendering context of the canvas.
 * @param width  - Canvas width in pixels.
 * @param height - Canvas height in pixels.
 */
export function prepareCaptcha(ctx: CanvasRenderingContext2D, width: number, height: number): void {
	const imgData = ctx.getImageData(0, 0, width, height);
	const data = imgData.data;
	const radius = 1;

	/** Returns true when all three channels are below 50 (very dark). */
	function isBlack(r: number, g: number, b: number): boolean {
		return r < 50 && g < 50 && b < 50;
	}

	/** Returns true when the colour is a mid-range grey (no strong hue). */
	function isGrayish(r: number, g: number, b: number): boolean {
		return Math.abs(r - g) < 15 && Math.abs(g - b) < 15 && r > 100 && r < 200;
	}

	/**
	 * Checks whether a black pixel exists within `radius` pixels of (x, y).
	 * Used to preserve dark structures when removing grey noise.
	 */
	function hasNearbyBlack(x: number, y: number): boolean {
		for (let dx = -radius; dx <= radius; dx++) {
			for (let dy = -radius; dy <= radius; dy++) {
				const nx = x + dx;
				const ny = y + dy;
				if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
					const i = (ny * width + nx) * 4;
					if (isBlack(data[i], data[i + 1], data[i + 2])) return true;
				}
			}
		}
		return false;
	}

	for (let y = 0; y < height; y++) {
		for (let x = 0; x < width; x++) {
			const i = (y * width + x) * 4;
			const [r, g, b] = [data[i], data[i + 1], data[i + 2]];
			if (isGrayish(r, g, b) && !hasNearbyBlack(x, y)) {
				data[i] = data[i + 1] = data[i + 2] = 255;
			}
		}
	}
	for (let y = 0; y < height; y++) {
		for (let x = 0; x < width; x++) {
			const i = (y * width + x) * 4;
			const [r, g, b] = [data[i], data[i + 1], data[i + 2]];

			if (isGrayish(r, g, b)) {
				data[i] = data[i + 1] = data[i + 2] = 0; // make black
			}
		}
	}
	ctx.putImageData(imgData, 0, 0);
}

observeDOM(() => {
	const loginFormElement = document.querySelector<HTMLFormElement>("#loginform");

	if (loginFormElement) {
		const captchaImg = loginFormElement.querySelector<HTMLImageElement>("#captchaImg");
		const captchaInput = loginFormElement.querySelector<HTMLInputElement>("#txtInput");

		if (!captchaImg || !captchaInput)
			return false;

		// Wait for image to load to get correct dimensions
		captchaImg.addEventListener("load", function(): void {
			const { width, height } = captchaImg;

			// 1. Create canvas
			const canvas = document.createElement("canvas");
			canvas.width = width;
			canvas.height = height;
			canvas.style.display = "block";

			// 2. Copy styles (optional)
			canvas.style.cssText = getComputedStyle(captchaImg).cssText;
			canvas.style.display = "none";

			// 3. Insert canvas before image
			captchaImg.parentNode?.querySelectorAll("canvas")
				.forEach((c) => { c.remove(); });
			captchaImg.parentNode?.insertBefore(canvas, captchaImg);

			// 4. Hide image
			captchaImg.style.display = "none";

			// 5. Draw the image onto the canvas
			const ctx = canvas.getContext("2d");
			if (!ctx)
				return;
			ctx.drawImage(captchaImg, 0, 0);
			prepareCaptcha(ctx, canvas.width, canvas.height);
			canvas.style.display = "";
			const dataURL = canvas.toDataURL("image/png");

			// Perform OCR
			chrome.runtime.sendMessage<OCRRequest, OCRResponse>(
				{ type: "OCR", dataURL },
				function(ocrResponse): void {
					if (ocrResponse.success) {
						const textResult = ocrResponse.text.trim();
						if (ocrResponse.confidence < 80 || textResult.length !== 6)
							// Reset the captcha if confidence is low
							captchaImg.src = `generateCaptcha?${new Date().getTime().toString()}`;
						else
							captchaInput.value = textResult;
					}
				}
			);
		});
	}

	// This runs forever
	return false;
});