providers.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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. # 州名全名 → 缩写(Stripe billingAdministrativeArea 下拉框用缩写)
  13. _STATE_ABBREV = {
  14. "Alabama": "AL", "Alaska": "AK", "Arizona": "AZ", "Arkansas": "AR", "California": "CA",
  15. "Colorado": "CO", "Connecticut": "CT", "Delaware": "DE", "Florida": "FL", "Georgia": "GA",
  16. "Hawaii": "HI", "Idaho": "ID", "Illinois": "IL", "Indiana": "IN", "Iowa": "IA",
  17. "Kansas": "KS", "Kentucky": "KY", "Louisiana": "LA", "Maine": "ME", "Maryland": "MD",
  18. "Massachusetts": "MA", "Michigan": "MI", "Minnesota": "MN", "Mississippi": "MS",
  19. "Missouri": "MO", "Montana": "MT", "Nebraska": "NE", "Nevada": "NV", "New Hampshire": "NH",
  20. "New Jersey": "NJ", "New Mexico": "NM", "New York": "NY", "North Carolina": "NC",
  21. "North Dakota": "ND", "Ohio": "OH", "Oklahoma": "OK", "Oregon": "OR", "Pennsylvania": "PA",
  22. "Rhode Island": "RI", "South Carolina": "SC", "South Dakota": "SD", "Tennessee": "TN",
  23. "Texas": "TX", "Utah": "UT", "Vermont": "VT", "Virginia": "VA", "Washington": "WA",
  24. "West Virginia": "WV", "Wisconsin": "WI", "Wyoming": "WY",
  25. "District of Columbia": "DC",
  26. }
  27. # 经过验证的美国地址池(street+city+zip 匹配,Stripe 税务验证可过)
  28. _VERIFIED_US_ADDRESSES = [
  29. {"street": "350 Fifth Avenue", "city": "New York", "state": "New York", "state_abbrev": "NY", "zip": "10118"},
  30. {"street": "1600 Pennsylvania Avenue NW", "city": "Washington", "state": "District of Columbia", "state_abbrev": "DC", "zip": "20500"},
  31. {"street": "2215 N First Street", "city": "San Jose", "state": "California", "state_abbrev": "CA", "zip": "95131"},
  32. {"street": "405 Howard Street", "city": "San Francisco", "state": "California", "state_abbrev": "CA", "zip": "94105"},
  33. {"street": "233 S Wacker Drive", "city": "Chicago", "state": "Illinois", "state_abbrev": "IL", "zip": "60606"},
  34. {"street": "100 Summer Street", "city": "Boston", "state": "Massachusetts", "state_abbrev": "MA", "zip": "02110"},
  35. {"street": "1330 Avenue of the Americas", "city": "New York", "state": "New York", "state_abbrev": "NY", "zip": "10019"},
  36. {"street": "500 Boylston Street", "city": "Boston", "state": "Massachusetts", "state_abbrev": "MA", "zip": "02116"},
  37. {"street": "555 California Street", "city": "San Francisco", "state": "California", "state_abbrev": "CA", "zip": "94104"},
  38. {"street": "1100 Peachtree Street NE", "city": "Atlanta", "state": "Georgia", "state_abbrev": "GA", "zip": "30309"},
  39. {"street": "200 Clarendon Street", "city": "Boston", "state": "Massachusetts", "state_abbrev": "MA", "zip": "02116"},
  40. {"street": "600 Travis Street", "city": "Houston", "state": "Texas", "state_abbrev": "TX", "zip": "77002"},
  41. {"street": "1000 Louisiana Street", "city": "Houston", "state": "Texas", "state_abbrev": "TX", "zip": "77002"},
  42. {"street": "191 N Upper Wacker Drive", "city": "Chicago", "state": "Illinois", "state_abbrev": "IL", "zip": "60606"},
  43. {"street": "1 Lincoln Plaza", "city": "New York", "state": "New York", "state_abbrev": "NY", "zip": "10023"},
  44. ]
  45. # VISA 起 4,16 位;Mastercard 起 51-55 或 2221-2720,16 位
  46. CARD_BIN_POOLS = {
  47. "visa": [str(random.randint(4, 4)) + "".join(str(random.randint(0, 9)) for _ in range(5)) for _ in range(0)],
  48. }
  49. def _luhn_check_digit(number_without_check: str) -> str:
  50. digits = [int(c) for c in number_without_check]
  51. # 从右往左、每隔一位(即偶数索引位)×2
  52. parity = (len(digits) + 1) % 2 # 让最后一位 parity=0 才需要×2
  53. total = 0
  54. for i, d in enumerate(digits):
  55. if i % 2 == parity:
  56. d *= 2
  57. if d > 9:
  58. d -= 9
  59. total += d
  60. return str((10 - total % 10) % 10)
  61. def _gen_visa_pan() -> str:
  62. # 起 4,再补 14 位随机,最后 1 位 Luhn
  63. body = "4" + "".join(str(random.randint(0, 9)) for _ in range(14))
  64. return body + _luhn_check_digit(body)
  65. def _gen_mastercard_pan() -> str:
  66. # 起 51-55,再补 13 位随机,最后 1 位 Luhn
  67. prefix = str(random.randint(51, 55))
  68. body = prefix + "".join(str(random.randint(0, 9)) for _ in range(13))
  69. return body + _luhn_check_digit(body)
  70. def generate_local_card(brand: str = "visa") -> dict:
  71. """本地随机生成一张 Luhn 合规的 VISA / Mastercard 测试卡。
  72. expiry 取未来 1-4 年的随机月份,CVV 三位随机。
  73. """
  74. brand = (brand or "visa").lower()
  75. if brand == "mastercard":
  76. pan = _gen_mastercard_pan()
  77. else:
  78. pan = _gen_visa_pan()
  79. brand = "visa"
  80. now = time.localtime()
  81. exp_year = (now.tm_year + random.randint(1, 4)) % 100
  82. exp_month = random.randint(1, 12)
  83. expiry = f"{exp_month:02d} / {exp_year:02d}"
  84. cvv = "".join(str(random.randint(0, 9)) for _ in range(3))
  85. return {
  86. "number": pan,
  87. "expiry": expiry,
  88. "cvv": cvv,
  89. "brand": brand,
  90. }
  91. DEFAULT_BROWSER_HEADERS = {
  92. "Accept": "application/json, text/plain, */*",
  93. "Accept-Language": "zh-CN,zh;q=0.9",
  94. "Cache-Control": "no-cache",
  95. "Pragma": "no-cache",
  96. "DNT": "1",
  97. "Priority": "u=1, i",
  98. "Sec-Ch-Ua": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"',
  99. "Sec-Ch-Ua-Mobile": "?0",
  100. "Sec-Ch-Ua-Platform": '"macOS"',
  101. "Sec-Fetch-Dest": "empty",
  102. "Sec-Fetch-Mode": "cors",
  103. "Sec-Fetch-Site": "same-site",
  104. "User-Agent": (
  105. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
  106. "AppleWebKit/537.36 (KHTML, like Gecko) "
  107. "Chrome/148.0.0.0 Safari/537.36"
  108. ),
  109. }
  110. def _post_json(url: str, body: dict, headers: dict | None = None, timeout: int = 15, log=print) -> tuple[int, str, dict]:
  111. data = json.dumps(body).encode("utf-8")
  112. req = urllib.request.Request(url, data=data, method="POST")
  113. merged = {**DEFAULT_BROWSER_HEADERS, "Content-Type": "application/json;charset=UTF-8"}
  114. merged.update(headers or {})
  115. for k, v in merged.items():
  116. req.add_header(k, v)
  117. started = time.time()
  118. log(f"[http] POST {url} body={json.dumps(body, ensure_ascii=False)}")
  119. try:
  120. with urllib.request.urlopen(req, timeout=timeout) as resp:
  121. status = resp.status
  122. text = resp.read().decode("utf-8", errors="replace")
  123. except urllib.error.HTTPError as exc:
  124. text = exc.read().decode("utf-8", errors="replace")
  125. log(f"[http] {url} HTTP {exc.code} 耗时{int((time.time()-started)*1000)}ms 返回={text[:300]}")
  126. raise
  127. log(f"[http] {url} HTTP {status} 耗时{int((time.time()-started)*1000)}ms 返回={text[:300]}")
  128. return status, text, json.loads(text or "{}")
  129. def _normalize_expiry(expires: str) -> str:
  130. m = re.match(r"^\s*(\d{1,2})\s*/\s*(\d{2,4})\s*$", expires or "")
  131. if not m:
  132. return expires
  133. mm = m.group(1).zfill(2)
  134. yy = m.group(2)
  135. if len(yy) == 4:
  136. yy = yy[2:]
  137. return f"{mm} / {yy}"
  138. def fetch_visa_card(max_attempts: int = 8, log=print, *, prefer_local: bool = True) -> dict:
  139. """获取一张可用的卡。默认本地随机生成(避免接口返回重复卡导致 PayPal CC_LINKED_TO_FULL_ACCOUNT),
  140. 若 prefer_local=False 则走旧的接口逻辑。
  141. """
  142. if prefer_local:
  143. brand = random.choice(["visa", "mastercard"])
  144. card = generate_local_card(brand=brand)
  145. log(f"[card] 本地生成 {card['brand'].upper()} 卡 尾号 {card['number'][-4:]} 有效期 {card['expiry']} CVV {card['cvv']}")
  146. return card
  147. log(f"[card] 开始通过接口获取 VISA 卡,最多重试 {max_attempts} 次")
  148. for attempt in range(1, max_attempts + 1):
  149. log(f"[card] 第 {attempt}/{max_attempts} 次请求 {CARD_API}")
  150. try:
  151. _, _, data = _post_json(
  152. CARD_API,
  153. {"count": 4, "method": "random_credit_card"},
  154. headers={
  155. "Origin": "https://www.suijidaquan.com",
  156. "Referer": "https://www.suijidaquan.com/",
  157. },
  158. log=log,
  159. )
  160. except (urllib.error.URLError, json.JSONDecodeError) as exc:
  161. log(f"[card] 请求异常: {exc!r}")
  162. time.sleep(0.6)
  163. continue
  164. cards = data.get("data") or []
  165. types = [c.get("Credit_Card_Type") for c in cards]
  166. log(f"[card] 本次返回 {len(cards)} 张卡,类型 = {types}")
  167. for c in cards:
  168. if (c.get("Credit_Card_Type") or "").lower() == "visa":
  169. card = {
  170. "number": c["Credit_Card_Number"],
  171. "expiry": _normalize_expiry(c["Expires"]),
  172. "cvv": c["CVV2"],
  173. }
  174. log(f"[card] 命中 VISA 尾号 {card['number'][-4:]} 有效期 {card['expiry']} CVV {card['cvv']}")
  175. return card
  176. log("[card] 本次未拿到 VISA,准备重试")
  177. time.sleep(0.4)
  178. raise RuntimeError(f"已重试 {max_attempts} 次,仍未取到 VISA 卡")
  179. def fetch_us_address(log=print) -> dict:
  180. """获取美国地址。优先从预置地址池随机选(保证 street+zip 匹配),API 失败时也回退到预置池。"""
  181. # 50% 概率用预置地址池(经过验证的地址,Stripe 税务验证可靠)
  182. # 50% 概率尝试 API(可能有 street/zip 不匹配的风险)
  183. if random.random() < 0.5:
  184. addr = random.choice(_VERIFIED_US_ADDRESSES)
  185. log(f"[addr] 使用预置地址: {addr}")
  186. return dict(addr)
  187. log(f"[addr] 请求随机美国地址 {ADDR_API}")
  188. try:
  189. _, _, data = _post_json(ADDR_API, {"path": "/", "method": "address"}, log=log)
  190. a = data.get("address") or data
  191. state_full = a.get("State_Full") or a.get("State") or a.get("state") or "New York"
  192. addr = {
  193. "street": a.get("Address") or a.get("street") or "123 Main St",
  194. "city": a.get("City") or a.get("city") or "New York",
  195. "state": state_full,
  196. "state_abbrev": _STATE_ABBREV.get(state_full, state_full[:2].upper()),
  197. "zip": (a.get("Zip_Code") or a.get("zip") or "10001")[:5],
  198. }
  199. except Exception as exc:
  200. log(f"[addr] 取地址失败,使用预置地址池: {exc!r}")
  201. addr = dict(random.choice(_VERIFIED_US_ADDRESSES))
  202. log(f"[addr] 解析结果: {addr}")
  203. return addr
  204. def fetch_sms_code(timeout: int = 180, interval: int = 5, log=print, *, sms_api_url: str = "") -> str:
  205. api_url = (sms_api_url or SMS_API).strip()
  206. log(f"[sms] 开始轮询验证码,api={api_url[:60]}... 超时 {timeout}s,间隔 {interval}s")
  207. deadline = time.time() + timeout
  208. last_text = ""
  209. polls = 0
  210. while time.time() < deadline:
  211. polls += 1
  212. try:
  213. req = urllib.request.Request(api_url)
  214. req.add_header("Accept", "*/*")
  215. req.add_header("Accept-Language", "zh-CN,zh;q=0.9")
  216. req.add_header(
  217. "User-Agent",
  218. "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
  219. "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
  220. )
  221. with urllib.request.urlopen(req, timeout=10) as resp:
  222. text = resp.read().decode("utf-8", errors="replace").strip()
  223. except Exception as exc:
  224. log(f"[sms] 第{polls}次轮询请求异常: {exc!r}")
  225. time.sleep(interval)
  226. continue
  227. if text != last_text:
  228. log(f"[sms] 第{polls}次轮询新返回: {text}")
  229. last_text = text
  230. else:
  231. log(f"[sms] 第{polls}次轮询无变化")
  232. parts = text.split("|")
  233. status = parts[0].lower() if parts else ""
  234. content = parts[1] if len(parts) > 1 else ""
  235. if status == "yes":
  236. m = re.search(r"\b(\d{4,8})\b", content)
  237. if m:
  238. code = m.group(1)
  239. log(f"[sms] 命中验证码: {code}")
  240. return code
  241. log(f"[sms] status=yes 但未能从内容中匹配到数字: {content!r}")
  242. time.sleep(interval)
  243. raise TimeoutError(f"等待短信验证码超时 {timeout}s(共轮询 {polls} 次,最后返回={last_text!r})")