Squidでキャッシュプロキシサーバーを構築する
スクレイピングを試す際、短期間でアクセスが集中しないようにとこれまで自力で行っていたキャッシュの管理が難しくなってきたため、キャッシュ機能を持たせたプロキシサーバーを経由してアクセスできるようにしてみました。
環境
- VSCode: 1.82.2
- Docker Desktop: 4.18.0
- Debian bullseye
- Python: 3.11.3
- Jupyter Notebook: 1.0.0
- Playwright: 1.38.0
- Squid: 4.13
Squidのインストール
今回は既存のDev Containerに環境を構築していきます。
.devcontainer/devcontainer.json
と.devcontainer/Dockerfile
、そこから呼び出しているシェルスクリプトの.devcontainer/postCreateCommand.sh
は下記となります。
devcontainer.json
{
"name": "Python 3",
"build": {
"dockerfile": "Dockerfile",
"args": {
"VARIANT": "3"
}
},
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
],
"extensions": [
"ms-python.python",
"ms-toolsai.jupyter",
"alexcvzz.vscode-sqlite",
"ms-vscode.live-server",
"eamodio.gitlens"
],
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
"appPort": [
8888
],
"postCreateCommand": "/bin/bash .devcontainer/postCreateCommand.sh",
"containerEnv": {
"DISPLAY": "host.docker.internal:0"
}
}
Dockerfile
ARG VARIANT=3
FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT}
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends sqlite3 \
&& apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*
RUN pip install jupyter
postCreateCommand.sh
pip install playwright beautifulsoup4 lxml pandas requests pydantic matplotlib
playwright install
playwright install-deps
apt install libpng-dev
sudo apt install -y squid-openssl
インストール方法
Squidのssl_bump
機能を利用するため、Squidのパッケージsquid
ではなくsquid-openssl
をインストールします。
なぜかというと、--with-gnutls
または--with-openssl
を有効化してコンパイルされたパッケージが必要になるためです。
sudo apt install -y squid-openssl
自己署名証明書の生成とシステムへのインストール
HTTPSの通信内容をキャッシュするには、いわゆる中間者攻撃(MITM攻撃)と同様の仕組みを用いる必要があります。
つまりブラウザとSquid、Squidとアクセス対象のWebサーバーの2つの通信を行えるようにします。
Squidではssl_bump
機能によりこれを実現できます。
まずは自己署名証明書を用意し、クライアント(Playwright+ブラウザ)で利用できるようにOSにインストールします。
自己署名証明書ファイルを生成する
証明書を生成し、権限も変更しておきます。
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
chmod 400 key.pem cert.pem
証明書を /usr/local/share/ca-certificates/
ディレクトリに移動します。
sudo cp /workspaces/notebook-python/cert.pem /usr/local/share/ca-certificates/your_certificate.crt
下記のコマンドで、証明書のリストを更新します。
sudo update-ca-certificates
公開鍵のハッシュ値を計算する
後の作業で必要になるため、生成したcert.pem
に含まれる公開鍵のハッシュ値を取得しておきます。
openssl x509 -pubkey -noout -in /workspaces/notebook-python/cert.pem | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
この時の計算結果はこのようになりました。
QFWlmpsM4EaZZE55kTrpgmcyGy6YkIL2Bmwyjx2unqQ=
Squidの設定ファイルを編集する
/etc/squid/squid.conf
を編集します。
とりあえずバックアップをとっておきましょう。
sudo cp /etc/squid/squid.conf /etc/squid/squid.conf.old
試行錯誤の末、squid.conf
の内容は以下のようになりました。
# DNSのIPv4優先設定を有効化
dns_v4_first on
# ローカルネットワーク(LAN)のアクセス制御リストを作成
acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
acl localnet src 10.0.0.0/8 # RFC 1918 ローカルプライベートネットワーク(LAN)
acl localnet src 100.64.0.0/10 # RFC 6598 共有アドレススペース(CGN)
acl localnet src 169.254.0.0/16 # RFC 3927 リンクローカル(直接接続された)マシン
acl localnet src 172.16.0.0/12 # RFC 1918 ローカルプライベートネットワーク(LAN)
acl localnet src 192.168.0.0/16 # RFC 1918 ローカルプライベートネットワーク(LAN)
acl localnet src fc00::/7 # RFC 4193 ローカルプライベートネットワーク範囲
acl localnet src fe80::/10 # RFC 4291 リンクローカル(直接接続された)マシン
# SSLポート(443)へのアクセス制御リストを作成
acl SSL_ports port 443
# セーフポートへのアクセス制御リストを作成
acl Safe_ports port 80 # http
acl Safe_ports port 21 # ftp
acl Safe_ports port 443 # https
acl Safe_ports port 70 # gopher
acl Safe_ports port 210 # wais
acl Safe_ports port 1025-65535 # 登録されていないポート
acl Safe_ports port 280 # http-mgmt
acl Safe_ports port 488 # gss-http
acl Safe_ports port 591 # filemaker
acl Safe_ports port 777 # 多言語対応のhttp
# CONNECTメソッドへのアクセス制御リストを作成
acl CONNECT method CONNECT
# セーフポート以外へのHTTPアクセスを拒否
http_access deny !Safe_ports
# SSLポート以外へのCONNECTアクセスを拒否
http_access deny CONNECT !SSL_ports
# ローカルホストへのアクセスを許可(マネージャー用)
http_access allow localhost manager
# マネージャーへのアクセスを拒否
http_access deny manager
# 追加の設定ファイルをインクルード
include /etc/squid/conf.d/*
# ローカルホストへのHTTPアクセスを許可
http_access allow localhost
# すべてのアクセスを拒否
http_access deny all
# HTTPポート(3128)でSSLバンプを設定
http_port 3128 ssl-bump \
cert=/workspaces/notebook-python/cert.pem \
key=/workspaces/notebook-python/key.pem
# SSLバンプのステップとアクションを設定
ssl_bump stare all
ssl_bump bump all
# SslBump1ステップを定義
acl step1 at_step SslBump1
# キャッシュディレクトリを設定
cache_dir ufs /var/spool/squid 100 16 256
# コアダンプディレクトリを設定
coredump_dir /var/spool/squid
# FTPとGopherのリフレッシュパターンを設定
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
# 特定のウェブサイトのリフレッシュパターンを設定
refresh_pattern ^https://webscraper\.io/test-sites/e-commerce/static 10080 100% 10080 reload-into-ims
# CGIとその他のリフレッシュパターンを設定
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
refresh_pattern . 0 20% 4320
ssl_bump
機能を利用するためには、最低限下記の設定を行う必要がありました。
先程生成した証明書ファイルのパスをhttp_port
に指定します。
http_port 3128 ssl-bump \
cert=/workspaces/notebook-python/cert.pem \
key=/workspaces/notebook-python/key.pem
ssl_bump stare all
ssl_bump bump all
acl step1 at_step SslBump1
SSL証明書データベースを初期化する
SquidはWebサイトにアクセスする際にサーバー証明書を動的に生成し、それをキャッシュして利用するため、Squidを起動する前に以下のコマンドで初期化する必要があります。
またsudo
を付加して実行すると所有者がroot
になってしまうため、所有者をproxy
に戻しておきます。
sudo /usr/lib/squid/security_file_certgen -c -s /var/spool/squid/ssl_db -M 4MB
sudo chown -R proxy:proxy /var/spool/squid/ssl_db
ブラウザ操作のコードを作成する
Python(Jupyter Notebook)でPlaywrightを操作するため、下記のコードを作成しました。
今までの状態ではChromeブラウザでアクセスすると証明書エラーが発生するため、信頼できる証明書としてブラウザに認識させる起動オプションを指定してChromeを起動する必要があります。
--ignore-certificate-errors-spki-list
に先程計算したハッシュ値を指定することで、証明書を信頼することができます。
またこのオプションは--user_data_dir
も指定する必要があるので、特に指定がなければ/dev/null
を設定します。
Contextを生成する際に指定するプロキシは、ポートを変更していなければhttp://localhost:3128
を指定します。
今回アクセスする対象のWebページとして下記を指定しました。
これはスクレイピングを試すために用意されたWebサイトのようで、同じECサイトのサンプルでもいくつかパターンがあります。
Static | Web Scraper Test Sites
import asyncio
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.launch(
args=[
"--ignore-certificate-errors-spki-list=QFWlmpsM4EaZZE55kTrpgmcyGy6YkIL2Bmwyjx2unqQ=",
"--user_data_dir=/dev/null",
],
)
context = await browser.new_context(proxy={"server": "http://localhost:3128"}) # Squidのデフォルトポートは3128
page = await context.new_page()
await page.goto("https://webscraper.io/test-sites/e-commerce/static")
await page.wait_for_load_state("networkidle")
content = await page.content()
print(content)
await context.close()
await browser.close()
動作確認
ここまで設定できたら、動作確認のためにSquidを起動してコードを実行してみます。 Squidを起動するには下記を実行します。
sudo service squid start
access.log
この時の/var/log/squid/access.log
は以下のようになりました。
2回目以降のアクセスはキャッシュを参照できているようです。
1697204571.489 105 127.0.0.1 NONE/200 0 CONNECT webscraper.io:443 - HIER_DIRECT/13.33.174.51 -
1697204572.108 588 127.0.0.1 TCP_REFRESH_MODIFIED/200 4042 GET https://webscraper.io/test-sites/e-commerce/static - HIER_DIRECT/13.33.174.51 text/html
1697204572.158 21 127.0.0.1 TCP_REFRESH_UNMODIFIED/200 75646 GET https://webscraper.io/css/app.css? - HIER_DIRECT/13.33.174.51 text/css
1697204572.164 24 127.0.0.1 NONE/200 0 CONNECT webscraper.io:443 - HIER_DIRECT/13.33.174.51 -
1697204572.201 25 127.0.0.1 TCP_REFRESH_UNMODIFIED/200 1230 GET https://webscraper.io/css/ws-icons.css? - HIER_DIRECT/13.33.174.51 text/css
1697204572.214 26 127.0.0.1 NONE/200 0 CONNECT webscraper.io:443 - HIER_DIRECT/13.33.174.51 -
1697204572.221 34 127.0.0.1 NONE/200 0 CONNECT webscraper.io:443 - HIER_DIRECT/13.33.174.51 -
1697204572.221 34 127.0.0.1 NONE/200 0 CONNECT webscraper.io:443 - HIER_DIRECT/13.33.174.51 -
1697204572.221 34 127.0.0.1 NONE/200 0 CONNECT webscraper.io:443 - HIER_DIRECT/13.33.174.51 -
1697204572.252 61 127.0.0.1 NONE/200 0 CONNECT fonts.googleapis.com:443 - HIER_DIRECT/142.250.207.10 -
1697204572.253 63 127.0.0.1 NONE/200 0 CONNECT fonts.googleapis.com:443 - HIER_DIRECT/142.250.207.10 -
1697204572.253 63 127.0.0.1 NONE/200 0 CONNECT www.googletagmanager.com:443 - HIER_DIRECT/142.251.222.40 -
1697204572.256 16 127.0.0.1 TCP_REFRESH_UNMODIFIED/200 5571 GET https://webscraper.io/img/logo_white.svg - HIER_DIRECT/13.33.174.51 image/svg+xml
1697204572.257 18 127.0.0.1 TCP_REFRESH_UNMODIFIED/200 74628 GET https://webscraper.io/js/app.js? - HIER_DIRECT/13.33.174.51 application/javascript
1697204572.286 64 127.0.0.1 TCP_REFRESH_UNMODIFIED/200 36056 GET https://webscraper.io/js/vendor.js? - HIER_DIRECT/13.33.174.51 application/javascript
1697204572.357 45 127.0.0.1 TCP_MISS/200 1517 GET https://fonts.googleapis.com/css2? - HIER_DIRECT/142.250.207.10 text/css
1697204572.357 44 127.0.0.1 TCP_MISS/200 1546 GET https://fonts.googleapis.com/css2? - HIER_DIRECT/142.250.207.10 text/css
1697204572.415 102 127.0.0.1 TCP_MISS/200 89371 GET https://www.googletagmanager.com/gtm.js? - HIER_DIRECT/142.251.222.40 application/javascript
1697204572.564 28 127.0.0.1 NONE/200 0 CONNECT webscraper.io:443 - HIER_DIRECT/13.33.174.51 -
1697204572.578 46 127.0.0.1 NONE/200 0 CONNECT webscraper.io:443 - HIER_DIRECT/13.33.174.51 -
1697204572.600 65 127.0.0.1 NONE/200 0 CONNECT fonts.gstatic.com:443 - HIER_DIRECT/142.251.222.35 -
1697204572.600 28 127.0.0.1 TCP_REFRESH_UNMODIFIED/200 39152 GET https://webscraper.io/fonts/Candara-subset.woff2? - HIER_DIRECT/13.33.174.51 application/octet-stream
1697204572.603 70 127.0.0.1 NONE/200 0 CONNECT fonts.gstatic.com:443 - HIER_DIRECT/142.251.222.35 -
1697204572.627 3 127.0.0.1 TCP_HIT/200 16954 GET https://fonts.gstatic.com/s/roboto/v30/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 - HIER_NONE/- font/woff2
1697204572.627 2 127.0.0.1 TCP_HIT/200 34126 GET https://fonts.gstatic.com/s/montserrat/v26/JTUSjIg1_i6t8kCHKm459Wlhyw.woff2 - HIER_NONE/- font/woff2
1697204572.638 50 127.0.0.1 TCP_REFRESH_UNMODIFIED/200 7563 GET https://webscraper.io/fonts/ws-icons.woff2? - HIER_DIRECT/13.33.174.51 application/octet-stream
1697204572.652 59 127.0.0.1 NONE/200 0 CONNECT googleads.g.doubleclick.net:443 - HIER_DIRECT/142.251.42.130 -
1697204572.664 66 127.0.0.1 NONE/200 0 CONNECT www.google-analytics.com:443 - HIER_DIRECT/216.239.36.178 -
1697204572.698 6 127.0.0.1 TCP_REFRESH_UNMODIFIED/200 21644 GET https://www.google-analytics.com/analytics.js - HIER_DIRECT/216.239.36.178 text/javascript
1697204572.744 53 127.0.0.1 TCP_MISS/200 2266 GET https://googleads.g.doubleclick.net/pagead/viewthroughconversion/653162778/? - HIER_DIRECT/142.251.42.130 text/javascript
1697204572.746 151 127.0.0.1 TCP_MISS/200 95313 GET https://www.googletagmanager.com/gtag/js? - HIER_DIRECT/142.251.222.40 application/javascript
1697204572.829 55 127.0.0.1 NONE/200 0 CONNECT www.google-analytics.com:443 - HIER_DIRECT/216.239.36.178 -
1697204572.883 46 127.0.0.1 TCP_MISS/200 641 POST https://www.google-analytics.com/j/collect? - HIER_DIRECT/216.239.36.178 text/plain
1697204572.911 57 127.0.0.1 NONE/200 0 CONNECT www.google.co.jp:443 - HIER_DIRECT/142.251.42.195 -
1697204572.913 64 127.0.0.1 NONE/200 0 CONNECT analytics.google.com:443 - HIER_DIRECT/142.250.207.14 -
1697204572.922 68 127.0.0.1 NONE/200 0 CONNECT www.google.com:443 - HIER_DIRECT/142.251.42.132 -
1697204572.922 70 127.0.0.1 NONE/200 0 CONNECT www.google.co.jp:443 - HIER_DIRECT/142.251.42.195 -
1697204572.941 92 127.0.0.1 NONE/200 0 CONNECT stats.g.doubleclick.net:443 - HIER_DIRECT/142.251.8.157 -
1697204572.964 722 127.0.0.1 TCP_REFRESH_UNMODIFIED/200 13271 GET https://webscraper.io/images/test-sites/e-commerce/items/cart2.png - HIER_DIRECT/13.33.174.51 image/png
1697204572.980 84 127.0.0.1 NONE/200 0 CONNECT stats.g.doubleclick.net:443 - HIER_DIRECT/142.251.8.157 -
1697204572.986 44 127.0.0.1 TCP_MISS/204 566 POST https://analytics.google.com/g/collect? - HIER_DIRECT/142.250.207.14 text/plain
1697204572.986 47 127.0.0.1 TCP_MISS/200 700 GET https://www.google.co.jp/ads/ga-audiences? - HIER_DIRECT/142.251.42.195 image/gif
1697204572.993 53 127.0.0.1 TCP_MISS/200 763 GET https://www.google.co.jp/pagead/1p-user-list/653162778/? - HIER_DIRECT/142.251.42.195 image/gif
1697204572.998 44 127.0.0.1 TCP_MISS/200 711 POST https://stats.g.doubleclick.net/j/collect? - HIER_DIRECT/142.251.8.157 text/plain
1697204573.004 50 127.0.0.1 TCP_MISS/200 763 GET https://www.google.com/pagead/1p-user-list/653162778/? - HIER_DIRECT/142.251.42.132 image/gif
1697204573.030 40 127.0.0.1 TCP_MISS/204 566 POST https://stats.g.doubleclick.net/g/collect? - HIER_DIRECT/142.251.8.157 text/plain