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