Skip to content

Commit 5ea6e03

Browse files
feat: add Tailscale exit node support for school system requests
- Add tailscale sidecar container in docker-compose.yml (profile: tailscale) - Add get_school_http_session() with SOCKS5 proxy support in http_client.py - Switch fetcher.py to use school-specific HTTP session - Add PySocks dependency to requirements.txt - Add TS_AUTHKEY, TS_EXIT_NODE, TS_HOSTNAME env vars to .env.example - Only school system requests route through proxy; Turnstile etc. unaffected - Feature is opt-in: requires --profile tailscale and env vars to activate
1 parent 6c86512 commit 5ea6e03

5 files changed

Lines changed: 55 additions & 3 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
1717

1818
# 其他選填參數
1919
# SHARE_TTL=7200
20+
21+
# Tailscale Exit Node (選填,啟用後學校系統請求會透過指定的 exit node 轉發)
22+
# TS_AUTHKEY=tskey-auth-xxxxx
23+
# TS_EXIT_NODE=100.x.x.x
24+
# TS_HOSTNAME=grades-docker

app/services/http_client.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def get_http_session() -> TimeoutSession:
6868
"""
6969
Returns a requests.Session customized with default timeouts and automatic retries.
7070
Configurable via environment variables.
71+
This session does NOT use any proxy — suitable for Turnstile, internal services, etc.
7172
"""
7273
try:
7374
total_retries = int(os.environ.get("HTTP_RETRY_TOTAL", 3))
@@ -106,3 +107,29 @@ def get_http_session() -> TimeoutSession:
106107
session.mount("https://", adapter)
107108

108109
return session
110+
111+
112+
def get_school_http_session() -> TimeoutSession:
113+
"""
114+
Returns a requests.Session for school system requests.
115+
If SCHOOL_SOCKS_PROXY is set (e.g. socks5://tailscale:1055), routes traffic
116+
through a Tailscale exit node. Otherwise behaves identically to get_http_session().
117+
"""
118+
session = get_http_session()
119+
120+
socks_proxy = os.environ.get("SCHOOL_SOCKS_PROXY", "").strip()
121+
if socks_proxy:
122+
session.proxies = {
123+
"http": socks_proxy,
124+
"https": socks_proxy,
125+
}
126+
# 只記錄 proxy host:port,不記錄任何 auth 資訊
127+
try:
128+
from urllib.parse import urlparse
129+
parsed = urlparse(socks_proxy)
130+
safe_addr = f"{parsed.hostname}:{parsed.port}"
131+
except Exception:
132+
safe_addr = "(parse error)"
133+
logger.info(f"School HTTP session using SOCKS proxy: {safe_addr}")
134+
135+
return session

docker-compose.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ services:
88
- PYTHONUNBUFFERED=1
99
- REDIS_URL=redis://redis:6379/0
1010
- TZ=Asia/Taipei
11+
- SCHOOL_SOCKS_PROXY=${TS_EXIT_NODE:+socks5://tailscale:1055}
1112
ports:
1213
- "5000:5000"
1314

@@ -27,6 +28,23 @@ services:
2728
retries: 3
2829
start_period: 10s
2930

31+
tailscale:
32+
image: tailscale/tailscale:latest
33+
hostname: ${TS_HOSTNAME:-grades-docker}
34+
environment:
35+
- TS_AUTHKEY=${TS_AUTHKEY:-}
36+
- TS_EXTRA_ARGS=--exit-node=${TS_EXIT_NODE:-}
37+
- TS_TAILSCALED_EXTRA_ARGS=--socks5-server=0.0.0.0:1055
38+
- TS_STATE_DIR=/var/lib/tailscale
39+
- TS_USERSPACE=true
40+
volumes:
41+
- tailscale_state:/var/lib/tailscale
42+
deploy:
43+
restart_policy:
44+
condition: any
45+
profiles:
46+
- tailscale
47+
3048
tunnel:
3149
image: cloudflare/cloudflared:latest
3250
command: tunnel run
@@ -46,3 +64,4 @@ services:
4664

4765
volumes:
4866
redis_data:
67+
tailscale_state:

fetcher.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ class GradeFetcher:
5252

5353
def __init__(self, session_factory=None):
5454
if session_factory is None:
55-
# Delay import to avoid circular dependency if any, or just use the injected one
56-
from app.services.http_client import get_http_session
57-
self.session_factory = get_http_session
55+
# 使用學校系統專用的 HTTP session,當環境設定 SOCKS proxy 時會走 Tailscale exit node
56+
from app.services.http_client import get_school_http_session
57+
self.session_factory = get_school_http_session
5858
else:
5959
self.session_factory = session_factory
6060

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Flask-Session==0.8.0
88

99
# HTTP & Scraping
1010
requests==2.32.5
11+
PySocks==1.7.1
1112
urllib3==2.6.3
1213
beautifulsoup4==4.14.3
1314
certifi==2026.2.25

0 commit comments

Comments
 (0)