diff --git a/subscribe/airport.py b/subscribe/airport.py index 977a0803f..fbdd6b24e 100644 --- a/subscribe/airport.py +++ b/subscribe/airport.py @@ -52,6 +52,9 @@ # 生成随机字符串时候选字符 LETTERS = set(string.ascii_letters + string.digits) +# 常见非标准 API 前缀 +ANOTHER_API_PREFIX = "/api?scheme=" + # 标记数字位数 # SUFFIX_BITS = 2 @@ -102,6 +105,9 @@ class RegisterRequire: # 是否是 sspanel 面板 sspanel: bool = False + # 接口地址前缀 + api_prefix: str = "" + class NoRedirHandler(urllib.request.HTTPRedirectHandler): def http_error_302(self, req, fp, code, msg, headers): @@ -123,8 +129,14 @@ def sniff(url: str) -> int: except Exception: return -2 - url = f"{domain}/api/v1/passport/auth/login" - return False if sniff(url=url) == 200 else sniff(url=f"{domain}/auth/login") == 200 + return ( + False + if ( + sniff(url=f"{domain}/api/v1/passport/auth/login") == 200 + or sniff(url=f"{domain}/api?scheme=passport/auth/login") == 200 + ) + else sniff(url=f"{domain}/auth/login") == 200 + ) class AirPort: @@ -138,10 +150,12 @@ def __init__( include: str = "", liveness: bool = True, coupon: str = "", + api_prefix: str = "/api/v1/", ): if site.endswith("/"): site = site[: len(site) - 1] + self.api_prefix = utils.get_subpath(api_prefix) if sub.strip() != "": if sub.startswith(utils.FILEPATH_PROTOCAL): ref = sub[8:] @@ -156,10 +170,10 @@ def __init__( self.send_email = "" else: self.sub = "" - self.fetch = f"{site}/api/v1/user/server/fetch" + self.fetch = f"{site}{self.api_prefix}user/server/fetch" self.registed = False - self.send_email = f"{site}/api/v1/passport/comm/sendEmailVerify" - self.reg = f"{site}/api/v1/passport/auth/register" + self.send_email = f"{site}{self.api_prefix}passport/comm/sendEmailVerify" + self.reg = f"{site}{self.api_prefix}passport/auth/register" self.ref = site self.name = name self.rename = rename @@ -167,20 +181,42 @@ def __init__( self.include = include self.liveness = liveness self.coupon = "" if utils.isblank(coupon) else coupon - self.headers = {"User-Agent": utils.USER_AGENT, "Referer": self.ref + "/", "Origin": self.ref} + self.headers = { + "User-Agent": utils.USER_AGENT, + "Referer": self.ref + "/", + "Origin": self.ref, + "Host": utils.extract_domain(self.ref), + } self.username = "" self.password = "" self.available = True @staticmethod - def get_register_require(domain: str, proxy: str = "", default: bool = True) -> RegisterRequire: + def get_register_require( + domain: str, + proxy: str = "", + default: bool = True, + api_prefix: str = "", + ) -> RegisterRequire: domain = utils.extract_domain(url=domain, include_protocal=True) if not domain: return RegisterRequire(verify=default, invite=default, recaptcha=default) - url = f"{domain}/api/v1/guest/comm/config" - content = utils.http_get(url=url, retry=2, proxy=proxy) - if not content or not content.startswith("{") and content.endswith("}"): + api_prefix = utils.trim(api_prefix) + subpath = utils.get_subpath(api_prefix) + + content = utils.http_get( + url=f"{domain}{subpath}guest/comm/config", + retry=2, + proxy=proxy, + ) + if not content and (not api_prefix or subpath != ANOTHER_API_PREFIX): + api_prefix = ANOTHER_API_PREFIX + + logger.debug(f"[QueryError] try to explore another register require, domain: {domain}") + content = utils.http_get(url=f"{domain}{api_prefix}guest/comm/config", retry=2, proxy=proxy) + + if not content.startswith("{") and content.endswith("}"): logger.debug(f"[QueryError] cannot get register require, domain: {domain}") return RegisterRequire(verify=default, invite=default, recaptcha=default) @@ -204,6 +240,7 @@ def get_register_require(domain: str, proxy: str = "", default: bool = True) -> invite=invite_force, recaptcha=recaptcha, whitelist=whitelist, + api_prefix=api_prefix, ) except: @@ -216,7 +253,11 @@ def sen_email_verify(self, email: str, retry: int = 3) -> bool: data = urllib.parse.urlencode(params).encode(encoding="UTF8") headers = deepcopy(self.headers) + headers["Content-Type"] = "application/x-www-form-urlencoded" + if self.api_prefix == ANOTHER_API_PREFIX: + headers["Content-Type"] = "application/json" + data = json.dumps(params).encode(encoding="UTF8") try: request = urllib.request.Request(self.send_email, data=data, headers=headers, method="POST") @@ -249,6 +290,10 @@ def register( headers = deepcopy(self.headers) headers["Content-Type"] = "application/x-www-form-urlencoded" + if self.api_prefix == ANOTHER_API_PREFIX: + headers["Content-Type"] = "application/json" + data = json.dumps(params).encode(encoding="UTF8") + try: request = urllib.request.Request(self.reg, data=data, headers=headers, method="POST") response = urllib.request.urlopen(request, timeout=10, context=utils.CTX) @@ -275,14 +320,17 @@ def register( authorization=authorization, ) - if token: - self.sub = f"{self.ref}/api/v1/client/subscribe?token={token}" - else: - subscribe_info = renewal.get_subscribe_info( - domain=self.ref, cookies=cookies, authorization=authorization - ) - if subscribe_info: - self.sub = subscribe_info.sub_url + subscribe_info = renewal.get_subscribe_info( + domain=self.ref, + cookies=cookies, + authorization=authorization, + api_prefix=self.api_prefix, + ) + if subscribe_info: + self.sub = subscribe_info.sub_url + if not self.sub: + if token: + self.sub = f"{self.ref}/api/v1/client/subscribe?token={token}" else: logger.error(f"[RegisterError] cannot get token when register, domain: {self.ref}") @@ -298,12 +346,15 @@ def order_plan( authorization: str = "", retry: int = 3, ) -> bool: + jsonify = self.api_prefix == ANOTHER_API_PREFIX plan = renewal.get_free_plan( domain=self.ref, cookies=cookies, authorization=authorization, retry=retry, coupon=self.coupon, + api_prefix=self.api_prefix, + jsonify=jsonify, ) if not plan: @@ -312,7 +363,12 @@ def order_plan( else: logger.info(f"found free plan, domain: {self.ref}, plan: {plan}") - methods = renewal.get_payment_method(domain=self.ref, cookies=cookies, authorization=authorization) + methods = renewal.get_payment_method( + domain=self.ref, + cookies=cookies, + authorization=authorization, + api_prefix=self.api_prefix, + ) method = random.choice(methods) if methods else 1 params = { @@ -322,6 +378,8 @@ def order_plan( "plan_id": plan.plan_id, "method": method, "coupon_code": self.coupon, + "api_prefix": self.api_prefix, + "jsonify": jsonify, } success = renewal.flow( @@ -368,7 +426,15 @@ def get_subscribe( return "", "" invite_code = utils.trim(invite_code) - rr = rr if rr is not None else self.get_register_require(domain=self.ref, default=False) + rr = ( + rr + if rr is not None + else self.get_register_require( + domain=self.ref, + default=False, + api_prefix=self.api_prefix, + ) + ) # 需要邀请码或者强制验证 if ( @@ -379,6 +445,9 @@ def get_subscribe( self.available = False return "", "" + # API地址前缀 + self.api_prefix = rr.api_prefix or self.api_prefix + if not rr.verify: email = utils.random_chars(length=random.randint(6, 10), punctuation=False) password = utils.random_chars(length=random.randint(8, 16), punctuation=True) @@ -391,10 +460,10 @@ def get_subscribe( email = f"{email}@{email_domain}" return self.register(email=email, password=password, invite_code=invite_code, retry=retry) else: - onlygmail = True if rr.whitelist and rr.verify else False + only_gmail = True if rr.whitelist and rr.verify else False try: - mailbox = mailtm.create_instance(onlygmail=onlygmail) + mailbox = mailtm.create_instance(only_gmail=only_gmail) account = mailbox.get_account() if not account: logger.error(f"cannot create temporary email account, site: {self.ref}") @@ -404,28 +473,30 @@ def get_subscribe( with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: starttime = time.time() try: - future = executor.submit(mailbox.monitor_account, account, 240, random.randint(1, 3)) + future = executor.submit(mailbox.monitor_account, account, 120, random.randint(1, 3)) success = self.sen_email_verify(email=account.address, retry=3) if not success: executor.shutdown(wait=False) return "", "" - message = future.result(timeout=180) - logger.info( + message = future.result(timeout=120) + logger.debug( f"email has been received, domain: {self.ref}\tcost: {int(time.time()- starttime)}s" ) except concurrent.futures.TimeoutError: - logger.error(f"receiving mail timeout, site: {self.ref}, address: {account.address}") + logger.error( + f"receiving mail timeout, site: {self.ref}, address: {mailbox.api_address}, email: {account.address}" + ) if not message: - logger.error(f"cannot receive any message, site: {self.ref}") return "", "" # 如果标准正则无法提取验证码则直接匹配数字 - mask = mailbox.extract_mask(message.text) or mailbox.extract_mask(message.text, r"\s+([0-9]{6})") + mask = mailbox.extract_mask(message.text) or mailbox.extract_mask(message.text, r"[:\s]+([0-9]{6})") mailbox.delete_account(account=account) if not mask: - logger.error(f"cannot fetch mask, url: {self.ref}") + logger.error(f"cannot fetch mask, url: {self.ref}, message: {message.text}") return "", "" + return self.register( email=account.address, password=account.password, @@ -463,12 +534,17 @@ def parse( with open(self.sub, "r", encoding="UTF8") as f: text = f.read() else: - headers = deepcopy(self.headers) - headers["Accept-Encoding"] = "gzip" - headers["User-Agent"] = "V2RayN; Clash.Meta; Mihomo" - + headers = {"User-Agent": "Clash.Meta; Mihomo"} trace = os.environ.get("TRACE_ENABLE", "false").lower() in ["true", "1"] - text = utils.http_get(url=self.sub, headers=headers, retry=retry, timeout=30, trace=trace).strip() + + text = utils.http_get( + url=self.sub, + headers=headers, + retry=retry, + timeout=30, + trace=trace, + interval=1, + ).strip() if "" == text or ( text.startswith("{") and text.endswith("}") and not re.search(r'"outbounds":', text, flags=re.I) diff --git a/subscribe/collect.py b/subscribe/collect.py index 695c7c79e..69224fefb 100644 --- a/subscribe/collect.py +++ b/subscribe/collect.py @@ -89,12 +89,13 @@ def parse_domains(content: str) -> dict: if not line or line.startswith("#"): continue - words = line.rsplit(delimiter, maxsplit=2) + words = line.rsplit(delimiter, maxsplit=3) address = utils.trim(words[0]) coupon = utils.trim(words[1]) if len(words) > 1 else "" invite_code = utils.trim(words[2]) if len(words) > 2 else "" + api_prefix = utils.trim(words[3]) if len(words) > 3 else "" - records[address] = {"coupon": coupon, "invite_code": invite_code} + records[address] = {"coupon": coupon, "invite_code": invite_code, "api_prefix": api_prefix} return records @@ -153,7 +154,7 @@ def parse_domains(content: str) -> dict: if candidates: for k, v in candidates.items(): item = domains.get(k, {}) - item["coupon"] = v + item.update(v) domains[k] = item @@ -185,6 +186,7 @@ def parse_domains(content: str) -> dict: domain=domain, coupon=param.get("coupon", ""), invite_code=param.get("invite_code", ""), + api_prefix=param.get("api_prefix", ""), bin_name=bin_name, rigid=rigid, chuck=chuck, diff --git a/subscribe/crawl.py b/subscribe/crawl.py index a7d0e24ff..febd3e528 100644 --- a/subscribe/crawl.py +++ b/subscribe/crawl.py @@ -1697,7 +1697,7 @@ def attempt_aurora() -> str: return domain domains = crawl_channel(channel=channel, page_num=page_num, fun=extract_airport_site) - candidates = {} if not domains else {x: "" for x in domains} + candidates = {} if not domains else {utils.extract_domain(x, True): "" for x in domains} materials = dict() jctj = crawl_jctj(convert=False) @@ -1743,10 +1743,17 @@ def attempt_aurora() -> str: logger.info(f"[AirPortCollector] extract real base url finished, start to check it now") result = utils.multi_thread_run(func=validate_domain, tasks=tasks, num_threads=num_thread, show_progress=display) - availables = [tasks[i][0] for i in range(len(tasks)) if result[i]] - logger.info(f"[AirPortCollector] finished collect airport, availables: {len(availables)}") + availables = dict() + for i in range(len(tasks)): + if not result[i][0]: + continue - return {x: records.get(x, "") for x in availables} + site = tasks[i][0] + coupon = records.get(site, "") + availables[site] = {"coupon": coupon, "api_prefix": result[i][1]} + + logger.info(f"[AirPortCollector] finished collect airport, availables: {len(availables)}") + return availables def save_candidates(candidates: dict, filepath: str, delimiter: str) -> None: @@ -1767,30 +1774,24 @@ def save_candidates(candidates: dict, filepath: str, delimiter: str) -> None: elif v and isinstance(v, dict): coupon = utils.trim(v.get("coupon", "")) invite_code = utils.trim(v.get("invite_code", "")) + api_prefix = utils.trim(v.get("api_prefix", "")) - if coupon or invite_code: - text += f"\t{delimiter}\t{coupon}" - - if invite_code: - text += f"\t{delimiter}\t{invite_code}" - + text = f"{text}\t{delimiter}\t{coupon}\t{delimiter}\t{invite_code}\t{delimiter}\t{api_prefix}" lines.append(text) utils.write_file(filename=filepath, lines=lines) -def validate_domain(url: str, rigid: bool = True, chuck: bool = False) -> bool: +def validate_domain(url: str, rigid: bool = True, chuck: bool = False) -> tuple[bool, str]: try: if not url: - return False + return False, "" rr = airport.AirPort.get_register_require(domain=url) - if rr.invite or (chuck and rr.recaptcha) or (rigid and rr.whitelist and rr.verify): - return False - - return True + flag = rr.invite or (chuck and rr.recaptcha) or (rigid and rr.whitelist and rr.verify) + return not flag, rr.api_prefix except: - return False + return False, "" def batch_call(tasks: dict) -> list[dict]: diff --git a/subscribe/mailtm.py b/subscribe/mailtm.py index d39c0cb9b..88492b67c 100644 --- a/subscribe/mailtm.py +++ b/subscribe/mailtm.py @@ -777,14 +777,12 @@ def delete_account(self, account: Account) -> bool: return True -def create_instance(onlygmail: bool = False) -> TemporaryMail: - if onlygmail: +def create_instance(only_gmail: bool = False) -> TemporaryMail: + if only_gmail: return Emailnator(onlygmail=True) - num = random.randint(0, 2) - if num == 0: - return SnapMail() - elif num == 1: + num = random.randint(0, 1) + if num == 1: return MailTM() else: return MOAKT() diff --git a/subscribe/process.py b/subscribe/process.py index f8d03b41a..43d218893 100644 --- a/subscribe/process.py +++ b/subscribe/process.py @@ -399,6 +399,9 @@ def assign( # 需要人机验证时是否直接放弃 chuck = site.get("chuck", False) + # 接口地址前缀 + api_prefix = site.get("api_prefix", "") + if not source: source = Origin.TEMPORARY.name if not domain else Origin.OWNED.name site["origin"] = source @@ -421,7 +424,8 @@ def assign( for i in range(num): index = -1 if num == 1 else i + 1 sub = subscribe[i] if subscribe else "" - renew = {} if utils.isblank(coupon) else {"coupon_code": coupon} + renew = {"coupon_code": coupon, "api_prefix": api_prefix} + globalid += 1 if accounts: renew.update(accounts[i]) @@ -450,6 +454,7 @@ def assign( chuck=chuck, special_protocols=special_protocols, invite_code=invite_code, + api_prefix=api_prefix, ) found = workflow.exists(tasks=tasks, task=task) if found: diff --git a/subscribe/renewal.py b/subscribe/renewal.py index ead63d688..1fc42cc3a 100644 --- a/subscribe/renewal.py +++ b/subscribe/renewal.py @@ -14,6 +14,7 @@ import urllib.parse import urllib.request import warnings +from copy import deepcopy from dataclasses import dataclass from datetime import datetime @@ -64,12 +65,14 @@ class Plan: trafficflow: float -def get_cookies(domain: str, username: str, password: str, retry: int = 3) -> tuple[str, str]: +def get_cookies( + domain: str, username: str, password: str, retry: int = 3, api_prefix: str = "", jsonify: bool = False +) -> tuple[str, str]: if utils.isblank(domain) or utils.isblank(username) or utils.isblank(password): return "", "" - login_url = domain + "/api/v1/passport/auth/login" - headers = HEADER + login_url = domain + utils.get_subpath(api_prefix) + "passport/auth/login" + headers = deepcopy(HEADER) headers["origin"] = domain headers["referer"] = domain + "/" @@ -79,7 +82,7 @@ def get_cookies(domain: str, username: str, password: str, retry: int = 3) -> tu } headers = {"user-agent": utils.USER_AGENT, "referer": domain} - text, authorization = login(login_url, user_info, headers, retry) + text, authorization = login(login_url, user_info, headers, retry, jsonify) return utils.extract_cookie(text), authorization @@ -97,16 +100,21 @@ def generate_headers(domain: str, cookies: str, authorization: str, headers: dic return headers -def login(url: str, params: dict, headers: dict, retry: int = 3) -> tuple[str, str]: +def login(url: str, params: dict, headers: dict, retry: int = 3, jsonify: bool = False) -> tuple[str, str]: if not params: logger.error("[RenewalError] cannot login because parameters is empty") return "", "" try: - data = urllib.parse.urlencode(params).encode(encoding="UTF8") - request = urllib.request.Request(url, data=data, headers=headers, method="POST") + if jsonify: + headers["Content-Type"] = "application/json" + data = json.dumps(params).encode(encoding="UTF8") + else: + data = urllib.parse.urlencode(params).encode(encoding="UTF8") + request = urllib.request.Request(url, data=data, headers=headers, method="POST") response = urllib.request.urlopen(request, timeout=10, context=utils.CTX) + cookies, authorization = "", "" if response.getcode() == 200: cookies = response.getheader("Set-Cookie") @@ -120,23 +128,26 @@ def login(url: str, params: dict, headers: dict, retry: int = 3) -> tuple[str, s return cookies, authorization - except Exception as e: - logger.error(e) + except: retry -= 1 - if retry > 0: - return login(url, params, headers, retry) + return login(url, params, headers, retry, jsonify) logger.error("[LoginError] URL: {}".format(utils.extract_domain(url))) return "", "" -def order(url: str, params: dict, headers: dict, retry: int = 3) -> str: +def order(url: str, params: dict, headers: dict, retry: int = 3, jsonify: bool = False) -> str: try: - data = urllib.parse.urlencode(params).encode(encoding="UTF8") - request = urllib.request.Request(url, data=data, headers=headers, method="POST") + if jsonify: + headers["Content-Type"] = "application/json" + data = json.dumps(params).encode(encoding="UTF8") + else: + data = urllib.parse.urlencode(params).encode(encoding="UTF8") + request = urllib.request.Request(url, data=data, headers=headers, method="POST") response = urllib.request.urlopen(request, timeout=10, context=utils.CTX) + trade_no = None if response.getcode() == 200: result = json.loads(response.read().decode("UTF8")) @@ -146,12 +157,10 @@ def order(url: str, params: dict, headers: dict, retry: int = 3) -> str: return trade_no - except Exception as e: - logger.error(e) + except: retry -= 1 - if retry > 0: - return order(url, params, headers, retry) + return order(url, params, headers, retry, jsonify) logger.error("[OrderError] URL: {}".format(utils.extract_domain(url))) @@ -172,22 +181,24 @@ def fetch(url: str, headers: dict, retry: int = 3) -> str: return None - except Exception as e: - logger.error(e) + except: retry -= 1 - if retry > 0: return fetch(url, headers, retry) logger.error("[FetchError] URL: {}".format(utils.extract_domain(url))) -def payment(url: str, params: dict, headers: dict, retry: int = 3) -> bool: +def payment(url: str, params: dict, headers: dict, retry: int = 3, jsonify: bool = False) -> bool: try: data = urllib.parse.urlencode(params).encode(encoding="UTF8") - request = urllib.request.Request(url, data=data, headers=headers, method="POST") + if jsonify: + headers["Content-Type"] = "application/json" + data = json.dumps(params).encode(encoding="UTF8") + request = urllib.request.Request(url, data=data, headers=headers, method="POST") response = urllib.request.urlopen(request, timeout=10, context=utils.CTX) + success = False if response.getcode() == 200: result = json.loads(response.read().decode("UTF8")) @@ -197,12 +208,10 @@ def payment(url: str, params: dict, headers: dict, retry: int = 3) -> bool: return success - except Exception as e: - logger.error(e) + except: retry -= 1 - if retry > 0: - return payment(url, params, headers, retry) + return payment(url, params, headers, retry, jsonify) logger.error("[PaymentError] URL: {}".format(utils.extract_domain(url))) return False @@ -215,21 +224,28 @@ def checkout( planid: int = -1, retry: int = 3, link: str = "", + api_prefix: str = "", + jsonify: bool = False, ) -> dict: if utils.isblank(domain) or utils.isblank(coupon): return {} - link = "/api/v1/user/coupon/check" if utils.isblank(link) else link + link = utils.get_subpath(api_prefix) + "user/coupon/check" if utils.isblank(link) else link try: url = f"{domain}{link}" params = {"code": coupon} if type(planid) == int and planid >= 0: params["plan_id"] = planid - payload = urllib.parse.urlencode(params).encode(encoding="UTF8") - request = urllib.request.Request(url, data=payload, headers=headers, method="POST") + if jsonify: + headers["Content-Type"] = "application/json" + payload = json.dumps(params).encode(encoding="UTF8") + else: + payload = urllib.parse.urlencode(params).encode(encoding="UTF8") + request = urllib.request.Request(url, data=payload, headers=headers, method="POST") response = urllib.request.urlopen(request, timeout=10, context=utils.CTX) + data = {} if response.getcode() == 200: result = json.loads(response.read().decode("UTF8")) @@ -238,10 +254,8 @@ def checkout( logger.info(response.read().decode("UTF8")) return data - except Exception as e: - logger.error(e) + except Exception: retry -= 1 - if retry > 0: return checkout( domain=domain, @@ -250,18 +264,22 @@ def checkout( planid=planid, retry=retry, link=link, + api_prefix=api_prefix, + jsonify=jsonify, ) logger.error("[CheckError] URL: {}".format(utils.extract_domain(url))) return {} -def get_payment_method(domain: str, cookies: str, authorization: str = "", retry: int = 3) -> list: +def get_payment_method( + domain: str, cookies: str, authorization: str = "", retry: int = 3, api_prefix: str = "" +) -> list: if not domain or (not cookies and not authorization): logger.error(f"query payment method error, cookies and authorization is empty, domain: {domain}") return [] - url = domain + "/api/v1/user/order/getPaymentMethod" + url = domain + utils.get_subpath(api_prefix) + "user/order/getPaymentMethod" headers = generate_headers(domain=domain, cookies=cookies, authorization=authorization) content = utils.http_get(url=url, headers=headers, retry=retry) @@ -276,12 +294,12 @@ def get_payment_method(domain: str, cookies: str, authorization: str = "", retry return [] -def unclosed_ticket(domain: str, headers: dict) -> tuple[int, int, str]: +def unclosed_ticket(domain: str, headers: dict, api_prefix: str = "") -> tuple[int, int, str]: if utils.isblank(domain) or not headers: logger.info(f"[TicketError] cannot fetch tickets because invalidate arguments, domain: {domain}") return -1, -1, "" - url = f"{domain}/api/v1/user/ticket/fetch" + url = domain + utils.get_subpath(api_prefix) + "user/ticket/fetch" content = utils.http_get(url=url, headers=headers) try: tickets = json.loads(content).get("data", []) @@ -300,17 +318,24 @@ def unclosed_ticket(domain: str, headers: dict) -> tuple[int, int, str]: return -1, -1, "" -def close_ticket(domain: str, tid: int, headers: dict, retry: int = 3) -> bool: +def close_ticket( + domain: str, tid: int, headers: dict, retry: int = 3, api_prefix: str = "", jsonify: bool = False +) -> bool: if utils.isblank(domain) or tid < 0 or not headers or retry <= 0: logger.info(f"[TicketError] cannot close ticket because invalidate arguments, domain: {domain}, tid: {tid}") - url = domain + "/api/v1/user/ticket/close" + url = domain + utils.get_subpath(api_prefix) + "user/ticket/close" params = {"id": tid} try: data = urllib.parse.urlencode(params).encode(encoding="UTF8") + if jsonify: + headers["Content-Type"] = "application/json" + data = json.dumps(params).encode(encoding="UTF8") + request = urllib.request.Request(url, data=data, headers=headers, method="POST") response = urllib.request.urlopen(request, timeout=10, context=utils.CTX) + if response.getcode() == 200: content = response.read().decode("UTF8") try: @@ -320,9 +345,15 @@ def close_ticket(domain: str, tid: int, headers: dict, retry: int = 3) -> bool: return False - except Exception as e: - logger.error(e) - return close_ticket(domain=domain, tid=tid, headers=headers, retry=retry - 1) + except: + return close_ticket( + domain=domain, + tid=tid, + headers=headers, + retry=retry - 1, + api_prefix=api_prefix, + jsonify=jsonify, + ) def submit_ticket( @@ -331,6 +362,8 @@ def submit_ticket( ticket: dict, authorization: str = "", retry: int = 3, + api_prefix: str = "", + jsonify: bool = False, ) -> bool: if retry <= 0: logger.error(f"[TicketError] achieved max retry when submit ticket, domain: {domain}") @@ -356,7 +389,7 @@ def submit_ticket( headers = generate_headers(domain=domain, cookies=cookies, authorization=authorization) # check last unclosed ticket - tid, timestamp, title = unclosed_ticket(domain=domain, headers=headers) + tid, timestamp, title = unclosed_ticket(domain=domain, headers=headers, api_prefix=api_prefix) if tid > 0 and timestamp > 0: # do not submit ticket if there are unclosed tickets in the last three days if time.time() - timestamp < 259200000: @@ -369,20 +402,26 @@ def submit_ticket( logger.info( f"[TicketInfo] found a unclosed ticket, domain: {domain}, tid: {tid}, subject: {title}, try close it now" ) - success = close_ticket(domain=domain, tid=tid, headers=headers) + success = close_ticket(domain=domain, tid=tid, headers=headers, api_prefix=api_prefix, jsonify=jsonify) if not success: logger.error( f"[TicketError] cannot submit a ticket because found an unclosed ticket but cannot close it, domain: {domain}, tid: {tid}, subject: {title}" ) return False - url = domain + "/api/v1/user/ticket/save" + url = domain + utils.get_subpath(api_prefix) + "user/ticket/save" params = {"subject": subject, "level": level, "message": message} try: - data = urllib.parse.urlencode(params).encode(encoding="UTF8") + if jsonify: + headers["Content-Type"] = "application/json" + data = json.dumps(params).encode(encoding="UTF8") + else: + data = urllib.parse.urlencode(params).encode(encoding="UTF8") + request = urllib.request.Request(url, data=data, headers=headers, method="POST") response = urllib.request.urlopen(request, timeout=10, context=utils.CTX) + if response.getcode() == 200: content = response.read().decode("UTF8") try: @@ -392,23 +431,33 @@ def submit_ticket( return False - except Exception as e: - logger.error(e) + except: return submit_ticket( domain=domain, cookies=cookies, ticket=ticket, authorization=authorization, retry=retry - 1, + api_prefix=api_prefix, + jsonify=jsonify, ) -def get_free_plan(domain: str, cookies: str, authorization: str = "", retry: int = 3, coupon: str = "") -> Plan: +def get_free_plan( + domain: str, + cookies: str, + authorization: str = "", + retry: int = 3, + coupon: str = "", + api_prefix: str = "", + jsonify: bool = False, +) -> Plan: if not domain or (not cookies and not authorization): logger.error(f"fetch free plans error, cookies and authorization is empty, domain: {domain}") return None - url = domain + "/api/v1/user/plan/fetch" + api_prefix = utils.get_subpath(api_prefix) + url = domain + api_prefix + "user/plan/fetch" headers = generate_headers(domain=domain, cookies=cookies, authorization=authorization) content = utils.http_get(url=url, headers=headers, retry=retry) if not content: @@ -416,7 +465,14 @@ def get_free_plan(domain: str, cookies: str, authorization: str = "", retry: int discount = None if not utils.isblank(coupon): - discount = checkout(domain=domain, coupon=coupon, headers=headers, retry=retry) + discount = checkout( + domain=domain, + coupon=coupon, + headers=headers, + retry=retry, + api_prefix=api_prefix, + jsonify=jsonify, + ) try: plans = [] @@ -491,12 +547,14 @@ def isfree(planid: str, package: str, price: float, discount: dict) -> bool: return couponvalue == 100 -def get_subscribe_info(domain: str, cookies: str, authorization: str = "", retry: int = 3) -> SubscribeInfo: +def get_subscribe_info( + domain: str, cookies: str, authorization: str = "", retry: int = 3, api_prefix: str = "" +) -> SubscribeInfo: if not domain or (not cookies and not authorization): logger.error(f"query subscribe information error, cookies and authorization is empty, domain: {domain}") return None - url = domain + "/api/v1/user/getSubscribe" + url = domain + utils.get_subpath(api_prefix) + "user/getSubscribe" headers = generate_headers(domain=domain, cookies=cookies, authorization=authorization) content = utils.http_get(url=url, headers=headers, retry=retry) @@ -520,19 +578,20 @@ def get_subscribe_info(domain: str, cookies: str, authorization: str = "", retry reset_day = 365 if (reset_day is None or reset_day < 0) else reset_day used = data.get("d", 0) trafficflow = data.get("transfer_enable", 1) - used_rate = round(used / trafficflow, 2) + used_rate = 0.00 if not trafficflow else round(used / trafficflow, 2) plan = data.get("plan", {}) - renew_enable = plan.get("renew", 0) == 1 - reset_price = plan.get("reset_price", 1) + renew_enable = plan.get("renew", 0) == 1 if plan else False + reset_price = plan.get("reset_price", 1) if plan else 1 reset_enable = False if reset_price is None else reset_price <= 0 package = "" - for p in PACKAGES: - price = plan.get(p, None) - if price is not None and price <= 0: - package = p - break + if isinstance(plan, dict): + for p in PACKAGES: + price = plan.get(p, None) + if price is not None and price <= 0: + package = p + break return SubscribeInfo( plan_id=plan_id, @@ -554,7 +613,6 @@ def flow( params: dict, reset: bool = False, retry: int = 3, - headers: dict = HEADER, cookies: str = "", authorization: str = "", ) -> bool: @@ -563,12 +621,16 @@ def flow( if not re.search(regex, domain): return False - fetch_url = domain + params.get("fetch", "/api/v1/user/order/fetch") - order_url = domain + params.get("order", "/api/v1/user/order/save") - payment_url = domain + params.get("payment", "/api/v1/user/order/checkout") + api_prefix = utils.get_subpath(params.get("api_prefix", "")) + jsonify = params.get("jsonify", False) + + fetch_url = domain + params.get("fetch", f"{api_prefix}user/order/fetch") + order_url = domain + params.get("order", f"{api_prefix}user/order/save") + payment_url = domain + params.get("payment", f"{api_prefix}user/order/checkout") method = params.get("method", 1) coupon = params.get("coupon_code", "") + headers = deepcopy(HEADER) headers["origin"] = domain headers["referer"] = domain + "/" @@ -578,22 +640,21 @@ def flow( "password": params.get("passwd", ""), } - login_url = domain + params.get("login", "/api/v1/passport/auth/login") - text, authorization = login(login_url, user_info, headers, retry) + login_url = domain + params.get("login", f"{api_prefix}passport/auth/login") + text, authorization = login(login_url, user_info, headers, retry, jsonify) cookies = utils.extract_cookie(text) if len(cookies) <= 0 and not authorization: return False headers = generate_headers(domain=domain, cookies=cookies, authorization=authorization, headers=headers) - trade_no = fetch(fetch_url, headers, retry) if trade_no: payload = {"trade_no": trade_no, "method": method} if coupon: payload["coupon_code"] = coupon - if not payment(payment_url, payload, headers, retry): + if not payment(payment_url, payload, headers, retry, jsonify): return False if reset: package = "reset_price" @@ -610,7 +671,7 @@ def flow( } if coupon: - link = params.get("check", "/api/v1/user/coupon/check") + link = params.get("check", f"{api_prefix}user/coupon/check") result = checkout( domain=domain, coupon=coupon, @@ -618,6 +679,7 @@ def flow( planid=plan_id, retry=retry, link=link, + jsonify=jsonify, ) if not result: logger.info("failed to renewal because coupon is valid") @@ -625,17 +687,17 @@ def flow( payload["coupon_code"] = coupon - trade_no = order(order_url, payload, headers, retry) + trade_no = order(order_url, payload, headers, retry, jsonify) if not trade_no: logger.info("renewal error because cannot order") return False payload = {"trade_no": trade_no, "method": method} - success = payment(payment_url, payload, headers, retry) + success = payment(payment_url, payload, headers, retry, jsonify) return success -def add_traffic_flow(domain: str, params: dict) -> str: +def add_traffic_flow(domain: str, params: dict, jsonify: bool = False) -> str: if not domain or not params: logger.error(f"[RenewalError] invalidate arguments") return "" @@ -646,8 +708,20 @@ def add_traffic_flow(domain: str, params: dict) -> str: logger.info(f"[RenewalError] email or password cannot be empty, domain: {domain}") return "" - cookies, authorization = get_cookies(domain=domain, username=email, password=password) - subscribe = get_subscribe_info(domain=domain, cookies=cookies, authorization=authorization) + api_prefix = params.get("api_prefix", "") + cookies, authorization = get_cookies( + domain=domain, + username=email, + password=password, + api_prefix=api_prefix, + jsonify=jsonify, + ) + subscribe = get_subscribe_info( + domain=domain, + cookies=cookies, + authorization=authorization, + api_prefix=api_prefix, + ) if not subscribe: logger.info(f"[RenewalError] cannot fetch subscribe information") return "" @@ -657,7 +731,12 @@ def add_traffic_flow(domain: str, params: dict) -> str: coupon_code = params.get("coupon_code", "") method = params.get("method", -1) if method <= 0: - methods = get_payment_method(domain=domain, cookies=cookies, authorization=authorization) + methods = get_payment_method( + domain=domain, + cookies=cookies, + authorization=authorization, + api_prefix=api_prefix, + ) if not methods: method = 1 else: @@ -670,6 +749,8 @@ def add_traffic_flow(domain: str, params: dict) -> str: "plan_id": plan_id, "method": method, "coupon_code": coupon_code, + "api_prefix": api_prefix, + "jsonify": jsonify, } renew = params.get("enable", True) @@ -723,6 +804,8 @@ def add_traffic_flow(domain: str, params: dict) -> str: cookies=cookies, ticket=ticket, authorization=authorization, + api_prefix=api_prefix, + jsonify=jsonify, ) logger.info(f"ticket submit {'successed' if success else 'failed'}, domain: {domain}") diff --git a/subscribe/utils.py b/subscribe/utils.py index 83bb14cda..9d2a58c4c 100644 --- a/subscribe/utils.py +++ b/subscribe/utils.py @@ -472,6 +472,16 @@ def get_emoji(text: str, patterns: dict, default: str = "") -> str: return default +def get_subpath(api_prefix: str, default: str = "/api/v1/") -> str: + path = trim(api_prefix) or trim(default) or "/api/v1/" + if not path.startswith("/"): + path = "/" + path + if not path.endswith("=") and not path.endswith("/"): + path += "/" + + return path + + def multi_process_run(func: typing.Callable, tasks: list) -> list: if not func or not isinstance(func, typing.Callable): logger.error(f"skip execute due to func is not callable") diff --git a/subscribe/workflow.py b/subscribe/workflow.py index 24a92db68..393874738 100644 --- a/subscribe/workflow.py +++ b/subscribe/workflow.py @@ -10,7 +10,7 @@ import renewal import utils -from airport import AirPort +from airport import ANOTHER_API_PREFIX, AirPort from logger import logger from origin import Origin from push import PushTo @@ -82,6 +82,9 @@ class TaskConfig: # 邀请码 invite_code: str = "" + # 接口地址前缀,如 /api/v1/ 或 /api?scheme= + api_prefix: str = "/api/v1/" + def execute(task_conf: TaskConfig) -> list: if not task_conf or not isinstance(task_conf, TaskConfig): @@ -96,13 +99,18 @@ def execute(task_conf: TaskConfig) -> list: include=task_conf.include, liveness=task_conf.liveness, coupon=task_conf.coupon, + api_prefix=task_conf.api_prefix, ) logger.info(f"start fetch proxy: name=[{task_conf.name}]\tid=[{task_conf.index}]\tdomain=[{obj.ref}]") # 套餐续期 if task_conf.renew: - sub_url = renewal.add_traffic_flow(domain=obj.ref, params=task_conf.renew) + sub_url = renewal.add_traffic_flow( + domain=obj.ref, + params=task_conf.renew, + jsonify=obj.api_prefix == ANOTHER_API_PREFIX, + ) if sub_url and not obj.registed: obj.registed = True obj.sub = sub_url