using Shadowsocks.Interop.Utils; using Shadowsocks.Models; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Shadowsocks.CLI { public class ConfigConverter { /// /// Gets or sets whether to prefix group name to server names. /// public bool PrefixGroupName { get; set; } /// /// Gets or sets the list of servers that are not in any groups. /// public List Servers { get; set; } = new(); public ConfigConverter(bool prefixGroupName = false) => PrefixGroupName = prefixGroupName; /// /// Collects servers from ss:// links or SIP008 delivery links. /// /// URLs to collect servers from. /// A token that may be used to cancel the asynchronous operation. /// A task that represents the asynchronous operation. public async Task FromUrls(IEnumerable uris, CancellationToken cancellationToken = default) { var sip008Links = new List(); foreach (var uri in uris) { switch (uri.Scheme) { case "ss": { if (Server.TryParse(uri, out var server)) Servers.Add(server); break; } case "https": sip008Links.Add(uri); break; } } if (sip008Links.Count > 0) { var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30.0) }; var tasks = sip008Links.Select(async x => await httpClient.GetFromJsonAsync(x, JsonHelper.snakeCaseJsonDeserializerOptions, cancellationToken)) .ToList(); while (tasks.Count > 0) { var finishedTask = await Task.WhenAny(tasks); var group = await finishedTask; if (group != null) Servers.AddRange(group.Servers); tasks.Remove(finishedTask); } } } /// /// Collects servers from SIP008 JSON files. /// /// JSON file paths. /// A token that may be used to cancel the read operation. /// A task that represents the asynchronous read operation. public async Task FromSip008Json(IEnumerable paths, CancellationToken cancellationToken = default) { foreach (var path in paths) { using var jsonFile = new FileStream(path, FileMode.Open); var group = await JsonSerializer.DeserializeAsync(jsonFile, JsonHelper.snakeCaseJsonDeserializerOptions, cancellationToken); if (group != null) { if (PrefixGroupName && !string.IsNullOrEmpty(group.Name)) group.Servers.ForEach(x => x.Name = $"{group.Name} - {x.Name}"); Servers.AddRange(group.Servers); } } } /// /// Collects servers from outbounds in V2Ray JSON files. /// /// JSON file paths. /// A token that may be used to cancel the read operation. /// A task that represents the asynchronous read operation. public async Task FromV2rayJson(IEnumerable paths, CancellationToken cancellationToken = default) { foreach (var path in paths) { using var jsonFile = new FileStream(path, FileMode.Open); var v2rayConfig = await JsonSerializer.DeserializeAsync(jsonFile, JsonHelper.camelCaseJsonDeserializerOptions, cancellationToken); if (v2rayConfig?.Outbounds != null) { foreach (var outbound in v2rayConfig.Outbounds) { if (outbound.Protocol == "shadowsocks" && outbound.Settings is JsonElement jsonElement) { var jsonText = jsonElement.GetRawText(); var ssConfig = JsonSerializer.Deserialize(jsonText, JsonHelper.camelCaseJsonDeserializerOptions); if (ssConfig != null) foreach (var ssServer in ssConfig.Servers) { var server = new Server { Name = outbound.Tag, Host = ssServer.Address, Port = ssServer.Port, Method = ssServer.Method, Password = ssServer.Password }; Servers.Add(server); } } } } } } /// /// Converts saved servers to ss:// URLs. /// /// A list of ss:// URLs. public List ToUrls() { var urls = new List(); foreach (var server in Servers) urls.Add(server.ToUrl()); return urls; } /// /// Converts saved servers to SIP008 JSON. /// /// JSON file path. /// A token that may be used to cancel the write operation. /// A task that represents the asynchronous write operation. public Task ToSip008Json(string path, CancellationToken cancellationToken = default) { var group = new Group(); group.Servers.AddRange(Servers); var fullPath = Path.GetFullPath(path); var directoryPath = Path.GetDirectoryName(fullPath) ?? throw new ArgumentException("Invalid path", nameof(path)); Directory.CreateDirectory(directoryPath); using var jsonFile = new FileStream(fullPath, FileMode.Create); return JsonSerializer.SerializeAsync(jsonFile, group, JsonHelper.snakeCaseJsonSerializerOptions, cancellationToken); } /// /// Converts saved servers to V2Ray outbounds. /// /// JSON file path. /// Whether to prefix group name to server names. /// A token that may be used to cancel the write operation. /// A task that represents the asynchronous write operation. public Task ToV2rayJson(string path, CancellationToken cancellationToken = default) { var v2rayConfig = new Interop.V2Ray.Config { Outbounds = new() }; foreach (var server in Servers) { var ssOutbound = Interop.V2Ray.OutboundObject.GetShadowsocks(server); v2rayConfig.Outbounds.Add(ssOutbound); } // enforce outbound tag uniqueness var serversWithDuplicateTags = v2rayConfig.Outbounds.GroupBy(x => x.Tag) .Where(x => x.Count() > 1); foreach (var serversWithSameTag in serversWithDuplicateTags) { var duplicates = serversWithSameTag.ToList(); for (var i = 0; i < duplicates.Count; i++) { duplicates[i].Tag = $"{duplicates[i].Tag} {i}"; } } var fullPath = Path.GetFullPath(path); var directoryPath = Path.GetDirectoryName(fullPath) ?? throw new ArgumentException("Invalid path", nameof(path)); Directory.CreateDirectory(directoryPath); using var jsonFile = new FileStream(fullPath, FileMode.Create); return JsonSerializer.SerializeAsync(jsonFile, v2rayConfig, JsonHelper.camelCaseJsonSerializerOptions, cancellationToken); } } }