diff --git a/shadowsocks-csharp/Controller/Service/Sip003Plugin.cs b/shadowsocks-csharp/Controller/Service/Sip003Plugin.cs new file mode 100644 index 00000000..afd395c9 --- /dev/null +++ b/shadowsocks-csharp/Controller/Service/Sip003Plugin.cs @@ -0,0 +1,134 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using Shadowsocks.Model; + +namespace Shadowsocks.Controller.Service +{ + // 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) + { + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } + + if (string.IsNullOrWhiteSpace(server.plugin)) + { + return null; + } + + return new Sip003Plugin(server.plugin, server.plugin_opts, server.server, server.server_port); + } + + private Sip003Plugin(string plugin, string pluginOpts, string serverAddress, int serverPort) + { + if (plugin == null) throw new ArgumentNullException(nameof(plugin)); + if (string.IsNullOrWhiteSpace(serverAddress)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(serverAddress)); + } + if ((ushort)serverPort != serverPort) + { + throw new ArgumentOutOfRangeException("serverPort"); + } + + var appPath = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().GetName().CodeBase).LocalPath); + + _pluginProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = plugin, + UseShellExecute = false, + CreateNoWindow = true, + ErrorDialog = false, + WindowStyle = ProcessWindowStyle.Hidden, + WorkingDirectory = appPath ?? Environment.CurrentDirectory, + Environment = + { + ["SS_REMOTE_HOST"] = serverAddress, + ["SS_REMOTE_PORT"] = serverPort.ToString(), + ["SS_PLUGIN_OPTIONS"] = pluginOpts + } + } + }; + } + + 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.Start(); + _started = true; + } + + return true; + } + + static int GetNextFreeTcpPort() + { + var l = new TcpListener(IPAddress.Loopback, 0); + l.Start(); + int 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; + } + } + } +} \ No newline at end of file diff --git a/shadowsocks-csharp/Controller/Service/TCPRelay.cs b/shadowsocks-csharp/Controller/Service/TCPRelay.cs index f90349b0..d6bbe153 100644 --- a/shadowsocks-csharp/Controller/Service/TCPRelay.cs +++ b/shadowsocks-csharp/Controller/Service/TCPRelay.cs @@ -570,7 +570,15 @@ namespace Shadowsocks.Controller // Setting up proxy IProxy remote; EndPoint proxyEP = null; - if (_config.proxy.useProxy) + EndPoint serverEP = SocketUtil.GetEndPoint(_server.server, _server.server_port); + EndPoint pluginEP = _controller.GetPluginLocalEndPointIfConfigured(_server); + + if (pluginEP != null) + { + serverEP = pluginEP; + remote = new DirectConnect(); + } + else if (_config.proxy.useProxy) { switch (_config.proxy.proxyType) { @@ -607,7 +615,7 @@ namespace Shadowsocks.Controller proxyTimer.Enabled = true; proxyTimer.Session = session; - proxyTimer.DestEndPoint = SocketUtil.GetEndPoint(_server.server, _server.server_port); + proxyTimer.DestEndPoint = serverEP; proxyTimer.Server = _server; _proxyConnected = false; diff --git a/shadowsocks-csharp/Controller/ShadowsocksController.cs b/shadowsocks-csharp/Controller/ShadowsocksController.cs index 553c50e5..9b27a700 100644 --- a/shadowsocks-csharp/Controller/ShadowsocksController.cs +++ b/shadowsocks-csharp/Controller/ShadowsocksController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; @@ -14,6 +15,8 @@ using Shadowsocks.Model; using Shadowsocks.Properties; using Shadowsocks.Util; using System.Linq; +using Shadowsocks.Controller.Service; +using Shadowsocks.Proxy; namespace Shadowsocks.Controller { @@ -33,6 +36,8 @@ namespace Shadowsocks.Controller private StrategyManager _strategyManager; private PrivoxyRunner privoxyRunner; private GFWListUpdater gfwListUpdater; + private readonly ConcurrentDictionary _pluginsByServer; + public AvailabilityStatistics availabilityStatistics = AvailabilityStatistics.Instance; public StatisticsStrategyConfiguration StatisticsConfiguration { get; private set; } @@ -79,6 +84,7 @@ namespace Shadowsocks.Controller _config = Configuration.Load(); StatisticsConfiguration = StatisticsStrategyConfiguration.Load(); _strategyManager = new StrategyManager(this); + _pluginsByServer = new ConcurrentDictionary(); StartReleasingMemory(); StartTrafficStatistics(61); } @@ -144,6 +150,23 @@ namespace Shadowsocks.Controller return GetCurrentServer(); } + public EndPoint GetPluginLocalEndPointIfConfigured(Server server) + { + var plugin = _pluginsByServer.GetOrAdd(server, Sip003Plugin.CreateIfConfigured); + if (plugin == null) + { + return null; + } + + if (plugin.StartIfNeeded()) + { + Logging.Info( + $"Started SIP003 plugin for {server.Identifier()} on {plugin.LocalEndPoint} - PID: {plugin.ProcessId}"); + } + + return plugin.LocalEndPoint; + } + public void SaveServers(List servers, int localPort) { _config.configs = servers; @@ -259,6 +282,7 @@ namespace Shadowsocks.Controller { _listener.Stop(); } + StopPlugins(); if (privoxyRunner != null) { privoxyRunner.Stop(); @@ -270,6 +294,15 @@ namespace Shadowsocks.Controller Encryption.RNG.Close(); } + private void StopPlugins() + { + foreach (var serverAndPlugin in _pluginsByServer) + { + serverAndPlugin.Value?.Dispose(); + } + _pluginsByServer.Clear(); + } + public void TouchPACFile() { string pacFilename = _pacServer.TouchPACFile(); @@ -297,13 +330,41 @@ namespace Shadowsocks.Controller public static string GetQRCode(Server server) { string tag = string.Empty; - string parts = $"{server.method}:{server.password}@{server.server}:{server.server_port}"; - string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(parts)); - if(!server.remarks.IsNullOrEmpty()) + string url = string.Empty; + + if (string.IsNullOrWhiteSpace(server.plugin)) + { + // For backwards compatiblity, if no plugin, use old url format + string parts = $"{server.method}:{server.password}@{server.server}:{server.server_port}"; + string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(parts)); + url = base64; + } + else + { + // SIP002 + string parts = $"{server.method}:{server.password}"; + string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(parts)); + string websafeBase64 = base64.Replace('+', '-').Replace('/', '_').TrimEnd('='); + + string pluginPart = server.plugin; + if (!string.IsNullOrWhiteSpace(server.plugin_opts)) + { + pluginPart += ";" + server.plugin_opts; + } + + url = string.Format( + "{0}@{1}:{2}/?plugin={3}", + websafeBase64, + HttpUtility.UrlEncode(server.server, Encoding.UTF8), + server.server_port, + HttpUtility.UrlEncode(pluginPart, Encoding.UTF8)); + } + + if (!server.remarks.IsNullOrEmpty()) { tag = $"#{HttpUtility.UrlEncode(server.remarks, Encoding.UTF8)}"; } - return $"ss://{base64}{tag}"; + return $"ss://{url}{tag}"; } public void UpdatePACFromGFWList() @@ -421,6 +482,8 @@ namespace Shadowsocks.Controller protected void Reload() { + StopPlugins(); + Encryption.RNG.Reload(); // some logic in configuration updated the config when saving, we need to read it again _config = Configuration.Load(); diff --git a/shadowsocks-csharp/Model/Server.cs b/shadowsocks-csharp/Model/Server.cs index 3d71dc75..6082354c 100755 --- a/shadowsocks-csharp/Model/Server.cs +++ b/shadowsocks-csharp/Model/Server.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Text; -using System.Text.RegularExpressions; using System.Web; using Shadowsocks.Controller; @@ -10,10 +10,6 @@ namespace Shadowsocks.Model [Serializable] public class Server { - public static readonly Regex - UrlFinder = new Regex(@"ss://(?[A-Za-z0-9+-/=_]+)(?:#(?\S+))?", RegexOptions.IgnoreCase), - DetailsParser = new Regex(@"^((?.+?):(?.*)@(?.+?):(?\d+?))$", RegexOptions.IgnoreCase); - private const int DefaultServerTimeoutSec = 5; public const int MaxServerTimeoutSec = 20; @@ -21,6 +17,8 @@ namespace Shadowsocks.Model public int server_port; public string password; public string method; + public string plugin; + public string plugin_opts; public string remarks; public int timeout; @@ -65,6 +63,8 @@ namespace Shadowsocks.Model server = ""; server_port = 8388; method = "aes-256-cfb"; + plugin = ""; + plugin_opts = ""; password = ""; remarks = ""; timeout = DefaultServerTimeoutSec; @@ -72,26 +72,95 @@ namespace Shadowsocks.Model public static List GetServers(string ssURL) { - var matches = UrlFinder.Matches(ssURL); - if (matches.Count <= 0) return null; + var serverUrls = ssURL.Split('\r', '\n'); + List servers = new List(); - foreach (Match match in matches) + foreach (string serverUrl in serverUrls) { - Server tmp = new Server(); - var base64 = match.Groups["base64"].Value; - var tag = match.Groups["tag"].Value; - if (!tag.IsNullOrEmpty()) + if (string.IsNullOrWhiteSpace(serverUrl)) + { + continue; + } + + Uri parsedUrl; + try + { + parsedUrl = new Uri(serverUrl); + } + catch (UriFormatException) + { + continue; + } + + Server tmp = new Server + { + remarks = parsedUrl.GetComponents(UriComponents.Fragment, UriFormat.Unescaped) + }; + + string possiblyUnpaddedBase64 = parsedUrl.GetComponents(UriComponents.UserInfo, UriFormat.Unescaped); + bool isOldFormatUrl = possiblyUnpaddedBase64.Length == 0; + if (isOldFormatUrl) + { + int prefixLength = "ss://".Length; + int indexOfHashOrSlash = serverUrl.LastIndexOfAny( + new[] { '/', '#' }, + serverUrl.Length - 1, + serverUrl.Length - prefixLength); + + int substringLength = serverUrl.Length - prefixLength; + if (indexOfHashOrSlash >= 0) + { + substringLength = indexOfHashOrSlash - prefixLength; + } + + possiblyUnpaddedBase64 = serverUrl.Substring(prefixLength, substringLength).TrimEnd('/'); + } + else + { + // Web-safe base64 to normal base64 + possiblyUnpaddedBase64 = possiblyUnpaddedBase64.Replace('-', '+').Replace('_', '/'); + } + + string base64 = possiblyUnpaddedBase64.PadRight( + possiblyUnpaddedBase64.Length + (4 - possiblyUnpaddedBase64.Length % 4) % 4, + '='); + + string innerUserInfoOrUrl = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + string userInfo; + if (isOldFormatUrl) { - tmp.remarks = HttpUtility.UrlDecode(tag, Encoding.UTF8); + Uri innerUrl = new Uri("inner://" + innerUserInfoOrUrl); + userInfo = innerUrl.GetComponents(UriComponents.UserInfo, UriFormat.Unescaped); + tmp.server = innerUrl.GetComponents(UriComponents.Host, UriFormat.Unescaped); + tmp.server_port = innerUrl.Port; } - Match details = DetailsParser.Match(Encoding.UTF8.GetString(Convert.FromBase64String( - base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '=')))); - if (!details.Success) + else + { + userInfo = innerUserInfoOrUrl; + tmp.server = parsedUrl.GetComponents(UriComponents.Host, UriFormat.Unescaped); + tmp.server_port = parsedUrl.Port; + } + + string[] userInfoParts = userInfo.Split(new[] { ':' }, 2); + if (userInfoParts.Length != 2) + { continue; - tmp.method = details.Groups["method"].Value; - tmp.password = details.Groups["password"].Value; - tmp.server = details.Groups["hostname"].Value; - tmp.server_port = int.Parse(details.Groups["port"].Value); + } + + tmp.method = userInfoParts[0]; + tmp.password = userInfoParts[1]; + + NameValueCollection queryParameters = HttpUtility.ParseQueryString(parsedUrl.Query); + string[] pluginParts = HttpUtility.UrlDecode(queryParameters["plugin"] ?? "").Split(new[] { ';' }, 2); + if (pluginParts.Length > 0) + { + tmp.plugin = pluginParts[0] ?? ""; + } + + if (pluginParts.Length > 1) + { + tmp.plugin_opts = pluginParts[1] ?? ""; + } servers.Add(tmp); } diff --git a/shadowsocks-csharp/View/ConfigForm.Designer.cs b/shadowsocks-csharp/View/ConfigForm.Designer.cs index f389cfe2..f0c241ae 100755 --- a/shadowsocks-csharp/View/ConfigForm.Designer.cs +++ b/shadowsocks-csharp/View/ConfigForm.Designer.cs @@ -29,6 +29,8 @@ private void InitializeComponent() { this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + this.PluginOptionsLabel = new System.Windows.Forms.Label(); + this.PluginTextBox = new System.Windows.Forms.TextBox(); this.RemarksTextBox = new System.Windows.Forms.TextBox(); this.RemarksLabel = new System.Windows.Forms.Label(); this.IPLabel = new System.Windows.Forms.Label(); @@ -41,6 +43,8 @@ this.EncryptionSelect = new System.Windows.Forms.ComboBox(); this.TimeoutLabel = new System.Windows.Forms.Label(); this.TimeoutTextBox = new System.Windows.Forms.TextBox(); + this.PluginLabel = new System.Windows.Forms.Label(); + this.PluginOptionsTextBox = new System.Windows.Forms.TextBox(); this.panel2 = new System.Windows.Forms.Panel(); this.OKButton = new System.Windows.Forms.Button(); this.MyCancelButton = new System.Windows.Forms.Button(); @@ -74,8 +78,10 @@ this.tableLayoutPanel1.ColumnCount = 2; this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); - this.tableLayoutPanel1.Controls.Add(this.RemarksTextBox, 1, 5); - this.tableLayoutPanel1.Controls.Add(this.RemarksLabel, 0, 5); + this.tableLayoutPanel1.Controls.Add(this.PluginOptionsLabel, 0, 5); + this.tableLayoutPanel1.Controls.Add(this.PluginTextBox, 1, 4); + this.tableLayoutPanel1.Controls.Add(this.RemarksTextBox, 1, 7); + this.tableLayoutPanel1.Controls.Add(this.RemarksLabel, 0, 7); this.tableLayoutPanel1.Controls.Add(this.IPLabel, 0, 0); this.tableLayoutPanel1.Controls.Add(this.ServerPortLabel, 0, 1); this.tableLayoutPanel1.Controls.Add(this.PasswordLabel, 0, 2); @@ -84,13 +90,15 @@ this.tableLayoutPanel1.Controls.Add(this.PasswordTextBox, 1, 2); this.tableLayoutPanel1.Controls.Add(this.EncryptionLabel, 0, 3); this.tableLayoutPanel1.Controls.Add(this.EncryptionSelect, 1, 3); - this.tableLayoutPanel1.Controls.Add(this.TimeoutLabel, 0, 6); - this.tableLayoutPanel1.Controls.Add(this.TimeoutTextBox, 1, 6); + this.tableLayoutPanel1.Controls.Add(this.TimeoutLabel, 0, 8); + this.tableLayoutPanel1.Controls.Add(this.TimeoutTextBox, 1, 8); + this.tableLayoutPanel1.Controls.Add(this.PluginLabel, 0, 4); + this.tableLayoutPanel1.Controls.Add(this.PluginOptionsTextBox, 1, 5); this.tableLayoutPanel1.Location = new System.Drawing.Point(8, 21); this.tableLayoutPanel1.Margin = new System.Windows.Forms.Padding(0); this.tableLayoutPanel1.Name = "tableLayoutPanel1"; this.tableLayoutPanel1.Padding = new System.Windows.Forms.Padding(3); - this.tableLayoutPanel1.RowCount = 7; + this.tableLayoutPanel1.RowCount = 9; this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); @@ -98,26 +106,48 @@ this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel1.Size = new System.Drawing.Size(255, 167); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanel1.Size = new System.Drawing.Size(253, 215); this.tableLayoutPanel1.TabIndex = 0; // + // PluginOptionsLabel + // + this.PluginOptionsLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.PluginOptionsLabel.AutoSize = true; + this.PluginOptionsLabel.Location = new System.Drawing.Point(6, 140); + this.PluginOptionsLabel.Name = "PluginOptionsLabel"; + this.PluginOptionsLabel.Size = new System.Drawing.Size(75, 13); + this.PluginOptionsLabel.TabIndex = 15; + this.PluginOptionsLabel.Text = "Plugin Options"; + // + // PluginTextBox + // + this.PluginTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.PluginTextBox.Location = new System.Drawing.Point(87, 111); + this.PluginTextBox.MaxLength = 256; + this.PluginTextBox.Name = "PluginTextBox"; + this.PluginTextBox.Size = new System.Drawing.Size(160, 20); + this.PluginTextBox.TabIndex = 4; + this.PluginTextBox.WordWrap = false; + // // RemarksTextBox // this.RemarksTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); - this.RemarksTextBox.Location = new System.Drawing.Point(89, 113); + this.RemarksTextBox.Location = new System.Drawing.Point(87, 163); this.RemarksTextBox.MaxLength = 32; this.RemarksTextBox.Name = "RemarksTextBox"; - this.RemarksTextBox.Size = new System.Drawing.Size(160, 21); - this.RemarksTextBox.TabIndex = 4; + this.RemarksTextBox.Size = new System.Drawing.Size(160, 20); + this.RemarksTextBox.TabIndex = 6; this.RemarksTextBox.WordWrap = false; // // RemarksLabel // this.RemarksLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; this.RemarksLabel.AutoSize = true; - this.RemarksLabel.Location = new System.Drawing.Point(36, 117); + this.RemarksLabel.Location = new System.Drawing.Point(32, 166); this.RemarksLabel.Name = "RemarksLabel"; - this.RemarksLabel.Size = new System.Drawing.Size(47, 12); + this.RemarksLabel.Size = new System.Drawing.Size(49, 13); this.RemarksLabel.TabIndex = 9; this.RemarksLabel.Text = "Remarks"; // @@ -125,9 +155,9 @@ // this.IPLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; this.IPLabel.AutoSize = true; - this.IPLabel.Location = new System.Drawing.Point(24, 10); + this.IPLabel.Location = new System.Drawing.Point(30, 9); this.IPLabel.Name = "IPLabel"; - this.IPLabel.Size = new System.Drawing.Size(59, 12); + this.IPLabel.Size = new System.Drawing.Size(51, 13); this.IPLabel.TabIndex = 0; this.IPLabel.Text = "Server IP"; // @@ -135,9 +165,9 @@ // this.ServerPortLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; this.ServerPortLabel.AutoSize = true; - this.ServerPortLabel.Location = new System.Drawing.Point(12, 37); + this.ServerPortLabel.Location = new System.Drawing.Point(21, 35); this.ServerPortLabel.Name = "ServerPortLabel"; - this.ServerPortLabel.Size = new System.Drawing.Size(71, 12); + this.ServerPortLabel.Size = new System.Drawing.Size(60, 13); this.ServerPortLabel.TabIndex = 1; this.ServerPortLabel.Text = "Server Port"; // @@ -145,39 +175,39 @@ // this.PasswordLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; this.PasswordLabel.AutoSize = true; - this.PasswordLabel.Location = new System.Drawing.Point(30, 64); + this.PasswordLabel.Location = new System.Drawing.Point(28, 61); this.PasswordLabel.Name = "PasswordLabel"; - this.PasswordLabel.Size = new System.Drawing.Size(53, 12); + this.PasswordLabel.Size = new System.Drawing.Size(53, 13); this.PasswordLabel.TabIndex = 2; this.PasswordLabel.Text = "Password"; // // IPTextBox // this.IPTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); - this.IPTextBox.Location = new System.Drawing.Point(89, 6); + this.IPTextBox.Location = new System.Drawing.Point(87, 6); this.IPTextBox.MaxLength = 512; this.IPTextBox.Name = "IPTextBox"; - this.IPTextBox.Size = new System.Drawing.Size(160, 21); + this.IPTextBox.Size = new System.Drawing.Size(160, 20); this.IPTextBox.TabIndex = 0; this.IPTextBox.WordWrap = false; // // ServerPortTextBox // this.ServerPortTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); - this.ServerPortTextBox.Location = new System.Drawing.Point(89, 33); + this.ServerPortTextBox.Location = new System.Drawing.Point(87, 32); this.ServerPortTextBox.MaxLength = 10; this.ServerPortTextBox.Name = "ServerPortTextBox"; - this.ServerPortTextBox.Size = new System.Drawing.Size(160, 21); + this.ServerPortTextBox.Size = new System.Drawing.Size(160, 20); this.ServerPortTextBox.TabIndex = 1; this.ServerPortTextBox.WordWrap = false; // // PasswordTextBox // this.PasswordTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); - this.PasswordTextBox.Location = new System.Drawing.Point(89, 60); + this.PasswordTextBox.Location = new System.Drawing.Point(87, 58); this.PasswordTextBox.MaxLength = 256; this.PasswordTextBox.Name = "PasswordTextBox"; - this.PasswordTextBox.Size = new System.Drawing.Size(160, 21); + this.PasswordTextBox.Size = new System.Drawing.Size(160, 20); this.PasswordTextBox.TabIndex = 2; this.PasswordTextBox.UseSystemPasswordChar = true; this.PasswordTextBox.WordWrap = false; @@ -186,9 +216,9 @@ // this.EncryptionLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; this.EncryptionLabel.AutoSize = true; - this.EncryptionLabel.Location = new System.Drawing.Point(18, 91); + this.EncryptionLabel.Location = new System.Drawing.Point(24, 88); this.EncryptionLabel.Name = "EncryptionLabel"; - this.EncryptionLabel.Size = new System.Drawing.Size(65, 12); + this.EncryptionLabel.Size = new System.Drawing.Size(57, 13); this.EncryptionLabel.TabIndex = 8; this.EncryptionLabel.Text = "Encryption"; // @@ -199,7 +229,7 @@ this.EncryptionSelect.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; this.EncryptionSelect.FormattingEnabled = true; this.EncryptionSelect.ImeMode = System.Windows.Forms.ImeMode.NoControl; - this.EncryptionSelect.ItemHeight = 12; + this.EncryptionSelect.ItemHeight = 13; this.EncryptionSelect.Items.AddRange(new object[] { "rc4-md5", "salsa20", @@ -219,30 +249,50 @@ "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305"}); - this.EncryptionSelect.Location = new System.Drawing.Point(89, 87); + this.EncryptionSelect.Location = new System.Drawing.Point(87, 84); this.EncryptionSelect.Name = "EncryptionSelect"; - this.EncryptionSelect.Size = new System.Drawing.Size(160, 20); + this.EncryptionSelect.Size = new System.Drawing.Size(160, 21); this.EncryptionSelect.TabIndex = 3; // // TimeoutLabel // this.TimeoutLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; this.TimeoutLabel.AutoSize = true; - this.TimeoutLabel.Location = new System.Drawing.Point(6, 144); + this.TimeoutLabel.Location = new System.Drawing.Point(11, 192); this.TimeoutLabel.Name = "TimeoutLabel"; this.TimeoutLabel.RightToLeft = System.Windows.Forms.RightToLeft.No; - this.TimeoutLabel.Size = new System.Drawing.Size(77, 12); + this.TimeoutLabel.Size = new System.Drawing.Size(70, 13); this.TimeoutLabel.TabIndex = 10; this.TimeoutLabel.Text = "Timeout(Sec)"; // // TimeoutTextBox // this.TimeoutTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); - this.TimeoutTextBox.Location = new System.Drawing.Point(89, 140); + this.TimeoutTextBox.Location = new System.Drawing.Point(87, 189); this.TimeoutTextBox.MaxLength = 5; this.TimeoutTextBox.Name = "TimeoutTextBox"; - this.TimeoutTextBox.Size = new System.Drawing.Size(160, 21); - this.TimeoutTextBox.TabIndex = 11; + this.TimeoutTextBox.Size = new System.Drawing.Size(160, 20); + this.TimeoutTextBox.TabIndex = 7; + // + // PluginLabel + // + this.PluginLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.PluginLabel.AutoSize = true; + this.PluginLabel.Location = new System.Drawing.Point(45, 114); + this.PluginLabel.Name = "PluginLabel"; + this.PluginLabel.Size = new System.Drawing.Size(36, 13); + this.PluginLabel.TabIndex = 12; + this.PluginLabel.Text = "Plugin"; + // + // PluginOptionsTextBox + // + this.PluginOptionsTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right))); + this.PluginOptionsTextBox.Location = new System.Drawing.Point(87, 137); + this.PluginOptionsTextBox.MaxLength = 256; + this.PluginOptionsTextBox.Name = "PluginOptionsTextBox"; + this.PluginOptionsTextBox.Size = new System.Drawing.Size(160, 20); + this.PluginOptionsTextBox.TabIndex = 5; + this.PluginOptionsTextBox.WordWrap = false; // // panel2 // @@ -262,7 +312,7 @@ this.OKButton.Margin = new System.Windows.Forms.Padding(3, 3, 3, 0); this.OKButton.Name = "OKButton"; this.OKButton.Size = new System.Drawing.Size(75, 23); - this.OKButton.TabIndex = 12; + this.OKButton.TabIndex = 15; this.OKButton.Text = "OK"; this.OKButton.UseVisualStyleBackColor = true; this.OKButton.Click += new System.EventHandler(this.OKButton_Click); @@ -275,7 +325,7 @@ this.MyCancelButton.Margin = new System.Windows.Forms.Padding(3, 3, 0, 0); this.MyCancelButton.Name = "MyCancelButton"; this.MyCancelButton.Size = new System.Drawing.Size(75, 23); - this.MyCancelButton.TabIndex = 13; + this.MyCancelButton.TabIndex = 16; this.MyCancelButton.Text = "Cancel"; this.MyCancelButton.UseVisualStyleBackColor = true; this.MyCancelButton.Click += new System.EventHandler(this.CancelButton_Click); @@ -287,7 +337,7 @@ this.DeleteButton.Margin = new System.Windows.Forms.Padding(3, 6, 0, 3); this.DeleteButton.Name = "DeleteButton"; this.DeleteButton.Size = new System.Drawing.Size(80, 23); - this.DeleteButton.TabIndex = 9; + this.DeleteButton.TabIndex = 11; this.DeleteButton.Text = "&Delete"; this.DeleteButton.UseVisualStyleBackColor = true; this.DeleteButton.Click += new System.EventHandler(this.DeleteButton_Click); @@ -299,7 +349,7 @@ this.AddButton.Margin = new System.Windows.Forms.Padding(0, 6, 3, 3); this.AddButton.Name = "AddButton"; this.AddButton.Size = new System.Drawing.Size(80, 23); - this.AddButton.TabIndex = 8; + this.AddButton.TabIndex = 10; this.AddButton.Text = "&Add"; this.AddButton.UseVisualStyleBackColor = true; this.AddButton.Click += new System.EventHandler(this.AddButton_Click); @@ -312,7 +362,7 @@ this.ServerGroupBox.Location = new System.Drawing.Point(178, 0); this.ServerGroupBox.Margin = new System.Windows.Forms.Padding(12, 0, 0, 0); this.ServerGroupBox.Name = "ServerGroupBox"; - this.ServerGroupBox.Size = new System.Drawing.Size(266, 205); + this.ServerGroupBox.Size = new System.Drawing.Size(264, 252); this.ServerGroupBox.TabIndex = 0; this.ServerGroupBox.TabStop = false; this.ServerGroupBox.Text = "Server"; @@ -321,12 +371,11 @@ // this.ServersListBox.FormattingEnabled = true; this.ServersListBox.IntegralHeight = false; - this.ServersListBox.ItemHeight = 12; this.ServersListBox.Location = new System.Drawing.Point(0, 0); this.ServersListBox.Margin = new System.Windows.Forms.Padding(0); this.ServersListBox.Name = "ServersListBox"; this.ServersListBox.Size = new System.Drawing.Size(166, 148); - this.ServersListBox.TabIndex = 7; + this.ServersListBox.TabIndex = 9; this.ServersListBox.SelectedIndexChanged += new System.EventHandler(this.ServersListBox_SelectedIndexChanged); // // tableLayoutPanel2 @@ -349,7 +398,7 @@ this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanel2.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanel2.Size = new System.Drawing.Size(444, 301); + this.tableLayoutPanel2.Size = new System.Drawing.Size(442, 348); this.tableLayoutPanel2.TabIndex = 7; // // tableLayoutPanel6 @@ -362,7 +411,7 @@ this.tableLayoutPanel6.Controls.Add(this.MoveDownButton, 1, 0); this.tableLayoutPanel6.Controls.Add(this.MoveUpButton, 0, 0); this.tableLayoutPanel6.Dock = System.Windows.Forms.DockStyle.Top; - this.tableLayoutPanel6.Location = new System.Drawing.Point(0, 269); + this.tableLayoutPanel6.Location = new System.Drawing.Point(0, 316); this.tableLayoutPanel6.Margin = new System.Windows.Forms.Padding(0); this.tableLayoutPanel6.Name = "tableLayoutPanel6"; this.tableLayoutPanel6.RowCount = 1; @@ -377,7 +426,7 @@ this.MoveDownButton.Margin = new System.Windows.Forms.Padding(3, 6, 0, 3); this.MoveDownButton.Name = "MoveDownButton"; this.MoveDownButton.Size = new System.Drawing.Size(80, 23); - this.MoveDownButton.TabIndex = 11; + this.MoveDownButton.TabIndex = 14; this.MoveDownButton.Text = "Move D&own"; this.MoveDownButton.UseVisualStyleBackColor = true; this.MoveDownButton.Click += new System.EventHandler(this.MoveDownButton_Click); @@ -389,7 +438,7 @@ this.MoveUpButton.Margin = new System.Windows.Forms.Padding(0, 6, 3, 3); this.MoveUpButton.Name = "MoveUpButton"; this.MoveUpButton.Size = new System.Drawing.Size(80, 23); - this.MoveUpButton.TabIndex = 10; + this.MoveUpButton.TabIndex = 13; this.MoveUpButton.Text = "Move &Up"; this.MoveUpButton.UseVisualStyleBackColor = true; this.MoveUpButton.Click += new System.EventHandler(this.MoveUpButton_Click); @@ -405,7 +454,7 @@ this.tableLayoutPanel5.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); this.tableLayoutPanel5.Controls.Add(this.ProxyPortTextBox, 1, 0); this.tableLayoutPanel5.Controls.Add(this.ProxyPortLabel, 0, 0); - this.tableLayoutPanel5.Location = new System.Drawing.Point(248, 205); + this.tableLayoutPanel5.Location = new System.Drawing.Point(256, 252); this.tableLayoutPanel5.Margin = new System.Windows.Forms.Padding(0); this.tableLayoutPanel5.Name = "tableLayoutPanel5"; this.tableLayoutPanel5.Padding = new System.Windows.Forms.Padding(3); @@ -415,26 +464,26 @@ this.tableLayoutPanel5.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 58F)); this.tableLayoutPanel5.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 58F)); this.tableLayoutPanel5.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 58F)); - this.tableLayoutPanel5.Size = new System.Drawing.Size(196, 64); + this.tableLayoutPanel5.Size = new System.Drawing.Size(186, 64); this.tableLayoutPanel5.TabIndex = 9; // // ProxyPortTextBox // this.ProxyPortTextBox.Anchor = System.Windows.Forms.AnchorStyles.Left; - this.ProxyPortTextBox.Location = new System.Drawing.Point(77, 21); + this.ProxyPortTextBox.Location = new System.Drawing.Point(67, 22); this.ProxyPortTextBox.MaxLength = 10; this.ProxyPortTextBox.Name = "ProxyPortTextBox"; - this.ProxyPortTextBox.Size = new System.Drawing.Size(113, 21); - this.ProxyPortTextBox.TabIndex = 6; + this.ProxyPortTextBox.Size = new System.Drawing.Size(113, 20); + this.ProxyPortTextBox.TabIndex = 8; this.ProxyPortTextBox.WordWrap = false; // // ProxyPortLabel // this.ProxyPortLabel.Anchor = System.Windows.Forms.AnchorStyles.Right; this.ProxyPortLabel.AutoSize = true; - this.ProxyPortLabel.Location = new System.Drawing.Point(6, 26); + this.ProxyPortLabel.Location = new System.Drawing.Point(6, 25); this.ProxyPortLabel.Name = "ProxyPortLabel"; - this.ProxyPortLabel.Size = new System.Drawing.Size(65, 12); + this.ProxyPortLabel.Size = new System.Drawing.Size(55, 13); this.ProxyPortLabel.TabIndex = 3; this.ProxyPortLabel.Text = "Proxy Port"; // @@ -449,7 +498,7 @@ this.tableLayoutPanel3.Controls.Add(this.MyCancelButton, 1, 0); this.tableLayoutPanel3.Controls.Add(this.OKButton, 0, 0); this.tableLayoutPanel3.Dock = System.Windows.Forms.DockStyle.Right; - this.tableLayoutPanel3.Location = new System.Drawing.Point(285, 272); + this.tableLayoutPanel3.Location = new System.Drawing.Point(283, 319); this.tableLayoutPanel3.Margin = new System.Windows.Forms.Padding(3, 3, 0, 3); this.tableLayoutPanel3.Name = "tableLayoutPanel3"; this.tableLayoutPanel3.RowCount = 1; @@ -468,7 +517,7 @@ this.tableLayoutPanel4.Controls.Add(this.DeleteButton, 1, 0); this.tableLayoutPanel4.Controls.Add(this.AddButton, 0, 0); this.tableLayoutPanel4.Dock = System.Windows.Forms.DockStyle.Top; - this.tableLayoutPanel4.Location = new System.Drawing.Point(0, 205); + this.tableLayoutPanel4.Location = new System.Drawing.Point(0, 252); this.tableLayoutPanel4.Margin = new System.Windows.Forms.Padding(0); this.tableLayoutPanel4.Name = "tableLayoutPanel4"; this.tableLayoutPanel4.RowCount = 2; @@ -484,7 +533,7 @@ this.DuplicateButton.Margin = new System.Windows.Forms.Padding(0, 6, 3, 3); this.DuplicateButton.Name = "DuplicateButton"; this.DuplicateButton.Size = new System.Drawing.Size(80, 23); - this.DuplicateButton.TabIndex = 10; + this.DuplicateButton.TabIndex = 12; this.DuplicateButton.Text = "Dupli&cate"; this.DuplicateButton.UseVisualStyleBackColor = true; this.DuplicateButton.Click += new System.EventHandler(this.DuplicateButton_Click); @@ -559,6 +608,10 @@ private System.Windows.Forms.Button DuplicateButton; private System.Windows.Forms.Label TimeoutLabel; private System.Windows.Forms.TextBox TimeoutTextBox; + private System.Windows.Forms.Label PluginOptionsLabel; + private System.Windows.Forms.TextBox PluginTextBox; + private System.Windows.Forms.Label PluginLabel; + private System.Windows.Forms.TextBox PluginOptionsTextBox; } } diff --git a/shadowsocks-csharp/View/ConfigForm.cs b/shadowsocks-csharp/View/ConfigForm.cs index f1f5f092..837251ef 100755 --- a/shadowsocks-csharp/View/ConfigForm.cs +++ b/shadowsocks-csharp/View/ConfigForm.cs @@ -47,6 +47,8 @@ namespace Shadowsocks.View ServerPortLabel.Text = I18N.GetString("Server Port"); PasswordLabel.Text = I18N.GetString("Password"); EncryptionLabel.Text = I18N.GetString("Encryption"); + PluginLabel.Text = I18N.GetString("Plugin"); + PluginOptionsLabel.Text = I18N.GetString("Plugin Options"); ProxyPortLabel.Text = I18N.GetString("Proxy Port"); RemarksLabel.Text = I18N.GetString("Remarks"); TimeoutLabel.Text = I18N.GetString("Timeout(Sec)"); @@ -94,6 +96,8 @@ namespace Shadowsocks.View } server.password = PasswordTextBox.Text; server.method = EncryptionSelect.Text; + server.plugin = PluginTextBox.Text; + server.plugin_opts = PluginOptionsTextBox.Text; server.remarks = RemarksTextBox.Text; if (!int.TryParse(TimeoutTextBox.Text, out server.timeout)) { @@ -127,6 +131,8 @@ namespace Shadowsocks.View PasswordTextBox.Text = server.password; ProxyPortTextBox.Text = _modifiedConfiguration.localPort.ToString(); EncryptionSelect.Text = server.method ?? "aes-256-cfb"; + PluginTextBox.Text = server.plugin; + PluginOptionsTextBox.Text = server.plugin_opts; RemarksTextBox.Text = server.remarks; TimeoutTextBox.Text = server.timeout.ToString(); } diff --git a/shadowsocks-csharp/shadowsocks-csharp.csproj b/shadowsocks-csharp/shadowsocks-csharp.csproj index 7d49b650..bf03af1f 100644 --- a/shadowsocks-csharp/shadowsocks-csharp.csproj +++ b/shadowsocks-csharp/shadowsocks-csharp.csproj @@ -145,6 +145,7 @@ + diff --git a/test/UnitTest.cs b/test/UnitTest.cs index 350d71e7..8894a17d 100755 --- a/test/UnitTest.cs +++ b/test/UnitTest.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Collections.Generic; using Shadowsocks.Controller.Hotkeys; using Shadowsocks.Encryption.Stream; +using Shadowsocks.Model; namespace test { @@ -224,5 +225,136 @@ namespace test throw; } } + + [TestMethod] + public void ParseAndGenerateShadowsocksUrl() + { + var server = new Server + { + server = "192.168.100.1", + server_port = 8888, + password = "test", + method = "bf-cfb" + }; + var serverCanonUrl = "ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4"; + + var serverWithRemark = new Server + { + server = server.server, + server_port = server.server_port, + password = server.password, + method = server.method, + remarks = "example-server" + }; + var serverWithRemarkCanonUrl = "ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4#example-server"; + + var serverWithPlugin = new Server + { + server = server.server, + server_port = server.server_port, + password = server.password, + method = server.method, + plugin = "obfs-local", + plugin_opts = "obfs=http;obfs-host=google.com" + }; + var serverWithPluginCanonUrl = + "ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888/?plugin=obfs-local%3bobfs%3dhttp%3bobfs-host%3dgoogle.com"; + + var serverWithPluginAndRemark = new Server + { + server = server.server, + server_port = server.server_port, + password = server.password, + method = server.method, + plugin = serverWithPlugin.plugin, + plugin_opts = serverWithPlugin.plugin_opts, + remarks = serverWithRemark.remarks + }; + var serverWithPluginAndRemarkCanonUrl = + "ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888/?plugin=obfs-local%3bobfs%3dhttp%3bobfs-host%3dgoogle.com#example-server"; + + RunParseShadowsocksUrlTest( + string.Join( + "\r\n", + serverCanonUrl, + "\r\n", + "ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4/", + serverWithRemarkCanonUrl, + "ss://YmYtY2ZiOnRlc3RAMTkyLjE2OC4xMDAuMTo4ODg4/#example-server"), + new[] + { + server, + server, + serverWithRemark, + serverWithRemark + }); + + RunParseShadowsocksUrlTest( + string.Join( + "\r\n", + "ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888", + "\r\n", + "ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888/", + "ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888#example-server", + "ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888/#example-server", + serverWithPluginCanonUrl, + serverWithPluginAndRemarkCanonUrl, + "ss://YmYtY2ZiOnRlc3Q@192.168.100.1:8888/?plugin=obfs-local%3bobfs%3dhttp%3bobfs-host%3dgoogle.com&unsupported=1#example-server"), + new[] + { + server, + server, + serverWithRemark, + serverWithRemark, + serverWithPlugin, + serverWithPluginAndRemark, + serverWithPluginAndRemark + }); + + var generateUrlCases = new Dictionary + { + [serverCanonUrl] = server, + [serverWithRemarkCanonUrl] = serverWithRemark, + [serverWithPluginCanonUrl] = serverWithPlugin, + [serverWithPluginAndRemarkCanonUrl] = serverWithPluginAndRemark + }; + RunGenerateShadowsocksUrlTest(generateUrlCases); + } + + private static void RunParseShadowsocksUrlTest(string testCase, IReadOnlyList expected) + { + var actual = Server.GetServers(testCase); + if (actual.Count != expected.Count) + { + Assert.Fail("Wrong number of configs. Expected: {0}. Actual: {1}", expected.Count, actual.Count); + } + + for (int i = 0; i < expected.Count; i++) + { + var expectedServer = expected[i]; + var actualServer = actual[i]; + + Assert.AreEqual(expectedServer.server, actualServer.server); + Assert.AreEqual(expectedServer.server_port, actualServer.server_port); + Assert.AreEqual(expectedServer.password, actualServer.password); + Assert.AreEqual(expectedServer.method, actualServer.method); + Assert.AreEqual(expectedServer.plugin, actualServer.plugin); + Assert.AreEqual(expectedServer.plugin_opts, actualServer.plugin_opts); + Assert.AreEqual(expectedServer.remarks, actualServer.remarks); + Assert.AreEqual(expectedServer.timeout, actualServer.timeout); + } + } + + private static void RunGenerateShadowsocksUrlTest(IReadOnlyDictionary testCases) + { + foreach (var testCase in testCases) + { + string expected = testCase.Key; + Server config = testCase.Value; + + var actual = ShadowsocksController.GetQRCode(config); + Assert.AreEqual(expected, actual); + } + } } }