Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

📐 PAC: add options for direct and proxied groups #2990

Merged
merged 2 commits into from
Oct 16, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 137 additions & 49 deletions shadowsocks-csharp/Controller/Service/GeositeUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,12 @@ public static class GeositeUpdater
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);
Expand Down Expand Up @@ -87,8 +81,7 @@ public static async Task UpdatePACFromGeosite()
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))
{
Expand Down Expand Up @@ -154,7 +147,7 @@ public static async Task UpdatePACFromGeosite()
// update stuff
geositeDB = downloadedBytes;
LoadGeositeList();
bool pacFileChanged = MergeAndWritePACFile(group, blacklist);
bool pacFileChanged = MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, blacklist);
UpdateCompleted?.Invoke(null, new GeositeResultEventArgs(pacFileChanged));
}
catch (Exception ex)
Expand All @@ -176,10 +169,17 @@ public static async Task UpdatePACFromGeosite()
}
}

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);
Expand All @@ -197,9 +197,39 @@ public static bool MergeAndWritePACFile(string group, bool blacklist)
/// </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> proxiedGroups, bool blacklist)
{
string abpContent;
if (File.Exists(PACDaemon.USER_ABP_FILE))
Expand All @@ -215,18 +245,18 @@ private static string MergePACFile(IList<DomainObject> domains, bool blacklist)
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(domains, blacklist);
List<string> ruleLines = GenerateRules(directGroups, proxiedGroups, blacklist);
abpContent =
$@"var __USERRULES__ = {JsonConvert.SerializeObject(userruleLines, Formatting.Indented)};
var __RULES__ = {JsonConvert.SerializeObject(gfwLines, Formatting.Indented)};
var __RULES__ = {JsonConvert.SerializeObject(ruleLines, 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))
Expand All @@ -241,47 +271,105 @@ private static List<string> PreProcessGFWList(string content)
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 groups)
{
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();
}
}
}
4 changes: 2 additions & 2 deletions shadowsocks-csharp/Controller/Service/PACDaemon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public string TouchPACFile()
{
if (!File.Exists(PAC_FILE))
{
GeositeUpdater.MergeAndWritePACFile(config.geositeGroup, config.geositeBlacklistMode);
GeositeUpdater.MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, config.geositePreferDirect);
}
return PAC_FILE;
}
Expand All @@ -63,7 +63,7 @@ internal string GetPACContent()
{
if (!File.Exists(PAC_FILE))
{
GeositeUpdater.MergeAndWritePACFile(config.geositeGroup, config.geositeBlacklistMode);
GeositeUpdater.MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, config.geositePreferDirect);
}
return File.ReadAllText(PAC_FILE, Encoding.UTF8);
}
Expand Down
2 changes: 1 addition & 1 deletion shadowsocks-csharp/Controller/ShadowsocksController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ private void PacServer_PACUpdateError(object sender, ErrorEventArgs e)
private static readonly IEnumerable<char> IgnoredLineBegins = new[] { '!', '[' };
private void PacDaemon_UserRuleFileChanged(object sender, EventArgs e)
{
GeositeUpdater.MergeAndWritePACFile(_config.geositeGroup, _config.geositeBlacklistMode);
GeositeUpdater.MergeAndWritePACFile(_config.geositeDirectGroups, _config.geositeProxiedGroups, _config.geositePreferDirect);
UpdateSystemProxy();
}

Expand Down
Loading