vfs_registration_bot.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. import os
  2. import random
  3. import socket
  4. import json
  5. import time
  6. import string
  7. import logging
  8. import base64
  9. import requests
  10. from datetime import datetime, timezone
  11. from urllib.parse import urlparse, urlencode
  12. from bs4 import BeautifulSoup
  13. from cryptography.hazmat.primitives import serialization, hashes
  14. from cryptography.hazmat.primitives.asymmetric import padding
  15. from cryptography.hazmat.backends import default_backend
  16. from DrissionPage import ChromiumPage, ChromiumOptions
  17. from vs_types import RateLimiteddError, BizLogicError
  18. from utils.cloudflare_bypass_for_scraping import CloudflareBypasser
  19. # --- 配置日志 ---
  20. logging.basicConfig(
  21. level=logging.INFO,
  22. format='%(asctime)s [%(levelname)s] %(message)s',
  23. datefmt='%H:%M:%S'
  24. )
  25. logger = logging.getLogger("VFSRegistrar")
  26. # --- 常量 ---
  27. VFS_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
  28. MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuupFgB+lYIOtSxrRoHzc
  29. LmCZKJ6+oSbgqgOPzFMM0TasOeLw0NXEn1XfIzXdx75+tegNKwyIZumoh0yhubKs
  30. t59GV321kN0iquYRHrdh3ygfDDHlS9rROQeBqRga0ncSADtbLMrBPqXJjPCoV76y
  31. t92towriKoH75BhiazY0mghm4LjmAWrV0u/GNpV3tk9bxbtHEXGaFmxCJqjg+7x6
  32. 1e5wXLfvpj9w1QsiSWOSJxLOyICz/9ByxXycQQFdNmjnnnwco9Gt/Mi33NYH71j0
  33. 5oXIjklFC4lvJqaqSY5lS7Vwb9oCt9zX9J0Yz4z4e/3V+0jgRnWOFGofyks4FKe2
  34. GQIDAQAB
  35. -----END PUBLIC KEY-----"""
  36. def upload_account_to_server(account):
  37. """
  38. 将注册成功的账号上报到中心服务器
  39. """
  40. api_url = 'https://visafly.top/api/account/add'
  41. api_token = 'tok_e946329a60ff45ba807f3f41b0e8b7fc' # 你的 Bearer Token
  42. # 构造请求头
  43. headers = {
  44. 'accept': 'application/json',
  45. 'Authorization': f'Bearer {api_token}',
  46. 'Content-Type': 'application/json'
  47. }
  48. # 构造 extra_data (存放 VFS 特有的国家、领区、手机号信息)
  49. extra_payload = {
  50. "country_code": account.get("country_code"),
  51. "mission_code": account.get("mission_code"),
  52. "phone_country_code": account.get("phone_country_code"),
  53. "phone_number": account.get("phone_number"),
  54. "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  55. }
  56. # 构造主 Payload
  57. payload = {
  58. "pool_name": account.get("pool_name", "default_pool"),
  59. "username": account.get("username"),
  60. "password": account.get("password"),
  61. "extra_data": extra_payload
  62. }
  63. try:
  64. logger.info(f"Uploading account {account['username']} to server...")
  65. resp = requests.post(api_url, json=payload, headers=headers, timeout=10)
  66. if resp.status_code == 200:
  67. logger.info(f"✅ [API Upload Success] Server responded: {resp.text}")
  68. return True
  69. else:
  70. logger.error(f"❌ [API Upload Failed] Status: {resp.status_code}, Body: {resp.text}")
  71. return False
  72. except Exception as e:
  73. logger.error(f"❌ [API Upload Error]: {e}")
  74. return False
  75. class VFSHelper:
  76. """工具方法的静态类"""
  77. @staticmethod
  78. def generate_mobile_number(country_code=353, e164_format=False):
  79. if country_code == 353: # Ireland
  80. prefix = random.choice(['83', '85', '86', '87', '89'])
  81. number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(7)])}"
  82. return f"+353{number}" if e164_format else number
  83. elif country_code == 44: # UK
  84. prefix_second = random.choice(['1', '2', '3', '4', '5', '7', '8', '9'])
  85. number = f"7{prefix_second}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
  86. return f"+44{number}" if e164_format else number
  87. elif country_code == 86: # China
  88. prefixes = ["130", "131", "132", "133", "135", "136", "138", "139", "150", "158", "159", "186"]
  89. prefix = random.choice(prefixes)
  90. number = f"{prefix}{''.join([str(random.randint(0, 9)) for _ in range(8)])}"
  91. return f"+86{number}" if e164_format else number
  92. return "".join([str(random.randint(0, 9)) for _ in range(10)])
  93. @staticmethod
  94. def generate_password(length=12):
  95. chars = string.ascii_letters + string.digits + "@#$%"
  96. while True:
  97. pwd = ''.join(random.choices(chars, k=length))
  98. if (any(c.islower() for c in pwd) and
  99. any(c.isupper() for c in pwd) and
  100. any(c.isdigit() for c in pwd) and
  101. any(c in "@#$%" for c in pwd)):
  102. return pwd
  103. @staticmethod
  104. def encrypt_password(password: str) -> str:
  105. public_key = serialization.load_pem_public_key(
  106. VFS_PUBLIC_KEY_PEM.encode(), backend=default_backend()
  107. )
  108. ciphertext = public_key.encrypt(
  109. password.encode(),
  110. padding.OAEP(
  111. mgf=padding.MGF1(algorithm=hashes.SHA256()),
  112. algorithm=hashes.SHA256(),
  113. label=None
  114. )
  115. )
  116. return base64.b64encode(ciphertext).decode()
  117. @staticmethod
  118. def get_client_source() -> str:
  119. timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
  120. payload = f"GA;{timestamp}Z"
  121. return VFSHelper.encrypt_password(payload)
  122. class BrowserResponse:
  123. """标准化浏览器响应"""
  124. def __init__(self, result_dict):
  125. result_dict = result_dict or {}
  126. self.status_code = result_dict.get('status', 0)
  127. self.text = result_dict.get('body', '')
  128. self.headers = result_dict.get('headers', {})
  129. self.url = result_dict.get('url', '')
  130. self._json = None
  131. def json(self):
  132. if self._json is None:
  133. try:
  134. self._json = json.loads(self.text) if self.text else {}
  135. except json.JSONDecodeError:
  136. self._json = {}
  137. return self._json
  138. class VFSRegistrationBot:
  139. def __init__(self, config):
  140. self.config = config
  141. self.page = None
  142. self.proxy_url = config.get("proxy_url")
  143. def _init_browser(self):
  144. """初始化浏览器配置"""
  145. co = ChromiumOptions()
  146. # 查找可用端口
  147. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  148. s.bind(('', 0))
  149. port = s.getsockname()[1]
  150. co.set_local_port(port)
  151. chrome_path = os.getenv("CHROME_BIN")
  152. if chrome_path:
  153. co.set_paths(browser_path=chrome_path)
  154. if self.proxy_url:
  155. co.set_argument(f'--proxy-server={self.proxy_url}')
  156. co.headless(False) # VFS 验证码通常需要有头模式
  157. co.set_argument('--no-sandbox')
  158. co.set_argument('--disable-gpu')
  159. co.set_argument('--disable-dev-shm-usage')
  160. co.set_argument('--window-size=1920,1080')
  161. co.set_argument('--disable-blink-features=AutomationControlled')
  162. # 创建页面对象
  163. self.page = ChromiumPage(co)
  164. # 设置超时
  165. self.page.set.timeouts(15)
  166. def _perform_js_fetch(self, method, url, headers=None, data=None, json_data=None, retry_count=0):
  167. """注入JS执行Fetch请求,绕过部分指纹检测"""
  168. if not self.page:
  169. raise BizLogicError("Browser not initialized")
  170. if retry_count > 3:
  171. raise BizLogicError("Max retries exceeded for request")
  172. fetch_options = {
  173. "method": method.upper(),
  174. "headers": headers or {},
  175. "credentials": "include"
  176. }
  177. if json_data:
  178. fetch_options['body'] = json.dumps(json_data)
  179. fetch_options['headers']['Content-Type'] = 'application/json'
  180. elif data:
  181. fetch_options['body'] = urlencode(data) if isinstance(data, dict) else str(data)
  182. fetch_options['headers']['Content-Type'] = 'application/x-www-form-urlencoded'
  183. logger.debug(f"Request: {method} {url}")
  184. js_script = f"""
  185. const url = "{url}";
  186. const options = {json.dumps(fetch_options)};
  187. return fetch(url, options)
  188. .then(async response => {{
  189. const text = await response.text();
  190. const headers = {{}};
  191. response.headers.forEach((value, key) => headers[key] = value);
  192. return {{
  193. status: response.status,
  194. body: text,
  195. headers: headers,
  196. url: response.url
  197. }};
  198. }})
  199. .catch(err => ({{ status: 0, body: err.toString() }}));
  200. """
  201. try:
  202. res_dict = self.page.run_js(js_script, timeout=60)
  203. resp = BrowserResponse(res_dict)
  204. if resp.status_code == 200:
  205. return resp
  206. # 处理 Cloudflare 403 拦截
  207. if resp.status_code == 403 and ("cloudflare" in resp.text.lower() or "Just a moment" in resp.text):
  208. logger.warning(f"Cloudflare 403 detected. Retrying ({retry_count+1})...")
  209. new_token = self._refresh_turnstile()
  210. if new_token and json_data and "captcha_api_key" in json_data:
  211. json_data["captcha_api_key"] = new_token
  212. return self._perform_js_fetch(method, url, headers, data, json_data, retry_count + 1)
  213. if resp.status_code == 429:
  214. raise RateLimiteddError(f"Rate Limit: {resp.text[:100]}")
  215. return resp # 返回其他状态码供调用者处理 (如 400 业务错误)
  216. except Exception as e:
  217. logger.error(f"JS Execution Error: {e}")
  218. raise BizLogicError(f"Fetch failed: {e}")
  219. def _handle_cookie_banner(self):
  220. """处理 Cookie 弹窗"""
  221. try:
  222. js = """
  223. var btn = document.getElementById('onetrust-accept-btn-handler');
  224. if(btn) { btn.click(); return true; }
  225. var banner = document.getElementById('onetrust-banner-sdk');
  226. if(banner) { banner.remove(); return true; }
  227. """
  228. self.page.run_js(js)
  229. except:
  230. pass
  231. def _refresh_turnstile(self):
  232. """刷新并获取 Cloudflare Token"""
  233. logger.info("Attempting to refresh Turnstile token...")
  234. try:
  235. self.page.run_js('try{window.turnstile.reset()}catch(e){}')
  236. cf_bypasser = CloudflareBypasser(self.page, log=True)
  237. for i in range(30):
  238. time.sleep(1)
  239. try:
  240. ele = self.page.ele('@name=cf-turnstile-response')
  241. if ele and ele.value:
  242. logger.info("Turnstile token obtained.")
  243. return ele.value
  244. except:
  245. pass
  246. if i > 5:
  247. try:
  248. cf_bypasser.click_verification_button(is_dfs=False)
  249. except:
  250. pass
  251. except Exception as e:
  252. logger.error(f"Turnstile refresh failed: {e}")
  253. return None
  254. def _wait_for_activation_link(self, username, max_wait_sec=60):
  255. """轮询获取激活链接"""
  256. logger.info(f"Waiting for email to {username}...")
  257. start_time = time.time()
  258. master_email = self.config.get("master_email", "visafly666@gmail.com")
  259. # 配置代理
  260. proxies = {
  261. "http": self.proxy_url,
  262. "https": self.proxy_url,
  263. } if self.proxy_url else None
  264. while time.time() - start_time < max_wait_sec:
  265. try:
  266. utc_now_str = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
  267. params = {
  268. "email": master_email,
  269. "sender": 'donotreply at vfsglobal.com',
  270. "recipient": username,
  271. "subjectKeywords": 'Welcome',
  272. "bodyKeywords": 'ActivateAccount',
  273. "sentDate": utc_now_str,
  274. "expiry": str(60)
  275. }
  276. url = f"https://visafly.top/api/email-authorizations/fetch"
  277. headers = {
  278. "Authorization": "Bearer tok_e946329a60ff45ba807f3f41b0e8b7fc",
  279. "Content-Type": "application/json",
  280. "Accept": "application/json, text/plain, */*"
  281. }
  282. # 发送请求,添加代理和超时
  283. # 注意:如果 body 为空,建议传 json={} 而不是 data=""
  284. resp = requests.post(url, headers=headers, params=params, json={}, proxies=proxies, timeout=60)
  285. # --- 关键改进:先检查状态码 ---
  286. if resp.status_code != 200:
  287. logger.warning(f"Email API returned status {resp.status_code}. Body: {resp.text[:100]}")
  288. time.sleep(15)
  289. continue
  290. # --- 关键改进:安全解析 JSON ---
  291. try:
  292. result = resp.json()
  293. except json.JSONDecodeError:
  294. logger.error(f"Failed to decode JSON. Response was: {resp.text[:200]}")
  295. time.sleep(15)
  296. continue
  297. if result.get('code') != 0:
  298. # 这里的错误通常是业务逻辑错误(如:邮件还没到)
  299. logger.debug(f"API Message: {result.get('message')}")
  300. else:
  301. data = result.get('data', {})
  302. content = data.get('body', "")
  303. if content:
  304. soup = BeautifulSoup(content, "html.parser")
  305. link = soup.find("a", string="ActivateAccount")
  306. if link:
  307. raw_link = link["href"]
  308. clean_url = raw_link.replace(" ", "").replace("\n", "").replace("\r", "").strip()
  309. return clean_url
  310. except Exception as e:
  311. logger.warning(f"Error fetching email: {e}")
  312. time.sleep(15)
  313. logger.info("Checking email again...")
  314. return None
  315. def register(self, account):
  316. """执行单个账号注册"""
  317. website = self.config['website']
  318. try:
  319. self._init_browser()
  320. logger.info(f"Opening {website}")
  321. # 1. 设置超时
  322. self.page.set.timeouts(page_load=30, script=30)
  323. # 2. 尝试打开页面
  324. try:
  325. self.page.get(website, retry=0, timeout=30)
  326. except Exception:
  327. # 超时强制停止,防止卡死
  328. logger.warning(f"Page load timed out (Stopped manually). Checking URL...")
  329. self.page.stop_loading()
  330. # 1. 过盾
  331. cf_token = None
  332. cf_bypasser = CloudflareBypasser(self.page, log=True)
  333. for _ in range(40):
  334. time.sleep(1)
  335. current_url = self.page.url
  336. if "page-not-found" in current_url:
  337. logger.error(f"❌ [BLOCKED] Redirected to 'Page Not Found' during check. Aborting.")
  338. return False
  339. # 如果页面标题变成 403 Forbidden
  340. if "403" in self.page.title and "Just a moment" not in self.page.title:
  341. logger.error(f"❌ [BLOCKED] 403 Forbidden detected. Aborting.")
  342. return False
  343. self._handle_cookie_banner()
  344. # 尝试获取 Token
  345. try:
  346. ele = self.page.ele('@name=cf-turnstile-response')
  347. if ele and ele.value:
  348. cf_token = ele.value
  349. if cf_token:
  350. break
  351. except:
  352. pass
  353. # 尝试点击
  354. try:
  355. cf_bypasser.click_verification_button(is_dfs=False)
  356. except:
  357. pass
  358. if not cf_token:
  359. raise BizLogicError("Failed to obtain initial Cloudflare token")
  360. # 2. 构造注册请求
  361. post_data = {
  362. 'emailid': account['username'],
  363. 'password': VFSHelper.encrypt_password(account['password']),
  364. 'confirmPassword': VFSHelper.encrypt_password(account['password']),
  365. 'processPerDataAgreed': True,
  366. 'intTransPerDataAgreed': True,
  367. 'termAndConditionAgreed': True,
  368. 'missioncode': account['mission_code'],
  369. 'countrycode': account['country_code'],
  370. 'languageCode': 'en',
  371. 'dialcode': str(account['phone_country_code']),
  372. 'contact': account['phone_number'],
  373. 'captcha_version': 'cloudflare-v1',
  374. 'captcha_api_key': cf_token,
  375. 'cultureCode': 'en-US',
  376. 'IsSpecialUser': False,
  377. }
  378. headers = {
  379. 'content-type': 'application/json;charset=utf-8',
  380. 'accept': 'application/json, text/plain, */*',
  381. 'route': f"{account['country_code']}/en/{account['mission_code']}",
  382. 'clientsource': VFSHelper.get_client_source(),
  383. }
  384. logger.info(f"Submitting registration for {account['username']}")
  385. resp = self._perform_js_fetch(
  386. 'POST',
  387. 'https://lift-api.vfsglobal.com/user/registration',
  388. headers=headers,
  389. json_data=post_data
  390. )
  391. logger.info(f"Registration response: {resp.text}")
  392. resp_data = resp.json()
  393. if resp_data.get("code") == "200":
  394. logger.info("Registration API success. Waiting for email...")
  395. activate_link = self._wait_for_activation_link(account['username'])
  396. if activate_link:
  397. logger.info(f"Activating account: {activate_link}")
  398. # 在当前浏览器上下文中打开链接,保持环境一致性
  399. # === 关键步骤:打开新标签页并验证结果 ===
  400. # 打开新标签页
  401. activate_tab = self.page.new_tab(activate_link)
  402. try:
  403. # 等待页面加载并查找 "Activation Successful" 文本
  404. # timeout=30 表示最多等待 30 秒
  405. logger.info("Waiting for 'Activation Successful' message on page...")
  406. # DrissionPage 查找包含特定文本的元素
  407. success_ele = activate_tab.ele('Activation Successful', timeout=30)
  408. if success_ele:
  409. logger.info(f"✅ Account {account['username']} activated successfully (Verified).")
  410. return True
  411. else:
  412. # 如果没找到成功提示,尝试读取页面内容找错误原因
  413. body_text = activate_tab.ele('tag:body').text[:200]
  414. logger.error(f"Activation verification failed. Page text: {body_text}")
  415. return False
  416. except Exception as e:
  417. logger.error(f"Error checking activation status: {e}")
  418. return False
  419. finally:
  420. # 无论成功失败,关闭激活标签页,切回主标签
  421. activate_tab.close()
  422. else:
  423. logger.error("Timeout waiting for activation email.")
  424. else:
  425. logger.error(f"Registration failed: {resp_data}")
  426. except Exception as e:
  427. logger.error(f"Registration process exception: {e}", exc_info=True)
  428. finally:
  429. if self.page:
  430. try:
  431. self.page.quit()
  432. except:
  433. pass
  434. return False
  435. # --- 主流程 ---
  436. def generate_account_details(config, pool_name):
  437. """生成账号数据字典"""
  438. rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
  439. username = f"{pool_name}_{rand_suffix}@{config['email_domain']}.com"
  440. phone = VFSHelper.generate_mobile_number(config['phone_country_code'])
  441. return {
  442. 'pool_name': pool_name,
  443. 'country_code': config['country_code'],
  444. 'mission_code': config['mission_code'],
  445. 'phone_country_code': config['phone_country_code'],
  446. 'phone_number': phone,
  447. 'username': username,
  448. 'password': VFSHelper.generate_password(),
  449. }
  450. def main():
  451. # 配置
  452. config = {
  453. "pool_name": "ie.at.booker",
  454. "account_prefix": "ie_at",
  455. "email_domain": "gmail-app",
  456. "master_email": "visafly666@gmail.com",
  457. "proxy_url": "http://127.0.0.1:7890",
  458. "target_count": 5,
  459. "phone_country_code": 353,
  460. "country_code": "irl",
  461. "mission_code": "aut",
  462. "website": "https://visa.vfsglobal.com/irl/en/aut/register",
  463. }
  464. bot = VFSRegistrationBot(config)
  465. success_accounts = []
  466. print(">>> Starting Registration Bot <<<")
  467. while len(success_accounts) < config['target_count']:
  468. account = generate_account_details(config, config['account_prefix'])
  469. logger.info(f"Processing Account: {account['username']}")
  470. is_success = bot.register(account)
  471. if is_success:
  472. success_accounts.append(account)
  473. logger.info(f"Progress: {len(success_accounts)}/{config['target_count']}")
  474. upload_account_to_server(account)
  475. # 保存结果到文件,防止中途退出丢失
  476. with open("registered_accounts.json", "w", encoding='utf-8') as f:
  477. json.dump(success_accounts, f, indent=4, ensure_ascii=False)
  478. else:
  479. logger.warning("Retrying with new account details...")
  480. # 稍微暂停,避免请求过于频繁
  481. time.sleep(5)
  482. print(">>> All tasks completed <<<")
  483. if __name__ == "__main__":
  484. main()