Browse Source

5️⃣ Update to `net5.0` and mass cleanups

pull/3073/head
database64128 3 years ago
parent
commit
2709380c86
No known key found for this signature in database GPG Key ID: 1CA27546BEDB8B01
43 changed files with 737 additions and 674 deletions
  1. +20
    -0
      Shadowsocks.Interop/Settings/InteropSettings.cs
  2. +8
    -0
      Shadowsocks.Interop/Shadowsocks.Interop.csproj
  3. +16
    -0
      Shadowsocks.Interop/SsRust/Config.cs
  4. +16
    -0
      Shadowsocks.Interop/V2Ray/Config.cs
  5. +26
    -0
      Shadowsocks.Net/Settings/NetSettings.cs
  6. +1
    -1
      Shadowsocks.Net/Shadowsocks.Net.csproj
  7. +2
    -5
      Shadowsocks.PAC/GeositeUpdater.cs
  8. +7
    -0
      Shadowsocks.PAC/PACSettings.cs
  9. +1
    -1
      Shadowsocks.PAC/Shadowsocks.PAC.csproj
  10. +1
    -1
      Shadowsocks.Protobuf/Shadowsocks.Protobuf.csproj
  11. +0
    -67
      Shadowsocks.WPF/Behaviors/OnlineConfigResolver.cs
  12. +0
    -125
      Shadowsocks.WPF/Behaviors/Utilities.cs
  13. +1
    -1
      Shadowsocks.WPF/Localization/LocalizationProvider.cs
  14. +22
    -0
      Shadowsocks.WPF/Models/AppSettings.cs
  15. +0
    -252
      Shadowsocks.WPF/Models/Server.cs
  16. +19
    -3
      Shadowsocks.WPF/Models/Settings.cs
  17. +1
    -3
      Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml
  18. +0
    -6
      Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml.user
  19. +18
    -0
      Shadowsocks.WPF/Properties/PublishProfiles/win-arm.pubxml
  20. +2
    -2
      Shadowsocks.WPF/Properties/PublishProfiles/win-x64.pubxml
  21. +2
    -2
      Shadowsocks.WPF/Properties/PublishProfiles/win-x86.pubxml
  22. +45
    -0
      Shadowsocks.WPF/Services/OnlineConfigService.cs
  23. +13
    -15
      Shadowsocks.WPF/Services/PortForwarder.cs
  24. +28
    -35
      Shadowsocks.WPF/Services/PrivoxyRunner.cs
  25. +9
    -12
      Shadowsocks.WPF/Services/Sip003Plugin.cs
  26. +49
    -46
      Shadowsocks.WPF/Services/UpdateChecker.cs
  27. +8
    -2
      Shadowsocks.WPF/Shadowsocks.WPF.csproj
  28. +26
    -25
      Shadowsocks.WPF/Utils/AutoStartup.cs
  29. +4
    -6
      Shadowsocks.WPF/Utils/FileManager.cs
  30. +2
    -2
      Shadowsocks.WPF/Utils/IPCService.cs
  31. +15
    -13
      Shadowsocks.WPF/Utils/ProtocolHandler.cs
  32. +1
    -4
      Shadowsocks.WPF/Utils/SystemProxy.cs
  33. +117
    -0
      Shadowsocks.WPF/Utils/Utilities.cs
  34. +2
    -12
      Shadowsocks.WPF/ViewModels/ForwardProxyViewModel.cs
  35. +0
    -11
      Shadowsocks.WPF/ViewModels/OnlineConfigViewModel.cs
  36. +7
    -11
      Shadowsocks.WPF/ViewModels/ServerSharingViewModel.cs
  37. +11
    -7
      Shadowsocks.WPF/ViewModels/VersionUpdatePromptViewModel.cs
  38. +2
    -2
      Shadowsocks.WPF/Views/VersionUpdatePromptView.xaml.cs
  39. +69
    -0
      Shadowsocks/Models/Group.cs
  40. +87
    -0
      Shadowsocks/Models/JsonSnakeCaseNamingPolicy.cs
  41. +72
    -1
      Shadowsocks/Models/Server.cs
  42. +1
    -1
      Shadowsocks/Shadowsocks.csproj
  43. +6
    -0
      shadowsocks-windows.sln

+ 20
- 0
Shadowsocks.Interop/Settings/InteropSettings.cs View File

@@ -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 = "";
}
}
}

+ 8
- 0
Shadowsocks.Interop/Shadowsocks.Interop.csproj View File

@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>

+ 16
- 0
Shadowsocks.Interop/SsRust/Config.cs View File

@@ -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()
{

}
}
}

+ 16
- 0
Shadowsocks.Interop/V2Ray/Config.cs View File

@@ -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()
{

}
}
}

+ 26
- 0
Shadowsocks.Net/Settings/NetSettings.cs View File

@@ -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
- 1
Shadowsocks.Net/Shadowsocks.Net.csproj View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>


+ 2
- 5
Shadowsocks.PAC/GeositeUpdater.cs View File

@@ -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.");
}
}



+ 7
- 0
Shadowsocks.PAC/PACSettings.cs View File

@@ -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
- 1
Shadowsocks.PAC/Shadowsocks.PAC.csproj View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>



+ 1
- 1
Shadowsocks.Protobuf/Shadowsocks.Protobuf.csproj View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>

<ItemGroup>


+ 0
- 67
Shadowsocks.WPF/Behaviors/OnlineConfigResolver.cs View File

@@ -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);
}
}
}
}

+ 0
- 125
Shadowsocks.WPF/Behaviors/Utilities.cs View File

@@ -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;
}
}
}
}
}

+ 1
- 1
Shadowsocks.WPF/Localization/LocalizationProvider.cs View File

@@ -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}");
}
}

+ 22
- 0
Shadowsocks.WPF/Models/AppSettings.cs View File

@@ -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 = "";
}
}
}

+ 0
- 252
Shadowsocks.WPF/Models/Server.cs View File

@@ -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;
}
}
}

+ 19
- 3
Shadowsocks.WPF/Models/Settings.cs View File

@@ -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>();
}


}
}

+ 1
- 3
Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml View File

@@ -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>

+ 0
- 6
Shadowsocks.WPF/Properties/PublishProfiles/FolderProfile.pubxml.user View File

@@ -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>

+ 18
- 0
Shadowsocks.WPF/Properties/PublishProfiles/win-arm.pubxml View File

@@ -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>

+ 2
- 2
Shadowsocks.WPF/Properties/PublishProfiles/win-x64.pubxml View File

@@ -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>


+ 2
- 2
Shadowsocks.WPF/Properties/PublishProfiles/win-x86.pubxml View File

@@ -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>


+ 45
- 0
Shadowsocks.WPF/Services/OnlineConfigService.cs View File

@@ -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
}
}
}

+ 13
- 15
Shadowsocks.WPF/Services/PortForwarder.cs View File

@@ -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, "");
}
}
}


+ 28
- 35
Shadowsocks.WPF/Services/PrivoxyRunner.cs View File

@@ -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;
}
}


+ 9
- 12
Shadowsocks.WPF/Services/Sip003Plugin.cs View File

@@ -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) { }



+ 49
- 46
Shadowsocks.WPF/Services/UpdateChecker.cs View File

@@ -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();
}



+ 8
- 2
Shadowsocks.WPF/Shadowsocks.WPF.csproj View File

@@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Authors>clowwindy &amp; 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>

Shadowsocks.WPF/Behaviors/AutoStartup.cs → Shadowsocks.WPF/Utils/AutoStartup.cs View File

@@ -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");
}
}
}

Shadowsocks.WPF/Behaviors/FileManager.cs → Shadowsocks.WPF/Utils/FileManager.cs View File

@@ -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;
}
}

Shadowsocks.WPF/Behaviors/IPCService.cs → Shadowsocks.WPF/Utils/IPCService.cs View File

@@ -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;


Shadowsocks.WPF/Behaviors/ProtocolHandler.cs → Shadowsocks.WPF/Utils/ProtocolHandler.cs View File

@@ -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.");
}
}
}
}

Shadowsocks.WPF/Behaviors/SystemProxy.cs → Shadowsocks.WPF/Utils/SystemProxy.cs View File

@@ -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;

+ 117
- 0
Shadowsocks.WPF/Utils/Utilities.cs View File

@@ -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;
}
}
}
}
}

+ 2
- 12
Shadowsocks.WPF/ViewModels/ForwardProxyViewModel.cs View File

@@ -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; }


+ 0
- 11
Shadowsocks.WPF/ViewModels/OnlineConfigViewModel.cs View File

@@ -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; }


+ 7
- 11
Shadowsocks.WPF/ViewModels/ServerSharingViewModel.cs View File

@@ -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);


+ 11
- 7
Shadowsocks.WPF/ViewModels/VersionUpdatePromptViewModel.cs View File

@@ -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; }



+ 2
- 2
Shadowsocks.WPF/Views/VersionUpdatePromptView.xaml.cs View File

@@ -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);


+ 69
- 0
Shadowsocks/Models/Group.cs View File

@@ -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>();
}
}
}

+ 87
- 0
Shadowsocks/Models/JsonSnakeCaseNamingPolicy.cs View File

@@ -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();
}
}
}

+ 72
- 1
Shadowsocks/Models/Server.cs View File

@@ -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
- 1
Shadowsocks/Shadowsocks.csproj View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>



+ 6
- 0
shadowsocks-windows.sln View File

@@ -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


Loading…
Cancel
Save