using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Text; using System.Web; using Shadowsocks.Controller; namespace Shadowsocks.Model { [Serializable] public class Server { private const int DefaultServerTimeoutSec = 5; public const int MaxServerTimeoutSec = 20; public string server; public int server_port; public string password; public string method; public string plugin; public string plugin_opts; public string remarks; public int timeout; public override int GetHashCode() { return server.GetHashCode() ^ server_port; } public override bool Equals(object obj) { Server o2 = (Server)obj; return server == o2.server && server_port == o2.server_port; } public string FriendlyName() { if (server.IsNullOrEmpty()) { return I18N.GetString("New server"); } string serverStr; // CheckHostName() won't do a real DNS lookup var hostType = Uri.CheckHostName(server); switch (hostType) { case UriHostNameType.IPv6: serverStr = $"[{server}]:{server_port}"; break; default: // IPv4 and domain name serverStr = $"{server}:{server_port}"; break; } return remarks.IsNullOrEmpty() ? serverStr : $"{remarks} ({serverStr})"; } public Server() { server = ""; server_port = 8388; method = "aes-256-cfb"; plugin = ""; plugin_opts = ""; password = ""; remarks = ""; timeout = DefaultServerTimeoutSec; } public static List GetServers(string ssURL) { var serverUrls = ssURL.Split('\r', '\n'); List servers = new List(); foreach (string serverUrl in serverUrls) { 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) { 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; } 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 = 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); } return servers; } public string Identifier() { return server + ':' + server_port; } } }