using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Windows; using Newtonsoft.Json; using NLog; using Shadowsocks.Controller; namespace Shadowsocks.Model { [Serializable] public class Configuration { [JsonIgnore] private static readonly Logger logger = LogManager.GetCurrentClassLogger(); public string version; public List configs; public List onlineConfigSource; // when strategy is set, index is ignored public string strategy; public int index; public bool global; public bool enabled; public bool shareOverLan; public bool firstRun; public int localPort; public bool portableMode; public bool showPluginOutput; public string pacUrl; public bool useOnlinePac; public bool secureLocalPac; // enable secret for PAC server public bool regeneratePacOnUpdate; // regenerate pac.txt on version update public bool autoCheckUpdate; public bool checkPreRelease; public string skippedUpdateVersion; // skip the update with this version number public bool isVerboseLogging; // hidden options public bool isIPv6Enabled; // for experimental ipv6 support public bool generateLegacyUrl; // for pre-sip002 url compatibility public string geositeUrl; // for custom geosite source (and rule group) public List geositeDirectGroups; // groups of domains that we connect without the proxy public List geositeProxiedGroups; // groups of domains that we connect via the proxy public bool geositePreferDirect; // a.k.a blacklist mode public string userAgent; //public NLogConfig.LogLevel logLevel; public LogViewerConfig logViewer; public ForwardProxyConfig proxy; public HotkeyConfig hotkey; [JsonIgnore] public bool firstRunOnNewVersion; public Configuration() { version = UpdateChecker.Version; strategy = ""; index = 0; global = false; enabled = false; shareOverLan = false; firstRun = true; localPort = 1080; portableMode = true; showPluginOutput = false; pacUrl = ""; useOnlinePac = false; secureLocalPac = true; regeneratePacOnUpdate = true; autoCheckUpdate = false; checkPreRelease = false; skippedUpdateVersion = ""; isVerboseLogging = false; // hidden options isIPv6Enabled = false; generateLegacyUrl = false; geositeUrl = ""; geositeDirectGroups = new List() { "private", "cn", "geolocation-!cn@cn", }; geositeProxiedGroups = new List() { "geolocation-!cn", }; geositePreferDirect = false; userAgent = "ShadowsocksWindows/$version"; logViewer = new LogViewerConfig(); proxy = new ForwardProxyConfig(); hotkey = new HotkeyConfig(); firstRunOnNewVersion = false; configs = new List(); onlineConfigSource = new List(); } [JsonIgnore] public string userAgentString; // $version substituted with numeral version in it [JsonIgnore] NLogConfig nLogConfig; private static readonly string CONFIG_FILE = "gui-config.json"; #if DEBUG private static readonly NLogConfig.LogLevel verboseLogLevel = NLogConfig.LogLevel.Trace; #else private static readonly NLogConfig.LogLevel verboseLogLevel = NLogConfig.LogLevel.Debug; #endif [JsonIgnore] public string LocalHost => isIPv6Enabled ? "[::1]" : "127.0.0.1"; public Server GetCurrentServer() { if (index >= 0 && index < configs.Count) return configs[index]; else return GetDefaultServer(); } public WebProxy WebProxy => enabled ? new WebProxy( isIPv6Enabled ? $"[{IPAddress.IPv6Loopback}]" : IPAddress.Loopback.ToString(), localPort) : null; /// /// Used by multiple forms to validate a server. /// Communication is done by throwing exceptions. /// /// public static void CheckServer(Server server) { CheckServer(server.server); CheckPort(server.server_port); CheckPassword(server.password); CheckTimeout(server.timeout, Server.MaxServerTimeoutSec); } /// /// Loads the configuration from file. /// /// An Configuration object. public static Configuration Load() { Configuration config; if (File.Exists(CONFIG_FILE)) { try { string configContent = File.ReadAllText(CONFIG_FILE); config = JsonConvert.DeserializeObject(configContent, new JsonSerializerSettings() { ObjectCreationHandling = ObjectCreationHandling.Replace }); return config; } catch (Exception e) { if (!(e is FileNotFoundException)) logger.LogUsefulException(e); } } config = new Configuration(); return config; } /// /// Process the loaded configurations and set up things. /// /// A reference of Configuration object. public static void Process(ref Configuration config) { // Verify if the configured geosite groups exist. // Reset to default if ANY one of the configured group doesn't exist. if (!ValidateGeositeGroupList(config.geositeDirectGroups)) ResetGeositeDirectGroup(ref config.geositeDirectGroups); if (!ValidateGeositeGroupList(config.geositeProxiedGroups)) ResetGeositeProxiedGroup(ref config.geositeProxiedGroups); // Mark the first run of a new version. var appVersion = new Version(UpdateChecker.Version); var configVersion = new Version(config.version); if (appVersion.CompareTo(configVersion) > 0) { config.firstRunOnNewVersion = true; } // Add an empty server configuration if (config.configs.Count == 0) config.configs.Add(GetDefaultServer()); // Selected server if (config.index == -1 && string.IsNullOrEmpty(config.strategy)) config.index = 0; if (config.index >= config.configs.Count) config.index = config.configs.Count - 1; // Check OS IPv6 support if (!System.Net.Sockets.Socket.OSSupportsIPv6) config.isIPv6Enabled = false; config.proxy.CheckConfig(); // Replace $version with the version number. config.userAgentString = config.userAgent.Replace("$version", config.version); // NLog log level try { config.nLogConfig = NLogConfig.LoadXML(); switch (config.nLogConfig.GetLogLevel()) { case NLogConfig.LogLevel.Fatal: case NLogConfig.LogLevel.Error: case NLogConfig.LogLevel.Warn: case NLogConfig.LogLevel.Info: config.isVerboseLogging = false; break; case NLogConfig.LogLevel.Debug: case NLogConfig.LogLevel.Trace: config.isVerboseLogging = true; break; } } catch (Exception e) { MessageBox.Show($"Cannot get the log level from NLog config file. Please check if the nlog config file exists with corresponding XML nodes.\n{e.Message}"); } } /// /// Saves the Configuration object to file. /// /// A Configuration object. public static void Save(Configuration config) { config.configs = SortByOnlineConfig(config.configs); FileStream configFileStream = null; StreamWriter configStreamWriter = null; try { configFileStream = File.Open(CONFIG_FILE, FileMode.Create); configStreamWriter = new StreamWriter(configFileStream); var jsonString = JsonConvert.SerializeObject(config, Formatting.Indented); configStreamWriter.Write(jsonString); configStreamWriter.Flush(); // NLog config.nLogConfig.SetLogLevel(config.isVerboseLogging ? verboseLogLevel : NLogConfig.LogLevel.Info); NLogConfig.SaveXML(config.nLogConfig); } catch (Exception e) { logger.LogUsefulException(e); } finally { if (configStreamWriter != null) configStreamWriter.Dispose(); if (configFileStream != null) configFileStream.Dispose(); } } public static List SortByOnlineConfig(IEnumerable servers) { var groups = servers.GroupBy(s => s.group); List ret = new List(); ret.AddRange(groups.Where(g => string.IsNullOrEmpty(g.Key)).SelectMany(g => g)); ret.AddRange(groups.Where(g => !string.IsNullOrEmpty(g.Key)).SelectMany(g => g)); return ret; } /// /// Validates if the groups in the list are all valid. /// /// The list of groups to validate. /// /// True if all groups are valid. /// False if any one of them is invalid. /// public static bool ValidateGeositeGroupList(List groups) { foreach (var geositeGroup in groups) if (!GeositeUpdater.CheckGeositeGroup(geositeGroup)) // found invalid group { #if DEBUG logger.Debug($"Available groups:"); foreach (var group in GeositeUpdater.Geosites.Keys) logger.Debug($"{group}"); #endif logger.Warn($"The Geosite group {geositeGroup} doesn't exist. Resetting to default groups."); return false; } return true; } public static void ResetGeositeDirectGroup(ref List geositeDirectGroups) { geositeDirectGroups.Clear(); geositeDirectGroups.Add("private"); geositeDirectGroups.Add("cn"); geositeDirectGroups.Add("geolocation-!cn@cn"); } public static void ResetGeositeProxiedGroup(ref List geositeProxiedGroups) { geositeProxiedGroups.Clear(); geositeProxiedGroups.Add("geolocation-!cn"); } public static void ResetUserAgent(Configuration config) { config.userAgent = "ShadowsocksWindows/$version"; config.userAgentString = config.userAgent.Replace("$version", config.version); } public static Server AddDefaultServerOrServer(Configuration config, Server server = null, int? index = null) { if (config?.configs != null) { server = (server ?? GetDefaultServer()); config.configs.Insert(index.GetValueOrDefault(config.configs.Count), server); //if (index.HasValue) // config.configs.Insert(index.Value, server); //else // config.configs.Add(server); } return server; } public static Server GetDefaultServer() { return new Server(); } public static void CheckPort(int port) { if (port <= 0 || port > 65535) throw new ArgumentException(I18N.GetString("Port out of range")); } public static void CheckLocalPort(int port) { CheckPort(port); if (port == 8123) throw new ArgumentException(I18N.GetString("Port can't be 8123")); } private static void CheckPassword(string password) { if (string.IsNullOrEmpty(password)) throw new ArgumentException(I18N.GetString("Password can not be blank")); } public static void CheckServer(string server) { if (string.IsNullOrEmpty(server)) throw new ArgumentException(I18N.GetString("Server IP can not be blank")); } public static void CheckTimeout(int timeout, int maxTimeout) { if (timeout <= 0 || timeout > maxTimeout) throw new ArgumentException( I18N.GetString("Timeout is invalid, it should not exceed {0}", maxTimeout)); } } }