自分のPCを踏み台にする超簡易プロキシ
経緯
OpenAI の operator 機能を使って、とあるサイトの内容を取得させようとした。
しかし、 operator は恐らく米国あたりの VPN を経由していて、そのサイトは一部の VPN からのアクセスをブロックするようで、うまくアクセスできなかった。VPN をブロックしているので、他の VPN を使う、ということも難しい(そもそも環境的に VPN を噛ませることが不可能)。
なので、リクエストをバイパスする Web サーバーを用意し、自分の PC からパブリックな URL を公開して、 URL を入力するだけでプロキシできる Python スクリプトを作成した。
一応、このスクリプトを適当なクラウド環境やらで動かすことで、自分の PC 以外の IP でプロキシすることも可能。
「サイトにブロックされている環境から、VPNなどの複雑な設定をせずに、URLをいじるだけで適当にアクセスしたい」という用途向け。セキュリティ的にも機能的にもガバガバなので、適当な用途以外では使えない。
動作
https://xxx.ngrok-free.app/https://example.com
にアクセスすることで、 example.com
の内容が表示される。 example.com
へのリクエスト自体は自分の PC から行われるので、本来ブロックされる環境からでもアクセスできる。
使い方
後述するコードを ./proxy.py
に置いたとしたら、uv run ./proxy.py
で依存関係のインストール等含め実行できる。
localhost:5000
で待ち受けるので、ngrok http http://localhost:5000
などで ngrok を用いて https 化と外部公開を行う。
コード
コードはすべて o1 pro で生成した。
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "beautifulsoup4",
# "flask",
# "requests",
# ]
# ///
from flask import Flask, request, Response
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, urljoin
app = Flask(__name__)
def get_request_scheme():
"""
リバースプロキシ (ngrok等) 環境でも適切なスキームを取得するための補助関数。
X-Forwarded-Proto があればそれを使い、無ければ Flask が見る scheme を使う。
"""
if 'X-Forwarded-Proto' in request.headers:
return request.headers['X-Forwarded-Proto']
return request.scheme
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def proxy(path):
"""
例:
/https://example.com/foo/bar -> 実際には https://example.com/foo/bar にアクセスする
"""
# パスが http:// もしくは https:// で始まらない場合の対応
# (必要に応じてエラーにするなり、デフォルトを付与するなど変更してください)
if not (path.startswith("http://") or path.startswith("https://")):
return "パスが http:// または https:// で始まっていません。", 400
target_url = path # リクエストパスそのものをターゲットURLとみなす
# ターゲットURLへリクエスト(必要に応じてヘッダなど細かく調整)
resp = requests.get(target_url)
# レスポンスの Content-Type をチェック
content_type = resp.headers.get('Content-Type', '')
# HTMLの場合のみリンクを書き換えが必要
if 'text/html' in content_type:
soup = BeautifulSoup(resp.text, 'html.parser')
# 例えば <a href="...">, <img src="...">, <form action="..."> などを探して書き換える
rewrite_attrs = {
'a': 'href',
'img': 'src',
'script': 'src',
'link': 'href',
'iframe': 'src',
'form': 'action'
}
# ターゲットURL からホストやパスを解析
parsed_target = urlparse(target_url)
# proxy_root: 現在のアクセス(プロキシ)が使用しているURLのルート (例: https://xxxx.ngrok.io/)
proxy_root = f"{get_request_scheme()}://{request.host}/"
for tag_name, attr_name in rewrite_attrs.items():
for tag in soup.find_all(tag_name):
if not tag.has_attr(attr_name):
continue
original_val = tag[attr_name]
if not original_val:
continue
# ここでリンクを書き換えて、プロキシを通すURLに変換する
# urljoin を使うと相対パス -> 絶対パスへの変換などが簡単に行える
# real_abs_url: ターゲットページ基準での「完全なURL」
real_abs_url = urljoin(target_url, original_val)
# これを再度 proxy_root 経由のパスにする
# 例: real_abs_url = "https://example.com/contests/"
# -> "https://(プロキシのhost)/https://example.com/contests/"
tag[attr_name] = proxy_root + real_abs_url
# 書き換えたHTMLを返す
return Response(str(soup), content_type=content_type, status=resp.status_code)
else:
# HTML以外(画像、CSS、JSなど)はそのまま返却
return Response(resp.content, content_type=content_type, status=resp.status_code)
if __name__ == "__main__":
# Flask アプリを起動
# ポートなどは適宜
app.run(host="0.0.0.0", port=5000, debug=True)
Discussion