@@ -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 = ""; | |||
} | |||
} | |||
} |
@@ -0,0 +1,8 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>net5.0</TargetFramework> | |||
<Nullable>enable</Nullable> | |||
</PropertyGroup> | |||
</Project> |
@@ -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() | |||
{ | |||
} | |||
} | |||
} |
@@ -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() | |||
{ | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -1,7 +1,7 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
<TargetFramework>net5.0</TargetFramework> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
@@ -25,8 +25,6 @@ namespace Shadowsocks.PAC | |||
{ | |||
public event EventHandler<GeositeResultEventArgs>? 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."); | |||
} | |||
} | |||
@@ -16,6 +16,7 @@ namespace Shadowsocks.PAC | |||
RegeneratePacOnVersionUpdate = true; | |||
CustomPACUrl = ""; | |||
CustomGeositeUrl = ""; | |||
CustomGeositeSha256SumUrl = ""; | |||
GeositeDirectGroups = new List<string>() | |||
{ | |||
"private", | |||
@@ -67,6 +68,12 @@ namespace Shadowsocks.PAC | |||
/// </summary> | |||
public string CustomGeositeUrl { get; set; } | |||
/// <summary> | |||
/// Specifies the custom Geosite database's corresponding SHA256 checksum download URL. | |||
/// Leave empty to disable checksum verification for your custom Geosite database. | |||
/// </summary> | |||
public string CustomGeositeSha256SumUrl { get; set; } | |||
/// <summary> | |||
/// A list of Geosite groups | |||
/// that we use direct connection for. | |||
@@ -1,7 +1,7 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
<TargetFramework>net5.0</TargetFramework> | |||
<Nullable>enable</Nullable> | |||
</PropertyGroup> | |||
@@ -1,7 +1,7 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
<TargetFramework>net5.0</TargetFramework> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
@@ -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<List<Server>> 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<Server> EMPTY_SERVERS = Array.Empty<Server>(); | |||
internal static IEnumerable<Server> GetServers(this string json) => | |||
JToken.Parse(json).SearchJToken().AsEnumerable(); | |||
private static IEnumerable<Server> SearchJArray(JArray array) => | |||
array == null ? EMPTY_SERVERS : array.SelectMany(SearchJToken).ToList(); | |||
private static IEnumerable<Server> SearchJObject(JObject obj) | |||
{ | |||
if (obj == null) | |||
return EMPTY_SERVERS; | |||
if (BASIC_FORMAT.All(field => obj.ContainsKey(field))) | |||
return new[] { obj.ToObject<Server>() }; | |||
var servers = new List<Server>(); | |||
foreach (var kv in obj) | |||
{ | |||
var token = kv.Value; | |||
servers.AddRange(SearchJToken(token)); | |||
} | |||
return servers; | |||
} | |||
private static IEnumerable<Server> SearchJToken(this JToken token) | |||
{ | |||
switch (token.Type) | |||
{ | |||
default: | |||
return Array.Empty<Server>(); | |||
case JTokenType.Object: | |||
return SearchJObject(token as JObject); | |||
case JTokenType.Array: | |||
return SearchJArray(token as JArray); | |||
} | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -7,6 +7,6 @@ namespace Shadowsocks.WPF.Localization | |||
{ | |||
private static readonly string CallingAssemblyName = Assembly.GetCallingAssembly().GetName().Name; | |||
public T GetLocalizedValue<T>(string key) => LocExtension.GetLocalizedValue<T>($"{CallingAssemblyName}:Strings:{key}"); | |||
public static T GetLocalizedValue<T>(string key) => LocExtension.GetLocalizedValue<T>($"{CallingAssemblyName}:Strings:{key}"); | |||
} | |||
} |
@@ -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 = ""; | |||
} | |||
} | |||
} |
@@ -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://(?<base64>[A-Za-z0-9+-/=_]+)(?:#(?<tag>\S+))?", RegexOptions.IgnoreCase); | |||
private static readonly Regex DetailsParser = new Regex(@"^((?<method>.+?):(?<password>.*)@(?<hostname>.+?):(?<port>\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<Server> GetServers(string ssURL) | |||
{ | |||
return ssURL | |||
.Split('\r', '\n', ' ') | |||
.Select(u => ParseURL(u)) | |||
.Where(s => s != null) | |||
.ToList(); | |||
} | |||
public string Identifier() | |||
{ | |||
return server + ':' + server_port; | |||
} | |||
} | |||
} |
@@ -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<Group> Groups { get; set; } | |||
public Settings() | |||
{ | |||
App = new AppSettings(); | |||
Interop = new InteropSettings(); | |||
Net = new NetSettings(); | |||
PAC = new PACSettings(); | |||
Groups = new List<Group>(); | |||
} | |||
} | |||
} |
@@ -6,9 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. | |||
<PropertyGroup> | |||
<Configuration>Release</Configuration> | |||
<Platform>Any CPU</Platform> | |||
<PublishDir>bin\Release\netcoreapp3.1\publish\</PublishDir> | |||
<PublishDir>bin\Release\net5.0-windows\publish\</PublishDir> | |||
<PublishProtocol>FileSystem</PublishProtocol> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
<SelfContained>false</SelfContained> | |||
</PropertyGroup> | |||
</Project> |
@@ -1,6 +0,0 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<!-- | |||
https://go.microsoft.com/fwlink/?LinkID=208121. | |||
--> | |||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
</Project> |
@@ -0,0 +1,18 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<!-- | |||
https://go.microsoft.com/fwlink/?LinkID=208121. | |||
--> | |||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | |||
<PropertyGroup> | |||
<Configuration>Release</Configuration> | |||
<Platform>Any CPU</Platform> | |||
<PublishDir>bin\Release\net5.0-windows\win-arm\publish\</PublishDir> | |||
<PublishProtocol>FileSystem</PublishProtocol> | |||
<TargetFramework>net5.0-windows</TargetFramework> | |||
<RuntimeIdentifier>win-arm</RuntimeIdentifier> | |||
<SelfContained>true</SelfContained> | |||
<PublishSingleFile>True</PublishSingleFile> | |||
<PublishReadyToRun>False</PublishReadyToRun> | |||
<PublishTrimmed>True</PublishTrimmed> | |||
</PropertyGroup> | |||
</Project> |
@@ -6,9 +6,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121. | |||
<PropertyGroup> | |||
<Configuration>Release</Configuration> | |||
<Platform>Any CPU</Platform> | |||
<PublishDir>bin\Release\netcoreapp3.1\publish\</PublishDir> | |||
<PublishDir>bin\Release\net5.0-windows\win-x64\publish\</PublishDir> | |||
<PublishProtocol>FileSystem</PublishProtocol> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
<TargetFramework>net5.0-windows</TargetFramework> | |||
<RuntimeIdentifier>win-x64</RuntimeIdentifier> | |||
<SelfContained>true</SelfContained> | |||
<PublishSingleFile>True</PublishSingleFile> | |||
@@ -6,9 +6,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121. | |||
<PropertyGroup> | |||
<Configuration>Release</Configuration> | |||
<Platform>Any CPU</Platform> | |||
<PublishDir>bin\Release\netcoreapp3.1\publish\</PublishDir> | |||
<PublishDir>bin\Release\net5.0-windows\win-x86\publish\</PublishDir> | |||
<PublishProtocol>FileSystem</PublishProtocol> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
<TargetFramework>net5.0-windows</TargetFramework> | |||
<RuntimeIdentifier>win-x86</RuntimeIdentifier> | |||
<SelfContained>true</SelfContained> | |||
<PublishSingleFile>True</PublishSingleFile> | |||
@@ -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 | |||
{ | |||
/// <summary> | |||
/// The service for updating a group from an SIP008 online configuration source. | |||
/// </summary> | |||
public class OnlineConfigService | |||
{ | |||
private Group _group; | |||
private HttpClient _httpClient; | |||
public OnlineConfigService(Group group) | |||
{ | |||
_group = group; | |||
_httpClient = Locator.Current.GetService<HttpClient>(); | |||
} | |||
/// <summary> | |||
/// Updates the group from the configured online configuration source. | |||
/// </summary> | |||
/// <returns></returns> | |||
public async Task Update() | |||
{ | |||
// Download | |||
var downloadedGroup = await _httpClient.GetFromJsonAsync<Group>(_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 | |||
} | |||
} | |||
} |
@@ -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, ""); | |||
} | |||
} | |||
} | |||
@@ -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; | |||
} | |||
} | |||
@@ -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) { } | |||
@@ -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<HttpClient>(); | |||
_version = new Version(Version); | |||
_config = Program.MainController.GetCurrentConfiguration(); | |||
NewReleaseVersion = ""; | |||
NewReleaseZipFilename = ""; | |||
} | |||
/// <summary> | |||
@@ -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<AppSettings>(); | |||
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. | |||
/// </summary> | |||
/// <param name="releaseObject">The update release object.</param> | |||
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 | |||
/// </summary> | |||
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<AppSettings>(); | |||
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(); | |||
} | |||
@@ -2,7 +2,7 @@ | |||
<PropertyGroup> | |||
<OutputType>Exe</OutputType> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
<TargetFramework>net5.0-windows</TargetFramework> | |||
<UseWPF>true</UseWPF> | |||
<Authors>clowwindy & community 2020</Authors> | |||
<PackageId>Shadowsocks.WPF</PackageId> | |||
@@ -50,7 +50,7 @@ | |||
<PackageReference Include="ReactiveUI.Fody" Version="12.1.5" /> | |||
<PackageReference Include="ReactiveUI.Validation" Version="1.8.6" /> | |||
<PackageReference Include="ReactiveUI.WPF" Version="12.1.5" /> | |||
<PackageReference Include="Splat.NLog" Version="9.6.1" /> | |||
<PackageReference Include="Splat.NLog" Version="9.7.1" /> | |||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20371.2" /> | |||
<PackageReference Include="WPFLocalizeExtension" Version="3.8.0" /> | |||
<PackageReference Include="ZXing.Net" Version="0.16.6" /> | |||
@@ -81,7 +81,9 @@ | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\Shadowsocks.Interop\Shadowsocks.Interop.csproj" /> | |||
<ProjectReference Include="..\Shadowsocks.Net\Shadowsocks.Net.csproj" /> | |||
<ProjectReference Include="..\Shadowsocks.PAC\Shadowsocks.PAC.csproj" /> | |||
<ProjectReference Include="..\Shadowsocks\Shadowsocks.csproj" /> | |||
</ItemGroup> | |||
@@ -113,4 +115,8 @@ | |||
<Resource Include="Resources\ssw128.png" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<Folder Include="Properties\PublishProfiles\" /> | |||
</ItemGroup> | |||
</Project> |
@@ -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<string>(Program.Args) | |||
string[] args = new List<string>(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"); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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<RequestAddUrlEventArgs> OpenUrlRequested; | |||
@@ -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."); | |||
} | |||
} | |||
} | |||
} |
@@ -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; |
@@ -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; | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -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; } | |||
@@ -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<string>(_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<Unit, bool> Update { get; } | |||
@@ -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 | |||
/// <summary> | |||
/// The view model class for the server sharing user control. | |||
/// </summary> | |||
public ServerSharingViewModel() | |||
public ServerSharingViewModel(List<Server> 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<Unit, Unit> 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!; | |||
/// <summary> | |||
/// 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); | |||
@@ -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<UpdateChecker>(); | |||
_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; } | |||
@@ -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 | |||
/// </summary> | |||
public partial class VersionUpdatePromptView : ReactiveUserControl<VersionUpdatePromptViewModel> | |||
{ | |||
public VersionUpdatePromptView(JToken releaseObject) | |||
public VersionUpdatePromptView(JsonElement releaseObject) | |||
{ | |||
InitializeComponent(); | |||
ViewModel = new VersionUpdatePromptViewModel(releaseObject); | |||
@@ -0,0 +1,69 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
using System.Text.Json.Serialization; | |||
namespace Shadowsocks.Models | |||
{ | |||
public class Group | |||
{ | |||
/// <summary> | |||
/// Group name. | |||
/// </summary> | |||
public string Name { get; set; } | |||
/// <summary> | |||
/// UUID of the group. | |||
/// </summary> | |||
public Guid Id { get; set; } | |||
/// <summary> | |||
/// URL of SIP008 online configuration delivery source. | |||
/// </summary> | |||
public string OnlineConfigSource { get; set; } | |||
/// <summary> | |||
/// SIP008 configuration version. | |||
/// </summary> | |||
public int Version { get; set; } | |||
/// <summary> | |||
/// A list of servers in the group. | |||
/// </summary> | |||
public List<Server> Servers { get; set; } | |||
/// <summary> | |||
/// Data used in bytes. | |||
/// The value is fetched from SIP008 provider. | |||
/// </summary> | |||
public ulong BytesUsed { get; set; } | |||
/// <summary> | |||
/// Data remaining to be used in bytes. | |||
/// The value is fetched from SIP008 provider. | |||
/// </summary> | |||
public ulong BytesRemaining { get; set; } | |||
public Group() | |||
{ | |||
Name = ""; | |||
Id = new Guid(); | |||
OnlineConfigSource = ""; | |||
Version = 1; | |||
BytesUsed = 0UL; | |||
BytesRemaining = 0UL; | |||
Servers = new List<Server>(); | |||
} | |||
public Group(string name) | |||
{ | |||
Name = name; | |||
Id = new Guid(); | |||
OnlineConfigSource = ""; | |||
Version = 1; | |||
BytesUsed = 0UL; | |||
BytesRemaining = 0UL; | |||
Servers = new List<Server>(); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} |
@@ -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; | |||
/// <summary> | |||
/// Converts this server object into an ss:// URL. | |||
/// </summary> | |||
/// <returns></returns> | |||
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; | |||
} | |||
/// <summary> | |||
/// Tries to parse an ss:// URL into a Server object. | |||
/// </summary> | |||
/// <param name="url">The ss:// URL to parse.</param> | |||
/// <param name="server"> | |||
/// A Server object represented by the URL. | |||
/// A new empty Server object if the URL is invalid.</param> | |||
/// <returns>True for success. False for failure.</returns> | |||
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; | |||
} | |||
} | |||
} | |||
} |
@@ -1,7 +1,7 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
<TargetFramework>net5.0</TargetFramework> | |||
<Nullable>enable</Nullable> | |||
</PropertyGroup> | |||
@@ -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 | |||