3434from io import StringIO
3535from werkzeug .wrappers .response import Response as WerkzeugResponse
3636from bs4 import BeautifulSoup
37+ import tls_client
38+ import base64
3739
3840
3941# ====================== 全局变量 ======================
@@ -243,49 +245,29 @@ def get_port():
243245
244246def 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
291273def _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 ("/" )
324354def 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" ])
381501def 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():
556680def 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 ]]] = []
0 commit comments