mirror of
https://github.com/2dust/v2rayN.git
synced 2025-11-29 03:02:53 +00:00
Cert Pinning
This commit is contained in:
parent
7b5686cd8f
commit
bd7b7ca1e8
20 changed files with 394 additions and 51 deletions
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.Specialized;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Security.Principal;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
|
|
@ -762,6 +764,83 @@ public class Utils
|
|||
return null;
|
||||
}
|
||||
|
||||
public static async Task<string?> GetCertPem(string target, string serverName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (domain, _, port, _) = ParseUrl(target);
|
||||
|
||||
using var client = new TcpClient();
|
||||
await client.ConnectAsync(domain, port > 0 ? port : 443);
|
||||
|
||||
using var ssl = new SslStream(client.GetStream(), false,
|
||||
(sender, cert, chain, errors) => true);
|
||||
|
||||
await ssl.AuthenticateAsClientAsync(serverName);
|
||||
|
||||
var remote = ssl.RemoteCertificate;
|
||||
if (remote == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var leaf = new X509Certificate2(remote);
|
||||
return ExportCertToPem(leaf);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<List<string>> GetCertChainPem(string target, string serverName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pemList = new List<string>();
|
||||
var (domain, _, port, _) = ParseUrl(target);
|
||||
|
||||
using var client = new TcpClient();
|
||||
await client.ConnectAsync(domain, port > 0 ? port : 443);
|
||||
|
||||
using var ssl = new SslStream(client.GetStream(), false,
|
||||
(sender, cert, chain, errors) => true);
|
||||
|
||||
await ssl.AuthenticateAsClientAsync(serverName);
|
||||
|
||||
// TODO: Pinning Trusted CA certificates to avoid MITM
|
||||
|
||||
if (ssl.RemoteCertificate is not X509Certificate2 certChain)
|
||||
{
|
||||
return pemList;
|
||||
}
|
||||
|
||||
var chain = new X509Chain();
|
||||
chain.Build(certChain);
|
||||
|
||||
foreach (var element in chain.ChainElements)
|
||||
{
|
||||
var pem = ExportCertToPem(element.Certificate);
|
||||
pemList.Add(pem);
|
||||
}
|
||||
|
||||
return pemList;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
public static string ExportCertToPem(X509Certificate2 cert)
|
||||
{
|
||||
var der = cert.Export(X509ContentType.Cert);
|
||||
var b64 = Convert.ToBase64String(der, Base64FormattingOptions.InsertLineBreaks);
|
||||
return $"-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----\n";
|
||||
}
|
||||
|
||||
#endregion 杂项
|
||||
|
||||
#region TempPath
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ public static class ConfigHandler
|
|||
item.Mldsa65Verify = profileItem.Mldsa65Verify;
|
||||
item.Extra = profileItem.Extra;
|
||||
item.MuxEnabled = profileItem.MuxEnabled;
|
||||
item.Cert = profileItem.Cert;
|
||||
}
|
||||
|
||||
var ret = item.ConfigType switch
|
||||
|
|
|
|||
|
|
@ -141,4 +141,5 @@ public class ProfileItem : ReactiveObject
|
|||
public string Mldsa65Verify { get; set; }
|
||||
public string Extra { get; set; }
|
||||
public bool? MuxEnabled { get; set; }
|
||||
public string Cert { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ public class Tls4Sbox
|
|||
public bool? fragment { get; set; }
|
||||
public string? fragment_fallback_delay { get; set; }
|
||||
public bool? record_fragment { get; set; }
|
||||
public List<string>? certificate { get; set; }
|
||||
}
|
||||
|
||||
public class Multiplex4Sbox
|
||||
|
|
|
|||
|
|
@ -354,6 +354,14 @@ public class TlsSettings4Ray
|
|||
public string? shortId { get; set; }
|
||||
public string? spiderX { get; set; }
|
||||
public string? mldsa65Verify { get; set; }
|
||||
public List<CertificateSettings4Ray>? certificates { get; set; }
|
||||
public bool? disableSystemRoot { get; set; }
|
||||
}
|
||||
|
||||
public class CertificateSettings4Ray
|
||||
{
|
||||
public List<string>? certificate { get; set; }
|
||||
public string? usage { get; set; }
|
||||
}
|
||||
|
||||
public class TcpSettings4Ray
|
||||
|
|
|
|||
28
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
28
v2rayN/ServiceLib/Resx/ResUI.Designer.cs
generated
|
|
@ -2562,6 +2562,25 @@ namespace ServiceLib.Resx {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Certificate Pinning 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbCertPinning {
|
||||
get {
|
||||
return ResourceManager.GetString("TbCertPinning", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Server certificate (PEM format, optional). Entering a certificate will pin it.
|
||||
///Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled. 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbCertPinningTips {
|
||||
get {
|
||||
return ResourceManager.GetString("TbCertPinningTips", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Clear system proxy 的本地化字符串。
|
||||
/// </summary>
|
||||
|
|
@ -2769,6 +2788,15 @@ namespace ServiceLib.Resx {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fetch Certificate 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string TbFetchCert {
|
||||
get {
|
||||
return ResourceManager.GetString("TbFetchCert", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 Fingerprint 的本地化字符串。
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1602,4 +1602,14 @@
|
|||
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
|
||||
<value>Auto add filtered configuration from subscription groups</value>
|
||||
</data>
|
||||
<data name="TbCertPinning" xml:space="preserve">
|
||||
<value>Certificate Pinning</value>
|
||||
</data>
|
||||
<data name="TbCertPinningTips" xml:space="preserve">
|
||||
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
|
||||
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
|
||||
</data>
|
||||
<data name="TbFetchCert" xml:space="preserve">
|
||||
<value>Fetch Certificate</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1599,4 +1599,14 @@
|
|||
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
|
||||
<value>Auto add filtered configuration from subscription groups</value>
|
||||
</data>
|
||||
<data name="TbCertPinning" xml:space="preserve">
|
||||
<value>Certificate Pinning</value>
|
||||
</data>
|
||||
<data name="TbCertPinningTips" xml:space="preserve">
|
||||
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
|
||||
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
|
||||
</data>
|
||||
<data name="TbFetchCert" xml:space="preserve">
|
||||
<value>Fetch Certificate</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1602,4 +1602,14 @@
|
|||
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
|
||||
<value>Auto add filtered configuration from subscription groups</value>
|
||||
</data>
|
||||
<data name="TbCertPinning" xml:space="preserve">
|
||||
<value>Certificate Pinning</value>
|
||||
</data>
|
||||
<data name="TbCertPinningTips" xml:space="preserve">
|
||||
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
|
||||
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
|
||||
</data>
|
||||
<data name="TbFetchCert" xml:space="preserve">
|
||||
<value>Fetch Certificate</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1602,4 +1602,14 @@
|
|||
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
|
||||
<value>Auto add filtered configuration from subscription groups</value>
|
||||
</data>
|
||||
<data name="TbCertPinning" xml:space="preserve">
|
||||
<value>Certificate Pinning</value>
|
||||
</data>
|
||||
<data name="TbCertPinningTips" xml:space="preserve">
|
||||
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
|
||||
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
|
||||
</data>
|
||||
<data name="TbFetchCert" xml:space="preserve">
|
||||
<value>Fetch Certificate</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1602,4 +1602,14 @@
|
|||
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
|
||||
<value>Auto add filtered configuration from subscription groups</value>
|
||||
</data>
|
||||
<data name="TbCertPinning" xml:space="preserve">
|
||||
<value>Certificate Pinning</value>
|
||||
</data>
|
||||
<data name="TbCertPinningTips" xml:space="preserve">
|
||||
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
|
||||
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
|
||||
</data>
|
||||
<data name="TbFetchCert" xml:space="preserve">
|
||||
<value>Fetch Certificate</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1599,4 +1599,14 @@
|
|||
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
|
||||
<value>自动从订阅分组添加过滤后的配置</value>
|
||||
</data>
|
||||
<data name="TbCertPinning" xml:space="preserve">
|
||||
<value>固定证书</value>
|
||||
</data>
|
||||
<data name="TbCertPinningTips" xml:space="preserve">
|
||||
<value>服务器证书(PEM 格式,可选)。填入后将固定该证书。
|
||||
启用“跳过证书验证”时,请勿使用 '获取证书'。</value>
|
||||
</data>
|
||||
<data name="TbFetchCert" xml:space="preserve">
|
||||
<value>获取证书</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -1599,4 +1599,14 @@
|
|||
<data name="TbPolicyGroupSubChildTip" xml:space="preserve">
|
||||
<value>自動從訂閱分組新增過濾後的配置</value>
|
||||
</data>
|
||||
<data name="TbCertPinning" xml:space="preserve">
|
||||
<value>Certificate Pinning</value>
|
||||
</data>
|
||||
<data name="TbCertPinningTips" xml:space="preserve">
|
||||
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
|
||||
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
|
||||
</data>
|
||||
<data name="TbFetchCert" xml:space="preserve">
|
||||
<value>Fetch Certificate</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -204,54 +204,6 @@ public partial class CoreConfigSingboxService
|
|||
return await Task.FromResult<BaseServer4Sbox?>(null);
|
||||
}
|
||||
|
||||
private async Task<int> GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!node.ConfigType.IsGroupType())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
|
||||
if (hasCycle)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
|
||||
if (childProfiles.Count <= 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
switch (node.ConfigType)
|
||||
{
|
||||
case EConfigType.PolicyGroup:
|
||||
if (ignoreOriginChain)
|
||||
{
|
||||
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EConfigType.ProxyChain:
|
||||
await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenOutboundMux(ProfileItem node, Outbound4Sbox outbound)
|
||||
{
|
||||
try
|
||||
|
|
@ -280,7 +232,7 @@ public partial class CoreConfigSingboxService
|
|||
{
|
||||
try
|
||||
{
|
||||
if (node.StreamSecurity == Global.StreamSecurityReality || node.StreamSecurity == Global.StreamSecurity)
|
||||
if (node.StreamSecurity is Global.StreamSecurityReality or Global.StreamSecurity)
|
||||
{
|
||||
var server_name = string.Empty;
|
||||
if (node.Sni.IsNotEmpty())
|
||||
|
|
@ -307,7 +259,18 @@ public partial class CoreConfigSingboxService
|
|||
fingerprint = node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : node.Fingerprint
|
||||
};
|
||||
}
|
||||
if (node.StreamSecurity == Global.StreamSecurityReality)
|
||||
if (node.StreamSecurity == Global.StreamSecurity)
|
||||
{
|
||||
var certs = node.Cert
|
||||
?.Split("-----END CERTIFICATE-----", StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.TrimEx())
|
||||
.Where(s => !s.IsNullOrEmpty())
|
||||
.Select(s => s + "\n-----END CERTIFICATE-----")
|
||||
.Select(s => s.Replace("\r\n", "\n"))
|
||||
.ToList() ?? new();
|
||||
tls.certificate = certs.Count > 0 ? certs : null;
|
||||
}
|
||||
else
|
||||
{
|
||||
tls.reality = new Reality4Sbox()
|
||||
{
|
||||
|
|
@ -404,6 +367,54 @@ public partial class CoreConfigSingboxService
|
|||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!node.ConfigType.IsGroupType())
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
|
||||
if (hasCycle)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
|
||||
if (childProfiles.Count <= 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
switch (node.ConfigType)
|
||||
{
|
||||
case EConfigType.PolicyGroup:
|
||||
if (ignoreOriginChain)
|
||||
{
|
||||
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case EConfigType.ProxyChain:
|
||||
await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.SaveLog(_tag, ex);
|
||||
}
|
||||
return await Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task<int> GenMoreOutbounds(ProfileItem node, SingboxConfig singboxConfig)
|
||||
{
|
||||
if (node.Subid.IsNullOrEmpty())
|
||||
|
|
|
|||
|
|
@ -245,6 +245,13 @@ public partial class CoreConfigV2rayService
|
|||
var host = node.RequestHost.TrimEx();
|
||||
var path = node.Path.TrimEx();
|
||||
var sni = node.Sni.TrimEx();
|
||||
var certs = node.Cert
|
||||
?.Split("-----END CERTIFICATE-----", StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.TrimEx())
|
||||
.Where(s => !s.IsNullOrEmpty())
|
||||
.Select(s => s + "\n-----END CERTIFICATE-----")
|
||||
.Select(s => s.Replace("\r\n", "\n"))
|
||||
.ToList() ?? new();
|
||||
var useragent = "";
|
||||
if (!_config.CoreBasicItem.DefUserAgent.IsNullOrEmpty())
|
||||
{
|
||||
|
|
@ -277,6 +284,21 @@ public partial class CoreConfigV2rayService
|
|||
{
|
||||
tlsSettings.serverName = Utils.String2List(host)?.First();
|
||||
}
|
||||
if (certs.Count > 0)
|
||||
{
|
||||
var certsettings = new List<CertificateSettings4Ray>();
|
||||
foreach (var cert in certs)
|
||||
{
|
||||
var certPerLine = cert.Split("\n").ToList();
|
||||
certsettings.Add(new CertificateSettings4Ray
|
||||
{
|
||||
certificate = certPerLine,
|
||||
usage = "verify",
|
||||
});
|
||||
}
|
||||
tlsSettings.certificates = certsettings;
|
||||
tlsSettings.disableSystemRoot = true;
|
||||
}
|
||||
streamSettings.tlsSettings = tlsSettings;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ public class AddServerViewModel : MyReactiveObject
|
|||
[Reactive]
|
||||
public string? CoreType { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public string Cert { get; set; }
|
||||
|
||||
public ReactiveCommand<Unit, Unit> FetchCertCmd { get; }
|
||||
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
|
||||
|
||||
public AddServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView)
|
||||
|
|
@ -15,6 +19,10 @@ public class AddServerViewModel : MyReactiveObject
|
|||
_config = AppManager.Instance.Config;
|
||||
_updateView = updateView;
|
||||
|
||||
FetchCertCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
await FetchCert();
|
||||
});
|
||||
SaveCmd = ReactiveCommand.CreateFromTask(async () =>
|
||||
{
|
||||
await SaveServerAsync();
|
||||
|
|
@ -33,6 +41,7 @@ public class AddServerViewModel : MyReactiveObject
|
|||
SelectedSource = JsonUtils.DeepCopy(profileItem);
|
||||
}
|
||||
CoreType = SelectedSource?.CoreType?.ToString();
|
||||
Cert = SelectedSource?.Cert?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private async Task SaveServerAsync()
|
||||
|
|
@ -77,6 +86,7 @@ public class AddServerViewModel : MyReactiveObject
|
|||
}
|
||||
}
|
||||
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
|
||||
SelectedSource.Cert = Cert.IsNullOrEmpty() ? null : Cert;
|
||||
|
||||
if (await ConfigHandler.AddServer(_config, SelectedSource) == 0)
|
||||
{
|
||||
|
|
@ -88,4 +98,23 @@ public class AddServerViewModel : MyReactiveObject
|
|||
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchCert()
|
||||
{
|
||||
if (SelectedSource.StreamSecurity != Global.StreamSecurity)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var domain = SelectedSource.Address;
|
||||
var serverName = SelectedSource.Sni.IsNullOrEmpty() ? SelectedSource.Address : SelectedSource.Sni;
|
||||
if (!Utils.IsDomain(serverName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (SelectedSource.Port > 0)
|
||||
{
|
||||
domain += $":{SelectedSource.Port}";
|
||||
}
|
||||
Cert = await Utils.GetCertPem(domain, serverName);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -713,7 +713,7 @@
|
|||
Grid.Row="7"
|
||||
ColumnDefinitions="180,Auto"
|
||||
IsVisible="False"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto">
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
|
|
@ -767,6 +767,53 @@
|
|||
Grid.Column="1"
|
||||
Width="200"
|
||||
Margin="{StaticResource Margin4}" />
|
||||
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbCertPinning}" />
|
||||
<Button
|
||||
Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
Classes="IconButton"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="{StaticResource MarginLr8}">
|
||||
<Button.Content>
|
||||
<PathIcon Data="{StaticResource SemiIconMore}" >
|
||||
<PathIcon.RenderTransform>
|
||||
<RotateTransform Angle="90" />
|
||||
</PathIcon.RenderTransform>
|
||||
</PathIcon>
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
<Flyout>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Static resx:ResUI.TbCertPinningTips}" />
|
||||
<Button
|
||||
x:Name="btnFetchCert"
|
||||
HorizontalAlignment="Left"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Content="{x:Static resx:ResUI.TbFetchCert}" />
|
||||
<TextBox
|
||||
x:Name="txtCert"
|
||||
Width="400"
|
||||
MinHeight="100"
|
||||
Margin="{StaticResource Margin4}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Classes="TextArea"
|
||||
MinLines="6"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid
|
||||
x:Name="gridRealityMore"
|
||||
|
|
|
|||
|
|
@ -185,6 +185,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
|
|||
this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.SelectedValue).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);
|
||||
//reality
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables);
|
||||
|
|
@ -193,6 +194,7 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
|
|||
this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables);
|
||||
|
||||
this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -928,6 +928,7 @@
|
|||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180" />
|
||||
|
|
@ -995,6 +996,47 @@
|
|||
Width="200"
|
||||
Margin="{StaticResource Margin4}"
|
||||
Style="{StaticResource DefComboBox}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Row="5"
|
||||
Grid.Column="0"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbCertPinning}" />
|
||||
|
||||
<materialDesign:PopupBox
|
||||
Grid.Row="5"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Left"
|
||||
StaysOpen="True"
|
||||
Style="{StaticResource MaterialDesignToolForegroundPopupBox}">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ToolbarTextBlock}"
|
||||
Text="{x:Static resx:ResUI.TbCertPinningTips}"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Name="btnFetchCert"
|
||||
Width="100"
|
||||
Margin="{StaticResource MarginLeftRight4}"
|
||||
HorizontalAlignment="Left"
|
||||
Content="{x:Static resx:ResUI.TbFetchCert}"
|
||||
Style="{StaticResource DefButton}" />
|
||||
<TextBox
|
||||
x:Name="txtCert"
|
||||
Width="400"
|
||||
Margin="{StaticResource Margin4}"
|
||||
VerticalAlignment="Center"
|
||||
AcceptsReturn="True"
|
||||
MinLines="6"
|
||||
Style="{StaticResource MyOutlinedTextBox}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</materialDesign:PopupBox>
|
||||
|
||||
</Grid>
|
||||
<Grid
|
||||
x:Name="gridRealityMore"
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ public partial class AddServerWindow
|
|||
this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);
|
||||
//reality
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.Text).DisposeWith(disposables);
|
||||
|
|
@ -188,6 +189,7 @@ public partial class AddServerWindow
|
|||
this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables);
|
||||
this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables);
|
||||
|
||||
this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables);
|
||||
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue