vfs_registration_bot.py 20 KB

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