"""卡号、地址、短信三个外部数据源。""" from __future__ import annotations import json import random import re import time import urllib.error import urllib.request CARD_API = "https://api2.suijidaquan.com/api/v2/random-credit-card" ADDR_API = "https://www.meiguodizhi.com/api/v1/dz" SMS_API = "http://a.62-us.com/api/get_sms?key=a5d3262e05efaba982aba7cfae20b8bc" # 州名全名 → 缩写(Stripe billingAdministrativeArea 下拉框用缩写) _STATE_ABBREV = { "Alabama": "AL", "Alaska": "AK", "Arizona": "AZ", "Arkansas": "AR", "California": "CA", "Colorado": "CO", "Connecticut": "CT", "Delaware": "DE", "Florida": "FL", "Georgia": "GA", "Hawaii": "HI", "Idaho": "ID", "Illinois": "IL", "Indiana": "IN", "Iowa": "IA", "Kansas": "KS", "Kentucky": "KY", "Louisiana": "LA", "Maine": "ME", "Maryland": "MD", "Massachusetts": "MA", "Michigan": "MI", "Minnesota": "MN", "Mississippi": "MS", "Missouri": "MO", "Montana": "MT", "Nebraska": "NE", "Nevada": "NV", "New Hampshire": "NH", "New Jersey": "NJ", "New Mexico": "NM", "New York": "NY", "North Carolina": "NC", "North Dakota": "ND", "Ohio": "OH", "Oklahoma": "OK", "Oregon": "OR", "Pennsylvania": "PA", "Rhode Island": "RI", "South Carolina": "SC", "South Dakota": "SD", "Tennessee": "TN", "Texas": "TX", "Utah": "UT", "Vermont": "VT", "Virginia": "VA", "Washington": "WA", "West Virginia": "WV", "Wisconsin": "WI", "Wyoming": "WY", "District of Columbia": "DC", } # 经过验证的美国地址池(street+city+zip 匹配,Stripe 税务验证可过) _VERIFIED_US_ADDRESSES = [ {"street": "350 Fifth Avenue", "city": "New York", "state": "New York", "state_abbrev": "NY", "zip": "10118"}, {"street": "1600 Pennsylvania Avenue NW", "city": "Washington", "state": "District of Columbia", "state_abbrev": "DC", "zip": "20500"}, {"street": "2215 N First Street", "city": "San Jose", "state": "California", "state_abbrev": "CA", "zip": "95131"}, {"street": "405 Howard Street", "city": "San Francisco", "state": "California", "state_abbrev": "CA", "zip": "94105"}, {"street": "233 S Wacker Drive", "city": "Chicago", "state": "Illinois", "state_abbrev": "IL", "zip": "60606"}, {"street": "100 Summer Street", "city": "Boston", "state": "Massachusetts", "state_abbrev": "MA", "zip": "02110"}, {"street": "1330 Avenue of the Americas", "city": "New York", "state": "New York", "state_abbrev": "NY", "zip": "10019"}, {"street": "500 Boylston Street", "city": "Boston", "state": "Massachusetts", "state_abbrev": "MA", "zip": "02116"}, {"street": "555 California Street", "city": "San Francisco", "state": "California", "state_abbrev": "CA", "zip": "94104"}, {"street": "1100 Peachtree Street NE", "city": "Atlanta", "state": "Georgia", "state_abbrev": "GA", "zip": "30309"}, {"street": "200 Clarendon Street", "city": "Boston", "state": "Massachusetts", "state_abbrev": "MA", "zip": "02116"}, {"street": "600 Travis Street", "city": "Houston", "state": "Texas", "state_abbrev": "TX", "zip": "77002"}, {"street": "1000 Louisiana Street", "city": "Houston", "state": "Texas", "state_abbrev": "TX", "zip": "77002"}, {"street": "191 N Upper Wacker Drive", "city": "Chicago", "state": "Illinois", "state_abbrev": "IL", "zip": "60606"}, {"street": "1 Lincoln Plaza", "city": "New York", "state": "New York", "state_abbrev": "NY", "zip": "10023"}, ] # VISA 起 4,16 位;Mastercard 起 51-55 或 2221-2720,16 位 CARD_BIN_POOLS = { "visa": [str(random.randint(4, 4)) + "".join(str(random.randint(0, 9)) for _ in range(5)) for _ in range(0)], } def _luhn_check_digit(number_without_check: str) -> str: digits = [int(c) for c in number_without_check] # 从右往左、每隔一位(即偶数索引位)×2 parity = (len(digits) + 1) % 2 # 让最后一位 parity=0 才需要×2 total = 0 for i, d in enumerate(digits): if i % 2 == parity: d *= 2 if d > 9: d -= 9 total += d return str((10 - total % 10) % 10) def _gen_visa_pan() -> str: # 起 4,再补 14 位随机,最后 1 位 Luhn body = "4" + "".join(str(random.randint(0, 9)) for _ in range(14)) return body + _luhn_check_digit(body) def _gen_mastercard_pan() -> str: # 起 51-55,再补 13 位随机,最后 1 位 Luhn prefix = str(random.randint(51, 55)) body = prefix + "".join(str(random.randint(0, 9)) for _ in range(13)) return body + _luhn_check_digit(body) def generate_local_card(brand: str = "visa") -> dict: """本地随机生成一张 Luhn 合规的 VISA / Mastercard 测试卡。 expiry 取未来 1-4 年的随机月份,CVV 三位随机。 """ brand = (brand or "visa").lower() if brand == "mastercard": pan = _gen_mastercard_pan() else: pan = _gen_visa_pan() brand = "visa" now = time.localtime() exp_year = (now.tm_year + random.randint(1, 4)) % 100 exp_month = random.randint(1, 12) expiry = f"{exp_month:02d} / {exp_year:02d}" cvv = "".join(str(random.randint(0, 9)) for _ in range(3)) return { "number": pan, "expiry": expiry, "cvv": cvv, "brand": brand, } DEFAULT_BROWSER_HEADERS = { "Accept": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9", "Cache-Control": "no-cache", "Pragma": "no-cache", "DNT": "1", "Priority": "u=1, i", "Sec-Ch-Ua": '"Chromium";v="148", "Google Chrome";v="148", "Not/A)Brand";v="99"', "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": '"macOS"', "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-site", "User-Agent": ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/148.0.0.0 Safari/537.36" ), } def _post_json(url: str, body: dict, headers: dict | None = None, timeout: int = 15, log=print) -> tuple[int, str, dict]: data = json.dumps(body).encode("utf-8") req = urllib.request.Request(url, data=data, method="POST") merged = {**DEFAULT_BROWSER_HEADERS, "Content-Type": "application/json;charset=UTF-8"} merged.update(headers or {}) for k, v in merged.items(): req.add_header(k, v) started = time.time() log(f"[http] POST {url} body={json.dumps(body, ensure_ascii=False)}") try: with urllib.request.urlopen(req, timeout=timeout) as resp: status = resp.status text = resp.read().decode("utf-8", errors="replace") except urllib.error.HTTPError as exc: text = exc.read().decode("utf-8", errors="replace") log(f"[http] {url} HTTP {exc.code} 耗时{int((time.time()-started)*1000)}ms 返回={text[:300]}") raise log(f"[http] {url} HTTP {status} 耗时{int((time.time()-started)*1000)}ms 返回={text[:300]}") return status, text, json.loads(text or "{}") def _normalize_expiry(expires: str) -> str: m = re.match(r"^\s*(\d{1,2})\s*/\s*(\d{2,4})\s*$", expires or "") if not m: return expires mm = m.group(1).zfill(2) yy = m.group(2) if len(yy) == 4: yy = yy[2:] return f"{mm} / {yy}" def fetch_visa_card(max_attempts: int = 8, log=print, *, prefer_local: bool = True) -> dict: """获取一张可用的卡。默认本地随机生成(避免接口返回重复卡导致 PayPal CC_LINKED_TO_FULL_ACCOUNT), 若 prefer_local=False 则走旧的接口逻辑。 """ if prefer_local: brand = random.choice(["visa", "mastercard"]) card = generate_local_card(brand=brand) log(f"[card] 本地生成 {card['brand'].upper()} 卡 尾号 {card['number'][-4:]} 有效期 {card['expiry']} CVV {card['cvv']}") return card log(f"[card] 开始通过接口获取 VISA 卡,最多重试 {max_attempts} 次") for attempt in range(1, max_attempts + 1): log(f"[card] 第 {attempt}/{max_attempts} 次请求 {CARD_API}") try: _, _, data = _post_json( CARD_API, {"count": 4, "method": "random_credit_card"}, headers={ "Origin": "https://www.suijidaquan.com", "Referer": "https://www.suijidaquan.com/", }, log=log, ) except (urllib.error.URLError, json.JSONDecodeError) as exc: log(f"[card] 请求异常: {exc!r}") time.sleep(0.6) continue cards = data.get("data") or [] types = [c.get("Credit_Card_Type") for c in cards] log(f"[card] 本次返回 {len(cards)} 张卡,类型 = {types}") for c in cards: if (c.get("Credit_Card_Type") or "").lower() == "visa": card = { "number": c["Credit_Card_Number"], "expiry": _normalize_expiry(c["Expires"]), "cvv": c["CVV2"], } log(f"[card] 命中 VISA 尾号 {card['number'][-4:]} 有效期 {card['expiry']} CVV {card['cvv']}") return card log("[card] 本次未拿到 VISA,准备重试") time.sleep(0.4) raise RuntimeError(f"已重试 {max_attempts} 次,仍未取到 VISA 卡") def fetch_us_address(log=print) -> dict: """获取美国地址。优先从预置地址池随机选(保证 street+zip 匹配),API 失败时也回退到预置池。""" # 50% 概率用预置地址池(经过验证的地址,Stripe 税务验证可靠) # 50% 概率尝试 API(可能有 street/zip 不匹配的风险) if random.random() < 0.5: addr = random.choice(_VERIFIED_US_ADDRESSES) log(f"[addr] 使用预置地址: {addr}") return dict(addr) log(f"[addr] 请求随机美国地址 {ADDR_API}") try: _, _, data = _post_json(ADDR_API, {"path": "/", "method": "address"}, log=log) a = data.get("address") or data state_full = a.get("State_Full") or a.get("State") or a.get("state") or "New York" addr = { "street": a.get("Address") or a.get("street") or "123 Main St", "city": a.get("City") or a.get("city") or "New York", "state": state_full, "state_abbrev": _STATE_ABBREV.get(state_full, state_full[:2].upper()), "zip": (a.get("Zip_Code") or a.get("zip") or "10001")[:5], } except Exception as exc: log(f"[addr] 取地址失败,使用预置地址池: {exc!r}") addr = dict(random.choice(_VERIFIED_US_ADDRESSES)) log(f"[addr] 解析结果: {addr}") return addr def fetch_sms_code(timeout: int = 180, interval: int = 5, log=print, *, sms_api_url: str = "") -> str: api_url = (sms_api_url or SMS_API).strip() log(f"[sms] 开始轮询验证码,api={api_url[:60]}... 超时 {timeout}s,间隔 {interval}s") deadline = time.time() + timeout last_text = "" polls = 0 while time.time() < deadline: polls += 1 try: req = urllib.request.Request(api_url) req.add_header("Accept", "*/*") req.add_header("Accept-Language", "zh-CN,zh;q=0.9") req.add_header( "User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", ) with urllib.request.urlopen(req, timeout=10) as resp: text = resp.read().decode("utf-8", errors="replace").strip() except Exception as exc: log(f"[sms] 第{polls}次轮询请求异常: {exc!r}") time.sleep(interval) continue if text != last_text: log(f"[sms] 第{polls}次轮询新返回: {text}") last_text = text else: log(f"[sms] 第{polls}次轮询无变化") parts = text.split("|") status = parts[0].lower() if parts else "" content = parts[1] if len(parts) > 1 else "" if status == "yes": m = re.search(r"\b(\d{4,8})\b", content) if m: code = m.group(1) log(f"[sms] 命中验证码: {code}") return code log(f"[sms] status=yes 但未能从内容中匹配到数字: {content!r}") time.sleep(interval) raise TimeoutError(f"等待短信验证码超时 {timeout}s(共轮询 {polls} 次,最后返回={last_text!r})")