Release v0.1.2
This commit is contained in:
+142
-17
@@ -1,7 +1,8 @@
|
||||
// SbLauncher — GUI для sing-box.
|
||||
//
|
||||
// ПРИНЦИП (изменён по сравнению с прошлой версией):
|
||||
// Источник правды — profiles.json (серверы, общее) и local.json (ключ,
|
||||
// Источник правды — profiles.base.json (базовые серверы), profiles.json
|
||||
// (личные серверы) и local.json (ключ,
|
||||
// адрес, адаптер — своё на каждой машине). Конфиг sing-box НЕ хранится:
|
||||
// он собирается из шаблона templates\*.json при каждом запуске и кладётся
|
||||
// в run\config.json. Поэтому папок conf_* больше нет, и править один и
|
||||
@@ -187,6 +188,7 @@ namespace SbLauncher
|
||||
public Paths(string root) { Root = root; }
|
||||
|
||||
public string Profiles { get { return Path.Combine(Root, "profiles.json"); } }
|
||||
public string BaseProfiles { get { return Path.Combine(Root, "profiles.base.json"); } }
|
||||
public string Local { get { return Path.Combine(Root, "local.json"); } }
|
||||
public string TemplatesDir { get { return Path.Combine(Root, "templates"); } }
|
||||
public string SbExe { get { return Path.Combine(Root, Path.Combine("bin", "sb.exe")); } }
|
||||
@@ -217,6 +219,7 @@ namespace SbLauncher
|
||||
public string SpiderX = ""; // старые профили сохраняют это поле
|
||||
public string Protocol = "vless";
|
||||
public Dictionary<string, object> Outbound; // точная конфигурация импортированного ключа
|
||||
public string BaseId = ""; // стабильный id только у серверов из релиза
|
||||
|
||||
public string Endpoint
|
||||
{
|
||||
@@ -232,19 +235,115 @@ namespace SbLauncher
|
||||
public int Mtu = 1280;
|
||||
}
|
||||
|
||||
// profiles.json — локальная копия серверов; в тестовом репозитории не коммитится
|
||||
// Релиз обновляет profiles.base.json. profiles.json хранит только личные
|
||||
// серверы и скрытые базовые id; local.json независимо хранит ключ и адрес.
|
||||
public class Profiles
|
||||
{
|
||||
public WgCfg Wg = new WgCfg();
|
||||
public List<ServerCfg> Servers = new List<ServerCfg>();
|
||||
public HashSet<string> HiddenBaseServers = new HashSet<string>(StringComparer.Ordinal);
|
||||
public bool Layered;
|
||||
public bool NeedsMigration;
|
||||
bool _wgOverride;
|
||||
|
||||
public static Profiles Load(string path)
|
||||
{
|
||||
Profiles personal = ReadFile(path, false);
|
||||
string basePath = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(path)), "profiles.base.json");
|
||||
if (!string.Equals(Path.GetFileName(path), "profiles.json", StringComparison.OrdinalIgnoreCase) ||
|
||||
!File.Exists(basePath)) return personal;
|
||||
|
||||
Profiles baseline = ReadFile(basePath, true);
|
||||
Profiles merged = new Profiles();
|
||||
merged.Layered = true;
|
||||
merged.NeedsMigration = File.Exists(path) && !personal.Layered;
|
||||
merged.HiddenBaseServers.UnionWith(personal.HiddenBaseServers);
|
||||
merged._wgOverride = personal._wgOverride && (personal.Layered || !SameWg(personal.Wg, baseline.Wg));
|
||||
merged.Wg = merged._wgOverride ? personal.Wg : baseline.Wg;
|
||||
|
||||
// Старый общий список превращается в личный слой без дубликатов.
|
||||
// Непохожие на базовые записи сохраняем, включая старые настройки.
|
||||
List<ServerCfg> own = new List<ServerCfg>();
|
||||
foreach (ServerCfg server in personal.Servers)
|
||||
{
|
||||
bool sharedLegacy = false;
|
||||
if (!personal.Layered)
|
||||
foreach (ServerCfg shared in baseline.Servers)
|
||||
if (SameProfile(shared, server)) { sharedLegacy = true; break; }
|
||||
if (!sharedLegacy) own.Add(server);
|
||||
}
|
||||
// Личные коды не меняем при появлении базового сервера с тем же кодом.
|
||||
List<ServerCfg> reserved = new List<ServerCfg>(own);
|
||||
foreach (ServerCfg shared in baseline.Servers)
|
||||
{
|
||||
if (merged.HiddenBaseServers.Contains(shared.BaseId)) continue;
|
||||
GiveUniqueCode(shared, reserved);
|
||||
reserved.Add(shared);
|
||||
merged.Servers.Add(shared);
|
||||
}
|
||||
merged.Servers.AddRange(own);
|
||||
return merged;
|
||||
}
|
||||
|
||||
static bool SameWg(WgCfg a, WgCfg b)
|
||||
{
|
||||
return a.Server == b.Server && a.Port == b.Port &&
|
||||
a.PeerPublicKey == b.PeerPublicKey && a.Mtu == b.Mtu;
|
||||
}
|
||||
|
||||
static bool SameProfile(ServerCfg a, ServerCfg b)
|
||||
{
|
||||
// Compare complete connection settings: a customized SNI/transport
|
||||
// with the same endpoint and UUID must remain a personal profile.
|
||||
return KeyImporter.Same(
|
||||
new ServerCfg { Protocol = a.Protocol, Server = a.Server, Port = a.Port,
|
||||
Outbound = ProxyOutbound.Build(a, ConfigBuilder.ModeDirect) },
|
||||
new ServerCfg { Protocol = b.Protocol, Server = b.Server, Port = b.Port,
|
||||
Outbound = ProxyOutbound.Build(b, ConfigBuilder.ModeDirect) });
|
||||
}
|
||||
|
||||
public static void GiveUniqueCode(ServerCfg server, List<ServerCfg> all)
|
||||
{
|
||||
string original = server.Code;
|
||||
int number = 2;
|
||||
while (all.Exists(delegate(ServerCfg existing) {
|
||||
return string.Equals(existing.Code, server.Code, StringComparison.OrdinalIgnoreCase);
|
||||
})) server.Code = original + (number++).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public Profiles Copy()
|
||||
{
|
||||
Profiles next = new Profiles();
|
||||
next.Wg = Wg;
|
||||
next.Servers.AddRange(Servers);
|
||||
next.HiddenBaseServers.UnionWith(HiddenBaseServers);
|
||||
next.Layered = Layered;
|
||||
next.NeedsMigration = NeedsMigration;
|
||||
next._wgOverride = _wgOverride;
|
||||
return next;
|
||||
}
|
||||
|
||||
public void Remove(ServerCfg server)
|
||||
{
|
||||
Servers.Remove(server);
|
||||
if (server.BaseId.Length > 0) HiddenBaseServers.Add(server.BaseId);
|
||||
}
|
||||
|
||||
static Profiles ReadFile(string path, bool isBase)
|
||||
{
|
||||
Profiles p = new Profiles();
|
||||
if (!File.Exists(path)) return p;
|
||||
|
||||
Dictionary<string, object> root = J.Parse(File.ReadAllText(path, Encoding.UTF8));
|
||||
if (root == null) return p;
|
||||
p.Layered = J.Int(root, "schema_version", 0) >= 2;
|
||||
object hidden;
|
||||
if (root.TryGetValue("hidden_base_servers", out hidden))
|
||||
{
|
||||
object[] ids = hidden as object[];
|
||||
if (ids != null) foreach (object id in ids)
|
||||
if (id is string) p.HiddenBaseServers.Add((string)id);
|
||||
}
|
||||
|
||||
object wo;
|
||||
if (root.TryGetValue("wireguard", out wo))
|
||||
@@ -252,6 +351,7 @@ namespace SbLauncher
|
||||
Dictionary<string, object> w = J.Obj(wo);
|
||||
if (w != null)
|
||||
{
|
||||
p._wgOverride = true;
|
||||
p.Wg.Server = J.Str(w, "server", "");
|
||||
p.Wg.Port = J.Int(w, "port", 51820);
|
||||
p.Wg.PeerPublicKey = J.Str(w, "peer_public_key", "");
|
||||
@@ -276,6 +376,12 @@ namespace SbLauncher
|
||||
s.Flow = J.Str(d, "flow", "xtls-rprx-vision");
|
||||
s.SpiderX = J.Str(d, "spider_x", "");
|
||||
s.Protocol = J.Str(d, "protocol", "vless");
|
||||
if (isBase)
|
||||
{
|
||||
s.BaseId = J.Str(d, "id", s.Code);
|
||||
if (s.BaseId.Length == 0 || p.Servers.Exists(delegate(ServerCfg existing) { return existing.BaseId == s.BaseId; }))
|
||||
throw new FormatException("У базовых серверов должны быть уникальные непустые id.");
|
||||
}
|
||||
object outbound;
|
||||
if (d.TryGetValue("outbound", out outbound)) s.Outbound = J.Obj(outbound);
|
||||
if (s.Outbound != null) s.Protocol = J.Str(s.Outbound, "type", s.Protocol);
|
||||
@@ -284,25 +390,43 @@ namespace SbLauncher
|
||||
return p;
|
||||
}
|
||||
|
||||
// Пишется только когда сервер добавили/удалили из GUI. Файл
|
||||
// Пишется при миграции или добавлении/удалении сервера из GUI. Файл
|
||||
// пересобирается целиком, поэтому свои комментарии ВНУТРИ списка
|
||||
// серверов при этом теряются — шапка восстанавливается.
|
||||
public void Save(string path)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine("// Серверы и общие параметры этого тестового экземпляра.");
|
||||
sb.AppendLine("// Храните данные серверов локально, а ключ WireGuard — в local.json.");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" \"wireguard\": {");
|
||||
sb.AppendLine(" \"server\": \"" + J.Esc(Wg.Server) + "\",");
|
||||
sb.AppendLine(" \"port\": " + Wg.Port.ToString(CultureInfo.InvariantCulture) + ",");
|
||||
sb.AppendLine(" \"peer_public_key\": \"" + J.Esc(Wg.PeerPublicKey) + "\",");
|
||||
sb.AppendLine(" \"mtu\": " + Wg.Mtu.ToString(CultureInfo.InvariantCulture));
|
||||
sb.AppendLine(" },");
|
||||
sb.AppendLine(" \"servers\": [");
|
||||
for (int i = 0; i < Servers.Count; i++)
|
||||
if (NeedsMigration && File.Exists(path))
|
||||
{
|
||||
ServerCfg s = Servers[i];
|
||||
string backup = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(path)), "profiles.legacy-backup.json");
|
||||
if (!File.Exists(backup)) File.Copy(path, backup);
|
||||
}
|
||||
List<ServerCfg> stored = Layered
|
||||
? Servers.FindAll(delegate(ServerCfg server) { return server.BaseId.Length == 0; })
|
||||
: Servers;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine("// Личные серверы и настройки списка. Не включайте этот файл в релиз.");
|
||||
sb.AppendLine("// Базовые серверы — profiles.base.json; ключ и адрес — local.json.");
|
||||
sb.AppendLine("{");
|
||||
if (Layered)
|
||||
{
|
||||
sb.AppendLine(" \"schema_version\": 2,");
|
||||
List<string> hidden = new List<string>(HiddenBaseServers);
|
||||
hidden.Sort(StringComparer.Ordinal);
|
||||
sb.AppendLine(" \"hidden_base_servers\": " + J.ToJson(hidden.ToArray()) + ",");
|
||||
}
|
||||
if (!Layered || _wgOverride)
|
||||
{
|
||||
sb.AppendLine(" \"wireguard\": {");
|
||||
sb.AppendLine(" \"server\": \"" + J.Esc(Wg.Server) + "\",");
|
||||
sb.AppendLine(" \"port\": " + Wg.Port.ToString(CultureInfo.InvariantCulture) + ",");
|
||||
sb.AppendLine(" \"peer_public_key\": \"" + J.Esc(Wg.PeerPublicKey) + "\",");
|
||||
sb.AppendLine(" \"mtu\": " + Wg.Mtu.ToString(CultureInfo.InvariantCulture));
|
||||
sb.AppendLine(" },");
|
||||
}
|
||||
sb.AppendLine(" \"servers\": [");
|
||||
for (int i = 0; i < stored.Count; i++)
|
||||
{
|
||||
ServerCfg s = stored[i];
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" \"code\": \"" + J.Esc(s.Code) + "\",");
|
||||
sb.AppendLine(" \"name\": \"" + J.Esc(s.Name) + "\",");
|
||||
@@ -322,11 +446,12 @@ namespace SbLauncher
|
||||
sb.AppendLine(" \"protocol\": \"" + J.Esc(s.Protocol) + "\"" + (s.Outbound != null ? "," : ""));
|
||||
if (s.Outbound != null)
|
||||
sb.AppendLine(" \"outbound\": " + J.ToJson(s.Outbound));
|
||||
sb.AppendLine(i == Servers.Count - 1 ? " }" : " },");
|
||||
sb.AppendLine(i == stored.Count - 1 ? " }" : " },");
|
||||
}
|
||||
sb.AppendLine(" ]");
|
||||
sb.AppendLine("}");
|
||||
J.Write(path, sb.ToString());
|
||||
NeedsMigration = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user