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);
}
}
}