mirror of
https://github.com/snltty/linker.git
synced 2025-12-19 18:06:47 +08:00
app
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
namespace linker.app
|
||||
using linker.libs.extends;
|
||||
|
||||
namespace linker.app
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
@@ -10,6 +12,8 @@
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
|
||||
|
||||
|
||||
|
||||
MainPage = new MainPage();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,6 @@ namespace linker.app
|
||||
{
|
||||
webview.Source = new Uri($"http://127.0.0.1:1804?t={DateTime.Now.Ticks}");
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -88,7 +88,9 @@ namespace linker.app
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// VPN服务,先启动服务,才能创建VPN连接
|
||||
/// </summary>
|
||||
[Service(Label = "VpnServiceLinker", Name = "com.snltty.linker.app.VpnServiceLinker", Enabled = true, Permission = "android.permission.BIND_VPN_SERVICE")]
|
||||
public class VpnServiceLinker : VpnService, ILinkerTunDeviceCallback, ITuntapProxyCallback
|
||||
{
|
||||
@@ -142,6 +144,9 @@ namespace linker.app
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 前台服务,运行Linker
|
||||
/// </summary>
|
||||
[Service(Label = "ForegroundService", Name = "com.snltty.linker.app.ForegroundService", Exported = true)]
|
||||
[IntentFilter(new string[] { "com.snltty.linker.app.ForegroundService" })]
|
||||
public sealed class ForegroundService : Service
|
||||
@@ -214,6 +219,7 @@ namespace linker.app
|
||||
|
||||
LinkerMessengerEntry.Initialize();
|
||||
LinkerMessengerEntry.AddService<IWebServerFileReader, WebServerFileReader>();
|
||||
LinkerMessengerEntry.AddService<ISystemInformation, SystemInformation>();
|
||||
LinkerMessengerEntry.Build();
|
||||
LinkerMessengerEntry.Setup(ExcludeModule.Logger, config);
|
||||
IPlatformApplication.Current.Services.GetService<InitializeService>().SendOnInitialized();
|
||||
@@ -284,6 +290,9 @@ namespace linker.app
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VPN设备
|
||||
/// </summary>
|
||||
public sealed class LinkerVpnDevice : ILinkerTunDevice
|
||||
{
|
||||
private string name = string.Empty;
|
||||
@@ -487,6 +496,9 @@ namespace linker.app
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理页面的文件读取,跟PC的不太一样
|
||||
/// </summary>
|
||||
public sealed class WebServerFileReader : IWebServerFileReader
|
||||
{
|
||||
ReceiveDataBuffer receiveDataBuffer = new ReceiveDataBuffer();
|
||||
@@ -503,5 +515,18 @@ namespace linker.app
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统信息
|
||||
/// </summary>
|
||||
public sealed class SystemInformation: ISystemInformation
|
||||
{
|
||||
public string Get()
|
||||
{
|
||||
var deviceInfo = DeviceInfo.Current;
|
||||
return $"{deviceInfo.Manufacturer} {deviceInfo.Name} {deviceInfo.VersionString} {deviceInfo.Platform} {deviceInfo.Idiom.ToString()}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<IsFirstTimeProjectOpen>False</IsFirstTimeProjectOpen>
|
||||
<ActiveDebugFramework>net8.0-android</ActiveDebugFramework>
|
||||
<ActiveDebugProfile>Pixel 5 - API 34 (Android 14.0 - API 34)</ActiveDebugProfile>
|
||||
<SelectedPlatformGroup>Emulator</SelectedPlatformGroup>
|
||||
<ActiveDebugFramework>net8.0-ios</ActiveDebugFramework>
|
||||
<ActiveDebugProfile>模拟器</ActiveDebugProfile>
|
||||
<SelectedPlatformGroup>Simulator</SelectedPlatformGroup>
|
||||
<DefaultDevice>pixel_5_-_api_34</DefaultDevice>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-android|AnyCPU'">
|
||||
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(TargetPlatformIdentifier)'=='iOS'">
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Update="App.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.IO.Compression;
|
||||
using linker.libs;
|
||||
using linker.messenger.signin;
|
||||
using linker.messenger.api;
|
||||
using System.Text;
|
||||
using linker.messenger.relay.client.transport;
|
||||
namespace linker.messenger.store.file
|
||||
{
|
||||
public sealed class ConfigApiController : IApiController
|
||||
@@ -14,8 +16,9 @@ namespace linker.messenger.store.file
|
||||
private readonly IMessengerSender sender;
|
||||
private readonly SignInClientState signInClientState;
|
||||
private readonly IApiStore apiStore;
|
||||
private readonly ExportResolver exportResolver;
|
||||
|
||||
public ConfigApiController(RunningConfig runningConfig, FileConfig config, SignInClientTransfer signInClientTransfer, IMessengerSender sender, SignInClientState signInClientState, IApiStore apiStore)
|
||||
public ConfigApiController(RunningConfig runningConfig, FileConfig config, SignInClientTransfer signInClientTransfer, IMessengerSender sender, SignInClientState signInClientState, IApiStore apiStore, ExportResolver exportResolver)
|
||||
{
|
||||
this.runningConfig = runningConfig;
|
||||
this.config = config;
|
||||
@@ -23,6 +26,7 @@ namespace linker.messenger.store.file
|
||||
this.sender = sender;
|
||||
this.signInClientState = signInClientState;
|
||||
this.apiStore = apiStore;
|
||||
this.exportResolver = exportResolver;
|
||||
}
|
||||
|
||||
public object Get(ApiControllerParamsInfo param)
|
||||
@@ -79,6 +83,80 @@ namespace linker.messenger.store.file
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool InstallCopy(ApiControllerParamsInfo param)
|
||||
{
|
||||
try
|
||||
{
|
||||
Dictionary<string, string> dic = Encoding.UTF8.GetString(Convert.FromBase64String(param.Content)).DeJson<Dictionary<string, string>>();
|
||||
config.Save(dic);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public async Task<bool> InstallSave(ApiControllerParamsInfo param)
|
||||
{
|
||||
try
|
||||
{
|
||||
InstallSaveInfo info = param.Content.DeJson<InstallSaveInfo>();
|
||||
|
||||
string value = await exportResolver.Get(info.Server, info.Value);
|
||||
Dictionary<string, string> dic = Encoding.UTF8.GetString(Convert.FromBase64String(value)).DeJson<Dictionary<string, string>>();
|
||||
config.Save(dic);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[Access(AccessValue.Export)]
|
||||
public async Task<string> Copy(ApiControllerParamsInfo param)
|
||||
{
|
||||
try
|
||||
{
|
||||
ConfigExportInfo configExportInfo = param.Content.DeJson<ConfigExportInfo>();
|
||||
|
||||
var (client, clientObject, common, commonObject) = await GetConfig(configExportInfo).ConfigureAwait(false);
|
||||
Dictionary<string, string> dic = new Dictionary<string, string>
|
||||
{
|
||||
{"Client",clientObject.ToJson()},
|
||||
{"Common",commonObject.ToJson()},
|
||||
};
|
||||
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(dic.ToJson()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.Instance.Error(ex);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
[Access(AccessValue.Export)]
|
||||
public async Task<string> Save(ApiControllerParamsInfo param)
|
||||
{
|
||||
try
|
||||
{
|
||||
ConfigExportInfo configExportInfo = param.Content.DeJson<ConfigExportInfo>();
|
||||
|
||||
var (client, clientObject, common, commonObject) = await GetConfig(configExportInfo).ConfigureAwait(false);
|
||||
Dictionary<string, object> dic = new Dictionary<string, object>
|
||||
{
|
||||
{"Client",clientObject},
|
||||
{"Common",commonObject},
|
||||
};
|
||||
string value = Convert.ToBase64String(Encoding.UTF8.GetBytes(dic.ToJson()));
|
||||
return await exportResolver.Save(signInClientState.Connection.Address, value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.Instance.Error(ex);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
[Access(AccessValue.Export)]
|
||||
public async Task<bool> Export(ApiControllerParamsInfo param)
|
||||
{
|
||||
@@ -105,31 +183,9 @@ namespace linker.messenger.store.file
|
||||
string configPath = Path.Combine(rootPath, $"configs");
|
||||
Directory.CreateDirectory(configPath);
|
||||
|
||||
ConfigClientInfo client = config.Data.Client.ToJson().DeJson<ConfigClientInfo>();
|
||||
client.Id = string.Empty;
|
||||
client.Name = string.Empty;
|
||||
if (configExportInfo.Single || client.OnlyNode)
|
||||
{
|
||||
client.Id = await signInClientTransfer.GetNewId().ConfigureAwait(false);
|
||||
client.Name = configExportInfo.Name;
|
||||
}
|
||||
if (client.OnlyNode == false)
|
||||
{
|
||||
client.CApi.ApiPassword = configExportInfo.ApiPassword;
|
||||
}
|
||||
|
||||
client.Access = config.Data.Client.Access & (AccessValue)configExportInfo.Access;
|
||||
client.OnlyNode = true;
|
||||
client.Action.Args.Clear();
|
||||
client.Action.Arg = string.Empty;
|
||||
|
||||
client.Groups = [config.Data.Client.Groups[0]];
|
||||
File.WriteAllText(Path.Combine(configPath, $"client.json"), client.Serialize(client));
|
||||
|
||||
ConfigCommonInfo common = config.Data.Common.ToJson().DeJson<ConfigCommonInfo>();
|
||||
common.Install = true;
|
||||
common.Modes = ["client"];
|
||||
File.WriteAllText(Path.Combine(configPath, $"common.json"), common.ToJsonFormat());
|
||||
var (client, clientObject, common, commonObject) = await GetConfig(configExportInfo).ConfigureAwait(false);
|
||||
File.WriteAllText(Path.Combine(configPath, $"client.json"), config.Data.Client.Serialize(clientObject));
|
||||
File.WriteAllText(Path.Combine(configPath, $"common.json"), config.Data.Common.Serialize(commonObject));
|
||||
|
||||
|
||||
ZipFile.CreateFromDirectory(rootPath, zipPath);
|
||||
@@ -182,6 +238,68 @@ namespace linker.messenger.store.file
|
||||
}
|
||||
|
||||
|
||||
private async Task<(ConfigClientInfo, object, ConfigCommonInfo, object)> GetConfig(ConfigExportInfo configExportInfo)
|
||||
{
|
||||
ConfigClientInfo client = config.Data.Client.ToJson().DeJson<ConfigClientInfo>();
|
||||
client.Id = string.Empty;
|
||||
client.Name = string.Empty;
|
||||
if (configExportInfo.Single || client.OnlyNode)
|
||||
{
|
||||
client.Id = await signInClientTransfer.GetNewId().ConfigureAwait(false);
|
||||
client.Name = configExportInfo.Name;
|
||||
}
|
||||
if (client.OnlyNode == false)
|
||||
{
|
||||
client.CApi.ApiPassword = configExportInfo.ApiPassword;
|
||||
}
|
||||
|
||||
client.Access = (AccessValue)((ulong)config.Data.Client.Access & configExportInfo.Access);
|
||||
|
||||
|
||||
if (configExportInfo.Relay) client.Relay = new RelayClientInfo { Servers = new RelayServerInfo[] { client.Relay.Servers[0] } };
|
||||
else client.Relay = new RelayClientInfo { Servers = new RelayServerInfo[] { new RelayServerInfo { } } };
|
||||
|
||||
if (configExportInfo.SForward) client.SForward = new linker.messenger.sforward.SForwardConfigClientInfo { SecretKey = client.SForward.SecretKey };
|
||||
else client.SForward = new linker.messenger.sforward.SForwardConfigClientInfo { };
|
||||
|
||||
if (configExportInfo.Server) client.Servers = new SignInClientServerInfo[] { client.Servers[0] };
|
||||
else client.Servers = new SignInClientServerInfo[] { };
|
||||
|
||||
if (configExportInfo.Group) client.Groups = new SignInClientGroupInfo[] { client.Groups[0] };
|
||||
else client.Groups = new SignInClientGroupInfo[] { };
|
||||
|
||||
if (configExportInfo.Updater) client.Updater = new linker.messenger.updater.UpdaterConfigClientInfo { SecretKey = client.Updater.SecretKey };
|
||||
else client.Updater = new linker.messenger.updater.UpdaterConfigClientInfo { };
|
||||
|
||||
|
||||
if (configExportInfo.Tunnel) client.Tunnel = new TunnelConfigClientInfo { Transports = client.Tunnel.Transports };
|
||||
else client.Tunnel = new TunnelConfigClientInfo { Transports = new List<linker.tunnel.transport.TunnelTransportItemInfo>() };
|
||||
|
||||
ConfigCommonInfo common = config.Data.Common.ToJson().DeJson<ConfigCommonInfo>();
|
||||
common.Install = true;
|
||||
common.Modes = ["client"];
|
||||
|
||||
return (client, new
|
||||
{
|
||||
client.Id,
|
||||
client.Name,
|
||||
client.CApi,
|
||||
client.Access,
|
||||
Groups = new SignInClientGroupInfo[] { config.Data.Client.Groups[0] },
|
||||
Servers = new SignInClientServerInfo[] { config.Data.Client.Servers[0] },
|
||||
client.SForward,
|
||||
client.Updater,
|
||||
Relay = new { Servers = new RelayServerInfo[] { client.Relay.Servers[0] } },
|
||||
client.Tunnel,
|
||||
}, common, new { Install = true, Modes = new string[] { "client" } });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public sealed class InstallSaveInfo
|
||||
{
|
||||
public string Server { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ConfigInstallInfo
|
||||
@@ -246,6 +364,14 @@ namespace linker.messenger.store.file
|
||||
public string ApiPassword { get; set; }
|
||||
public bool Single { get; set; }
|
||||
public ulong Access { get; set; }
|
||||
|
||||
public bool Relay { get; set; }
|
||||
public bool SForward { get; set; }
|
||||
public bool Updater { get; set; }
|
||||
public bool Server { get; set; }
|
||||
public bool Group { get; set; }
|
||||
public bool Tunnel { get; set; }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -99,6 +99,9 @@ namespace linker.messenger.store.file
|
||||
|
||||
serviceCollection.AddSingleton<IPlanStore, PlanStore>();
|
||||
|
||||
|
||||
serviceCollection.AddSingleton<ExportResolver>();
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
public static ServiceProvider UseStoreFile(this ServiceProvider serviceProvider,Dictionary<string,string> configDic)
|
||||
@@ -121,6 +124,12 @@ namespace linker.messenger.store.file
|
||||
|
||||
});
|
||||
|
||||
ResolverTransfer resolverTransfer = serviceProvider.GetService<ResolverTransfer>();
|
||||
resolverTransfer.AddResolvers(new List<IResolver>
|
||||
{
|
||||
serviceProvider.GetService<ExportResolver>(),
|
||||
});
|
||||
|
||||
return serviceProvider;
|
||||
}
|
||||
}
|
||||
|
||||
147
src/linker.messenger.store.file/ExportResolver.cs
Normal file
147
src/linker.messenger.store.file/ExportResolver.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
|
||||
using linker.libs;
|
||||
using linker.libs.extends;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using System.Buffers;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace linker.messenger.store.file
|
||||
{
|
||||
public sealed class ExportResolver : IResolver
|
||||
{
|
||||
public byte Type => 222;
|
||||
|
||||
private readonly IMemoryCache cache = new MemoryCache(new MemoryCacheOptions { });
|
||||
|
||||
private readonly ISerializer serializer;
|
||||
public ExportResolver(ISerializer serializer)
|
||||
{
|
||||
this.serializer = serializer;
|
||||
}
|
||||
|
||||
public async Task<string> Save(string server, string value)
|
||||
{
|
||||
return await Save(NetworkHelper.GetEndPoint(server, 1802), value).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<string> Save(IPEndPoint server, string value)
|
||||
{
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);
|
||||
Socket socket = new Socket(server.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(server).WaitAsync(TimeSpan.FromMilliseconds(5000));
|
||||
|
||||
await socket.SendAsync(new byte[] { Type });
|
||||
await socket.SendAsync(serializer.Serialize(new ExportSaveInfo { Type = ExportSaveType.Save, Value = value }.ToJson()));
|
||||
|
||||
int length = await socket.ReceiveAsync(buffer.AsMemory(), SocketFlags.None).AsTask().WaitAsync(TimeSpan.FromMilliseconds(5000)).ConfigureAwait(false);
|
||||
|
||||
return serializer.Deserialize<string>(buffer.AsSpan(0, length));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
socket.SafeClose();
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
public async Task<string> Get(string server, string value)
|
||||
{
|
||||
return await Get(NetworkHelper.GetEndPoint(server, 1802), value).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<string> Get(IPEndPoint server, string value)
|
||||
{
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(8*1024);
|
||||
Socket socket = new Socket(server.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(server).WaitAsync(TimeSpan.FromMilliseconds(5000));
|
||||
|
||||
await socket.SendAsync(new byte[] { Type });
|
||||
await socket.SendAsync(serializer.Serialize(new ExportSaveInfo { Type = ExportSaveType.Get, Value = value }.ToJson()));
|
||||
|
||||
int length = await socket.ReceiveAsync(buffer.AsMemory(), SocketFlags.None).AsTask().WaitAsync(TimeSpan.FromMilliseconds(5000)).ConfigureAwait(false);
|
||||
|
||||
return serializer.Deserialize<string>(buffer.AsSpan(0, length));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
socket.SafeClose();
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public async Task Resolve(Socket socket, Memory<byte> memory)
|
||||
{
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
|
||||
try
|
||||
{
|
||||
int length = await socket.ReceiveAsync(buffer.AsMemory(), SocketFlags.None).ConfigureAwait(false);
|
||||
ExportSaveInfo info = serializer.Deserialize<string>(buffer.AsMemory(0, length).Span).DeJson<ExportSaveInfo>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(info.Value))
|
||||
{
|
||||
socket.SafeClose();
|
||||
return;
|
||||
}
|
||||
Console.WriteLine(info.Type);
|
||||
switch (info.Type)
|
||||
{
|
||||
case ExportSaveType.Save:
|
||||
{
|
||||
string key = Guid.NewGuid().ToString();
|
||||
cache.Set(key, info.Value, TimeSpan.FromMinutes(10));
|
||||
await socket.SendAsync(serializer.Serialize(key));
|
||||
}
|
||||
break;
|
||||
case ExportSaveType.Get:
|
||||
{
|
||||
if (cache.TryGetValue(info.Value, out string value))
|
||||
{
|
||||
cache.Remove(info.Value);
|
||||
await socket.SendAsync(serializer.Serialize(value));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
socket.SafeClose();
|
||||
if (LoggerHelper.Instance.LoggerLevel <= LoggerTypes.DEBUG)
|
||||
LoggerHelper.Instance.Error(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
public async Task Resolve(Socket socket, IPEndPoint ep, Memory<byte> memory)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ExportSaveInfo
|
||||
{
|
||||
public ExportSaveType Type { get; set; } = ExportSaveType.Save;
|
||||
public string Value { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public enum ExportSaveType : byte
|
||||
{
|
||||
Save = 0,
|
||||
Get = 1,
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ namespace linker.messenger.tuntap
|
||||
|
||||
serviceCollection.AddSingleton<TuntapExRoute>();
|
||||
|
||||
serviceCollection.AddSingleton<ISystemInformation, SystemInformation>();
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
public static ServiceProvider UseTuntapClient(this ServiceProvider serviceProvider)
|
||||
@@ -70,6 +72,8 @@ namespace linker.messenger.tuntap
|
||||
serviceCollection.AddSingleton<TuntapServerMessenger>();
|
||||
serviceCollection.AddSingleton<LeaseServerTreansfer>();
|
||||
|
||||
serviceCollection.AddSingleton<ISystemInformation, SystemInformation>();
|
||||
|
||||
return serviceCollection;
|
||||
}
|
||||
public static ServiceProvider UseTuntapServer(this ServiceProvider serviceProvider)
|
||||
|
||||
17
src/linker.messenger.tuntap/ISystemInformation.cs
Normal file
17
src/linker.messenger.tuntap/ISystemInformation.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace linker.messenger.tuntap
|
||||
{
|
||||
public interface ISystemInformation
|
||||
{
|
||||
public string Get()
|
||||
{
|
||||
return $"{System.Runtime.InteropServices.RuntimeInformation.OSDescription} {(string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SNLTTY_LINKER_IS_DOCKER")) == false ? "Docker" : "")}";
|
||||
}
|
||||
}
|
||||
public sealed class SystemInformation: ISystemInformation { }
|
||||
}
|
||||
@@ -30,8 +30,9 @@ namespace linker.messenger.tuntap
|
||||
private readonly TuntapTransfer tuntapTransfer;
|
||||
private readonly ExRouteTransfer exRouteTransfer;
|
||||
private readonly SignInClientState signInClientState;
|
||||
private readonly ISystemInformation systemInformation;
|
||||
|
||||
public TuntapDecenter(ISignInClientStore signInClientStore, SignInClientState signInClientState, ISerializer serializer, TuntapProxy tuntapProxy, TuntapConfigTransfer tuntapConfigTransfer, TuntapTransfer tuntapTransfer, ExRouteTransfer exRouteTransfer)
|
||||
public TuntapDecenter(ISignInClientStore signInClientStore, SignInClientState signInClientState, ISerializer serializer, TuntapProxy tuntapProxy, TuntapConfigTransfer tuntapConfigTransfer, TuntapTransfer tuntapTransfer, ExRouteTransfer exRouteTransfer, ISystemInformation systemInformation)
|
||||
{
|
||||
this.signInClientStore = signInClientStore;
|
||||
this.serializer = serializer;
|
||||
@@ -40,7 +41,7 @@ namespace linker.messenger.tuntap
|
||||
this.tuntapTransfer = tuntapTransfer;
|
||||
this.exRouteTransfer = exRouteTransfer;
|
||||
this.signInClientState = signInClientState;
|
||||
|
||||
this.systemInformation = systemInformation;
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
@@ -70,7 +71,7 @@ namespace linker.messenger.tuntap
|
||||
Status = tuntapTransfer.Status,
|
||||
SetupError = tuntapTransfer.SetupError,
|
||||
NatError = tuntapTransfer.NatError,
|
||||
SystemInfo = $"{System.Runtime.InteropServices.RuntimeInformation.OSDescription} {(string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SNLTTY_LINKER_IS_DOCKER")) == false ? "Docker" : "")}",
|
||||
SystemInfo = systemInformation.Get(),
|
||||
|
||||
Forwards = tuntapConfigTransfer.Info.Forwards,
|
||||
Switch = tuntapConfigTransfer.Info.Switch
|
||||
|
||||
@@ -164,17 +164,17 @@ namespace linker.tun
|
||||
string str = CommandHelper.Linux(string.Empty, new string[] {
|
||||
$"sysctl -w net.ipv4.ip_forward=1",
|
||||
|
||||
$"iptables -t nat -A POSTROUTING -o {Name} -j MASQUERADE",
|
||||
$"iptables -A FORWARD -i {interfaceLinux} -o {Name} -j ACCEPT",
|
||||
$"iptables -A FORWARD -i {Name} -j ACCEPT",
|
||||
|
||||
$"iptables -t nat -A POSTROUTING -o {Name} -j MASQUERADE",
|
||||
$"iptables -t nat -A POSTROUTING ! -o {Name} -s {network}/{prefixLength} -j MASQUERADE",
|
||||
|
||||
isSupport ? $"iptables -A FORWARD -i {Name} -o {interfaceLinux} -m state --state ESTABLISHED,RELATED -j ACCEPT"
|
||||
: $"iptables -A FORWARD -i {Name} -o {interfaceLinux} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT",
|
||||
|
||||
$"iptables -A FORWARD -i {Name} -j ACCEPT",
|
||||
|
||||
isSupport ? $"iptables -A FORWARD -o {Name} -m state --state ESTABLISHED,RELATED -j ACCEPT"
|
||||
: $"iptables -A FORWARD -o {Name} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT",
|
||||
$"iptables -t nat -A POSTROUTING ! -o {Name} -s {network}/{prefixLength} -j MASQUERADE",
|
||||
|
||||
$"iptables -t nat -A POSTROUTING ! -o {Name} -s {network}/{prefixLength} -j MASQUERADE",
|
||||
});
|
||||
RestartFirewall();
|
||||
}
|
||||
@@ -351,6 +351,4 @@ namespace linker.tun
|
||||
return await Task.FromResult(output.Contains("state UP")).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
1
src/linker.web/public/google.svg
Normal file
1
src/linker.web/public/google.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745458919168" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2606" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M214.101333 512c0-32.512 5.546667-63.701333 15.36-92.928L57.173333 290.218667A491.861333 491.861333 0 0 0 4.693333 512c0 79.701333 18.858667 154.88 52.394667 221.610667l172.202667-129.066667A290.56 290.56 0 0 1 214.101333 512" fill="#FBBC05" p-id="2607"></path><path d="M516.693333 216.192c72.106667 0 137.258667 25.002667 188.458667 65.962667L854.101333 136.533333C763.349333 59.178667 646.997333 11.392 516.693333 11.392c-202.325333 0-376.234667 113.28-459.52 278.826667l172.373334 128.853333c39.68-118.016 152.832-202.88 287.146666-202.88" fill="#EA4335" p-id="2608"></path><path d="M516.693333 807.808c-134.357333 0-247.509333-84.864-287.232-202.88l-172.288 128.853333c83.242667 165.546667 257.152 278.826667 459.52 278.826667 124.842667 0 244.053333-43.392 333.568-124.757333l-163.584-123.818667c-46.122667 28.458667-104.234667 43.776-170.026666 43.776" fill="#34A853" p-id="2609"></path><path d="M1005.397333 512c0-29.568-4.693333-61.44-11.648-91.008H516.650667V614.4h274.602666c-13.696 65.962667-51.072 116.650667-104.533333 149.632l163.541333 123.818667c93.994667-85.418667 155.136-212.650667 155.136-375.850667" fill="#4285F4" p-id="2610"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/linker.web/public/xiaomi.svg
Normal file
1
src/linker.web/public/xiaomi.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1745458938525" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3743" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0m184.32 0l655.36 0q184.32 0 184.32 184.32l0 655.36q0 184.32-184.32 184.32l-655.36 0q-184.32 0-184.32-184.32l0-655.36q0-184.32 184.32-184.32Z" fill="#FE7945" p-id="3744"></path><path d="M184.32 286.72h317.44a153.6 153.6 0 0 1 153.6 153.6v307.2H184.32v-460.8z" fill="#FFFFFF" p-id="3745"></path><path d="M737.28 286.72h102.4v460.8H737.28z" fill="#FFFFFF" p-id="3746"></path><path d="M286.72 389.12h215.04a51.2 51.2 0 0 1 51.2 51.2v307.2H286.72v-358.4z" fill="#FE7945" p-id="3747"></path><path d="M368.64 491.52h102.4v256H368.64z" fill="#FFFFFF" p-id="3748"></path></svg>
|
||||
|
After Width: | Height: | Size: 905 B |
@@ -7,6 +7,18 @@ export const getConfig = () => {
|
||||
export const install = (data) => {
|
||||
return sendWebsocketMsg('config/install', data);
|
||||
}
|
||||
export const installCopy = (data) => {
|
||||
return sendWebsocketMsg('config/InstallCopy', data);
|
||||
}
|
||||
export const installSave = (data) => {
|
||||
return sendWebsocketMsg('config/InstallSave', data);
|
||||
}
|
||||
export const exportConfig = (data) => {
|
||||
return sendWebsocketMsg('config/export', data);
|
||||
}
|
||||
export const copyConfig = (data) => {
|
||||
return sendWebsocketMsg('config/copy', data);
|
||||
}
|
||||
export const saveConfig = (data) => {
|
||||
return sendWebsocketMsg('config/save', data);
|
||||
}
|
||||
@@ -63,6 +63,16 @@ export default {
|
||||
'status.exportNamePlease': 'Please input device name',
|
||||
'status.exportApiPassword': 'Api Pwd',
|
||||
'status.exportApiPasswordPlease': 'Please input api password',
|
||||
'status.exportDownload': 'Download',
|
||||
'status.exportCopy': 'Copy',
|
||||
'status.exportSave': 'Save',
|
||||
'status.exportRelay': 'Relay secretKey',
|
||||
'status.exportSForward': 'SForward secretKey',
|
||||
'status.exportUpdater': 'Update secretKey',
|
||||
'status.exportServer': 'Messenger server',
|
||||
'status.exportGroup': 'Group',
|
||||
'status.exportTunnel': 'Tunnel config',
|
||||
|
||||
'status.api': 'Manager api',
|
||||
'status.apiClear': 'Clear',
|
||||
'status.apiAlter': 'Alter',
|
||||
|
||||
@@ -57,12 +57,21 @@ export default {
|
||||
'status.cdkey': 'CDKEY商城',
|
||||
|
||||
'status.export': '导出配置',
|
||||
'status.exportText': '导出配置,作为子设备运行,如果使用docker,容器映射configs文件夹即可',
|
||||
'status.exportText': '导出配置,客户端覆盖配置文件,或粘贴配置信息,或在线加载',
|
||||
'status.exportSingle': '单设备',
|
||||
'status.exportName': '设备名',
|
||||
'status.exportNamePlease': '请输入设备名',
|
||||
'status.exportApiPassword': '接口密码',
|
||||
'status.exportApiPasswordPlease': '请输入接口密码',
|
||||
'status.exportDownload': '下载',
|
||||
'status.exportCopy': '复制',
|
||||
'status.exportSave': '保存',
|
||||
'status.exportRelay': '中继密钥',
|
||||
'status.exportSForward': '内网穿透密钥',
|
||||
'status.exportUpdater': '更新密钥',
|
||||
'status.exportServer': '信标服务器',
|
||||
'status.exportGroup': '当前分组',
|
||||
'status.exportTunnel': '打洞协议',
|
||||
|
||||
'status.api': '管理接口',
|
||||
'status.apiClear': '清除',
|
||||
|
||||
@@ -105,7 +105,7 @@ export default {
|
||||
|
||||
const route = useRoute();
|
||||
const globalData = injectGlobalData();
|
||||
const hasConfig = computed(()=>globalData.value.hasAccess('Config') || globalData.value.hasAccess('Sync') || globalData.value.hasAccess('Group'));
|
||||
const hasConfig = computed(()=>globalData.value.hasAccess('Config'));
|
||||
const hasLogger = computed(()=>globalData.value.hasAccess('LoggerShow'));
|
||||
const hasTransport = computed(()=>globalData.value.hasAccess('Transport'));
|
||||
const hasAction = computed(()=>globalData.value.hasAccess('Action'));
|
||||
|
||||
@@ -3,16 +3,21 @@
|
||||
<el-col :span="8">
|
||||
<el-checkbox v-model="state.checkAll" @change="handleCheckAllChange" label="全选" :indeterminate="state.isIndeterminate" />
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-checkbox v-model="state.full" ><span class="red">满权限(顶级管理权)</span></el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-checkbox-group v-model="state.checkList" @change="handleCheckedChange">
|
||||
<el-row>
|
||||
<template v-for="(item,index) in access" :key="index">
|
||||
<el-col :span="8">
|
||||
<el-checkbox :value="item.Value" :label="item.Text" />
|
||||
</el-col>
|
||||
</template>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
<div class="access-wrap scrollbar">
|
||||
<el-checkbox-group v-model="state.checkList" @change="handleCheckedChange">
|
||||
<el-row>
|
||||
<template v-for="(item,index) in access" :key="index">
|
||||
<el-col :span="8">
|
||||
<el-checkbox :value="item.Value" :label="item.Text" />
|
||||
</el-col>
|
||||
</template>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { computed, onMounted, reactive } from 'vue';
|
||||
@@ -46,10 +51,12 @@ export default {
|
||||
globalData.value.config.Client.Accesss.Group.Value,
|
||||
],
|
||||
checkAll:false,
|
||||
full:false,
|
||||
isIndeterminate:false
|
||||
});
|
||||
|
||||
const getValue = ()=>{
|
||||
if(state.full) return (+(BigInt(0xffffffffffffffff)>>BigInt(12)).toString())-1;
|
||||
return +state.checkList.reduce((sum,item)=>{
|
||||
return (sum | BigInt(item));
|
||||
},BigInt(0)).toString();
|
||||
@@ -84,4 +91,5 @@ export default {
|
||||
</script>
|
||||
<style lang="stylus" scoped>
|
||||
.el-col {text-align:left;}
|
||||
.access-wrap{height:40rem}
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@ export const provideTuntap = () => {
|
||||
});
|
||||
provide(tuntapSymbol, tuntap);
|
||||
|
||||
const reg = /ios|android|windows|ubuntu|openwrt|armbian|archlinux|fedora|centos|rocky|alpine|debian|linux|docker/g;
|
||||
const reg = /google|huawei|xiaomi|ios|android|windows|ubuntu|openwrt|armbian|archlinux|fedora|centos|rocky|alpine|debian|linux|docker/g;
|
||||
|
||||
const _getTuntapInfo = () => {
|
||||
clearTimeout(tuntap.value.timer);
|
||||
|
||||
42
src/linker.web/src/views/full/install/Copy.vue
Normal file
42
src/linker.web/src/views/full/install/Copy.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<el-input v-model="state.content" type="textarea" :rows="10" resize="none"></el-input>
|
||||
</div>
|
||||
<div class="t-c mgt-1">
|
||||
<el-button type="primary" @click="handleSave">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { installCopy } from '@/apis/config';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
|
||||
const state = reactive({ content:'' })
|
||||
const handleSave = ()=>{
|
||||
if(!state.content) return;
|
||||
installCopy(state.content).then((res)=>{
|
||||
if(!res){
|
||||
ElMessage.error('保存失败,可能格式有误,无法解析');
|
||||
return;
|
||||
}
|
||||
ElMessage.success('保存成功');
|
||||
window.location.reload();
|
||||
}).catch(()=>{
|
||||
ElMessage.error('保存失败');
|
||||
})
|
||||
}
|
||||
return {
|
||||
state,handleSave
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
</style>
|
||||
@@ -2,93 +2,40 @@
|
||||
<div>
|
||||
<el-dialog v-model="state.show" title="初始化配置" width="600" top="2vh">
|
||||
<div>
|
||||
<div class="head">
|
||||
<el-steps :active="step.step" finish-status="success">
|
||||
<template v-for="(item,index) in state.steps">
|
||||
<el-step :title="item" />
|
||||
</template>
|
||||
</el-steps>
|
||||
</div>
|
||||
<div class="body">
|
||||
<el-card shadow="never" v-if="step.step == 1">
|
||||
<Common ref="currentDom"></Common>
|
||||
</el-card>
|
||||
<el-card shadow="never" v-if="step.step == 2">
|
||||
<Server ref="currentDom"></Server>
|
||||
</el-card>
|
||||
<el-card shadow="never" v-if="step.step == 3">
|
||||
<Client ref="currentDom"></Client>
|
||||
</el-card>
|
||||
<el-card shadow="never" v-if="step.step == 4">
|
||||
<div class="t-c">完成保存后,请重启软件</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="footer t-c">
|
||||
<el-button :disabled="step.step <= 1" @click="handlePrev">上一步</el-button>
|
||||
<el-button v-if="step.step < state.steps.length" type="primary" @click="handleNext">下一步</el-button>
|
||||
<el-button v-else type="primary" @click="handleSave">完成</el-button>
|
||||
</div>
|
||||
<el-tabs type="border-card">
|
||||
<el-tab-pane label="手动输入">
|
||||
<Input></Input>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="粘贴配置">
|
||||
<Copy></Copy>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="在线导入">
|
||||
<Save></Save>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { injectGlobalData } from '@/provide';
|
||||
import { install } from '@/apis/config';
|
||||
import { reactive, ref, provide, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import Common from './Common.vue'
|
||||
import Client from './Client.vue'
|
||||
import Server from './Server.vue'
|
||||
import { reactive} from 'vue';
|
||||
import Input from './Input.vue'
|
||||
import Copy from './Copy.vue'
|
||||
import Save from './Save.vue'
|
||||
export default {
|
||||
components: { Common,Client,Server },
|
||||
components: { Input,Copy,Save},
|
||||
setup(props) {
|
||||
|
||||
const globalData = injectGlobalData();
|
||||
const globalData = injectGlobalData();
|
||||
const state = reactive({
|
||||
show: globalData.value.config.Common.Install == false,
|
||||
steps:computed(()=>['选择模式',
|
||||
globalData.value.isPc ? '服务端' : '',
|
||||
'客户端',
|
||||
'完成'])
|
||||
});
|
||||
show: globalData.value.config.Common.Install == false
|
||||
});
|
||||
|
||||
const currentDom = ref(null);
|
||||
const step = ref({
|
||||
step:1,
|
||||
increment:1,
|
||||
json:{},
|
||||
form:{server:{},client:{},common:{}}
|
||||
});
|
||||
provide('step',step);
|
||||
const handlePrev = ()=>{
|
||||
step.value.step --;
|
||||
step.value.increment = -1;
|
||||
}
|
||||
const handleNext = ()=>{
|
||||
step.value.increment = 1;
|
||||
currentDom.value.handleValidate().then((json)=>{
|
||||
step.value.json = Object.assign(step.value.json,json.json);
|
||||
step.value.form = Object.assign(step.value.form,json.form);
|
||||
step.value.step ++;
|
||||
}).catch(()=>{
|
||||
});
|
||||
}
|
||||
const handleSave = ()=>{
|
||||
install(step.value.json).then(()=>{
|
||||
ElMessage.success('保存成功');
|
||||
}).catch(()=>{
|
||||
ElMessage.error('保存失败');
|
||||
})
|
||||
}
|
||||
|
||||
return { state,globalData,currentDom,step,handlePrev,handleNext,handleSave};
|
||||
return { state,globalData};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="stylus" scoped>
|
||||
.body{margin-top:1rem;}
|
||||
.footer{
|
||||
margin-top:2rem
|
||||
}
|
||||
|
||||
</style>
|
||||
90
src/linker.web/src/views/full/install/Input.vue
Normal file
90
src/linker.web/src/views/full/install/Input.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="head">
|
||||
<el-steps :active="step.step" finish-status="success">
|
||||
<template v-for="(item,index) in state.steps">
|
||||
<el-step :title="item" />
|
||||
</template>
|
||||
</el-steps>
|
||||
</div>
|
||||
<div class="body">
|
||||
<el-card shadow="never" v-if="step.step == 1">
|
||||
<Common ref="currentDom"></Common>
|
||||
</el-card>
|
||||
<el-card shadow="never" v-if="step.step == 2">
|
||||
<Server ref="currentDom"></Server>
|
||||
</el-card>
|
||||
<el-card shadow="never" v-if="step.step == 3">
|
||||
<Client ref="currentDom"></Client>
|
||||
</el-card>
|
||||
<el-card shadow="never" v-if="step.step == 4">
|
||||
<div class="t-c">完成保存后,请重启软件</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="footer t-c">
|
||||
<el-button :disabled="step.step <= 1" @click="handlePrev">上一步</el-button>
|
||||
<el-button v-if="step.step < state.steps.length" type="primary" @click="handleNext">下一步</el-button>
|
||||
<el-button v-else type="primary" @click="handleSave">完成</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { injectGlobalData } from '@/provide';
|
||||
import { install } from '@/apis/config';
|
||||
import { reactive, ref, provide, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import Common from './Common.vue'
|
||||
import Client from './Client.vue'
|
||||
import Server from './Server.vue'
|
||||
export default {
|
||||
components: { Common,Client,Server },
|
||||
setup(props) {
|
||||
|
||||
const globalData = injectGlobalData();
|
||||
const state = reactive({
|
||||
steps:computed(()=>['选择模式',
|
||||
globalData.value.isPc ? '服务端' : '',
|
||||
'客户端',
|
||||
'完成'])
|
||||
});
|
||||
|
||||
const currentDom = ref(null);
|
||||
const step = ref({
|
||||
step:1,
|
||||
increment:1,
|
||||
json:{},
|
||||
form:{server:{},client:{},common:{}}
|
||||
});
|
||||
provide('step',step);
|
||||
const handlePrev = ()=>{
|
||||
step.value.step --;
|
||||
step.value.increment = -1;
|
||||
}
|
||||
const handleNext = ()=>{
|
||||
step.value.increment = 1;
|
||||
currentDom.value.handleValidate().then((json)=>{
|
||||
step.value.json = Object.assign(step.value.json,json.json);
|
||||
step.value.form = Object.assign(step.value.form,json.form);
|
||||
step.value.step ++;
|
||||
}).catch(()=>{
|
||||
});
|
||||
}
|
||||
const handleSave = ()=>{
|
||||
install(step.value.json).then(()=>{
|
||||
ElMessage.success('保存成功');
|
||||
window.location.reload();
|
||||
}).catch(()=>{
|
||||
ElMessage.error('保存失败');
|
||||
})
|
||||
}
|
||||
|
||||
return { state,globalData,currentDom,step,handlePrev,handleNext,handleSave};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="stylus" scoped>
|
||||
.body{margin-top:1rem;}
|
||||
.footer{
|
||||
margin-top:2rem
|
||||
}
|
||||
</style>
|
||||
61
src/linker.web/src/views/full/install/Save.vue
Normal file
61
src/linker.web/src/views/full/install/Save.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form ref="formDom" :model="state.ruleForm" :rules="state.rules" label-width="auto">
|
||||
<el-form-item label="服务器" prop="server">
|
||||
<el-input v-model="state.ruleForm.server" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密钥" prop="value">
|
||||
<el-input v-model="state.ruleForm.value" />
|
||||
</el-form-item>
|
||||
<el-form-item label="" prop="Btns">
|
||||
<div class="t-c w-100">
|
||||
<el-button type="primary" @click="handleSave">确认</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { installSave } from '@/apis/config';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { reactive, ref } from 'vue';
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
|
||||
const state = reactive({
|
||||
ruleForm: {
|
||||
server: '',
|
||||
value:'',
|
||||
},
|
||||
rules: {
|
||||
server: [{ required: true, message: "必填", trigger: "blur" }],
|
||||
value: [{ required: true, message: "必填", trigger: "blur" }],
|
||||
}
|
||||
})
|
||||
const formDom = ref(null);
|
||||
const handleSave = ()=>{
|
||||
formDom.value.validate((valid) => {
|
||||
if (!valid) return;
|
||||
installSave(state.ruleForm).then((res)=>{
|
||||
if(!res){
|
||||
ElMessage.error('保存失败,可能服务器或者密钥不正确,或者密钥已被使用');
|
||||
return;
|
||||
}
|
||||
ElMessage.success('保存成功');
|
||||
window.location.reload();
|
||||
}).catch(()=>{
|
||||
ElMessage.error('保存失败');
|
||||
})
|
||||
});
|
||||
}
|
||||
return {
|
||||
state,formDom,handleSave
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
</style>
|
||||
@@ -24,25 +24,61 @@
|
||||
<span>{{$t('status.exportApiPassword')}} : </span><el-input type="password" show-password :disabled="onlyNode" v-model="state.apipassword" maxlength="36" show-word-limit style="width:15rem"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="8"><el-checkbox v-model="state.relay" :label="$t('status.exportRelay')" /></el-col>
|
||||
<el-col :span="8"><el-checkbox v-model="state.sforward" :label="$t('status.exportSForward')" /></el-col>
|
||||
<el-col :span="8"><el-checkbox v-model="state.updater" :label="$t('status.exportUpdater')" /></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="8"><el-checkbox v-model="state.server" :label="$t('status.exportServer')" /></el-col>
|
||||
<el-col :span="8"><el-checkbox v-model="state.group" :label="$t('status.exportGroup')" /></el-col>
|
||||
<el-col :span="8"><el-checkbox v-model="state.tunnel" :label="$t('status.exportTunnel')" /></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Access ref="accessDom" :machineid="machineId"></Access>
|
||||
</el-card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button plain @click="state.show = false" :loading="state.loading">{{$t('common.cancel') }}</el-button>
|
||||
<el-button type="success" plain @click="handleExport" :loading="state.loading">{{$t('common.confirm') }}</el-button>
|
||||
<el-button type="default" plain @click="handleExport" :loading="state.loading">{{$t('status.exportDownload') }}</el-button>
|
||||
<el-button type="info" plain @click="handleCopy" :loading="state.loading">{{$t('status.exportCopy') }}</el-button>
|
||||
<el-button type="success" plain @click="handleSave" :loading="state.loading">{{$t('status.exportSave') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog class="options-center" :title="$t('status.export')" destroy-on-close v-model="state.showCopy" center width="580" top="1vh">
|
||||
<div class="port-wrap">
|
||||
<el-input v-model="state.copyContent" type="textarea" :rows="10" resize="none" readonly></el-input>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button plain @click="copyToClipboard">{{$t('status.exportCopy') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog class="options-center" :title="$t('status.export')" destroy-on-close v-model="state.showSave" center width="300" top="1vh">
|
||||
<div class="port-wrap">
|
||||
<div>
|
||||
<el-input v-model="state.saveServer" readonly></el-input>
|
||||
</div>
|
||||
<div style="margin-top:1rem">
|
||||
<el-input v-model="state.saveContent" readonly></el-input>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button plain @click="copySaveToClipboard">{{$t('status.exportCopy') }}</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import {Share} from '@element-plus/icons-vue'
|
||||
import { exportConfig } from '@/apis/config';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { exportConfig,copyConfig,saveConfig } from '@/apis/config';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { injectGlobalData } from '@/provide';
|
||||
import Access from '@/views/full/devices/Access.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -62,6 +98,20 @@ export default {
|
||||
single:true,
|
||||
name:'',
|
||||
apipassword:onlyNode.value? globalData.value.config.Client.CApi.ApiPassword :'',
|
||||
|
||||
relay:true,
|
||||
sforward:true,
|
||||
updater:true,
|
||||
server:true,
|
||||
group:true,
|
||||
tunnel:true,
|
||||
|
||||
copyContent:'',
|
||||
showCopy:false,
|
||||
|
||||
saveServer:globalData.value.config.Client.Server.Host,
|
||||
saveContent:'',
|
||||
showSave:false
|
||||
});
|
||||
const accessDom = ref(null);
|
||||
|
||||
@@ -74,7 +124,13 @@ export default {
|
||||
access:accessDom.value.getValue(),
|
||||
single:state.single,
|
||||
name:state.name,
|
||||
apipassword:state.apipassword
|
||||
apipassword:state.apipassword,
|
||||
relay:state.relay,
|
||||
sforward:state.sforward,
|
||||
updater:state.updater,
|
||||
server:state.server,
|
||||
group:state.group,
|
||||
tunnel:state.tunnel,
|
||||
}
|
||||
|
||||
if(json.single){
|
||||
@@ -104,6 +160,37 @@ export default {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
const handleSave = ()=>{
|
||||
const json = getJson();
|
||||
if(!json){
|
||||
return;
|
||||
};
|
||||
state.loading = true;
|
||||
saveConfig(json).then((res)=>{
|
||||
state.loading = false;
|
||||
state.show = false;
|
||||
ElMessage.success(t('common.oper'));
|
||||
|
||||
state.saveContent = res;
|
||||
state.showSave = true;
|
||||
|
||||
}).catch(()=>{
|
||||
ElMessage.error(t('common.operFail'));
|
||||
state.loading = false;
|
||||
});
|
||||
}
|
||||
const copySaveToClipboard = async ()=>{
|
||||
try {
|
||||
await navigator.clipboard.writeText(`在初始化linker客户端时,填写服务器和密钥,导入配置\n服务器: ${state.saveServer}\n密钥: ${state.saveContent}`);
|
||||
ElMessage.success(t('common.oper'));
|
||||
return true;
|
||||
} catch (err) {
|
||||
ElMessage.error(t('common.operFail'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = ()=>{
|
||||
|
||||
const json = getJson();
|
||||
@@ -123,7 +210,35 @@ export default {
|
||||
state.loading = false;
|
||||
});
|
||||
}
|
||||
return {config:props.config,onlyNode,hasExport,machineId, state,accessDom,handleExport};
|
||||
const handleCopy = ()=>{
|
||||
const json = getJson();
|
||||
if(!json){
|
||||
return;
|
||||
};
|
||||
state.loading = true;
|
||||
copyConfig(json).then((res)=>{
|
||||
state.loading = false;
|
||||
state.show = false;
|
||||
ElMessage.success(t('common.oper'));
|
||||
state.copyContent = res;
|
||||
state.showCopy = true;
|
||||
}).catch(()=>{
|
||||
ElMessage.error(t('common.operFail'));
|
||||
state.loading = false;
|
||||
});
|
||||
}
|
||||
const copyToClipboard = async()=> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(state.copyContent);
|
||||
ElMessage.success(t('common.oper'));
|
||||
return true;
|
||||
} catch (err) {
|
||||
ElMessage.error(t('common.operFail'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return {config:props.config,onlyNode,hasExport,machineId, state,accessDom,handleSave,handleExport,handleCopy,copyToClipboard,copySaveToClipboard};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div class="head-wrap">
|
||||
<div class="tools flex">
|
||||
|
||||
<span class="label">服务器 </span><el-input v-model="state.server" readonly style="width: 14rem;" size="small"></el-input>
|
||||
<span class="label">分组 : {{state.group}}</span>
|
||||
<span class="flex-1"></span>
|
||||
<el-button size="small" @click="handleRefresh">
|
||||
刷新(F5)<el-icon><Refresh /></el-icon>
|
||||
@@ -25,6 +24,7 @@ export default {
|
||||
const globalData = injectGlobalData();
|
||||
const state = reactive({
|
||||
server:computed(()=>globalData.value.config.Client.Server.Host),
|
||||
group:computed(()=>globalData.value.config.Client.Group.Name),
|
||||
});
|
||||
const handleRefresh = ()=>{
|
||||
window.location.reload();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
v1.7.4
|
||||
2025-04-24 00:55:37
|
||||
2025-04-24 17:28:28
|
||||
1. 一些优化
|
||||
2. 内置应用层SNAT,用于无法使用系统NetNat的windows系统
|
||||
3. 如果你设备很多,请尝试升级其中一个成功重启后再升级其它
|
||||
Reference in New Issue
Block a user