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 10 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. using NLog;
  2. using Shadowsocks.Properties;
  3. using Shadowsocks.Util;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Text;
  9. using Newtonsoft.Json;
  10. using Shadowsocks.Model;
  11. using System.Net;
  12. using System.Net.Http;
  13. using System.Threading.Tasks;
  14. using System.Security.Cryptography;
  15. namespace Shadowsocks.Controller
  16. {
  17. public class GeositeResultEventArgs : EventArgs
  18. {
  19. public bool Success;
  20. public GeositeResultEventArgs(bool success)
  21. {
  22. this.Success = success;
  23. }
  24. }
  25. public static class GeositeUpdater
  26. {
  27. private static Logger logger = LogManager.GetCurrentClassLogger();
  28. public static event EventHandler<GeositeResultEventArgs> UpdateCompleted;
  29. public static event ErrorEventHandler Error;
  30. private static readonly string DATABASE_PATH = Utils.GetTempPath("dlc.dat");
  31. private static HttpClientHandler httpClientHandler;
  32. private static HttpClient httpClient;
  33. private static readonly string GEOSITE_URL = "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat";
  34. private static readonly string GEOSITE_SHA256SUM_URL = "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat.sha256sum";
  35. private static byte[] geositeDB;
  36. public static readonly Dictionary<string, IList<DomainObject>> Geosites = new Dictionary<string, IList<DomainObject>>();
  37. static GeositeUpdater()
  38. {
  39. //socketsHttpHandler = new SocketsHttpHandler();
  40. //httpClient = new HttpClient(socketsHttpHandler);
  41. if (File.Exists(DATABASE_PATH) && new FileInfo(DATABASE_PATH).Length > 0)
  42. {
  43. geositeDB = File.ReadAllBytes(DATABASE_PATH);
  44. }
  45. else
  46. {
  47. geositeDB = Resources.dlc_dat;
  48. File.WriteAllBytes(DATABASE_PATH, Resources.dlc_dat);
  49. }
  50. LoadGeositeList();
  51. }
  52. /// <summary>
  53. /// load new GeoSite data from geositeDB
  54. /// </summary>
  55. static void LoadGeositeList()
  56. {
  57. var list = GeositeList.Parser.ParseFrom(geositeDB);
  58. foreach (var item in list.Entries)
  59. {
  60. Geosites[item.GroupName.ToLower()] = item.Domains;
  61. }
  62. }
  63. public static void ResetEvent()
  64. {
  65. UpdateCompleted = null;
  66. Error = null;
  67. }
  68. public static async Task UpdatePACFromGeosite()
  69. {
  70. string geositeUrl = GEOSITE_URL;
  71. string geositeSha256sumUrl = GEOSITE_SHA256SUM_URL;
  72. SHA256 mySHA256 = SHA256.Create();
  73. var config = Program.MainController.GetCurrentConfiguration();
  74. string group = config.geositeGroup;
  75. bool blacklist = config.geositeBlacklistMode;
  76. if (!string.IsNullOrWhiteSpace(config.geositeUrl))
  77. {
  78. logger.Info("Found custom Geosite URL in config file");
  79. geositeUrl = config.geositeUrl;
  80. }
  81. logger.Info($"Checking Geosite from {geositeUrl}");
  82. // use System.Net.Http.HttpClient to download GeoSite db.
  83. // NASTY workaround: new HttpClient every update
  84. // because we can't change proxy on existing socketsHttpHandler instance
  85. httpClientHandler = new HttpClientHandler();
  86. httpClient = new HttpClient(httpClientHandler);
  87. if (config.enabled)
  88. {
  89. httpClientHandler.Proxy = new WebProxy(
  90. config.isIPv6Enabled
  91. ? $"[{IPAddress.IPv6Loopback}]"
  92. : IPAddress.Loopback.ToString(),
  93. config.localPort);
  94. }
  95. try
  96. {
  97. // download checksum first
  98. var geositeSha256sum = await httpClient.GetStringAsync(geositeSha256sumUrl);
  99. geositeSha256sum = geositeSha256sum.Substring(0, 64).ToUpper();
  100. logger.Info($"Got Sha256sum: {geositeSha256sum}");
  101. // compare downloaded checksum with local geositeDB
  102. byte[] localDBHashBytes = mySHA256.ComputeHash(geositeDB);
  103. string localDBHash = BitConverter.ToString(localDBHashBytes).Replace("-", String.Empty);
  104. logger.Info($"Local Sha256sum: {localDBHash}");
  105. // if already latest
  106. if (geositeSha256sum == localDBHash)
  107. {
  108. logger.Info("Local GeoSite DB is up to date.");
  109. return;
  110. }
  111. // not latest. download new DB
  112. var downloadedBytes = await httpClient.GetByteArrayAsync(geositeUrl);
  113. // verify sha256sum
  114. byte[] downloadedDBHashBytes = mySHA256.ComputeHash(downloadedBytes);
  115. string downloadedDBHash = BitConverter.ToString(downloadedDBHashBytes).Replace("-", String.Empty);
  116. logger.Info($"Actual Sha256sum: {downloadedDBHash}");
  117. if (geositeSha256sum != downloadedDBHash)
  118. {
  119. logger.Info("Sha256sum Verification: FAILED. Downloaded GeoSite DB is corrupted. Aborting the update.");
  120. throw new Exception("Sha256sum mismatch");
  121. }
  122. else
  123. {
  124. logger.Info("Sha256sum Verification: PASSED. Applying to local GeoSite DB.");
  125. }
  126. // write to geosite file
  127. using (FileStream geositeFileStream = File.Create(DATABASE_PATH))
  128. await geositeFileStream.WriteAsync(downloadedBytes, 0, downloadedBytes.Length);
  129. // update stuff
  130. geositeDB = downloadedBytes;
  131. LoadGeositeList();
  132. bool pacFileChanged = MergeAndWritePACFile(group, blacklist);
  133. UpdateCompleted?.Invoke(null, new GeositeResultEventArgs(pacFileChanged));
  134. }
  135. catch (Exception ex)
  136. {
  137. Error?.Invoke(null, new ErrorEventArgs(ex));
  138. }
  139. finally
  140. {
  141. if (httpClientHandler != null)
  142. {
  143. httpClientHandler.Dispose();
  144. httpClientHandler = null;
  145. }
  146. if (httpClient != null)
  147. {
  148. httpClient.Dispose();
  149. httpClient = null;
  150. }
  151. }
  152. }
  153. public static bool MergeAndWritePACFile(string group, bool blacklist)
  154. {
  155. IList<DomainObject> domains = Geosites[group];
  156. string abpContent = MergePACFile(domains, blacklist);
  157. if (File.Exists(PACDaemon.PAC_FILE))
  158. {
  159. string original = FileManager.NonExclusiveReadAllText(PACDaemon.PAC_FILE, Encoding.UTF8);
  160. if (original == abpContent)
  161. {
  162. return false;
  163. }
  164. }
  165. File.WriteAllText(PACDaemon.PAC_FILE, abpContent, Encoding.UTF8);
  166. return true;
  167. }
  168. private static string MergePACFile(IList<DomainObject> domains, bool blacklist)
  169. {
  170. string abpContent;
  171. if (File.Exists(PACDaemon.USER_ABP_FILE))
  172. {
  173. abpContent = FileManager.NonExclusiveReadAllText(PACDaemon.USER_ABP_FILE, Encoding.UTF8);
  174. }
  175. else
  176. {
  177. abpContent = Resources.abp_js;
  178. }
  179. List<string> userruleLines = new List<string>();
  180. if (File.Exists(PACDaemon.USER_RULE_FILE))
  181. {
  182. string userrulesString = FileManager.NonExclusiveReadAllText(PACDaemon.USER_RULE_FILE, Encoding.UTF8);
  183. userruleLines = PreProcessGFWList(userrulesString);
  184. }
  185. List<string> gfwLines = GeositeToGFWList(domains, blacklist);
  186. abpContent =
  187. $@"var __USERRULES__ = {JsonConvert.SerializeObject(userruleLines, Formatting.Indented)};
  188. var __RULES__ = {JsonConvert.SerializeObject(gfwLines, Formatting.Indented)};
  189. {abpContent}";
  190. return abpContent;
  191. }
  192. private static readonly IEnumerable<char> IgnoredLineBegins = new[] { '!', '[' };
  193. private static List<string> PreProcessGFWList(string content)
  194. {
  195. List<string> valid_lines = new List<string>();
  196. using (var sr = new StringReader(content))
  197. {
  198. foreach (var line in sr.NonWhiteSpaceLines())
  199. {
  200. if (line.BeginWithAny(IgnoredLineBegins))
  201. continue;
  202. valid_lines.Add(line);
  203. }
  204. }
  205. return valid_lines;
  206. }
  207. private static List<string> GeositeToGFWList(IList<DomainObject> domains, bool blacklist)
  208. {
  209. return blacklist ? GeositeToGFWListBlack(domains) : GeositeToGFWListWhite(domains);
  210. }
  211. private static List<string> GeositeToGFWListBlack(IList<DomainObject> domains)
  212. {
  213. List<string> ret = new List<string>(domains.Count + 100);// 100 overhead
  214. foreach (var d in domains)
  215. {
  216. string domain = d.Value;
  217. switch (d.Type)
  218. {
  219. case DomainObject.Types.Type.Plain:
  220. ret.Add(domain);
  221. break;
  222. case DomainObject.Types.Type.Regex:
  223. ret.Add($"/{domain}/");
  224. break;
  225. case DomainObject.Types.Type.Domain:
  226. ret.Add($"||{domain}");
  227. break;
  228. case DomainObject.Types.Type.Full:
  229. ret.Add($"|http://{domain}");
  230. ret.Add($"|https://{domain}");
  231. break;
  232. }
  233. }
  234. return ret;
  235. }
  236. private static List<string> GeositeToGFWListWhite(IList<DomainObject> domains)
  237. {
  238. return GeositeToGFWListBlack(domains)
  239. .Select(r => $"@@{r}") // convert to whitelist
  240. .Prepend("/.*/") // blacklist all other site
  241. .ToList();
  242. }
  243. }
  244. }