File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff 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
Original file line number Diff line number Diff 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
Original file line number Diff line number Diff 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
4765volumes :
4866 redis_data :
67+ tailscale_state :
Original file line number Diff line number Diff 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
Original file line number Diff line number Diff line change @@ -8,6 +8,7 @@ Flask-Session==0.8.0
88
99# HTTP & Scraping
1010requests == 2.32.5
11+ PySocks == 1.7.1
1112urllib3 == 2.6.3
1213beautifulsoup4 == 4.14.3
1314certifi == 2026.2.25
You can’t perform that action at this time.
0 commit comments