Add support for exporting complete Quantumult configuration file

Fix incorrect generation of Quantumult X configuratios.
Fix broken group name option.
Add external configuration support for customize Quantumult(X) base.
Update Travis CI build script.
Tweak Emoji match rules.
Clean up codes.
This commit is contained in:
Tindy X
2020-01-31 19:30:04 +08:00
parent c5fa786500
commit b085c0d0c1
8 changed files with 226 additions and 51 deletions

View File

@@ -1,11 +1,11 @@
language: cpp
sudo: required
os: linux
stages:
- name: deploy
if: branch = master
- name: before_script
if: branch = master
matrix:
jobs:
include:
- name: "macOS Build"
os: osx
@@ -15,9 +15,8 @@ matrix:
- bash scripts/build.macos.release.sh
deploy:
provider: releases
api_key: "$GITHUB_OAUTH_TOKEN"
token: "$GITHUB_OAUTH_TOKEN"
file: "subconverter_darwin64.tar.gz"
skip_cleanup: true
draft: true
on:
tags: true
@@ -29,9 +28,8 @@ matrix:
- mv subconverter_linux64.tar.gz subconverter_linux32.tar.gz
deploy:
provider: releases
api_key: "$GITHUB_OAUTH_TOKEN"
token: "$GITHUB_OAUTH_TOKEN"
file: "subconverter_linux32.tar.gz"
skip_cleanup: true
draft: true
on:
tags: true
@@ -41,9 +39,8 @@ matrix:
- docker run -v $TRAVIS_BUILD_DIR:/root/workdir alpine:latest /bin/sh -c "apk add bash git && cd /root/workdir && chmod +x scripts/build.alpine.release.sh && bash scripts/build.alpine.release.sh"
deploy:
provider: releases
api_key: "$GITHUB_OAUTH_TOKEN"
token: "$GITHUB_OAUTH_TOKEN"
file: "subconverter_linux64.tar.gz"
skip_cleanup: true
draft: true
on:
tags: true
@@ -56,9 +53,8 @@ matrix:
- mv subconverter_linux64.tar.gz subconverter_armhf.tar.gz
deploy:
provider: releases
api_key: "$GITHUB_OAUTH_TOKEN"
token: "$GITHUB_OAUTH_TOKEN"
file: "subconverter_armhf.tar.gz"
skip_cleanup: true
draft: true
on:
tags: true
@@ -71,9 +67,8 @@ matrix:
- mv subconverter_linux64.tar.gz subconverter_aarch64.tar.gz
deploy:
provider: releases
api_key: "$GITHUB_OAUTH_TOKEN"
token: "$GITHUB_OAUTH_TOKEN"
file: "subconverter_aarch64.tar.gz"
skip_cleanup: true
draft: true
on:
tags: true

View File

@@ -40,6 +40,8 @@ clash_rule_base=config/forcerule.yml
;surge_rule_base=surge.conf
;surfboard_rule_base=surfboard.conf
;mellow_rule_base=mellow.conf
;quan_rule_base=quan.conf
;quanx_rule_base=quanx.conf
;Options for renaming nodes
;rename=Test-(.*?)-(.*?)-(.*?)\((.*?)\)@\1\4x测试线路_自\2到\3

View File

@@ -27,6 +27,9 @@ surfboard_rule_base=surfboard.conf
;Mellow config base used by the generator, supports local files/URL
mellow_rule_base=mellow.conf
;Quantumult X config base used by the generator, supports local files/URL
quan_rule_base=quan.conf
;Quantumult X config base used by the generator, supports local files/URL
quanx_rule_base=quanx.conf
@@ -146,8 +149,8 @@ rule=ES,🇪🇸
rule=EU,🇪🇺
rule=(Finland|芬兰|赫尔辛基),🇫🇮
rule=(FR|France|法国|巴黎),🇫🇷
rule=(UK|England|UnitedKingdom|英国|英|伦敦),🇬🇧
rule=(HK|HongKong|香港|深港|沪港|呼港|HKT|HKBN|HGC|WTT|CMI|穗港|京港|港),🇭🇰
rule=(UK|England|United.*?Kingdom|英国|英|伦敦),🇬🇧
rule=(?i)(HK|Hong.*?Kong|香港|深港|沪港|呼港|HKT|HKBN|HGC|WTT|CMI|穗港|京港|港),🇭🇰
rule=(Indonesia|印尼|印度尼西亚|雅加达),🇮🇩
rule=(Ireland|爱尔兰|都柏林),🇮🇪
rule=(India|印度|孟买),🇮🇳

27
base/quan.conf Normal file
View File

@@ -0,0 +1,27 @@
[SERVER]
[SOURCE]
[BACKUP-SERVER]
[SUSPEND-SSID]
[POLICY]
[DNS]
1.1.1.1
[REWRITE]
[URL-REJECTION]
[TCP]
[GLOBAL]
[HOST]
[STATE]
STATE,AUTO
[MITM]

View File

@@ -40,7 +40,7 @@ std::string proxy_ruleset, proxy_subscription;
std::string clash_rule_base;
string_array clash_extra_group;
std::string surge_rule_base, surfboard_rule_base, mellow_rule_base, quanx_rule_base;
std::string surge_rule_base, surfboard_rule_base, mellow_rule_base, quan_rule_base, quanx_rule_base;
std::string surge_ssr_path;
//pre-compiled rule bases
@@ -172,6 +172,8 @@ void readConf()
surfboard_rule_base = ini.Get("surfboard_rule_base");
if(ini.ItemExist("mellow_rule_base"))
mellow_rule_base = ini.Get("mellow_rule_base");
if(ini.ItemExist("quan_rule_base"))
quan_rule_base = ini.Get("quan_rule_base");
if(ini.ItemExist("quanx_rule_base"))
quanx_rule_base = ini.Get("quanx_rule_base");
if(ini.ItemExist("append_proxy_type"))
@@ -289,6 +291,8 @@ struct ExternalConfig
std::string surge_rule_base;
std::string surfboard_rule_base;
std::string mellow_rule_base;
std::string quan_rule_base;
std::string quanx_rule_base;
string_array rename;
string_array emoji;
bool overwrite_original_rules = false;
@@ -305,7 +309,6 @@ int loadExternalConfig(std::string &path, ExternalConfig &ext, std::string proxy
INIReader ini;
ini.store_isolated_line = true;
ini.keep_empty_section = false;
ini.SetIsolatedItemsSection("custom");
if(ini.Parse(base_content) != INIREADER_EXCEPTION_NONE)
{
@@ -327,6 +330,10 @@ int loadExternalConfig(std::string &path, ExternalConfig &ext, std::string proxy
ext.surfboard_rule_base = ini.Get("surfboard_rule_base");
if(ini.ItemExist("mellow_rule_base"))
ext.mellow_rule_base = ini.Get("mellow_rule_base");
if(ini.ItemExist("quan_rule_base"))
ext.quan_rule_base = ini.Get("quan_rule_base");
if(ini.ItemExist("quanx_rule_base"))
ext.quanx_rule_base = ini.Get("quanx_rule_base");
if(ini.ItemExist("overwrite_original_rules"))
ext.overwrite_original_rules = ini.GetBool("overwrite_original_rules");
@@ -390,6 +397,7 @@ std::string subconverter(RESPONSE_CALLBACK_ARGS)
//for external configuration
std::string ext_clash_base = clash_rule_base, ext_surge_base = surge_rule_base, ext_mellow_base = mellow_rule_base, ext_surfboard_base = surfboard_rule_base;
std::string ext_quan_base = quan_rule_base, ext_quanx_base = quanx_rule_base;
//validate urls
if(!url.size())
@@ -434,6 +442,10 @@ std::string subconverter(RESPONSE_CALLBACK_ARGS)
ext_surfboard_base = extconf.surfboard_rule_base;
if(extconf.mellow_rule_base.size())
ext_mellow_base = extconf.mellow_rule_base;
if(extconf.quan_rule_base.size())
ext_quan_base = extconf.quan_rule_base;
if(extconf.quanx_rule_base.size())
ext_quanx_base = extconf.quanx_rule_base;
if(extconf.rename.size())
ext.rename_array = extconf.rename;
if(extconf.emoji.size())
@@ -530,10 +542,6 @@ std::string subconverter(RESPONSE_CALLBACK_ARGS)
else
exclude_remarks = def_exclude_remarks;
//check custom group name
if(group.size())
custom_group = group;
//start parsing urls
string_array stream_temp = safe_get_streams(), time_temp = safe_get_times();
for(std::string &x : urls)
@@ -554,6 +562,11 @@ std::string subconverter(RESPONSE_CALLBACK_ARGS)
return "No nodes were found!";
}
//check custom group name
if(group.size())
for(nodeInfo &x : nodes)
x.group = group;
if(subInfo.size() && groupID == 1)
extra_headers.emplace("Subscription-UserInfo", subInfo);
@@ -667,26 +680,34 @@ std::string subconverter(RESPONSE_CALLBACK_ARGS)
else if(target == "quan")
{
std::cerr<<"Quantumult"<<std::endl;
output_content = netchToQuan(nodes, ext);
if(!ext.nodelist)
{
if(fileExist(ext_quan_base))
base_content = fileGet(ext_quan_base, false);
else
base_content = webGet(ext_quan_base, getSystemProxy());
}
output_content = netchToQuan(nodes, base_content, rca, extra_group, ext);
if(upload == "true")
uploadGist("quan", upload_path, output_content, false);
}
else if(target == "quanx")
{
std::cerr<<"Quantumult X"<<std::endl;
if(fileExist(quanx_rule_base))
base_content = fileGet(quanx_rule_base, false);
if(!ext.nodelist)
{
if(fileExist(ext_quanx_base))
base_content = fileGet(ext_quanx_base, false);
else
base_content = webGet(quanx_rule_base, getSystemProxy());
base_content = webGet(ext_quanx_base, getSystemProxy());
}
output_content = netchToQuanX(nodes, base_content, rca, extra_group, ext);
if(upload == "true")
uploadGist("quanx", upload_path, output_content, false);
if(write_managed_config && managed_config_prefix.size())
output_content = "#!MANAGED-CONFIG " + managed_config_prefix + "/sub?" + argument + "\n\n" + output_content;
}
else if(target == "ssd")
{

View File

@@ -9,7 +9,7 @@
#include "webget.h"
#include "speedtestutil.h"
std::string override_conf_port, custom_group;
std::string override_conf_port;
int socksport;
bool ss_libev, ssr_libev;
extern bool api_mode;
@@ -107,8 +107,6 @@ int addNodes(std::string link, std::vector<nodeInfo> &allNodes, int groupID, std
if(linkType > 0)
{
explode(link, ss_libev, ssr_libev, override_conf_port, socksport, node);
if(custom_group.size() != 0)
node.group = custom_group;
if(node.linkType == -1)
{
writeLog(LOG_TYPE_ERROR, "No valid link found.");

View File

@@ -363,6 +363,9 @@ void rulesetToSurge(INIReader &base_rule, std::vector<ruleset_content> &ruleset_
case -1:
base_rule.SetCurrentSection("filter_local"); //Quantumult X
break;
case -2:
base_rule.SetCurrentSection("TCP"); //Quantumult
break;
default:
base_rule.SetCurrentSection("Rule");
}
@@ -382,7 +385,7 @@ void rulesetToSurge(INIReader &base_rule, std::vector<ruleset_content> &ruleset_
if(strLine == "MATCH")
strLine = "FINAL";
strLine += "," + rule_group;
if(surge_ver == -1)
if(surge_ver == -1 || surge_ver == -2)
{
if(std::count(strLine.begin(), strLine.end(), ',') > 2 && regReplace(strLine, rule_match_regex, "$2") == ",no-resolve")
strLine = regReplace(strLine, rule_match_regex, "$1$3$2");
@@ -416,13 +419,13 @@ void rulesetToSurge(INIReader &base_rule, std::vector<ruleset_content> &ruleset_
strStrm<<retrived_rules;
while(getline(strStrm, strLine, delimiter))
{
if(surge_ver == -1 && (strLine.find("IP-CIDR6") == 0 || strLine.find("URL-REGEX") == 0 || strLine.find("PROCESS-NAME") == 0 || strLine.find("AND") == 0 || strLine.find("OR") == 0)) //remove unsupported types
if((surge_ver == -1 || surge_ver == -2) && (strLine.find("IP-CIDR6") == 0 || strLine.find("URL-REGEX") == 0 || strLine.find("PROCESS-NAME") == 0 || strLine.find("AND") == 0 || strLine.find("OR") == 0)) //remove unsupported types
continue;
strLine = replace_all_distinct(strLine, "\r", ""); //remove line break
if(!strLine.size() || strLine.find("#") == 0 || strLine.find(";") == 0) //remove comments
continue;
strLine += "," + rule_group;
if(surge_ver == -1)
if(surge_ver == -1 || surge_ver == -2)
{
if(std::count(strLine.begin(), strLine.end(), ',') > 2 && regReplace(strLine, rule_match_regex, "$2") == ",no-resolve")
strLine = regReplace(strLine, rule_match_regex, "$1$3$2");
@@ -1246,7 +1249,29 @@ std::string netchToVMess(std::vector<nodeInfo> &nodes, extra_settings &ext)
return base64_encode(allLinks);
}
std::string netchToQuan(std::vector<nodeInfo> &nodes, extra_settings &ext)
std::string netchToQuan(std::vector<nodeInfo> &nodes, std::string &base_conf, std::vector<ruleset_content> &ruleset_content_array, string_array &extra_proxy_group, extra_settings &ext)
{
INIReader ini;
ini.store_any_line = true;
if(!ext.nodelist && ini.Parse(base_conf) != 0)
return std::string();
netchToQuan(nodes, ini, ruleset_content_array, extra_proxy_group, ext);
if(ext.nodelist)
{
string_array allnodes;
ini.GetAll("SERVER", "{NONAME}", allnodes);
std::string allLinks = std::accumulate(allnodes.begin(), allnodes.end(), allnodes[0], [](std::string a, std::string b)
{
return std::move(a) + "\n" + std::move(b);
});
return base64_encode(allLinks);
}
return ini.ToString();
}
void netchToQuan(std::vector<nodeInfo> &nodes, INIReader &ini, std::vector<ruleset_content> &ruleset_content_array, string_array &extra_proxy_group, extra_settings &ext)
{
rapidjson::Document json;
std::string type;
@@ -1256,6 +1281,7 @@ std::string netchToQuan(std::vector<nodeInfo> &nodes, extra_settings &ext)
std::string id, aid, transproto, faketype, host, path, quicsecure, quicsecret;
std::string proxyStr, allLinks;
bool tlssecure;
std::vector<nodeInfo> nodelist;
std::for_each(nodes.begin(), nodes.end(), [ext](nodeInfo &x)
{
@@ -1275,6 +1301,8 @@ std::string netchToQuan(std::vector<nodeInfo> &nodes, extra_settings &ext)
});
}
ini.SetCurrentSection("SERVER");
ini.EraseSection();
for(nodeInfo &x : nodes)
{
json.Parse(x.proxyStr.data());
@@ -1309,6 +1337,8 @@ std::string netchToQuan(std::vector<nodeInfo> &nodes, extra_settings &ext)
proxyStr += ", certificate=0";
if(transproto == "ws")
proxyStr += ", obfs=ws, obfs-path=\"" + path + "\", obfs-header=\"Host: " + host + "\"";
if(ext.nodelist)
proxyStr = "vmess://" + urlsafe_base64_encode(proxyStr);
break;
case SPEEDTEST_MESSAGE_FOUNDSSR:
@@ -1317,27 +1347,121 @@ std::string netchToQuan(std::vector<nodeInfo> &nodes, extra_settings &ext)
obfs = GetMember(json, "OBFS");
obfsparam = GetMember(json, "OBFSParam");
if(ext.nodelist)
{
proxyStr = "ssr://" + urlsafe_base64_encode(hostname + ":" + port + ":" + protocol + ":" + method + ":" + obfs + ":" + urlsafe_base64_encode(password) \
+ "/?group=" + urlsafe_base64_encode(x.group) + "&remarks=" + urlsafe_base64_encode(remark) \
+ "&obfsparam=" + urlsafe_base64_encode(obfsparam) + "&protoparam=" + urlsafe_base64_encode(protoparam));
}
else
{
proxyStr = remark + " = shadowsocksr, " + hostname + ", " + port + ", " + method + ", \"" + password + "\", group=" + x.group + ", protocol=" + protocol + ", obfs=" + obfs;
if(protoparam.size())
proxyStr += ", protocol_param=" + protoparam;
if(obfsparam.size())
proxyStr += ", obfs_param=" + obfsparam;
}
break;
case SPEEDTEST_MESSAGE_FOUNDSS:
plugin = GetMember(json, "Plugin");
pluginopts = GetMember(json, "PluginOption");
if(ext.nodelist)
{
proxyStr = "ss://" + urlsafe_base64_encode(method + ":" + password) + "@" + hostname + ":" + port;
if(plugin.size() & pluginopts.size())
if(plugin.size() && pluginopts.size())
{
proxyStr += "/?plugin=" + UrlEncode(plugin + ";" + pluginopts);
}
proxyStr += "&group=" + urlsafe_base64_encode(x.group) + "#" + UrlEncode(remark);
}
else
{
proxyStr = remark + " = shadowsocks, " + hostname + ", " + port + ", " + method + ", \"" + password + "\", group=" + x.group;
if(plugin == "simple-obfs" && pluginopts.size())
{
proxyStr += ", " + replace_all_distinct(pluginopts, ";", ", ");
}
}
break;
default:
continue;
}
allLinks += proxyStr + "\n";
ini.Set("{NONAME}", proxyStr);
nodelist.emplace_back(x);
}
return base64_encode(allLinks);
if(ext.nodelist)
return;
string_array filtered_nodelist;
ini.SetCurrentSection("POLICY");
ini.EraseSection();
std::string singlegroup;
std::string name, proxies;
string_array vArray;
for(std::string &x : extra_proxy_group)
{
eraseElements(filtered_nodelist);
unsigned int rules_upper_bound = 0;
vArray = split(x, "`");
if(vArray.size() < 3)
continue;
if(vArray[1] == "select")
{
type = "static";
rules_upper_bound = vArray.size();
}
else if(vArray[1] == "url-test")
{
if(vArray.size() < 5)
continue;
type = "auto";
rules_upper_bound = vArray.size() - 2;
}
else if(vArray[1] == "fallback")
{
if(vArray.size() < 5)
continue;
type = "static";
rules_upper_bound = vArray.size() - 2;
}
else if(vArray[1] == "load-balance")
{
if(vArray.size() < 5)
continue;
type = "balance, round-robin";
rules_upper_bound = vArray.size() - 2;
}
else
continue;
name = vArray[0];
for(unsigned int i = 2; i < rules_upper_bound; i++)
groupGenerate(vArray[i], nodelist, filtered_nodelist, true);
if(!filtered_nodelist.size())
filtered_nodelist.emplace_back("direct");
proxies = std::accumulate(std::next(filtered_nodelist.begin()), filtered_nodelist.end(), filtered_nodelist[0], [](std::string a, std::string b)
{
return std::move(a) + "\n" + std::move(b);
});
singlegroup = name + " : " + type;
if(type == "static")
singlegroup += ", " + filtered_nodelist[0];
singlegroup += "\n" + proxies + "\n";
ini.Set("{NONAME}", base64_encode(singlegroup));
}
if(ext.enable_rule_generator)
rulesetToSurge(ini, ruleset_content_array, -2, ext.overwrite_original_rules);
}
std::string netchToQuanX(std::vector<nodeInfo> &nodes, std::string &base_conf, std::vector<ruleset_content> &ruleset_content_array, string_array &extra_proxy_group, extra_settings &ext)
@@ -1349,7 +1473,7 @@ std::string netchToQuanX(std::vector<nodeInfo> &nodes, std::string &base_conf, s
netchToQuanX(nodes, ini, ruleset_content_array, extra_proxy_group, ext);
if(!ext.nodelist)
if(ext.nodelist)
{
string_array allnodes;
ini.GetAll("server_local", "{NONAME}", allnodes);
@@ -1419,11 +1543,15 @@ void netchToQuanX(std::vector<nodeInfo> &nodes, INIReader &ini, std::vector<rule
method = "chacha20-ietf-poly1305";
proxyStr = "vmess = " + hostname + ":" + port + ", method=" + method + ", password=" + id;
if(transproto == "ws")
proxyStr += ", obfs=ws, obfs-host=" + host + ", obfs-uri=" + path;
{
if(tlssecure)
proxyStr += ", obfs=wss";
else
proxyStr += ", obfs=ws";
proxyStr += ", obfs-host=" + host + ", obfs-uri=" + path;
}
else if(tlssecure)
proxyStr += ", obfs=over-tls, obfs-host=" + host;
if(ext.skip_cert_verify)
proxyStr += ", certificate=0";
break;
case SPEEDTEST_MESSAGE_FOUNDSS:
password = GetMember(json, "Password");

View File

@@ -41,7 +41,8 @@ std::string netchToSSR(std::vector<nodeInfo> &nodes, extra_settings &ext);
std::string netchToVMess(std::vector<nodeInfo> &nodes, extra_settings &ext);
std::string netchToQuanX(std::vector<nodeInfo> &nodes, std::string &base_conf, std::vector<ruleset_content> &ruleset_content_array, string_array &extra_proxy_group, extra_settings &ext);
void netchToQuanX(std::vector<nodeInfo> &nodes, INIReader &ini, std::vector<ruleset_content> &ruleset_content_array, string_array &extra_proxy_group, extra_settings &ext);
std::string netchToQuan(std::vector<nodeInfo> &nodes, extra_settings &ext);
std::string netchToQuan(std::vector<nodeInfo> &nodes, std::string &base_conf, std::vector<ruleset_content> &ruleset_content_array, string_array &extra_proxy_group, extra_settings &ext);
void netchToQuan(std::vector<nodeInfo> &nodes, INIReader &ini, std::vector<ruleset_content> &ruleset_content_array, string_array &extra_proxy_group, extra_settings &ext);
std::string netchToSSD(std::vector<nodeInfo> &nodes, std::string &group, extra_settings &ext);
std::string buildGistData(std::string name, std::string content);
int uploadGist(std::string name, std::string path, std::string content, bool writeManageURL);