using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Web; using System.Windows.Forms; using NLog; using Shadowsocks.Controller.Service; using Shadowsocks.Controller.Strategy; using Shadowsocks.Model; using Shadowsocks.Util; namespace Shadowsocks.Controller { public class ShadowsocksController { private static Logger logger = LogManager.GetCurrentClassLogger(); // controller: // handle user actions // manipulates UI // interacts with low level logic private Thread _ramThread; private Thread _trafficThread; private Listener _listener; private PACDaemon _pacDaemon; private PACServer _pacServer; private Configuration _config; private StrategyManager _strategyManager; private PrivoxyRunner privoxyRunner; private GFWListUpdater gfwListUpdater; private readonly ConcurrentDictionary _pluginsByServer; public AvailabilityStatistics availabilityStatistics = AvailabilityStatistics.Instance; public StatisticsStrategyConfiguration StatisticsConfiguration { get; private set; } private long _inboundCounter = 0; private long _outboundCounter = 0; public long InboundCounter => Interlocked.Read(ref _inboundCounter); public long OutboundCounter => Interlocked.Read(ref _outboundCounter); public Queue trafficPerSecondQueue; private bool stopped = false; public class PathEventArgs : EventArgs { public string Path; } public class TrafficPerSecond { public long inboundCounter; public long outboundCounter; public long inboundIncreasement; public long outboundIncreasement; } public event EventHandler ConfigChanged; public event EventHandler EnableStatusChanged; public event EventHandler EnableGlobalChanged; public event EventHandler ShareOverLANStatusChanged; public event EventHandler VerboseLoggingStatusChanged; public event EventHandler ShowPluginOutputChanged; public event EventHandler TrafficChanged; // when user clicked Edit PAC, and PAC file has already created public event EventHandler PACFileReadyToOpen; public event EventHandler UserRuleFileReadyToOpen; public event EventHandler UpdatePACFromGFWListCompleted; public event ErrorEventHandler UpdatePACFromGFWListError; public event ErrorEventHandler Errored; public ShadowsocksController() { _config = Configuration.Load(); StatisticsConfiguration = StatisticsStrategyConfiguration.Load(); _strategyManager = new StrategyManager(this); _pluginsByServer = new ConcurrentDictionary(); StartReleasingMemory(); StartTrafficStatistics(61); } public void Start(bool regHotkeys = true) { Reload(); if (regHotkeys) { HotkeyReg.RegAllHotkeys(); } } protected void ReportError(Exception e) { Errored?.Invoke(this, new ErrorEventArgs(e)); } public Server GetCurrentServer() { return _config.GetCurrentServer(); } // always return copy public Configuration GetConfigurationCopy() { return Configuration.Load(); } // always return current instance public Configuration GetCurrentConfiguration() { return _config; } public IList GetStrategies() { return _strategyManager.GetStrategies(); } public IStrategy GetCurrentStrategy() { foreach (var strategy in _strategyManager.GetStrategies()) { if (strategy.ID == _config.strategy) { return strategy; } } return null; } public Server GetAServer(IStrategyCallerType type, IPEndPoint localIPEndPoint, EndPoint destEndPoint) { IStrategy strategy = GetCurrentStrategy(); if (strategy != null) { return strategy.GetAServer(type, localIPEndPoint, destEndPoint); } if (_config.index < 0) { _config.index = 0; } return GetCurrentServer(); } public EndPoint GetPluginLocalEndPointIfConfigured(Server server) { var plugin = _pluginsByServer.GetOrAdd( server, x => Sip003Plugin.CreateIfConfigured(x, _config.showPluginOutput)); if (plugin == null) { return null; } try { if (plugin.StartIfNeeded()) { logger.Info( $"Started SIP003 plugin for {server.Identifier()} on {plugin.LocalEndPoint} - PID: {plugin.ProcessId}"); } } catch (Exception ex) { logger.Error("Failed to start SIP003 plugin: " + ex.Message); throw; } return plugin.LocalEndPoint; } public void SaveServers(List servers, int localPort, bool portableMode) { _config.configs = servers; _config.localPort = localPort; _config.portableMode = portableMode; Configuration.Save(_config); } public void SaveStrategyConfigurations(StatisticsStrategyConfiguration configuration) { StatisticsConfiguration = configuration; StatisticsStrategyConfiguration.Save(configuration); } public bool AddServerBySSURL(string ssURL) { try { if (ssURL.IsNullOrEmpty() || ssURL.IsWhiteSpace()) return false; var servers = Server.GetServers(ssURL); if (servers == null || servers.Count == 0) return false; foreach (var server in servers) { _config.configs.Add(server); } _config.index = _config.configs.Count - 1; SaveConfig(_config); return true; } catch (Exception e) { logger.LogUsefulException(e); return false; } } public void ToggleEnable(bool enabled) { _config.enabled = enabled; SaveConfig(_config); EnableStatusChanged?.Invoke(this, new EventArgs()); } public void ToggleGlobal(bool global) { _config.global = global; SaveConfig(_config); EnableGlobalChanged?.Invoke(this, new EventArgs()); } public void ToggleShareOverLAN(bool enabled) { _config.shareOverLan = enabled; SaveConfig(_config); ShareOverLANStatusChanged?.Invoke(this, new EventArgs()); } public void SaveProxy(ProxyConfig proxyConfig) { _config.proxy = proxyConfig; SaveConfig(_config); } public void ToggleVerboseLogging(bool enabled) { _config.isVerboseLogging = enabled; SaveConfig(_config); NLogConfig.LoadConfiguration(); // reload nlog VerboseLoggingStatusChanged?.Invoke(this, new EventArgs()); } public void ToggleShowPluginOutput(bool enabled) { _config.showPluginOutput = enabled; SaveConfig(_config); ShowPluginOutputChanged?.Invoke(this, new EventArgs()); } public void SelectServerIndex(int index) { _config.index = index; _config.strategy = null; SaveConfig(_config); } public void SelectStrategy(string strategyID) { _config.index = -1; _config.strategy = strategyID; SaveConfig(_config); } public void Stop() { if (stopped) { return; } stopped = true; if (_listener != null) { _listener.Stop(); } StopPlugins(); if (privoxyRunner != null) { privoxyRunner.Stop(); } if (_config.enabled) { SystemProxy.Update(_config, true, null); } Encryption.RNG.Close(); } private void StopPlugins() { foreach (var serverAndPlugin in _pluginsByServer) { serverAndPlugin.Value?.Dispose(); } _pluginsByServer.Clear(); } public void TouchPACFile() { string pacFilename = _pacDaemon.TouchPACFile(); PACFileReadyToOpen?.Invoke(this, new PathEventArgs() { Path = pacFilename }); } public void TouchUserRuleFile() { string userRuleFilename = _pacDaemon.TouchUserRuleFile(); UserRuleFileReadyToOpen?.Invoke(this, new PathEventArgs() { Path = userRuleFilename }); } public string GetServerURLForCurrentServer() { Server server = GetCurrentServer(); return GetServerURL(server); } public static string GetServerURL(Server server) { string tag = string.Empty; string url = string.Empty; if (string.IsNullOrWhiteSpace(server.plugin)) { // For backwards compatiblity, if no plugin, use old url format string parts = $"{server.method}:{server.password}@{server.server}:{server.server_port}"; string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(parts)); url = base64; } else { // SIP002 string parts = $"{server.method}:{server.password}"; string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(parts)); string websafeBase64 = base64.Replace('+', '-').Replace('/', '_').TrimEnd('='); string pluginPart = server.plugin; if (!string.IsNullOrWhiteSpace(server.plugin_opts)) { pluginPart += ";" + server.plugin_opts; } url = string.Format( "{0}@{1}:{2}/?plugin={3}", websafeBase64, server.FormatHostName(server.server), server.server_port, HttpUtility.UrlEncode(pluginPart, Encoding.UTF8)); } if (!server.remarks.IsNullOrEmpty()) { tag = $"#{HttpUtility.UrlEncode(server.remarks, Encoding.UTF8)}"; } return $"ss://{url}{tag}"; } public void UpdatePACFromGFWList() { if (gfwListUpdater != null) { gfwListUpdater.UpdatePACFromGFWList(_config); } } public void UpdateStatisticsConfiguration(bool enabled) { if (availabilityStatistics != null) { availabilityStatistics.UpdateConfiguration(this); _config.availabilityStatistics = enabled; SaveConfig(_config); } } public void SavePACUrl(string pacUrl) { _config.pacUrl = pacUrl; SaveConfig(_config); ConfigChanged?.Invoke(this, new EventArgs()); } public void UseOnlinePAC(bool useOnlinePac) { _config.useOnlinePac = useOnlinePac; SaveConfig(_config); ConfigChanged?.Invoke(this, new EventArgs()); } public void ToggleSecureLocalPac(bool enabled) { _config.secureLocalPac = enabled; SaveConfig(_config); ConfigChanged?.Invoke(this, new EventArgs()); } public void ToggleCheckingUpdate(bool enabled) { _config.autoCheckUpdate = enabled; Configuration.Save(_config); ConfigChanged?.Invoke(this, new EventArgs()); } public void ToggleCheckingPreRelease(bool enabled) { _config.checkPreRelease = enabled; Configuration.Save(_config); ConfigChanged?.Invoke(this, new EventArgs()); } public void SaveLogViewerConfig(LogViewerConfig newConfig) { _config.logViewer = newConfig; newConfig.SaveSize(); Configuration.Save(_config); ConfigChanged?.Invoke(this, new EventArgs()); } public void SaveHotkeyConfig(HotkeyConfig newConfig) { _config.hotkey = newConfig; SaveConfig(_config); ConfigChanged?.Invoke(this, new EventArgs()); } public void UpdateLatency(Server server, TimeSpan latency) { if (_config.availabilityStatistics) { availabilityStatistics.UpdateLatency(server, (int)latency.TotalMilliseconds); } } public void UpdateInboundCounter(Server server, long n) { Interlocked.Add(ref _inboundCounter, n); if (_config.availabilityStatistics) { availabilityStatistics.UpdateInboundCounter(server, n); } } public void UpdateOutboundCounter(Server server, long n) { Interlocked.Add(ref _outboundCounter, n); if (_config.availabilityStatistics) { availabilityStatistics.UpdateOutboundCounter(server, n); } } protected void Reload() { Encryption.RNG.Reload(); // some logic in configuration updated the config when saving, we need to read it again _config = Configuration.Load(); NLogConfig.LoadConfiguration(); StatisticsConfiguration = StatisticsStrategyConfiguration.Load(); privoxyRunner = privoxyRunner ?? new PrivoxyRunner(); _pacDaemon = _pacDaemon ?? new PACDaemon(); _pacDaemon.PACFileChanged += PacDaemon_PACFileChanged; _pacDaemon.UserRuleFileChanged += PacDaemon_UserRuleFileChanged; _pacServer = _pacServer ?? new PACServer(_pacDaemon); _pacServer.UpdatePACURL(_config); // So PACServer works when system proxy disabled. gfwListUpdater = gfwListUpdater ?? new GFWListUpdater(); gfwListUpdater.UpdateCompleted += PacServer_PACUpdateCompleted; gfwListUpdater.Error += PacServer_PACUpdateError; availabilityStatistics.UpdateConfiguration(this); _listener?.Stop(); StopPlugins(); // don't put PrivoxyRunner.Start() before pacServer.Stop() // or bind will fail when switching bind address from 0.0.0.0 to 127.0.0.1 // though UseShellExecute is set to true now // http://stackoverflow.com/questions/10235093/socket-doesnt-close-after-application-exits-if-a-launched-process-is-open privoxyRunner.Stop(); try { var strategy = GetCurrentStrategy(); strategy?.ReloadServers(); StartPlugin(); privoxyRunner.Start(_config); TCPRelay tcpRelay = new TCPRelay(this, _config); UDPRelay udpRelay = new UDPRelay(this); List services = new List { tcpRelay, udpRelay, _pacServer, new PortForwarder(privoxyRunner.RunningPort) }; _listener = new Listener(services); _listener.Start(_config); } catch (Exception e) { // translate Microsoft language into human language // i.e. An attempt was made to access a socket in a way forbidden by its access permissions => Port already in use if (e is SocketException se) { if (se.SocketErrorCode == SocketError.AddressAlreadyInUse) { e = new Exception(I18N.GetString("Port {0} already in use", _config.localPort), e); } else if (se.SocketErrorCode == SocketError.AccessDenied) { e = new Exception(I18N.GetString("Port {0} is reserved by system", _config.localPort), e); } } logger.LogUsefulException(e); ReportError(e); } ConfigChanged?.Invoke(this, new EventArgs()); UpdateSystemProxy(); Utils.ReleaseMemory(true); } private void StartPlugin() { var server = _config.GetCurrentServer(); GetPluginLocalEndPointIfConfigured(server); } protected void SaveConfig(Configuration newConfig) { Configuration.Save(newConfig); Reload(); } private void UpdateSystemProxy() { SystemProxy.Update(_config, false, _pacServer); } private void PacDaemon_PACFileChanged(object sender, EventArgs e) { UpdateSystemProxy(); } private void PacServer_PACUpdateCompleted(object sender, GFWListUpdater.ResultEventArgs e) { UpdatePACFromGFWListCompleted?.Invoke(this, e); } private void PacServer_PACUpdateError(object sender, ErrorEventArgs e) { UpdatePACFromGFWListError?.Invoke(this, e); } private static readonly IEnumerable IgnoredLineBegins = new[] { '!', '[' }; private void PacDaemon_UserRuleFileChanged(object sender, EventArgs e) { if (!File.Exists(Utils.GetTempPath("gfwlist.txt"))) { UpdatePACFromGFWList(); } else { GFWListUpdater.MergeAndWritePACFile(FileManager.NonExclusiveReadAllText(Utils.GetTempPath("gfwlist.txt"))); } UpdateSystemProxy(); } public void CopyPacUrl() { Clipboard.SetDataObject(_pacServer.PacUrl); } #region Memory Management private void StartReleasingMemory() { _ramThread = new Thread(new ThreadStart(ReleaseMemory)) { IsBackground = true }; _ramThread.Start(); } private void ReleaseMemory() { while (true) { Utils.ReleaseMemory(false); Thread.Sleep(30 * 1000); } } #endregion #region Traffic Statistics private void StartTrafficStatistics(int queueMaxSize) { trafficPerSecondQueue = new Queue(); for (int i = 0; i < queueMaxSize; i++) { trafficPerSecondQueue.Enqueue(new TrafficPerSecond()); } _trafficThread = new Thread(new ThreadStart(() => TrafficStatistics(queueMaxSize))) { IsBackground = true }; _trafficThread.Start(); } private void TrafficStatistics(int queueMaxSize) { TrafficPerSecond previous, current; while (true) { previous = trafficPerSecondQueue.Last(); current = new TrafficPerSecond { inboundCounter = InboundCounter, outboundCounter = OutboundCounter }; current.inboundIncreasement = current.inboundCounter - previous.inboundCounter; current.outboundIncreasement = current.outboundCounter - previous.outboundCounter; trafficPerSecondQueue.Enqueue(current); if (trafficPerSecondQueue.Count > queueMaxSize) trafficPerSecondQueue.Dequeue(); TrafficChanged?.Invoke(this, new EventArgs()); Thread.Sleep(1000); } } #endregion } }