From 2709380c866085a167bd423bfdcee5d622d75b3e Mon Sep 17 00:00:00 2001 From: database64128 Date: Thu, 12 Nov 2020 21:16:41 +0800 Subject: [PATCH] =?UTF-8?q?5=EF=B8=8F=E2=83=A3=20Update=20to=20`net5.0`=20?= =?UTF-8?q?and=20mass=20cleanups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Settings/InteropSettings.cs | 20 ++ .../Shadowsocks.Interop.csproj | 8 + Shadowsocks.Interop/SsRust/Config.cs | 16 ++ Shadowsocks.Interop/V2Ray/Config.cs | 16 ++ Shadowsocks.Net/Settings/NetSettings.cs | 26 ++ Shadowsocks.Net/Shadowsocks.Net.csproj | 2 +- Shadowsocks.PAC/GeositeUpdater.cs | 7 +- Shadowsocks.PAC/PACSettings.cs | 7 + Shadowsocks.PAC/Shadowsocks.PAC.csproj | 2 +- .../Shadowsocks.Protobuf.csproj | 2 +- .../Behaviors/OnlineConfigResolver.cs | 67 ----- Shadowsocks.WPF/Behaviors/Utilities.cs | 125 --------- .../Localization/LocalizationProvider.cs | 2 +- Shadowsocks.WPF/Models/AppSettings.cs | 22 ++ Shadowsocks.WPF/Models/Server.cs | 252 ------------------ Shadowsocks.WPF/Models/Settings.cs | 22 +- .../PublishProfiles/FolderProfile.pubxml | 4 +- .../PublishProfiles/FolderProfile.pubxml.user | 6 - .../Properties/PublishProfiles/win-arm.pubxml | 18 ++ .../Properties/PublishProfiles/win-x64.pubxml | 4 +- .../Properties/PublishProfiles/win-x86.pubxml | 4 +- .../Services/OnlineConfigService.cs | 45 ++++ Shadowsocks.WPF/Services/PortForwarder.cs | 28 +- Shadowsocks.WPF/Services/PrivoxyRunner.cs | 63 ++--- Shadowsocks.WPF/Services/Sip003Plugin.cs | 21 +- Shadowsocks.WPF/Services/UpdateChecker.cs | 95 +++---- Shadowsocks.WPF/Shadowsocks.WPF.csproj | 10 +- .../{Behaviors => Utils}/AutoStartup.cs | 51 ++-- .../{Behaviors => Utils}/FileManager.cs | 10 +- .../{Behaviors => Utils}/IPCService.cs | 4 +- .../{Behaviors => Utils}/ProtocolHandler.cs | 28 +- .../{Behaviors => Utils}/SystemProxy.cs | 5 +- Shadowsocks.WPF/Utils/Utilities.cs | 117 ++++++++ .../ViewModels/ForwardProxyViewModel.cs | 14 +- .../ViewModels/OnlineConfigViewModel.cs | 11 - .../ViewModels/ServerSharingViewModel.cs | 18 +- .../VersionUpdatePromptViewModel.cs | 18 +- .../Views/VersionUpdatePromptView.xaml.cs | 4 +- Shadowsocks/Models/Group.cs | 69 +++++ .../Models/JsonSnakeCaseNamingPolicy.cs | 87 ++++++ Shadowsocks/Models/Server.cs | 73 ++++- Shadowsocks/Shadowsocks.csproj | 2 +- shadowsocks-windows.sln | 6 + 43 files changed, 737 insertions(+), 674 deletions(-) create mode 100644 Shadowsocks.Interop/Settings/InteropSettings.cs create mode 100644 Shadowsocks.Interop/Shadowsocks.Interop.csproj create mode 100644 Shadowsocks.Interop/SsRust/Config.cs create mode 100644 Shadowsocks.Interop/V2Ray/Config.cs create mode 100644 Shadowsocks.Net/Settings/NetSettings.cs delete mode 100644 Shadowsocks.WPF/Behaviors/OnlineConfigResolver.cs delete mode 100644 Shadowsocks.WPF/Behaviors/Utilities.cs create mode 100644 Shadowsocks.WPF/Models/AppSettings.cs delete mode 100644 Shadowsocks.WPF/Models/Server.cs delete mode 100644 Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml.user create mode 100644 Shadowsocks.WPF/Properties/PublishProfiles/win-arm.pubxml create mode 100644 Shadowsocks.WPF/Services/OnlineConfigService.cs rename Shadowsocks.WPF/{Behaviors => Utils}/AutoStartup.cs (68%) rename Shadowsocks.WPF/{Behaviors => Utils}/FileManager.cs (90%) rename Shadowsocks.WPF/{Behaviors => Utils}/IPCService.cs (97%) rename Shadowsocks.WPF/{Behaviors => Utils}/ProtocolHandler.cs (69%) rename Shadowsocks.WPF/{Behaviors => Utils}/SystemProxy.cs (94%) create mode 100644 Shadowsocks.WPF/Utils/Utilities.cs create mode 100644 Shadowsocks/Models/Group.cs create mode 100644 Shadowsocks/Models/JsonSnakeCaseNamingPolicy.cs diff --git a/Shadowsocks.Interop/Settings/InteropSettings.cs b/Shadowsocks.Interop/Settings/InteropSettings.cs new file mode 100644 index 00000000..d3aa1f35 --- /dev/null +++ b/Shadowsocks.Interop/Settings/InteropSettings.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shadowsocks.Interop.Settings +{ + public class InteropSettings + { + public string SsRustPath { get; set; } + public string V2RayCorePath { get; set; } + + public InteropSettings() + { + SsRustPath = ""; + V2RayCorePath = ""; + } + } +} diff --git a/Shadowsocks.Interop/Shadowsocks.Interop.csproj b/Shadowsocks.Interop/Shadowsocks.Interop.csproj new file mode 100644 index 00000000..2991887b --- /dev/null +++ b/Shadowsocks.Interop/Shadowsocks.Interop.csproj @@ -0,0 +1,8 @@ + + + + net5.0 + enable + + + diff --git a/Shadowsocks.Interop/SsRust/Config.cs b/Shadowsocks.Interop/SsRust/Config.cs new file mode 100644 index 00000000..1be66e0c --- /dev/null +++ b/Shadowsocks.Interop/SsRust/Config.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shadowsocks.Interop.SsRust +{ + public class Config + { + public Config() + { + + } + } +} diff --git a/Shadowsocks.Interop/V2Ray/Config.cs b/Shadowsocks.Interop/V2Ray/Config.cs new file mode 100644 index 00000000..4ef63bfb --- /dev/null +++ b/Shadowsocks.Interop/V2Ray/Config.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Shadowsocks.Interop.V2Ray +{ + public class Config + { + public Config() + { + + } + } +} diff --git a/Shadowsocks.Net/Settings/NetSettings.cs b/Shadowsocks.Net/Settings/NetSettings.cs new file mode 100644 index 00000000..7f432a9b --- /dev/null +++ b/Shadowsocks.Net/Settings/NetSettings.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Shadowsocks.Net.Settings +{ + public class NetSettings + { + public bool EnableSocks5 { get; set; } + public bool EnableHttp { get; set; } + public string Socks5ListeningAddress { get; set; } + public string HttpListeningAddress { get; set; } + public int Socks5ListeningPort { get; set; } + public int HttpListeningPort { get; set; } + + public NetSettings() + { + EnableSocks5 = true; + EnableHttp = true; + Socks5ListeningAddress = "::1"; + HttpListeningAddress = "::1"; + Socks5ListeningPort = 1080; + HttpListeningPort = 1080; + } + } +} diff --git a/Shadowsocks.Net/Shadowsocks.Net.csproj b/Shadowsocks.Net/Shadowsocks.Net.csproj index dbca59da..233b8b68 100644 --- a/Shadowsocks.Net/Shadowsocks.Net.csproj +++ b/Shadowsocks.Net/Shadowsocks.Net.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net5.0 diff --git a/Shadowsocks.PAC/GeositeUpdater.cs b/Shadowsocks.PAC/GeositeUpdater.cs index 75400e89..ac3af95e 100644 --- a/Shadowsocks.PAC/GeositeUpdater.cs +++ b/Shadowsocks.PAC/GeositeUpdater.cs @@ -25,8 +25,6 @@ namespace Shadowsocks.PAC { public event EventHandler? UpdateCompleted; - public event ErrorEventHandler? Error; - private readonly string DATABASE_PATH; private readonly string GEOSITE_URL = "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat"; @@ -65,7 +63,6 @@ namespace Shadowsocks.PAC public void ResetEvent() { UpdateCompleted = null; - Error = null; } public async Task UpdatePACFromGeosite(PACSettings pACSettings) @@ -127,9 +124,9 @@ namespace Shadowsocks.PAC bool pacFileChanged = MergeAndWritePACFile(pACSettings.GeositeDirectGroups, pACSettings.GeositeProxiedGroups, blacklist); UpdateCompleted?.Invoke(null, new GeositeResultEventArgs(pacFileChanged)); } - catch (Exception ex) + catch (Exception e) { - Error?.Invoke(null, new ErrorEventArgs(ex)); + this.Log().Error(e, "An error occurred while updating PAC."); } } diff --git a/Shadowsocks.PAC/PACSettings.cs b/Shadowsocks.PAC/PACSettings.cs index dc974527..7968b466 100644 --- a/Shadowsocks.PAC/PACSettings.cs +++ b/Shadowsocks.PAC/PACSettings.cs @@ -16,6 +16,7 @@ namespace Shadowsocks.PAC RegeneratePacOnVersionUpdate = true; CustomPACUrl = ""; CustomGeositeUrl = ""; + CustomGeositeSha256SumUrl = ""; GeositeDirectGroups = new List() { "private", @@ -67,6 +68,12 @@ namespace Shadowsocks.PAC /// public string CustomGeositeUrl { get; set; } + /// + /// Specifies the custom Geosite database's corresponding SHA256 checksum download URL. + /// Leave empty to disable checksum verification for your custom Geosite database. + /// + public string CustomGeositeSha256SumUrl { get; set; } + /// /// A list of Geosite groups /// that we use direct connection for. diff --git a/Shadowsocks.PAC/Shadowsocks.PAC.csproj b/Shadowsocks.PAC/Shadowsocks.PAC.csproj index 24d46a25..7526d87d 100644 --- a/Shadowsocks.PAC/Shadowsocks.PAC.csproj +++ b/Shadowsocks.PAC/Shadowsocks.PAC.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net5.0 enable diff --git a/Shadowsocks.Protobuf/Shadowsocks.Protobuf.csproj b/Shadowsocks.Protobuf/Shadowsocks.Protobuf.csproj index 7ef6635d..6775442b 100644 --- a/Shadowsocks.Protobuf/Shadowsocks.Protobuf.csproj +++ b/Shadowsocks.Protobuf/Shadowsocks.Protobuf.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net5.0 diff --git a/Shadowsocks.WPF/Behaviors/OnlineConfigResolver.cs b/Shadowsocks.WPF/Behaviors/OnlineConfigResolver.cs deleted file mode 100644 index 750f43da..00000000 --- a/Shadowsocks.WPF/Behaviors/OnlineConfigResolver.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using Shadowsocks.Models; - -namespace Shadowsocks.WPF.Behaviors -{ - public class OnlineConfigResolver - { - public static async Task> GetOnline(string url) - { - var httpClient = Program.MainController.GetHttpClient(); - string server_json = await httpClient.GetStringAsync(url); - var servers = server_json.GetServers(); - foreach (var server in servers) - { - server.group = url; - } - return servers.ToList(); - } - } - - internal static class OnlineConfigResolverEx - { - private static readonly string[] BASIC_FORMAT = new[] { "server", "server_port", "password", "method" }; - - private static readonly IEnumerable EMPTY_SERVERS = Array.Empty(); - - internal static IEnumerable GetServers(this string json) => - JToken.Parse(json).SearchJToken().AsEnumerable(); - - private static IEnumerable SearchJArray(JArray array) => - array == null ? EMPTY_SERVERS : array.SelectMany(SearchJToken).ToList(); - - private static IEnumerable SearchJObject(JObject obj) - { - if (obj == null) - return EMPTY_SERVERS; - - if (BASIC_FORMAT.All(field => obj.ContainsKey(field))) - return new[] { obj.ToObject() }; - - var servers = new List(); - foreach (var kv in obj) - { - var token = kv.Value; - servers.AddRange(SearchJToken(token)); - } - return servers; - } - - private static IEnumerable SearchJToken(this JToken token) - { - switch (token.Type) - { - default: - return Array.Empty(); - case JTokenType.Object: - return SearchJObject(token as JObject); - case JTokenType.Array: - return SearchJArray(token as JArray); - } - } - } -} diff --git a/Shadowsocks.WPF/Behaviors/Utilities.cs b/Shadowsocks.WPF/Behaviors/Utilities.cs deleted file mode 100644 index 45c13110..00000000 --- a/Shadowsocks.WPF/Behaviors/Utilities.cs +++ /dev/null @@ -1,125 +0,0 @@ -using Microsoft.Win32; -using NLog; -using System; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.Runtime.InteropServices; -using System.Windows.Forms; -using ZXing; -using ZXing.Common; -using ZXing.QrCode; - -namespace Shadowsocks.WPF.Behaviors -{ - public static class Utilities - { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - private static string _tempPath = null; - - // return path to store temporary files - public static string GetTempPath() - { - if (_tempPath == null) - { - bool isPortableMode = Configuration.Load().portableMode; - try - { - if (isPortableMode) - { - _tempPath = Directory.CreateDirectory("ss_win_temp").FullName; - // don't use "/", it will fail when we call explorer /select xxx/ss_win_temp\xxx.log - } - else - { - _tempPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), @"Shadowsocks\ss_win_temp_" + Program.ExecutablePath.GetHashCode())).FullName; - } - } - catch (Exception e) - { - logger.Error(e); - throw; - } - } - return _tempPath; - } - - // return a full path with filename combined which pointed to the temporary directory - public static string GetTempPath(string filename) => Path.Combine(GetTempPath(), filename); - - public static string ScanQRCodeFromScreen() - { - foreach (Screen screen in Screen.AllScreens) - { - using (Bitmap fullImage = new Bitmap(screen.Bounds.Width, - screen.Bounds.Height)) - { - using (Graphics g = Graphics.FromImage(fullImage)) - { - g.CopyFromScreen(screen.Bounds.X, - screen.Bounds.Y, - 0, 0, - fullImage.Size, - CopyPixelOperation.SourceCopy); - } - int maxTry = 10; - for (int i = 0; i < maxTry; i++) - { - int marginLeft = (int)((double)fullImage.Width * i / 2.5 / maxTry); - int marginTop = (int)((double)fullImage.Height * i / 2.5 / maxTry); - Rectangle cropRect = new Rectangle(marginLeft, marginTop, fullImage.Width - marginLeft * 2, fullImage.Height - marginTop * 2); - Bitmap target = new Bitmap(screen.Bounds.Width, screen.Bounds.Height); - - double imageScale = (double)screen.Bounds.Width / (double)cropRect.Width; - using (Graphics g = Graphics.FromImage(target)) - { - g.DrawImage(fullImage, new Rectangle(0, 0, target.Width, target.Height), - cropRect, - GraphicsUnit.Pixel); - } - var source = new BitmapLuminanceSource(target); - var bitmap = new BinaryBitmap(new HybridBinarizer(source)); - QRCodeReader reader = new QRCodeReader(); - var result = reader.decode(bitmap); - if (result != null) - return result.Text; - } - } - } - return null; - } - - public static void OpenInBrowser(string url) - { - try - { - Process.Start(url); - } - catch - { - // hack because of this: https://github.com/dotnet/corefx/issues/10361 - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Process.Start(new ProcessStartInfo(url) - { - UseShellExecute = true, - Verb = "open" - }); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - Process.Start("xdg-open", url); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - Process.Start("open", url); - } - else - { - throw; - } - } - } - } -} diff --git a/Shadowsocks.WPF/Localization/LocalizationProvider.cs b/Shadowsocks.WPF/Localization/LocalizationProvider.cs index 986e6c84..41358fed 100644 --- a/Shadowsocks.WPF/Localization/LocalizationProvider.cs +++ b/Shadowsocks.WPF/Localization/LocalizationProvider.cs @@ -7,6 +7,6 @@ namespace Shadowsocks.WPF.Localization { private static readonly string CallingAssemblyName = Assembly.GetCallingAssembly().GetName().Name; - public T GetLocalizedValue(string key) => LocExtension.GetLocalizedValue($"{CallingAssemblyName}:Strings:{key}"); + public static T GetLocalizedValue(string key) => LocExtension.GetLocalizedValue($"{CallingAssemblyName}:Strings:{key}"); } } diff --git a/Shadowsocks.WPF/Models/AppSettings.cs b/Shadowsocks.WPF/Models/AppSettings.cs new file mode 100644 index 00000000..c580a8a1 --- /dev/null +++ b/Shadowsocks.WPF/Models/AppSettings.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Shadowsocks.WPF.Models +{ + public class AppSettings + { + public bool StartOnBoot { get; set; } + public bool AssociateSsLinks { get; set; } + public bool VersionUpdateCheckForPrerelease { get; set; } + public string SkippedUpdateVersion { get; set; } + + public AppSettings() + { + StartOnBoot = false; + AssociateSsLinks = false; + VersionUpdateCheckForPrerelease = false; + SkippedUpdateVersion = ""; + } + } +} diff --git a/Shadowsocks.WPF/Models/Server.cs b/Shadowsocks.WPF/Models/Server.cs deleted file mode 100644 index 5456c84a..00000000 --- a/Shadowsocks.WPF/Models/Server.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Text; -using System.Web; -using Shadowsocks.Controller; -using System.Text.RegularExpressions; -using System.Linq; -using Newtonsoft.Json; -using System.ComponentModel; - -namespace Shadowsocks.WPF.Models -{ - [Serializable] - public class Server - { - public const string DefaultMethod = "chacha20-ietf-poly1305"; - public const int DefaultPort = 8388; - - #region ParseLegacyURL - private static readonly Regex UrlFinder = new Regex(@"ss://(?[A-Za-z0-9+-/=_]+)(?:#(?\S+))?", RegexOptions.IgnoreCase); - private static readonly Regex DetailsParser = new Regex(@"^((?.+?):(?.*)@(?.+?):(?\d+?))$", RegexOptions.IgnoreCase); - #endregion ParseLegacyURL - - private const int DefaultServerTimeoutSec = 5; - public const int MaxServerTimeoutSec = 20; - - public string server; - public int server_port; - public string password; - public string method; - - // optional fields - [DefaultValue("")] - [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string plugin; - [DefaultValue("")] - [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string plugin_opts; - [DefaultValue("")] - [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string plugin_args; - [DefaultValue("")] - [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string remarks; - - [DefaultValue("")] - [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] - public string group; - - public int timeout; - - public override int GetHashCode() - { - return server.GetHashCode() ^ server_port; - } - - public override bool Equals(object obj) => obj is Server o2 && server == o2.server && server_port == o2.server_port; - - public override string ToString() - { - if (string.IsNullOrEmpty(server)) - { - return I18N.GetString("New server"); - } - - string serverStr = $"{FormalHostName}:{server_port}"; - return string.IsNullOrEmpty(remarks) - ? serverStr - : $"{remarks} ({serverStr})"; - } - - public string GetURL(bool legacyUrl = false) - { - if (legacyUrl && string.IsNullOrWhiteSpace(plugin)) - { - // For backwards compatiblity, if no plugin, use old url format - string p = $"{method}:{password}@{server}:{server_port}"; - string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(p)); - return string.IsNullOrEmpty(remarks) - ? $"ss://{base64}" - : $"ss://{base64}#{HttpUtility.UrlEncode(remarks, Encoding.UTF8)}"; - } - - UriBuilder u = new UriBuilder("ss", null); - string b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{method}:{password}")); - u.UserName = b64.Replace('+', '-').Replace('/', '_').TrimEnd('='); - u.Host = server; - u.Port = server_port; - u.Fragment = HttpUtility.UrlEncode(remarks, Encoding.UTF8); - - if (!string.IsNullOrWhiteSpace(plugin)) - { - NameValueCollection param = HttpUtility.ParseQueryString(""); - - string pluginPart = plugin; - if (!string.IsNullOrWhiteSpace(plugin_opts)) - { - pluginPart += ";" + plugin_opts; - } - param["plugin"] = pluginPart; - u.Query = param.ToString(); - } - - return u.ToString(); - } - - [JsonIgnore] - public string FormalHostName - { - get - { - // CheckHostName() won't do a real DNS lookup - return (Uri.CheckHostName(server)) switch - { - // Add square bracket when IPv6 (RFC3986) - UriHostNameType.IPv6 => $"[{server}]", - // IPv4 or domain name - _ => server, - }; - } - } - - public Server() - { - server = ""; - server_port = DefaultPort; - method = DefaultMethod; - plugin = ""; - plugin_opts = ""; - plugin_args = ""; - password = ""; - remarks = ""; - timeout = DefaultServerTimeoutSec; - } - - private static Server ParseLegacyURL(string ssURL) - { - var match = UrlFinder.Match(ssURL); - if (!match.Success) - return null; - - Server server = new Server(); - var base64 = match.Groups["base64"].Value.TrimEnd('/'); - var tag = match.Groups["tag"].Value; - if (!string.IsNullOrEmpty(tag)) - { - server.remarks = HttpUtility.UrlDecode(tag, Encoding.UTF8); - } - Match details; - try - { - details = DetailsParser.Match(Encoding.UTF8.GetString(Convert.FromBase64String( - base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '=')))); - } - catch (FormatException) - { - return null; - } - if (!details.Success) - return null; - server.method = details.Groups["method"].Value; - server.password = details.Groups["password"].Value; - server.server = details.Groups["hostname"].Value; - server.server_port = int.Parse(details.Groups["port"].Value); - return server; - } - - public static Server ParseURL(string serverUrl) - { - string _serverUrl = serverUrl.Trim(); - if (!_serverUrl.StartsWith("ss://", StringComparison.InvariantCultureIgnoreCase)) - { - return null; - } - - Server legacyServer = ParseLegacyURL(serverUrl); - if (legacyServer != null) //legacy - { - return legacyServer; - } - else //SIP002 - { - Uri parsedUrl; - try - { - parsedUrl = new Uri(serverUrl); - } - catch (UriFormatException) - { - return null; - } - Server server = new Server - { - remarks = HttpUtility.UrlDecode(parsedUrl.GetComponents( - UriComponents.Fragment, UriFormat.Unescaped), Encoding.UTF8), - server = parsedUrl.IdnHost, - server_port = parsedUrl.Port, - }; - - // parse base64 UserInfo - string rawUserInfo = parsedUrl.GetComponents(UriComponents.UserInfo, UriFormat.Unescaped); - string base64 = rawUserInfo.Replace('-', '+').Replace('_', '/'); // Web-safe base64 to normal base64 - string userInfo; - try - { - userInfo = Encoding.UTF8.GetString(Convert.FromBase64String( - base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '='))); - } - catch (FormatException) - { - return null; - } - string[] userInfoParts = userInfo.Split(new char[] { ':' }, 2); - if (userInfoParts.Length != 2) - { - return null; - } - server.method = userInfoParts[0]; - server.password = userInfoParts[1]; - - NameValueCollection queryParameters = HttpUtility.ParseQueryString(parsedUrl.Query); - string[] pluginParts = (queryParameters["plugin"] ?? "").Split(new[] { ';' }, 2); - if (pluginParts.Length > 0) - { - server.plugin = pluginParts[0] ?? ""; - } - - if (pluginParts.Length > 1) - { - server.plugin_opts = pluginParts[1] ?? ""; - } - - return server; - } - } - - public static List GetServers(string ssURL) - { - return ssURL - .Split('\r', '\n', ' ') - .Select(u => ParseURL(u)) - .Where(s => s != null) - .ToList(); - } - - public string Identifier() - { - return server + ':' + server_port; - } - } -} diff --git a/Shadowsocks.WPF/Models/Settings.cs b/Shadowsocks.WPF/Models/Settings.cs index 4e7d06a7..30407a8d 100644 --- a/Shadowsocks.WPF/Models/Settings.cs +++ b/Shadowsocks.WPF/Models/Settings.cs @@ -1,3 +1,7 @@ +using Shadowsocks.Interop.Settings; +using Shadowsocks.Models; +using Shadowsocks.Net.Settings; +using Shadowsocks.PAC; using System; using System.Collections.Generic; using System.Text; @@ -6,11 +10,23 @@ namespace Shadowsocks.WPF.Models { public class Settings { + public AppSettings App { get; set; } + + public InteropSettings Interop { get; set; } + + public NetSettings Net { get; set; } + + public PACSettings PAC { get; set; } + + public List Groups { get; set; } + public Settings() { - + App = new AppSettings(); + Interop = new InteropSettings(); + Net = new NetSettings(); + PAC = new PACSettings(); + Groups = new List(); } - - } } diff --git a/Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml b/Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml index b2b41941..dad542e5 100644 --- a/Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml +++ b/Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml @@ -6,9 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Release Any CPU - bin\Release\netcoreapp3.1\publish\ + bin\Release\net5.0-windows\publish\ FileSystem - netcoreapp3.1 - false \ No newline at end of file diff --git a/Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml.user b/Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml.user deleted file mode 100644 index 312c6e3b..00000000 --- a/Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml.user +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/Shadowsocks.WPF/Properties/PublishProfiles/win-arm.pubxml b/Shadowsocks.WPF/Properties/PublishProfiles/win-arm.pubxml new file mode 100644 index 00000000..d38edafb --- /dev/null +++ b/Shadowsocks.WPF/Properties/PublishProfiles/win-arm.pubxml @@ -0,0 +1,18 @@ + + + + + Release + Any CPU + bin\Release\net5.0-windows\win-arm\publish\ + FileSystem + net5.0-windows + win-arm + true + True + False + True + + \ No newline at end of file diff --git a/Shadowsocks.WPF/Properties/PublishProfiles/win-x64.pubxml b/Shadowsocks.WPF/Properties/PublishProfiles/win-x64.pubxml index 0cb60638..d130a3fc 100644 --- a/Shadowsocks.WPF/Properties/PublishProfiles/win-x64.pubxml +++ b/Shadowsocks.WPF/Properties/PublishProfiles/win-x64.pubxml @@ -6,9 +6,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Release Any CPU - bin\Release\netcoreapp3.1\publish\ + bin\Release\net5.0-windows\win-x64\publish\ FileSystem - netcoreapp3.1 + net5.0-windows win-x64 true True diff --git a/Shadowsocks.WPF/Properties/PublishProfiles/win-x86.pubxml b/Shadowsocks.WPF/Properties/PublishProfiles/win-x86.pubxml index 4e6685ac..83be6696 100644 --- a/Shadowsocks.WPF/Properties/PublishProfiles/win-x86.pubxml +++ b/Shadowsocks.WPF/Properties/PublishProfiles/win-x86.pubxml @@ -6,9 +6,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Release Any CPU - bin\Release\netcoreapp3.1\publish\ + bin\Release\net5.0-windows\win-x86\publish\ FileSystem - netcoreapp3.1 + net5.0-windows win-x86 true True diff --git a/Shadowsocks.WPF/Services/OnlineConfigService.cs b/Shadowsocks.WPF/Services/OnlineConfigService.cs new file mode 100644 index 00000000..0a0e4132 --- /dev/null +++ b/Shadowsocks.WPF/Services/OnlineConfigService.cs @@ -0,0 +1,45 @@ +using Shadowsocks.Models; +using Splat; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Shadowsocks.WPF.Services +{ + /// + /// The service for updating a group from an SIP008 online configuration source. + /// + public class OnlineConfigService + { + private Group _group; + private HttpClient _httpClient; + + public OnlineConfigService(Group group) + { + _group = group; + _httpClient = Locator.Current.GetService(); + } + + /// + /// Updates the group from the configured online configuration source. + /// + /// + public async Task Update() + { + // Download + var downloadedGroup = await _httpClient.GetFromJsonAsync(_group.OnlineConfigSource); + if (downloadedGroup == null) + throw new Exception("An error occurred."); + // Merge downloaded group into existing group + _group.Version = downloadedGroup.Version; + _group.BytesUsed = downloadedGroup.BytesUsed; + _group.BytesRemaining = downloadedGroup.BytesRemaining; + _group.Servers = downloadedGroup.Servers; // TODO: preserve per-server statistics + } + } +} diff --git a/Shadowsocks.WPF/Services/PortForwarder.cs b/Shadowsocks.WPF/Services/PortForwarder.cs index 966d86fd..b8c363c6 100644 --- a/Shadowsocks.WPF/Services/PortForwarder.cs +++ b/Shadowsocks.WPF/Services/PortForwarder.cs @@ -1,8 +1,8 @@ +using Shadowsocks.Net; +using Splat; using System; using System.Net; using System.Net.Sockets; -using NLog; -using Shadowsocks.Net; namespace Shadowsocks.WPF.Services { @@ -39,10 +39,8 @@ namespace Shadowsocks.WPF.Services return true; } - private class Handler + private class Handler : IEnableLogger { - private static Logger logger = LogManager.GetCurrentClassLogger(); - private byte[] _firstPacket; private int _firstPacketLength; private Socket _local; @@ -74,7 +72,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, ""); Close(); } } @@ -93,7 +91,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, ""); Close(); } } @@ -110,7 +108,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, ""); Close(); } } @@ -131,7 +129,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, ""); Close(); } } @@ -158,7 +156,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, ""); Close(); } } @@ -185,7 +183,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, ""); Close(); } } @@ -204,7 +202,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, ""); Close(); } } @@ -223,7 +221,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, ""); Close(); } } @@ -255,7 +253,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, ""); } } if (_remote != null) @@ -267,7 +265,7 @@ namespace Shadowsocks.WPF.Services } catch (SocketException e) { - logger.LogUsefulException(e); + this.Log().Error(e, ""); } } } diff --git a/Shadowsocks.WPF/Services/PrivoxyRunner.cs b/Shadowsocks.WPF/Services/PrivoxyRunner.cs index 676b04c4..80b14ba0 100644 --- a/Shadowsocks.WPF/Services/PrivoxyRunner.cs +++ b/Shadowsocks.WPF/Services/PrivoxyRunner.cs @@ -1,3 +1,6 @@ +using Shadowsocks.Net.Settings; +using Shadowsocks.WPF.Utils; +using Splat; using System; using System.Diagnostics; using System.IO; @@ -5,42 +8,35 @@ using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; -using System.Windows.Forms; -using NLog; -using Shadowsocks.Model; -using Shadowsocks.Properties; -using Shadowsocks.Util; -using Shadowsocks.Util.ProcessManagement; namespace Shadowsocks.WPF.Services { - class PrivoxyRunner + public class PrivoxyRunner : IEnableLogger { - private static Logger logger = LogManager.GetCurrentClassLogger(); - private static int _uid; - private static string _uniqueConfigFile; - private Process _process; + private static string _uniqueConfigFile = ""; + private Process? _process; private int _runningPort; - static PrivoxyRunner() + public PrivoxyRunner() { try { - _uid = Program.WorkingDirectory.GetHashCode(); // Currently we use ss's StartupPath to identify different Privoxy instance. + _uid = Utils.Utilities.WorkingDirectory.GetHashCode(); // Currently we use ss's StartupPath to identify different Privoxy instance. _uniqueConfigFile = $"privoxy_{_uid}.conf"; - FileManager.UncompressFile(Utils.GetTempPath("ss_privoxy.exe"), Resources.privoxy_exe); + FileManager.UncompressFile(Utils.Utilities.GetTempPath("ss_privoxy.exe"), Properties.Resources.privoxy_exe); } catch (IOException e) { - logger.LogUsefulException(e); + this.Log().Error(e, "An error occurred while starting Privoxy."); + _uniqueConfigFile = ""; } } public int RunningPort => _runningPort; - public void Start(Configuration configuration) + public void Start(NetSettings netSettings) { if (_process == null) { @@ -49,16 +45,13 @@ namespace Shadowsocks.WPF.Services { KillProcess(p); } - string privoxyConfig = Resources.privoxy_conf; - _runningPort = GetFreePort(configuration.isIPv6Enabled); - privoxyConfig = privoxyConfig.Replace("__SOCKS_PORT__", configuration.localPort.ToString()); + string privoxyConfig = Properties.Resources.privoxy_conf; + _runningPort = GetFreePort(netSettings); + privoxyConfig = privoxyConfig.Replace("__SOCKS_PORT__", netSettings.Socks5ListeningPort.ToString()); privoxyConfig = privoxyConfig.Replace("__PRIVOXY_BIND_PORT__", _runningPort.ToString()); - privoxyConfig = configuration.isIPv6Enabled - ? privoxyConfig.Replace("__PRIVOXY_BIND_IP__", configuration.shareOverLan ? "[::]" : "[::1]") - .Replace("__SOCKS_HOST__", "[::1]") - : privoxyConfig.Replace("__PRIVOXY_BIND_IP__", configuration.shareOverLan ? "0.0.0.0" : "127.0.0.1") - .Replace("__SOCKS_HOST__", "127.0.0.1"); - FileManager.ByteArrayToFile(Utils.GetTempPath(_uniqueConfigFile), Encoding.UTF8.GetBytes(privoxyConfig)); + privoxyConfig = privoxyConfig.Replace("__PRIVOXY_BIND_IP__", $"[{netSettings.Socks5ListeningAddress}]") + .Replace("__SOCKS_HOST__", "[::1]"); // TODO: make sure it's correct + FileManager.ByteArrayToFile(Utils.Utilities.GetTempPath(_uniqueConfigFile), Encoding.UTF8.GetBytes(privoxyConfig)); _process = new Process { @@ -67,7 +60,7 @@ namespace Shadowsocks.WPF.Services { FileName = "ss_privoxy.exe", Arguments = _uniqueConfigFile, - WorkingDirectory = Utils.GetTempPath(), + WorkingDirectory = Utils.Utilities.GetTempPath(), WindowStyle = ProcessWindowStyle.Hidden, UseShellExecute = true, CreateNoWindow = true @@ -87,7 +80,7 @@ namespace Shadowsocks.WPF.Services } } - private static void KillProcess(Process p) + private void KillProcess(Process p) { try { @@ -101,7 +94,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, "An error occurred while stopping Privoxy."); } } @@ -115,16 +108,16 @@ namespace Shadowsocks.WPF.Services * UID is hash of ss's location. */ - private static bool IsChildProcess(Process process) + private bool IsChildProcess(Process process) { try { /* * Under PortableMode, we could identify it by the path of ss_privoxy.exe. */ - var path = process.MainModule.FileName; + var path = process.MainModule?.FileName; - return Utils.GetTempPath("ss_privoxy.exe").Equals(path); + return Utils.Utilities.GetTempPath("ss_privoxy.exe").Equals(path); } catch (Exception ex) @@ -134,18 +127,18 @@ namespace Shadowsocks.WPF.Services * are already dead, and that will cause exceptions here. * We could simply ignore those exceptions. */ - logger.LogUsefulException(ex); + this.Log().Error(ex, ""); return false; } } - private int GetFreePort(bool isIPv6 = false) + private int GetFreePort(NetSettings netSettings) { int defaultPort = 8123; try { // TCP stack please do me a favor - TcpListener l = new TcpListener(isIPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback, 0); + TcpListener l = new TcpListener(IPAddress.Parse(netSettings.Socks5ListeningAddress), 0); l.Start(); var port = ((IPEndPoint)l.LocalEndpoint).Port; l.Stop(); @@ -154,7 +147,7 @@ namespace Shadowsocks.WPF.Services catch (Exception e) { // in case access denied - logger.LogUsefulException(e); + this.Log().Error(e, ""); return defaultPort; } } diff --git a/Shadowsocks.WPF/Services/Sip003Plugin.cs b/Shadowsocks.WPF/Services/Sip003Plugin.cs index bee5f70d..e00c698b 100644 --- a/Shadowsocks.WPF/Services/Sip003Plugin.cs +++ b/Shadowsocks.WPF/Services/Sip003Plugin.cs @@ -1,11 +1,10 @@ +using Shadowsocks.Models; using System; using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; -using Shadowsocks.Model; -using Shadowsocks.Util.ProcessManagement; namespace Shadowsocks.WPF.Services { @@ -27,17 +26,17 @@ namespace Shadowsocks.WPF.Services throw new ArgumentNullException(nameof(server)); } - if (string.IsNullOrWhiteSpace(server.plugin)) + if (string.IsNullOrWhiteSpace(server.Plugin)) { return null; } return new Sip003Plugin( - server.plugin, - server.plugin_opts, + server.Plugin, + server.PluginOpts, server.plugin_args, - server.server, - server.server_port, + server.Host, + server.Port, showPluginOutput); } @@ -63,7 +62,7 @@ namespace Shadowsocks.WPF.Services CreateNoWindow = !showPluginOutput, ErrorDialog = false, WindowStyle = ProcessWindowStyle.Hidden, - WorkingDirectory = Program.WorkingDirectory ?? Environment.CurrentDirectory, + WorkingDirectory = Utils.Utilities.WorkingDirectory ?? Environment.CurrentDirectory, Environment = { ["SS_REMOTE_HOST"] = serverAddress, @@ -106,11 +105,10 @@ namespace Shadowsocks.WPF.Services // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d if (ex.NativeErrorCode == 0x00000002) { - throw new FileNotFoundException(I18N.GetString("Cannot find the plugin program file"), _pluginProcess.StartInfo.FileName, ex); + throw new FileNotFoundException("Cannot find the plugin program file", _pluginProcess.StartInfo.FileName, ex); } - throw new ApplicationException(I18N.GetString("Plugin Program"), ex); + throw new ApplicationException("Plugin Program", ex); } - _pluginJob.AddProcess(_pluginProcess.Handle); _started = true; } @@ -162,7 +160,6 @@ namespace Shadowsocks.WPF.Services try { _pluginProcess.Dispose(); - _pluginJob.Dispose(); } catch (Exception) { } diff --git a/Shadowsocks.WPF/Services/UpdateChecker.cs b/Shadowsocks.WPF/Services/UpdateChecker.cs index 0ec99435..54692664 100644 --- a/Shadowsocks.WPF/Services/UpdateChecker.cs +++ b/Shadowsocks.WPF/Services/UpdateChecker.cs @@ -1,23 +1,20 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Net; using System.Net.Http; using System.Reflection; -using System.Text.RegularExpressions; +using System.Text.Json; using System.Threading.Tasks; using System.Windows; -using Newtonsoft.Json.Linq; using NLog; -using Shadowsocks.Localization; -using Shadowsocks.Model; -using Shadowsocks.Util; -using Shadowsocks.Views; +using Shadowsocks.WPF.Localization; +using Shadowsocks.WPF.Models; +using Shadowsocks.WPF.Views; +using Splat; namespace Shadowsocks.WPF.Services { - public class UpdateChecker + public class UpdateChecker : IEnableLogger { private readonly Logger logger; private readonly HttpClient httpClient; @@ -25,24 +22,24 @@ namespace Shadowsocks.WPF.Services // https://developer.github.com/v3/repos/releases/ private const string UpdateURL = "https://api.github.com/repos/shadowsocks/shadowsocks-windows/releases"; - private Configuration _config; - private Window versionUpdatePromptWindow; - private JToken _releaseObject; + private Window? versionUpdatePromptWindow; + private JsonElement _releaseObject; public string NewReleaseVersion { get; private set; } public string NewReleaseZipFilename { get; private set; } - public event EventHandler CheckUpdateCompleted; + public event EventHandler? CheckUpdateCompleted; - public static readonly string Version = Assembly.GetEntryAssembly().GetName().Version.ToString(); + public static readonly string Version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "5.0.0"; private readonly Version _version; public UpdateChecker() { logger = LogManager.GetCurrentClassLogger(); - httpClient = Program.MainController.GetHttpClient(); + httpClient = Locator.Current.GetService(); _version = new Version(Version); - _config = Program.MainController.GetCurrentConfiguration(); + NewReleaseVersion = ""; + NewReleaseZipFilename = ""; } /// @@ -55,30 +52,33 @@ namespace Shadowsocks.WPF.Services // delay logger.Info($"Waiting for {millisecondsDelay}ms before checking for version update."); await Task.Delay(millisecondsDelay); - // update _config so we would know if the user checked or unchecked pre-release checks - _config = Program.MainController.GetCurrentConfiguration(); // start logger.Info($"Checking for version update."); + var appSettings = Locator.Current.GetService(); try { // list releases via API - var releasesListJsonString = await httpClient.GetStringAsync(UpdateURL); + var releasesListJsonStream = await httpClient.GetStreamAsync(UpdateURL); // parse - var releasesJArray = JArray.Parse(releasesListJsonString); - foreach (var releaseObject in releasesJArray) + using (JsonDocument jsonDocument = await JsonDocument.ParseAsync(releasesListJsonStream)) { - var releaseTagName = (string)releaseObject["tag_name"]; - var releaseVersion = new Version(releaseTagName); - if (releaseTagName == _config.skippedUpdateVersion) // finished checking - break; - if (releaseVersion.CompareTo(_version) > 0 && - (!(bool)releaseObject["prerelease"] || _config.checkPreRelease && (bool)releaseObject["prerelease"])) // selected + var releasesList = jsonDocument.RootElement; + foreach (var releaseObject in releasesList.EnumerateArray()) { - logger.Info($"Found new version {releaseTagName}."); - _releaseObject = releaseObject; - NewReleaseVersion = releaseTagName; - AskToUpdate(releaseObject); - return; + var releaseTagName = releaseObject.GetProperty("tag_name").GetString(); + var releaseVersion = new Version(releaseTagName ?? "5.0.0"); + var releaseIsPrerelease = releaseObject.GetProperty("prerelease").GetBoolean(); + if (releaseTagName == appSettings.SkippedUpdateVersion) // finished checking + break; + if (releaseVersion.CompareTo(_version) > 0 && + (!releaseIsPrerelease || appSettings.VersionUpdateCheckForPrerelease && releaseIsPrerelease)) // selected + { + logger.Info($"Found new version {releaseTagName}."); + _releaseObject = releaseObject; + NewReleaseVersion = releaseTagName ?? ""; + AskToUpdate(releaseObject); + return; + } } } logger.Info($"No new versions found."); @@ -86,7 +86,7 @@ namespace Shadowsocks.WPF.Services } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, "An error occurred while checking for version updates."); } } @@ -94,7 +94,7 @@ namespace Shadowsocks.WPF.Services /// Opens a window to show the update's information. /// /// The update release object. - private void AskToUpdate(JToken releaseObject) + private void AskToUpdate(JsonElement releaseObject) { if (versionUpdatePromptWindow == null) { @@ -113,7 +113,7 @@ namespace Shadowsocks.WPF.Services versionUpdatePromptWindow.Activate(); } - private void VersionUpdatePromptWindow_Closed(object sender, EventArgs e) + private void VersionUpdatePromptWindow_Closed(object? sender, EventArgs e) { versionUpdatePromptWindow = null; } @@ -126,14 +126,14 @@ namespace Shadowsocks.WPF.Services { try { - var assets = (JArray)_releaseObject["assets"]; + var assets = _releaseObject.GetProperty("assets"); // download all assets - foreach (JObject asset in assets) + foreach (var asset in assets.EnumerateArray()) { - var filename = (string)asset["name"]; - var browser_download_url = (string)asset["browser_download_url"]; + var filename = asset.GetProperty("name").GetString(); + var browser_download_url = asset.GetProperty("browser_download_url").GetString(); var response = await httpClient.GetAsync(browser_download_url); - using (var downloadedFileStream = File.Create(Utils.GetTempPath(filename))) + using (var downloadedFileStream = File.Create(Utils.Utilities.GetTempPath(filename))) await response.Content.CopyToAsync(downloadedFileStream); logger.Info($"Downloaded {filename}."); // store .zip filename @@ -143,11 +143,11 @@ namespace Shadowsocks.WPF.Services logger.Info("Finished downloading."); // notify user CloseVersionUpdatePromptWindow(); - Process.Start("explorer.exe", $"/select, \"{Utils.GetTempPath(NewReleaseZipFilename)}\""); + Process.Start("explorer.exe", $"/select, \"{Utils.Utilities.GetTempPath(NewReleaseZipFilename)}\""); } catch (Exception e) { - logger.LogUsefulException(e); + this.Log().Error(e, "An error occurred while downloading the version update."); } } @@ -156,10 +156,13 @@ namespace Shadowsocks.WPF.Services /// public void SkipUpdate() { - var version = (string)_releaseObject["tag_name"] ?? ""; - _config.skippedUpdateVersion = version; - Program.MainController.SaveSkippedUpdateVerion(version); - logger.Info($"The update {version} has been skipped and will be ignored next time."); + var appSettings = Locator.Current.GetService(); + if (_releaseObject.TryGetProperty("tag_name", out var tagNameJsonElement) && tagNameJsonElement.GetString() is string version) + { + appSettings.SkippedUpdateVersion = version; + // TODO: signal settings change + logger.Info($"The update {version} has been skipped and will be ignored next time."); + } CloseVersionUpdatePromptWindow(); } diff --git a/Shadowsocks.WPF/Shadowsocks.WPF.csproj b/Shadowsocks.WPF/Shadowsocks.WPF.csproj index 4c7342d0..af35c202 100644 --- a/Shadowsocks.WPF/Shadowsocks.WPF.csproj +++ b/Shadowsocks.WPF/Shadowsocks.WPF.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net5.0-windows true clowwindy & community 2020 Shadowsocks.WPF @@ -50,7 +50,7 @@ - + @@ -81,7 +81,9 @@ + + @@ -113,4 +115,8 @@ + + + + \ No newline at end of file diff --git a/Shadowsocks.WPF/Behaviors/AutoStartup.cs b/Shadowsocks.WPF/Utils/AutoStartup.cs similarity index 68% rename from Shadowsocks.WPF/Behaviors/AutoStartup.cs rename to Shadowsocks.WPF/Utils/AutoStartup.cs index 065c357b..64ee77da 100644 --- a/Shadowsocks.WPF/Behaviors/AutoStartup.cs +++ b/Shadowsocks.WPF/Utils/AutoStartup.cs @@ -1,36 +1,32 @@ +using Microsoft.Win32; +using Splat; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; -using Microsoft.Win32; -using NLog; -using Shadowsocks.Util; -namespace Shadowsocks.WPF.Behaviors +namespace Shadowsocks.WPF.Utils { - static class AutoStartup + public static class AutoStartup { - private static Logger logger = LogManager.GetCurrentClassLogger(); - - // Don't use Application.ExecutablePath - // see https://stackoverflow.com/questions/12945805/odd-c-sharp-path-issue - - private static string Key = "Shadowsocks_" + Program.ExecutablePath.GetHashCode(); + private static readonly string registryRunKey = @"Software\Microsoft\Windows\CurrentVersion\Run"; + private static readonly string Key = "Shadowsocks_" + Utilities.ExecutablePath.GetHashCode(); public static bool Set(bool enabled) { RegistryKey runKey = null; try { - runKey = Utils.OpenRegKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); + runKey = Registry.CurrentUser.CreateSubKey(registryRunKey, RegistryKeyPermissionCheck.ReadWriteSubTree); if (runKey == null) { - logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); + LogHost.Default.Error(@"Cannot find HKCU\{registryRunKey}."); return false; } if (enabled) { - runKey.SetValue(Key, Program.ExecutablePath); + runKey.SetValue(Key, Process.GetCurrentProcess().MainModule?.FileName); } else { @@ -42,7 +38,7 @@ namespace Shadowsocks.WPF.Behaviors } catch (Exception e) { - logger.LogUsefulException(e); + LogHost.Default.Error(e, "An error occurred while setting auto startup registry entry."); return false; } finally @@ -55,7 +51,9 @@ namespace Shadowsocks.WPF.Behaviors runKey.Dispose(); } catch (Exception e) - { logger.LogUsefulException(e); } + { + LogHost.Default.Error(e, "An error occurred while setting auto startup registry entry."); + } } } } @@ -65,10 +63,10 @@ namespace Shadowsocks.WPF.Behaviors RegistryKey runKey = null; try { - runKey = Utils.OpenRegKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); + runKey = Registry.CurrentUser.CreateSubKey(registryRunKey, RegistryKeyPermissionCheck.ReadWriteSubTree); if (runKey == null) { - logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); + LogHost.Default.Error(@"Cannot find HKCU\{registryRunKey}."); return false; } var check = false; @@ -80,10 +78,11 @@ namespace Shadowsocks.WPF.Behaviors continue; } // Remove other startup keys with the same executable path. fixes #3011 and also assures compatibility with older versions - if (Program.ExecutablePath.Equals(runKey.GetValue(valueName).ToString(), StringComparison.InvariantCultureIgnoreCase)) + if (Utilities.ExecutablePath.Equals(runKey.GetValue(valueName).ToString(), StringComparison.InvariantCultureIgnoreCase) + is bool matchedDuplicate && matchedDuplicate) { runKey.DeleteValue(valueName); - runKey.SetValue(Key, Program.ExecutablePath); + runKey.SetValue(Key, Utilities.ExecutablePath); check = true; } } @@ -91,7 +90,7 @@ namespace Shadowsocks.WPF.Behaviors } catch (Exception e) { - logger.LogUsefulException(e); + LogHost.Default.Error(e, "An error occurred while checking auto startup registry entries."); return false; } finally @@ -104,7 +103,9 @@ namespace Shadowsocks.WPF.Behaviors runKey.Dispose(); } catch (Exception e) - { logger.LogUsefulException(e); } + { + LogHost.Default.Error(e, "An error occurred while checking auto startup registry entries."); + } } } } @@ -132,7 +133,7 @@ namespace Shadowsocks.WPF.Behaviors if (register && !Check()) { // escape command line parameter - string[] args = new List(Program.Args) + string[] args = new List(Environment.GetCommandLineArgs()) .Select(p => p.Replace("\"", "\\\"")) // escape " to \" .Select(p => p.IndexOf(" ") >= 0 ? "\"" + p + "\"" : p) // encapsule with " .ToArray(); @@ -140,13 +141,13 @@ namespace Shadowsocks.WPF.Behaviors // first parameter is process command line parameter // needn't include the name of the executable in the command line RegisterApplicationRestart(cmdline, (int)(ApplicationRestartFlags.RESTART_NO_CRASH | ApplicationRestartFlags.RESTART_NO_HANG)); - logger.Debug("Register restart after system reboot, command line:" + cmdline); + LogHost.Default.Debug("Register restart after system reboot, command line:" + cmdline); } // requested unregister, which has no side effect else if (!register) { UnregisterApplicationRestart(); - logger.Debug("Unregister restart after system reboot"); + LogHost.Default.Debug("Unregister restart after system reboot"); } } } diff --git a/Shadowsocks.WPF/Behaviors/FileManager.cs b/Shadowsocks.WPF/Utils/FileManager.cs similarity index 90% rename from Shadowsocks.WPF/Behaviors/FileManager.cs rename to Shadowsocks.WPF/Utils/FileManager.cs index 477fd167..3e69b0fa 100644 --- a/Shadowsocks.WPF/Behaviors/FileManager.cs +++ b/Shadowsocks.WPF/Utils/FileManager.cs @@ -1,15 +1,13 @@ -using NLog; +using Splat; using System; using System.IO; using System.IO.Compression; using System.Text; -namespace Shadowsocks.WPF.Behaviors +namespace Shadowsocks.WPF.Utils { public static class FileManager { - private static Logger logger = LogManager.GetCurrentClassLogger(); - public static bool ByteArrayToFile(string fileName, byte[] content) { try @@ -20,7 +18,7 @@ namespace Shadowsocks.WPF.Behaviors } catch (Exception ex) { - logger.Error(ex); + LogHost.Default.Error(ex, ""); } return false; } @@ -60,7 +58,7 @@ namespace Shadowsocks.WPF.Behaviors } catch (Exception ex) { - logger.Error(ex); + LogHost.Default.Error(ex, ""); throw ex; } } diff --git a/Shadowsocks.WPF/Behaviors/IPCService.cs b/Shadowsocks.WPF/Utils/IPCService.cs similarity index 97% rename from Shadowsocks.WPF/Behaviors/IPCService.cs rename to Shadowsocks.WPF/Utils/IPCService.cs index 2fe2d2d8..6e3ad4e6 100644 --- a/Shadowsocks.WPF/Behaviors/IPCService.cs +++ b/Shadowsocks.WPF/Utils/IPCService.cs @@ -3,7 +3,7 @@ using System.IO.Pipes; using System.Net; using System.Text; -namespace Shadowsocks.WPF.Behaviors +namespace Shadowsocks.WPF.Utils { class RequestAddUrlEventArgs : EventArgs { @@ -19,7 +19,7 @@ namespace Shadowsocks.WPF.Behaviors { private const int INT32_LEN = 4; private const int OP_OPEN_URL = 1; - private static readonly string PIPE_PATH = $"Shadowsocks\\{Program.ExecutablePath.GetHashCode()}"; + private static readonly string PIPE_PATH = $"Shadowsocks\\{Utilities.ExecutablePath.GetHashCode()}"; public event EventHandler OpenUrlRequested; diff --git a/Shadowsocks.WPF/Behaviors/ProtocolHandler.cs b/Shadowsocks.WPF/Utils/ProtocolHandler.cs similarity index 69% rename from Shadowsocks.WPF/Behaviors/ProtocolHandler.cs rename to Shadowsocks.WPF/Utils/ProtocolHandler.cs index 595ab1fa..9a0dbd2b 100644 --- a/Shadowsocks.WPF/Behaviors/ProtocolHandler.cs +++ b/Shadowsocks.WPF/Utils/ProtocolHandler.cs @@ -1,15 +1,13 @@ using Microsoft.Win32; -using NLog; +using Splat; using System; -namespace Shadowsocks.WPF.Behaviors +namespace Shadowsocks.WPF.Utils { static class ProtocolHandler { const string ssURLRegKey = @"SOFTWARE\Classes\ss"; - private static Logger logger = LogManager.GetCurrentClassLogger(); - public static bool Set(bool enabled) { RegistryKey ssURLAssociation = null; @@ -19,7 +17,7 @@ namespace Shadowsocks.WPF.Behaviors ssURLAssociation = Registry.CurrentUser.CreateSubKey(ssURLRegKey, RegistryKeyPermissionCheck.ReadWriteSubTree); if (ssURLAssociation == null) { - logger.Error(@"Failed to create HKCU\SOFTWARE\Classes\ss to register ss:// association."); + LogHost.Default.Error(@"Failed to create HKCU\SOFTWARE\Classes\ss to register ss:// association."); return false; } if (enabled) @@ -27,19 +25,19 @@ namespace Shadowsocks.WPF.Behaviors ssURLAssociation.SetValue("", "URL:Shadowsocks"); ssURLAssociation.SetValue("URL Protocol", ""); var shellOpen = ssURLAssociation.CreateSubKey("shell").CreateSubKey("open").CreateSubKey("command"); - shellOpen.SetValue("", $"{Program.ExecutablePath} --open-url %1"); - logger.Info(@"Successfully added ss:// association."); + shellOpen.SetValue("", $"{Utilities.ExecutablePath} --open-url %1"); + LogHost.Default.Info(@"Successfully added ss:// association."); } else { Registry.CurrentUser.DeleteSubKeyTree(ssURLRegKey); - logger.Info(@"Successfully removed ss:// association."); + LogHost.Default.Info(@"Successfully removed ss:// association."); } return true; } catch (Exception e) { - logger.LogUsefulException(e); + LogHost.Default.Error(e, "An error occurred while setting ss:// association registry entries."); return false; } finally @@ -52,7 +50,9 @@ namespace Shadowsocks.WPF.Behaviors ssURLAssociation.Dispose(); } catch (Exception e) - { logger.LogUsefulException(e); } + { + LogHost.Default.Error(e, "An error occurred while setting ss:// association registry entries."); + } } } } @@ -70,11 +70,11 @@ namespace Shadowsocks.WPF.Behaviors } var shellOpen = ssURLAssociation.OpenSubKey("shell").OpenSubKey("open").OpenSubKey("command"); - return (string)shellOpen.GetValue("") == $"{Program.ExecutablePath} --open-url %1"; + return (string)shellOpen.GetValue("") == $"{Utilities.ExecutablePath} --open-url %1"; } catch (Exception e) { - logger.LogUsefulException(e); + LogHost.Default.Error(e, "An error occurred while checking ss:// association registry entries."); return false; } finally @@ -87,7 +87,9 @@ namespace Shadowsocks.WPF.Behaviors ssURLAssociation.Dispose(); } catch (Exception e) - { logger.LogUsefulException(e); } + { + LogHost.Default.Error(e, "An error occurred while checking ss:// association registry entries."); + } } } } diff --git a/Shadowsocks.WPF/Behaviors/SystemProxy.cs b/Shadowsocks.WPF/Utils/SystemProxy.cs similarity index 94% rename from Shadowsocks.WPF/Behaviors/SystemProxy.cs rename to Shadowsocks.WPF/Utils/SystemProxy.cs index d003eb86..82acf36f 100644 --- a/Shadowsocks.WPF/Behaviors/SystemProxy.cs +++ b/Shadowsocks.WPF/Utils/SystemProxy.cs @@ -1,14 +1,11 @@ -using NLog; using Shadowsocks.Net.SystemProxy; using Shadowsocks.WPF.Services.SystemProxy; using System.Windows; -namespace Shadowsocks.WPF.Behaviors +namespace Shadowsocks.WPF.Utils { public static class SystemProxy { - private static Logger logger = LogManager.GetCurrentClassLogger(); - public static void Update(Configuration config, bool forceDisable, PACServer pacSrv, bool noRetry = false) { bool global = config.global; diff --git a/Shadowsocks.WPF/Utils/Utilities.cs b/Shadowsocks.WPF/Utils/Utilities.cs new file mode 100644 index 00000000..c63f598f --- /dev/null +++ b/Shadowsocks.WPF/Utils/Utilities.cs @@ -0,0 +1,117 @@ +using Shadowsocks.WPF.Models; +using Splat; +using System; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Runtime.InteropServices; +using System.Windows; +using ZXing; +using ZXing.Common; +using ZXing.QrCode; + +namespace Shadowsocks.WPF.Utils +{ + public static class Utilities + { + private static string _tempPath = null!; + + public static readonly string ExecutablePath = Process.GetCurrentProcess().MainModule?.FileName ?? ""; + public static readonly string WorkingDirectory = Path.GetDirectoryName(ExecutablePath) ?? ""; + + // return path to store temporary files + public static string GetTempPath() + { + if (_tempPath == null) + { + bool isPortableMode = false; // TODO: fix --profile-directory + try + { + if (isPortableMode) + { + _tempPath = Directory.CreateDirectory("ss_win_temp").FullName; + // don't use "/", it will fail when we call explorer /select xxx/ss_win_temp\xxx.log + } + else + { + _tempPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), @"Shadowsocks\ss_win_temp_" + ExecutablePath?.GetHashCode())).FullName; + } + } + catch (Exception e) + { + LogHost.Default.Error(e); + throw; + } + } + return _tempPath; + } + + // return a full path with filename combined which pointed to the temporary directory + public static string GetTempPath(string filename) => Path.Combine(GetTempPath(), filename); + + public static string ScanQRCodeFromScreen() + { + var screenLeft = SystemParameters.VirtualScreenLeft; + var screenTop = SystemParameters.VirtualScreenTop; + var screenWidth = SystemParameters.VirtualScreenWidth; + var screenHeight = SystemParameters.VirtualScreenHeight; + + using (Bitmap bmp = new Bitmap((int)screenWidth, (int)screenHeight)) + { + using (Graphics g = Graphics.FromImage(bmp)) + g.CopyFromScreen((int)screenLeft, (int)screenTop, 0, 0, bmp.Size); + int maxTry = 10; + for (int i = 0; i < maxTry; i++) + { + int marginLeft = (int)((double)bmp.Width * i / 2.5 / maxTry); + int marginTop = (int)((double)bmp.Height * i / 2.5 / maxTry); + Rectangle cropRect = new Rectangle(marginLeft, marginTop, bmp.Width - marginLeft * 2, bmp.Height - marginTop * 2); + Bitmap target = new Bitmap((int)screenWidth, (int)screenHeight); + + double imageScale = screenWidth / cropRect.Width; + using (Graphics g = Graphics.FromImage(target)) + g.DrawImage(bmp, new Rectangle(0, 0, target.Width, target.Height), cropRect, GraphicsUnit.Pixel); + var source = new BitmapLuminanceSource(target); + var bitmap = new BinaryBitmap(new HybridBinarizer(source)); + QRCodeReader reader = new QRCodeReader(); + var result = reader.decode(bitmap); + if (result != null) + return result.Text; + } + } + return ""; + } + + public static void OpenInBrowser(string url) + { + try + { + Process.Start(url); + } + catch + { + // hack because of this: https://github.com/dotnet/corefx/issues/10361 + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Process.Start(new ProcessStartInfo(url) + { + UseShellExecute = true, + Verb = "open" + }); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", url); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url); + } + else + { + throw; + } + } + } + } +} diff --git a/Shadowsocks.WPF/ViewModels/ForwardProxyViewModel.cs b/Shadowsocks.WPF/ViewModels/ForwardProxyViewModel.cs index 607a2e71..0d7e293f 100644 --- a/Shadowsocks.WPF/ViewModels/ForwardProxyViewModel.cs +++ b/Shadowsocks.WPF/ViewModels/ForwardProxyViewModel.cs @@ -1,10 +1,8 @@ -using ReactiveUI; +using ReactiveUI; using ReactiveUI.Fody.Helpers; using ReactiveUI.Validation.Extensions; using ReactiveUI.Validation.Helpers; -using Shadowsocks.Controller; -using Shadowsocks.Model; -using Shadowsocks.View; +using Shadowsocks.WPF.Models; using System.Linq; using System.Reactive; using System.Reactive.Linq; @@ -15,10 +13,6 @@ namespace Shadowsocks.WPF.ViewModels { public ForwardProxyViewModel() { - _config = Program.MainController.GetCurrentConfiguration(); - _controller = Program.MainController; - _menuViewController = Program.MenuController; - if (!_config.proxy.useProxy) NoProxy = true; else if (_config.proxy.proxyType == 0) @@ -64,10 +58,6 @@ namespace Shadowsocks.WPF.ViewModels Cancel = ReactiveCommand.Create(_menuViewController.CloseForwardProxyWindow); } - private readonly Configuration _config; - private readonly ShadowsocksController _controller; - private readonly MenuViewController _menuViewController; - public ValidationHelper AddressRule { get; } public ValidationHelper PortRule { get; } public ValidationHelper TimeoutRule { get; } diff --git a/Shadowsocks.WPF/ViewModels/OnlineConfigViewModel.cs b/Shadowsocks.WPF/ViewModels/OnlineConfigViewModel.cs index 24b40e31..2682b1b0 100644 --- a/Shadowsocks.WPF/ViewModels/OnlineConfigViewModel.cs +++ b/Shadowsocks.WPF/ViewModels/OnlineConfigViewModel.cs @@ -2,10 +2,7 @@ using ReactiveUI; using ReactiveUI.Fody.Helpers; using ReactiveUI.Validation.Extensions; using ReactiveUI.Validation.Helpers; -using Shadowsocks.Controller; using Shadowsocks.WPF.Localization; -using Shadowsocks.Model; -using Shadowsocks.View; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -21,10 +18,6 @@ namespace Shadowsocks.WPF.ViewModels { public OnlineConfigViewModel() { - _config = Program.MainController.GetCurrentConfiguration(); - _controller = Program.MainController; - _menuViewController = Program.MenuController; - Sources = new ObservableCollection(_config.onlineConfigSource); SelectedSource = ""; Address = ""; @@ -91,10 +84,6 @@ namespace Shadowsocks.WPF.ViewModels }); } - private readonly Configuration _config; - private readonly ShadowsocksController _controller; - private readonly MenuViewController _menuViewController; - public ValidationHelper AddressRule { get; } public ReactiveCommand Update { get; } diff --git a/Shadowsocks.WPF/ViewModels/ServerSharingViewModel.cs b/Shadowsocks.WPF/ViewModels/ServerSharingViewModel.cs index 9ddfbd84..ab86896d 100644 --- a/Shadowsocks.WPF/ViewModels/ServerSharingViewModel.cs +++ b/Shadowsocks.WPF/ViewModels/ServerSharingViewModel.cs @@ -1,11 +1,10 @@ -using ReactiveUI; +using ReactiveUI; using ReactiveUI.Fody.Helpers; -using Shadowsocks.Model; +using Shadowsocks.Models; using System; using System.Collections.Generic; using System.Drawing; using System.IO; -using System.Linq; using System.Reactive; using System.Windows; using System.Windows.Media.Imaging; @@ -17,10 +16,9 @@ namespace Shadowsocks.WPF.ViewModels /// /// The view model class for the server sharing user control. /// - public ServerSharingViewModel() + public ServerSharingViewModel(List servers) { - _config = Program.MainController.GetCurrentConfiguration(); - Servers = _config.configs; + Servers = servers; SelectedServer = Servers[0]; this.WhenAnyValue(x => x.SelectedServer) @@ -29,8 +27,6 @@ namespace Shadowsocks.WPF.ViewModels CopyLink = ReactiveCommand.Create(() => Clipboard.SetText(SelectedServerUrl)); } - private readonly Configuration _config; - public ReactiveCommand CopyLink { get; } [Reactive] @@ -40,10 +36,10 @@ namespace Shadowsocks.WPF.ViewModels public Server SelectedServer { get; set; } [Reactive] - public string SelectedServerUrl { get; private set; } + public string SelectedServerUrl { get; private set; } = null!; [Reactive] - public BitmapImage SelectedServerUrlImage { get; private set; } + public BitmapImage SelectedServerUrlImage { get; private set; } = null!; /// /// Called when SelectedServer changed @@ -52,7 +48,7 @@ namespace Shadowsocks.WPF.ViewModels private void UpdateUrlAndImage() { // update SelectedServerUrl - SelectedServerUrl = SelectedServer.GetURL(_config.generateLegacyUrl); + SelectedServerUrl = SelectedServer.ToUrl().AbsoluteUri; // generate QR code var qrCode = ZXing.QrCode.Internal.Encoder.encode(SelectedServerUrl, ZXing.QrCode.Internal.ErrorCorrectionLevel.L); diff --git a/Shadowsocks.WPF/ViewModels/VersionUpdatePromptViewModel.cs b/Shadowsocks.WPF/ViewModels/VersionUpdatePromptViewModel.cs index ab90d196..8ebbac03 100644 --- a/Shadowsocks.WPF/ViewModels/VersionUpdatePromptViewModel.cs +++ b/Shadowsocks.WPF/ViewModels/VersionUpdatePromptViewModel.cs @@ -1,19 +1,23 @@ -using Newtonsoft.Json.Linq; using ReactiveUI; -using Shadowsocks.Controller; +using Shadowsocks.WPF.Services; +using Splat; using System.Reactive; +using System.Text.Json; namespace Shadowsocks.WPF.ViewModels { public class VersionUpdatePromptViewModel : ReactiveObject { - public VersionUpdatePromptViewModel(JToken releaseObject) + public VersionUpdatePromptViewModel(JsonElement releaseObject) { - _updateChecker = Program.MenuController.updateChecker; + _updateChecker = Locator.Current.GetService(); _releaseObject = releaseObject; + var releaseTagName = _releaseObject.GetProperty("tag_name").GetString(); + var releaseNotes = _releaseObject.GetProperty("body").GetString(); + var releaseIsPrerelease = _releaseObject.GetProperty("prerelease").GetBoolean(); ReleaseNotes = string.Concat( - $"# {((bool)_releaseObject["prerelease"] ? "⚠ Pre-release" : "ℹ Release")} {(string)_releaseObject["tag_name"] ?? "Failed to get tag name"}\r\n", - (string)_releaseObject["body"] ?? "Failed to get release notes"); + $"# {(releaseIsPrerelease ? "⚠ Pre-release" : "ℹ Release")} {releaseTagName ?? "Failed to get tag name"}\r\n", + releaseNotes ?? "Failed to get release notes"); Update = ReactiveCommand.CreateFromTask(_updateChecker.DoUpdate); SkipVersion = ReactiveCommand.Create(_updateChecker.SkipUpdate); @@ -21,7 +25,7 @@ namespace Shadowsocks.WPF.ViewModels } private readonly UpdateChecker _updateChecker; - private readonly JToken _releaseObject; + private readonly JsonElement _releaseObject; public string ReleaseNotes { get; } diff --git a/Shadowsocks.WPF/Views/VersionUpdatePromptView.xaml.cs b/Shadowsocks.WPF/Views/VersionUpdatePromptView.xaml.cs index 3bdac4b5..b7a9e152 100644 --- a/Shadowsocks.WPF/Views/VersionUpdatePromptView.xaml.cs +++ b/Shadowsocks.WPF/Views/VersionUpdatePromptView.xaml.cs @@ -1,7 +1,7 @@ -using Newtonsoft.Json.Linq; using ReactiveUI; using Shadowsocks.WPF.ViewModels; using System.Reactive.Disposables; +using System.Text.Json; namespace Shadowsocks.WPF.Views { @@ -10,7 +10,7 @@ namespace Shadowsocks.WPF.Views /// public partial class VersionUpdatePromptView : ReactiveUserControl { - public VersionUpdatePromptView(JToken releaseObject) + public VersionUpdatePromptView(JsonElement releaseObject) { InitializeComponent(); ViewModel = new VersionUpdatePromptViewModel(releaseObject); diff --git a/Shadowsocks/Models/Group.cs b/Shadowsocks/Models/Group.cs new file mode 100644 index 00000000..5cd88fa2 --- /dev/null +++ b/Shadowsocks/Models/Group.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace Shadowsocks.Models +{ + public class Group + { + /// + /// Group name. + /// + public string Name { get; set; } + + /// + /// UUID of the group. + /// + public Guid Id { get; set; } + + /// + /// URL of SIP008 online configuration delivery source. + /// + public string OnlineConfigSource { get; set; } + + /// + /// SIP008 configuration version. + /// + public int Version { get; set; } + + /// + /// A list of servers in the group. + /// + public List Servers { get; set; } + + /// + /// Data used in bytes. + /// The value is fetched from SIP008 provider. + /// + public ulong BytesUsed { get; set; } + + /// + /// Data remaining to be used in bytes. + /// The value is fetched from SIP008 provider. + /// + public ulong BytesRemaining { get; set; } + + public Group() + { + Name = ""; + Id = new Guid(); + OnlineConfigSource = ""; + Version = 1; + BytesUsed = 0UL; + BytesRemaining = 0UL; + Servers = new List(); + } + + public Group(string name) + { + Name = name; + Id = new Guid(); + OnlineConfigSource = ""; + Version = 1; + BytesUsed = 0UL; + BytesRemaining = 0UL; + Servers = new List(); + } + } +} diff --git a/Shadowsocks/Models/JsonSnakeCaseNamingPolicy.cs b/Shadowsocks/Models/JsonSnakeCaseNamingPolicy.cs new file mode 100644 index 00000000..ecd498dd --- /dev/null +++ b/Shadowsocks/Models/JsonSnakeCaseNamingPolicy.cs @@ -0,0 +1,87 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Source: https://github.com/dotnet/corefx/pull/40003 +// See also: https://github.com/dotnet/runtime/issues/782 + +using System; +using System.Text; +using System.Text.Json; + +namespace Shadowsocks.Models +{ + public class JsonSnakeCaseNamingPolicy : JsonNamingPolicy + { + internal enum SnakeCaseState + { + Start, + Lower, + Upper, + NewWord + } + + public override string ConvertName(string name) + { + if (string.IsNullOrEmpty(name)) + { + return name; + } + + var sb = new StringBuilder(); + var state = SnakeCaseState.Start; + + var nameSpan = name.AsSpan(); + + for (int i = 0; i < nameSpan.Length; i++) + { + if (nameSpan[i] == ' ') + { + if (state != SnakeCaseState.Start) + { + state = SnakeCaseState.NewWord; + } + } + else if (char.IsUpper(nameSpan[i])) + { + switch (state) + { + case SnakeCaseState.Upper: + bool hasNext = (i + 1 < nameSpan.Length); + if (i > 0 && hasNext) + { + char nextChar = nameSpan[i + 1]; + if (!char.IsUpper(nextChar) && nextChar != '_') + { + sb.Append('_'); + } + } + break; + case SnakeCaseState.Lower: + case SnakeCaseState.NewWord: + sb.Append('_'); + break; + } + sb.Append(char.ToLowerInvariant(nameSpan[i])); + state = SnakeCaseState.Upper; + } + else if (nameSpan[i] == '_') + { + sb.Append('_'); + state = SnakeCaseState.Start; + } + else + { + if (state == SnakeCaseState.NewWord) + { + sb.Append('_'); + } + + sb.Append(nameSpan[i]); + state = SnakeCaseState.Lower; + } + } + + return sb.ToString(); + } + } +} diff --git a/Shadowsocks/Models/Server.cs b/Shadowsocks/Models/Server.cs index 4eab0cd6..33d59f9e 100644 --- a/Shadowsocks/Models/Server.cs +++ b/Shadowsocks/Models/Server.cs @@ -23,8 +23,9 @@ namespace Shadowsocks.Models public Server() { Host = ""; + Port = 8388; Password = ""; - Method = ""; + Method = "chacha20-ietf-poly1305"; Plugin = ""; PluginOpts = ""; Name = ""; @@ -50,5 +51,75 @@ namespace Shadowsocks.Models Name = name; Uuid = uuid; } + + public override bool Equals(object? obj) => obj is Server server && Uuid == server.Uuid; + public override int GetHashCode() => base.GetHashCode(); + public override string ToString() => Name; + + /// + /// Converts this server object into an ss:// URL. + /// + /// + public Uri ToUrl() + { + UriBuilder uriBuilder = new UriBuilder("ss", Host, Port) + { + UserName = Utilities.Base64Url.Encode($"{Method}:{Password}"), + Fragment = Name, + }; + if (!string.IsNullOrEmpty(Plugin)) + if (!string.IsNullOrEmpty(PluginOpts)) + uriBuilder.Query = $"plugin={Uri.EscapeDataString($"{Plugin};{PluginOpts}")}"; // manually escape as a workaround + else + uriBuilder.Query = $"plugin={Plugin}"; + return uriBuilder.Uri; + } + + /// + /// Tries to parse an ss:// URL into a Server object. + /// + /// The ss:// URL to parse. + /// + /// A Server object represented by the URL. + /// A new empty Server object if the URL is invalid. + /// True for success. False for failure. + public static bool TryParse(string url, out Server server) + { + try + { + var uri = new Uri(url); + if (uri.Scheme != "ss") + throw new ArgumentException("Wrong URL scheme"); + var userinfo_base64url = uri.UserInfo; + var userinfo = Utilities.Base64Url.DecodeToString(userinfo_base64url); + var userinfoSplitArray = userinfo.Split(':', 2); + var method = userinfoSplitArray[0]; + var password = userinfoSplitArray[1]; + server = new Server(uri.Fragment, new Guid().ToString(), uri.Host, uri.Port, password, method); + // find the plugin query + var parsedQueriesArray = uri.Query.Split("?&"); + var pluginQueryContent = ""; + foreach (var query in parsedQueriesArray) + { + if (query.StartsWith("plugin=") && query.Length > 7) + { + pluginQueryContent = query[7..]; // remove "plugin=" + } + } + var unescapedpluginQuery = Uri.UnescapeDataString(pluginQueryContent); + var parsedPluginQueryArray = unescapedpluginQuery.Split(';', 2); + if (parsedPluginQueryArray.Length == 2) // is valid plugin query + { + server.Plugin = parsedPluginQueryArray[0]; + server.PluginOpts = parsedPluginQueryArray[1]; + } + return true; + } + catch + { + server = new Server(); + return false; + } + } } } diff --git a/Shadowsocks/Shadowsocks.csproj b/Shadowsocks/Shadowsocks.csproj index 80fe9451..2991887b 100644 --- a/Shadowsocks/Shadowsocks.csproj +++ b/Shadowsocks/Shadowsocks.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net5.0 enable diff --git a/shadowsocks-windows.sln b/shadowsocks-windows.sln index 577a7b65..db1f1e47 100644 --- a/shadowsocks-windows.sln +++ b/shadowsocks-windows.sln @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.Protobuf", "Sha EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.WPF", "Shadowsocks.WPF\Shadowsocks.WPF.csproj", "{EA1FB2D4-B5A7-47A6-B097-2F4D29E23010}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shadowsocks.Interop", "Shadowsocks.Interop\Shadowsocks.Interop.csproj", "{1CC6E8A9-1875-430C-B2BB-F227ACD711B1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -61,6 +63,10 @@ Global {EA1FB2D4-B5A7-47A6-B097-2F4D29E23010}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA1FB2D4-B5A7-47A6-B097-2F4D29E23010}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA1FB2D4-B5A7-47A6-B097-2F4D29E23010}.Release|Any CPU.Build.0 = Release|Any CPU + {1CC6E8A9-1875-430C-B2BB-F227ACD711B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1CC6E8A9-1875-430C-B2BB-F227ACD711B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1CC6E8A9-1875-430C-B2BB-F227ACD711B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1CC6E8A9-1875-430C-B2BB-F227ACD711B1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE