Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions Server.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;

namespace Shadowsocks.Models
{
public class Server : IServer
{
/// <inheritdoc/>
[JsonPropertyName("server")]
public string Host { get; set; }

/// <inheritdoc/>
[JsonPropertyName("server_port")]
public int Port { get; set; }

/// <inheritdoc/>
public string Password { get; set; }

/// <inheritdoc/>
public string Method { get; set; }

/// <inheritdoc/>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string? Plugin { get; set; }

/// <inheritdoc/>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public string? PluginOpts { get; set; }

/// <summary>
/// Gets or sets the arguments passed to the plugin process.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public List<string>? PluginArgs { get; set; }

/// <inheritdoc/>
[JsonPropertyName("remarks")]
public string Name { get; set; }

/// <inheritdoc/>
[JsonPropertyName("id")]
public string Uuid { get; set; }

public Server()
{
Host = "";
Port = 8388;
Password = "";
Method = "chacha20-ietf-poly1305";
Name = "";
Uuid = Guid.NewGuid().ToString();
}

public bool Equals(IServer? other) => other is Server anotherServer && Uuid == anotherServer.Uuid;
public override int GetHashCode() => Uuid.GetHashCode();
public override string ToString() => Name;

/// <summary>
/// Converts this server object into an ss:// URL.
/// </summary>
/// <returns></returns>
public Uri ToUrl()
{
UriBuilder uriBuilder = new("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, [NotNullWhen(true)] out Server? server)
{
server = null;
return Uri.TryCreate(url, UriKind.Absolute, out var uri) && TryParse(uri, out server);
}

/// <summary>
/// Tries to parse an ss:// URL into a Server object.
/// </summary>
/// <param name="uri">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(Uri uri, [NotNullWhen(true)] out Server? server)
{
server = null;
try
{
if (uri.Scheme != "ss")
return false;
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];
var host = uri.HostNameType == UriHostNameType.IPv6 ? uri.Host[1..^1] : uri.Host;
var escapedFragment = string.IsNullOrEmpty(uri.Fragment) ? uri.Fragment : uri.Fragment[1..];
var name = Uri.UnescapeDataString(escapedFragment);
server = new Server()
{
Name = name,
Uuid = Guid.NewGuid().ToString(),
Host = host,
Port = uri.Port,
Password = password,
Method = 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="
}
}
if (string.IsNullOrEmpty(pluginQueryContent)) // no plugin
return true;
var unescapedpluginQuery = Uri.UnescapeDataString(pluginQueryContent);
var parsedPluginQueryArray = unescapedpluginQuery.Split(';', 2);
if (parsedPluginQueryArray.Length == 1)
{
server.Plugin = parsedPluginQueryArray[0];
}
else if (parsedPluginQueryArray.Length == 2) // is valid plugin query
{
server.Plugin = parsedPluginQueryArray[0];
server.PluginOpts = parsedPluginQueryArray[1];
}
return true;
}
catch
{
return false;
}
}
}
}
175 changes: 175 additions & 0 deletions Sip003Plugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using Shadowsocks.WPF.Models;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;

namespace Shadowsocks.WPF.Services
{
// https://github.com/shadowsocks/shadowsocks-org/wiki/Plugin
public sealed class Sip003Plugin : IDisposable
{
public IPEndPoint? LocalEndPoint { get; private set; }
public int ProcessId => _started ? _pluginProcess.Id : 0;

private readonly object _startProcessLock = new object();
private readonly Process _pluginProcess;
private bool _started;
private bool _disposed;

public static Sip003Plugin? CreateIfConfigured(Server server, bool showPluginOutput)
{
if (server == null)
{
throw new ArgumentNullException(nameof(server));
}

if (string.IsNullOrWhiteSpace(server.Plugin))
{
return null;
}

return new Sip003Plugin(
server.Plugin,
server.PluginOpts,
server.PluginArgs,
server.Host,
server.Port,
showPluginOutput);
}

private Sip003Plugin(string plugin, string? pluginOpts, List<string>? pluginArgs, string serverAddress, int serverPort, bool showPluginOutput)
{
if (plugin == null) throw new ArgumentNullException(nameof(plugin));
if (string.IsNullOrWhiteSpace(serverAddress))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(serverAddress));
}
if (serverPort <= 0 || serverPort > 65535)
{
throw new ArgumentOutOfRangeException(nameof(serverPort));
}

var pluginProcessStartInfo = new ProcessStartInfo
{
FileName = plugin,
UseShellExecute = false,
CreateNoWindow = !showPluginOutput,
ErrorDialog = false,
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = Utils.Utilities.WorkingDirectory ?? Environment.CurrentDirectory,
Environment =
{
["SS_REMOTE_HOST"] = serverAddress,
["SS_REMOTE_PORT"] = serverPort.ToString(),
["SS_PLUGIN_OPTIONS"] = pluginOpts
}
};
if (pluginArgs != null)
foreach (var arg in pluginArgs)
pluginProcessStartInfo.ArgumentList.Add(arg);

_pluginProcess = new Process()
{
StartInfo = pluginProcessStartInfo,
};
}

public bool StartIfNeeded()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().FullName);
}

lock (_startProcessLock)
{
if (_started && !_pluginProcess.HasExited)
{
return false;
}

var localPort = GetNextFreeTcpPort();
LocalEndPoint = new IPEndPoint(IPAddress.Loopback, localPort);

_pluginProcess.StartInfo.Environment["SS_LOCAL_HOST"] = LocalEndPoint.Address.ToString();
_pluginProcess.StartInfo.Environment["SS_LOCAL_PORT"] = LocalEndPoint.Port.ToString();
_pluginProcess.StartInfo.Arguments = ExpandEnvironmentVariables(_pluginProcess.StartInfo.Arguments, _pluginProcess.StartInfo.EnvironmentVariables);
try
{
_pluginProcess.Start();
}
catch (System.ComponentModel.Win32Exception ex)
{
// do not use File.Exists(...), it can not handle the scenarios when the plugin file is in system environment path.
// https://docs.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values
//if ((uint)ex.ErrorCode == 0x80004005)
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d
if (ex.NativeErrorCode == 0x00000002)
{
throw new FileNotFoundException("Cannot find the plugin program file", _pluginProcess.StartInfo.FileName, ex);
}
throw new ApplicationException("Plugin Program", ex);
}
_started = true;
}

return true;
}

public static string ExpandEnvironmentVariables(string name, StringDictionary? environmentVariables = null)
{
// Expand the environment variables from the new process itself
if (environmentVariables != null)
{
foreach(string key in environmentVariables.Keys)
{
name = name.Replace($"%{key}%", environmentVariables[key], StringComparison.OrdinalIgnoreCase);
}
}
// Also expand the environment variables from current main process (system)
name = Environment.ExpandEnvironmentVariables(name);
return name;
}

public static int GetNextFreeTcpPort()
{
var l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
var port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return port;
}

public void Dispose()
{
if (_disposed)
{
return;
}

try
{
if (!_pluginProcess.HasExited)
{
_pluginProcess.Kill();
_pluginProcess.WaitForExit();
}
}
catch (Exception) { }
finally
{
try
{
_pluginProcess.Dispose();
}
catch (Exception) { }

_disposed = true;
}
}
}
}
Loading