19日に更新してた

アフィリエイトはないよ

【golang】puppeteer-stealth もどきを chromedp でやってみた

chatGPT に描いてもらった【golang】puppeteer-stealth もどきを chromedp でやってみた の画像

はじめに

プライバシー対策として、python の puppeteer-stealth もどきを chromedp でやってみようかと思ったのですが何から手を付けていいものかわからないため、chatGPT に聞いてみました。

すると、このサイトで通るようにしましょうとのことだったので、とりあえずアクセスしてスクリーンショットを取るようにして問題点を一つづつ潰していきました。
bot.sannysoft.com

やったこと

  • navigator.webdriver を削除
  • languages を偽装
  • plugins を偽装
  • defineProperty は一部のセキュリティソフト*1により挙動が監視されやすいため、挙動検証を目的として代替手法を試行、直接代入する
  • permissions.query の notifications 対応
  • headless 時にユーザーエージェントの指定

コード中のコメントそのままですが、こんな感じです。
Headless モードで必要なユーザーエージェントは chrome のバージョンが 137 系でテストしたため以下のようにしております。
Headed モードでは opts の後半の段落の記載の必要はありません。

ソース

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/chromedp/cdproto/page"
	"github.com/chromedp/chromedp"
)

func main() {
	// 起動オプション
	opts := append(chromedp.DefaultExecAllocatorOptions[:],
		chromedp.NoFirstRun,
		chromedp.NoDefaultBrowserCheck,
		chromedp.Flag("disable-extensions", false),
		chromedp.Flag("enable-automation", false),

		chromedp.Flag("headless", true),
		chromedp.WindowSize(1280, 800),
		chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"),
	)

	var buf []byte
	// Chromeコンテキスト初期化
	allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
	defer cancel()

	startTime := time.Now()
	ctx, cancelurl := chromedp.NewContext(allocCtx)
	defer cancelurl()

	// ステルススクリプト(全自動化検出対策)
	stealthJS := `(() => {
		// navigator.webdriver を削除
		delete Navigator.prototype.webdriver;


		window.chrome = {
			runtime: {},
		};

		// languages を偽装
		Object.defineProperty(navigator, 'languages', {
			get: () => ['ja-JP', 'ja', "en-US", "en",]
		});

		// plugins を偽装
		const createFakePluginArray = () => {
			const fakeMimeType = {
			type: "application/pdf",
			suffixes: "pdf",
			description: "Portable Document Format"
			};

			function makePlugin(name) {
			const plugin = {
				name,
				filename: "internal-pdf-viewer",
				description: "Portable Document Format",
				0: fakeMimeType,
				length: 1,
				item: i => fakeMimeType,
				namedItem: name => fakeMimeType
			};
			fakeMimeType.enabledPlugin = plugin;
			return plugin;
			}

			const plugins = [
			makePlugin("PDF Viewer"),
			makePlugin("Chrome PDF Viewer"),
			makePlugin("Chromium PDF Viewer"),
			makePlugin("Microsoft Edge PDF Viewer"),
			makePlugin("WebKit built-in PDF")
			];

			const pluginArray = {
			length: plugins.length,
			item: i => plugins[i],
			namedItem: name => plugins.find(p => p.name === name),
			[Symbol.iterator]: function* () {
				yield* plugins;
			}
			};

			plugins.forEach((p, i) => pluginArray[i] = p);
			Object.setPrototypeOf(pluginArray, PluginArray.prototype);
			return pluginArray;
		};

		// defineProperty を使わず直接代入することで検出回避(definePropertyはウイルスソフトの要監視対象)
		navigator.plugins = createFakePluginArray();
		
		// permissions.query の notifications 対応
		const originalQuery = window.navigator.permissions.query;

		window.navigator.permissions.query = function (parameters) {
			if (parameters.name === 'notifications') {
				return Promise.resolve({ state: Notification.permission });
			}
			return originalQuery(parameters);
		};

		 Object.defineProperty(window, 'chrome', {
			writable: true,
			enumerable: true,
			configurable: false,
			value: {
			app: {},
			csi: function () {},
			loadTimes: function () {}
			}
		});

		Object.defineProperty(window.chrome, 'webstore', {
			get: function () {
			throw new TypeError("Cannot read properties of undefined (reading 'constructor')");
			}
		});

		Object.defineProperty(window.chrome, 'runtime', {
			get: function () {
			throw new TypeError("Cannot read properties of undefined (reading 'constructor')");
			}
		});

		Object.defineProperty(window.chrome, 'connect', {
			get: function () {
			throw new TypeError("Cannot read properties of undefined (reading 'connect')");
			}
		});

		Object.defineProperty(window.chrome, 'sendMessage', {
			get: function () {
			throw new TypeError("Cannot read properties of undefined (reading 'sendMessage')");
			}
		});

	})();`

	// スクリプトをすべての新規ページにプリロード注入
	if err := chromedp.Run(ctx,
		chromedp.ActionFunc(func(ctx context.Context) error {
			_, err := page.AddScriptToEvaluateOnNewDocument(stealthJS).Do(ctx)
			return err
		}),
	); err != nil {
		log.Fatal(err)
	}

	url := "https://bot.sannysoft.com/"

	// テストサイトにアクセス
	if err := chromedp.Run(ctx,
		chromedp.Navigate(url),
		chromedp.Sleep(2*time.Second),
		// スクリーンショットを取る 2つ目の引数はJpegの品質(0-100)
		chromedp.FullScreenshot(&buf, 90),
		chromedp.Sleep(1*time.Second),
	); err != nil {
		log.Fatal(err)
	}

	if err := os.WriteFile("screenshot.jpg", buf, 0644); err != nil {
		log.Fatal(err)
	}
	fmt.Println(time.Since(startTime))
}

おわりに

chromedp はとても融通が利くので、個人利用での自動化にはもってこいです。ダウンローダ*2を作るのにはとても便利ですので、機会があれば使用してみてください。
ページの画像を作るときにGopher君入れてと chatGPT に頼みました。

※ 本記事は、自動化ツールによる検出対策の技術的理解と検証を目的としており、商用サイトの利用規約に反する自動操作や不正アクセスを推奨するものではありません。

*1:僕の環境ではノートン

*2:とかダウンロード用の wrapper