You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

GeositeUpdater.cs 14 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. using Splat;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Net.Http;
  8. using System.Threading.Tasks;
  9. using System.Security.Cryptography;
  10. using System.Text.Json;
  11. namespace Shadowsocks.PAC
  12. {
  13. public class GeositeResultEventArgs : EventArgs
  14. {
  15. public bool Success;
  16. public GeositeResultEventArgs(bool success)
  17. {
  18. Success = success;
  19. }
  20. }
  21. public class GeositeUpdater : IEnableLogger
  22. {
  23. public event EventHandler<GeositeResultEventArgs>? UpdateCompleted;
  24. private readonly string DATABASE_PATH;
  25. private readonly string GEOSITE_URL = "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat";
  26. private readonly string GEOSITE_SHA256SUM_URL = "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat.sha256sum";
  27. private byte[] geositeDB;
  28. public readonly Dictionary<string, IList<DomainObject>> Geosites = new Dictionary<string, IList<DomainObject>>();
  29. public GeositeUpdater(string dlcPath)
  30. {
  31. DATABASE_PATH = dlcPath;
  32. if (File.Exists(DATABASE_PATH) && new FileInfo(DATABASE_PATH).Length > 0)
  33. {
  34. geositeDB = File.ReadAllBytes(DATABASE_PATH);
  35. }
  36. else
  37. {
  38. geositeDB = Properties.Resources.dlc;
  39. File.WriteAllBytes(DATABASE_PATH, Properties.Resources.dlc);
  40. }
  41. LoadGeositeList();
  42. }
  43. /// <summary>
  44. /// load new GeoSite data from geositeDB
  45. /// </summary>
  46. private void LoadGeositeList()
  47. {
  48. var list = GeositeList.Parser.ParseFrom(geositeDB);
  49. foreach (var item in list.Entries)
  50. {
  51. Geosites[item.GroupName.ToLowerInvariant()] = item.Domains;
  52. }
  53. }
  54. public void ResetEvent()
  55. {
  56. UpdateCompleted = null;
  57. }
  58. public async Task UpdatePACFromGeosite(PACSettings pACSettings)
  59. {
  60. string geositeUrl = GEOSITE_URL;
  61. string geositeSha256sumUrl = GEOSITE_SHA256SUM_URL;
  62. SHA256 mySHA256 = SHA256.Create();
  63. bool blacklist = pACSettings.PACDefaultToDirect;
  64. var httpClient = Locator.Current.GetService<HttpClient>();
  65. if (!string.IsNullOrWhiteSpace(pACSettings.CustomGeositeUrl))
  66. {
  67. this.Log().Info("Found custom Geosite URL in config file");
  68. geositeUrl = pACSettings.CustomGeositeUrl;
  69. }
  70. this.Log().Info($"Checking Geosite from {geositeUrl}");
  71. try
  72. {
  73. // download checksum first
  74. var geositeSha256sum = await httpClient.GetStringAsync(geositeSha256sumUrl);
  75. geositeSha256sum = geositeSha256sum.Substring(0, 64).ToUpper();
  76. this.Log().Info($"Got Sha256sum: {geositeSha256sum}");
  77. // compare downloaded checksum with local geositeDB
  78. byte[] localDBHashBytes = mySHA256.ComputeHash(geositeDB);
  79. string localDBHash = BitConverter.ToString(localDBHashBytes).Replace("-", String.Empty);
  80. this.Log().Info($"Local Sha256sum: {localDBHash}");
  81. // if already latest
  82. if (geositeSha256sum == localDBHash)
  83. {
  84. this.Log().Info("Local GeoSite DB is up to date.");
  85. return;
  86. }
  87. // not latest. download new DB
  88. var downloadedBytes = await httpClient.GetByteArrayAsync(geositeUrl);
  89. // verify sha256sum
  90. byte[] downloadedDBHashBytes = mySHA256.ComputeHash(downloadedBytes);
  91. string downloadedDBHash = BitConverter.ToString(downloadedDBHashBytes).Replace("-", String.Empty);
  92. this.Log().Info($"Actual Sha256sum: {downloadedDBHash}");
  93. if (geositeSha256sum != downloadedDBHash)
  94. {
  95. this.Log().Info("Sha256sum Verification: FAILED. Downloaded GeoSite DB is corrupted. Aborting the update.");
  96. throw new Exception("Sha256sum mismatch");
  97. }
  98. else
  99. {
  100. this.Log().Info("Sha256sum Verification: PASSED. Applying to local GeoSite DB.");
  101. }
  102. // write to geosite file
  103. using (FileStream geositeFileStream = File.Create(DATABASE_PATH))
  104. await geositeFileStream.WriteAsync(downloadedBytes, 0, downloadedBytes.Length);
  105. // update stuff
  106. geositeDB = downloadedBytes;
  107. LoadGeositeList();
  108. bool pacFileChanged = MergeAndWritePACFile(pACSettings.GeositeDirectGroups, pACSettings.GeositeProxiedGroups, blacklist);
  109. UpdateCompleted?.Invoke(null, new GeositeResultEventArgs(pacFileChanged));
  110. }
  111. catch (Exception e)
  112. {
  113. this.Log().Error(e, "An error occurred while updating PAC.");
  114. }
  115. }
  116. /// <summary>
  117. /// Merge and write pac.txt from geosite.
  118. /// Used at multiple places.
  119. /// </summary>
  120. /// <param name="directGroups">A list of geosite groups configured for direct connection.</param>
  121. /// <param name="proxiedGroups">A list of geosite groups configured for proxied connection.</param>
  122. /// <param name="blacklist">Whether to use blacklist mode. False for whitelist.</param>
  123. /// <returns></returns>
  124. public bool MergeAndWritePACFile(List<string> directGroups, List<string> proxiedGroups, bool blacklist)
  125. {
  126. string abpContent = MergePACFile(directGroups, proxiedGroups, blacklist);
  127. if (File.Exists(PACDaemon.PAC_FILE))
  128. {
  129. string original = File.ReadAllText(PACDaemon.PAC_FILE);
  130. if (original == abpContent)
  131. {
  132. return false;
  133. }
  134. }
  135. File.WriteAllText(PACDaemon.PAC_FILE, abpContent, Encoding.UTF8);
  136. return true;
  137. }
  138. /// <summary>
  139. /// Checks if the specified group exists in GeoSite database.
  140. /// </summary>
  141. /// <param name="group">The group name to check for.</param>
  142. /// <returns>True if the group exists. False if the group doesn't exist.</returns>
  143. public bool CheckGeositeGroup(string group) => SeparateAttributeFromGroupName(group, out string groupName, out _) && Geosites.ContainsKey(groupName);
  144. /// <summary>
  145. /// Separates the attribute (e.g. @cn) from a group name.
  146. /// No checks are performed.
  147. /// </summary>
  148. /// <param name="group">A group name potentially with a trailing attribute.</param>
  149. /// <param name="groupName">The group name with the attribute stripped.</param>
  150. /// <param name="attribute">The attribute.</param>
  151. /// <returns>True for success. False for more than one '@'.</returns>
  152. private bool SeparateAttributeFromGroupName(string group, out string groupName, out string attribute)
  153. {
  154. var splitGroupAttributeList = group.Split('@');
  155. if (splitGroupAttributeList.Length == 1) // no attribute
  156. {
  157. groupName = splitGroupAttributeList[0];
  158. attribute = "";
  159. }
  160. else if (splitGroupAttributeList.Length == 2) // has attribute
  161. {
  162. groupName = splitGroupAttributeList[0];
  163. attribute = splitGroupAttributeList[1];
  164. }
  165. else
  166. {
  167. groupName = "";
  168. attribute = "";
  169. return false;
  170. }
  171. return true;
  172. }
  173. private string MergePACFile(List<string> directGroups, List<string> proxiedGroups, bool blacklist)
  174. {
  175. string abpContent;
  176. if (File.Exists(PACDaemon.USER_ABP_FILE))
  177. {
  178. abpContent = File.ReadAllText(PACDaemon.USER_ABP_FILE);
  179. }
  180. else
  181. {
  182. abpContent = Properties.Resources.abp;
  183. }
  184. List<string> userruleLines = new List<string>();
  185. if (File.Exists(PACDaemon.USER_RULE_FILE))
  186. {
  187. string userrulesString = File.ReadAllText(PACDaemon.USER_RULE_FILE);
  188. userruleLines = ProcessUserRules(userrulesString);
  189. }
  190. List<string> ruleLines = GenerateRules(directGroups, proxiedGroups, blacklist);
  191. var jsonSerializerOptions = new JsonSerializerOptions()
  192. {
  193. WriteIndented = true,
  194. };
  195. abpContent =
  196. $@"var __USERRULES__ = {JsonSerializer.Serialize(userruleLines, jsonSerializerOptions)};
  197. var __RULES__ = {JsonSerializer.Serialize(ruleLines, jsonSerializerOptions)};
  198. {abpContent}";
  199. return abpContent;
  200. }
  201. private List<string> ProcessUserRules(string content)
  202. {
  203. List<string> valid_lines = new List<string>();
  204. using (var stringReader = new StringReader(content))
  205. {
  206. for (string? line = stringReader.ReadLine(); line != null; line = stringReader.ReadLine())
  207. {
  208. if (string.IsNullOrWhiteSpace(line) || line.StartsWith("!") || line.StartsWith("["))
  209. continue;
  210. valid_lines.Add(line);
  211. }
  212. }
  213. return valid_lines;
  214. }
  215. /// <summary>
  216. /// Generates rule lines based on user preference.
  217. /// </summary>
  218. /// <param name="directGroups">A list of geosite groups configured for direct connection.</param>
  219. /// <param name="proxiedGroups">A list of geosite groups configured for proxied connection.</param>
  220. /// <param name="blacklist">Whether to use blacklist mode. False for whitelist.</param>
  221. /// <returns>A list of rule lines.</returns>
  222. private List<string> GenerateRules(List<string> directGroups, List<string> proxiedGroups, bool blacklist)
  223. {
  224. List<string> ruleLines;
  225. if (blacklist) // blocking + exception rules
  226. {
  227. ruleLines = GenerateBlockingRules(proxiedGroups);
  228. ruleLines.AddRange(GenerateExceptionRules(directGroups));
  229. }
  230. else // proxy all + exception rules
  231. {
  232. ruleLines = new List<string>()
  233. {
  234. "/.*/" // block/proxy all unmatched domains
  235. };
  236. ruleLines.AddRange(GenerateExceptionRules(directGroups));
  237. }
  238. return ruleLines;
  239. }
  240. /// <summary>
  241. /// Generates rules that match domains that should be proxied.
  242. /// </summary>
  243. /// <param name="groups">A list of source groups.</param>
  244. /// <returns>A list of rule lines.</returns>
  245. private List<string> GenerateBlockingRules(List<string> groups)
  246. {
  247. List<string> ruleLines = new List<string>();
  248. foreach (var group in groups)
  249. {
  250. // separate group name and attribute
  251. SeparateAttributeFromGroupName(group, out string groupName, out string attribute);
  252. var domainObjects = Geosites[groupName];
  253. if (!string.IsNullOrEmpty(attribute)) // has attribute
  254. {
  255. var attributeObject = new DomainObject.Types.Attribute
  256. {
  257. Key = attribute,
  258. BoolValue = true
  259. };
  260. foreach (var domainObject in domainObjects)
  261. {
  262. if (domainObject.Attribute.Contains(attributeObject))
  263. switch (domainObject.Type)
  264. {
  265. case DomainObject.Types.Type.Plain:
  266. ruleLines.Add(domainObject.Value);
  267. break;
  268. case DomainObject.Types.Type.Regex:
  269. ruleLines.Add($"/{domainObject.Value}/");
  270. break;
  271. case DomainObject.Types.Type.Domain:
  272. ruleLines.Add($"||{domainObject.Value}");
  273. break;
  274. case DomainObject.Types.Type.Full:
  275. ruleLines.Add($"|http://{domainObject.Value}");
  276. ruleLines.Add($"|https://{domainObject.Value}");
  277. break;
  278. }
  279. }
  280. }
  281. else // no attribute
  282. foreach (var domainObject in domainObjects)
  283. {
  284. switch (domainObject.Type)
  285. {
  286. case DomainObject.Types.Type.Plain:
  287. ruleLines.Add(domainObject.Value);
  288. break;
  289. case DomainObject.Types.Type.Regex:
  290. ruleLines.Add($"/{domainObject.Value}/");
  291. break;
  292. case DomainObject.Types.Type.Domain:
  293. ruleLines.Add($"||{domainObject.Value}");
  294. break;
  295. case DomainObject.Types.Type.Full:
  296. ruleLines.Add($"|http://{domainObject.Value}");
  297. ruleLines.Add($"|https://{domainObject.Value}");
  298. break;
  299. }
  300. }
  301. }
  302. return ruleLines;
  303. }
  304. /// <summary>
  305. /// Generates rules that match domains that should be connected directly without a proxy.
  306. /// </summary>
  307. /// <param name="groups">A list of source groups.</param>
  308. /// <returns>A list of rule lines.</returns>
  309. private List<string> GenerateExceptionRules(List<string> groups)
  310. => GenerateBlockingRules(groups)
  311. .Select(r => $"@@{r}") // convert blocking rules to exception rules
  312. .ToList();
  313. }
  314. }