using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.Net; using System.IO; using System.IO.Compression; using System.Security.Cryptography; using Shadowsocks.Model; using Shadowsocks.Properties; namespace Shadowsocks.Controller { public class GfwListUpdater { private const string GFWLIST_URL = "https://autoproxy-gfwlist.googlecode.com/svn/trunk/gfwlist.txt"; private const int EXPIRE_HOURS = 6; public IWebProxy proxy = null; public bool useSystemProxy = true; public class GfwListChangedArgs : EventArgs { public string[] GfwList { get; set; } } public event EventHandler GfwListChanged; private bool running = false; private bool closed = false; private int jobId = 0; DateTime lastUpdateTimeUtc; string lastUpdateMd5; private object locker = new object(); public GfwListUpdater() { } ~GfwListUpdater() { Stop(); } public void Start() { lock (locker) { if (running) return; running = true; closed = false; jobId++; new Thread(new ParameterizedThreadStart(UpdateJob)).Start(jobId); } } public void Stop() { lock(locker) { closed = true; running = false; jobId++; } } public void ScheduleUpdateTime(int delaySeconds) { lock(locker) { lastUpdateTimeUtc = DateTime.UtcNow.AddHours(-1 * EXPIRE_HOURS).AddSeconds(delaySeconds); } } private string DownloadGfwListFile() { try { WebClient http = new WebClient(); http.Proxy = useSystemProxy ? WebRequest.GetSystemWebProxy() : proxy; return http.DownloadString(new Uri(GFWLIST_URL)); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } return null; } private bool IsExpire() { lock (locker) { TimeSpan ts = DateTime.UtcNow - lastUpdateTimeUtc; bool expire = ((int)ts.TotalHours) >= EXPIRE_HOURS; if (expire) lastUpdateTimeUtc = DateTime.UtcNow; return expire; } } private bool IsJobStop(int currentJobId) { lock (locker) { if (!running || closed || currentJobId != this.jobId) return true; } return false; } private bool IsGfwListChanged(string content) { byte[] inputBytes = Encoding.UTF8.GetBytes(content); byte[] md5Bytes = MD5.Create().ComputeHash(inputBytes); string md5 = ""; for (int i = 0; i < md5Bytes.Length; i++) md5 += md5Bytes[i].ToString("x").PadLeft(2, '0'); if (md5 == lastUpdateMd5) return false; lastUpdateMd5 = md5; return true; } private void ParseGfwList(string response) { if (!IsGfwListChanged(response)) return; if (GfwListChanged != null) { try { Parser parser = new Parser(response); GfwListChangedArgs args = new GfwListChangedArgs { GfwList = parser.GetReducedDomains() }; GfwListChanged(this, args); } catch(Exception ex) { Console.WriteLine(ex.ToString()); } } } private void UpdateJob(object state) { int currentJobId = (int)state; int retryTimes = 3; while (!IsJobStop(currentJobId)) { if (IsExpire()) { string response = DownloadGfwListFile(); if (response != null) { ParseGfwList(response); } else if (retryTimes > 0) { ScheduleUpdateTime(30); /*Delay 30 seconds to retry*/ retryTimes--; } else { retryTimes = 3; /* reset retry times, and wait next update time. */ } } Thread.Sleep(1000); } } class Parser { public string Content { get; private set; } public Parser(string response) { byte[] bytes = Convert.FromBase64String(response); this.Content = Encoding.ASCII.GetString(bytes); } public string[] GetLines() { return Content.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); } /* refer https://github.com/clowwindy/gfwlist2pac/blob/master/gfwlist2pac/main.py */ public string[] GetDomains() { string[] lines = GetLines(); List domains = new List(lines.Length); for(int i =0;i < lines.Length;i++) { string line = lines[i]; if (line.IndexOf(".*") >= 0) continue; else if (line.IndexOf("*") >= 0) line = line.Replace("*", "/"); if (line.StartsWith("||")) line = line.Substring(2); else if (line.StartsWith("|")) line = line.Substring(1); else if (line.StartsWith(".")) line = line.Substring(1); if (line.StartsWith("!")) continue; else if (line.StartsWith("[")) continue; else if (line.StartsWith("@")) continue; /*ignore white list*/ domains.Add(line); } return domains.ToArray(); } /* refer https://github.com/clowwindy/gfwlist2pac/blob/master/gfwlist2pac/main.py */ public string[] GetReducedDomains() { string[] domains = GetDomains(); List new_domains = new List(domains.Length); IDictionary tld_dic = GetTldDictionary(); foreach(string domain in domains) { string last_root_domain = null; int pos; pos = domain.LastIndexOf('.'); last_root_domain = domain.Substring(pos + 1); if (!tld_dic.ContainsKey(last_root_domain)) continue; while(pos > 0) { pos = domain.LastIndexOf('.', pos - 1); last_root_domain = domain.Substring(pos + 1); if (tld_dic.ContainsKey(last_root_domain)) continue; else break; } if (last_root_domain != null) new_domains.Add(last_root_domain); } return new_domains.ToArray(); } private string[] GetTlds() { string[] tlds = null; byte[] pacGZ = Resources.tld_txt; byte[] buffer = new byte[1024]; int n; using(MemoryStream sb = new MemoryStream()) { using (GZipStream input = new GZipStream(new MemoryStream(pacGZ), CompressionMode.Decompress, false)) { while ((n = input.Read(buffer, 0, buffer.Length)) > 0) { sb.Write(buffer, 0, n); } } tlds = System.Text.Encoding.UTF8.GetString(sb.ToArray()) .Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); } return tlds; } private IDictionary GetTldDictionary() { string[] tlds = GetTlds(); IDictionary dic = new Dictionary(tlds.Length); foreach (string tld in tlds) { if (!dic.ContainsKey(tld)) dic.Add(tld, tld); } return dic; } } } }