
はじめに
プライバシー対策として、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)) }