@@ -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"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
@@ -25,8 +25,6 @@ namespace Shadowsocks.PAC | |||||
{ | { | ||||
public event EventHandler<GeositeResultEventArgs>? UpdateCompleted; | public event EventHandler<GeositeResultEventArgs>? UpdateCompleted; | ||||
public event ErrorEventHandler? Error; | |||||
private readonly string DATABASE_PATH; | private readonly string DATABASE_PATH; | ||||
private readonly string GEOSITE_URL = "https://github.com/v2fly/domain-list-community/raw/release/dlc.dat"; | 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() | public void ResetEvent() | ||||
{ | { | ||||
UpdateCompleted = null; | UpdateCompleted = null; | ||||
Error = null; | |||||
} | } | ||||
public async Task UpdatePACFromGeosite(PACSettings pACSettings) | public async Task UpdatePACFromGeosite(PACSettings pACSettings) | ||||
@@ -127,9 +124,9 @@ namespace Shadowsocks.PAC | |||||
bool pacFileChanged = MergeAndWritePACFile(pACSettings.GeositeDirectGroups, pACSettings.GeositeProxiedGroups, blacklist); | bool pacFileChanged = MergeAndWritePACFile(pACSettings.GeositeDirectGroups, pACSettings.GeositeProxiedGroups, blacklist); | ||||
UpdateCompleted?.Invoke(null, new GeositeResultEventArgs(pacFileChanged)); | 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; | RegeneratePacOnVersionUpdate = true; | ||||
CustomPACUrl = ""; | CustomPACUrl = ""; | ||||
CustomGeositeUrl = ""; | CustomGeositeUrl = ""; | ||||
CustomGeositeSha256SumUrl = ""; | |||||
GeositeDirectGroups = new List<string>() | GeositeDirectGroups = new List<string>() | ||||
{ | { | ||||
"private", | "private", | ||||
@@ -67,6 +68,12 @@ namespace Shadowsocks.PAC | |||||
/// </summary> | /// </summary> | ||||
public string CustomGeositeUrl { get; set; } | 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> | /// <summary> | ||||
/// A list of Geosite groups | /// A list of Geosite groups | ||||
/// that we use direct connection for. | /// that we use direct connection for. | ||||
@@ -1,7 +1,7 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
<Nullable>enable</Nullable> | <Nullable>enable</Nullable> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
@@ -1,7 +1,7 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
<ItemGroup> | <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; | 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; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Text; | using System.Text; | ||||
@@ -6,11 +10,23 @@ namespace Shadowsocks.WPF.Models | |||||
{ | { | ||||
public class Settings | 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() | 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> | <PropertyGroup> | ||||
<Configuration>Release</Configuration> | <Configuration>Release</Configuration> | ||||
<Platform>Any CPU</Platform> | <Platform>Any CPU</Platform> | ||||
<PublishDir>bin\Release\netcoreapp3.1\publish\</PublishDir> | |||||
<PublishDir>bin\Release\net5.0-windows\publish\</PublishDir> | |||||
<PublishProtocol>FileSystem</PublishProtocol> | <PublishProtocol>FileSystem</PublishProtocol> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<SelfContained>false</SelfContained> | |||||
</PropertyGroup> | </PropertyGroup> | ||||
</Project> | </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> | <PropertyGroup> | ||||
<Configuration>Release</Configuration> | <Configuration>Release</Configuration> | ||||
<Platform>Any CPU</Platform> | <Platform>Any CPU</Platform> | ||||
<PublishDir>bin\Release\netcoreapp3.1\publish\</PublishDir> | |||||
<PublishDir>bin\Release\net5.0-windows\win-x64\publish\</PublishDir> | |||||
<PublishProtocol>FileSystem</PublishProtocol> | <PublishProtocol>FileSystem</PublishProtocol> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0-windows</TargetFramework> | |||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier> | <RuntimeIdentifier>win-x64</RuntimeIdentifier> | ||||
<SelfContained>true</SelfContained> | <SelfContained>true</SelfContained> | ||||
<PublishSingleFile>True</PublishSingleFile> | <PublishSingleFile>True</PublishSingleFile> | ||||
@@ -6,9 +6,9 @@ https://go.microsoft.com/fwlink/?LinkID=208121. | |||||
<PropertyGroup> | <PropertyGroup> | ||||
<Configuration>Release</Configuration> | <Configuration>Release</Configuration> | ||||
<Platform>Any CPU</Platform> | <Platform>Any CPU</Platform> | ||||
<PublishDir>bin\Release\netcoreapp3.1\publish\</PublishDir> | |||||
<PublishDir>bin\Release\net5.0-windows\win-x86\publish\</PublishDir> | |||||
<PublishProtocol>FileSystem</PublishProtocol> | <PublishProtocol>FileSystem</PublishProtocol> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0-windows</TargetFramework> | |||||
<RuntimeIdentifier>win-x86</RuntimeIdentifier> | <RuntimeIdentifier>win-x86</RuntimeIdentifier> | ||||
<SelfContained>true</SelfContained> | <SelfContained>true</SelfContained> | ||||
<PublishSingleFile>True</PublishSingleFile> | <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; | ||||
using System.Net; | using System.Net; | ||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
using NLog; | |||||
using Shadowsocks.Net; | |||||
namespace Shadowsocks.WPF.Services | namespace Shadowsocks.WPF.Services | ||||
{ | { | ||||
@@ -39,10 +39,8 @@ namespace Shadowsocks.WPF.Services | |||||
return true; | return true; | ||||
} | } | ||||
private class Handler | |||||
private class Handler : IEnableLogger | |||||
{ | { | ||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
private byte[] _firstPacket; | private byte[] _firstPacket; | ||||
private int _firstPacketLength; | private int _firstPacketLength; | ||||
private Socket _local; | private Socket _local; | ||||
@@ -74,7 +72,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, ""); | |||||
Close(); | Close(); | ||||
} | } | ||||
} | } | ||||
@@ -93,7 +91,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, ""); | |||||
Close(); | Close(); | ||||
} | } | ||||
} | } | ||||
@@ -110,7 +108,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, ""); | |||||
Close(); | Close(); | ||||
} | } | ||||
} | } | ||||
@@ -131,7 +129,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, ""); | |||||
Close(); | Close(); | ||||
} | } | ||||
} | } | ||||
@@ -158,7 +156,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, ""); | |||||
Close(); | Close(); | ||||
} | } | ||||
} | } | ||||
@@ -185,7 +183,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, ""); | |||||
Close(); | Close(); | ||||
} | } | ||||
} | } | ||||
@@ -204,7 +202,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, ""); | |||||
Close(); | Close(); | ||||
} | } | ||||
} | } | ||||
@@ -223,7 +221,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, ""); | |||||
Close(); | Close(); | ||||
} | } | ||||
} | } | ||||
@@ -255,7 +253,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, ""); | |||||
} | } | ||||
} | } | ||||
if (_remote != null) | if (_remote != null) | ||||
@@ -267,7 +265,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (SocketException e) | 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; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.IO; | using System.IO; | ||||
@@ -5,42 +8,35 @@ using System.Linq; | |||||
using System.Net; | using System.Net; | ||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
using System.Text; | 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 | namespace Shadowsocks.WPF.Services | ||||
{ | { | ||||
class PrivoxyRunner | |||||
public class PrivoxyRunner : IEnableLogger | |||||
{ | { | ||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
private static int _uid; | private static int _uid; | ||||
private static string _uniqueConfigFile; | |||||
private Process _process; | |||||
private static string _uniqueConfigFile = ""; | |||||
private Process? _process; | |||||
private int _runningPort; | private int _runningPort; | ||||
static PrivoxyRunner() | |||||
public PrivoxyRunner() | |||||
{ | { | ||||
try | 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"; | _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) | catch (IOException e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, "An error occurred while starting Privoxy."); | |||||
_uniqueConfigFile = ""; | |||||
} | } | ||||
} | } | ||||
public int RunningPort => _runningPort; | public int RunningPort => _runningPort; | ||||
public void Start(Configuration configuration) | |||||
public void Start(NetSettings netSettings) | |||||
{ | { | ||||
if (_process == null) | if (_process == null) | ||||
{ | { | ||||
@@ -49,16 +45,13 @@ namespace Shadowsocks.WPF.Services | |||||
{ | { | ||||
KillProcess(p); | 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 = 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 | _process = new Process | ||||
{ | { | ||||
@@ -67,7 +60,7 @@ namespace Shadowsocks.WPF.Services | |||||
{ | { | ||||
FileName = "ss_privoxy.exe", | FileName = "ss_privoxy.exe", | ||||
Arguments = _uniqueConfigFile, | Arguments = _uniqueConfigFile, | ||||
WorkingDirectory = Utils.GetTempPath(), | |||||
WorkingDirectory = Utils.Utilities.GetTempPath(), | |||||
WindowStyle = ProcessWindowStyle.Hidden, | WindowStyle = ProcessWindowStyle.Hidden, | ||||
UseShellExecute = true, | UseShellExecute = true, | ||||
CreateNoWindow = true | CreateNoWindow = true | ||||
@@ -87,7 +80,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
} | } | ||||
private static void KillProcess(Process p) | |||||
private void KillProcess(Process p) | |||||
{ | { | ||||
try | try | ||||
{ | { | ||||
@@ -101,7 +94,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | 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. | * UID is hash of ss's location. | ||||
*/ | */ | ||||
private static bool IsChildProcess(Process process) | |||||
private bool IsChildProcess(Process process) | |||||
{ | { | ||||
try | try | ||||
{ | { | ||||
/* | /* | ||||
* Under PortableMode, we could identify it by the path of ss_privoxy.exe. | * 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) | catch (Exception ex) | ||||
@@ -134,18 +127,18 @@ namespace Shadowsocks.WPF.Services | |||||
* are already dead, and that will cause exceptions here. | * are already dead, and that will cause exceptions here. | ||||
* We could simply ignore those exceptions. | * We could simply ignore those exceptions. | ||||
*/ | */ | ||||
logger.LogUsefulException(ex); | |||||
this.Log().Error(ex, ""); | |||||
return false; | return false; | ||||
} | } | ||||
} | } | ||||
private int GetFreePort(bool isIPv6 = false) | |||||
private int GetFreePort(NetSettings netSettings) | |||||
{ | { | ||||
int defaultPort = 8123; | int defaultPort = 8123; | ||||
try | try | ||||
{ | { | ||||
// TCP stack please do me a favor | // 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(); | l.Start(); | ||||
var port = ((IPEndPoint)l.LocalEndpoint).Port; | var port = ((IPEndPoint)l.LocalEndpoint).Port; | ||||
l.Stop(); | l.Stop(); | ||||
@@ -154,7 +147,7 @@ namespace Shadowsocks.WPF.Services | |||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
// in case access denied | // in case access denied | ||||
logger.LogUsefulException(e); | |||||
this.Log().Error(e, ""); | |||||
return defaultPort; | return defaultPort; | ||||
} | } | ||||
} | } | ||||
@@ -1,11 +1,10 @@ | |||||
using Shadowsocks.Models; | |||||
using System; | using System; | ||||
using System.Collections.Specialized; | using System.Collections.Specialized; | ||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.IO; | using System.IO; | ||||
using System.Net; | using System.Net; | ||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Util.ProcessManagement; | |||||
namespace Shadowsocks.WPF.Services | namespace Shadowsocks.WPF.Services | ||||
{ | { | ||||
@@ -27,17 +26,17 @@ namespace Shadowsocks.WPF.Services | |||||
throw new ArgumentNullException(nameof(server)); | throw new ArgumentNullException(nameof(server)); | ||||
} | } | ||||
if (string.IsNullOrWhiteSpace(server.plugin)) | |||||
if (string.IsNullOrWhiteSpace(server.Plugin)) | |||||
{ | { | ||||
return null; | return null; | ||||
} | } | ||||
return new Sip003Plugin( | return new Sip003Plugin( | ||||
server.plugin, | |||||
server.plugin_opts, | |||||
server.Plugin, | |||||
server.PluginOpts, | |||||
server.plugin_args, | server.plugin_args, | ||||
server.server, | |||||
server.server_port, | |||||
server.Host, | |||||
server.Port, | |||||
showPluginOutput); | showPluginOutput); | ||||
} | } | ||||
@@ -63,7 +62,7 @@ namespace Shadowsocks.WPF.Services | |||||
CreateNoWindow = !showPluginOutput, | CreateNoWindow = !showPluginOutput, | ||||
ErrorDialog = false, | ErrorDialog = false, | ||||
WindowStyle = ProcessWindowStyle.Hidden, | WindowStyle = ProcessWindowStyle.Hidden, | ||||
WorkingDirectory = Program.WorkingDirectory ?? Environment.CurrentDirectory, | |||||
WorkingDirectory = Utils.Utilities.WorkingDirectory ?? Environment.CurrentDirectory, | |||||
Environment = | Environment = | ||||
{ | { | ||||
["SS_REMOTE_HOST"] = serverAddress, | ["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 | // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d | ||||
if (ex.NativeErrorCode == 0x00000002) | 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; | _started = true; | ||||
} | } | ||||
@@ -162,7 +160,6 @@ namespace Shadowsocks.WPF.Services | |||||
try | try | ||||
{ | { | ||||
_pluginProcess.Dispose(); | _pluginProcess.Dispose(); | ||||
_pluginJob.Dispose(); | |||||
} | } | ||||
catch (Exception) { } | catch (Exception) { } | ||||
@@ -1,23 +1,20 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Diagnostics; | using System.Diagnostics; | ||||
using System.IO; | using System.IO; | ||||
using System.Net; | |||||
using System.Net.Http; | using System.Net.Http; | ||||
using System.Reflection; | using System.Reflection; | ||||
using System.Text.RegularExpressions; | |||||
using System.Text.Json; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using System.Windows; | using System.Windows; | ||||
using Newtonsoft.Json.Linq; | |||||
using NLog; | 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 | namespace Shadowsocks.WPF.Services | ||||
{ | { | ||||
public class UpdateChecker | |||||
public class UpdateChecker : IEnableLogger | |||||
{ | { | ||||
private readonly Logger logger; | private readonly Logger logger; | ||||
private readonly HttpClient httpClient; | private readonly HttpClient httpClient; | ||||
@@ -25,24 +22,24 @@ namespace Shadowsocks.WPF.Services | |||||
// https://developer.github.com/v3/repos/releases/ | // https://developer.github.com/v3/repos/releases/ | ||||
private const string UpdateURL = "https://api.github.com/repos/shadowsocks/shadowsocks-windows/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 NewReleaseVersion { get; private set; } | ||||
public string NewReleaseZipFilename { 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; | private readonly Version _version; | ||||
public UpdateChecker() | public UpdateChecker() | ||||
{ | { | ||||
logger = LogManager.GetCurrentClassLogger(); | logger = LogManager.GetCurrentClassLogger(); | ||||
httpClient = Program.MainController.GetHttpClient(); | |||||
httpClient = Locator.Current.GetService<HttpClient>(); | |||||
_version = new Version(Version); | _version = new Version(Version); | ||||
_config = Program.MainController.GetCurrentConfiguration(); | |||||
NewReleaseVersion = ""; | |||||
NewReleaseZipFilename = ""; | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -55,30 +52,33 @@ namespace Shadowsocks.WPF.Services | |||||
// delay | // delay | ||||
logger.Info($"Waiting for {millisecondsDelay}ms before checking for version update."); | logger.Info($"Waiting for {millisecondsDelay}ms before checking for version update."); | ||||
await Task.Delay(millisecondsDelay); | await Task.Delay(millisecondsDelay); | ||||
// update _config so we would know if the user checked or unchecked pre-release checks | |||||
_config = Program.MainController.GetCurrentConfiguration(); | |||||
// start | // start | ||||
logger.Info($"Checking for version update."); | logger.Info($"Checking for version update."); | ||||
var appSettings = Locator.Current.GetService<AppSettings>(); | |||||
try | try | ||||
{ | { | ||||
// list releases via API | // list releases via API | ||||
var releasesListJsonString = await httpClient.GetStringAsync(UpdateURL); | |||||
var releasesListJsonStream = await httpClient.GetStreamAsync(UpdateURL); | |||||
// parse | // 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."); | logger.Info($"No new versions found."); | ||||
@@ -86,7 +86,7 @@ namespace Shadowsocks.WPF.Services | |||||
} | } | ||||
catch (Exception e) | 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. | /// Opens a window to show the update's information. | ||||
/// </summary> | /// </summary> | ||||
/// <param name="releaseObject">The update release object.</param> | /// <param name="releaseObject">The update release object.</param> | ||||
private void AskToUpdate(JToken releaseObject) | |||||
private void AskToUpdate(JsonElement releaseObject) | |||||
{ | { | ||||
if (versionUpdatePromptWindow == null) | if (versionUpdatePromptWindow == null) | ||||
{ | { | ||||
@@ -113,7 +113,7 @@ namespace Shadowsocks.WPF.Services | |||||
versionUpdatePromptWindow.Activate(); | versionUpdatePromptWindow.Activate(); | ||||
} | } | ||||
private void VersionUpdatePromptWindow_Closed(object sender, EventArgs e) | |||||
private void VersionUpdatePromptWindow_Closed(object? sender, EventArgs e) | |||||
{ | { | ||||
versionUpdatePromptWindow = null; | versionUpdatePromptWindow = null; | ||||
} | } | ||||
@@ -126,14 +126,14 @@ namespace Shadowsocks.WPF.Services | |||||
{ | { | ||||
try | try | ||||
{ | { | ||||
var assets = (JArray)_releaseObject["assets"]; | |||||
var assets = _releaseObject.GetProperty("assets"); | |||||
// download all 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); | 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); | await response.Content.CopyToAsync(downloadedFileStream); | ||||
logger.Info($"Downloaded {filename}."); | logger.Info($"Downloaded {filename}."); | ||||
// store .zip filename | // store .zip filename | ||||
@@ -143,11 +143,11 @@ namespace Shadowsocks.WPF.Services | |||||
logger.Info("Finished downloading."); | logger.Info("Finished downloading."); | ||||
// notify user | // notify user | ||||
CloseVersionUpdatePromptWindow(); | CloseVersionUpdatePromptWindow(); | ||||
Process.Start("explorer.exe", $"/select, \"{Utils.GetTempPath(NewReleaseZipFilename)}\""); | |||||
Process.Start("explorer.exe", $"/select, \"{Utils.Utilities.GetTempPath(NewReleaseZipFilename)}\""); | |||||
} | } | ||||
catch (Exception e) | 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> | /// </summary> | ||||
public void SkipUpdate() | 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(); | CloseVersionUpdatePromptWindow(); | ||||
} | } | ||||
@@ -2,7 +2,7 @@ | |||||
<PropertyGroup> | <PropertyGroup> | ||||
<OutputType>Exe</OutputType> | <OutputType>Exe</OutputType> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0-windows</TargetFramework> | |||||
<UseWPF>true</UseWPF> | <UseWPF>true</UseWPF> | ||||
<Authors>clowwindy & community 2020</Authors> | <Authors>clowwindy & community 2020</Authors> | ||||
<PackageId>Shadowsocks.WPF</PackageId> | <PackageId>Shadowsocks.WPF</PackageId> | ||||
@@ -50,7 +50,7 @@ | |||||
<PackageReference Include="ReactiveUI.Fody" Version="12.1.5" /> | <PackageReference Include="ReactiveUI.Fody" Version="12.1.5" /> | ||||
<PackageReference Include="ReactiveUI.Validation" Version="1.8.6" /> | <PackageReference Include="ReactiveUI.Validation" Version="1.8.6" /> | ||||
<PackageReference Include="ReactiveUI.WPF" Version="12.1.5" /> | <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="System.CommandLine" Version="2.0.0-beta1.20371.2" /> | ||||
<PackageReference Include="WPFLocalizeExtension" Version="3.8.0" /> | <PackageReference Include="WPFLocalizeExtension" Version="3.8.0" /> | ||||
<PackageReference Include="ZXing.Net" Version="0.16.6" /> | <PackageReference Include="ZXing.Net" Version="0.16.6" /> | ||||
@@ -81,7 +81,9 @@ | |||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | <ItemGroup> | ||||
<ProjectReference Include="..\Shadowsocks.Interop\Shadowsocks.Interop.csproj" /> | |||||
<ProjectReference Include="..\Shadowsocks.Net\Shadowsocks.Net.csproj" /> | <ProjectReference Include="..\Shadowsocks.Net\Shadowsocks.Net.csproj" /> | ||||
<ProjectReference Include="..\Shadowsocks.PAC\Shadowsocks.PAC.csproj" /> | |||||
<ProjectReference Include="..\Shadowsocks\Shadowsocks.csproj" /> | <ProjectReference Include="..\Shadowsocks\Shadowsocks.csproj" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
@@ -113,4 +115,8 @@ | |||||
<Resource Include="Resources\ssw128.png" /> | <Resource Include="Resources\ssw128.png" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | |||||
<Folder Include="Properties\PublishProfiles\" /> | |||||
</ItemGroup> | |||||
</Project> | </Project> |
@@ -1,36 +1,32 @@ | |||||
using Microsoft.Win32; | |||||
using Splat; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Diagnostics; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Runtime.InteropServices; | 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) | public static bool Set(bool enabled) | ||||
{ | { | ||||
RegistryKey runKey = null; | RegistryKey runKey = null; | ||||
try | try | ||||
{ | { | ||||
runKey = Utils.OpenRegKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); | |||||
runKey = Registry.CurrentUser.CreateSubKey(registryRunKey, RegistryKeyPermissionCheck.ReadWriteSubTree); | |||||
if (runKey == null) | if (runKey == null) | ||||
{ | { | ||||
logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); | |||||
LogHost.Default.Error(@"Cannot find HKCU\{registryRunKey}."); | |||||
return false; | return false; | ||||
} | } | ||||
if (enabled) | if (enabled) | ||||
{ | { | ||||
runKey.SetValue(Key, Program.ExecutablePath); | |||||
runKey.SetValue(Key, Process.GetCurrentProcess().MainModule?.FileName); | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
@@ -42,7 +38,7 @@ namespace Shadowsocks.WPF.Behaviors | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
LogHost.Default.Error(e, "An error occurred while setting auto startup registry entry."); | |||||
return false; | return false; | ||||
} | } | ||||
finally | finally | ||||
@@ -55,7 +51,9 @@ namespace Shadowsocks.WPF.Behaviors | |||||
runKey.Dispose(); | runKey.Dispose(); | ||||
} | } | ||||
catch (Exception e) | 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; | RegistryKey runKey = null; | ||||
try | try | ||||
{ | { | ||||
runKey = Utils.OpenRegKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); | |||||
runKey = Registry.CurrentUser.CreateSubKey(registryRunKey, RegistryKeyPermissionCheck.ReadWriteSubTree); | |||||
if (runKey == null) | if (runKey == null) | ||||
{ | { | ||||
logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); | |||||
LogHost.Default.Error(@"Cannot find HKCU\{registryRunKey}."); | |||||
return false; | return false; | ||||
} | } | ||||
var check = false; | var check = false; | ||||
@@ -80,10 +78,11 @@ namespace Shadowsocks.WPF.Behaviors | |||||
continue; | continue; | ||||
} | } | ||||
// Remove other startup keys with the same executable path. fixes #3011 and also assures compatibility with older versions | // 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.DeleteValue(valueName); | ||||
runKey.SetValue(Key, Program.ExecutablePath); | |||||
runKey.SetValue(Key, Utilities.ExecutablePath); | |||||
check = true; | check = true; | ||||
} | } | ||||
} | } | ||||
@@ -91,7 +90,7 @@ namespace Shadowsocks.WPF.Behaviors | |||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
LogHost.Default.Error(e, "An error occurred while checking auto startup registry entries."); | |||||
return false; | return false; | ||||
} | } | ||||
finally | finally | ||||
@@ -104,7 +103,9 @@ namespace Shadowsocks.WPF.Behaviors | |||||
runKey.Dispose(); | runKey.Dispose(); | ||||
} | } | ||||
catch (Exception e) | 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()) | if (register && !Check()) | ||||
{ | { | ||||
// escape command line parameter | // 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.Replace("\"", "\\\"")) // escape " to \" | ||||
.Select(p => p.IndexOf(" ") >= 0 ? "\"" + p + "\"" : p) // encapsule with " | .Select(p => p.IndexOf(" ") >= 0 ? "\"" + p + "\"" : p) // encapsule with " | ||||
.ToArray(); | .ToArray(); | ||||
@@ -140,13 +141,13 @@ namespace Shadowsocks.WPF.Behaviors | |||||
// first parameter is process command line parameter | // first parameter is process command line parameter | ||||
// needn't include the name of the executable in the command line | // needn't include the name of the executable in the command line | ||||
RegisterApplicationRestart(cmdline, (int)(ApplicationRestartFlags.RESTART_NO_CRASH | ApplicationRestartFlags.RESTART_NO_HANG)); | 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 | // requested unregister, which has no side effect | ||||
else if (!register) | else if (!register) | ||||
{ | { | ||||
UnregisterApplicationRestart(); | 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; | ||||
using System.IO; | using System.IO; | ||||
using System.IO.Compression; | using System.IO.Compression; | ||||
using System.Text; | using System.Text; | ||||
namespace Shadowsocks.WPF.Behaviors | |||||
namespace Shadowsocks.WPF.Utils | |||||
{ | { | ||||
public static class FileManager | public static class FileManager | ||||
{ | { | ||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public static bool ByteArrayToFile(string fileName, byte[] content) | public static bool ByteArrayToFile(string fileName, byte[] content) | ||||
{ | { | ||||
try | try | ||||
@@ -20,7 +18,7 @@ namespace Shadowsocks.WPF.Behaviors | |||||
} | } | ||||
catch (Exception ex) | catch (Exception ex) | ||||
{ | { | ||||
logger.Error(ex); | |||||
LogHost.Default.Error(ex, ""); | |||||
} | } | ||||
return false; | return false; | ||||
} | } | ||||
@@ -60,7 +58,7 @@ namespace Shadowsocks.WPF.Behaviors | |||||
} | } | ||||
catch (Exception ex) | catch (Exception ex) | ||||
{ | { | ||||
logger.Error(ex); | |||||
LogHost.Default.Error(ex, ""); | |||||
throw ex; | throw ex; | ||||
} | } | ||||
} | } |
@@ -3,7 +3,7 @@ using System.IO.Pipes; | |||||
using System.Net; | using System.Net; | ||||
using System.Text; | using System.Text; | ||||
namespace Shadowsocks.WPF.Behaviors | |||||
namespace Shadowsocks.WPF.Utils | |||||
{ | { | ||||
class RequestAddUrlEventArgs : EventArgs | class RequestAddUrlEventArgs : EventArgs | ||||
{ | { | ||||
@@ -19,7 +19,7 @@ namespace Shadowsocks.WPF.Behaviors | |||||
{ | { | ||||
private const int INT32_LEN = 4; | private const int INT32_LEN = 4; | ||||
private const int OP_OPEN_URL = 1; | 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; | public event EventHandler<RequestAddUrlEventArgs> OpenUrlRequested; | ||||
@@ -1,15 +1,13 @@ | |||||
using Microsoft.Win32; | using Microsoft.Win32; | ||||
using NLog; | |||||
using Splat; | |||||
using System; | using System; | ||||
namespace Shadowsocks.WPF.Behaviors | |||||
namespace Shadowsocks.WPF.Utils | |||||
{ | { | ||||
static class ProtocolHandler | static class ProtocolHandler | ||||
{ | { | ||||
const string ssURLRegKey = @"SOFTWARE\Classes\ss"; | const string ssURLRegKey = @"SOFTWARE\Classes\ss"; | ||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public static bool Set(bool enabled) | public static bool Set(bool enabled) | ||||
{ | { | ||||
RegistryKey ssURLAssociation = null; | RegistryKey ssURLAssociation = null; | ||||
@@ -19,7 +17,7 @@ namespace Shadowsocks.WPF.Behaviors | |||||
ssURLAssociation = Registry.CurrentUser.CreateSubKey(ssURLRegKey, RegistryKeyPermissionCheck.ReadWriteSubTree); | ssURLAssociation = Registry.CurrentUser.CreateSubKey(ssURLRegKey, RegistryKeyPermissionCheck.ReadWriteSubTree); | ||||
if (ssURLAssociation == null) | 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; | return false; | ||||
} | } | ||||
if (enabled) | if (enabled) | ||||
@@ -27,19 +25,19 @@ namespace Shadowsocks.WPF.Behaviors | |||||
ssURLAssociation.SetValue("", "URL:Shadowsocks"); | ssURLAssociation.SetValue("", "URL:Shadowsocks"); | ||||
ssURLAssociation.SetValue("URL Protocol", ""); | ssURLAssociation.SetValue("URL Protocol", ""); | ||||
var shellOpen = ssURLAssociation.CreateSubKey("shell").CreateSubKey("open").CreateSubKey("command"); | 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 | else | ||||
{ | { | ||||
Registry.CurrentUser.DeleteSubKeyTree(ssURLRegKey); | Registry.CurrentUser.DeleteSubKeyTree(ssURLRegKey); | ||||
logger.Info(@"Successfully removed ss:// association."); | |||||
LogHost.Default.Info(@"Successfully removed ss:// association."); | |||||
} | } | ||||
return true; | return true; | ||||
} | } | ||||
catch (Exception e) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
LogHost.Default.Error(e, "An error occurred while setting ss:// association registry entries."); | |||||
return false; | return false; | ||||
} | } | ||||
finally | finally | ||||
@@ -52,7 +50,9 @@ namespace Shadowsocks.WPF.Behaviors | |||||
ssURLAssociation.Dispose(); | ssURLAssociation.Dispose(); | ||||
} | } | ||||
catch (Exception e) | 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"); | 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) | catch (Exception e) | ||||
{ | { | ||||
logger.LogUsefulException(e); | |||||
LogHost.Default.Error(e, "An error occurred while checking ss:// association registry entries."); | |||||
return false; | return false; | ||||
} | } | ||||
finally | finally | ||||
@@ -87,7 +87,9 @@ namespace Shadowsocks.WPF.Behaviors | |||||
ssURLAssociation.Dispose(); | ssURLAssociation.Dispose(); | ||||
} | } | ||||
catch (Exception e) | 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.Net.SystemProxy; | ||||
using Shadowsocks.WPF.Services.SystemProxy; | using Shadowsocks.WPF.Services.SystemProxy; | ||||
using System.Windows; | using System.Windows; | ||||
namespace Shadowsocks.WPF.Behaviors | |||||
namespace Shadowsocks.WPF.Utils | |||||
{ | { | ||||
public static class SystemProxy | public static class SystemProxy | ||||
{ | { | ||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public static void Update(Configuration config, bool forceDisable, PACServer pacSrv, bool noRetry = false) | public static void Update(Configuration config, bool forceDisable, PACServer pacSrv, bool noRetry = false) | ||||
{ | { | ||||
bool global = config.global; | 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.Fody.Helpers; | ||||
using ReactiveUI.Validation.Extensions; | using ReactiveUI.Validation.Extensions; | ||||
using ReactiveUI.Validation.Helpers; | using ReactiveUI.Validation.Helpers; | ||||
using Shadowsocks.Controller; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.View; | |||||
using Shadowsocks.WPF.Models; | |||||
using System.Linq; | using System.Linq; | ||||
using System.Reactive; | using System.Reactive; | ||||
using System.Reactive.Linq; | using System.Reactive.Linq; | ||||
@@ -15,10 +13,6 @@ namespace Shadowsocks.WPF.ViewModels | |||||
{ | { | ||||
public ForwardProxyViewModel() | public ForwardProxyViewModel() | ||||
{ | { | ||||
_config = Program.MainController.GetCurrentConfiguration(); | |||||
_controller = Program.MainController; | |||||
_menuViewController = Program.MenuController; | |||||
if (!_config.proxy.useProxy) | if (!_config.proxy.useProxy) | ||||
NoProxy = true; | NoProxy = true; | ||||
else if (_config.proxy.proxyType == 0) | else if (_config.proxy.proxyType == 0) | ||||
@@ -64,10 +58,6 @@ namespace Shadowsocks.WPF.ViewModels | |||||
Cancel = ReactiveCommand.Create(_menuViewController.CloseForwardProxyWindow); | Cancel = ReactiveCommand.Create(_menuViewController.CloseForwardProxyWindow); | ||||
} | } | ||||
private readonly Configuration _config; | |||||
private readonly ShadowsocksController _controller; | |||||
private readonly MenuViewController _menuViewController; | |||||
public ValidationHelper AddressRule { get; } | public ValidationHelper AddressRule { get; } | ||||
public ValidationHelper PortRule { get; } | public ValidationHelper PortRule { get; } | ||||
public ValidationHelper TimeoutRule { get; } | public ValidationHelper TimeoutRule { get; } | ||||
@@ -2,10 +2,7 @@ using ReactiveUI; | |||||
using ReactiveUI.Fody.Helpers; | using ReactiveUI.Fody.Helpers; | ||||
using ReactiveUI.Validation.Extensions; | using ReactiveUI.Validation.Extensions; | ||||
using ReactiveUI.Validation.Helpers; | using ReactiveUI.Validation.Helpers; | ||||
using Shadowsocks.Controller; | |||||
using Shadowsocks.WPF.Localization; | using Shadowsocks.WPF.Localization; | ||||
using Shadowsocks.Model; | |||||
using Shadowsocks.View; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Collections.ObjectModel; | using System.Collections.ObjectModel; | ||||
@@ -21,10 +18,6 @@ namespace Shadowsocks.WPF.ViewModels | |||||
{ | { | ||||
public OnlineConfigViewModel() | public OnlineConfigViewModel() | ||||
{ | { | ||||
_config = Program.MainController.GetCurrentConfiguration(); | |||||
_controller = Program.MainController; | |||||
_menuViewController = Program.MenuController; | |||||
Sources = new ObservableCollection<string>(_config.onlineConfigSource); | Sources = new ObservableCollection<string>(_config.onlineConfigSource); | ||||
SelectedSource = ""; | SelectedSource = ""; | ||||
Address = ""; | 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 ValidationHelper AddressRule { get; } | ||||
public ReactiveCommand<Unit, bool> Update { get; } | public ReactiveCommand<Unit, bool> Update { get; } | ||||
@@ -1,11 +1,10 @@ | |||||
using ReactiveUI; | |||||
using ReactiveUI; | |||||
using ReactiveUI.Fody.Helpers; | using ReactiveUI.Fody.Helpers; | ||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Models; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Drawing; | using System.Drawing; | ||||
using System.IO; | using System.IO; | ||||
using System.Linq; | |||||
using System.Reactive; | using System.Reactive; | ||||
using System.Windows; | using System.Windows; | ||||
using System.Windows.Media.Imaging; | using System.Windows.Media.Imaging; | ||||
@@ -17,10 +16,9 @@ namespace Shadowsocks.WPF.ViewModels | |||||
/// <summary> | /// <summary> | ||||
/// The view model class for the server sharing user control. | /// The view model class for the server sharing user control. | ||||
/// </summary> | /// </summary> | ||||
public ServerSharingViewModel() | |||||
public ServerSharingViewModel(List<Server> servers) | |||||
{ | { | ||||
_config = Program.MainController.GetCurrentConfiguration(); | |||||
Servers = _config.configs; | |||||
Servers = servers; | |||||
SelectedServer = Servers[0]; | SelectedServer = Servers[0]; | ||||
this.WhenAnyValue(x => x.SelectedServer) | this.WhenAnyValue(x => x.SelectedServer) | ||||
@@ -29,8 +27,6 @@ namespace Shadowsocks.WPF.ViewModels | |||||
CopyLink = ReactiveCommand.Create(() => Clipboard.SetText(SelectedServerUrl)); | CopyLink = ReactiveCommand.Create(() => Clipboard.SetText(SelectedServerUrl)); | ||||
} | } | ||||
private readonly Configuration _config; | |||||
public ReactiveCommand<Unit, Unit> CopyLink { get; } | public ReactiveCommand<Unit, Unit> CopyLink { get; } | ||||
[Reactive] | [Reactive] | ||||
@@ -40,10 +36,10 @@ namespace Shadowsocks.WPF.ViewModels | |||||
public Server SelectedServer { get; set; } | public Server SelectedServer { get; set; } | ||||
[Reactive] | [Reactive] | ||||
public string SelectedServerUrl { get; private set; } | |||||
public string SelectedServerUrl { get; private set; } = null!; | |||||
[Reactive] | [Reactive] | ||||
public BitmapImage SelectedServerUrlImage { get; private set; } | |||||
public BitmapImage SelectedServerUrlImage { get; private set; } = null!; | |||||
/// <summary> | /// <summary> | ||||
/// Called when SelectedServer changed | /// Called when SelectedServer changed | ||||
@@ -52,7 +48,7 @@ namespace Shadowsocks.WPF.ViewModels | |||||
private void UpdateUrlAndImage() | private void UpdateUrlAndImage() | ||||
{ | { | ||||
// update SelectedServerUrl | // update SelectedServerUrl | ||||
SelectedServerUrl = SelectedServer.GetURL(_config.generateLegacyUrl); | |||||
SelectedServerUrl = SelectedServer.ToUrl().AbsoluteUri; | |||||
// generate QR code | // generate QR code | ||||
var qrCode = ZXing.QrCode.Internal.Encoder.encode(SelectedServerUrl, ZXing.QrCode.Internal.ErrorCorrectionLevel.L); | var qrCode = ZXing.QrCode.Internal.Encoder.encode(SelectedServerUrl, ZXing.QrCode.Internal.ErrorCorrectionLevel.L); | ||||
@@ -1,19 +1,23 @@ | |||||
using Newtonsoft.Json.Linq; | |||||
using ReactiveUI; | using ReactiveUI; | ||||
using Shadowsocks.Controller; | |||||
using Shadowsocks.WPF.Services; | |||||
using Splat; | |||||
using System.Reactive; | using System.Reactive; | ||||
using System.Text.Json; | |||||
namespace Shadowsocks.WPF.ViewModels | namespace Shadowsocks.WPF.ViewModels | ||||
{ | { | ||||
public class VersionUpdatePromptViewModel : ReactiveObject | public class VersionUpdatePromptViewModel : ReactiveObject | ||||
{ | { | ||||
public VersionUpdatePromptViewModel(JToken releaseObject) | |||||
public VersionUpdatePromptViewModel(JsonElement releaseObject) | |||||
{ | { | ||||
_updateChecker = Program.MenuController.updateChecker; | |||||
_updateChecker = Locator.Current.GetService<UpdateChecker>(); | |||||
_releaseObject = releaseObject; | _releaseObject = releaseObject; | ||||
var releaseTagName = _releaseObject.GetProperty("tag_name").GetString(); | |||||
var releaseNotes = _releaseObject.GetProperty("body").GetString(); | |||||
var releaseIsPrerelease = _releaseObject.GetProperty("prerelease").GetBoolean(); | |||||
ReleaseNotes = string.Concat( | 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); | Update = ReactiveCommand.CreateFromTask(_updateChecker.DoUpdate); | ||||
SkipVersion = ReactiveCommand.Create(_updateChecker.SkipUpdate); | SkipVersion = ReactiveCommand.Create(_updateChecker.SkipUpdate); | ||||
@@ -21,7 +25,7 @@ namespace Shadowsocks.WPF.ViewModels | |||||
} | } | ||||
private readonly UpdateChecker _updateChecker; | private readonly UpdateChecker _updateChecker; | ||||
private readonly JToken _releaseObject; | |||||
private readonly JsonElement _releaseObject; | |||||
public string ReleaseNotes { get; } | public string ReleaseNotes { get; } | ||||
@@ -1,7 +1,7 @@ | |||||
using Newtonsoft.Json.Linq; | |||||
using ReactiveUI; | using ReactiveUI; | ||||
using Shadowsocks.WPF.ViewModels; | using Shadowsocks.WPF.ViewModels; | ||||
using System.Reactive.Disposables; | using System.Reactive.Disposables; | ||||
using System.Text.Json; | |||||
namespace Shadowsocks.WPF.Views | namespace Shadowsocks.WPF.Views | ||||
{ | { | ||||
@@ -10,7 +10,7 @@ namespace Shadowsocks.WPF.Views | |||||
/// </summary> | /// </summary> | ||||
public partial class VersionUpdatePromptView : ReactiveUserControl<VersionUpdatePromptViewModel> | public partial class VersionUpdatePromptView : ReactiveUserControl<VersionUpdatePromptViewModel> | ||||
{ | { | ||||
public VersionUpdatePromptView(JToken releaseObject) | |||||
public VersionUpdatePromptView(JsonElement releaseObject) | |||||
{ | { | ||||
InitializeComponent(); | InitializeComponent(); | ||||
ViewModel = new VersionUpdatePromptViewModel(releaseObject); | 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() | public Server() | ||||
{ | { | ||||
Host = ""; | Host = ""; | ||||
Port = 8388; | |||||
Password = ""; | Password = ""; | ||||
Method = ""; | |||||
Method = "chacha20-ietf-poly1305"; | |||||
Plugin = ""; | Plugin = ""; | ||||
PluginOpts = ""; | PluginOpts = ""; | ||||
Name = ""; | Name = ""; | ||||
@@ -50,5 +51,75 @@ namespace Shadowsocks.Models | |||||
Name = name; | Name = name; | ||||
Uuid = uuid; | 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"> | <Project Sdk="Microsoft.NET.Sdk"> | ||||
<PropertyGroup> | <PropertyGroup> | ||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
<TargetFramework>net5.0</TargetFramework> | |||||
<Nullable>enable</Nullable> | <Nullable>enable</Nullable> | ||||
</PropertyGroup> | </PropertyGroup> | ||||
@@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.Protobuf", "Sha | |||||
EndProject | EndProject | ||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.WPF", "Shadowsocks.WPF\Shadowsocks.WPF.csproj", "{EA1FB2D4-B5A7-47A6-B097-2F4D29E23010}" | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shadowsocks.WPF", "Shadowsocks.WPF\Shadowsocks.WPF.csproj", "{EA1FB2D4-B5A7-47A6-B097-2F4D29E23010}" | ||||
EndProject | EndProject | ||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shadowsocks.Interop", "Shadowsocks.Interop\Shadowsocks.Interop.csproj", "{1CC6E8A9-1875-430C-B2BB-F227ACD711B1}" | |||||
EndProject | |||||
Global | Global | ||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
Debug|Any CPU = Debug|Any CPU | 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}.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.ActiveCfg = Release|Any CPU | ||||
{EA1FB2D4-B5A7-47A6-B097-2F4D29E23010}.Release|Any CPU.Build.0 = 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 | EndGlobalSection | ||||
GlobalSection(SolutionProperties) = preSolution | GlobalSection(SolutionProperties) = preSolution | ||||
HideSolutionNode = FALSE | HideSolutionNode = FALSE | ||||