Skip to content

Commit 76d9ebd

Browse files
authored
Merge pull request #7 from GDUTMeow/dev
fix(v1.5.2): 修复 Welearn 因为上了 WAF 导致请求失败的问题
2 parents 67ac2b8 + b5fea21 commit 76d9ebd

File tree

5 files changed

+397
-158
lines changed

5 files changed

+397
-158
lines changed

app.py

Lines changed: 202 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
from io import StringIO
3535
from werkzeug.wrappers.response import Response as WerkzeugResponse
3636
from bs4 import BeautifulSoup
37+
import tls_client
38+
import base64
3739

3840

3941
# ====================== 全局变量 ======================
@@ -243,49 +245,29 @@ def get_port():
243245

244246
def get_user_info(client: httpx.Client) -> Dict[str, Optional[str]]:
245247
"""
246-
从用户信息页面提取用户详细信息
247-
:param client: 已认证的httpx客户端
248-
:return: 包含用户信息的字典,格式为:
249-
{
250-
"username": "用户名",
251-
"name": "姓名",
252-
"student_id": "学号",
253-
"school": "学校",
254-
"birth_year": "出生年份"
255-
}
248+
从用户信息页面提取用户详细信息(使用 tls_client 绕过 WAF)
249+
:param client: 保留参数以兼容,但内部使用 tls_client
250+
:return: 用户信息字典
256251
"""
257252
info_url = "https://welearn.sflep.com/user/userinfo.aspx"
258253

259254
try:
260-
# 发送GET请求获取用户信息页面
261-
response = client.get(info_url)
262-
response.raise_for_status()
263-
264-
# 使用BeautifulSoup解析HTML
255+
session = build_tls_session()
256+
response = session.get(info_url, headers={"Referer": "https://welearn.sflep.com/student/index.aspx"})
265257
soup = BeautifulSoup(response.text, "html.parser")
266258
user_div = soup.find("div", id="user1")
267-
268259
if not user_div:
269260
raise ValueError("未找到用户信息面板")
270-
271-
# 使用CSS选择器精确查找各字段
272261
return {
273262
"username": _find_input_value(user_div, "lblAccount"),
274263
"name": _find_input_value(user_div, "txtName"),
275264
"student_id": _find_input_value(user_div, "txtStuNo"),
276265
"school": _find_selected_school(user_div),
277266
"birth_year": _find_selected_birthyear(user_div),
278267
}
279-
280-
except httpx.HTTPStatusError as e:
281-
print(f"请求失败,状态码: {e.response.status_code}")
282268
except Exception as e:
283269
print(f"获取用户信息出错: {str(e)}")
284-
285-
# 发生错误时返回空值字典
286-
return {
287-
key: None for key in ["username", "name", "student_id", "school", "birth_year"]
288-
}
270+
return {key: None for key in ["username", "name", "student_id", "school", "birth_year"]}
289271

290272

291273
def _find_input_value(soup: BeautifulSoup, id_fragment: str) -> Optional[str]:
@@ -319,6 +301,54 @@ def _find_selected_birthyear(soup: BeautifulSoup) -> Optional[str]:
319301
return selected.get("value") if selected else None
320302

321303

304+
# WAF 绕过部分
305+
def to_hex_byte_array(byte_array: bytes) -> str:
306+
return ''.join([f'{byte:02x}' for byte in byte_array])
307+
308+
309+
def generate_cipher_text(password: str):
310+
# 获取当前时间戳
311+
T0 = int(round(time.time() * 1000))
312+
P = password.encode('utf-8')
313+
V = (T0 >> 16) & 0xFF
314+
for byte in P:
315+
V ^= byte
316+
remainder = V % 100
317+
T1 = int((T0 / 100) * 100 + remainder)
318+
P1 = to_hex_byte_array(P)
319+
S = f"{T1}*" + P1
320+
S_encoded = S.encode('utf-8')
321+
E = base64.b64encode(S_encoded).decode('utf-8')
322+
return [E, T1]
323+
324+
325+
def build_tls_session() -> tls_client.Session:
326+
"""构建一个带浏览器指纹的 tls_client 会话,并注入当前 Cookie。"""
327+
session = tls_client.Session(client_identifier="chrome_120", random_tls_extension_order=True)
328+
session.headers.update({
329+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
330+
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
331+
'cache-control': 'no-cache',
332+
'upgrade-insecure-requests': '1',
333+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
334+
'sec-ch-ua': '"Chromium";v="120", "Not?A_Brand";v="8", "Google Chrome";v="120"',
335+
'sec-ch-ua-platform': '"Windows"',
336+
'sec-ch-ua-mobile': '?0',
337+
})
338+
try:
339+
if state.cookies:
340+
for k, v in state.cookies.items():
341+
for domain in ("welearn.sflep.com", ".sflep.com", "sso.sflep.com"):
342+
try:
343+
session.cookies.set(k, v, domain=domain)
344+
except Exception:
345+
pass
346+
except Exception:
347+
pass
348+
return session
349+
350+
351+
322352
# ====================== 路由定义 ======================
323353
@app.route("/")
324354
def index():
@@ -377,6 +407,96 @@ def api_login(cookies: str = None):
377407
return jsonify(success=False, error=str(e))
378408

379409

410+
@app.route("/api/login_pw", methods=["POST"])
411+
def api_login_pw():
412+
"""使用用户名/密码登录"""
413+
state.reset()
414+
try:
415+
data = request.json or {}
416+
user = data.get("user")
417+
pwd = data.get("pwd")
418+
if not user or not pwd:
419+
return jsonify(success=False, error="缺少用户名或密码")
420+
421+
log_message(f"尝试用户名/密码登录: {user}")
422+
session = tls_client.Session(client_identifier="chrome_120", debug=False)
423+
424+
resp = session.get(
425+
"https://welearn.sflep.com/user/prelogin.aspx?loginret=http://welearn.sflep.com/user/loginredirect.aspx"
426+
)
427+
rurl = resp.headers.get("Location") or resp.text
428+
429+
try:
430+
code_challenge = rurl.split("&")[4].split("=")[1]
431+
state_param = rurl.split("&")[6].split("=")[1]
432+
except Exception:
433+
m1 = re.search(r"code_challenge=([^&]+)", rurl)
434+
m2 = re.search(r"state=([^&]+)", rurl)
435+
code_challenge = m1.group(1) if m1 else ""
436+
state_param = m2.group(1) if m2 else ""
437+
438+
rturl = (
439+
f"/connect/authorize/callback?client_id=welearn_web&redirect_uri=https%3A%2F%2Fwelearn.sflep.com%2Fsignin-sflep&response_type=code&scope=openid%20profile%20email%20phone%20address&code_challenge={code_challenge}&code_challenge_method=S256&state={state_param}&x-client-SKU=ID_NET472&x-client-ver=6.32.1.0"
440+
)
441+
442+
while True:
443+
enpwd = generate_cipher_text(pwd)
444+
form_data = {
445+
"rturl": rturl,
446+
"account": user,
447+
"pwd": str(enpwd[0]),
448+
"ts": str(enpwd[1]),
449+
}
450+
451+
response = session.post("https://sso.sflep.com/idsvr/account/login", data=form_data)
452+
try:
453+
rt_json = response.json()
454+
except Exception:
455+
return jsonify(success=False, error="登录返回解析失败")
456+
457+
code = rt_json.get("code", -1)
458+
if code == -1:
459+
continue
460+
if code == 1:
461+
return jsonify(success=False, error="帐号或密码错误")
462+
463+
next_url = f"https://sso.sflep.com/idsvr" + rt_json.get("data", "")
464+
465+
while True:
466+
resp2 = session.get(next_url)
467+
if resp2.status_code in (301, 302, 303, 307, 308):
468+
next_url = resp2.headers.get("Location")
469+
if not next_url:
470+
break
471+
else:
472+
break
473+
474+
if code == 0:
475+
try:
476+
cookie_dict = {}
477+
try:
478+
cookie_dict = session.cookies.get_dict()
479+
except Exception:
480+
for c in getattr(session.cookies, 'jar', []):
481+
cookie_dict[c.name] = c.value
482+
483+
client.cookies.update(cookie_dict)
484+
state.cookies = cookie_dict
485+
with open("config.json", "w") as f:
486+
json.dump({"cookies": ";".join([f"{k}={v}" for k, v in cookie_dict.items()])}, f)
487+
488+
userinfo = get_user_info(client)
489+
log_message(f"登录成功: {userinfo}")
490+
state.task_status = "idle"
491+
return jsonify(success=True, error="", msg=f"登陆成功:欢迎,{userinfo.get('name')}")
492+
except Exception as e:
493+
return jsonify(success=False, error=str(e))
494+
495+
except Exception as e:
496+
log_message(f"密码登录异常: {e}", "APPERR")
497+
return jsonify(success=False, error=str(e))
498+
499+
380500
@app.route("/api/getCourses", methods=["GET"])
381501
def get_courses():
382502
"""获取课程列表"""
@@ -386,8 +506,10 @@ def get_courses():
386506

387507
try:
388508
url = f"https://welearn.sflep.com/ajax/authCourse.aspx?action=gmc&nocache={round(random.random(), 16)}"
389-
response = client.get(
390-
url, headers={"Referer": "https://welearn.sflep.com/student/index.aspx"}
509+
session = build_tls_session()
510+
response = session.get(
511+
url,
512+
headers={"Referer": "https://welearn.sflep.com/student/index.aspx"},
391513
)
392514
log_message(f"获取课程列表: {response.text}", "APPDEBUG")
393515
courses: List[CourseInfo] = response.json()["clist"]
@@ -405,8 +527,9 @@ def get_lessons():
405527

406528
try:
407529
_global.cid = request.args.get("cid")
530+
session = build_tls_session()
408531
url = f"https://welearn.sflep.com/student/course_info.aspx?cid={_global.cid}"
409-
response = client.get(
532+
response = session.get(
410533
url,
411534
headers={"Referer": "https://welearn.sflep.com/student/course_info.aspx"},
412535
)
@@ -418,7 +541,7 @@ def get_lessons():
418541
"APPDEBUG",
419542
)
420543
url = "https://welearn.sflep.com/ajax/StudyStat.aspx"
421-
response = client.get(
544+
response = session.get(
422545
url,
423546
params={"action": "courseunits", "cid": _global.cid, "uid": _global.uid},
424547
headers={"Referer": "https://welearn.sflep.com/student/course_info.aspx"},
@@ -453,7 +576,8 @@ def get_sections():
453576
url = (
454577
f"https://welearn.sflep.com/ajax/StudyStat.aspx?action=scoLeaves&cid={_global.cid}&uid={_global.uid}&unitidx={unit_index}&classid={_global.classid}"
455578
)
456-
response = client.get(
579+
session = build_tls_session()
580+
response = session.get(
457581
url,
458582
headers={
459583
"Referer": f"https://welearn.sflep.com/student/course_info.aspx?cid={_global.cid}"
@@ -556,10 +680,43 @@ def exit():
556680
def validate_cookies(cookies: Dict[str, str]) -> bool:
557681
"""验证Cookie有效性"""
558682
try:
683+
session = tls_client.Session(client_identifier="chrome_120", random_tls_extension_order=True)
684+
# 常见浏览器请求头
685+
session.headers.update({
686+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
687+
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
688+
'cache-control': 'no-cache',
689+
'upgrade-insecure-requests': '1',
690+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
691+
'sec-ch-ua': '"Chromium";v="120", "Not?A_Brand";v="8", "Google Chrome";v="120"',
692+
'sec-ch-ua-platform': '"Windows"',
693+
'sec-ch-ua-mobile': '?0',
694+
})
695+
# 注入用户提供的 Cookie
696+
for k, v in cookies.items():
697+
try:
698+
session.cookies.set(k, v, domain="welearn.sflep.com")
699+
session.cookies.set(k, v, domain=".sflep.com")
700+
session.cookies.set(k, v, domain="sso.sflep.com")
701+
except Exception:
702+
pass
703+
559704
test_url = "https://welearn.sflep.com/user/userinfo.aspx"
560-
resp = client.get(test_url, cookies=cookies)
561-
log_message(f"Cookie验证返回:{resp.text}", "APPDEBUG")
562-
return "我的资料" in resp.text
705+
resp = session.get(test_url, headers={"Referer": "https://welearn.sflep.com/student/index.aspx"})
706+
log_message(f"Cookie验证返回(tls_client):{resp.text}", "APPDEBUG")
707+
text = resp.text
708+
if ("我的资料" in text) or ("id=\"user1\"" in text):
709+
return True
710+
711+
# 若命中阿里云 WAF 405 页面,尝试预热并重试
712+
if ("<title>405</title>" in text) or ("Your Request ID is" in text):
713+
_ = session.get("https://welearn.sflep.com/user/prelogin.aspx?loginret=https://welearn.sflep.com/user/loginredirect.aspx")
714+
resp2 = session.get(test_url, headers={"Referer": "https://welearn.sflep.com/student/index.aspx"})
715+
text2 = resp2.text
716+
log_message(f"Cookie验证重试返回(tls_client):{text2}", "APPDEBUG")
717+
return ("我的资料" in text2) or ("id=\"user1\"" in text2)
718+
719+
return False
563720
except Exception as e:
564721
log_message(f"Cookie验证失败: {str(e)}")
565722
return False
@@ -585,17 +742,17 @@ def __init__(
585742
def run(self):
586743
try:
587744
state.task_status = "brain_burst"
745+
session = build_tls_session()
588746
infoHeaders = {
589747
"Referer": f"https://welearn.sflep.com/student/course_info.aspx?cid={_global.cid}",
590748
}
591749
# 重新计算总数
592750
state.progress = {"current": 0, "total": 0}
593751
for lesson in self.lessonIds: # 获取课程详细列表
594-
response = client.get(
752+
response = session.get(
595753
f"https://welearn.sflep.com/ajax/StudyStat.aspx?action=scoLeaves&cid={_global.cid}&uid={_global.uid}&unitidx={_global.lessonIndex.index(lesson)}&classid={_global.classid}",
596754
headers=infoHeaders,
597755
)
598-
resp = response
599756
if "异常" in response.text or "出错了" in response.text:
600757
state.task_status = "error"
601758
log_message(
@@ -629,7 +786,7 @@ def run(self):
629786
)
630787
id = section["id"]
631788
# 第一种刷课方法
632-
client.post(
789+
session.post(
633790
"https://welearn.sflep.com/Ajax/SCO.aspx",
634791
data={
635792
"action": "startsco160928",
@@ -641,7 +798,7 @@ def run(self):
641798
"Referer": f"https://welearn.sflep.com/Student/StudyCourse.aspx?cid={_global.cid}&classid={_global.classid}&sco={id}"
642799
},
643800
)
644-
response = client.post(
801+
response = session.post(
645802
"https://welearn.sflep.com/Ajax/SCO.aspx",
646803
data={
647804
"action": "setscoinfo",
@@ -674,7 +831,7 @@ def run(self):
674831
pass
675832
continue
676833
else: # 第二种刷课法
677-
response = client.post(
834+
response = session.post(
678835
"https://welearn.sflep.com/Ajax/SCO.aspx",
679836
data={
680837
"action": "savescoinfo160928",
@@ -736,10 +893,12 @@ def _http_request_with_retry(self, method, url, **kwargs):
736893
"""带有重试机制的 HTTP 请求"""
737894
for attempt in range(self.max_retries):
738895
try:
739-
response = client.request(method, url, **kwargs)
740-
response.raise_for_status()
896+
if method.upper() == "GET":
897+
response = self.session.get(url, **kwargs)
898+
else:
899+
response = self.session.post(url, **kwargs)
741900
return response
742-
except (httpx.TimeoutException, httpx.HTTPError, Exception) as e:
901+
except Exception as e:
743902
if attempt < self.max_retries - 1:
744903
log_message(
745904
f"请求 {url} 失败 ({str(e)}),{self.retry_delay} 秒后重试...",
@@ -869,6 +1028,7 @@ def run(self):
8691028
"""线程主函数"""
8701029
try:
8711030
state.task_status = "away_from_keyboard"
1031+
self.session = build_tls_session()
8721032

8731033
# 预加载所有选择单元的小节以计算总任务数,并保持与文档描述一致
8741034
units_sections: List[List[Dict[str, Any]]] = []

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ dependencies = [
88
"bs4>=0.0.2",
99
"flask>=3.1.2",
1010
"httpx>=0.28.1",
11+
"tls-client>=1.0.1",
1112
]

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
flask
22
bs4
3-
httpx
3+
httpx
4+
tls-client

0 commit comments

Comments
 (0)