megatrump commited on
Commit
6eaf348
·
1 Parent(s): 1d668b6
Files changed (5) hide show
  1. .gitignore +9 -0
  2. Dockerfile +2 -4
  3. app.py +88 -46
  4. requirements.txt +5 -4
  5. start.sh +7 -3
.gitignore CHANGED
@@ -1 +1,10 @@
 
 
 
 
 
 
 
 
1
  test.sh
 
 
1
+
2
+ .env
3
+ .venv
4
+ __pycache__
5
+ *.log
6
+
7
+ tools/
8
+
9
  test.sh
10
+ test.py
Dockerfile CHANGED
@@ -1,7 +1,7 @@
1
  ARG API_KEY
2
  ARG SUBSCRIBE_URLS
3
 
4
- FROM ghcr.io/astral-sh/uv:python3.12-bookworm
5
 
6
  RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
7
 
@@ -12,11 +12,9 @@ ENV PATH="/home/user/.local/bin:$PATH"
12
  WORKDIR /app
13
 
14
  COPY --chown=user ./requirements.txt requirements.txt
15
- RUN uv venv -p 3.12
16
- RUN uv pip install --no-cache-dir --upgrade -r requirements.txt
17
 
18
  COPY --chown=user . /app
19
  ENTRYPOINT ["/usr/bin/dumb-init", "--", "/bin/bash", "start.sh"]
20
 
21
  # CMD ["uv", "run", "uvicorn", "app:asgi_app", "--host", "0.0.0.0", "--port", "7860"]
22
-
 
1
  ARG API_KEY
2
  ARG SUBSCRIBE_URLS
3
 
4
+ FROM python:3.12
5
 
6
  RUN apt-get update && apt-get install -y --no-install-recommends dumb-init
7
 
 
12
  WORKDIR /app
13
 
14
  COPY --chown=user ./requirements.txt requirements.txt
15
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
 
16
 
17
  COPY --chown=user . /app
18
  ENTRYPOINT ["/usr/bin/dumb-init", "--", "/bin/bash", "start.sh"]
19
 
20
  # CMD ["uv", "run", "uvicorn", "app:asgi_app", "--host", "0.0.0.0", "--port", "7860"]
 
app.py CHANGED
@@ -1,15 +1,28 @@
1
  import os
2
  import re
3
- import sys
4
  import yaml
5
- import requests
6
- from flask import Flask, Response, request
7
- from urllib.parse import urlencode
8
- from asgiref.wsgi import WsgiToAsgi
9
-
10
- app = Flask(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  # 从环境变量中获取密钥
 
13
  API_KEY = os.environ.get('API_KEY')
14
  SUBSCRIBE_URLS = os.environ.get('SUBSCRIBE_URLS') # 存储真实URL的变量
15
 
@@ -21,15 +34,15 @@ def subscribe_mixin(content: str) -> str:
21
 
22
  my_auto_group_name = "AI Unrestrict"
23
  regex_list = [
24
- re.compile(r"美国", re.IGNORECASE), # 美国 (不区分大小写)
25
- re.compile(r"america", re.IGNORECASE), # america (不区分大小写)
26
- re.compile(r"us", re.IGNORECASE), # us (不区分大小写)
27
- re.compile(r"新加坡", re.IGNORECASE), # 新加坡 (不区分大小写)
28
- re.compile(r"singapore", re.IGNORECASE), # singapore (不区分大小写)
29
- re.compile(r"sg", re.IGNORECASE), # sg (不区分大小写)
30
- re.compile(r"加拿大", re.IGNORECASE), # 加拿大 (不区分大小写)
31
- re.compile(r"canada", re.IGNORECASE), # canada (不区分大小写)
32
- re.compile(r"ca", re.IGNORECASE), # ca (不区分大小写)
33
  ]
34
  matching_proxies = [] # 用于存储匹配的代理名称
35
 
@@ -38,9 +51,9 @@ def subscribe_mixin(content: str) -> str:
38
  for proxy in d["proxies"]:
39
  if "name" in proxy:
40
  for regex in regex_list:
41
- if regex.search(proxy["name"]): # 使用 re.search
42
  matching_proxies.append(proxy["name"])
43
- break # 匹配到一个 regex 就跳出循环,避免重复添加
44
 
45
  # 2. 创建新的 proxy-group 对象
46
  new_proxy_group = {
@@ -58,56 +71,85 @@ def subscribe_mixin(content: str) -> str:
58
  # 4. 将 myAutoGroupName 添加到第一个 proxy-group 的 "proxies" 列表的最前面
59
  if d["proxy-groups"] and len(d["proxy-groups"]) > 0 and \
60
  "proxies" in d["proxy-groups"][0] and isinstance(d["proxy-groups"][0]["proxies"], list):
61
- d["proxy-groups"][0]["proxies"].insert(0, my_auto_group_name) # 使用 insert(0, ...)
62
  else:
63
- d["proxy-groups"] = [new_proxy_group] # 如果 proxy-groups 不存在,则创建
64
 
65
- # 将修改后的字典转换回 YAML 字符串
66
- modified_yaml = yaml.dump(d, allow_unicode=True, indent=2) # 使用 yaml.dump
67
 
68
  return modified_yaml
69
 
70
  except yaml.YAMLError as e:
71
  print(f"YAML 解析错误:{e}")
72
- return "" # 或者抛出异常,根据你的需求
73
  except Exception as e:
74
  print(f"其他错误:{e}")
75
  return ""
76
 
77
  @app.get("/getsub")
78
- def read_subscribe():
79
  # 验证API Key
80
- key = request.args.get('key')
81
  if key != API_KEY:
82
- return {"error": "Unauthorized"}, 401
83
 
84
  # 从环境变量获取URL列表
85
  if not SUBSCRIBE_URLS:
86
- return {"error": "SUBSCRIBE_URLS not configured"}, 500
87
 
88
  urls = SUBSCRIBE_URLS.split('\n')
89
- cleaned_urls = [url.strip() for url in urls]
90
- new_url = '|'.join(cleaned_urls)
91
- encoded_url = urlencode({
 
 
 
 
92
  'target': 'clash',
93
- 'url': new_url
94
- }) # Correct way to encode the URL
95
-
96
- target_url = f"http://127.0.0.1:25500/sub?{encoded_url}"
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  try:
99
- resp = requests.get(target_url)
100
- resp.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
101
- data = resp.text
102
- data = subscribe_mixin(data)
103
- return Response(data, mimetype='text/yaml')
104
- except requests.exceptions.RequestException as e:
105
- return {"error": str(e)}, 500 # Handle request errors and return an error response
106
-
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  @app.get("/")
109
- def read_root():
110
  return {"hello": 'world'}
111
-
112
-
113
- asgi_app = WsgiToAsgi(app)
 
1
  import os
2
  import re
 
3
  import yaml
4
+ import httpx
5
+ import base64
6
+ from dotenv import load_dotenv
7
+ from fastapi import FastAPI, Response, HTTPException, Request
8
+
9
+ HEADERS = {
10
+ 'accept': 'application/json, text/plain, */*',
11
+ 'accept-language': 'zh-CN',
12
+ 'pragma': 'no-cache',
13
+ 'sec-ch-ua': '"Not?A_Brand";v="8", "Chromium";v="108"',
14
+ 'sec-ch-ua-mobile': '?0',
15
+ 'sec-ch-ua-platform': '"Windows"',
16
+ 'sec-fetch-dest': 'empty',
17
+ 'sec-fetch-mode': 'cors',
18
+ 'sec-fetch-site': 'cross-site',
19
+ 'user-agent': 'ClashforWindows/0.20.39',
20
+ }
21
+
22
+ app = FastAPI()
23
 
24
  # 从环境变量中获取密钥
25
+ load_dotenv()
26
  API_KEY = os.environ.get('API_KEY')
27
  SUBSCRIBE_URLS = os.environ.get('SUBSCRIBE_URLS') # 存储真实URL的变量
28
 
 
34
 
35
  my_auto_group_name = "AI Unrestrict"
36
  regex_list = [
37
+ re.compile(r"美国", re.IGNORECASE),
38
+ re.compile(r"america", re.IGNORECASE),
39
+ re.compile(r"us", re.IGNORECASE),
40
+ re.compile(r"新加坡", re.IGNORECASE),
41
+ re.compile(r"singapore", re.IGNORECASE),
42
+ re.compile(r"sg", re.IGNORECASE),
43
+ re.compile(r"加拿大", re.IGNORECASE),
44
+ re.compile(r"canada", re.IGNORECASE),
45
+ re.compile(r"ca", re.IGNORECASE),
46
  ]
47
  matching_proxies = [] # 用于存储匹配的代理名称
48
 
 
51
  for proxy in d["proxies"]:
52
  if "name" in proxy:
53
  for regex in regex_list:
54
+ if regex.search(proxy["name"]):
55
  matching_proxies.append(proxy["name"])
56
+ break
57
 
58
  # 2. 创建新的 proxy-group 对象
59
  new_proxy_group = {
 
71
  # 4. 将 myAutoGroupName 添加到第一个 proxy-group 的 "proxies" 列表的最前面
72
  if d["proxy-groups"] and len(d["proxy-groups"]) > 0 and \
73
  "proxies" in d["proxy-groups"][0] and isinstance(d["proxy-groups"][0]["proxies"], list):
74
+ d["proxy-groups"][0]["proxies"].insert(0, my_auto_group_name)
75
  else:
76
+ d["proxy-groups"] = [new_proxy_group]
77
 
78
+ d.pop('socks-port', None)
79
+ modified_yaml = yaml.dump(d, allow_unicode=True, indent=2)
80
 
81
  return modified_yaml
82
 
83
  except yaml.YAMLError as e:
84
  print(f"YAML 解析错误:{e}")
85
+ return ""
86
  except Exception as e:
87
  print(f"其他错误:{e}")
88
  return ""
89
 
90
  @app.get("/getsub")
91
+ async def read_subscribe(request: Request, key: str):
92
  # 验证API Key
 
93
  if key != API_KEY:
94
+ raise HTTPException(status_code=401, detail="Unauthorized")
95
 
96
  # 从环境变量获取URL列表
97
  if not SUBSCRIBE_URLS:
98
+ raise HTTPException(status_code=500, detail="SUBSCRIBE_URLS not configured")
99
 
100
  urls = SUBSCRIBE_URLS.split('\n')
101
+ proxy_urls = [
102
+ f'{request.base_url}proxy?encoded_url={base64.b64encode(url.encode()).decode()}'
103
+ for url in urls
104
+ if url.strip()
105
+ ]
106
+ merged_urls: str = '|'.join(proxy_urls)
107
+ args = {
108
  'target': 'clash',
109
+ 'url': merged_urls
110
+ }
 
 
111
 
112
+ async with httpx.AsyncClient() as client:
113
+ try:
114
+ resp = await client.get('http://127.0.0.1:25500/sub', params=args, headers=HEADERS)
115
+ resp.raise_for_status()
116
+ data = resp.text
117
+ data = subscribe_mixin(data)
118
+ return Response(content=data, media_type='text/yaml')
119
+ except httpx.RequestError as e:
120
+ raise HTTPException(status_code=500, detail=str(e))
121
+ except Exception as e:
122
+ raise HTTPException(status_code=500, detail=str(e))
123
+
124
+ @app.get("/proxy")
125
+ async def proxy_url(encoded_url: str):
126
+ '''
127
+ 有一些订阅地址, 需要检测请求头.
128
+ 然而 tindy2013/subconverter 项目并不支持请求头配置.
129
+ 所以将请求重定向到本服务中, 然后通过自定义请求头绕过.
130
+ '''
131
  try:
132
+ try:
133
+ decoded_bytes = base64.b64decode(encoded_url)
134
+ target_url = decoded_bytes.decode('utf-8')
135
+ except Exception as e:
136
+ raise HTTPException(status_code=400, detail="Invalid base64 encoding")
137
+
138
+ async with httpx.AsyncClient() as client:
139
+ response = await client.get(target_url, headers=HEADERS)
140
+
141
+ return Response(
142
+ content=response.content,
143
+ status_code=response.status_code,
144
+ headers=dict(response.headers),
145
+ media_type=response.headers.get("content-type")
146
+ )
147
+
148
+ except httpx.RequestError as e:
149
+ raise HTTPException(status_code=500, detail=f"Request failed: {str(e)}")
150
+ except Exception as e:
151
+ raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
152
 
153
  @app.get("/")
154
+ async def read_root():
155
  return {"hello": 'world'}
 
 
 
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
 
2
- flask
 
 
3
  pyyaml
4
- asgiref
5
- requests
6
- uvicorn[standard]==0.17.*
 
1
 
2
+ httpx
3
+ pydantic
4
+ python-dotenv
5
  pyyaml
6
+ fastapi
7
+ uvicorn[standard]
 
start.sh CHANGED
@@ -1,11 +1,15 @@
1
  #!/bin/bash
2
 
 
 
 
3
  # 后台运行 tail 进程
4
- cd /app/tools && tar -xf subconverter_linux64.tar.gz
5
- cd /app/tools/subconverter && ./subconverter >> /app/subconverter.log 2>&1 &
6
 
7
  # 运行 uvicorn 应用
8
- cd /app && uv run uvicorn app:asgi_app --host 0.0.0.0 --port 7860
 
9
 
10
  # (可选) 等待 tail 进程结束,但它通常不会结束
11
  # wait $!
 
1
  #!/bin/bash
2
 
3
+ # 获取当前脚本所在的绝对路径
4
+ SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
5
+
6
  # 后台运行 tail 进程
7
+ cd "${SCRIPT_DIR}/tools" && tar -xf subconverter_linux64.tar.gz
8
+ cd "${SCRIPT_DIR}/tools/subconverter" && ./subconverter >> "${SCRIPT_DIR}/subconverter.log" 2>&1 &
9
 
10
  # 运行 uvicorn 应用
11
+ cd "${SCRIPT_DIR}"
12
+ uvicorn app:app --host 0.0.0.0 --port 7860
13
 
14
  # (可选) 等待 tail 进程结束,但它通常不会结束
15
  # wait $!