最近、クローラーにおける CAPTCHA および Canvas に関連するものを研究しており、簡単にコードスニペットを記録します。
{
const getImageData = CanvasRenderingContext2D.prototype.getImageData;
//
const noisify = function (canvas, context) {
if (context) {
const shift = {
'r': Math.floor(Math.random() * 10) - 5,
'g': Math.floor(Math.random() * 10) - 5,
'b': Math.floor(Math.random() * 10) - 5,
'a': Math.floor(Math.random() * 10) - 5
};
//
const width = canvas.width;
const height = canvas.height;
//
if (width && height) {
const imageData = getImageData.apply(context, [0, 0, width, height]);
//
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
const n = ((i * (width * 4)) + (j * 4));
imageData.data[n + 0] = imageData.data[n + 0] + shift.r;
imageData.data[n + 1] = imageData.data[n + 1] + shift.g;
imageData.data[n + 2] = imageData.data[n + 2] + shift.b;
imageData.data[n + 3] = imageData.data[n + 3] + shift.a;
}
}
//
window.top.postMessage("canvas-defender-alert", '*');
context.putImageData(imageData, 0, 0);
}
}
};
//
HTMLCanvasElement.prototype.toBlob = new Proxy(HTMLCanvasElement.prototype.toBlob, {
apply(target, self, args) {
noisify(self, self.getContext("2d"));
//
return Reflect.apply(target, self, args);
}
});
//
HTMLCanvasElement.prototype.toDataURL = new Proxy(HTMLCanvasElement.prototype.toDataURL, {
apply(target, self, args) {
noisify(self, self.getContext("2d"));
//
return Reflect.apply(target, self, args);
}
});
//
CanvasRenderingContext2D.prototype.getImageData = new Proxy(CanvasRenderingContext2D.prototype.getImageData, {
apply(target, self, args) {
noisify(self.canvas, self);
//
return Reflect.apply(target, self, args);
}
});
}
{
const mkey = "canvas-defender-sandboxed-frame";
document.documentElement.setAttribute(mkey, '');
//
window.addEventListener("message", function (e) {
if (e.data && e.data === mkey) {
e.preventDefault();
e.stopPropagation();
//
if (e.source) {
if (e.source.CanvasRenderingContext2D) {
e.source.CanvasRenderingContext2D.prototype.getImageData = CanvasRenderingContext2D.prototype.getImageData;
}
//
if (e.source.HTMLCanvasElement) {
e.source.HTMLCanvasElement.prototype.toBlob = HTMLCanvasElement.prototype.toBlob;
e.source.HTMLCanvasElement.prototype.toDataURL = HTMLCanvasElement.prototype.toDataURL;
}
}
}
}, false);
}
この JavaScript コードは Canvas 要素を処理するためのものです。以下はコードの説明です。
第一部:
const getImageData = CanvasRenderingContext2D.prototype.getImageData;
この行のコードは、getImageData
という名前の変数を作成し、CanvasRenderingContext2D.prototype.getImageData
の参照をそれに割り当てます。これにより、後のコードで getImageData
変数を使って CanvasRenderingContext2D
プロトタイプの getImageData
メソッドを呼び出すことができます。
次に、noisify
という関数があり、この関数は Canvas にノイズ効果を追加するためのものです。次のコードは noisify
関数の定義と実装です。
noisify
関数内部には以下のロジックがあります:
- まず、渡されたパラメータ
canvas
とcontext
を使って判断します。 - 次に、ノイズの色のオフセットを制御するためのランダム数を含む
shift
オブジェクトを生成します。 - その後、渡された Canvas の幅と高さを取得します。
- 幅と高さが存在する場合、
getImageData
メソッドを使って Canvas 上のピクセルデータを取得し、imageData
変数に保存します。 - 次に、ネストされたループを使って各ピクセルを走査し、
shift
オブジェクト内のランダム数に基づいてピクセルの色値をオフセットします。 - すべてのピクセルを処理した後、"canvas-defender-alert" イベントをトリガーするためにトップウィンドウにメッセージを送信します。
- 最後に、
context.putImageData(imageData, 0, 0)
を使って処理されたピクセルデータを再び Canvas に描画します。
第二部:
HTMLCanvasElement.prototype.toBlob = new Proxy(HTMLCanvasElement.prototype.toBlob, {
apply(target, self, args) {
noisify(self, self.getContext("2d"));
return Reflect.apply(target, self, args);
}
});
HTMLCanvasElement.prototype.toDataURL = new Proxy(HTMLCanvasElement.prototype.toDataURL, {
apply(target, self, args) {
noisify(self, self.getContext("2d"));
return Reflect.apply(target, self, args);
}
});
CanvasRenderingContext2D.prototype.getImageData = new Proxy(CanvasRenderingContext2D.prototype.getImageData, {
apply(target, self, args) {
noisify(self.canvas, self);
return Reflect.apply(target, self, args);
}
});
この部分のコードは、toBlob
、toDataURL
、getImageData
の 3 つのメソッドを再定義し、Proxy
を使用して代理処理を行います。これらのメソッドを代理することで、呼び出す際に自動的に noisify
関数を呼び出して Canvas にノイズ効果を追加します。
第三部:
const mkey = "canvas-defender-sandboxed-frame";
document.documentElement.setAttribute(mkey, '');
window.addEventListener("message", function (e) {
if (e.data && e.data === mkey) {
e.preventDefault();
e.stopPropagation();
if (e.source) {
if (e.source.CanvasRenderingContext2D) {
e.source.CanvasRenderingContext2D.prototype.getImageData = CanvasRenderingContext2D.prototype.getImageData;
}
if (e.source.HTMLCanvasElement) {
e.source.HTMLCanvasElement.prototype.toBlob = HTMLCanvasElement.prototype.toBlob;
e.source.HTMLCanvasElement.prototype.toDataURL = HTMLCanvasElement.prototype.toDataURL;
}
}
}
}, false);
この部分のコードは、メッセージをリッスンするためのイベントリスナーを定義します。特定のメッセージ mkey
を受信したときに、一連の操作を実行します。具体的には:
- まず、カスタム属性
mkey
をページのルート要素(document.documentElement
)に追加します。 - 次に、
"message"
イベントのリスナーを登録し、受信したメッセージを処理するための条件判断を行います。 - メッセージが
mkey
と等しい場合、デフォルトの動作とイベントの伝播を防ぎます。 - 次に、
e.source
を通じてメッセージの送信元ウィンドウオブジェクトにアクセスし、そのプロトタイプにCanvasRenderingContext2D
とHTMLCanvasElement
のメソッドがあるかどうかを確認します。 - もしあれば、対応するメソッドを以前に代理されたメソッドとして再定義し、これらのメソッドの呼び出しが
noisify
処理を経ることを保証します。
要するに、この JavaScript コードは Canvas 要素にノイズ効果を追加する機能を実現し、特定の条件下で Canvas 関連のメソッドを再定義および代理処理します。