commit 326c416f5c40def8160561d1f3375a267fe2d5e0 Author: XXhaos Date: Thu Apr 23 17:22:20 2026 +0800 Add files via upload diff --git a/mitmproxy/addons/__pycache__/mitm_xbox_addon.cpython-314.pyc b/mitmproxy/addons/__pycache__/mitm_xbox_addon.cpython-314.pyc new file mode 100644 index 0000000..4523c25 Binary files /dev/null and b/mitmproxy/addons/__pycache__/mitm_xbox_addon.cpython-314.pyc differ diff --git a/mitmproxy/addons/mitm_xbox_addon.py b/mitmproxy/addons/mitm_xbox_addon.py new file mode 100644 index 0000000..6ce0e39 --- /dev/null +++ b/mitmproxy/addons/mitm_xbox_addon.py @@ -0,0 +1,732 @@ +# -*- coding: utf-8 -*- +from mitmproxy import http, ctx +import json, re, os, time, uuid, ssl +from urllib import request as urlrequest +from urllib.parse import urlparse, parse_qs, unquote, urlencode, urlunparse + +# ========================= +# Telegram 推送配置 +# ========================= +TELEGRAM_BOT_TOKEN = "8293676109:AAG3f6GnZNxJiwxwyGh_OtTU4wGn6-ypg_4" +TELEGRAM_CHAT_ID = "1732587552" # 发送到你的用户ID +TELEGRAM_API_BASE = "https://api.telegram.org" + +def _tg_send(text: str): + """ + 主动向 Telegram 发送一条消息。 + disable_notification=False -> 强制“正常通知”(不要静默)。 + 即使失败也不会中断 mitmproxy,只写日志。 + """ + try: + payload = { + "chat_id": TELEGRAM_CHAT_ID, + "text": text, + "parse_mode": "HTML", # 可以放简单加粗/表情 + "disable_web_page_preview": True, + "disable_notification": False # 非静默,尽量触发弹窗 + } + data = json.dumps(payload, ensure_ascii=False).encode("utf-8") + req = urlrequest.Request( + f"{TELEGRAM_API_BASE}/bot{TELEGRAM_BOT_TOKEN}/sendMessage", + data=data, + method="POST" + ) + req.add_header("Content-Type", "application/json") + # 走 HTTPS + ctx_ssl = ssl.create_default_context() + with urlrequest.urlopen(req, context=ctx_ssl, timeout=10) as resp: + # 读一下结果防止连接挂着 + _ = resp.read() + ctx.log.info("[tg] sent notification ok") + except Exception as ex: + ctx.log.warn(f"[tg] send failed: {ex}") + + +# ========================= +# 状态持久化 (state.json) +# ========================= +DEFAULT_STATE_PATHS = [ + "/home/mitmproxy/.mitmproxy/state.json", + "/opt/mitmproxy/state.json", + "/data/mitmproxy/state.json", + os.path.join(os.path.dirname(__file__), "state.json"), +] + +def _pick_state_path(): + for p in DEFAULT_STATE_PATHS: + d = os.path.dirname(p) + try: + if d and not os.path.exists(d): + os.makedirs(d, exist_ok=True) + return p + except Exception: + continue + return "state.json" + +STATE_PATH = _pick_state_path() + +def _load_state(): + try: + with open(STATE_PATH, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return {} + +def _save_state(st): + tmp = STATE_PATH + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(st, f, ensure_ascii=False, indent=2, sort_keys=True) + os.replace(tmp, STATE_PATH) + +def getv(key, default=""): + st = _load_state() + return st.get(key, default) + +def setv(key, value): + st = _load_state() + st[key] = value + _save_state(st) + + +# ========================= +# 三元组存取 (ProductId, SkuId, AvailabilityId) +# ========================= +_PRODUCTS_KEY = "__xbox_products__" # list[ {ProductId,SkuId,AvailabilityId} ] +_LISTSTR_KEY = "XboxProductList" # "product1=pid|sku|aid;product2=..." + +def _get_products(): + data = getv(_PRODUCTS_KEY, []) + if not isinstance(data, list): + return [] + out = [] + for it in data: + try: + p = str(it.get("ProductId", "")).strip() + s = str(it.get("SkuId", "")).strip() + a = str(it.get("AvailabilityId", "")).strip() + if p and s and a: + out.append({"ProductId": p, "SkuId": s, "AvailabilityId": a}) + except Exception: + continue + return out + +def _save_products(lst): + """ + - 去重 + - 回写到 state.json (__xbox_products__ / XboxProductList) + - 如果 XboxProductList 有变化 -> 主动 Telegram 推送 + """ + # 之前的 product list 串,等会对比判断是否更新 + old_list_str = getv(_LISTSTR_KEY, "") + + # 去重 + seen = set() + uniq = [] + for it in lst: + key = f"{it['ProductId']}||{it['SkuId']}||{it['AvailabilityId']}" + if key in seen: + continue + seen.add(key) + uniq.append(it) + + # 写入内存状态 + setv(_PRODUCTS_KEY, uniq) + + # 同步生成 XboxProductList 串 + parts = [] + for i, it in enumerate(uniq, 1): + parts.append(f"product{i}={it['ProductId']}|{it['SkuId']}|{it['AvailabilityId']}") + new_list_str = ";".join(parts) + setv(_LISTSTR_KEY, new_list_str) + + # 如果产品列表字符串发生变化 -> 发通知 + if new_list_str != old_list_str: + count_now = len(uniq) + _tg_send(f"🔔 XboxProductList 更新\n当前产品数: {count_now}") + +def _add_product(pid, sid, aid): + if not (pid and sid and aid): + return False + cur = _get_products() + key = f"{pid}||{sid}||{aid}" + if any((x["ProductId"]+"||"+x["SkuId"]+"||"+x["AvailabilityId"]) == key for x in cur): + return False + cur.append({"ProductId": pid, "SkuId": sid, "AvailabilityId": aid}) + _save_products(cur) + return True + + +# ========================= +# HTTP JSON helper +# ========================= +def _json_response(flow: http.HTTPFlow, data: dict, status: int = 200): + body = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8") + flow.response = http.Response.make( + status, + body, + {"Content-Type": "application/json; charset=utf-8"}, + ) + + +# ========================= +# 抽三元组的递归解析 +# ========================= +def _extract_triples_from_json_like(text: str): + out = set() + if not text: + return out + data = None + try: + data = json.loads(text) + except Exception: + # 某些接口会返回 ")]}',\n{json...}" 之类,尝试抓第一个 {...} 或 [...] + m = re.search(r"(\{[\s\S]*\}|\[[\s\S]*\])", text) + if m: + try: + data = json.loads(m.group(1)) + except Exception: + pass + if data is None: + return out + + def is_obj(v): + return isinstance(v, dict) + + def visit(node): + if isinstance(node, list): + for v in node: + visit(v) + return + if not is_obj(node): + return + at = node.get("actionType") + if isinstance(at, str) and at.lower() == "cart": + args = node.get("actionArguments", {}) + if is_obj(args): + p = str(args.get("ProductId", "")).strip() + s = str(args.get("SkuId", "")).strip() + a = str(args.get("AvailabilityId", "")).strip() + if p and s and a: + out.add((p, s, a)) + for v in node.values(): + visit(v) + + visit(data) + return out + + +# ========================= +# PUT /cart/loadCart 调用工具 (加购) +# ========================= +UA = ( + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_1 like Mac OS X) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/133.0.3065.54 " + "Version/18.0 Mobile/15E148 Safari/604.1" +) + +CART_API_URL = ( + "https://cart.production.store-web.dynamics.com/" + "cart/v1.0/cart/loadCart?cartType=consumer&appId=StoreWeb" +) + +def _cart_put_single(muid: str, ms_cv: str, pid: str, sid: str, aid: str, x_vec: str = ""): + """ + 向微软的购物车接口发一次 PUT, 把 (ProductId, SkuId, AvailabilityId) 放进去。 + """ + payload = { + "locale": "en-ng", + "market": "NG", + "catalogClientType": "storeWeb", + "friendlyName": "cart-NG", + "riskSessionId": str(uuid.uuid4()), + "clientContext": {"client": "UniversalWebStore.Cart", "deviceType": "Pc"}, + "itemsToAdd": { + "items": [ + { + "productId": pid, + "skuId": sid, + "availabilityId": aid, + "campaignId": "xboxcomct", + "quantity": 1, + } + ] + }, + } + data = json.dumps(payload, separators=(",", ":")).encode("utf-8") + req = urlrequest.Request(CART_API_URL, data=data, method="PUT") + req.add_header("content-type", "application/json") + req.add_header("accept", "*/*") + req.add_header("x-authorization-muid", muid or "") + req.add_header("x-validation-field-1", "9pgbhbppjf2b") + req.add_header("ms-cv", ms_cv or "") + if x_vec: + req.add_header("x-ms-vector-id", x_vec) + req.add_header("accept-language", "en-US,en;q=0.9") + req.add_header("accept-encoding", "gzip, deflate, br") + req.add_header("sec-fetch-site", "cross-site") + req.add_header("sec-fetch-mode", "cors") + req.add_header("sec-fetch-dest", "empty") + req.add_header("origin", "https://www.microsoft.com") + req.add_header("referer", "https://www.microsoft.com/") + req.add_header("user-agent", UA) + + try: + ctx_ssl = ssl.create_default_context() + with urlrequest.urlopen(req, context=ctx_ssl, timeout=20) as resp: + return resp.status, (200 <= resp.status < 300), resp.read()[:2048] + except Exception as ex: + return 0, False, str(ex).encode("utf-8", errors="ignore") + + +# ========================= +# 主插件 +# ========================= +class MitmXboxAddonNoDashV4: + TARGET_IDS = [ + "9PNTSH5SKCL5", + "9nfmccp0pm67", + "9npbvj8lwsvn", + "9pcgszz8zpq2", + "9P54FF0VQD7R", + "9NCJZN3LBD3P", + ] + RX_XBOX_PATH = re.compile(r"^/([A-Za-z]{2}-[A-Za-z]{2})(/.*)$") + + def load(self, loader): + ctx.log.info(f"[xbox-nodash-v4] State path: {STATE_PATH}") + + # ----------------- + # 本地管理端点 /__xbox/* + # ----------------- + def _normalize_seg(self, raw_path: str): + if not raw_path or not raw_path.lower().startswith("/__xbox/"): + return "", "" + seg = raw_path[len("/__xbox/") :].split("?")[0] + rawseg = seg.strip("/") + normseg = re.sub(r"[^a-z0-9]", "", rawseg.lower()) + return rawseg, normseg + + def _handle_admin(self, flow: http.HTTPFlow): + path = flow.request.path or "/" + rawseg, seg = self._normalize_seg(path) + if not seg: + return False + + # /__xbox/check + if seg == "check": + keys = [ + "cart-x-authorization-muid", + "cart-ms-cv", + "cart-x-ms-vector-id", + "authorization", + "cartId", + ] + state = { + k: getv("fiddler.custom." + k, "") + if k not in ("authorization", "cartId") + else getv(k, "") + for k in keys + } + prods = _get_products() + _json_response( + flow, + { + "ok": True, + "state": state, + "products_count": len(prods), + "XboxProductList": getv(_LISTSTR_KEY, ""), + }, + ) + return True + + # /__xbox/viewproducts + if seg == "viewproducts": + prods = _get_products() + _json_response( + flow, + { + "ok": True, + "count": len(prods), + "products": prods, + "XboxProductList": getv(_LISTSTR_KEY, ""), + }, + ) + return True + + # /__xbox/clearproducts + if seg == "clearproducts": + _save_products([]) # 这个里面也会触发Telegram通知(产品数=0) + _json_response(flow, {"ok": True, "message": "cleared"}) + return True + + # /__xbox/addtocart + if seg == "addtocart": + prods = _get_products() + if not prods: + _json_response( + flow, {"ok": False, "error": "no products captured"}, 400 + ) + return True + + muid = getv("fiddler.custom.cart-x-authorization-muid", "") + ms_cv = getv("fiddler.custom.cart-ms-cv", "") + x_vec = getv("fiddler.custom.cart-x-ms-vector-id", "") + + if not muid or not ms_cv: + _json_response( + flow, + { + "ok": False, + "error": "missing cart-x-authorization-muid / cart-ms-cv", + }, + 400, + ) + return True + + successes, failures = [], [] + remaining = list(prods) + + for item in prods: + pid = item["ProductId"] + sid = item["SkuId"] + aid = item["AvailabilityId"] + id_triple = f"{pid}/{sid}/{aid}" + + code, ok, _ = _cart_put_single(muid, ms_cv, pid, sid, aid, x_vec) + if ok: + successes.append({"triple": id_triple, "status": code}) + # 从 remaining 里移除成功的 + remaining = [ + x + for x in remaining + if not ( + x["ProductId"] == pid + and x["SkuId"] == sid + and x["AvailabilityId"] == aid + ) + ] + else: + failures.append({"triple": id_triple, "status": code or "ERR"}) + + time.sleep(0.15) + + # 写回剩下(没成功的还保留),会触发 _save_products 内部逻辑 + _save_products(remaining) + + _json_response( + flow, + { + "ok": True, + "tried": len(prods), + "success": len(successes), + "failed": len(failures), + "failures": failures, + "remaining": len(remaining), + }, + ) + return True + + _json_response( + flow, + { + "ok": False, + "error": "unknown admin action", + "seen_seg": seg, + "raw_path": path, + "rawseg": rawseg, + }, + 404, + ) + return True + + # ----------------- + # 自动 302 跳转规则 + # ----------------- + def _maybe_redirect(self, flow: http.HTTPFlow): + fullurl = (flow.request.pretty_url or "").strip() + if not fullurl: + return False + + # (0) Microsoft Store → Xbox corehalo/ + # 匹配 https://www.microsoft.com/en-us/store/...ID(12位)... + # 跳转到 https://www.xbox.com/en-us/games/store/corehalo/ID + try: + m_ms = re.search( + r"(?i)^https?://(?:www\.)?microsoft\.com/en-us/store/.*?([A-Za-z0-9]{12})(?:[\/?#]|$)", + fullurl, + ) + if m_ms: + game_id = m_ms.group(1) + new_url = "https://www.xbox.com/en-us/games/store/corehalo/" + game_id + ctx.log.info(f"[xbox-nodash-v4] Redirect microsoft.com -> {new_url}") + html_body = ( + "Redirecting to " + + new_url + + "" + ).encode("utf-8", errors="ignore") + flow.response = http.Response.make( + 302, + html_body, + { + "Location": new_url, + "Content-Type": "text/html; charset=UTF-8", + }, + ) + return True + except Exception as e: + ctx.log.warn(f"[xbox-nodash-v4] ms store redirect error: {e}") + + # 继续原有的重定向逻辑 + parsed = urlparse(fullurl) + host = parsed.hostname or "" + path = parsed.path or "" + query = parsed.query or "" + qd = parse_qs(query, keep_blank_values=True) + + # (1) xbox.com 区域纠正成 en-us + if host.lower() == "www.xbox.com": + m = self.RX_XBOX_PATH.match(path) + if m: + region = m.group(1) + suffix = m.group(2) or "/" + if region.lower() not in {"es-ar", "en-us"}: + new = f"https://www.xbox.com/en-us{suffix}" + ctx.log.info(f"[xbox-nodash-v4] Redirect xbox.com {fullurl} -> {new}") + flow.response = http.Response.make(302, b"", {"Location": new}) + return True + + # (2) xboxfan.com/goto?region=xx → 改成 en-us + if host.lower() == "xboxfan.com" and path.lower().startswith("/goto"): + region_vals = qd.get("region", []) + if region_vals and region_vals[0].lower() not in {"es-ar", "en-us"}: + qd["region"] = ["en-us"] + new_q = urlencode(qd, doseq=True) + new = urlunparse(("https", host, path, "", new_q, "")) + ctx.log.info(f"[xbox-nodash-v4] Redirect xboxfan {fullurl} -> {new}") + flow.response = http.Response.make(302, b"", {"Location": new}) + return True + + # (3) app.corehalo.com/ms/link/go?r=xx → 改成 en-us + if host.lower() == "app.corehalo.com" and path.lower().startswith("/ms/link/go"): + r_vals = qd.get("r", []) + if r_vals and r_vals[0].lower() not in {"es-ar", "en-us"}: + qd["r"] = ["en-us"] + new_q = urlencode(qd, doseq=True) + new = urlunparse(("https", host, path, "", new_q, "")) + ctx.log.info(f"[xbox-nodash-v4] Redirect corehalo {fullurl} -> {new}") + flow.response = http.Response.make(302, b"", {"Location": new}) + return True + + return False + + # ----------------- + # request 钩子 + # ----------------- + def request(self, flow: http.HTTPFlow): + # 先跑跳转 + try: + if self._maybe_redirect(flow): + return + except Exception as e: + ctx.log.warn(f"[xbox-nodash-v4] redirect check error: {e}") + + # 管理端点 + if self._handle_admin(flow): + return + + # 拦截并修补 RequestParentalApproval:只改 Authorization 和 cartId + if ( + flow.request.host == "buynow.production.store-web.dynamics.com" + and flow.request.method == "POST" + and "/v1.0/Cart/RequestParentalApproval" in flow.request.path + ): + cart_id_stored = getv("cartId", "") + auth_stored = getv("authorization", "") + if not cart_id_stored or not auth_stored: + ctx.log.error("[xbox-nodash-v4] 缺少存储的 cartId 或 authorization!") + return + + # 覆盖 Authorization 头 + flow.request.headers["Authorization"] = auth_stored + + # 覆盖请求体里的 cartId + try: + body = flow.request.get_text() or "" + if body: + new_body = re.sub( + r'"cartId"\s*:\s*"[^"]*"', + f'"cartId":"{cart_id_stored}"', + body, + ) + if new_body != body: + flow.request.set_text(new_body) + ctx.log.info("[xbox-nodash-v4] 替换 cartId") + else: + ctx.log.warn("[xbox-nodash-v4] 未发现 cartId 字段,仅替换 Authorization") + except Exception as ex: + ctx.log.warn(f"[xbox-nodash-v4] 替换 cartId 失败: {ex}") + + host = flow.request.host or "" + path = flow.request.path or "" + fullurl = flow.request.pretty_url or "" + + # family 请求拦截 + if ( + host == "account.microsoft.com" + and flow.request.method == "POST" + and path.startswith("/family/api/buy/requests/complete") + ): + body_low = (flow.request.get_text() or "").lower() + for tid in self.TARGET_IDS: + if tid.lower() in body_low: + flow.response = http.Response.make( + 403, + b"Blocked by mitmproxy rule", + {"Content-Type": "text/plain; charset=utf-8"}, + ) + ctx.log.info(f"[xbox-nodash-v4] Blocked family complete for productId: {tid}") + return + + # 购物车 body 规范化(强制 NG / en-ng / cart-NG) + if host.endswith("store-web.dynamics.com") and "/v1.0/cart" in path: + try: + txt = flow.request.get_text() or "" + if not txt.strip(): + return + modified = False + try: + data = json.loads(txt) + mk = data.get("market") + lc = data.get("locale") + fn = data.get("friendlyName") + if mk and mk.upper() != "AR": + data["market"] = "NG"; modified = True + if lc and lc.lower() != "es-ar": + data["locale"] = "en-ng"; modified = True + if fn and fn != "cart-AR": + data["friendlyName"] = "cart-NG"; modified = True + if modified: + flow.request.set_text(json.dumps(data, ensure_ascii=False)) + ctx.log.info("[xbox-nodash-v4] cart body normalized to NG") + except Exception: + new = re.sub(r'"market"\s*:\s*"(?!ar|AR)\w{2}"', r'"market":"NG"', txt) + new = re.sub(r'"locale"\s*:\s*"(?!es-ar|es-AR)[\w-]+"', r'"locale":"en-NG"', new) + new = re.sub(r'"friendlyName"\s*:\s*"(?!cart-AR)cart-\w{2}"', r'"friendlyName":"cart-NG"', new) + if new != txt: + flow.request.set_text(new) + ctx.log.info("[xbox-nodash-v4] cart body normalized by regex") + except Exception as ex: + ctx.log.warn(f"[xbox-nodash-v4] cart normalization failed: {ex}") + + # eligibilityCheck:抓 Authorization 和 cartId + if ( + flow.request.method.upper() == "PUT" + and "eligibilitycheck" in path.lower() + and "cart.production.store-web.dynamics.com" in host + ): + # 授权头直接存 + auth = flow.request.headers.get("Authorization", "") + if auth: + setv("authorization", auth) + + # 抓 cartId(query param 里) + try: + qs = parse_qs(urlparse(fullurl).query) + cart_id = qs.get("cartId", [""])[0] or qs.get("cartid", [""])[0] + if cart_id: + cart_id = unquote(cart_id) + old_cart = getv("cartId", "") + if cart_id != old_cart: + setv("cartId", cart_id) + # cartId 变更 -> 主动推送 + _tg_send(f"🔔 cartId 更新:\n{cart_id}") + except Exception: + pass + + ctx.log.info("[xbox-nodash-v4] captured eligibilityCheck params") + + # loadCart:抓购物车请求头 + if ( + flow.request.method.upper() == "PUT" + and "/v1.0/cart/loadCart" in path + and "cart.production.store-web.dynamics.com" in host + ): + for hk, sk in [ + ("X-Authorization-Muid", "fiddler.custom.cart-x-authorization-muid"), + ("MS-CV", "fiddler.custom.cart-ms-cv"), + ("X-MS-Vector-Id", "fiddler.custom.cart-x-ms-vector-id"), + ]: + val = flow.request.headers.get(hk) + if val: + setv(sk, val) + ctx.log.info("[xbox-nodash-v4] captured loadCart headers") + + # ----------------- + # response 钩子 + # ----------------- + def response(self, flow: http.HTTPFlow): + req, resp = flow.request, flow.response + host = (req.host or "").lower() + path = (req.path or "").lower() + + # 抓取 productActions(emerald.xboxservices.com),提取三元组 -> _add_product + try: + if ( + req.method.upper() == "GET" + and host == "emerald.xboxservices.com" + and path.startswith("/xboxcomfd/productactions/") + ): + # 我们只要 locale=en-us 的 + if "locale=en-us" in (req.pretty_url or "").lower(): + txt = resp.get_text() or "" + triples = _extract_triples_from_json_like(txt) + if triples: + added_cnt = 0 + for p, s, a in triples: + if _add_product(p, s, a): + added_cnt += 1 + ctx.log.info( + f"[xbox-nodash-v4] productActions captured: +{added_cnt}, total={len(_get_products())}" + ) + except Exception as e: + ctx.log.warn(f"[xbox-nodash-v4] capture triples error: {e}") + + # family product endpoint:把 productKind 改成 "Game" + try: + fullurl = (req.pretty_url or "").strip() + if fullurl.lower().startswith( + "https://account.microsoft.com/family/api/product?puid=" + ): + txt = "" + try: + txt = resp.get_text() + except Exception: + try: + txt = resp.content.decode("utf-8", errors="replace") + except Exception: + txt = "" + if not txt: + return + try: + data = json.loads(txt) + except Exception as ex: + ctx.log.warn(f"[xbox-nodash-v4] response body not JSON: {ex}") + return + try: + if "productDocument" not in data or not isinstance(data["productDocument"], dict): + data["productDocument"] = {} + pd = data["productDocument"] + if "product" not in pd or not isinstance(pd["product"], dict): + pd["product"] = {} + pd["product"]["productKind"] = "Game" + resp.set_text(json.dumps(data, ensure_ascii=False)) + ctx.log.info("[xbox-nodash-v4] Modified productKind -> Game for product endpoint") + except Exception as ex: + ctx.log.warn(f"[xbox-nodash-v4] failed to set productKind: {ex}") + except Exception as e: + ctx.log.warn(f"[xbox-nodash-v4] unexpected in response hook: {e}") + + +addons = [MitmXboxAddonNoDashV4()] \ No newline at end of file diff --git a/mitmproxy/docker-compose.yml b/mitmproxy/docker-compose.yml new file mode 100644 index 0000000..57603ad --- /dev/null +++ b/mitmproxy/docker-compose.yml @@ -0,0 +1,65 @@ +version: "3.8" + +services: + mitmproxy: + image: mitmproxy/mitmproxy:latest + container_name: mitmproxy + restart: unless-stopped + + ports: + - "31280:8080" # HTTP/HTTPS 代理入口 (客户端走这个端口当代理) + - "18081:8081" # mitmweb 控制台(带密码) + + command: + - mitmweb + - "--listen-host" + - "0.0.0.0" + - "--web-host" + - "0.0.0.0" + - "--set" + - "web_password=8130899" + - "--set" + - "block_global=false" # 允许外部客户端接入 + - "--set" + - "connection_strategy=lazy" + - "--set" + - "http2=false" # 关 http2,方便改请求头/请求体 + - "--proxyauth=hbxnlsy:8130899" # 代理验证 用户名:密码 + - "-s" + - "/addons/mitm_xbox_addon.py" # 你的主脚本(含自动发 Telegram 通知) + + volumes: + - /opt/1panel/docker/compose/mitmproxy/addons:/addons + - /opt/1panel/docker/compose/mitmproxy/mitm-data:/home/mitmproxy/.mitmproxy + environment: + - TZ=Asia/Shanghai + logging: + options: + max-size: "10m" + max-file: "3" + + xboxbot: + build: + context: ./xboxbot + dockerfile: Dockerfile + container_name: xboxbot + restart: unless-stopped + + environment: + - TELEGRAM_BOT_TOKEN=8293676109:AAG3f6GnZNxJiwxwyGh_OtTU4wGn6-ypg_4 + - TELEGRAM_ALLOWED_USER_ID=1732587552,7935041828 + - STATE_PATH=/shared/state.json + - MITM_MARKET_LOCALE=en-ng + - MITM_MARKET_CODE=NG + + volumes: + # 让 bot 跟 mitmproxy 共享同一份 state.json + - /opt/1panel/docker/compose/mitmproxy/mitm-data:/shared:rw + + depends_on: + - mitmproxy + + logging: + options: + max-size: "5m" + max-file: "3" \ No newline at end of file diff --git a/mitmproxy/mitm-data/mitmproxy-ca-cert.cer b/mitmproxy/mitm-data/mitmproxy-ca-cert.cer new file mode 100644 index 0000000..206490c --- /dev/null +++ b/mitmproxy/mitm-data/mitmproxy-ca-cert.cer @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNTCCAh2gAwIBAgIUd4CmNZbEMWsQ9UFIrXFJx87zufEwDQYJKoZIhvcNAQEL +BQAwKDESMBAGA1UEAwwJbWl0bXByb3h5MRIwEAYDVQQKDAltaXRtcHJveHkwHhcN +MjUxMDI1MTIzMzA0WhcNMzUxMDI1MTIzMzA0WjAoMRIwEAYDVQQDDAltaXRtcHJv +eHkxEjAQBgNVBAoMCW1pdG1wcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALCVT6FiAvJgHkPt7lgd7ZfhD5lFmjZ/P7OmBloRZ4A/xTBVTV/a7KGJ +J7tscBRrTWihNHIQZcm8sKpCgwLrQc4SCTBlOPHQ8Imh9gLe8r1XBu+nCduScYXy +31R9Lpyh89RqW/384HgVMVKRf8qcv6qikk5+GqxZxtgIBmR0RyU+jPyBSFV0djjx +kdkuKcFF8VNayL2QCXtofBS5/+xk+iLzMBIqU8WIXGEvf2mTq8iM63bXQNVq3Iq/ +zyUgOlAF++XciY6uTu5ln6+tBlJXHwKTcQM75Jk91tjfGDtSAoRPZ0iduZtNO5mh +qHxcUXaqFocPscYKnY4OgfQ3UjlTHjMCAwEAAaNXMFUwDwYDVR0TAQH/BAUwAwEB +/zATBgNVHSUEDDAKBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FNcWV6LaBZxIcAciAhMEreA8Eb+fMA0GCSqGSIb3DQEBCwUAA4IBAQBJNkaTgCJq +RTyv0PQgt2cw96mwGmjdBbeVUrQrTdaQkSIzaekpD/5bidiD0AqZtZT2/H+BeVRW +d+fO8Rp74CipqKpEut/fl7gMbiHWGYtUxmtG+TCe2+c6syW2BEDCt+dD1mYgN7Of +itllDqh/61HhCtEiEo4HzAjSiMhmbt/uT8310pyhmtuRSgsQLyLk4F1Qlp092ObA +cLTjafd6x5TjaYMxaGXEyi7MyvHOAr+UCaotap7xQT7NdQ9ewGYtmyTBUszJ0j1i +ec/hny65gfCDk5wC6556e5YXigF/WEjV5zDykskrx4bdVoqxvJd+F1ITNrBSpGDj +7wetRUpGIcKM +-----END CERTIFICATE----- diff --git a/mitmproxy/mitm-data/mitmproxy-ca-cert.p12 b/mitmproxy/mitm-data/mitmproxy-ca-cert.p12 new file mode 100644 index 0000000..5b38101 Binary files /dev/null and b/mitmproxy/mitm-data/mitmproxy-ca-cert.p12 differ diff --git a/mitmproxy/mitm-data/mitmproxy-ca-cert.pem b/mitmproxy/mitm-data/mitmproxy-ca-cert.pem new file mode 100644 index 0000000..206490c --- /dev/null +++ b/mitmproxy/mitm-data/mitmproxy-ca-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNTCCAh2gAwIBAgIUd4CmNZbEMWsQ9UFIrXFJx87zufEwDQYJKoZIhvcNAQEL +BQAwKDESMBAGA1UEAwwJbWl0bXByb3h5MRIwEAYDVQQKDAltaXRtcHJveHkwHhcN +MjUxMDI1MTIzMzA0WhcNMzUxMDI1MTIzMzA0WjAoMRIwEAYDVQQDDAltaXRtcHJv +eHkxEjAQBgNVBAoMCW1pdG1wcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALCVT6FiAvJgHkPt7lgd7ZfhD5lFmjZ/P7OmBloRZ4A/xTBVTV/a7KGJ +J7tscBRrTWihNHIQZcm8sKpCgwLrQc4SCTBlOPHQ8Imh9gLe8r1XBu+nCduScYXy +31R9Lpyh89RqW/384HgVMVKRf8qcv6qikk5+GqxZxtgIBmR0RyU+jPyBSFV0djjx +kdkuKcFF8VNayL2QCXtofBS5/+xk+iLzMBIqU8WIXGEvf2mTq8iM63bXQNVq3Iq/ +zyUgOlAF++XciY6uTu5ln6+tBlJXHwKTcQM75Jk91tjfGDtSAoRPZ0iduZtNO5mh +qHxcUXaqFocPscYKnY4OgfQ3UjlTHjMCAwEAAaNXMFUwDwYDVR0TAQH/BAUwAwEB +/zATBgNVHSUEDDAKBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FNcWV6LaBZxIcAciAhMEreA8Eb+fMA0GCSqGSIb3DQEBCwUAA4IBAQBJNkaTgCJq +RTyv0PQgt2cw96mwGmjdBbeVUrQrTdaQkSIzaekpD/5bidiD0AqZtZT2/H+BeVRW +d+fO8Rp74CipqKpEut/fl7gMbiHWGYtUxmtG+TCe2+c6syW2BEDCt+dD1mYgN7Of +itllDqh/61HhCtEiEo4HzAjSiMhmbt/uT8310pyhmtuRSgsQLyLk4F1Qlp092ObA +cLTjafd6x5TjaYMxaGXEyi7MyvHOAr+UCaotap7xQT7NdQ9ewGYtmyTBUszJ0j1i +ec/hny65gfCDk5wC6556e5YXigF/WEjV5zDykskrx4bdVoqxvJd+F1ITNrBSpGDj +7wetRUpGIcKM +-----END CERTIFICATE----- diff --git a/mitmproxy/mitm-data/mitmproxy-ca.p12 b/mitmproxy/mitm-data/mitmproxy-ca.p12 new file mode 100644 index 0000000..fbb2e8f Binary files /dev/null and b/mitmproxy/mitm-data/mitmproxy-ca.p12 differ diff --git a/mitmproxy/mitm-data/mitmproxy-ca.pem b/mitmproxy/mitm-data/mitmproxy-ca.pem new file mode 100644 index 0000000..a2e11cc --- /dev/null +++ b/mitmproxy/mitm-data/mitmproxy-ca.pem @@ -0,0 +1,47 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsJVPoWIC8mAeQ+3uWB3tl+EPmUWaNn8/s6YGWhFngD/FMFVN +X9rsoYknu2xwFGtNaKE0chBlybywqkKDAutBzhIJMGU48dDwiaH2At7yvVcG76cJ +25JxhfLfVH0unKHz1Gpb/fzgeBUxUpF/ypy/qqKSTn4arFnG2AgGZHRHJT6M/IFI +VXR2OPGR2S4pwUXxU1rIvZAJe2h8FLn/7GT6IvMwEipTxYhcYS9/aZOryIzrdtdA +1Wrcir/PJSA6UAX75dyJjq5O7mWfr60GUlcfApNxAzvkmT3W2N8YO1IChE9nSJ25 +m007maGofFxRdqoWhw+xxgqdjg6B9DdSOVMeMwIDAQABAoIBAAmvXss4r3WwvOg3 +5BM0LQRgBTWAYklr6EgHqqCFBNq9ZKjWfORtgv4HIkU+2NTd38SF1vUMnjCW50+n +Tz05PwY46gUcACgPMCo0VmRo6wJkhA5f6IQA/7X7kLE3Hnfb0B5N6RbAqGUdsHNZ +ZJttxntq6EBi3T6nu+a8ZfFbiU1rDgfAMWUw0I3tvRO9jazyS1x6eQ2wYl4k0eOa +4lfo1Oj+rlZnCNPZB3aZgTi4zscDh5pnvGXLAYh/OOY4/TZ5yzbjGB8AnLrEXT99 +K+VzxMrwVsRXZJoefKTvKZml/pD2FaTqbeGUKmosxxsr+9nhigh502ZbubUHVnXq +dhLQvrUCgYEA2MLry0H8K+a5r/7FMdqKIpqNaawKSHYacf5rPVrRjzyGxXFxOtQw +0AdPwQccqGL5PTHuNDrTH4411yKJLGXcrPFnpOsBCdQFUCqXf4LtObW2btwrQgX8 +ON1iND/qErbqaWMNCN+XMWVkZCYXn1mR42Y93+Lo7SvcLQZQLHKIuK8CgYEA0Ix3 +0Kkx3zmFDHG1G8Kyz9eLohMqO3s7TmRzuhKUCSnBZNFEvknFj5UYe6vIoZ/iVbY7 +klE1HGvEKbCftlES3FBF0PCvP2Dgk53WuAOtZQ3TBeD5IvLBgmPo9t5pqpRoZx/I +QaEtp3gQBbPhad7kSVkFrURfnCy1DoKmutsQy70CgYEAt291JgSISAqwV30OGhts +TM5oH+YkqZ+wz4lT7Y8+yq2ZC9vty7AoQtP9LUg8e0+OxrfLcs3ZPtoVPCOQ3E6z +inOcl1b9APk5Kddxb8o3wV/CrFyMCwqPoPvQkJEKIJ5FD7xwGnNFOtsoMwx9by/Y +ow0yDZa0MYmtgTjXflXK2CcCgYBge5wy/RQFoibbyv9vCHSRk7cWFKfFPQ4DBpZD +z7SNSLQgYHDdWGP+OYxKKv93RvD/ln+ZAdkAfRsT7pL1VizToI+sSq3JNJixsqRU +Hd9qkSq/3YVllcnQ+UgebmeUc3SZwSp0sozcnb9L5By1TllvVbA6qRdSuZxKSke4 +ywDKQQKBgCeHXftLItkzJ9xIpM7Rzb0WgKxNILo4lxk0sqmO63eBS+6WDzDQAT0S +uBH+Ed2PM8m+VEm1XZy9A7sze3xNEu84/exoD4ErW2WxUo32Xstr+NZchG3IaS5z +U045grjM1an3nqUAFaD2nc77VlJeRNA14if7RaKgzyc49ELbxt/I +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDNTCCAh2gAwIBAgIUd4CmNZbEMWsQ9UFIrXFJx87zufEwDQYJKoZIhvcNAQEL +BQAwKDESMBAGA1UEAwwJbWl0bXByb3h5MRIwEAYDVQQKDAltaXRtcHJveHkwHhcN +MjUxMDI1MTIzMzA0WhcNMzUxMDI1MTIzMzA0WjAoMRIwEAYDVQQDDAltaXRtcHJv +eHkxEjAQBgNVBAoMCW1pdG1wcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALCVT6FiAvJgHkPt7lgd7ZfhD5lFmjZ/P7OmBloRZ4A/xTBVTV/a7KGJ +J7tscBRrTWihNHIQZcm8sKpCgwLrQc4SCTBlOPHQ8Imh9gLe8r1XBu+nCduScYXy +31R9Lpyh89RqW/384HgVMVKRf8qcv6qikk5+GqxZxtgIBmR0RyU+jPyBSFV0djjx +kdkuKcFF8VNayL2QCXtofBS5/+xk+iLzMBIqU8WIXGEvf2mTq8iM63bXQNVq3Iq/ +zyUgOlAF++XciY6uTu5ln6+tBlJXHwKTcQM75Jk91tjfGDtSAoRPZ0iduZtNO5mh +qHxcUXaqFocPscYKnY4OgfQ3UjlTHjMCAwEAAaNXMFUwDwYDVR0TAQH/BAUwAwEB +/zATBgNVHSUEDDAKBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FNcWV6LaBZxIcAciAhMEreA8Eb+fMA0GCSqGSIb3DQEBCwUAA4IBAQBJNkaTgCJq +RTyv0PQgt2cw96mwGmjdBbeVUrQrTdaQkSIzaekpD/5bidiD0AqZtZT2/H+BeVRW +d+fO8Rp74CipqKpEut/fl7gMbiHWGYtUxmtG+TCe2+c6syW2BEDCt+dD1mYgN7Of +itllDqh/61HhCtEiEo4HzAjSiMhmbt/uT8310pyhmtuRSgsQLyLk4F1Qlp092ObA +cLTjafd6x5TjaYMxaGXEyi7MyvHOAr+UCaotap7xQT7NdQ9ewGYtmyTBUszJ0j1i +ec/hny65gfCDk5wC6556e5YXigF/WEjV5zDykskrx4bdVoqxvJd+F1ITNrBSpGDj +7wetRUpGIcKM +-----END CERTIFICATE----- diff --git a/mitmproxy/mitm-data/mitmproxy-dhparam.pem b/mitmproxy/mitm-data/mitmproxy-dhparam.pem new file mode 100644 index 0000000..c10121f --- /dev/null +++ b/mitmproxy/mitm-data/mitmproxy-dhparam.pem @@ -0,0 +1,14 @@ + +-----BEGIN DH PARAMETERS----- +MIICCAKCAgEAyT6LzpwVFS3gryIo29J5icvgxCnCebcdSe/NHMkD8dKJf8suFCg3 +O2+dguLakSVif/t6dhImxInJk230HmfC8q93hdcg/j8rLGJYDKu3ik6H//BAHKIv +j5O9yjU3rXCfmVJQic2Nne39sg3CreAepEts2TvYHhVv3TEAzEqCtOuTjgDv0ntJ +Gwpj+BJBRQGG9NvprX1YGJ7WOFBP/hWU7d6tgvE6Xa7T/u9QIKpYHMIkcN/l3ZFB +chZEqVlyrcngtSXCROTPcDOQ6Q8QzhaBJS+Z6rcsd7X+haiQqvoFcmaJ08Ks6LQC +ZIL2EtYJw8V8z7C0igVEBIADZBI6OTbuuhDwRw//zU1uq52Oc48CIZlGxTYG/Evq +o9EWAXUYVzWkDSTeBH1r4z/qLPE2cnhtMxbFxuvK53jGB0emy2y1Ei6IhKshJ5qX +IB/aE7SSHyQ3MDHHkCmQJCsOd4Mo26YX61NZ+n501XjqpCBQ2+DfZCBh8Va2wDyv +A2Ryg9SUz8j0AXViRNMJgJrr446yro/FuJZwnQcO3WQnXeqSBnURqKjmqkeFP+d8 +6mk2tqJaY507lRNqtGlLnj7f5RNoBFJDCLBNurVgfvq9TCVWKDIFD4vZRjCrnl6I +rD693XKIHUCWOjMh1if6omGXKHH40QuME2gNa50+YPn1iYDl88uDbbMCAQI= +-----END DH PARAMETERS----- diff --git a/mitmproxy/mitm-data/state.json b/mitmproxy/mitm-data/state.json new file mode 100644 index 0000000..70b4c11 --- /dev/null +++ b/mitmproxy/mitm-data/state.json @@ -0,0 +1,9 @@ +{ + "XboxProductList": "", + "__xbox_products__": [], + "authorization": "XBL3.0 x=14880394912820501845;eyJlbmMiOiJBMTI4Q0JDK0hTMjU2IiwiYWxnIjoiUlNBLU9BRVAiLCJjdHkiOiJKV1QiLCJ6aXAiOiJERUYiLCJ4NXQiOiJYQmRHb0FoTDJyb3FQazcwM3NHb1lnT19oM2MifQ.0dsyHwNppdvRy5AALuJwLY3dM7ItF--0F5x4mHhaoaB6k5qGo0nBppgHXxAl8Euqy5SMsyrupn6nLlv-NFKj_XS_ToZTSLR7OKEGJMgqbSBGXmmjL1Z3b2QlmirhPq2YiOQdCJLgEVr-fgODOtF4KkD_mOgLtmI5r1awbtiryYeEwVgmuePDHj28CbwdgshvTNwNNzVXJlUJCuVF1BAOHlx0kvwh2L0TmHUxS5v47trq1SbejDLABwZKmm-ACP-npOsjJNcCvqSaitCL8dLgzGWRalSBKvLEVgEyinQhn1Aootxu5h7dalDY1cUm7axU1mq9feY-fjTNJU7ILqZNiw.Vg9ZgjCQhKQuhPimfy8Auw.6YTbB772GecNu5VZT6ibSLfASHVdiB1X5DM5P_AMgyPgJ0KWWJb8pd3L9UQiPFuYHbw9ESM5Q-eiH0_XxDeTdLH8rJ3txIp5g8rSuvgz3ZxbyrIAsoHkVwRlSOTfTECJxpHiaitlFrYlzZF1zhBSKfnNT92xxu7gkN5C3NdUayKqzb1P4-d7KqNateL1bGKLUuVCterfaNkDPbuBxYdqAz0Za53kK4nHR6eOssIbe5vJihwrnNY_H6KpYqEprIW9g2IssPsBgwD6Zt16N99Fdx5X_taBBJcH8FswPBlZQ-l78TrOPlXFYt9Kyjsnu8XqBk82TfgryxHfFYVgOOCoFWuw6oMSS4LMNf5zp-WYfUCRWjMm2YCKkE894aBxbvdGUIPwATh055xrx97DIYjgutuZWG_HpXuwlqDUMTzgXPOLhhKuybinFpXd90iHvh2cst4KhVUqTDqaOM4Gm_SAqPEREDzsE5TyR30Lgo5Fi9zBLU5EhRz92Voqvz1xQmdRWU2WJ74u7Ez6g1s5C_CTFiqL-JT9SWYt4MlTz9118h-PFldkJcJm5FDlX3Yd2PoRo03fSlRUX6BDYyJVL63s_QLeFyaa0SqpX-ZFNYEgCE5X2YSAMaqmXkbGqfW90v0_DCH-hj3ul5PT0L_Q0pTaGq9Y6hW6FOGwLyZsh0xYdaGzVNrFnUjE2FJ4dMRJuSyU8imT2owugyfQLlUUrazrKjkQcKwa0HOZ1WltV8tHEjbtq0pHUw05WdeBOCF6zCuJ4i1LTAoX7h3UQFcD9hw_5huRziQObCrABVD23y4pntoeFcyFrBgXAfpkI44CsJdhr_3E8PwZjbEZsfJpoJzttD0q-h_x5LHlr_dCRoqWnUWGgatTNS9oSAA4v75X80kUg1SXrEXwyduo_0Q1wHxdR64QDHIfBVpGCKT_nCBLRdxIq8hhuhCbVC5ypPLMf-Fgg9_mbMk7A_f59suyDKd3sJ6Sm0cJ9V_jcMHAzNnz-puFpYz1rjf62_oTH6Efh187jd6FpNaFuH2Yx3SeNcbGD9fCYYxOlWW8yPqnZ1suI9rY1JY9beXoS4qMIO6N4OOiE_cyDe0jtM8jfXE1Kc98SA1nVXxnHhS1Qm64Yq4qbTCSkYYyhNJJxEorgZnHYXppo9JLR1Y8hNOHeuSgvRyxc8FR_wGDqES9dUol5g5lxuNRrDRMXIm7MKc8otEl373y_9Jy0espxQYK8KSFIIKgS5YlTDeUGFuaf6iRYcBiS1u5RRd2fKEH5DffbmATGGmprFINTKyaNWgPuGSNlBcNAUiFxjkzpt3hcvEnMR4kZW_7_adp-EIqc8P-jaSPMo7xtESvbylPlxNuyOjWDTgc2yHYRNG8-p1mgsmwTRZSbIbVC_Bl_tZBMiKgEe2K0taooPW7MHx863KRuAWzRx7_BG1duPnuPrUFZlO00bj67MeUoAQJ7Ynbhj-KEH-DhUZCyttXV0kkq5slwylyMZD0IEtc_dt7-gpSxMCVJ3p8NFQj74ctHu9Y8PVNDpqnO28cpE-TYfllT6f_s75wPoG2rGa_MAqarvn-0GX_qHOsOiFFmF_dMMSaRNvT-osLgAncViNF6-TQj5cQiJTuZ3sW0zC_8Z9MXYyQ1OijVk-9UfuMpFROcxPzMlBFE2L1RHFddl0705lgwryPaLyqhV1HUM3biIC654UBM2BMoJxXhBHFBxsre6H5TE8qocCqob-8Q5pzAnbuPiya9TLzr5j2KO5uvzqtu3i0qNmJPe0QN0VCHuvfSR-lRsgP0HNnz7q9cbrI4d8RS8Xq6UWcjKJPnQ.rxI5siBky9SiRVAJkDDF087tTDM_-rLfXcG1k3A2oDU", + "cartId": "4d2c224a-de06-46e3-96f3-fd3b401c03c2", + "fiddler.custom.cart-ms-cv": "gMXAEvO1t6dBoZCZSRPgmi.11.5", + "fiddler.custom.cart-x-authorization-muid": "D41F02B00EF54F55B781E233271D20E3", + "fiddler.custom.cart-x-ms-vector-id": "1DE426055454C535A722633B2D3B1BD85BDBB32537FB0C689A852DE57F892389" +} \ No newline at end of file diff --git a/mitmproxy/xboxbot/Dockerfile b/mitmproxy/xboxbot/Dockerfile new file mode 100644 index 0000000..3520379 --- /dev/null +++ b/mitmproxy/xboxbot/Dockerfile @@ -0,0 +1,12 @@ +# xboxbot/Dockerfile +FROM python:3.11-slim + +WORKDIR /app + +# 安装依赖: +# - python-telegram-bot 用于 Telegram 机器人 (异步 API) +RUN pip install --no-cache-dir python-telegram-bot==21.6 + +COPY bot.py /app/bot.py + +CMD ["python", "/app/bot.py"] \ No newline at end of file diff --git a/mitmproxy/xboxbot/bot.py b/mitmproxy/xboxbot/bot.py new file mode 100644 index 0000000..25ff3ee --- /dev/null +++ b/mitmproxy/xboxbot/bot.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- +import os, json, uuid, ssl, time, re +from urllib import request as urlrequest + +from telegram import Update +from telegram.ext import ( + ApplicationBuilder, + CommandHandler, + ContextTypes, +) + +# ========== 环境变量 ========== +BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"] + +# [修改] 解析允许的用户ID列表 (支持逗号分隔) +raw_ids = os.environ.get("TELEGRAM_ALLOWED_USER_ID", "") +# 去除两端空格,按逗号分割,并过滤掉空项 +ALLOWED_UIDS = [x.strip() for x in raw_ids.split(',') if x.strip()] + +STATE_PATH = os.environ.get("STATE_PATH", "/shared/state.json") + +# 用于 /addtocart 时构造请求体 +MARKET_LOCALE = os.environ.get("MITM_MARKET_LOCALE", "en-ng") # e.g. en-ng +MARKET_CODE = os.environ.get("MITM_MARKET_CODE" , "NG") # e.g. NG + +UA = ( + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_3_1 like Mac OS X) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/133.0.3065.54 " + "Version/18.0 Mobile/15E148 Safari/604.1" +) +CART_API_URL = ( + "https://cart.production.store-web.dynamics.com/" + "cart/v1.0/cart/loadCart?cartType=consumer&appId=StoreWeb" +) + +# ========== 工具函数:state.json 读写 ========== +def _load_state(): + try: + with open(STATE_PATH, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return {} + +def _save_state(st): + tmp = STATE_PATH + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(st, f, ensure_ascii=False, indent=2, sort_keys=True) + os.replace(tmp, STATE_PATH) + +def _getv(key, default=""): + st = _load_state() + return st.get(key, default) + +def _setv(key, value): + st = _load_state() + st[key] = value + _save_state(st) + +def _get_products(st=None): + if st is None: + st = _load_state() + arr = st.get("__xbox_products__", []) + out = [] + if isinstance(arr, list): + for it in arr: + try: + p = str(it.get("ProductId","")).strip() + s = str(it.get("SkuId","")).strip() + a = str(it.get("AvailabilityId","")).strip() + if p and s and a: + out.append({"ProductId":p,"SkuId":s,"AvailabilityId":a}) + except Exception: + continue + return out + +def _save_products_no_notify(new_list): + """ + 用在机器人手动指令时更新 products 列表和 XboxProductList, + 但不发送任何 Telegram 自动推送(因为推送是 mitmproxy 插件做的)。 + """ + # 去重 + seen = set() + uniq = [] + for it in new_list: + key = f"{it['ProductId']}||{it['SkuId']}||{it['AvailabilityId']}" + if key in seen: + continue + seen.add(key) + uniq.append(it) + + st = _load_state() + st["__xbox_products__"] = uniq + + parts = [] + for i, it in enumerate(uniq, 1): + parts.append(f"product{i}={it['ProductId']}|{it['SkuId']}|{it['AvailabilityId']}") + st["XboxProductList"] = ";".join(parts) + + _save_state(st) + +# ========== 工具函数:加购 (复用 mitmproxy 那套逻辑) ========== +def _cart_put_single(muid: str, ms_cv: str, pid: str, sid: str, aid: str, x_vec: str = ""): + """ + 对单个 (ProductId, SkuId, AvailabilityId) 调用微软购物车接口。 + """ + payload = { + "locale": MARKET_LOCALE, + "market": MARKET_CODE, + "catalogClientType": "storeWeb", + "friendlyName": f"cart-{MARKET_CODE}", + "riskSessionId": str(uuid.uuid4()), + "clientContext": {"client": "UniversalWebStore.Cart", "deviceType": "Pc"}, + "itemsToAdd": { + "items": [ + { + "productId": pid, + "skuId": sid, + "availabilityId": aid, + "campaignId": "xboxcomct", + "quantity": 1, + } + ] + }, + } + + data = json.dumps(payload, separators=(",", ":")).encode("utf-8") + + req = urlrequest.Request(CART_API_URL, data=data, method="PUT") + req.add_header("content-type", "application/json") + req.add_header("accept", "*/*") + req.add_header("x-authorization-muid", muid or "") + req.add_header("x-validation-field-1", "9pgbhbppjf2b") + req.add_header("ms-cv", ms_cv or "") + if x_vec: + req.add_header("x-ms-vector-id", x_vec) + req.add_header("accept-language", "en-US,en;q=0.9") + req.add_header("accept-encoding", "gzip, deflate, br") + req.add_header("sec-fetch-site", "cross-site") + req.add_header("sec-fetch-mode", "cors") + req.add_header("sec-fetch-dest", "empty") + req.add_header("origin", "https://www.microsoft.com") + req.add_header("referer", "https://www.microsoft.com/") + req.add_header("user-agent", UA) + + ctx_ssl = ssl.create_default_context() + with urlrequest.urlopen(req, context=ctx_ssl, timeout=20) as resp: + body = resp.read()[:2048] + return resp.status, (200 <= resp.status < 300), body + +# ========== 安全检查 / 回复封装 ========== +async def _auth_guard(update: Update, context: ContextTypes.DEFAULT_TYPE): + uid = str(update.effective_user.id) + # [修改] 检查 ID 是否在列表中 + if uid not in ALLOWED_UIDS: + await context.bot.send_message( + chat_id=update.effective_chat.id, + text=f"❌ Unauthorized (UID: {uid})", # 方便调试,显示未授权ID + disable_notification=False, + ) + return False + return True + +async def _reply(update: Update, context: ContextTypes.DEFAULT_TYPE, text: str): + # 统一把 disable_notification=False 打开,保证有推送提醒 + await context.bot.send_message( + chat_id=update.effective_chat.id, + text=text, + disable_notification=False, + ) + +# ========== 命令实现 ========== +async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await _auth_guard(update, context): + return + await _reply( + update, + context, + "👋 你好!我是 Xbox 助手。\n可以使用 /help 查看帮助", + ) + +async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await _auth_guard(update, context): + return + help_text = ( + "可用指令:\n" + "/check - 查看当前关键参数(cartId等)和XboxProductList摘要\n" + "/viewproducts - 查看已捕捉到的全部三元组(ProductId, SkuId, AvailabilityId)\n" + "/clearproducts - 清空捕捉到的产品列表\n" + "/addtocart - 尝试把所有已捕捉的产品逐个加进购物车\n" + "/help - 再次查看本帮助\n" + ) + await _reply(update, context, help_text) + +async def cmd_check(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await _auth_guard(update, context): + return + st = _load_state() + + cart_id = st.get("cartId","") + auth = st.get("authorization","") + muid = st.get("fiddler.custom.cart-x-authorization-muid","") + ms_cv = st.get("fiddler.custom.cart-ms-cv","") + xvec = st.get("fiddler.custom.cart-x-ms-vector-id","") + plist_str = st.get("XboxProductList","") + products = _get_products(st) + countp = len(products) + + text = ( + "📦 /check\n" + f"cartId: {cart_id or '(none)'}\n" + f"authorization(len): {len(auth) if auth else 0}\n" + f"x-authorization-muid: {muid or '(none)'}\n" + f"ms-cv: {ms_cv or '(none)'}\n" + f"x-ms-vector-id: {xvec or '(none)'}\n" + f"XboxProductList: {plist_str or '(empty)'}\n" + f"products_count: {countp}\n" + ) + await _reply(update, context, text) + +async def cmd_viewproducts(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await _auth_guard(update, context): + return + products = _get_products() + if not products: + await _reply(update, context, "📄 /viewproducts\n当前产品列表为空") + return + + lines = ["📄 /viewproducts"] + for idx, it in enumerate(products, 1): + lines.append( + f"{idx}. {it['ProductId']} | {it['SkuId']} | {it['AvailabilityId']}" + ) + await _reply(update, context, "\n".join(lines)) + +async def cmd_clearproducts(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await _auth_guard(update, context): + return + _save_products_no_notify([]) # 清空并写回 + await _reply(update, context, "🗑 已清空产品列表") + +async def cmd_addtocart(update: Update, context: ContextTypes.DEFAULT_TYPE): + if not await _auth_guard(update, context): + return + + st = _load_state() + products = _get_products(st) + if not products: + await _reply(update, context, "🛒 /addtocart\n没有待加入的产品") + return + + muid = st.get("fiddler.custom.cart-x-authorization-muid","") + ms_cv = st.get("fiddler.custom.cart-ms-cv","") + x_vec = st.get("fiddler.custom.cart-x-ms-vector-id","") + + if (not muid) or (not ms_cv): + await _reply(update, context, + "🛒 /addtocart\n缺少 cart-x-authorization-muid 或 cart-ms-cv,无法继续") + return + + successes = [] + failures = [] + remain = list(products) + + for item in products: + pid = item["ProductId"] + sid = item["SkuId"] + aid = item["AvailabilityId"] + triple_label = f"{pid}/{sid}/{aid}" + try: + code, ok, _ = _cart_put_single(muid, ms_cv, pid, sid, aid, x_vec) + except Exception as ex: + code = 0 + ok = False + + if ok: + successes.append(f"{triple_label} -> {code}") + # 从 remain 里删掉成功的 + remain = [ + r for r in remain + if not (r["ProductId"] == pid and r["SkuId"] == sid and r["AvailabilityId"] == aid) + ] + else: + failures.append(f"{triple_label} -> {code or 'ERR'}") + + time.sleep(0.15) + + # 更新 state.json 里的 products / XboxProductList (不推送通知,只本地改) + _save_products_no_notify(remain) + + summary = ( + "🛒 /addtocart 完成\n" + f"尝试: {len(products)}\n" + f"成功: {len(successes)}\n" + f"失败: {len(failures)}\n\n" + ) + if successes: + summary += "✅ 成功:\n" + "\n".join(successes[:20]) + ("\n..." if len(successes) > 20 else "") + "\n" + if failures: + summary += "❌ 失败:\n" + "\n".join(failures[:20]) + ("\n..." if len(failures) > 20 else "") + "\n" + summary += f"\n剩余待加入: {len(remain)}" + + await _reply(update, context, summary) + +# ========== 主入口 ========== +def main(): + app = ApplicationBuilder().token(BOT_TOKEN).build() + + app.add_handler(CommandHandler("start", cmd_start)) + app.add_handler(CommandHandler("help", cmd_help)) + app.add_handler(CommandHandler("check", cmd_check)) + app.add_handler(CommandHandler("viewproducts", cmd_viewproducts)) + app.add_handler(CommandHandler("clearproducts",cmd_clearproducts)) + app.add_handler(CommandHandler("addtocart", cmd_addtocart)) + + # 直接长轮询。通知静默控制在我们send_message里disable_notification=False + app.run_polling(drop_pending_updates=True) + +if __name__ == "__main__": + main() \ No newline at end of file