├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Clash ├── Snippet.py ├── ToClash.py ├── ToClashV1.py ├── TopologicalSort.py └── __init__.py ├── Diagrams └── Filter │ ├── Class.wsd │ └── Use Case.wsd ├── Emoji ├── __init__.py ├── emoji.py └── flag_emoji.json ├── Emoji_Debug.py ├── Expand ├── ExpandPolicyPath.py ├── ExpandRuleSet.py ├── GetUrlContent.py └── __init__.py ├── Filter ├── __init__.py └── filter.py ├── README.md ├── Surge3 ├── ToSurge3.py └── __init__.py ├── Surge3Expand_Debug.py ├── Surge3ListFilter_Debug.py ├── Surge3ToClash_Debug.py ├── Unite ├── CheckPolicyPath.py ├── GetElement │ ├── GetGeneralElement.py │ ├── GetHeaderRewriteElement.py │ ├── GetHostElement.py │ ├── GetMITMElement.py │ ├── GetProxyElement.py │ ├── GetProxyGroupElement.py │ ├── GetReplicaElement.py │ ├── GetRuleElement.py │ ├── GetURLRewriteElement.py │ └── __init__.py ├── GetProxyGroupType.py ├── Surge3LikeConfig2XML.py └── __init__.py ├── main.py ├── rename.md ├── requirements.txt └── supervisor.conf /.gitignore: -------------------------------------------------------------------------------- 1 | *.xml 2 | test.py 3 | *.pyc 4 | *.yml 5 | *.log -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File (Integrated Terminal)", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "internalConsole", 13 | "internalConsoleOptions": "openOnSessionStart" 14 | }, 15 | { 16 | "name": "Python: Remote Attach", 17 | "type": "python", 18 | "request": "attach", 19 | "port": 5678, 20 | "host": "localhost", 21 | "pathMappings": [ 22 | { 23 | "localRoot": "${workspaceFolder}", 24 | "remoteRoot": "." 25 | } 26 | ] 27 | }, 28 | { 29 | "name": "Python: Module", 30 | "type": "python", 31 | "request": "launch", 32 | "module": "enter-your-module-name-here", 33 | "console": "integratedTerminal" 34 | }, 35 | { 36 | "name": "Python: Django", 37 | "type": "python", 38 | "request": "launch", 39 | "program": "${workspaceFolder}/manage.py", 40 | "console": "integratedTerminal", 41 | "args": [ 42 | "runserver", 43 | "--noreload", 44 | "--nothreading" 45 | ], 46 | "django": true 47 | }, 48 | { 49 | "name": "Python: Flask", 50 | "type": "python", 51 | "request": "launch", 52 | "module": "flask", 53 | "env": { 54 | "FLASK_APP": "app.py" 55 | }, 56 | "args": [ 57 | "run", 58 | "--no-debugger", 59 | "--no-reload" 60 | ], 61 | "jinja": true 62 | }, 63 | { 64 | "name": "Python: Current File (External Terminal)", 65 | "type": "python", 66 | "request": "launch", 67 | "program": "${file}", 68 | "console": "externalTerminal" 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": true, 3 | "python.autoComplete.extraPaths": [ 4 | "${env:SPARK_HOME}\\python", 5 | "${env:SPARK_HOME}\\python\\pyspark" 6 | ], 7 | "python.linting.enabled": true 8 | } -------------------------------------------------------------------------------- /Clash/Snippet.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import yaml 3 | 4 | 5 | def AddSnippet(url, dic): 6 | content = requests.get(url).text 7 | snippet = yaml.load(content) 8 | dic = {**dic, **snippet} 9 | return dic 10 | -------------------------------------------------------------------------------- /Clash/ToClash.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | import yaml 4 | 5 | from . import Snippet 6 | from .Snippet import AddSnippet 7 | 8 | ProxyInfo = { 9 | "name": "name", 10 | "server": "server", 11 | "type": "type", 12 | "port": "port", 13 | "encrypt-method": "cipher", 14 | "password": "password", 15 | "obfs": "obfs", 16 | "obfs-host": "obfs-host", 17 | "udp-relay": "udp" 18 | } 19 | 20 | ProxyGroupInfo = ["url", "interval"] 21 | 22 | 23 | AllowBuiltIn = ["DIRECT", "REJECT"] 24 | AllowRuleTag = ["DOMAIN-SUFFIX", "DOMAIN-KEYWORD", 25 | "DOMAIN", "IP-CIDR", "SOURCE-IP-CIDR", "GEOIP", "FINAL"] 26 | 27 | 28 | def ToClash(root, snippet=None): 29 | Replace = {} 30 | conf = {"port": 7890, 31 | "socks-port": 7891, 32 | "allow-lan": False, 33 | "mode": "Rule", 34 | "log-level": "info", 35 | "external-controller": '0.0.0.0:9090', 36 | "secret": "", 37 | "Proxy": [], 38 | "Proxy Group": [], 39 | "Rule": [] 40 | } 41 | 42 | # add snippet 43 | if snippet: 44 | conf = AddSnippet(snippet, conf) 45 | 46 | for elem in root.find("Proxy"): 47 | if elem.tag == "Built-in": 48 | Replace[elem.get("name")] = elem.get("policy").upper() 49 | else: 50 | dic = {} 51 | for attrib in ProxyInfo: 52 | if attrib in elem.attrib: 53 | if attrib == "type" and elem.get(attrib) == "custom": 54 | value = "ss" 55 | else: 56 | if(elem.get(attrib) == "true"): 57 | value = True 58 | elif(elem.get(attrib) == "false"): 59 | value = False 60 | else: 61 | value = elem.get(attrib) 62 | dic[ProxyInfo[attrib]] = value 63 | conf["Proxy"].append(dic) 64 | 65 | for elem in root.findall("ProxyGroup/policy"): 66 | dic = {} 67 | dic["name"] = elem.get("name") 68 | dic["type"] = elem.get("type") 69 | proxies = [] 70 | for it in elem: 71 | if it.text in Replace: 72 | if Replace[it.text] in AllowBuiltIn: 73 | proxies.append(Replace[it.text]) 74 | else: 75 | proxies.append(it.text) 76 | dic["proxies"] = proxies 77 | if elem.get("type") != "select": 78 | dic["url"] = elem.get("url", "http://www.gstatic.com/generate_204") 79 | dic["interval"] = elem.get("interval", "600") 80 | conf["Proxy Group"].append(dic) 81 | for elem in root.find("Rule"): 82 | if elem.tag == "comment" or elem.tag not in AllowRuleTag: 83 | continue 84 | if elem.tag == "FINAL": 85 | l = "MATCH, "+elem.get("policy") 86 | else: 87 | l = elem.tag+", "+elem.get("match")+", "+elem.get("policy") 88 | conf["Rule"].append(l) 89 | 90 | return yaml.dump(conf) 91 | 92 | 93 | if __name__ == "__main__": 94 | tree = ET.parse("Private_Demo.xml") 95 | root = tree.getroot() 96 | ToClash(root) 97 | -------------------------------------------------------------------------------- /Clash/ToClashV1.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | import yaml 4 | 5 | from . import Snippet 6 | from .Snippet import AddSnippet 7 | 8 | ProxyInfo = { 9 | "name": "name", 10 | "server": "server", 11 | "type": "type", 12 | "port": "port", 13 | "encrypt-method": "cipher", 14 | "password": "password", 15 | "obfs": "plugin", 16 | "obfs-host": "plugin-opts", 17 | "udp-relay": "udp" 18 | } 19 | 20 | ProxyGroupInfo = ["url", "interval"] 21 | 22 | 23 | AllowBuiltIn = ["DIRECT", "REJECT"] 24 | AllowRuleTag = ["DOMAIN-SUFFIX", "DOMAIN-KEYWORD", 25 | "DOMAIN", "IP-CIDR", "SRC-IP-CIDR", "GEOIP", "FINAL"] 26 | 27 | 28 | def ToClashV1(root, snippet=None): 29 | Replace = {} 30 | conf = {"port": 7890, 31 | "socks-port": 7891, 32 | "allow-lan": False, 33 | "mode": "Rule", 34 | "log-level": "info", 35 | "external-controller": '0.0.0.0:9090', 36 | "secret": "", 37 | "proxies": [], 38 | "proxy-groups": [], 39 | "rules": [] 40 | } 41 | 42 | # add snippet 43 | if snippet: 44 | conf = AddSnippet(snippet, conf) 45 | 46 | for elem in root.find("Proxy"): 47 | if elem.tag == "Built-in": 48 | Replace[elem.get("name")] = elem.get("policy").upper() 49 | else: 50 | dic = {} 51 | for attrib in ProxyInfo: 52 | if attrib == "obfs" and attrib in elem.attrib: 53 | dic["plugin"] = "obfs" 54 | if "plugin-opts" not in dic: 55 | dic["plugin-opts"] = {} 56 | dic["plugin-opts"]["mode"] = elem.get(attrib) 57 | elif attrib == "obfs-host" and attrib in elem.attrib: 58 | if "plugin-opts" not in dic: 59 | dic["plugin-opts"] = {} 60 | dic["plugin-opts"]["host"] = elem.get(attrib) 61 | elif attrib in elem.attrib: 62 | if attrib == "type" and elem.get(attrib) == "custom": 63 | value = "ss" 64 | else: 65 | if(elem.get(attrib) == "true"): 66 | value = True 67 | elif(elem.get(attrib) == "false"): 68 | value = False 69 | else: 70 | value = elem.get(attrib) 71 | dic[ProxyInfo[attrib]] = value 72 | conf["proxies"].append(dic) 73 | 74 | for elem in root.findall("ProxyGroup/policy"): 75 | dic = {} 76 | dic["name"] = elem.get("name") 77 | dic["type"] = elem.get("type") 78 | proxies = [] 79 | for it in elem: 80 | if it.text in Replace: 81 | if Replace[it.text] in AllowBuiltIn: 82 | proxies.append(Replace[it.text]) 83 | else: 84 | proxies.append(it.text) 85 | dic["proxies"] = proxies 86 | if elem.get("type") != "select": 87 | dic["url"] = elem.get("url", "http://www.gstatic.com/generate_204") 88 | dic["interval"] = elem.get("interval", "600") 89 | conf["proxy-groups"].append(dic) 90 | for elem in root.find("Rule"): 91 | if elem.tag == "comment" or elem.tag not in AllowRuleTag: 92 | continue 93 | if elem.tag == "FINAL": 94 | l = "MATCH, "+elem.get("policy") 95 | else: 96 | l = elem.tag+", "+elem.get("match")+", "+elem.get("policy") 97 | conf["rules"].append(l) 98 | 99 | return yaml.dump(conf) 100 | -------------------------------------------------------------------------------- /Clash/TopologicalSort.py: -------------------------------------------------------------------------------- 1 | import xml.dom.minidom 2 | import xml.etree.ElementTree as ET 3 | 4 | 5 | def dfs(root, elem, visit): 6 | visit.add(elem.get("name")) 7 | result = list() 8 | for it in elem: 9 | name = it.text 10 | if name in visit: 11 | continue 12 | for i in root.findall("ProxyGroup/policy"): 13 | if i.get("name") == name: 14 | result += dfs(root, i, visit) 15 | break 16 | result.append(elem) 17 | return result 18 | 19 | 20 | def TopologicalSort(root): 21 | visit = set() 22 | ProxyGroup = root.find("ProxyGroup") 23 | result = list() 24 | for it in ProxyGroup: 25 | if it.get("name") not in visit: 26 | result += dfs(root, it, visit) 27 | ProxyGroup[:] = result 28 | # result = xml.dom.minidom.parseString( 29 | # ET.tostring(root)).toprettyxml() 30 | # print(result) 31 | return root 32 | -------------------------------------------------------------------------------- /Clash/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0KABE/ConfConvertor/05afa4e1387f9772c4cbaafa69943c29a9e7053a/Clash/__init__.py -------------------------------------------------------------------------------- /Diagrams/Filter/Class.wsd: -------------------------------------------------------------------------------- 1 | @startuml API Filter Class Diagram 2 | title API Filter Class Diagram 3 | 4 | abstract Filter{ 5 | + filename: string 6 | + url: string 7 | + type: string 8 | + regex: regex 9 | + rename: string 10 | 11 | + {abstract} filter_source(): Response 12 | } 13 | 14 | class SurgeListFilter{ 15 | + filter_proxy(): str 16 | + filter_by_regex(): str 17 | + filter_source(): str 18 | } 19 | SurgeListFilter --|> Filter 20 | 21 | class SurgeConfFilter 22 | SurgeConfFilter --|> SurgeListFilter 23 | 24 | class SSFilter 25 | SSFilter --|> Filter 26 | 27 | class SSRFilter 28 | SSRFilter --|> Filter 29 | 30 | @enduml -------------------------------------------------------------------------------- /Diagrams/Filter/Use Case.wsd: -------------------------------------------------------------------------------- 1 | @startuml API Filter Use Case Diagram 2 | title API Filter Use Case Diagram 3 | 4 | actor User 5 | 6 | rectangle Surge_Filter{ 7 | 'Surge3 8 | User --> (Filter SurgeList) 9 | User --> (Filter SurgeConfg) 10 | (Filter SurgeConfg) ..> (Filter SurgeList): <> 11 | (Filter SurgeConfg) ..> (Filter Sruge Proxy): <> 12 | } 13 | 14 | rectangle SS/SSR_Filter{ 15 | 'Shadowsocks 16 | User --> (Filter Shadowsocks) 17 | (Filter Shadowsocks) ..> (Decode SS Base64): <> 18 | (Decode SS Base64) ..> (Decode SS/SSR Base64): <> 19 | 20 | 'ShadowsocksR 21 | User --> (Filter ShadowsocksR) 22 | (Filter ShadowsocksR) ..> (Decode SSR Base64): <> 23 | (Decode SSR Base64) ..> (Decode SS/SSR Base64): <> 24 | } 25 | @enduml -------------------------------------------------------------------------------- /Emoji/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0KABE/ConfConvertor/05afa4e1387f9772c4cbaafa69943c29a9e7053a/Emoji/__init__.py -------------------------------------------------------------------------------- /Emoji/emoji.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import base64 3 | import json 4 | import urllib 5 | import urllib.parse 6 | from enum import Enum 7 | 8 | import emoji 9 | import requests 10 | from flask import Request, Response, make_response 11 | 12 | DEFAULT_EMOJI_URL = "https://raw.githubusercontent.com/0KABE/ConfConvertor/master/Emoji/flag_emoji.json" 13 | DEFAULT_FILE_NAME = "Emoji" 14 | 15 | 16 | class EmojiParm(Enum): 17 | TYPE = "type" 18 | URL = "url" 19 | FILE_NAME = "filename" 20 | DEL_EMOJI = "delEmoji" 21 | DIRECTION = "direction" 22 | EMOJI = "emoji" 23 | 24 | 25 | class EmojiType(Enum): 26 | SURGE_LIST = "surgelist" 27 | SS = "ss" 28 | SSR = "ssr" 29 | 30 | 31 | class Direction(Enum): 32 | HEAD = "head" 33 | TAIL = "tail" 34 | 35 | 36 | class Emoji(object): 37 | def __init__(self, request: Request): 38 | self.type: EmojiType = EmojiType( 39 | request.args.get(EmojiParm.TYPE.value, EmojiType.SURGE_LIST.value)) 40 | self.url: str = request.args.get(EmojiParm.URL.value) 41 | self.filename: str = request.args.get( 42 | EmojiParm.FILE_NAME.value, DEFAULT_FILE_NAME) 43 | self.delete: bool = False if request.args.get( 44 | EmojiParm.DEL_EMOJI.value) == "false" else True 45 | self.direction: Direction = Direction( 46 | request.args.get(EmojiParm.DIRECTION.value, Direction.TAIL.value)) 47 | self.emoji_url: str = request.args.get( 48 | EmojiParm.EMOJI.value, DEFAULT_EMOJI_URL) 49 | self.emoji_content: str = requests.get(self.emoji_url).content.decode() 50 | self.emoji: dict = json.loads(self.emoji_content) 51 | self.url_content: list = self._download_url_content() 52 | 53 | def convert(self) -> Response: 54 | if(self.delete): 55 | self._del_emoji() 56 | response: Response = make_response( 57 | "\n".join(self._add_emoji().url_content)) 58 | response.headers["Content-Disposition"] = "attachment; filename="+self.filename 59 | return response 60 | 61 | def _download_url_content(self) -> list: 62 | return requests.get(self.url).content.decode().splitlines() 63 | 64 | def _add_emoji_by_line(self, line: str): 65 | index = None 66 | key = None 67 | for k in self.emoji.keys(): 68 | for v in self.emoji[k]: 69 | if self.direction == Direction.TAIL: 70 | pos = line.rfind(v) 71 | if pos != -1 and (index == None or index < pos): 72 | index = pos 73 | key = k 74 | elif self.direction == Direction.HEAD: 75 | pos = line.find(v) 76 | if pos != -1 and (index == None or index > pos): 77 | index = pos 78 | key = k 79 | if index != None: 80 | return key+line 81 | else: 82 | return line 83 | 84 | @abc.abstractmethod 85 | def _add_emoji(self): 86 | pass 87 | 88 | @abc.abstractmethod 89 | def _del_emoji(self): 90 | pass 91 | 92 | 93 | class SurgeListEmoji(Emoji): 94 | def _del_emoji(self): 95 | for i in range(len(self.url_content)): 96 | self.url_content[i] = emoji.get_emoji_regexp().sub( 97 | u'', self.url_content[i]) 98 | return self 99 | 100 | def _add_emoji(self): 101 | res: list = [] 102 | for line in self.url_content: 103 | res.append(self._add_emoji_by_line(line)) 104 | self.url_content = res 105 | return self 106 | 107 | 108 | class SSEmoji(Emoji): 109 | def _download_url_content(self): 110 | content: bytes = requests.get(self.url).content 111 | content_decoded: str = content.decode() 112 | if(content_decoded.startswith("ss://")): 113 | return content_decoded.splitlines() 114 | else: 115 | return base64.b64decode(content).decode().splitlines() 116 | 117 | def _node_name(self, url: str) -> str: 118 | return urllib.parse.unquote(urllib.parse.urlparse(url).fragment) 119 | 120 | def _del_emoji(self): 121 | for i in range(len(self.url_content)): 122 | url = self.url_content[i] 123 | parsed = urllib.parse.urlparse(url) 124 | parsed = parsed._replace(fragment=emoji.get_emoji_regexp().sub( 125 | u'', self._node_name(url))) 126 | self.url_content[i] = urllib.parse.urlunparse(parsed) 127 | return self 128 | 129 | def _add_emoji(self): 130 | res = [] 131 | for url in self.url_content: 132 | parsed = urllib.parse.urlparse(url) 133 | name = self._add_emoji_by_line(self._node_name(url)) 134 | parsed = parsed._replace(fragment=name) 135 | res.append(urllib.parse.urlunparse(parsed)) 136 | self.url_content = res 137 | return self 138 | 139 | def convert(self) -> Response: 140 | if(self.delete): 141 | self._del_emoji() 142 | response: Response = make_response( 143 | base64.b64encode( 144 | "\n".join(self._add_emoji().url_content).encode()).decode()) 145 | response.headers["Content-Disposition"] = "attachment; filename="+self.filename 146 | return response 147 | 148 | 149 | class SSREmoji(SSEmoji): 150 | def _download_url_content(self): 151 | content: bytes = requests.get(self.url).content 152 | content_decoded = content.decode() 153 | if(content_decoded.startswith("ssr://")): 154 | return content_decoded.splitlines() 155 | else: 156 | # add missing padding 157 | content += b'='*(-len(content) % 4) 158 | return base64.urlsafe_b64decode(content).decode().splitlines() 159 | 160 | def _parse_ssr_url(self, url: str): 161 | # ssr://netloc -> netloc 162 | base64_content: str = urllib.parse.urlparse(url).netloc 163 | # add missing padding 164 | base64_content += '='*(-len(base64_content) % 4) 165 | # urlsafe base64 decode 166 | # decode bytes to str 167 | # add "ssr://" at the leading 168 | content: str = "ssr://" + \ 169 | base64.urlsafe_b64decode(base64_content).decode() 170 | return content 171 | 172 | def _unparse_ssr_url(self, url: str): 173 | base64_content: str = url.replace("ssr://", "", 1) 174 | ssr_url = "ssr://" + \ 175 | base64.urlsafe_b64encode( 176 | base64_content.encode()).decode().rstrip("=") 177 | return ssr_url 178 | 179 | def _node_name(self, url: str): 180 | # get parameter dictionary 181 | param_dict: dict = urllib.parse.parse_qs( 182 | urllib.parse.urlparse(url).query, keep_blank_values=True) 183 | # replace ' ' by '+' 184 | remarks = param_dict["remarks"][0].replace(' ', '+') 185 | # add missing padding 186 | # get the parameter remarks 187 | name: str = base64.urlsafe_b64decode( 188 | remarks+'='*(-len(remarks) % 4)).decode() 189 | return name 190 | 191 | def _del_emoji(self): 192 | res = [] 193 | for url in self.url_content: 194 | url = self._parse_ssr_url(url) 195 | name = emoji.get_emoji_regexp().sub( 196 | u'', self._node_name(url)) 197 | parsed = urllib.parse.urlparse(url) 198 | query: dict = urllib.parse.parse_qs( 199 | parsed.query) 200 | query["remarks"][0] = base64.urlsafe_b64encode( 201 | name.encode()).decode().rstrip("=") 202 | for key in query: 203 | query[key] = "".join(query[key]) 204 | parsed = parsed._replace(query=urllib.parse.urlencode(query)) 205 | res.append(self._unparse_ssr_url(urllib.parse.urlunparse(parsed))) 206 | self.url_content = res 207 | return self 208 | 209 | def _add_emoji(self): 210 | res = [] 211 | for url in self.url_content: 212 | url = self._parse_ssr_url(url) 213 | name = self._add_emoji_by_line(self._node_name(url)) 214 | parsed = urllib.parse.urlparse(url) 215 | query: dict = urllib.parse.parse_qs( 216 | parsed.query) 217 | query["remarks"][0] = base64.urlsafe_b64encode( 218 | name.encode()).decode().rstrip("=") 219 | for key in query: 220 | query[key] = "".join(query[key]) 221 | 222 | parsed = parsed._replace(query=urllib.parse.urlencode(query)) 223 | res.append(self._unparse_ssr_url(urllib.parse.urlunparse(parsed))) 224 | # res.append(urllib.parse.urlunparse(parsed)) 225 | self.url_content = res 226 | return self 227 | 228 | def convert(self) -> Response: 229 | if(self.delete): 230 | self._del_emoji() 231 | response: Response = make_response( 232 | base64.urlsafe_b64encode( 233 | "\n".join(self._add_emoji().url_content).encode()).decode().rstrip("=")) 234 | response.headers["Content-Disposition"] = "attachment; filename="+self.filename 235 | return response 236 | -------------------------------------------------------------------------------- /Emoji/flag_emoji.json: -------------------------------------------------------------------------------- 1 | { 2 | "🏳️‍🌈": [ 3 | "流量", 4 | "时间", 5 | "应急" 6 | ], 7 | "🇦🇷": [ 8 | "阿根廷" 9 | ], 10 | "🇦🇹": [ 11 | "奥地利", 12 | "维也纳" 13 | ], 14 | "🇦🇺": [ 15 | "澳大利亚", 16 | "悉尼" 17 | ], 18 | "🇧🇷": [ 19 | "巴西", 20 | "圣保罗" 21 | ], 22 | "🇨🇦": [ 23 | "加拿大", 24 | "蒙特利尔", 25 | "温哥华" 26 | ], 27 | "🇨🇭": [ 28 | "瑞士", 29 | "苏黎世" 30 | ], 31 | "🇨🇳": [ 32 | "中国", 33 | "江苏", 34 | "北京", 35 | "上海", 36 | "广州", 37 | "深圳", 38 | "杭州", 39 | "徐州", 40 | "青岛", 41 | "宁波", 42 | "镇江" 43 | ], 44 | "🇩🇪": [ 45 | "德国", 46 | "法兰克福" 47 | ], 48 | "🇫🇮": [ 49 | "芬兰", 50 | "赫尔辛基" 51 | ], 52 | "🇫🇷": [ 53 | "法国", 54 | "巴黎" 55 | ], 56 | "🇬🇧": [ 57 | "英国", 58 | "伦敦" 59 | ], 60 | "🇭🇰": [ 61 | "香港", 62 | "深港", 63 | "沪港" 64 | ], 65 | "🇮🇩": [ 66 | "印尼", 67 | "印度尼西亚", 68 | "雅加达" 69 | ], 70 | "🇮🇪": [ 71 | "爱尔兰", 72 | "都柏林" 73 | ], 74 | "🇮🇳": [ 75 | "印度", 76 | "孟买" 77 | ], 78 | "🇮🇹": [ 79 | "意大利", 80 | "米兰" 81 | ], 82 | "🇯🇵": [ 83 | "日本", 84 | "东京", 85 | "大阪", 86 | "埼玉", 87 | "沪日" 88 | ], 89 | "🇰🇵": [ 90 | "朝鲜" 91 | ], 92 | "🇰🇷": [ 93 | "韩国", 94 | "首尔" 95 | ], 96 | "🇲🇴": [ 97 | "澳门" 98 | ], 99 | "🇲🇾": [ 100 | "马来西亚" 101 | ], 102 | "🇳🇱": [ 103 | "荷兰", 104 | "阿姆斯特丹" 105 | ], 106 | "🇵🇭": [ 107 | "菲律宾" 108 | ], 109 | "🇷🇴": [ 110 | "罗马尼亚" 111 | ], 112 | "🇷🇺": [ 113 | "俄罗斯", 114 | "伯力", 115 | "莫斯科", 116 | "圣彼得堡", 117 | "西伯利亚", 118 | "新西伯利亚" 119 | ], 120 | "🇸🇬": [ 121 | "新加坡", 122 | "狮城" 123 | ], 124 | "🇹🇭": [ 125 | "泰国", 126 | "曼谷" 127 | ], 128 | "🇹🇷": [ 129 | "土耳其", 130 | "伊斯坦布尔" 131 | ], 132 | "🇹🇼": [ 133 | "台湾", 134 | "台北", 135 | "台中", 136 | "新北", 137 | "彰化" 138 | ], 139 | "🇺🇲": [ 140 | "美国", 141 | "波特兰", 142 | "达拉斯", 143 | "俄勒冈", 144 | "凤凰城", 145 | "费利蒙", 146 | "硅谷", 147 | "拉斯维加斯", 148 | "洛杉矶", 149 | "圣何塞", 150 | "圣克拉拉", 151 | "西雅图", 152 | "芝加哥", 153 | "沪美" 154 | ], 155 | "🇻🇳": [ 156 | "越南" 157 | ], 158 | "🇿🇦": [ 159 | "南非" 160 | ] 161 | } 162 | -------------------------------------------------------------------------------- /Emoji_Debug.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | from flask import Flask, Response, make_response, request 5 | 6 | import main as Main 7 | 8 | app = Flask(__name__) 9 | 10 | 11 | @app.route('/', methods=['GET', 'POST']) 12 | def main(): 13 | """Responds to any HTTP request. 14 | Args: 15 | request (flask.Request): HTTP request object. 16 | Returns: 17 | The response text or any set of values that can be turned into a 18 | Response object using 19 | #flask.Flask.make_response>`. 20 | `make_response str: 30 | """ 31 | get all proxies from the url 32 | """ 33 | # get the decoded content from the url 34 | # strip unnecessary whitespace 35 | return requests.get( 36 | self.url).content.decode().strip() 37 | 38 | def filter_by_regex(self, content: str) -> str: 39 | proxies: list = content.splitlines() 40 | prog = re.compile(self.regex) 41 | result: list = [] # filter result 42 | for line in proxies: 43 | math_group = prog.match(line) 44 | # if the line match the regex 45 | if math_group: 46 | # if need to rename 47 | if self.rename: 48 | proxy: str 49 | for part in self.rename.splitlines(): 50 | if part in math_group.groupdict(): 51 | proxy += math_group.group(part) 52 | else: 53 | proxy += part 54 | result.append(proxy) 55 | else: 56 | result.append(line) 57 | return "\n".join(result) 58 | 59 | def filter_source(self): 60 | response = make_response(self.filter_by_regex(self.filter_proxy())) 61 | response.headers["Content-Disposition"] = "attachment; filename="+self.filename 62 | return response 63 | 64 | 65 | class SrugeListFilter(Filter): 66 | def __init__(self, request: Request): 67 | super().__init__(request) 68 | if self.filename is None: 69 | self.filename = "Filter.list" 70 | 71 | def filter_proxy(self) -> str: 72 | """ 73 | get all proxies from the url 74 | """ 75 | # get the decoded content from the url 76 | # strip unnecessary whitespace 77 | return requests.get( 78 | self.url).content.decode().strip() 79 | 80 | def filter_by_regex(self, content: str) -> str: 81 | proxies: list = content.splitlines() 82 | prog = re.compile(self.regex) 83 | result: list = [] # filter result 84 | for line in proxies: 85 | math_group = prog.match(line) 86 | # if the line match the regex 87 | if math_group: 88 | # if need to rename 89 | if self.rename: 90 | proxy: str 91 | for part in self.rename.splitlines(): 92 | if part in math_group.groupdict(): 93 | proxy += math_group.group(part) 94 | else: 95 | proxy += part 96 | result.append(proxy) 97 | else: 98 | result.append(line) 99 | return "\n".join(result) 100 | 101 | def filter_source(self): 102 | response = make_response(self.filter_by_regex(self.filter_proxy())) 103 | response.headers["Content-Disposition"] = "attachment; filename="+self.filename 104 | return response 105 | 106 | 107 | class SurgeConfFilter(SrugeListFilter): 108 | def filter_proxy(self) -> str: 109 | content: list = requests.get( 110 | self.url).content.decode().strip().splitlines() 111 | proxies: list = [] 112 | status: str = "" 113 | for line in content: 114 | if line.startswith("["): 115 | status = line 116 | elif status == "[Proxy]": 117 | proxies.append(line) 118 | return "\n".join(proxies) 119 | 120 | 121 | class SSFilter(Filter): 122 | def __init__(self, request): 123 | super().__init__(request) 124 | if self.filename is None: 125 | self.filename = "Filter.txt" 126 | 127 | def filter_source(self): 128 | response = make_response(self.filter_by_regex()) 129 | response.headers["Content-Disposition"] = "attachment; filename="+self.filename 130 | return response 131 | 132 | def download_content(self): 133 | return base64.b64decode( 134 | requests.get(self.url).content).decode().splitlines() 135 | 136 | def filter_by_regex(self): 137 | content: list = self.download_content() 138 | prog = re.compile(self.regex) 139 | result: list = [] # filter result 140 | for line in content: 141 | name: str = self.node_name(line) 142 | match = prog.match(name) 143 | if match: 144 | result.append(line) 145 | return "\n".join(result).encode() 146 | 147 | def node_name(self, url: str) -> str: 148 | return urllib.parse.unquote(urllib.parse.urlparse(url).fragment) 149 | 150 | 151 | class SSRFilter(SSFilter): 152 | def download_content(self): 153 | content: bytes = requests.get(self.url).content 154 | # add missing padding 155 | content += b'='*(-len(content) % 4) 156 | return base64.urlsafe_b64decode(content).decode().splitlines() 157 | 158 | def node_name(self, url: str): 159 | # ssr://netloc -> netloc 160 | base64_content: str = urllib.parse.urlparse(url).netloc 161 | # add missing padding 162 | base64_content += '='*(-len(base64_content) % 4) 163 | # urlsafe base64 decode 164 | # decode bytes to str 165 | # add "ssr://" at the leading 166 | content: str = "ssr://" + \ 167 | base64.urlsafe_b64decode(base64_content).decode() 168 | # get parameter dictionary 169 | param_dict: dict = urllib.parse.parse_qs( 170 | urllib.parse.urlparse(content).query) 171 | # replace ' ' by '+' 172 | remarks = param_dict["remarks"][0].replace(' ', '+') 173 | # add missing padding 174 | # get the parameter remarks 175 | name: str = base64.urlsafe_b64decode( 176 | remarks+'='*(-len(remarks) % 4)).decode() 177 | return name 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConfConvertor 2 | 3 | 旨在能够使用一套配置通过转换API转换成适应于各类科学上网的配置文件 4 | 5 | 拟通过在类Surge3Pro的配置文件上增加一些Clash的特殊的内容。 6 | 例如: 7 | 当调用导出为Surge配置文件时,从类Surge3Pro的配置文件中抽取Surge3Pro支持的内容(例如Surge 不支持V2ray),组成Surge3Pro的配置文件 8 | 9 | 可以实现一份配置文件同时支持Clash & Surge3 10 | 11 | ## API: Surge3介绍 12 | 13 | 相比与Surge3Expand 新的API Surge3 ***~~不在~~*** 默认将policy-path, RULE-SET 全部展开 14 | **去除load-balance** 15 | 16 | 在Surge3Pro中,不支持policy-path与其他policy-path混用或policy-path与其他策略组混用 17 | 即如果需要使用policy-path来远程下载节点信息,则该策略组将只允许一个policy-path 18 | 例如: 19 | 20 | ```conf 21 | policy1 = select, policy-path=www.example.com/path/file.list 合法 22 | policy2 = select, policy-path=www.example.com/path/file.list, policy1 非法 23 | policy3 = select, policy-path=www.example.com/path/file1.list, policy-path=www.example.com/path/file2.list 非法 24 | ``` 25 | 26 | ~~现在,API Surge3将会判断策略组中是否存在上述的情况,若存在上述的在Surge中非法的情况,才会对所有policy-path进行展开 27 | 如果策略组中没有存在上述的情况,保留policy-path交给Surge3展开总是更好的~~ 28 | **因为Surge3托管文件不能手动更新PolicyPath 29 | 现在API Surge3 将会直接将policypath展开** 30 | 31 | ## API: Clash介绍 32 | 33 | **支持load-balance** 34 | 在Clash中,靠后的策略组中包含的策略组必须位于该策略组前面,而Surge中则没有这个限制,可以任意排序。 35 | 在这个API中,Clash将会通过排序来使得策略组的顺序满足Clash的要求。 36 | 例如: 37 | 38 | ```yaml 39 | - name: Policy1 40 | type: select 41 | proxies: 42 | - Policy2 43 | - Policy3 44 | - name: Policy2 45 | type: select 46 | proxies: 47 | - Node1 48 | - Node2 49 | - name: Policy3 50 | type: select 51 | proxies: 52 | - Node3 53 | - Node4 54 | ``` 55 | 56 | 上述的序列关系无法在Clash使用,需要对该策略组的顺序重新排列 57 | 58 | ```yaml 59 | - name: Policy2 60 | type: select 61 | proxies: 62 | - Node1 63 | - Node2 64 | - name: Policy3 65 | type: select 66 | proxies: 67 | - Node3 68 | - Node4 69 | - name: Policy1 70 | type: select 71 | proxies: 72 | - Policy2 73 | - Policy3 74 | ``` 75 | 76 | 以上便是一个排列后在Clash中合法的顺序组合 77 | 除此之外,该API将会对policy-path以及RULE-SET进行展开,去除某些在clash中不支持的内容。 78 | 例如: 79 | 80 | * reject-tinygif 81 | * USER-AGENT 82 | * MITM 83 | * 等等 84 | 85 | **如果担心数据安全性等问题,可以选择在自己的服务器上搭建,源代码已经在代码库中给出** 86 | **如果遇到BUG 或者 有好的Feature 欢迎提Issue** 87 | 88 | ## 使用方法 89 | 90 | ### Surge3 91 | 92 | 将类Surge3配置转换为Surge3配置 93 | 94 | URL: 95 | 支持的参数:url(必须),filename(非必须),interval(非必须),strict(非必须) 96 | 97 | | 参数 | 必须 | 描述 | 缺省值 | 98 | | :------------------- | :--- | :---------------------------------------------------------------------------- | :---------- | 99 | | url | 是 | 待转换的类Surge3Pro配置url地址 | 无 | 100 | | filename | 否 | 返回的配置文件名称 | Config.conf | 101 | | interval | 否 | 托管配置的更新间隔(s) | 86400 | 102 | | strict(true/false) | 否 | 在更新间隔到达时是否强制更新,如果为false则在更新失败后依旧使用原来的托管配置 | false | 103 | 104 | ### Clash 105 | 106 | 将类Surge3配置转换为Clash配置 107 | 108 | URL: 109 | Clash Core v1.0 URL: 其他保持不变 110 | 111 | | 参数 | 必须 | 描述 | 缺省值 | 112 | | :------- | :--- | :---------------------------------------------------------------- | :--------- | 113 | | url | 是 | 待转换的类Surge3Pro配置url地址 | 无 | 114 | | filename | 否 | 返回的配置文件名称 | Config.yml | 115 | | sort | 否 | 是否对此策略组进行排序以满足被引用的策略组在引用策略组的上方 | true | 116 | | snippet | 否 | 为clash配置附加额外的参数(例如DNS)参数格式为yaml格式(同Clash) | 无 | 117 | 118 | ### Filter 119 | 120 | 对节点进行过滤 121 | 122 | URL: 123 | 124 | | 参数 | 必须 | 描述 | 缺省值 | 取值范围 | 125 | | :------- | :--- | :----------------------------------------------------------- | :--------------- | :------------------------- | 126 | | type | 是 | 源文件类型 | 无 | surgelist/surgeconf/ss/ssr/qxlist | 127 | | url | 是 | 源文件地址 | 无 | 128 | | regex | 是 | 用于过滤的正则表达式 | 无 | 129 | | filename | 否 | 返回的list文件名 | Filter.list | 130 | | rename | 否 | 根据该参数自定义节点名称(仅可在surgelist|surgeconf|qxlist中使用) | 返回原始节点名称 | 131 | 132 | **所有url参数**建议进行url编码 133 | 134 | **rename参数具体的使用方法参见**: 135 | 136 | | | | 137 | | :---------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------- | 138 | | 过滤surge list | | 139 | | 过滤surge托管配置 | | 140 | | 过滤surge list并且需要对节点名称自定义 | | 141 | | 过滤surge托管配置并且需要对节点名称自定义 | | 142 | 143 | ### Emoji 144 | 145 | 作用:在节点名称前添加Emoji 146 | 147 | URL: 148 | 149 | | 参数 | 必须 | 描述 | 缺省值 | 取值范围 | 150 | | :-------------------- | :--- | :------------------------------------------------------------------- | :---------------- | :--------------: | 151 | | type | 是 | 源文件类型 | surgelist | surgelist/ss/ssr | 152 | | url | 是 | 源文件地址 | 无 | 153 | | filename | 否 | 返回的配置文件名称 | Emoji | 154 | | delEmoji (true/false) | 否 | 在添加emoji前删除所有emoji | true | 155 | | direction (head/tail) | 否 | 添加Emoji关键词的优先方向(head:从左到右匹配, tail:从右到左匹配) | tail | 156 | | emoji | 否 | 自定义emoji的json文件url | API自带的残疾json | 157 | 158 | emoji参数对应的格式: 159 | 160 | ## 使用方法(demo) 161 | 162 | 因为API需要一个url参数来获取类Surge配置文件,因此一种方法是使用GitHub私有gist来远程存放链接 163 | 例如, 现在的远程链接: 164 | 则: 165 | 在Surge3Pro中的托管链接为: 166 | 在Clash中的托管链接为: 167 | 168 | ## 感谢 169 | 170 | * Shiro 171 | * 🅚ⒺⓋⒾⓃ 🅧ⒾⓃⒼ 172 | * 🆉🄴🄰🄻🅂🄾🄽 173 | * 旺仔(JO2EY) 174 | * Alban White 175 | 176 | ## 打赏 177 | 178 | ```text 179 | #吱口令#长按复制此条消息,打开支付宝给我转账ijL3kr36HM 180 | ``` 181 | 182 | ## Telegram 183 | 184 | [TG群组](https://t.me/OKAB3Script) 185 | 186 | [TG频道](https://t.me/OKAB3_Script_Channel) 187 | -------------------------------------------------------------------------------- /Surge3/ToSurge3.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def ToSurge3(root): 5 | """ 6 | Args: 7 | root(xml.etree.ElementTree.Element): the root element of the xml 8 | Return: 9 | A string contains Surge3Pro Configuration Content 10 | Do: 11 | Convert the content Surge3Pro-supported to String 12 | """ 13 | Surge3 = "" 14 | KeyWordsCorrespond = {"General": "[General]", "Replica": "[Replica]", "Proxy": "[Proxy]", "ProxyGroup": "[Proxy Group]", "Rule": "[Rule]", 15 | "Host": "[Host]", "URLRewrite": "[URL Rewrite]", "HeaderRewrite": "[Header Rewrite]", "SSIDSetting": "[SSID Setting]", "MITM": "[MITM]"} 16 | ProxyTypeAttrib = {"ss": {"type": "", "server": "", "port": "", "encrypt-method": "encrypt-method=", "password": "password=", "obfs": "obfs=", "obfs-host": "obfs-host=", "tfo": "tfo=", "udp-relay": "udp-relay="}, 17 | "custom": {"type": "", "server": "", "port": "", "encrypt-method": "", "password": "", "module": "", "obfs": "obfs=", "obfs-host": "obfs-host="}} 18 | for elem in root: 19 | if elem.tag == "comment": 20 | continue 21 | Surge3 += KeyWordsCorrespond[elem.tag]+"\n" 22 | if elem.tag == "General": 23 | for sub in elem: 24 | if sub.tag == "comment": 25 | Surge3 += sub.text+"\n" 26 | else: 27 | Surge3 += sub.tag+" = "+sub.text+"\n" 28 | elif elem.tag == "Replica": 29 | for sub in elem: 30 | if sub.tag == "comment": 31 | Surge3 += sub.text+"\n" 32 | else: 33 | Surge3 += sub.tag+" = "+sub.text+"\n" 34 | elif elem.tag == "Proxy": 35 | for sub in elem: 36 | if sub.tag == "comment": 37 | Surge3 += sub.text+"\n" 38 | elif sub.tag == "Built-in": 39 | Surge3 += sub.get("name")+" = "+sub.get("policy")+"\n" 40 | else: 41 | ProxyType = sub.get("type") 42 | if sub.get("type") in ProxyTypeAttrib: 43 | l = list() 44 | for key in ProxyTypeAttrib[ProxyType]: 45 | if key in sub.attrib: 46 | l.append( 47 | ProxyTypeAttrib[ProxyType][key]+sub.get(key)) 48 | Surge3 += sub.get("name")+" = "+", ".join(l)+"\n" 49 | elif elem.tag == "ProxyGroup": 50 | RequiredPara = ("name", "type") 51 | for sub in elem: 52 | if sub.tag == "comment": 53 | Surge3 += sub.text+"\n" 54 | else: 55 | if sub.get("type") == "load-balance": 56 | continue 57 | l = list() 58 | l.append(sub.get("type")) 59 | for it in sub: 60 | if it.get("type") == "load-balance": 61 | continue 62 | if it .tag == "policy-path": 63 | l.append("policy-path = "+it.text) 64 | else: 65 | l.append(it.text) 66 | for it in sub.attrib: 67 | if it in RequiredPara: 68 | continue 69 | l.append(it+" = "+sub.get(it)) 70 | Surge3 += sub.get("name")+" = "+", ".join(l)+"\n" 71 | elif elem.tag == "Rule": 72 | for sub in elem: 73 | if sub.tag == "comment": 74 | Surge3 += sub.text+"\n" 75 | elif sub.tag == "FINAL": 76 | Surge3 += sub.tag+", "+sub.get("policy") 77 | if "dns-failed" in sub.attrib and sub.attrib["dns-failed"] == "true": 78 | Surge3 += ", dns-failed" 79 | Surge3 += "\n" 80 | else: 81 | Surge3 += sub.tag+", " + \ 82 | sub.get("match")+", "+sub.get("policy")+"\n" 83 | elif elem.tag == "Host": 84 | for sub in elem: 85 | if sub.tag == "comment": 86 | Surge3 += sub.text+"\n" 87 | else: 88 | Surge3 += sub.get("key")+" = "+sub.get("value")+"\n" 89 | elif elem.tag == "URLRewrite": 90 | Type_Correspond = {"Type_302": "302", "Type_reject": "reject", 91 | "Type_header": "header", "Type_307": "307"} 92 | for sub in elem: 93 | if sub.tag == "comment": 94 | Surge3 += sub.text+"\n" 95 | else: 96 | Surge3 += sub.get("regex")+" "+sub.get("replace") + \ 97 | " "+Type_Correspond[sub.tag]+"\n" 98 | elif elem.tag == "HeaderRewrite": 99 | Type_Correspond = {"Type_header-replace": "header-replace", 100 | "Type_header-add": "header-add", "Type_header-del": "header-del"} 101 | for sub in elem: 102 | if sub.tag == "comment": 103 | Surge3 += sub.text+"\n" 104 | else: 105 | if sub.tag == "Type_header-del": 106 | Surge3 += sub.get("regex")+" "+Type_Correspond[sub.tag]+" "+sub.get( 107 | "field")+"\n" 108 | else: 109 | Surge3 += sub.get("regex")+" "+Type_Correspond[sub.tag]+" "+sub.get( 110 | "field")+" "+sub.get("value")+"\n" 111 | elif elem.tag == "MITM": 112 | for sub in elem: 113 | Surge3 += sub.tag+" = "+sub.text+"\n" 114 | return Surge3 115 | -------------------------------------------------------------------------------- /Surge3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0KABE/ConfConvertor/05afa4e1387f9772c4cbaafa69943c29a9e7053a/Surge3/__init__.py -------------------------------------------------------------------------------- /Surge3Expand_Debug.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import requests 4 | from flask import Flask, Response, make_response, request 5 | 6 | from main import Surge3 7 | 8 | app = Flask(__name__) 9 | 10 | 11 | @app.route('/', methods=['GET', 'POST']) 12 | def main(): 13 | """Responds to any HTTP request. 14 | Args: 15 | request (flask.Request): HTTP request object. 16 | Returns: 17 | The response text or any set of values that can be turned into a 18 | Response object using 19 | #flask.Flask.make_response>`. 20 | `make_response `. 17 | `make_response `. 21 | `make_response = 1 and len(elem.getchildren()) > 1: 12 | expand = True 13 | break 14 | return expand 15 | -------------------------------------------------------------------------------- /Unite/GetElement/GetGeneralElement.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def GetGeneralElement(line): 5 | l = line.split("=") 6 | element = ET.Element(l[0].replace(" ", "")) 7 | element.text = l[1].strip() 8 | return element 9 | -------------------------------------------------------------------------------- /Unite/GetElement/GetHeaderRewriteElement.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def GetHeaderRewriteElement(line): 5 | l = line.split(" ", 3) 6 | element = ET.Element("Type_"+l[1]) 7 | element.set("regex", l[0]) 8 | element.set("field", l[2]) 9 | if l[1] != "header-del": 10 | element.set("value", l[3]) 11 | return element 12 | -------------------------------------------------------------------------------- /Unite/GetElement/GetHostElement.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def GetHostElement(line): 5 | l = line.split("=") 6 | element = ET.Element("Item") 7 | element.set("key", l[0].strip()) 8 | element.set("value", l[1].strip()) 9 | return element 10 | -------------------------------------------------------------------------------- /Unite/GetElement/GetMITMElement.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def GetMITMElement(line): 5 | l = line.split("=", 1) 6 | element = ET.Element(l[0].replace(" ", "")) 7 | element.text = l[1].strip() 8 | return element 9 | -------------------------------------------------------------------------------- /Unite/GetElement/GetProxyElement.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def GetProxyElement(line): 5 | Info_Correspond = {"ss": ("type", "server", "port", "encrypt-method", "password", "obfs", "obfs-host", "tfo", "udp-relay"), 6 | "custom": ("type", "server", "port", "encrypt-method", "password", "module", "obfs", "obfs-host")} 7 | l = line.split("=", 1) 8 | if l[1].find(",") == -1: 9 | element = ET.Element("Built-in") 10 | element.set("name", l[0].strip()) 11 | element.set("policy", l[1].strip()) 12 | else: 13 | element = ET.Element("External") 14 | element.set("name", l[0].strip()) 15 | info = l[1].split(",") 16 | ProxyType = info[0].strip() 17 | for i in range(len(info)): 18 | if info[i].find("=") == -1: 19 | element.set(Info_Correspond[ProxyType] 20 | [i], info[i].strip()) 21 | else: 22 | key = info[i].split("=")[0].strip() 23 | value = info[i].split("=")[1].strip() 24 | element.set(key, value) 25 | return element 26 | -------------------------------------------------------------------------------- /Unite/GetElement/GetProxyGroupElement.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def GetProxyGroupElement(line): 5 | l = line.split("=", 1) 6 | values = l[1].split(",") 7 | element = ET.Element("policy") 8 | element.set("name", l[0].strip()) 9 | for i in range(len(values)): 10 | if i == 0: 11 | element.set("type", values[i].strip()) 12 | elif values[i].find("=") != -1: 13 | option = values[i].split("=", 1) 14 | if option[0].strip() == "policy-path": 15 | sub = ET.Element("policy-path") 16 | sub.text = option[1].strip() 17 | element.append(sub) 18 | else: 19 | element.set(option[0].strip(), option[1].strip()) 20 | else: 21 | sub = ET.Element("policy") 22 | sub.text = values[i].strip() 23 | element.append(sub) 24 | return element 25 | -------------------------------------------------------------------------------- /Unite/GetElement/GetReplicaElement.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def GetReplicaElement(line): 5 | l = line.split("=") 6 | element = ET.Element(l[0].replace(" ", "")) 7 | element.text = l[1].strip() 8 | return element 9 | -------------------------------------------------------------------------------- /Unite/GetElement/GetRuleElement.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def GetRuleElement(line, policy_name=None): 5 | l = line.split(",") 6 | element = ET.Element(l[0].replace(" ", "")) 7 | if element.tag == "FINAL": 8 | if policy_name == None: 9 | element.set("policy", l[1]) 10 | else: 11 | element.set("policy", policy_name) 12 | if "dns-failed" in l: 13 | element.set("dns-failed", "true") 14 | else: 15 | element.set("match", l[1].strip()) 16 | if policy_name == None: 17 | element.set("policy", l[2]) 18 | else: 19 | element.set("policy", policy_name) 20 | if "no-resolve" in l: 21 | element.set("no-resolve", "true") 22 | 23 | return element 24 | -------------------------------------------------------------------------------- /Unite/GetElement/GetURLRewriteElement.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def GetURLRewriteElement(line): 5 | l = line.split(" ", 3) 6 | element = ET.Element("Type_"+l[2]) 7 | element.set("type", l[2]) 8 | element.set("regex", l[0]) 9 | element.set("replace", l[1]) 10 | return element 11 | -------------------------------------------------------------------------------- /Unite/GetElement/__init__.py: -------------------------------------------------------------------------------- 1 | # from .GetGeneralElement import GetGeneralElement 2 | # from .GetHeaderRewriteElement import GetHeaderRewriteElement 3 | # from .GetHostElement import GetHostElement 4 | # from .GetMITMElement import GetMITMElement 5 | # from .GetProxyElement import GetProxyElement 6 | # from .GetProxyGroupElement import GetProxyGroupElement 7 | # from .GetReplicaElement import GetReplicaElement 8 | # from .GetRuleElement import GetRuleElement 9 | # from .GetURLRewriteElement import GetURLRewriteElement 10 | 11 | -------------------------------------------------------------------------------- /Unite/GetProxyGroupType.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | 4 | def ProxyGroupTypeDict(root): 5 | dic = dict() 6 | for elem in root.find("Proxy"): 7 | dic[elem.get("name")] = elem.tag 8 | for elem in root.findall("ProxyGroup/policy"): 9 | dic[elem.get("name")] = elem.get("type") 10 | return dic 11 | 12 | 13 | def GetProxyGroupType(root): 14 | dic = ProxyGroupTypeDict(root) 15 | for policy in root.findall("ProxyGroup/policy"): 16 | for elem in policy: 17 | elem.set("type", dic[elem.text]) 18 | return root 19 | -------------------------------------------------------------------------------- /Unite/Surge3LikeConfig2XML.py: -------------------------------------------------------------------------------- 1 | import xml.dom.minidom 2 | import xml.etree.ElementTree as ET 3 | 4 | from Unite.GetElement.GetGeneralElement import GetGeneralElement 5 | from Unite.GetElement.GetHeaderRewriteElement import GetHeaderRewriteElement 6 | from Unite.GetElement.GetHostElement import GetHostElement 7 | from Unite.GetElement.GetMITMElement import GetMITMElement 8 | from Unite.GetElement.GetProxyElement import GetProxyElement 9 | from Unite.GetElement.GetProxyGroupElement import GetProxyGroupElement 10 | from Unite.GetElement.GetReplicaElement import GetReplicaElement 11 | from Unite.GetElement.GetRuleElement import GetRuleElement 12 | from Unite.GetElement.GetURLRewriteElement import GetURLRewriteElement 13 | 14 | CommentKeywords = ("#", ";", "//") 15 | TypeKeywords = ("[General]", "[Replica]", "[Proxy]", "[Proxy Group]", "[Rule]", 16 | "[Host]", "[URL Rewrite]", "[Header Rewrite]", "[SSID Setting]", "[MITM]") 17 | 18 | 19 | def Content2XML(content): 20 | # f = open("OKAB3.conf", "r", encoding="utf-8") 21 | root = ET.Element("config") 22 | CurElement = root 23 | for line in content.splitlines(): 24 | TypeIndex = {"comment": "0", "General": "1", "Replica": "2", "Proxy": "3", "ProxyGroup": "4", "Rule": "5", 25 | "Host": "6", "URLRewrite": "7", "HeaderRewrite": "8", "SSIDSetting": "9", "MITM": "10"} 26 | line = line.strip("\n") 27 | # 类型关键词 28 | if line in TypeKeywords: 29 | line = line.strip("[") 30 | line = line.strip("]") 31 | line = line.replace(" ", "") 32 | sub = ET.Element(line) 33 | sub.set("index", TypeIndex[line]) 34 | root.append(sub) 35 | CurElement = root.find(line) 36 | # 备注 37 | elif line.startswith(CommentKeywords): 38 | temp = ET.Element("comment") 39 | temp.text = line 40 | temp.set("index", TypeIndex["comment"]) 41 | if not line.startswith("#!MANAGED-CONFIG"): 42 | CurElement.append(temp) 43 | # 排除空行或者只有空白符 44 | elif line != "" and not line.isspace(): 45 | if CurElement.tag == "General": 46 | CurElement.append(GetGeneralElement(line)) 47 | elif CurElement.tag == "Replica": 48 | CurElement.append(GetReplicaElement(line)) 49 | elif CurElement.tag == "Proxy": 50 | CurElement.append(GetProxyElement(line)) 51 | elif CurElement.tag == "ProxyGroup": 52 | CurElement.append(GetProxyGroupElement(line)) 53 | elif CurElement.tag == "Rule": 54 | CurElement.append(GetRuleElement(line)) 55 | elif CurElement.tag == "Host": 56 | CurElement.append(GetHostElement(line)) 57 | elif CurElement.tag == "URLRewrite": 58 | CurElement.append(GetURLRewriteElement(line)) 59 | elif CurElement.tag == "HeaderRewrite": 60 | CurElement.append(GetHeaderRewriteElement(line)) 61 | elif CurElement.tag == "MITM": 62 | CurElement.append(GetMITMElement(line)) 63 | # result = xml.dom.minidom.parseString( 64 | # ET.tostring(root)).toprettyxml() 65 | # open("Private_Demo.xml", "w", encoding="utf-8").write(result) 66 | return root 67 | 68 | # if __name__ == "__main__": 69 | # tree = ET.ElementTree(root) 70 | # # tree.write("test.xml", xml_declaration="true", encoding="utf-8") 71 | # result = xml.dom.minidom.parseString( 72 | # ET.tostring(tree.getroot())).toprettyxml() 73 | # open("Private_Demo.xml", "w", encoding="utf-8").write(result) 74 | -------------------------------------------------------------------------------- /Unite/__init__.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import xml.dom.minidom 3 | import xml.etree.ElementTree as ET 4 | 5 | import requests 6 | from flask import Request, make_response, request, Flask 7 | 8 | from Clash.ToClash import ToClash 9 | from Clash.ToClashV1 import ToClashV1 10 | from Clash.TopologicalSort import TopologicalSort 11 | from Emoji.emoji import EmojiParm, EmojiType, SSEmoji, SSREmoji, SurgeListEmoji 12 | from Expand.ExpandPolicyPath import ExpandPolicyPath 13 | from Expand.ExpandRuleSet import ExpandRuleSet 14 | from Filter.filter import SrugeListFilter, SSFilter, SSRFilter, SurgeConfFilter,QuanXListFilter 15 | from Surge3.ToSurge3 import ToSurge3 16 | from Unite.CheckPolicyPath import NeedExpandPolicyPath 17 | from Unite.GetProxyGroupType import GetProxyGroupType 18 | from Unite.Surge3LikeConfig2XML import Content2XML 19 | 20 | app = Flask(__name__) 21 | 22 | 23 | @app.route('/surge3', methods=['GET', 'POST']) 24 | def Surge3(): 25 | """ 26 | Args: 27 | request (flask.Request): HTTP request object. 28 | Return: 29 | A Surge3Pro-support configuration 30 | Do: 31 | Get 2 parameters: url & filename 32 | url: the url of the remote file 33 | filename: the file name of the configuration will be returned, default(no filename parameter in request) to Config.conf 34 | 35 | Function ExpandPolicyPath will be excuted only when 'Proxy Group' illegal format be exist 36 | Illegal format: a 'Proxy Group' only allow one policy when there is a policy-path 37 | """ 38 | url = request.args.get('url') 39 | filename = request.args.get("filename", "Config.conf") 40 | interval = request.args.get("interval", "86400") 41 | strict = request.args.get("strict", "false") 42 | content = requests.get(url).text 43 | result = "#!MANAGED-CONFIG https://api.OKAB3.com/surge3?url=" + url + \ 44 | "&filename="+filename+"&interval="+interval+"&strict=" + \ 45 | strict + " interval="+interval+" strict="+strict+"\n" 46 | x = Content2XML(content) 47 | x = ExpandPolicyPath(x) 48 | x = GetProxyGroupType(x) 49 | 50 | result += ToSurge3(x) 51 | 52 | response = make_response(result) 53 | response.headers["Content-Disposition"] = "attachment; filename="+filename 54 | return response 55 | 56 | 57 | @app.route('/clash', methods=['GET', 'POST']) 58 | def Clash(): 59 | url = request.args.get('url') 60 | filename = request.args.get("filename", "Config.yml") 61 | snippet = request.args.get("snippet") 62 | sort = request.args.get("sort", "True") 63 | url_text = requests.get(url).content.decode() 64 | x = Content2XML(url_text) 65 | x = ExpandPolicyPath(x) 66 | x = ExpandRuleSet(x) 67 | if(sort == "True"): 68 | x = TopologicalSort(x) 69 | 70 | result = ToClash(x, snippet) 71 | 72 | response = make_response(result) 73 | response.headers["Content-Disposition"] = "attachment; filename="+filename 74 | return response 75 | 76 | @app.route('/clash/v1', methods=['GET', 'POST']) 77 | def ClashV1(): 78 | url = request.args.get('url') 79 | filename = request.args.get("filename", "Config.yml") 80 | snippet = request.args.get("snippet") 81 | sort = request.args.get("sort", "True") 82 | url_text = requests.get(url).content.decode() 83 | x = Content2XML(url_text) 84 | x = ExpandPolicyPath(x) 85 | x = ExpandRuleSet(x) 86 | if(sort == "True"): 87 | x = TopologicalSort(x) 88 | 89 | result = ToClashV1(x, snippet) 90 | 91 | response = make_response(result) 92 | response.headers["Content-Disposition"] = "attachment; filename="+filename 93 | return response 94 | 95 | @app.route('/filter', methods=['GET', 'POST']) 96 | def Filter(): 97 | filter_type = str(request.args.get("type")) 98 | filter_type_lower = filter_type.lower() 99 | if filter_type_lower == "surgelist": 100 | return SrugeListFilter(request).filter_source() 101 | elif filter_type_lower == "qxlist": 102 | return QuanXListFilter(request).filter_source() 103 | elif filter_type_lower == "surgeconf": 104 | return SurgeConfFilter(request).filter_source() 105 | elif filter_type_lower == "ss": 106 | return SSFilter(request).filter_source() 107 | elif filter_type_lower == "ssr": 108 | return SSRFilter(request).filter_source() 109 | else: 110 | return "Illegal value for parameter type: "+filter_type+". Please see https://github.com/0KABE/ConfConvertor for details" 111 | 112 | 113 | @app.route('/emoji', methods=['GET', 'POST']) 114 | def Emoji(): 115 | source_type: EmojiType = EmojiType(request.args.get(EmojiParm.TYPE.value)) 116 | if source_type == EmojiType.SURGE_LIST: 117 | return SurgeListEmoji(request).convert() 118 | elif source_type == EmojiType.SS: 119 | return SSEmoji(request).convert() 120 | elif source_type == EmojiType.SSR: 121 | return SSREmoji(request).convert() 122 | 123 | 124 | if __name__ == '__main__': 125 | app.debug = False 126 | app.run(host='localhost', port=5000) 127 | -------------------------------------------------------------------------------- /rename.md: -------------------------------------------------------------------------------- 1 | # 使用Filter中的rename参数来自定义节点信息 2 | ## 原理 3 | >正则表达式(英语:Regular Expression,在代码中常简写为regex、regexp或RE),又称正规表示式、正规表示法、正规运算式、规则运算式、常规表示法,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。--https://zh.wikipedia.org/wiki/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F 4 | 5 | 简单来说,正则表达式能够用于表达相似字符串的集合,例如Surge的节点list中每一行都是相同的格式,因此我们能够使用正则表达式来处理它,甚至是对每一行的内容做出相同的更改。 6 | 7 | 为了能够对每一行的内容做出更改,我们需要在正则表达式中对每一个组起一个别名,我们在每个圆括号()中添加?P<别名>来对这个圆括号内的内容添加一个别名 8 | 9 | 对于下面的正则表达式: 10 | ``` 11 | (?P.*?) *-> *(?P.*?) *(?P\d*) *\| *(?PIPLC?)(?P.*=.*) 12 | ``` 13 | 依次对每个括号内匹配到的内容起了别名:Demestic, Area, No, ISP, Remain 14 | 15 | **需要注意的是,正则表达式将会对一行的所有数据进行匹配,因此,需要一个别名来代表一行中剩余的字符串内容,否则对于上面的正则,将会舍弃每一行IPLC后所有的内容,上方的正则中Remain别名的作用便是这个。** 16 | 17 | **以上对于接下来如果对list中每一行的内容自定义很重要,请务必理解。** 18 | 19 | 添加完别名后,就可以使用rename参数来对每一行的内容自定义了 20 | 对于这个rename参数: 21 | ``` 22 | Domestic 23 | _ 24 | Area 25 | + 26 | ISP 27 | 28 | No 29 | Remain 30 | ``` 31 | API将会从上到下取出Domestic Area ISP No Remain中匹配到的内容,重新拼接成一个新的字符串。 32 | **API将一行当作一个别名,如果遇到的不是在正则中定义的别名,则会直接附加在新字符串中** 33 | 34 | ## 注意事项 35 | * 不要忘记对参数url encode 36 | * 不要忘记匹配每一行的剩余内容,否则API将会直接舍弃 37 | 38 | ## 示例 39 | 本例将简单介绍如何对该list过滤节点的同时对节点自定义 40 | 如果你完全理解本脚本的运作方式,你会明白他能做的不仅仅是自定义节点名称,甚至能够更改每条中特定部分的字段,例如更改以下obfs-host的值,删除udp-replay=true等等 41 | **以下是范例中使用的原始待自定义的list:** 42 | ``` 43 | 🇨🇳 中国上海 - Back = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 44 | 🇨🇳 中国上海 -> 台湾 1 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 45 | 🇺🇸 中国上海 -> 美国 2 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 46 | 🇯🇵 中国上海 -> 日本 3 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 47 | 🇯🇵 中国上海 -> 日本 1 | IPLC = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 48 | 🇯🇵 中国上海 -> 日本 2 | IPLC = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 49 | 🇯🇵 中国上海 -> 日本 3 | IPLC = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 50 | 🇯🇵 中国上海 -> 日本 4 | IPLC = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 51 | 🇯🇵 中国上海 -> 日本 5 | IPLC = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 52 | 🇺🇸 中国上海 -> 美国 1 | IPLC = ss, xxx.xxx.xxx.xxx, 152, encrypt-method=chacha20-ietf-poly1305, password=UrTAdN, obfs=tls, obfs-host=8d9c317407.wns.windows.com, udp-relay=true 53 | 🇷🇺 中国北京 -> 俄罗斯 1 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 54 | 🇩🇪 中国北京 -> 德国 2 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 55 | 🇩🇪 中国北京 -> 德国 3 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 56 | 🇨🇳 中国徐州 - Back = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 57 | 🇭🇰 中国杭州 -> 香港 1 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 58 | 🇭🇰 中国深圳 -> 香港 1 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 59 | 🇭🇰 中国深圳 -> 香港 2 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 60 | 🇭🇰 中国深圳 -> 香港 3 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 61 | 🇲🇴 中国深圳 -> 澳门 4 | BGP = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 62 | 🇸🇬 中国深圳 -> 新加坡 1 | IPLC = ss, xxx.xxx.xxx.xxx, 152, encrypt-method=chacha20-ietf-poly1305, password=UrTAdN, obfs=tls, obfs-host=8d9c317407.wns.windows.com, udp-relay=true 63 | 🇭🇰 中国深圳 -> 香港 1 | IPLC = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 64 | 🇭🇰 中国深圳 -> 香港 2 | IPLC = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 65 | 🇭🇰 中国深圳 -> 香港 3 | IPLC = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 66 | 🇭🇰 中国深圳 -> 香港 4 | IPLC = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 67 | 🇭🇰 中国深圳 -> 香港 5 | IPLC = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 68 | 🇭🇰 中国深圳 -> 香港 6 | IPLC = ss, xxx.xxx.xxx.xxx, 152, encrypt-method=chacha20-ietf-poly1305, password=UrTAdN, obfs=tls, obfs-host=8d9c317407.wns.windows.com, udp-relay=true 69 | 🇨🇳 中国镇江 - Back = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 70 | ``` 71 | 72 | API Filter的参数 73 | url encode前 74 | ``` 75 | regex=(?P.*?) *-> *(?P.*?) *(?P\d*) *\| *(?PIPLC?)(?P.*=.*) 76 | rename=Domestic 77 | _ 78 | Area 79 | + 80 | ISP 81 | 82 | No 83 | Remain 84 | ``` 85 | url encode后 86 | ``` 87 | regex=%28%3FP%3CDomestic%3E.%2A%3F%29%20%2A-%3E%20%2A%28%3FP%3CArea%3E.%2A%3F%29%20%2A%28%3FP%3CNo%3E%5Cd%2A%29%20%2A%5C%7C%20%2A%28%3FP%3CISP%3EIPLC%3F%29%28%3FP%3CRemain%3E.%2A%3D.%2A%29 88 | rename=Domestic%0A%20_%20%0AArea%0A%20%2B%20%0AISP%0A%20%0ANo%0ARemain 89 | ``` 90 | 91 | 返回的内容 92 | ``` 93 | 🇯🇵 中国上海 _ 日本 + IPLC 1 = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 94 | 🇯🇵 中国上海 _ 日本 + IPLC 2 = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 95 | 🇯🇵 中国上海 _ 日本 + IPLC 3 = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 96 | 🇯🇵 中国上海 _ 日本 + IPLC 4 = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 97 | 🇯🇵 中国上海 _ 日本 + IPLC 5 = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 98 | 🇺🇸 中国上海 _ 美国 + IPLC 1 = ss, xxx.xxx.xxx.xxx, 152, encrypt-method=chacha20-ietf-poly1305, password=UrTAdN, obfs=tls, obfs-host=8d9c317407.wns.windows.com, udp-relay=true 99 | 🇸🇬 中国深圳 _ 新加坡 + IPLC 1 = ss, xxx.xxx.xxx.xxx, 152, encrypt-method=chacha20-ietf-poly1305, password=UrTAdN, obfs=tls, obfs-host=8d9c317407.wns.windows.com, udp-relay=true 100 | 🇭🇰 中国深圳 _ 香港 + IPLC 1 = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 101 | 🇭🇰 中国深圳 _ 香港 + IPLC 2 = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 102 | 🇭🇰 中国深圳 _ 香港 + IPLC 3 = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 103 | 🇭🇰 中国深圳 _ 香港 + IPLC 4 = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 104 | 🇭🇰 中国深圳 _ 香港 + IPLC 5 = ss, xxx.xxx.xxx.xxx, 37883, encrypt-method=xchacha20-ietf-poly1305, password=xxxxxxxxx, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 105 | 🇭🇰 中国深圳 _ 香港 + IPLC 6 = ss, xxx.xxx.xxx.xxx, 152, encrypt-method=chacha20-ietf-poly1305, password=UrTAdN, obfs=tls, obfs-host=8d9c317407.wns.windows.com, udp-relay=true 106 | ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | emoji 3 | pyyaml 4 | flask 5 | gunicorn 6 | supervisor 7 | requests -------------------------------------------------------------------------------- /supervisor.conf: -------------------------------------------------------------------------------- 1 | [program:ConfConvertor] ;项目名称 2 | directory = /mnt/d/github/ConfConvertor ; 程序的启动目录 3 | command = gunicorn -w 4 -b localhost:5000 main:app ; 启动命令,可以看出与手动在命令行启动的命令是一样 4 | autostart = true ; 在 supervisord 启动的时候也自动启动 5 | startsecs = 5 ; 启动 5 秒后没有异常退出,就当作已经正常启动了 6 | autorestart = true ; 程序异常退出后自动重启 7 | startretries = 3 ; 启动失败自动重试次数,默认是 3 8 | user = okab3 ; 用哪个用户启动 9 | redirect_stderr = true ; 把 stderr 重定向到 stdout,默认 false 10 | stdout_logfile_maxbytes = 50MB ; stdout 日志文件大小,默认 50MB 11 | stdout_logfile_backups = 20 ; stdout 日志文件备份数 12 | ; stdout 日志文件,需要注意当指定目录不存在时无法正常启动,所以需要手动创建目录(supervisord 会自动创建日志文件) 13 | stdout_logfile = /mnt/d/github/ConfConvertor/gunicorn.log 14 | loglevel=info --------------------------------------------------------------------------------