using NLog; using Shadowsocks.Properties; using Shadowsocks.Util; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Newtonsoft.Json; using Shadowsocks.Model; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Security.Cryptography; namespace Shadowsocks.Controller { public class GeositeResultEventArgs : EventArgs { public bool Success; public GeositeResultEventArgs(bool success) { this.Success = success; } } public static class GeositeUpdater { private static Logger logger = LogManager.GetCurrentClassLogger(); public static event EventHandler UpdateCompleted; public static event ErrorEventHandler Error; private static readonly string DATABASE_PATH = Utils.GetTempPath("dlc.dat"); private static HttpClientHandler httpClientHandler; 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 byte[] geositeDB; public static readonly Dictionary> Geosites = new Dictionary>(); static GeositeUpdater() { //socketsHttpHandler = new SocketsHttpHandler(); //httpClient = new HttpClient(socketsHttpHandler); if (File.Exists(DATABASE_PATH) && new FileInfo(DATABASE_PATH).Length > 0) { geositeDB = File.ReadAllBytes(DATABASE_PATH); } else { geositeDB = Resources.dlc_dat; File.WriteAllBytes(DATABASE_PATH, Resources.dlc_dat); } LoadGeositeList(); } /// /// load new GeoSite data from geositeDB /// static void LoadGeositeList() { var list = GeositeList.Parser.ParseFrom(geositeDB); foreach (var item in list.Entries) { Geosites[item.GroupName.ToLower()] = item.Domains; } } public static void ResetEvent() { UpdateCompleted = null; Error = null; } public static async Task UpdatePACFromGeosite() { string geositeUrl = GEOSITE_URL; string geositeSha256sumUrl = GEOSITE_SHA256SUM_URL; SHA256 mySHA256 = SHA256.Create(); var config = Program.MainController.GetCurrentConfiguration(); string group = config.geositeGroup; bool blacklist = config.geositeBlacklistMode; if (!string.IsNullOrWhiteSpace(config.geositeUrl)) { logger.Info("Found custom Geosite URL in config file"); geositeUrl = config.geositeUrl; } logger.Info($"Checking Geosite from {geositeUrl}"); // use System.Net.Http.HttpClient to download GeoSite db. // NASTY workaround: new HttpClient every update // because we can't change proxy on existing socketsHttpHandler instance httpClientHandler = new HttpClientHandler(); httpClient = new HttpClient(httpClientHandler); if (config.enabled) { httpClientHandler.Proxy = new WebProxy( config.isIPv6Enabled ? $"[{IPAddress.IPv6Loopback}]" : IPAddress.Loopback.ToString(), config.localPort); } try { // download checksum first var geositeSha256sum = await httpClient.GetStringAsync(geositeSha256sumUrl); geositeSha256sum = geositeSha256sum.Substring(0, 64).ToUpper(); logger.Info($"Got Sha256sum: {geositeSha256sum}"); // compare downloaded checksum with local geositeDB byte[] localDBHashBytes = mySHA256.ComputeHash(geositeDB); string localDBHash = BitConverter.ToString(localDBHashBytes).Replace("-", String.Empty); logger.Info($"Local Sha256sum: {localDBHash}"); // if already latest if (geositeSha256sum == localDBHash) { logger.Info("Local GeoSite DB is up to date."); return; } // not latest. download new DB var downloadedBytes = await httpClient.GetByteArrayAsync(geositeUrl); // verify sha256sum byte[] downloadedDBHashBytes = mySHA256.ComputeHash(downloadedBytes); string downloadedDBHash = BitConverter.ToString(downloadedDBHashBytes).Replace("-", String.Empty); logger.Info($"Actual Sha256sum: {downloadedDBHash}"); if (geositeSha256sum != downloadedDBHash) { logger.Info("Sha256sum Verification: FAILED. Downloaded GeoSite DB is corrupted. Aborting the update."); throw new Exception("Sha256sum mismatch"); } else { logger.Info("Sha256sum Verification: PASSED. Applying to local GeoSite DB."); } // write to geosite file using (FileStream geositeFileStream = File.Create(DATABASE_PATH)) await geositeFileStream.WriteAsync(downloadedBytes, 0, downloadedBytes.Length); // update stuff geositeDB = downloadedBytes; LoadGeositeList(); bool pacFileChanged = MergeAndWritePACFile(group, blacklist); UpdateCompleted?.Invoke(null, new GeositeResultEventArgs(pacFileChanged)); } catch (Exception ex) { Error?.Invoke(null, new ErrorEventArgs(ex)); } finally { if (httpClientHandler != null) { httpClientHandler.Dispose(); httpClientHandler = null; } if (httpClient != null) { httpClient.Dispose(); httpClient = null; } } } public static bool MergeAndWritePACFile(string group, bool blacklist) { IList domains = Geosites[group]; string abpContent = MergePACFile(domains, blacklist); if (File.Exists(PACDaemon.PAC_FILE)) { string original = FileManager.NonExclusiveReadAllText(PACDaemon.PAC_FILE, Encoding.UTF8); if (original == abpContent) { return false; } } File.WriteAllText(PACDaemon.PAC_FILE, abpContent, Encoding.UTF8); return true; } private static string MergePACFile(IList domains, bool blacklist) { string abpContent; if (File.Exists(PACDaemon.USER_ABP_FILE)) { abpContent = FileManager.NonExclusiveReadAllText(PACDaemon.USER_ABP_FILE, Encoding.UTF8); } else { abpContent = Resources.abp_js; } List userruleLines = new List(); if (File.Exists(PACDaemon.USER_RULE_FILE)) { string userrulesString = FileManager.NonExclusiveReadAllText(PACDaemon.USER_RULE_FILE, Encoding.UTF8); userruleLines = PreProcessGFWList(userrulesString); } List gfwLines = GeositeToGFWList(domains, blacklist); abpContent = $@"var __USERRULES__ = {JsonConvert.SerializeObject(userruleLines, Formatting.Indented)}; var __RULES__ = {JsonConvert.SerializeObject(gfwLines, Formatting.Indented)}; {abpContent}"; return abpContent; } private static readonly IEnumerable IgnoredLineBegins = new[] { '!', '[' }; private static List PreProcessGFWList(string content) { List valid_lines = new List(); using (var sr = new StringReader(content)) { foreach (var line in sr.NonWhiteSpaceLines()) { if (line.BeginWithAny(IgnoredLineBegins)) continue; valid_lines.Add(line); } } return valid_lines; } private static List GeositeToGFWList(IList domains, bool blacklist) { return blacklist ? GeositeToGFWListBlack(domains) : GeositeToGFWListWhite(domains); } private static List GeositeToGFWListBlack(IList domains) { List ret = new List(domains.Count + 100);// 100 overhead foreach (var d in domains) { string domain = d.Value; switch (d.Type) { 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; } } return ret; } private static List GeositeToGFWListWhite(IList domains) { return GeositeToGFWListBlack(domains) .Select(r => $"@@{r}") // convert to whitelist .Prepend("/.*/") // blacklist all other site .ToList(); } } }