@@ -39,18 +39,12 @@ namespace Shadowsocks.Controller
private static HttpClient httpClient;
private static readonly string GEOSITE_URL = "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat";
private static readonly string GEOSITE_SHA256SUM_URL = "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat.sha256sum";
private static readonly DomainObject.Types.Attribute geositeExcludeAttribute;
private static byte[] geositeDB;
public static readonly Dictionary<string, IList<DomainObject>> Geosites = new Dictionary<string, IList<DomainObject>>();
static GeositeUpdater()
{
geositeExcludeAttribute = new DomainObject.Types.Attribute
{
Key = "cn",
BoolValue = true
};
if (File.Exists(DATABASE_PATH) && new FileInfo(DATABASE_PATH).Length > 0)
{
geositeDB = File.ReadAllBytes(DATABASE_PATH);
@@ -87,8 +81,7 @@ namespace Shadowsocks.Controller
string geositeSha256sumUrl = GEOSITE_SHA256SUM_URL;
SHA256 mySHA256 = SHA256.Create();
var config = Program.MainController.GetCurrentConfiguration();
string group = config.geositeGroup;
bool blacklist = config.geositeBlacklistMode;
bool blacklist = config.geositePreferDirect;
if (!string.IsNullOrWhiteSpace(config.geositeUrl))
{
@@ -154,7 +147,7 @@ namespace Shadowsocks.Controller
// update stuff
geositeDB = downloadedBytes;
LoadGeositeList();
bool pacFileChanged = MergeAndWritePACFile(group, blacklist);
bool pacFileChanged = MergeAndWritePACFile(confi g.geositeDirectG roups, config.geositeProxiedGroups , blacklist);
UpdateCompleted?.Invoke(null, new GeositeResultEventArgs(pacFileChanged));
}
catch (Exception ex)
@@ -176,10 +169,17 @@ namespace Shadowsocks.Controller
}
}
public static bool MergeAndWritePACFile(string group, bool blacklist)
/// <summary>
/// Merge and write pac.txt from geosite.
/// Used at multiple places.
/// </summary>
/// <param name="directGroups">A list of geosite groups configured for direct connection.</param>
/// <param name="proxiedGroups">A list of geosite groups configured for proxied connection.</param>
/// <param name="blacklist">Whether to use blacklist mode. False for whitelist.</param>
/// <returns></returns>
public static bool MergeAndWritePACFile(List<string> directGroups, List<string> proxiedGroups, bool blacklist)
{
IList<DomainObject> domains = Geosites[group];
string abpContent = MergePACFile(domains, blacklist);
string abpContent = MergePACFile(directGroups, proxiedGroups, blacklist);
if (File.Exists(PACDaemon.PAC_FILE))
{
string original = FileManager.NonExclusiveReadAllText(PACDaemon.PAC_FILE, Encoding.UTF8);
@@ -197,9 +197,39 @@ namespace Shadowsocks.Controller
/// </summary>
/// <param name="group">The group name to check for.</param>
/// <returns>True if the group exists. False if the group doesn't exist.</returns>
public static bool CheckGeositeGroup(string group) => Geosites.ContainsKey(group);
public static bool CheckGeositeGroup(string group) => SeparateAttributeFromGroupName(group, out string groupName, out _) && Geosites.ContainsKey(groupName);
/// <summary>
/// Separates the attribute (e.g. @cn) from a group name.
/// No checks are performed.
/// </summary>
/// <param name="group">A group name potentially with a trailing attribute.</param>
/// <param name="groupName">The group name with the attribute stripped.</param>
/// <param name="attribute">The attribute.</param>
/// <returns>True for success. False for more than one '@'.</returns>
private static bool SeparateAttributeFromGroupName(string group, out string groupName, out string attribute)
{
var splitGroupAttributeList = group.Split('@');
if (splitGroupAttributeList.Length == 1) // no attribute
{
groupName = splitGroupAttributeList[0];
attribute = "";
}
else if (splitGroupAttributeList.Length == 2) // has attribute
{
groupName = splitGroupAttributeList[0];
attribute = splitGroupAttributeList[1];
}
else
{
groupName = "";
attribute = "";
return false;
}
return true;
}
private static string MergePACFile(IList<DomainObject> domains, bool blacklist)
private static string MergePACFile(List<string> directGroups, List<string> proxiedGroup s, bool blacklist)
{
string abpContent;
if (File.Exists(PACDaemon.USER_ABP_FILE))
@@ -215,18 +245,18 @@ namespace Shadowsocks.Controller
if (File.Exists(PACDaemon.USER_RULE_FILE))
{
string userrulesString = FileManager.NonExclusiveReadAllText(PACDaemon.USER_RULE_FILE, Encoding.UTF8);
userruleLines = PreProcessGFWList (userrulesString);
userruleLines = ProcessUserRules (userrulesString);
}
List<string> gfwLines = GeositeToGFWList(domain s, blacklist);
List<string> ruleLines = GenerateRules(directGroups, proxiedGroup s, blacklist);
abpContent =
$@"var __USERRULES__ = {JsonConvert.SerializeObject(userruleLines, Formatting.Indented)};
var __RULES__ = {JsonConvert.SerializeObject(gfw Lines, Formatting.Indented)};
var __RULES__ = {JsonConvert.SerializeObject(rule Lines, Formatting.Indented)};
{abpContent}";
return abpContent;
}
private static List<string> PreProcessGFWList (string content)
private static List<string> ProcessUserRules (string content)
{
List<string> valid_lines = new List<string>();
using (var stringReader = new StringReader(content))
@@ -241,47 +271,105 @@ var __RULES__ = {JsonConvert.SerializeObject(gfwLines, Formatting.Indented)};
return valid_lines;
}
private static List<string> GeositeToGFWList(IList<DomainObject> domains, bool blacklist)
/// <summary>
/// Generates rule lines based on user preference.
/// </summary>
/// <param name="directGroups">A list of geosite groups configured for direct connection.</param>
/// <param name="proxiedGroups">A list of geosite groups configured for proxied connection.</param>
/// <param name="blacklist">Whether to use blacklist mode. False for whitelist.</param>
/// <returns>A list of rule lines.</returns>
private static List<string> GenerateRules(List<string> directGroups, List<string> proxiedGroups, bool blacklist)
{
return blacklist ? GeositeToGFWListBlack(domains) : GeositeToGFWListWhite(domains);
List<string> ruleLines;
if (blacklist) // blocking + exception rules
{
ruleLines = GenerateBlockingRules(proxiedGroups);
ruleLines.AddRange(GenerateExceptionRules(directGroups));
}
else // proxy all + exception rules
{
ruleLines = new List<string>()
{
"/.*/" // block/proxy all unmatched domains
};
ruleLines.AddRange(GenerateExceptionRules(directGroups));
}
return ruleLines;
}
private static List<string> GeositeToGFWListBlack(IList<DomainObject> domains)
/// <summary>
/// Generates rules that match domains that should be proxied.
/// </summary>
/// <param name="groups">A list of source groups.</param>
/// <returns>A list of rule lines.</returns>
private static List<string> GenerateBlockingRules(List<string> groups)
{
List<string> ret = new List<string>(domains.Count + 100);// 100 overhead
foreach (var d in domains)
List<string> ruleLines = new List<string>();
foreach (var group in group s)
{
if (d.Attribute.Contains(geositeExcludeAttribute))
continue;
string domain = d.Value;
switch (d.Type)
// separate group name and attribute
SeparateAttributeFromGroupName(group, out string groupName, out string attribute);
var domainObjects = Geosites[groupName];
if (!string.IsNullOrEmpty(attribute)) // has attribute
{
case DomainObject.Types.Type.Plain:
ret.Add(domain);
break;
case DomainObject.Types.Type.Regex:
ret.Add($"/{domain}/");
break;
case DomainObject.Types.Type.Domain:
ret.Add($"||{domain}");
break;
case DomainObject.Types.Type.Full:
ret.Add($"|http://{domain}");
ret.Add($"|https://{domain}");
break;
var attributeObject = new DomainObject.Types.Attribute
{
Key = attribute,
BoolValue = true
};
foreach (var domainObject in domainObjects)
{
if (domainObject.Attribute.Contains(attributeObject))
switch (domainObject.Type)
{
case DomainObject.Types.Type.Plain:
ruleLines.Add(domainObject.Value);
break;
case DomainObject.Types.Type.Regex:
ruleLines.Add($"/{domainObject.Value}/");
break;
case DomainObject.Types.Type.Domain:
ruleLines.Add($"||{domainObject.Value}");
break;
case DomainObject.Types.Type.Full:
ruleLines.Add($"|http://{domainObject.Value}");
ruleLines.Add($"|https://{domainObject.Value}");
break;
}
}
}
else // no attribute
foreach (var domainObject in domainObjects)
{
switch (domainObject.Type)
{
case DomainObject.Types.Type.Plain:
ruleLines.Add(domainObject.Value);
break;
case DomainObject.Types.Type.Regex:
ruleLines.Add($"/{domainObject.Value}/");
break;
case DomainObject.Types.Type.Domain:
ruleLines.Add($"||{domainObject.Value}");
break;
case DomainObject.Types.Type.Full:
ruleLines.Add($"|http://{domainObject.Value}");
ruleLines.Add($"|https://{domainObject.Value}");
break;
}
}
}
return ret;
return ruleLines ;
}
private static List<string> GeositeToGFWListWhite(IList<DomainObject> domains)
{
return GeositeToGFWListBlack(domains)
.Select(r => $"@@{r}") // convert to whitelist
.Prepend("/.*/") // blacklist all other site
/// <summary>
/// Generates rules that match domains that should be connected directly without a proxy.
/// </summary>
/// <param name="groups">A list of source groups.</param>
/// <returns>A list of rule lines.</returns>
private static List<string> GenerateExceptionRules(List<string> groups)
=> GenerateBlockingRules(groups)
.Select(r => $"@@{r}") // convert blocking rules to exception rules
.ToList();
}
}
}