python脚本演示`机场/代理 订阅后端`是怎么返回流量等高级信息给clash的?

clash是怎么通过订阅链接获取套餐总流量数据(已使用流量)的?

到处翻了一下好像没什么文档讲这个(应该是开发者会自己翻别人代码吧?..索性没人做了), 有兴趣收集折腾了一下. 供其他好奇宝宝参考 :smiling_face_with_three_hearts:, 和开发?

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编码后的配置文件名称>
3 个赞

学习了,感谢分享。

不知道订阅转换能干嘛?没成功过

感谢分享

感谢大佬分享分享科普……

没什么, 因为不同人用不同的代理客户端.
于是就出现了订阅转换, 来方便的兼容各种代理客户端使用公开协议连接代理服务器实现代理

但是他不万能啊, 各种客户端按自己的想法做客户端. 很多都互相不兼容…
导致他不一定有用, 毕竟这个属于社区自己搞的, 有些客户端作者想改就改, 根本不考虑兼容你

nl权限怎么又bug啦!!! 我明明加了青铜权限要求啊, 怎么没了