You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

ConfigConverter.cs 8.7 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. using Shadowsocks.Interop.Utils;
  2. using Shadowsocks.Models;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Net.Http;
  8. using System.Net.Http.Json;
  9. using System.Text.Json;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. namespace Shadowsocks.CLI
  13. {
  14. public class ConfigConverter
  15. {
  16. /// <summary>
  17. /// Gets or sets whether to prefix group name to server names.
  18. /// </summary>
  19. public bool PrefixGroupName { get; set; }
  20. /// <summary>
  21. /// Gets or sets the list of servers that are not in any groups.
  22. /// </summary>
  23. public List<Server> Servers { get; set; } = new();
  24. public ConfigConverter(bool prefixGroupName = false) => PrefixGroupName = prefixGroupName;
  25. /// <summary>
  26. /// Collects servers from ss:// links or SIP008 delivery links.
  27. /// </summary>
  28. /// <param name="uris">URLs to collect servers from.</param>
  29. /// <param name="cancellationToken">A token that may be used to cancel the asynchronous operation.</param>
  30. /// <returns>A task that represents the asynchronous operation.</returns>
  31. public async Task FromUrls(IEnumerable<Uri> uris, CancellationToken cancellationToken = default)
  32. {
  33. var sip008Links = new List<Uri>();
  34. foreach (var uri in uris)
  35. {
  36. switch (uri.Scheme)
  37. {
  38. case "ss":
  39. {
  40. if (Server.TryParse(uri, out var server))
  41. Servers.Add(server);
  42. break;
  43. }
  44. case "https":
  45. sip008Links.Add(uri);
  46. break;
  47. }
  48. }
  49. if (sip008Links.Count > 0)
  50. {
  51. var httpClient = new HttpClient();
  52. httpClient.Timeout = TimeSpan.FromSeconds(30.0);
  53. var tasks = sip008Links.Select(async x => await httpClient.GetFromJsonAsync<Group>(x, JsonHelper.snakeCaseJsonDeserializerOptions, cancellationToken))
  54. .ToList();
  55. while (tasks.Count > 0)
  56. {
  57. var finishedTask = await Task.WhenAny(tasks);
  58. var group = await finishedTask;
  59. if (group != null)
  60. Servers.AddRange(group.Servers);
  61. tasks.Remove(finishedTask);
  62. }
  63. }
  64. }
  65. /// <summary>
  66. /// Collects servers from SIP008 JSON files.
  67. /// </summary>
  68. /// <param name="paths">JSON file paths.</param>
  69. /// <param name="cancellationToken">A token that may be used to cancel the read operation.</param>
  70. /// <returns>A task that represents the asynchronous read operation.</returns>
  71. public async Task FromSip008Json(IEnumerable<string> paths, CancellationToken cancellationToken = default)
  72. {
  73. foreach (var path in paths)
  74. {
  75. using var jsonFile = new FileStream(path, FileMode.Open);
  76. var group = await JsonSerializer.DeserializeAsync<Group>(jsonFile, JsonHelper.snakeCaseJsonDeserializerOptions, cancellationToken);
  77. if (group != null)
  78. {
  79. if (PrefixGroupName && !string.IsNullOrEmpty(group.Name))
  80. group.Servers.ForEach(x => x.Name = $"{group.Name} - {x.Name}");
  81. Servers.AddRange(group.Servers);
  82. }
  83. }
  84. }
  85. /// <summary>
  86. /// Collects servers from outbounds in V2Ray JSON files.
  87. /// </summary>
  88. /// <param name="paths">JSON file paths.</param>
  89. /// <param name="cancellationToken">A token that may be used to cancel the read operation.</param>
  90. /// <returns>A task that represents the asynchronous read operation.</returns>
  91. public async Task FromV2rayJson(IEnumerable<string> paths, CancellationToken cancellationToken = default)
  92. {
  93. foreach (var path in paths)
  94. {
  95. using var jsonFile = new FileStream(path, FileMode.Open);
  96. var v2rayConfig = await JsonSerializer.DeserializeAsync<Interop.V2Ray.Config>(jsonFile, JsonHelper.camelCaseJsonDeserializerOptions, cancellationToken);
  97. if (v2rayConfig?.Outbounds != null)
  98. {
  99. foreach (var outbound in v2rayConfig.Outbounds)
  100. {
  101. if (outbound.Protocol == "shadowsocks"
  102. && outbound.Settings is Interop.V2Ray.Protocols.Shadowsocks.OutboundConfigurationObject ssConfig)
  103. {
  104. foreach (var ssServer in ssConfig.Servers)
  105. {
  106. var server = new Server();
  107. server.Name = outbound.Tag;
  108. server.Host = ssServer.Address;
  109. server.Port = ssServer.Port;
  110. server.Method = ssServer.Method;
  111. server.Password = ssServer.Password;
  112. Servers.Add(server);
  113. }
  114. }
  115. }
  116. }
  117. }
  118. }
  119. /// <summary>
  120. /// Converts saved servers to ss:// URLs.
  121. /// </summary>
  122. /// <returns>A list of ss:// URLs.</returns>
  123. public List<Uri> ToUrls()
  124. {
  125. var urls = new List<Uri>();
  126. foreach (var server in Servers)
  127. urls.Add(server.ToUrl());
  128. return urls;
  129. }
  130. /// <summary>
  131. /// Converts saved servers to SIP008 JSON.
  132. /// </summary>
  133. /// <param name="path">JSON file path.</param>
  134. /// <param name="cancellationToken">A token that may be used to cancel the write operation.</param>
  135. /// <returns>A task that represents the asynchronous write operation.</returns>
  136. public Task ToSip008Json(string path, CancellationToken cancellationToken = default)
  137. {
  138. var group = new Group();
  139. group.Servers.AddRange(Servers);
  140. var fullPath = Path.GetFullPath(path);
  141. var directoryPath = Path.GetDirectoryName(fullPath) ?? throw new ArgumentException("Invalid path", nameof(path));
  142. Directory.CreateDirectory(directoryPath);
  143. using var jsonFile = new FileStream(fullPath, FileMode.Create);
  144. return JsonSerializer.SerializeAsync(jsonFile, group, JsonHelper.snakeCaseJsonSerializerOptions, cancellationToken);
  145. }
  146. /// <summary>
  147. /// Converts saved servers to V2Ray outbounds.
  148. /// </summary>
  149. /// <param name="path">JSON file path.</param>
  150. /// <param name="prefixGroupName">Whether to prefix group name to server names.</param>
  151. /// <param name="cancellationToken">A token that may be used to cancel the write operation.</param>
  152. /// <returns>A task that represents the asynchronous write operation.</returns>
  153. public Task ToV2rayJson(string path, CancellationToken cancellationToken = default)
  154. {
  155. var v2rayConfig = new Interop.V2Ray.Config();
  156. v2rayConfig.Outbounds = new();
  157. foreach (var server in Servers)
  158. {
  159. var ssOutbound = Interop.V2Ray.OutboundObject.GetShadowsocks(server);
  160. v2rayConfig.Outbounds.Add(ssOutbound);
  161. }
  162. // enforce outbound tag uniqueness
  163. var serversWithDuplicateTags = v2rayConfig.Outbounds.GroupBy(x => x.Tag)
  164. .Where(x => x.Count() > 1);
  165. foreach (var serversWithSameTag in serversWithDuplicateTags)
  166. {
  167. var duplicates = serversWithSameTag.ToList();
  168. for (var i = 0; i < duplicates.Count; i++)
  169. {
  170. duplicates[i].Tag = $"{duplicates[i].Tag} {i}";
  171. }
  172. }
  173. var fullPath = Path.GetFullPath(path);
  174. var directoryPath = Path.GetDirectoryName(fullPath) ?? throw new ArgumentException("Invalid path", nameof(path));
  175. Directory.CreateDirectory(directoryPath);
  176. using var jsonFile = new FileStream(fullPath, FileMode.Create);
  177. return JsonSerializer.SerializeAsync(jsonFile, v2rayConfig, JsonHelper.camelCaseJsonSerializerOptions, cancellationToken);
  178. }
  179. }
  180. }