providers.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. """卡号、地址、短信三个外部数据源。"""
  2. from __future__ import annotations
  3. import json
  4. import random
  5. import re
  6. import time
  7. import urllib.error
  8. import urllib.request
  9. CARD_API = "https://api2.suijidaquan.com/api/v2/random-credit-card"
  10. ADDR_API = "https://www.meiguodizhi.com/api/v1/dz"
  11. SMS_API = "http://a.62-us.com/api/get_sms?key=a5d3262e05efaba982aba7cfae20b8bc"
  12. # VISA 起 4,16 位;Mastercard 起 51-55 或 2221-2720,16 位
  13. CARD_BIN_POOLS = {
  14. "visa": [str(random.randint(4, 4)) + "".join(str(random.randint(0, 9)) for _ in range(5)) for _ in range(0)],
  15. }
  16. def _luhn_check_digit(number_without_check: str) -> str:
  17. digits = [int(c) for c in number_without_check]
  18. # 从右往左、每隔一位(即偶数索引位)×2
  19. parity = (len(digits) + 1) % 2 # 让最后一位 parity=0 才需要×2
  20. total = 0
  21. for i, d in enumerate(digits):
  22. if i % 2 == parity:
  23. d *= 2
  24. if d > 9:
  25. d -= 9
  26. total += d
  27. return str((10 - total % 10) % 10)
  28. def _gen_visa_pan() -> str:
  29. # 起 4,再补 14 位随机,最后 1 位 Luhn
  30. body = "4" + "".join(str(random.randint(0, 9)) for _ in range(14))
  31. return body + _luhn_check_digit(body)
  32. def _gen_mastercard_pan() -> str:
  33. # 起 51-55,再补 13 位随机,最后 1 位 Luhn
  34. prefix = str(random.randint(51, 55))
  35. body = prefix + "".join(str(random.randint(0, 9)) for _ in range(13))
  36. return body + _luhn_check_digit(body)
  37. def generate_local_card(brand: str = "visa") -> dict:
  38. """本地随机生成一张 Luhn 合规的 VISA / Mastercard 测试卡。
  39. expiry 取未来 1-4 年的随机月份,CVV 三位随机。
  40. """
  41. brand = (brand or "visa").lower()
  42. if brand == "mastercard":
  43. pan = _gen_mastercard_pan()
  44. else:
  45. pan = _gen_visa_pan()
  46. brand = "visa"
  47. now = time.localtime()
  48. exp_year = (now.tm_year + random.randint(1, 4)) % 100
  49. exp_month = random.randint(1, 12)
  50. expiry = f"{exp_month:02d} / {exp_year:02d}"
  51. cvv = "".join(str(random.randint(0, 9)) for _ in range(3))
  52. return {
  53. "number": pan,
  54. "expiry": expiry,
  55. "cvv": cvv,
  56. "brand": brand,
  57. }
  58. DEFAULT_BROWSER_HEADERS = {
  59. "Accept": "application/json, text/plain, */*",
  60. "Accept-Language": "zh-CN,zh;q=0.9",
  61. "Cache-Control": "no-cache",
  62. "Pragma": "no-cache",
  63. "DNT": "1",
  64. "Priority": "u=1, i",
  65. "Sec-Ch-Ua": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
  66. "Sec-Ch-Ua-Mobile": "?0",
  67. "Sec-Ch-Ua-Platform": '"macOS"',
  68. "Sec-Fetch-Dest": "empty",
  69. "Sec-Fetch-Mode": "cors",
  70. "Sec-Fetch-Site": "same-site",
  71. "User-Agent": (
  72. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
  73. "AppleWebKit/537.36 (KHTML, like Gecko) "
  74. "Chrome/148.0.0.0 Safari/537.36"
  75. ),
  76. }
  77. def _post_json(url: str, body: dict, headers: dict | None = None, timeout: int = 15, log=print) -> tuple[int, str, dict]:
  78. data = json.dumps(body).encode("utf-8")
  79. req = urllib.request.Request(url, data=data, method="POST")
  80. merged = {**DEFAULT_BROWSER_HEADERS, "Content-Type": "application/json;charset=UTF-8"}
  81. merged.update(headers or {})
  82. for k, v in merged.items():
  83. req.add_header(k, v)
  84. started = time.time()
  85. log(f"[http] POST {url} body={json.dumps(body, ensure_ascii=False)}")
  86. try:
  87. with urllib.request.urlopen(req, timeout=timeout) as resp:
  88. status = resp.status
  89. text = resp.read().decode("utf-8", errors="replace")
  90. except urllib.error.HTTPError as exc:
  91. text = exc.read().decode("utf-8", errors="replace")
  92. log(f"[http] {url} HTTP {exc.code} 耗时{int((time.time()-started)*1000)}ms 返回={text[:300]}")
  93. raise
  94. log(f"[http] {url} HTTP {status} 耗时{int((time.time()-started)*1000)}ms 返回={text[:300]}")
  95. return status, text, json.loads(text or "{}")
  96. def _normalize_expiry(expires: str) -> str:
  97. m = re.match(r"^\s*(\d{1,2})\s*/\s*(\d{2,4})\s*$", expires or "")
  98. if not m:
  99. return expires
  100. mm = m.group(1).zfill(2)
  101. yy = m.group(2)
  102. if len(yy) == 4:
  103. yy = yy[2:]
  104. return f"{mm} / {yy}"
  105. def fetch_visa_card(max_attempts: int = 8, log=print, *, prefer_local: bool = True) -> dict:
  106. """获取一张可用的卡。默认本地随机生成(避免接口返回重复卡导致 PayPal CC_LINKED_TO_FULL_ACCOUNT),
  107. 若 prefer_local=False 则走旧的接口逻辑。
  108. """
  109. if prefer_local:
  110. brand = random.choice(["visa", "mastercard"])
  111. card = generate_local_card(brand=brand)
  112. log(f"[card] 本地生成 {card['brand'].upper()} 卡 尾号 {card['number'][-4:]} 有效期 {card['expiry']} CVV {card['cvv']}")
  113. return card
  114. log(f"[card] 开始通过接口获取 VISA 卡,最多重试 {max_attempts} 次")
  115. for attempt in range(1, max_attempts + 1):
  116. log(f"[card] 第 {attempt}/{max_attempts} 次请求 {CARD_API}")
  117. try:
  118. _, _, data = _post_json(
  119. CARD_API,
  120. {"count": 4, "method": "random_credit_card"},
  121. headers={
  122. "Origin": "https://www.suijidaquan.com",
  123. "Referer": "https://www.suijidaquan.com/",
  124. },
  125. log=log,
  126. )
  127. except (urllib.error.URLError, json.JSONDecodeError) as exc:
  128. log(f"[card] 请求异常: {exc!r}")
  129. time.sleep(0.6)
  130. continue
  131. cards = data.get("data") or []
  132. types = [c.get("Credit_Card_Type") for c in cards]
  133. log(f"[card] 本次返回 {len(cards)} 张卡,类型 = {types}")
  134. for c in cards:
  135. if (c.get("Credit_Card_Type") or "").lower() == "visa":
  136. card = {
  137. "number": c["Credit_Card_Number"],
  138. "expiry": _normalize_expiry(c["Expires"]),
  139. "cvv": c["CVV2"],
  140. }
  141. log(f"[card] 命中 VISA 尾号 {card['number'][-4:]} 有效期 {card['expiry']} CVV {card['cvv']}")
  142. return card
  143. log("[card] 本次未拿到 VISA,准备重试")
  144. time.sleep(0.4)
  145. raise RuntimeError(f"已重试 {max_attempts} 次,仍未取到 VISA 卡")
  146. def fetch_us_address(log=print) -> dict:
  147. log(f"[addr] 请求随机美国地址 {ADDR_API}")
  148. try:
  149. _, _, data = _post_json(ADDR_API, {"path": "/", "method": "address"}, log=log)
  150. a = data.get("address") or data
  151. addr = {
  152. "street": a.get("Address") or a.get("street") or "123 Main St",
  153. "city": a.get("City") or a.get("city") or "New York",
  154. "state": a.get("State_Full") or a.get("State") or a.get("state") or "New York",
  155. "zip": (a.get("Zip_Code") or a.get("zip") or "10001")[:5],
  156. }
  157. except Exception as exc:
  158. log(f"[addr] 取地址失败,使用兜底: {exc!r}")
  159. addr = {"street": "123 Main St", "city": "New York", "state": "New York", "zip": "10001"}
  160. log(f"[addr] 解析结果: {addr}")
  161. return addr
  162. def fetch_sms_code(timeout: int = 180, interval: int = 5, log=print) -> str:
  163. log(f"[sms] 开始轮询验证码,超时 {timeout}s,间隔 {interval}s")
  164. deadline = time.time() + timeout
  165. last_text = ""
  166. polls = 0
  167. while time.time() < deadline:
  168. polls += 1
  169. try:
  170. req = urllib.request.Request(SMS_API)
  171. req.add_header("Accept", "*/*")
  172. req.add_header("Accept-Language", "zh-CN,zh;q=0.9")
  173. req.add_header(
  174. "User-Agent",
  175. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
  176. "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
  177. )
  178. with urllib.request.urlopen(req, timeout=10) as resp:
  179. text = resp.read().decode("utf-8", errors="replace").strip()
  180. except Exception as exc:
  181. log(f"[sms] 第{polls}次轮询请求异常: {exc!r}")
  182. time.sleep(interval)
  183. continue
  184. if text != last_text:
  185. log(f"[sms] 第{polls}次轮询新返回: {text}")
  186. last_text = text
  187. else:
  188. log(f"[sms] 第{polls}次轮询无变化")
  189. parts = text.split("|")
  190. status = parts[0].lower() if parts else ""
  191. content = parts[1] if len(parts) > 1 else ""
  192. if status == "yes":
  193. m = re.search(r"\b(\d{4,8})\b", content)
  194. if m:
  195. code = m.group(1)
  196. log(f"[sms] 命中验证码: {code}")
  197. return code
  198. log(f"[sms] status=yes 但未能从内容中匹配到数字: {content!r}")
  199. time.sleep(interval)
  200. raise TimeoutError(f"等待短信验证码超时 {timeout}s(共轮询 {polls} 次,最后返回={last_text!r})")