de_plugin.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. import time
  2. import json
  3. import random
  4. import re
  5. import os
  6. import base64
  7. from datetime import datetime
  8. from typing import List, Dict, Optional, Any, Callable
  9. from urllib.parse import urljoin
  10. from curl_cffi import requests, const
  11. from bs4 import BeautifulSoup
  12. from vs_plg import IVSPlg
  13. from vs_types import VSPlgConfig, VSQueryResult, VSBookResult, TimeSlot, DateAvailability, AvailabilityStatus, NotFoundError, PermissionDeniedError, RateLimiteddError, SessionExpiredOrInvalidError, BizLogicError
  14. from toolkit.vs_cloud_api import VSCloudApi
  15. def to_yyyymmdd(data_str: str, date_str_format: str, target_format: str="%Y-%m-%d"):
  16. # 转换日期到YYYY-MM-DD 固定格式
  17. dt = datetime.strptime(data_str, date_str_format)
  18. return dt.strftime("%Y-%m-%d")
  19. def get_alias_email(email: str, new_domain: str = "gmail-app.com") -> str:
  20. """
  21. 将邮箱域名替换为指定域名(默认 gmail-app.com)
  22. """
  23. if "@" not in email:
  24. raise ValueError(f"Invalid email: {email}")
  25. local_part, _ = email.rsplit("@", 1)
  26. return f"{local_part}@{new_domain}"
  27. class DePlugin(IVSPlg):
  28. """
  29. Germany (Visametric) 签证预约插件
  30. 适配 Visametric Ireland -> Germany 流程
  31. """
  32. def __init__(self, group_id: str):
  33. self.group_id = group_id
  34. self.config: Optional[VSPlgConfig] = None
  35. self.free_config: Dict[str, Any] = {}
  36. self.logger = None
  37. self.session: Optional[requests.Session] = None
  38. # 状态
  39. self.is_healthy = True
  40. # 关键上下文变量 (从页面提取)
  41. self.base_url = "https://ie-appointment.visametric.com"
  42. self.csrf_token = ""
  43. self.personal_info_val = ""
  44. self.email_val_control = ""
  45. # 默认 OCR 服务地址
  46. self.local_service_url = "http://127.0.0.1:8085"
  47. self.session_create_time: float = 0
  48. def get_group_id(self) -> str:
  49. return self.group_id
  50. def set_log(self, logger: Callable[[str], None]) -> None:
  51. self.logger = logger
  52. def set_config(self, config: VSPlgConfig):
  53. self.config = config
  54. self.free_config = config.free_config or {}
  55. if self.free_config.get("base_url"):
  56. self.base_url = self.free_config["base_url"].rstrip('/')
  57. if self.free_config.get("local_service_url"):
  58. self.local_service_url = self.free_config["local_service_url"]
  59. def health_check(self) -> bool:
  60. if not self.is_healthy:
  61. return False
  62. if self.session is None:
  63. return False
  64. if self.config.session_max_life > 0:
  65. current_time = time.time()
  66. elapsed_time = current_time - self.session_create_time
  67. if elapsed_time > self.config.session_max_life * 60:
  68. self._log(f"Session Life ({int(elapsed_time)}s) out of max life limit ({self.config.session_max_life * 60}s), mark as unhealth session")
  69. return False
  70. return True
  71. def create_session(self):
  72. """
  73. 初始化会话:过盾 -> 获取 CSRF -> 识别图片验证码 -> 提交验证码 -> 获取上下文
  74. """
  75. # 1. 初始化 Session
  76. curlopt = {
  77. const.CurlOpt.MAXAGE_CONN: 1800,
  78. const.CurlOpt.MAXLIFETIME_CONN: 1800,
  79. const.CurlOpt.VERBOSE: self.config.debug,
  80. }
  81. self.session = requests.Session(
  82. proxy=self._get_proxy_url(),
  83. impersonate="chrome124",
  84. curl_options=curlopt,
  85. use_thread_local_curl=False,
  86. http_version=const.CurlHttpVersion.V2TLS
  87. )
  88. # 2. 访问首页,获取 CSRF 和 Captcha 图片
  89. # Visametric 首页通常有 Cloudflare
  90. url_home = f"{self.base_url}/en"
  91. # 尝试过盾
  92. self._solve_cloudflare5S_challenge()
  93. default_headers = self._get_headers()
  94. default_headers.pop("X-Requested-With")
  95. resp = self._perform_request('GET', url_home, headers=default_headers)
  96. if self.config.debug:
  97. self._save_debug_html(resp.text, prefix="VisaMetric_Home_Page")
  98. html = resp.text
  99. soup = BeautifulSoup(html, 'html.parser')
  100. meta = soup.find('meta', {'name': 'csrf-token'})
  101. if not meta:
  102. raise NotFoundError(message='Missing csrf-token in html')
  103. self.csrf_token = meta.get('content', '')
  104. # 提取验证码图片 Base64, 正则匹配: "data:image/png;base64," + "..."
  105. match = re.search(r'"data:image/png;base64,"\s*\+\s*"(.*?)"', html)
  106. if not match:
  107. raise NotFoundError(message="Captcha image not found")
  108. captcha_b64 = base64.b64decode(match.group(1))
  109. # 3. 识别验证码
  110. resp = requests.post(
  111. f'{self.local_service_url}/predict/visametric',
  112. data=captcha_b64,
  113. headers={"Content-Type": "application/octet-stream"},
  114. timeout=10
  115. )
  116. if resp.status_code != 200:
  117. raise BizLogicError(message='Captcha ocr server failed')
  118. captcha_code = resp.json().get('data', '').replace('$', '')
  119. self._log(f"Captcha recognized: {captcha_code}")
  120. # 4. 提交验证码 (/appointment-form)
  121. # 这一步是为了让服务器验证 Session,并返回包含 personalinfo 的页面
  122. self._submit_captcha(captcha_code)
  123. self.session_create_time = time.time()
  124. self._log("Session created successfully.")
  125. def query(self) -> VSQueryResult:
  126. """
  127. 查询可用日期 (/getdate)
  128. """
  129. res = VSQueryResult()
  130. # 构造 Payload (参考 get_slot_day) 这里的 ID 需要根据实际情况配置
  131. consular_id = self.free_config.get("consularid", "1") # 1=Ireland?
  132. max_retries = self.free_config.get("slot_query_max_retries", 2)
  133. url = f"{self.base_url}/en/getdate"
  134. payload = {
  135. "consularid": consular_id,
  136. "exitid": "1",
  137. "servicetypeid": "1",
  138. "calendarType": "2",
  139. "totalperson": "1"
  140. }
  141. default_headers = self._get_headers()
  142. default_headers['X-CSRF-TOKEN'] = self.csrf_token
  143. for attempt in range(1, max_retries + 1):
  144. try:
  145. resp = self._perform_request('POST', url, data=payload, headers=default_headers)
  146. break # ✅ 请求成功,跳出重试循环
  147. except PermissionDeniedError:
  148. self._log(f"Getdate blocked (403), attempt {attempt}/{max_retries}")
  149. # 最后一次就不再绕盾了
  150. if attempt >= max_retries:
  151. raise PermissionDeniedError()
  152. self._solve_cloudflare5S_challenge()
  153. self._log("Cloudflare bypass success, retrying...")
  154. continue
  155. # Visametric 返回 JSON: {"getDateEnable": ["15-01-2026", "16-01-2026"]}
  156. j = resp.json()
  157. dates = j.get("getDateEnable", [])
  158. res.city = self.free_config.get('city', '')
  159. res.country = self.free_config.get('country', '')
  160. res.visa_type = self.free_config.get('visa_type', '')
  161. res.routing_key = self.free_config.get('routing_key', '')
  162. if dates:
  163. res.success = True
  164. res.availability_status = AvailabilityStatus.Available
  165. # Visametric 返回 DD-MM-YYYY → 标准化为 YYYY-MM-DD
  166. res.earliest_date = to_yyyymmdd(dates[0], "%d-%m-%Y")
  167. res.availability = [
  168. DateAvailability(
  169. date=to_yyyymmdd(d, "%d-%m-%Y"),
  170. times=[],
  171. )
  172. for d in dates
  173. ]
  174. else:
  175. # 查询成功,但没有可用日期
  176. res.success = False
  177. res.availability_status = AvailabilityStatus.NoneAvailable
  178. return res
  179. def book(self, slot_info: VSQueryResult, user_inputs: Dict) -> VSBookResult:
  180. """
  181. 执行预约:选择日期 -> 选择时间 -> 发邮件 -> 填表 -> 提交
  182. """
  183. res = VSBookResult()
  184. # 1. 筛选日期
  185. available_dates = [da.date for da in slot_info.availability]
  186. exp_start = user_inputs.get('expected_start_date', '')
  187. exp_end = user_inputs.get('expected_end_date', '')
  188. valid_dates = self._filter_dates(available_dates, exp_start, exp_end)
  189. if not valid_dates:
  190. raise NotFoundError(message="No dates match user constraints")
  191. target_date = random.choice(valid_dates)
  192. self._log(f"Selected date: {target_date}")
  193. # 2. 获取时间 (/senddate)
  194. time_slot = self._get_slot_time(target_date)
  195. self._log(f"Selected time: {time_slot['time']}")
  196. # 3. 触发邮件流程 (Step 1: /jky45fgd)
  197. alias_email = get_alias_email(user_inputs.get("email"), new_domain='gmail-app.com')
  198. self._send_email_step1(alias_email)
  199. # 4. 触发邮件流程 (Step 2: /confirmCodeSendMail) 这一步会发送包含验证码的邮件 根据原代码逻辑: send_email("0") 触发发送
  200. self._send_email_step2("0")
  201. # 5. 读取 OTP
  202. # Visametric 邮件发送者
  203. recipient = alias_email
  204. otp_code = self._read_otp_email(recipient)
  205. # 6. 提交验证码并确认 (/personal/appointment/create)
  206. book_res_html = self._confirm_appointment(target_date, time_slot, user_inputs, otp_code, alias_email)
  207. if "complete all required fields" in book_res_html.lower():
  208. raise BizLogicError(message='Comfirm appointment response <complete all required fields>')
  209. # 7. 提取结果
  210. match = re.search(r'https:\/\/checkout\.stripe\.com\/c\/pay\/[^\s"]+', book_res_html)
  211. res.success = True
  212. res.fee_amount = 3000
  213. res.fee_currency = 'EUR'
  214. res.book_date = target_date
  215. res.book_time = time_slot['time']
  216. if match:
  217. res.payment_link = match.group(0)
  218. self._log(f"Payment Link Found: {res.payment_link}")
  219. return res
  220. # ---------------------------------------------------------
  221. # 辅助方法
  222. # ---------------------------------------------------------
  223. def _log(self, message):
  224. if self.logger:
  225. self.logger(f'[DePlugin] [{self.group_id}] {message}')
  226. def _get_headers(self) -> Dict[str, str]:
  227. """基础 Header"""
  228. return {
  229. "Accept": "*/*",
  230. "Accept-Language": "en,zh-CN;q=0.9,zh;q=0.8",
  231. "Origin": self.base_url,
  232. "Referer": f"{self.base_url}/en/appointment-form", # 默认 Referer
  233. "X-Requested-With": "XMLHttpRequest"
  234. }
  235. def _get_proxy_url(self):
  236. # 构造代理
  237. proxy_url = ""
  238. if self.config.proxy.ip:
  239. s = self.config.proxy
  240. if s.username:
  241. proxy_url = f"{s.scheme}://{s.username}:{s.password}@{s.ip}:{s.port}"
  242. else:
  243. proxy_url = f"{s.scheme}://{s.ip}:{s.port}"
  244. return proxy_url
  245. def _save_debug_html(self, content: str, prefix: str = "debug"):
  246. save_dir = "debug_pages"
  247. if not os.path.exists(save_dir):
  248. os.makedirs(save_dir)
  249. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  250. filename = f"{save_dir}/{prefix}_{timestamp}.html"
  251. with open(filename, "w", encoding="utf-8") as f:
  252. f.write(content)
  253. self._log(f"HTML saved to: {filename}")
  254. def _submit_captcha(self, code):
  255. url = f"{self.base_url}/en/appointment-form"
  256. payload = {
  257. '_token': self.csrf_token,
  258. 'cpJvnsControl': '',
  259. 'mailConfirmCode': code
  260. }
  261. headers = self._get_headers()
  262. headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
  263. resp = self._perform_request('POST', url, data=payload, headers=headers)
  264. if self.config.debug:
  265. self._save_debug_html(resp.text, prefix="VisaMetric_Make_Appointment_Page")
  266. # 关键:提交验证码后,返回的 HTML 中包含了后续需要的加密参数
  267. match_pi = re.search(r"personalinfo:\s*'([^']*)'", resp.text)
  268. if match_pi:
  269. self.personal_info_val = match_pi.group(1)
  270. # emailValControl: '...'
  271. match_ev = re.search(r"emailValControl:\s*'([^']*)'", resp.text)
  272. if match_ev:
  273. self.email_val_control = match_ev.group(1)
  274. if not self.personal_info_val:
  275. raise NotFoundError(message="Personalinfo not found in captcha response")
  276. soup = BeautifulSoup(resp.text, 'html.parser')
  277. meta = soup.find('meta', {'name': 'csrf-token'})
  278. if not meta:
  279. raise NotFoundError(message='Missing csrf-token in html')
  280. self.csrf_token = meta.get('content', '')
  281. def _get_slot_time(self, date) -> Optional[Dict]:
  282. url = f"{self.base_url}/en/senddate"
  283. dt_m = datetime.strptime(date, "%Y-%m-%d")
  284. converted_date = dt_m.strftime("%d-%m-%Y")
  285. payload = {
  286. "fulldate": converted_date,
  287. "totalperson": "1",
  288. "set_new_consular_id": self.free_config.get("consularid", "1"),
  289. "set_new_exit_office_id": "1",
  290. "calendarType": "2",
  291. "set_new_service_type_id": "1",
  292. "personalinfo": self.personal_info_val
  293. }
  294. headers = self._get_headers()
  295. headers['X-CSRF-TOKEN'] = self.csrf_token # 这里需要 CSRF
  296. headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
  297. resp = self._perform_request('POST', url, data=payload, headers=headers)
  298. soup = BeautifulSoup(resp.text, 'html.parser')
  299. buttons = soup.find_all('button')
  300. slots = []
  301. for btn in buttons:
  302. i_tag = btn.find('i')
  303. if i_tag:
  304. time_val = i_tag.next_sibling.strip()
  305. slots.append({
  306. 'time': time_val,
  307. 'data_id': btn.get('data-id'),
  308. 'data_all': btn.get('data-all')
  309. })
  310. if slots:
  311. return random.choice(slots)
  312. else:
  313. raise NotFoundError(message='Not slot time available')
  314. def _send_email_step1(self, email):
  315. url = f"{self.base_url}/en/jky45fgd"
  316. payload = {
  317. "emailCheck": email,
  318. "personalinfo": self.personal_info_val
  319. }
  320. headers = self._get_headers()
  321. headers['X-CSRF-TOKEN'] = self.csrf_token
  322. headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
  323. self._perform_request('POST', url, data=payload, headers=headers)
  324. def _send_email_step2(self, code_val):
  325. url = f"{self.base_url}/en/confirmCodeSendMail"
  326. payload = {
  327. "confirmCode": code_val,
  328. "emailValControl": self.email_val_control
  329. }
  330. headers = self._get_headers()
  331. headers['X-CSRF-TOKEN'] = self.csrf_token
  332. headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
  333. self._perform_request('POST', url, data=payload, headers=headers)
  334. def _read_otp_email(self, recipient) -> str:
  335. """
  336. 读取 OTP 邮件
  337. """
  338. master_email = "visafly666@gmail.com"
  339. sender = 'Visametric - verify at visametric.com'
  340. subject_keywords = 'Verification Code'
  341. body_keywords = 'Verification code'
  342. now_utc = datetime.utcnow()
  343. formatted_utc_time = now_utc.strftime("%Y-%m-%d %H:%M:%S")
  344. self._log(f"Waiting for OTP email sent after {formatted_utc_time}...")
  345. # 3. 轮询查收
  346. for i in range(12):
  347. content_out = VSCloudApi.Instance().fetch_mail_content(
  348. master_email,
  349. sender,
  350. recipient,
  351. subject_keywords,
  352. body_keywords,
  353. formatted_utc_time,
  354. 300
  355. )
  356. if content_out:
  357. match = re.search(r'\b\d{6}\b', content_out)
  358. if match:
  359. otp = match.group(0)
  360. self._log(f"OTP code found: {otp}")
  361. return otp
  362. time.sleep(5)
  363. raise NotFoundError(message="OTP email not found (timeout)")
  364. def _confirm_appointment(self, date, slot_data, user_inputs, otp, alias_email):
  365. url = f"{self.base_url}/en/personal/appointment/create"
  366. # 处理日期格式 YYYY-MM-DD
  367. def _get_dob(d_str):
  368. try: return datetime.strptime(d_str[:10], "%Y-%m-%d")
  369. except: return datetime.now()
  370. dob = _get_dob(user_inputs.get('birthday', ''))
  371. payload = {
  372. "_token": self.csrf_token,
  373. "country": str(self.free_config.get("consularid", "1")),
  374. "visitingcountry": str(self.free_config.get("consularid", "1")),
  375. "city": "6", # Dublin? 需配置
  376. "office": "1",
  377. "officetype": "1",
  378. "totalPerson": "1",
  379. "name1": user_inputs.get('first_name', '').upper(),
  380. "surname1": user_inputs.get('last_name', '').upper(),
  381. "nationality1": "2", # 假设值
  382. "birthday1": str(dob.day),
  383. "birthmonth1": str(dob.month),
  384. "birthyear1": str(dob.year),
  385. "passport1": user_inputs.get('passport_no'),
  386. # 原代码 passport_expried 是 DD-MM-YYYY
  387. "passportExpirationDate1": datetime.strptime(user_inputs.get('passport_expiry_date', '')[:10], "%Y-%m-%d").strftime("%d-%m-%Y"),
  388. "email1": alias_email,
  389. "phone1": user_inputs.get('phone_no'),
  390. "alternativephone1": "",
  391. # 其他 person 留空
  392. "name2": "", "surname2": "", "nationality2": "0", "birthday2": "0", "birthmonth2": "0", "birthyear2": "0", "passport2": "", "passportExpirationDate2": "", "email2": alias_email, "phone2": user_inputs.get('phone_no'), "alternativephone2": "",
  393. "name3": "", "surname3": "", "nationality3": "0", "birthday3": "0", "birthmonth3": "0", "birthyear3": "0", "passport3": "", "passportExpirationDate3": "", "email3": alias_email, "phone3": user_inputs.get('phone_no'), "alternativephone3": "",
  394. "name4": "", "surname4": "", "nationality4": "0", "birthday4": "0", "birthmonth4": "0", "birthyear4": "0", "passport4": "", "passportExpirationDate4": "", "email4": alias_email, "phone4": user_inputs.get('phone_no'), "alternativephone4": "",
  395. "mailConfirmCode": otp,
  396. "ctval": slot_data['data_id'],
  397. "qtallvert": slot_data['data_all'],
  398. "oldofficetype": "1",
  399. "oldtotalperson": "1",
  400. "rePaymentControl": "0",
  401. # 关键:View Set
  402. "view_set_app_country": "Schengen - Tourism/Family&Friend Visit/Transit Visa/Other Purposes", # 需配置
  403. "view_set_app_office": "Dublin",
  404. "view_set_app_service_type": "NORMAL",
  405. "cargoactive": "0",
  406. "setnewcalendarstatus": "2",
  407. "availableDaycontrol": "0",
  408. "travelStartDate": datetime.strptime(user_inputs.get('travel_date', '')[:10], "%Y-%m-%d").strftime("%d-%m-%Y"),
  409. "personalapproveTerms": "1"
  410. }
  411. headers = self._get_headers()
  412. headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
  413. resp = self._perform_request('POST', url, data=payload, headers=headers)
  414. return resp.text
  415. def _filter_dates(self, dates: List[str], start_str: str, end_str: str) -> List[str]:
  416. """
  417. 根据用户的期望范围筛选可用日期
  418. :param dates: API 返回的可用日期列表 (YYYY-MM-DD)
  419. :param start_str: 用户期望开始日期 (YYYY-MM-DD)
  420. :param end_str: 用户期望结束日期 (YYYY-MM-DD)
  421. :return: 符合要求的日期列表
  422. """
  423. # 如果没有设置范围,则不过滤,返回所有日期
  424. if not start_str or not end_str:
  425. return dates
  426. valid_dates = []
  427. # 截取前10位以防带有时分秒
  428. s_date = datetime.strptime(start_str[:10], "%Y-%m-%d")
  429. e_date = datetime.strptime(end_str[:10], "%Y-%m-%d")
  430. for date_str in dates:
  431. curr_date = datetime.strptime(date_str, "%Y-%m-%d")
  432. # 比较范围 (闭区间)
  433. if s_date <= curr_date <= e_date:
  434. valid_dates.append(date_str)
  435. random.shuffle(valid_dates)
  436. return valid_dates
  437. def _perform_request(self, method, url, headers=None, data=None, json_data=None, params=None):
  438. """
  439. 统一 HTTP 请求封装,严格复刻 C++ 逻辑:
  440. 1. 发送 OPTIONS 请求
  441. 2. 发送实际请求
  442. """
  443. resp = self.session.request(method, url, headers=headers, data=data, json=json_data, params=params, timeout=30)
  444. if self.config.debug:
  445. self._log(f'[perform request] Response={resp.text}\nMethod={method}, Url={url}, Data={data}, JsonData={json_data}, Params={params}')
  446. if resp.status_code == 200:
  447. return resp
  448. elif resp.status_code in [401, 419]:
  449. self.is_healthy = False
  450. raise SessionExpiredOrInvalidError()
  451. elif resp.status_code == 403:
  452. raise PermissionDeniedError()
  453. elif resp.status_code == 429:
  454. self.is_healthy = False
  455. raise RateLimiteddError()
  456. else:
  457. raise BizLogicError(message=f"HTTP Error {resp.status_code}: {resp.text[:100]}")
  458. def _solve_cloudflare5S_challenge(self):
  459. """
  460. 解决 Cloudflare 5s 盾
  461. """
  462. self._log(f"Solving Cloudflare 5s...")
  463. website_url = f'{self.base_url}/en'
  464. # 1. 格式化代理字符串, 这里的接口要求格式通常是: host:port:user:pass (根据你的脚本示例)
  465. p = self.config.proxy
  466. if p.username:
  467. proxy_str = f"{p.ip}:{p.port}:{p.username}:{p.password}"
  468. else:
  469. proxy_str = f"{p.ip}:{p.port}"
  470. # 2. 提交任务
  471. task_id = VSCloudApi.Instance().create_task(
  472. command="AntiCloudflareTask",
  473. args={
  474. "proxy": proxy_str,
  475. "websiteUrl": website_url
  476. }
  477. )
  478. result_data = VSCloudApi.Instance().get_task_result(task_id, timeout=60)
  479. task_result = result_data.get("result", {}).get("token")
  480. cookies_list = task_result.get('cookies', [])
  481. for cookie in cookies_list:
  482. if cookie['name'] in ['__cf_bm', 'cf_clearance']:
  483. self.session.cookies.set(
  484. cookie['name'],
  485. cookie['value'],
  486. domain=cookie['domain'],
  487. path='/'
  488. )
  489. self.session.headers['User-Agent'] = task_result.get('userAgent')
  490. self._log("Cloudflare 5s challenge solved.")