mirror of
https://github.com/2dust/v2rayN.git
synced 2026-05-30 01:34:08 +00:00
Inner uri import and export
This commit is contained in:
parent
d13f7a4db6
commit
737da7d40c
16 changed files with 343 additions and 0 deletions
|
|
@ -64,6 +64,7 @@ public class Global
|
|||
public const string HttpsProtocol = "https://";
|
||||
public const string SocksProtocol = "socks://";
|
||||
public const string Socks5Protocol = "socks5://";
|
||||
public const string InnerUriProtocol = "v2rayn://";
|
||||
public const string AsIs = "AsIs";
|
||||
public const string IPIfNonMatch = "IPIfNonMatch";
|
||||
public const string IPOnDemand = "IPOnDemand";
|
||||
|
|
|
|||
|
|
@ -1767,6 +1767,53 @@ public static class ConfigHandler
|
|||
return -1;
|
||||
}
|
||||
|
||||
private static async Task<int> AddBatchServers4InnerUri(Config config, string strData, string subid, bool isSub)
|
||||
{
|
||||
if (strData.IsNullOrEmpty())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (isSub && subid.IsNotEmpty())
|
||||
{
|
||||
await RemoveServersViaSubid(config, subid, isSub);
|
||||
}
|
||||
|
||||
var lstServer = InnerFmt.Resolve(strData, subid);
|
||||
if (lstServer?.Count > 0)
|
||||
{
|
||||
var counter = 0;
|
||||
foreach (var profileItem in lstServer)
|
||||
{
|
||||
profileItem.Subid = subid;
|
||||
profileItem.IsSub = isSub;
|
||||
|
||||
var addStatus = profileItem.ConfigType switch
|
||||
{
|
||||
EConfigType.VMess => await AddVMessServer(config, profileItem),
|
||||
EConfigType.Shadowsocks => await AddShadowsocksServer(config, profileItem),
|
||||
EConfigType.SOCKS => await AddSocksServer(config, profileItem),
|
||||
EConfigType.Trojan => await AddTrojanServer(config, profileItem),
|
||||
EConfigType.VLESS => await AddVlessServer(config, profileItem),
|
||||
EConfigType.Hysteria2 => await AddHysteria2Server(config, profileItem),
|
||||
EConfigType.TUIC => await AddTuicServer(config, profileItem),
|
||||
EConfigType.WireGuard => await AddWireguardServer(config, profileItem),
|
||||
EConfigType.Anytls => await AddAnytlsServer(config, profileItem),
|
||||
EConfigType.Naive => await AddNaiveServer(config, profileItem),
|
||||
_ => -1,
|
||||
};
|
||||
if (addStatus == 0)
|
||||
{
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
await SaveConfig(config);
|
||||
return counter;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main entry point for adding batch servers from various formats
|
||||
/// Tries different parsing methods to import as many servers as possible
|
||||
|
|
@ -1815,6 +1862,11 @@ public static class ConfigHandler
|
|||
counter = await AddBatchServers4Wireguard(config, strData, subid, isSub);
|
||||
}
|
||||
|
||||
if (counter < 1)
|
||||
{
|
||||
counter = await AddBatchServers4InnerUri(config, strData, subid, isSub);
|
||||
}
|
||||
|
||||
//maybe other sub
|
||||
if (counter < 1)
|
||||
{
|
||||
|
|
|
|||
222
v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs
Normal file
222
v2rayN/ServiceLib/Handler/Fmt/InnerFmt.cs
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
namespace ServiceLib.Handler.Fmt;
|
||||
|
||||
public class InnerFmt
|
||||
{
|
||||
private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false));
|
||||
|
||||
public static List<ProfileItem>? Resolve(string strData, string subid)
|
||||
{
|
||||
var list = new List<ProfileItem>();
|
||||
// Overwrite externally imported indexIds to avoid possible sources of attacks
|
||||
var indexIdMap = new Dictionary<string, string>();
|
||||
using (var reader = new StringReader(strData))
|
||||
{
|
||||
while (reader.ReadLine() is { } line)
|
||||
{
|
||||
if (line.IsNullOrEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var trimmedLine = line.Trim();
|
||||
if (!line.StartsWith(Global.InnerUriProtocol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var profileItem = ResolveSingle(trimmedLine);
|
||||
if (profileItem is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (profileItem.ConfigType == EConfigType.Custom)
|
||||
{
|
||||
// Unsupported, also to avoid possible sources of attacks, skip it
|
||||
continue;
|
||||
}
|
||||
if (profileItem.ConfigVersion != 4)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// overwrite indexId
|
||||
var newIndexId = Utils.GetGuid(false);
|
||||
if (!profileItem.IndexId.IsNullOrEmpty())
|
||||
{
|
||||
// Ignore duplicated indexId
|
||||
indexIdMap[profileItem.IndexId] = newIndexId;
|
||||
}
|
||||
profileItem.IndexId = newIndexId;
|
||||
list.Add(profileItem);
|
||||
}
|
||||
}
|
||||
// For group-type profile items, also overwrite the ChildItems and ChildSubId
|
||||
foreach (var item in list.Where(i => i.ConfigType.IsGroupType()))
|
||||
{
|
||||
var protocolExtra = item.GetProtocolExtra();
|
||||
// Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks,
|
||||
// which means it will be replaced with the subid, otherwise set it to null
|
||||
//if (!protocolExtra.SubChildItems.IsNullOrEmpty())
|
||||
if (protocolExtra.SubChildItems == "self")
|
||||
{
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
SubChildItems = subid
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
SubChildItems = null
|
||||
};
|
||||
}
|
||||
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
|
||||
{
|
||||
var newChildIndexIds = childIndexIds
|
||||
.Select(id => indexIdMap.GetValueOrDefault(id, null))
|
||||
.Where(id => !id.IsNullOrEmpty())
|
||||
.ToList();
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
ChildItems = Utils.List2String(newChildIndexIds)
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
ChildItems = null
|
||||
};
|
||||
}
|
||||
item.SetProtocolExtra(protocolExtra);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static string? ToUri(List<ProfileItem> items)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.ConfigType == EConfigType.Custom)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var itemClone = JsonUtils.DeepCopy(item);
|
||||
if (itemClone is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// overwrite indexId
|
||||
var originalIndexId = itemClone.IndexId;
|
||||
var newIndexId = GetReproducibleExportId(originalIndexId);
|
||||
itemClone.IndexId = newIndexId;
|
||||
if (itemClone.ConfigType.IsGroupType())
|
||||
{
|
||||
var protocolExtra = itemClone.GetProtocolExtra();
|
||||
if (!protocolExtra.SubChildItems.IsNullOrEmpty())
|
||||
{
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
SubChildItems = "self"
|
||||
};
|
||||
}
|
||||
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
|
||||
{
|
||||
var newChildIndexIds = childIndexIds
|
||||
.Select(GetReproducibleExportId)
|
||||
.Where(id => !id.IsNullOrEmpty())
|
||||
.ToList();
|
||||
protocolExtra = protocolExtra with
|
||||
{
|
||||
ChildItems = Utils.List2String(newChildIndexIds)
|
||||
};
|
||||
}
|
||||
itemClone.SetProtocolExtra(protocolExtra);
|
||||
}
|
||||
var uri = ToUriSingle(itemClone);
|
||||
if (!uri.IsNullOrEmpty())
|
||||
{
|
||||
sb.AppendLine(uri);
|
||||
}
|
||||
}
|
||||
return sb.Length > 0 ? sb.ToString() : null;
|
||||
}
|
||||
|
||||
private static ProfileItem? ResolveSingle(string str)
|
||||
{
|
||||
// format: v2rayn://vless/{url-safe base64 encoded_string}
|
||||
var parsedUri = Utils.TryUri(str);
|
||||
if (parsedUri is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var segment = parsedUri.AbsolutePath.TrimStart('/');
|
||||
var decodedResult = Utils.Base64Decode(segment);
|
||||
var jsonNode = JsonUtils.ParseJson(decodedResult);
|
||||
if (jsonNode is not JsonObject jsonObj)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// flatten
|
||||
// move jsonObj.ProtoExtraObj to jsonObj.ProtoExtra (string)
|
||||
// move jsonObj.TransportExtraObj to jsonObj.TransportExtra (string)
|
||||
if (jsonObj.TryGetPropertyValue("ProtoExtraObj", out var protoExtraNode)
|
||||
&& protoExtraNode is JsonObject protoExtraObj)
|
||||
{
|
||||
jsonObj["ProtoExtra"] = JsonUtils.Serialize(protoExtraObj, false);
|
||||
jsonObj.Remove("ProtoExtraObj");
|
||||
}
|
||||
if (jsonObj.TryGetPropertyValue("TransportExtraObj", out var transportExtraNode)
|
||||
&& transportExtraNode is JsonObject transportExtraObj)
|
||||
{
|
||||
jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false);
|
||||
jsonObj.Remove("TransportExtraObj");
|
||||
}
|
||||
var profileItem = JsonUtils.Deserialize<ProfileItem>(JsonUtils.Serialize(jsonObj, false));
|
||||
return profileItem;
|
||||
}
|
||||
|
||||
private static string? ToUriSingle(ProfileItem item)
|
||||
{
|
||||
var jsonNode = JsonUtils.ParseJson(JsonUtils.Serialize(item, false));
|
||||
if (jsonNode is not JsonObject jsonObj)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// unflatten
|
||||
// move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj
|
||||
// move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj
|
||||
if (jsonObj.TryGetPropertyValue("ProtoExtra", out var protoExtraNode)
|
||||
&& protoExtraNode is JsonValue protoExtraValue
|
||||
&& protoExtraValue.TryGetValue<string>(out var protoExtraStr)
|
||||
&& !protoExtraStr.IsNullOrEmpty()
|
||||
&& JsonUtils.ParseJson(protoExtraStr) is JsonObject protoExtraObj)
|
||||
{
|
||||
jsonObj["ProtoExtraObj"] = protoExtraObj;
|
||||
jsonObj.Remove("ProtoExtra");
|
||||
}
|
||||
if (jsonObj.TryGetPropertyValue("TransportExtra", out var transportExtraNode)
|
||||
&& transportExtraNode is JsonValue transportExtraValue
|
||||
&& transportExtraValue.TryGetValue<string>(out var transportExtraStr)
|
||||
&& !transportExtraStr.IsNullOrEmpty()
|
||||
&& JsonUtils.ParseJson(transportExtraStr) is JsonObject transportExtraObj)
|
||||
{
|
||||
jsonObj["TransportExtraObj"] = transportExtraObj;
|
||||
jsonObj.Remove("TransportExtra");
|
||||
}
|
||||
var jsonStr = JsonUtils.Serialize(jsonObj, false);
|
||||
var encodedStr = Utils.Base64Encode(jsonStr).Replace('+', '-').Replace('/', '_').Replace("=", "");
|
||||
return $"{Global.InnerUriProtocol}{item.ConfigType.ToString().ToLower()}/{encodedStr}";
|
||||
}
|
||||
|
||||
private static string GetReproducibleExportId(string originalIndexId)
|
||||
{
|
||||
if (originalIndexId.IsNullOrEmpty())
|
||||
{
|
||||
return originalIndexId;
|
||||
}
|
||||
|
||||
var hash = HashCode.Combine(SessionSalt.Value, originalIndexId) & 0x7FFFFFFF;
|
||||
var bytes = BitConverter.GetBytes(hash);
|
||||
return Convert.ToBase64String(bytes).Replace("=", "");
|
||||
}
|
||||
}
|
||||
9
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
9
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
|
|
@ -1041,6 +1041,15 @@ namespace ServiceLib.Resx {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Backup to Clipboard 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string menuExport4Backup {
|
||||
get {
|
||||
return ResourceManager.GetString("menuExport4Backup", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Export 的本地化字符串。
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1728,4 +1728,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport4Backup" xml:space="preserve">
|
||||
<value>Backup to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1725,4 +1725,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport4Backup" xml:space="preserve">
|
||||
<value>Backup to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1728,4 +1728,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport4Backup" xml:space="preserve">
|
||||
<value>Backup to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1728,4 +1728,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
|
|||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport4Backup" xml:space="preserve">
|
||||
<value>Backup to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1728,4 +1728,7 @@
|
|||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport4Backup" xml:space="preserve">
|
||||
<value>Backup to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1725,4 +1725,7 @@
|
|||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport4Backup" xml:space="preserve">
|
||||
<value>备份至剪贴板 (多选)</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1725,4 +1725,7 @@
|
|||
<data name="TbPreSharedKey" xml:space="preserve">
|
||||
<value>PreSharedKey</value>
|
||||
</data>
|
||||
<data name="menuExport4Backup" xml:space="preserve">
|
||||
<value>Backup to Clipboard</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -72,6 +72,7 @@ public class ProfilesViewModel : MyReactiveObject
|
|||
public ReactiveCommand<Unit, Unit> Export2ClientConfigClipboardCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> Export2ShareUrlCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> Export2ShareUrlBase64Cmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> Export2InnerUriCmd { get; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> AddSubCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> EditSubCmd { get; }
|
||||
|
|
@ -212,6 +213,10 @@ public class ProfilesViewModel : MyReactiveObject
|
|||
{
|
||||
await Export2ShareUrlAsync(true);
|
||||
}, canEditRemove);
|
||||
Export2InnerUriCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
await Export2InnerUrlAsync();
|
||||
}, canEditRemove);
|
||||
|
||||
//Subscription
|
||||
AddSubCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||
|
|
@ -840,6 +845,32 @@ public class ProfilesViewModel : MyReactiveObject
|
|||
}
|
||||
}
|
||||
|
||||
public async Task Export2InnerUrlAsync()
|
||||
{
|
||||
var lstSelected = await GetProfileItems(true);
|
||||
if (lstSelected == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = string.Empty;
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
result = InnerFmt.ToUri(lstSelected);
|
||||
});
|
||||
|
||||
if (!result.IsNullOrEmpty())
|
||||
{
|
||||
await _updateView?.Invoke(EViewAction.SetClipboardData, result);
|
||||
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
|
||||
}
|
||||
else
|
||||
{
|
||||
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Add Servers
|
||||
|
||||
#region Subscription
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@
|
|||
Header="{x:Static resx:ResUI.menuExport2ShareUrl}"
|
||||
InputGesture="Ctrl+C" />
|
||||
<MenuItem x:Name="menuExport2ShareUrlBase64" Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" />
|
||||
<MenuItem x:Name="menuExport4Backup" Header="{x:Static resx:ResUI.menuExport4Backup}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
|
|||
this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2InnerUriCmd, v => v.menuExport4Backup).DisposeWith(disposables);
|
||||
|
||||
AppEvents.AppExitRequested
|
||||
.AsObservable()
|
||||
|
|
|
|||
|
|
@ -242,6 +242,10 @@
|
|||
x:Name="menuExport2ShareUrlBase64"
|
||||
Height="{StaticResource MenuItemHeight}"
|
||||
Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" />
|
||||
<MenuItem
|
||||
x:Name="menuExport4Backup"
|
||||
Height="{StaticResource MenuItemHeight}"
|
||||
Header="{x:Static resx:ResUI.menuExport4Backup}" />
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ public partial class ProfilesView
|
|||
this.BindCommand(ViewModel, vm => vm.Export2ClientConfigClipboardCmd, v => v.menuExport2ClientConfigClipboard).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlCmd, v => v.menuExport2ShareUrl).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2ShareUrlBase64Cmd, v => v.menuExport2ShareUrlBase64).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.Export2InnerUriCmd, v => v.menuExport4Backup).DisposeWith(disposables);
|
||||
|
||||
AppEvents.AppExitRequested
|
||||
.AsObservable()
|
||||
|
|
|
|||
Loading…
Reference in a new issue