|
|
@@ -44,27 +44,6 @@ class TlsPlugin(IVSPlg):
|
|
|
def health_check(self) -> bool:
|
|
|
return self.is_healthy
|
|
|
|
|
|
- def _save_debug_html(self, content: str, prefix: str = "debug"):
|
|
|
- save_dir = "debug_pages"
|
|
|
- if not os.path.exists(save_dir):
|
|
|
- os.makedirs(save_dir)
|
|
|
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
- filename = f"{save_dir}/{prefix}_{self.group_id}_{timestamp}.html"
|
|
|
- with open(filename, "w", encoding="utf-8") as f:
|
|
|
- f.write(content)
|
|
|
- VSC_INFO("tls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
|
|
|
-
|
|
|
- def _get_proxy_url(self):
|
|
|
- # 构造代理
|
|
|
- proxy_url = ""
|
|
|
- if self.config.proxy.ip:
|
|
|
- s = self.config.proxy
|
|
|
- if s.username:
|
|
|
- proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
|
|
|
- else:
|
|
|
- proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
|
|
|
- return proxy_url
|
|
|
-
|
|
|
def create_session(self):
|
|
|
"""
|
|
|
创建会话:处理 Cloudflare -> 登录 -> 获取 Travel Group
|
|
|
@@ -73,7 +52,7 @@ class TlsPlugin(IVSPlg):
|
|
|
curlopt = {
|
|
|
const.CurlOpt.MAXAGE_CONN: 1800,
|
|
|
const.CurlOpt.MAXLIFETIME_CONN: 1800,
|
|
|
- const.CurlOpt.VERBOSE: False,
|
|
|
+ const.CurlOpt.VERBOSE: self.config.debug,
|
|
|
}
|
|
|
|
|
|
self.session = requests.Session(
|
|
|
@@ -106,7 +85,9 @@ class TlsPlugin(IVSPlg):
|
|
|
'User-Agent': self.user_agent,
|
|
|
}
|
|
|
resp = self._perform_request("GET", login_page, headers=headers, params=params)
|
|
|
- self._save_debug_html(resp.text, 'Login_Page')
|
|
|
+
|
|
|
+ if self.config.debug:
|
|
|
+ self._save_debug_html(resp.text, prefix='Tls_Login_Page')
|
|
|
|
|
|
# 解析 Keycloak 登录地址
|
|
|
soup = BeautifulSoup(resp.text, 'html.parser')
|
|
|
@@ -138,8 +119,10 @@ class TlsPlugin(IVSPlg):
|
|
|
}
|
|
|
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
|
resp = self._perform_request("POST", authenticate_url, headers=headers, data=payload)
|
|
|
- self._save_debug_html(resp.text, 'Travel_Groups_Page')
|
|
|
+ if self.config.debug:
|
|
|
+ self._save_debug_html(resp.text, prefix='Tls_Travel_Groups_Page')
|
|
|
# 6. 解析 Travel Groups
|
|
|
+ self._check_page_is_session_expired_or_invalid("My travel group", resp.text)
|
|
|
groups = self._parse_travel_groups(resp.text)
|
|
|
|
|
|
# 选择匹配城市的 Group
|
|
|
@@ -150,7 +133,7 @@ class TlsPlugin(IVSPlg):
|
|
|
break
|
|
|
|
|
|
if not self.travel_group:
|
|
|
- raise NotFoundError(message=f"No group found for city {target_city}")
|
|
|
+ raise NotFoundError(message=f"No matched group found for city {target_city}")
|
|
|
VSC_INFO("tls_plg", "[%s] Session created. Group: %s", self.group_id, self.travel_group['group_number'])
|
|
|
|
|
|
def query(self) -> VSQueryResult:
|
|
|
@@ -159,8 +142,8 @@ class TlsPlugin(IVSPlg):
|
|
|
embassy = self.free_config.get('center', {})
|
|
|
group_num = self.travel_group['group_number']
|
|
|
interest_month = self.free_config.get("interest_month", time.strftime("%m-%Y"))
|
|
|
+ max_retries = self.free_config.get("max_retries", 2)
|
|
|
|
|
|
-
|
|
|
url = f'https://visas-fr.tlscontact.com/en-us/{group_num}/workflow/appointment-booking'
|
|
|
params = {
|
|
|
'location': embassy["code"],
|
|
|
@@ -173,15 +156,29 @@ class TlsPlugin(IVSPlg):
|
|
|
'user-agent': self.user_agent,
|
|
|
}
|
|
|
|
|
|
- resp = self._perform_request("GET", url, headers=headers, params=params)
|
|
|
-
|
|
|
- self._save_debug_html(resp.text, 'Query_Slot_Page')
|
|
|
- self._check_session_expired_page(resp.text)
|
|
|
+ for attempt in range(1, max_retries + 1):
|
|
|
+ try:
|
|
|
+ resp = self._perform_request("GET", url, headers=headers, params=params)
|
|
|
+ if self.config.debug:
|
|
|
+ self._save_debug_html(resp.text, prefix='Tls_Query_Slot_Page')
|
|
|
+ break # ✅ 请求成功,跳出重试循环
|
|
|
+
|
|
|
+ except PermissionDeniedError:
|
|
|
+ VSC_WARN(
|
|
|
+ "tls_plg",
|
|
|
+ "[TLS] Query Appointment-booking blocked (403), attempt %d/%d",
|
|
|
+ attempt, max_retries
|
|
|
+ )
|
|
|
+
|
|
|
+ # 最后一次就不再绕盾了
|
|
|
+ if attempt >= max_retries:
|
|
|
+ raise PermissionDeniedError()
|
|
|
|
|
|
- # 检测关键词
|
|
|
- if not "availableAppointments" in resp.text:
|
|
|
- raise NotFoundError(message='Query result not found availableAppointments')
|
|
|
+ self._solve_cloudflare5S_challenge()
|
|
|
+ VSC_INFO("tls_plg", "[TLS] Cloudflare bypass success, retrying...")
|
|
|
+ continue
|
|
|
|
|
|
+ self._check_page_is_session_expired_or_invalid('Book your appointment', resp.text)
|
|
|
|
|
|
# 3. 解析 Slots
|
|
|
all_slots = self._parse_appointment_slots(resp.text)
|
|
|
@@ -286,7 +283,8 @@ class TlsPlugin(IVSPlg):
|
|
|
body = "".join(body_parts).encode("utf-8")
|
|
|
|
|
|
resp = self.session.post(url, params=params, headers=headers, data=body, allow_redirects=False)
|
|
|
- self._save_debug_html(resp.text, 'Book_Appointment_Page')
|
|
|
+ if self.config.debug:
|
|
|
+ self._save_debug_html(resp.text, prefix='Tls_Book_Appointment_Page')
|
|
|
if resp.status_code == 303:
|
|
|
res.success = True
|
|
|
res.book_date = target_date
|
|
|
@@ -297,15 +295,36 @@ class TlsPlugin(IVSPlg):
|
|
|
res.success = False
|
|
|
return res
|
|
|
|
|
|
+ def _save_debug_html(self, content: str, prefix: str = "debug"):
|
|
|
+ save_dir = "debug_pages"
|
|
|
+ if not os.path.exists(save_dir):
|
|
|
+ os.makedirs(save_dir)
|
|
|
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
+ filename = f"{save_dir}/{prefix}_{timestamp}.html"
|
|
|
+ with open(filename, "w", encoding="utf-8") as f:
|
|
|
+ f.write(content)
|
|
|
+ VSC_INFO("tls_plg", "[%s] HTML saved to: %s", self.group_id, filename)
|
|
|
+
|
|
|
+ def _get_proxy_url(self):
|
|
|
+ # 构造代理
|
|
|
+ proxy_url = ""
|
|
|
+ if self.config.proxy.ip:
|
|
|
+ s = self.config.proxy
|
|
|
+ if s.username:
|
|
|
+ proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
|
|
|
+ else:
|
|
|
+ proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
|
|
|
+ return proxy_url
|
|
|
+
|
|
|
def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
|
|
|
"""
|
|
|
统一 HTTP 请求封装,严格复刻 C++ 逻辑:
|
|
|
1. 发送 OPTIONS 请求
|
|
|
2. 发送实际请求
|
|
|
"""
|
|
|
- print(f'[perform request] {method} {url} {data} {json_data} {params}')
|
|
|
resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
|
|
|
- VSC_INFO('tls_plg', resp.text)
|
|
|
+ if self.config.debug:
|
|
|
+ VSC_INFO('tls_plg', f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
|
|
|
if resp.status_code == 200:
|
|
|
return resp
|
|
|
elif resp.status_code == 401:
|
|
|
@@ -414,7 +433,7 @@ class TlsPlugin(IVSPlg):
|
|
|
|
|
|
def _parse_appointment_slots(self, html: str) -> List[Dict]:
|
|
|
slots = []
|
|
|
- pattern = r'availableAppointments\\?":\s*(\[.*?\])(?:,\\?"|\},)'
|
|
|
+ pattern = r'"availableAppointments\\":\s*(\[.*\]),\\"showFlexiAppointment'
|
|
|
match = re.search(pattern, html, re.DOTALL)
|
|
|
|
|
|
if match:
|
|
|
@@ -446,15 +465,17 @@ class TlsPlugin(IVSPlg):
|
|
|
'type': stype,
|
|
|
'cost': cost
|
|
|
})
|
|
|
+ return slots
|
|
|
else:
|
|
|
- VSC_WARN("tls_plg", 'Parsed appointment slots page, but not found availableAppointments')
|
|
|
+ VSC_WARN('tls_plg', 'Parsed appointment slot page, but not found availableAppointments')
|
|
|
return slots
|
|
|
-
|
|
|
- def _check_session_expired_page(self, html: str) -> bool:
|
|
|
+
|
|
|
+ def _check_page_is_session_expired_or_invalid(self, keyword, html: str) -> bool:
|
|
|
if not html:
|
|
|
self.is_healthy = False
|
|
|
raise SessionExpiredOrInvalidError()
|
|
|
- if 'availableAppointments' not in html:
|
|
|
+
|
|
|
+ if keyword not in html:
|
|
|
if 'redirected automatically' in html.lower():
|
|
|
self.is_healthy = False
|
|
|
raise SessionExpiredOrInvalidError()
|
|
|
@@ -462,5 +483,8 @@ class TlsPlugin(IVSPlg):
|
|
|
self.is_healthy = False
|
|
|
raise SessionExpiredOrInvalidError()
|
|
|
if 'session expired!' in html.lower() and 'for security reasons, your session has expired. please log in again to continue.' in html.lower() and 'you will be redirected automatically in 10 seconds.' in html.lower():
|
|
|
+ self.is_healthy = False
|
|
|
+ raise SessionExpiredOrInvalidError()
|
|
|
+ if 'temporarily blocked!' in html.lower() and 'Your session has been temporarily suspended due to the high number of your access to this page.' in html.lower() and 'You can try to access your account again in 2 hours.' in html.lower():
|
|
|
self.is_healthy = False
|
|
|
raise SessionExpiredOrInvalidError()
|