clash是怎么通过订阅链接获取套餐总流量数据(已使用流量)的?
到处翻了一下好像没什么文档讲这个(应该是开发者会自己翻别人代码吧?..索性没人做了), 有兴趣收集折腾了一下. 供其他好奇宝宝参考
, 和开发?
P.S.主要这玩意不像url订阅分享链接一样, 没有一个有公信力人/团体提出统一规范. 只能纯参考着玩了
python代码
import http.server
import socketserver
import time
import os
from urllib.parse import quote
# 尝试导入qr库生成二维码在终端
# pip install qrcode Pillow
isPrintQRCode=False
try:
import qrcode
isPrintQRCode=True
except:
isPrintQRCode=False
"""
极简 机场/代理 订阅后端逻辑演示
P.S. 部分参数来源于互联网, 未经考量仅供参考
"""
# 监听地址
PORT = 8080
HOST = "127.0.0.1"
# --- 配置订阅信息 ---
# 使用量和总流量 (字节)
UPLOAD_BYTES = 10 * 1024 * 1024 * 1024 # 10 GB
DOWNLOAD_BYTES = 50 * 1024 * 1024 * 1024 # 50 GB
TOTAL_BYTES = 100 * 1024 * 1024 * 1024 # 100 GB
# 到期时间 (Unix时间戳, 设为当前时间+30天)
EXPIRE_TIMESTAMP = int(time.time() + 30 * 24 * 3600)
# (剩余)重置日
RESET_TIME = 14
# 套餐名
PLAN_NAME = "VIP1"
# 配置文件网站主页
HOME_URL = "http://127.0.0.1"
# 配置文件自动更新间隔 (小时)
UPDATE_INTERVAL_HOURS = 12
# 订阅配置文件名
PROFILE_NAME = "订阅后端演示"
# 订阅配置文件路径(允许相对路径)
PROFILE_PATH = "sub.yaml"
# 构造 Subscription-Userinfo 头部的值
USERINFO_HEADER_VALUE = (
f"upload={UPLOAD_BYTES};" # 上传使用量(字节)
f"download={DOWNLOAD_BYTES};" # 下载使用量(字节)
f"total={TOTAL_BYTES};" # 总流量(字节)
f"expire={EXPIRE_TIMESTAMP};" # 到期时间(Unix时间戳)
f"reset_day={RESET_TIME};" # 非通用,(剩余)重置日
f"auto_reset_day={RESET_TIME};" # 非通用,(剩余)重置日
f"plan_name={quote(PLAN_NAME)};" # 非通用,套餐名(必须url参数编码)
f"hover={quote(PLAN_NAME)};" # 非通用,套餐名(必须url参数编码)
f"app_url={quote(HOME_URL)};" # 非通用,配置文件网站主页(必须url参数编码)
)
# ---------------------
class SubscriptionHandler(http.server.BaseHTTPRequestHandler):
"""
自定义 HTTP 请求处理器,用于模拟返回订阅信息
"""
def do_GET(self):
"""处理所有 GET 请求"""
print("") # 只是为了空一行
print("=" * 40)
# 发送 HTTP 响应状态码 (200 OK)
self.send_response(200)
# 设置 HTTP 响应头
self.send_header("Content-type", "text/plain; charset=utf-8")
# *** 关键:返回高级订阅信息响应头 ***
self.send_header("Content-Disposition", f'attachment;filename*=UTF-8\'\'{quote(PROFILE_NAME)}') # 订阅配置文件名(必须url参数编码)
self.send_header("Subscription-Userinfo", USERINFO_HEADER_VALUE) # 订阅信息集
self.send_header("Profile-Update-Interval", str(UPDATE_INTERVAL_HOURS)) # 配置文件自动更新间隔 (小时)
self.send_header("Profile-Web-Page-Url", str(HOME_URL)) # 非通用,配置文件网站主页
# 结束响应头
self.end_headers()
# 发送响应体 (放订阅配置的内容)
# 在响应体前面添加 str("#<title>{PROFILE_NAME}</title>") 以兼容clashmi客户端的只从html标题获取配置文件名的方式
self.wfile.write(f"#<title>{PROFILE_NAME}</title>\n".encode('utf-8'))
# 读取文件并推送二进制流
if os.path.isfile(PROFILE_PATH):
with open(PROFILE_PATH, "rb") as f:
self.wfile.write(f.read())
else:
self.wfile.write(b"proxies:\n# profile not found")
print(f"=== 请求处理完成 ===")
if __name__ == "__main__":
# 打印配置信息,方便对照
sub_url = f"http://{HOST}:{PORT}"
print(f"🚀 订阅演示服务器启动于 {sub_url}")
print("演示内容:")
print(f"* 监听地址: {HOST}:{PORT}")
print(f"* 订阅配置文件路径: {PROFILE_PATH}")
print(f"* 订阅配置文件名称: {PROFILE_NAME}")
print(f"* 订阅套餐名称: {PLAN_NAME}")
print(f"* 订阅(剩余)重置日: {RESET_TIME} 日")
print(f"* 订阅到期时间: {time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(EXPIRE_TIMESTAMP))}")
print(f"* 订阅使用流量/总流量: 上传 {UPLOAD_BYTES / (1024**3)} GB / 下载 {DOWNLOAD_BYTES / (1024**3)} GB / {TOTAL_BYTES / (1024**3)} GB")
print(f"* 订阅文件主页链接: {HOME_URL}")
print("-" * 40)
print(f"订阅后端部分响应头信息")
print(f"Content-Disposition:\n{f'attachment;filename*=UTF-8\'\'{quote(PROFILE_NAME)}'}")
print(f"Subscription-Userinfo:\n{USERINFO_HEADER_VALUE}")
print("-" * 40)
print("使用URL Schemes方式导入到客户端:")
print(f"clash://install-config?url={quote(sub_url)}&name={quote(PROFILE_NAME)}")
print("第三方url在线生成二维码导入:")
print(f"https://api.qrserver.com/v1/create-qr-code/?size=400x400&data={quote(sub_url)}")
# 如果qr库可用
if isPrintQRCode:
print("扫码在线导入订阅:")
qr = qrcode.QRCode(
version=None, # 大小自动
error_correction=qrcode.constants.ERROR_CORRECT_L,
# 1 像素一格,终端里刚好
box_size=1,
border=1,
)
qr.add_data(sub_url)
qr.make(fit=True)
# # 终端用红绿 Unicode 字符打印
# def term_reset(): print('', end='')
# def blk(): print('🟩', end='') # 黑块
# def wht(): print('🟥', end='') # 白块
# for row in qr.modules:
# for col in row:
# (blk if col else wht)()
# term_reset()
# print() # 换行
# 终端用另一种黑白 Unicode 字符打印
print("由于技术原因, 二维码可能需要手动鼠标选中使其发生颜色反转方可扫描. 并且在部分终端上可能发生乱码")
print("~" * 40)
modules = qr.modules
for r in range(0, len(modules), 2):
upper = modules[r]
lower = modules[r+1] if r+1 < len(modules) else [False]*len(upper)
line = ''
for u, l in zip(upper, lower):
if u and l: line += '█' # 上下都黑
elif u and not l: line += '▀' # 上黑下白
elif not u and l: line += '▄' # 上白下黑
else: line += ' ' # 上下都白
print(line)
print("=" * 40)
# 创建 TCP 服务器
with socketserver.TCPServer((HOST, PORT), SubscriptionHandler) as httpd:
try:
# 启动服务器并保持运行
httpd.serve_forever()
except KeyboardInterrupt:
# 捕获Ctrl+C
httpd.shutdown()
print("\n👋 服务器已关闭")
# 等待用户输入
print("\n操作完成.按回车(Enter)退出程序")
input()
没有什么技术力hhh, 悄摸摸扔水区就算了(nl好像没水区hhh,哪里都是水区)
参考了以下链接:
另附使用URL Schemes方式导入到客户端的格式
clash://install-config?url=<encodeURIComponent编码后的订阅url>&name=<encodeURIComponent编码后的配置文件名称>