mihomo.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. from datetime import datetime
  2. import httpx
  3. from pathlib import Path
  4. import os
  5. from contextlib import closing
  6. from socket import socket, AF_INET, SOCK_STREAM
  7. import asyncio
  8. from contextlib import closing
  9. from socket import AF_INET, SOCK_STREAM
  10. import httpx
  11. import yaml
  12. from pathlib import Path
  13. from typing import Optional, Dict, Any
  14. from fastapi import HTTPException
  15. def get_sub_file_info(file_path: str):
  16. with open(file_path, "r",encoding='utf-8') as f:
  17. sub_yaml = yaml.safe_load(f)
  18. groups = sub_yaml.get("proxy-groups", [])
  19. if not groups:
  20. raise ValueError("subscription file is not valid")
  21. name = groups[0].get("name", "")
  22. if not name:
  23. raise ValueError("subscription file is not valid")
  24. proxies = sub_yaml.get("proxies", [])
  25. if not proxies:
  26. raise ValueError("subscription file is not valid")
  27. fileter_proxies = []
  28. fileter_proxies = []
  29. keywords = ['流量', '套餐', '剩余', '测试']
  30. for proxy in proxies:
  31. if not any(keyword in proxy.get("name", "") for keyword in keywords):
  32. fileter_proxies.append(proxy)
  33. return name, groups,proxies
  34. async def async_proxy_delay(provider_name: str, external_controller: str) -> dict:
  35. """异步获取代理延迟
  36. 失败: {'message': 'get delay: all proxies timeout'}
  37. 成功: {'自动选择': 34, '🇦🇺澳大利亚悉尼': 159, '🇦🇺澳大利亚悉尼2': 1577,...}
  38. """
  39. url = f"http://{external_controller}/group/{provider_name}/delay?url=https%3A%2F%2Fwww.gstatic.com%2Fgenerate_204&timeout=2000"
  40. async with httpx.AsyncClient() as client:
  41. try:
  42. response = await client.get(url, timeout=10)
  43. response.raise_for_status()
  44. return response.json()
  45. except Exception as e:
  46. return {"error": str(e)}
  47. def get_provider_name(sub_file: Path|str) -> str:
  48. with open(sub_file, "r",encoding='utf-8') as f:
  49. sub_yaml = yaml.safe_load(f)
  50. groups = sub_yaml.get("proxy-groups", [])
  51. if not groups:
  52. return
  53. name = groups[0].get("name", "")
  54. if not name:
  55. return
  56. return name
  57. def get_external_controller(config_path: Path|str) -> Optional[str]:
  58. with open(config_path, "r", encoding="utf-8") as f:
  59. config = yaml.safe_load(f)
  60. if "external-controller" not in config:
  61. return None
  62. return config["external-controller"]
  63. def save_yaml_dump(config: dict, save_as: Path) -> Path:
  64. """保存配置文件"""
  65. save_as.parent.mkdir(parents=True, exist_ok=True)
  66. with open(save_as, 'w', encoding='utf-8') as f:
  67. yaml.dump(
  68. config,
  69. f,
  70. Dumper=yaml.SafeDumper,
  71. allow_unicode=True,
  72. indent=2,
  73. sort_keys=False
  74. )
  75. return save_as
  76. async def async_get_sub(sub_url: str, save_path: Path, timeout: int = 10) -> Path:
  77. """获取订阅文件"""
  78. headers = {'User-Agent': 'clash-verge/v1.7.5'}
  79. try:
  80. async with httpx.AsyncClient() as client:
  81. resp = await client.get(sub_url, headers=headers, follow_redirects=True, timeout=timeout)
  82. resp.raise_for_status()
  83. except httpx.HTTPError as e:
  84. raise HTTPException(status_code=500, detail=f"订阅获取失败: {str(e)}")
  85. save_path = Path(save_path)
  86. save_path.parent.mkdir(parents=True, exist_ok=True)
  87. with open(save_path, 'w', encoding='utf-8') as f:
  88. f.write(resp.text)
  89. return save_path
  90. async def get_sub(sub_url: str, save_path: Path, timeout: int = 10) -> Path:
  91. """获取订阅文件(异步版本)"""
  92. headers = {'User-Agent': 'clash-verge/v1.7.5'}
  93. try:
  94. async with httpx.AsyncClient() as client:
  95. resp = await client.get(sub_url, headers=headers, follow_redirects=True, timeout=timeout)
  96. resp.raise_for_status()
  97. except httpx.HTTPError as e:
  98. raise HTTPException(status_code=500, detail=f"订阅获取失败: {str(e)}")
  99. save_path = Path(save_path)
  100. save_path.parent.mkdir(parents=True, exist_ok=True)
  101. with open(save_path, 'w', encoding='utf-8') as f:
  102. f.write(resp.text)
  103. return save_path
  104. def update_config(
  105. read_path: Path,
  106. config_update: dict,
  107. save_as: Optional[Path] = None,
  108. ) -> Path:
  109. """更新配置文件"""
  110. config: Dict[str, Any] = {}
  111. if read_path.exists():
  112. with open(read_path, 'r', encoding='utf-8') as f:
  113. config = yaml.safe_load(f) or {}
  114. config.update(config_update)
  115. save_as = save_as or read_path
  116. save_yaml_dump(config, save_as)
  117. async def find_free_port(scope=(9350, 18000)):
  118. """
  119. 异步查找一个可用端口
  120. :param scope: 指定端口范围,为None时使用默认范围(9600-19600)
  121. :return: 可以使用的端口号
  122. """
  123. for port in range(*scope):
  124. try:
  125. # 创建异步套接字
  126. reader, writer = await asyncio.open_connection('127.0.0.1', port)
  127. writer.close()
  128. await writer.wait_closed()
  129. except (ConnectionRefusedError, OSError):
  130. # 端口未被占用,返回该端口
  131. return port
  132. raise OSError('未找到可用端口。')
  133. async def port_is_using(port, ip='127.0.0.1', timeout=1.0):
  134. """异步检查是否有服务在监听指定端口(添加超时控制)"""
  135. try:
  136. # 设置连接超时时间
  137. reader, writer = await asyncio.wait_for(
  138. asyncio.open_connection(ip, port),
  139. timeout=timeout
  140. )
  141. writer.close()
  142. await writer.wait_closed()
  143. return True
  144. except (ConnectionRefusedError, OSError, asyncio.TimeoutError):
  145. # TimeoutError 表示超时,视为无服务监听
  146. return False
  147. async def download_mihomo(download_path: Path, download_url: str):
  148. """下载mihomo可执行文件"""
  149. if download_path.exists():
  150. return
  151. try:
  152. resp = httpx.get(download_url, timeout=30)
  153. resp.raise_for_status()
  154. download_path.parent.mkdir(parents=True, exist_ok=True)
  155. download_path.write_bytes(resp.content)
  156. download_path.chmod(0o755)
  157. except Exception as e:
  158. raise RuntimeError(f"下载mihomo失败: {str(e)}")