using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Shadowsocks.Models { public class Server : IServer { /// [JsonPropertyName("server")] public string Host { get; set; } /// [JsonPropertyName("server_port")] public int Port { get; set; } /// public string Password { get; set; } /// public string Method { get; set; } /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? Plugin { get; set; } /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string? PluginOpts { get; set; } /// /// Gets or sets the arguments passed to the plugin process. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public List? PluginArgs { get; set; } /// [JsonPropertyName("remarks")] public string Name { get; set; } /// [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; /// /// Converts this server object into an ss:// URL. /// /// 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; } /// /// Tries to parse an ss:// URL into a Server object. /// /// The ss:// URL to parse. /// /// A Server object represented by the URL. /// A new empty Server object if the URL is invalid. /// /// True for success. False for failure. 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); } /// /// Tries to parse an ss:// URL into a Server object. /// /// The ss:// URL to parse. /// /// A Server object represented by the URL. /// A new empty Server object if the URL is invalid. /// /// True for success. False for failure. 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; } } } }