0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

壁にぶつかったら、まず分類する —— PageSift 1.2 のアクセス障壁リカバリ「itna」の設計

0
Posted at

PageSift は Web ページや PDF・Office 文書などを Markdown と構造化データに変換する、Rust 製のコンテンツ抽出エンジンです。1.2 では新しい crate pagesift-itna を追加しました。itna(名前は "anti" の逆さ読みです)は、アクセス障壁 —— レート制限、ログイン壁、ブロック、JavaScript チャレンジ —— に遭遇したときの「回復経路」を担います。

最初に立場をはっきりさせておきます。itna はデフォルトでオフです。有効化は設定ファイルに [itna] enabled = true と明示した場合だけ。そしてこの記事で書くのは「どうやって壁を破るか」ではなく、むしろ逆の話です。いつ諦めるか、いつ一歩だけ進むか、その規律をどうコードに固定したか —— それが itna の本体です。

1. いきなりリトライしない。まず分類する

障壁に当たったときの最悪の初手は、反射的にブラウザを起動して再試行することです。相手が「来るな」と言っているのか、「少し待て」と言っているのか、「本人確認が必要」と言っているのかで、正しい対応はまったく違います。

itna はまず HTTP 実行の結果を分類します(pagesift-itna の classify)。

結果 分類
429 / 503 + Retry-After Throttled(制限中)
ログイン壁 / 401 Login
403 / ブロック検知 Blocked
チャレンジ / CAPTCHA 検知 Challenge

そして分類ごとに、次に取れる行動を型で縛ります。エスカレーション(ブラウザ実行への移行)を許可するのは Blocked と Challenge のときだけです。Throttled と Login は、コード構造の時点でブラウザに進めません。テストにも throttling_and_login_never_escalate という、そのままの名前の断言があります。

これは設計として当然の判断です。429 は「ペースを落とせ」という合図であって、ブラウザで押し切る理由にはなりません。ログイン壁の向こう側は認証情報の問題で、レンダリングの問題ではありません。

2. エスカレーションの規律:最大で一度だけ、同じ出口で

Blocked / Challenge の場合でも、無制限に粘ることはしません。試行管理(Attempts)の上限は生成時に固定されています。

Attempts::new(timeout, 2, 1)

総試行は最大 2 回、そのうちブラウザ試行は最大 1 回。しかも 2 回の試行は**同じ egress(出口)**を使います。IP を変えて何度も叩く、という挙動は構造的に存在しません。

さらに、分類が Blocked / Challenge でもエスカレーションが許可されない条件があります(browser_allowed の門番)。

  • engine = "http" と明示されたリクエスト
  • 任意のカスタムヘッダを持つリクエスト
  • 認証情報を含む「センシティブ」なリクエスト(後述)
  • スクリプト発見モードのリクエスト

つまりエスカレーションは「素の公開ページ取得」に対してだけ開かれた、狭い経路です。

3. ネットワーク予算は共有し、尽きたら失敗する

ブラウザに進んだ後も、好きなだけリクエストを飛ばせるわけではありません。itna は 1 リクエスト全体に 1 つのネットワーク予算(NetworkBudget)を持たせます。内訳はナビゲーション 16 回、リソース 128 回、そして HTTP 側とブラウザ側で共有する単一の deadline です。

HTTP 側はリダイレクトのたびに、ブラウザ側は CDP のリクエスト傍受層でサブリクエストのたびに、この予算を消費します。尽きたらどうなるか —— こっそり続行するのではなく、エラーとして失敗します(fail-closed)。予算外の通信が「なかったこと」になる設計にはしていません。

そして全試行の軌跡 —— どのエンジンで実行し、どういう結果だったか、ブラウザ実行が何回あったか —— は、結果ドキュメントの metadata.pagesift.itna(失敗時はエラーの詳細内)に記録されます。回復経路を通ったかどうかは、後から必ず検証できます。

4. セッションは四つ組で隔離する

回復経路でブラウザを使う以上、セッション(Cookie 等の同一性)の扱いが安全の核心になります。itna のアイデンティティプールは、セッションを以下の四つ組をキーにリースします(pagesift-policy の IdentityKey)。

owner × origin × egress × fingerprint

リース取得時には、リクエスト先 URL の origin がキー内の origin と完全一致することも検証します。owner が違えば、同じサイトでもセッションは絶対に混ざりません。owner を持たない匿名リクエストはプールに入らず、使い捨てセッションになります。

そして共有そのものが禁止されるケースがあります。

  • Cookie / Authorization / Proxy-Authorization ヘッダを含む、またはサイト認証情報が設定されたリクエスト(センシティブ判定)
  • 任意のカスタムヘッダを持つリクエスト

センシティブ判定については、正直に書くと、当初の想定より実装のほうが厳しくなっています。「認証情報付きは共有しない」という方針を実装したら、結果としてカスタムヘッダが 1 つでもあれば共有もエスカレーションも両方止まる形になりました。安全側に倒れた結果なので、そのまま採用しています。

5. プロキシは「検証済みトンネル」でしか通らない

フィンガープリント付きトランスポートがプロキシを経由する場合、itna は対象ホストをローカルで解決し、SSRF チェックを通した IP アドレスだけをプロキシに渡します。プロキシ側で名前解決をさせる方式(SOCKS5 のリモート解決相当)も含め、対象が検証を通る前にトンネルを開くことはありません。socks5h のような指定でも解決はローカルで行います。

TLS の SNI と HTTP の authority には元のホスト名が残ります。プロキシが受け取るのは「検証済みの IP へのトンネル」だけです。

そして重要なのは失敗時の挙動です。プロキシやトンネルの確立に失敗したら、それはエラーとして返ります。通常のリクエストや直接接続への暗黙のフォールバック(ダウングレード)は存在しません。「プロキシが落ちたから素通しで取りに行く」は、この手の機能では最も起きてほしくない事故だからです。

6. Cookie の引き渡しは、正確な origin と Strict が前提

HTTP 実行からブラウザ実行へ移る際、HTTP 側で得た Cookie をブラウザへ引き渡す機能があります。ここも条件は厳しく固定しました。

  • 引き渡し先は正確な origin に限定され、ホストだけが一致する別スキーム・別ポートへの持ち出しは傍受層で遮断します(唯一の例外は標準的な http:80 → https:443 への昇格のみ)
  • インポートされる Cookie は一律 SameSite=Strict で設定します(セッションストアが SameSite を保持しないため、橋渡しした Cookie がクロスサイトのサブリソースに付いて出ていかないようにする措置です)
  • HTTP 側フィンガープリントの Chrome メジャーバージョンと、実行中ブラウザのメジャーバージョンが一致しない場合、引き渡し自体を拒否します

「だいたい同じサイトだから」という曖昧な引き渡しはしません。

7. やらないこと

itna の設計で一番時間をかけたのは、やることより「やらないこと」の線引きです。

ハードウェア同一性は偽装しません。 ブラウザ実行時にやっているのは自動化マーカーの除去だけです。起動引数で Blink の automation マーカーを消し、ドキュメント開始スクリプトで navigator.webdriver を undefined にします。正直に書くと、そのスクリプトにはもう 1 つだけ、window.chrome が存在しない場合に空のオブジェクトを補う処理があります。これはマーカー層の補完であって、UA や GPU、CPU コア数といったハードウェア・同一性パラメータの捏造はしていません。

CAPTCHA は解きません。 対話型 CAPTCHA ウィジェットのあるページは待たずに離脱し、ACCESS_BARRIER エラーに needsHuman: true を付けて返します。ソルバーは内蔵していませんし、今後もその予定はありません。解くべきなのは機械ではなく人間だ、という線引きです。なお、対話型でない JavaScript チャレンジについては、最大 8 秒(かつリクエストの timeout 以内)の有界待機をします。

ブラウザ実行は Chrome / CDP 系だけです。 通常のブラウザ描画では Lightpanda → moli → Chrome のエンジン連鎖を使いますが、itna の経路ではこの連鎖を意図的に切り、Chrome に固定します。理由はシンプルで、マーカー除去も、フィンガープリントも、Cookie の引き渡しも、すべて Chrome の意味論の上で検証しているからです。検証していないエンジンに回復経路を流すくらいなら、Chrome がない環境ではエラーを返すほうを選びました。

8. 証拠の範囲について、正直な注記

最後に、このリリースの証拠の範囲を明記します。

itna の挙動は、すべてローカルに構築した制御可能なフィクスチャに対するリリーステストで検証しています。実在の本番サイトや WAF に対する通過率は、計測していませんし、主張もしません。「どのサイトが通るか」を謳う代わりに、分類・規律・予算・隔離といった挙動の約束がコードとテストで固定されていることを示すのが、このリリースの証拠です。

ついでに書くと、README の速度・品質の数値は 1.1.0 時点の実測のままで、1.2 では再計測していません。速くなったとは言いません。

おわりに

itna は「反・○○」ではなく、"anti" を逆に読んだだけの名前の通り、発想も逆向きです。壁を破る道具ではなく、壁の前での振る舞いに規律を持たせるための仕組み —— 分類して、上げるのは一度だけ、予算が尽きたら止まり、同一性は混ぜず、通れなかったことは記録に残す。抽出エンジンが長く使われるには、通す力より先に、この種の自制が必要だと思っています。

  • GitHub: https://github.com/YOLOW92/pagesift
  • crates.io / npm でも公開しています。PageSift 本体は AI / LLM パイプライン向けに、Web・PDF・Office・メール・電子書籍などを Markdown と構造化データへ変換するエンジンです。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?