mirror of
https://github.com/EasyTier/EasyTier.git
synced 2025-10-05 08:47:01 +08:00
Add conversion method from TomlConfigLoader to NetworkConfig to enhance configuration experience (#990)
Some checks failed
EasyTier Core / pre_job (push) Has been cancelled
EasyTier GUI / pre_job (push) Has been cancelled
EasyTier Mobile / pre_job (push) Has been cancelled
EasyTier Test / pre_job (push) Has been cancelled
EasyTier Core / build_web (push) Has been cancelled
EasyTier Core / build (freebsd-13.2-x86_64, 13.2, ubuntu-22.04, x86_64-unknown-freebsd) (push) Has been cancelled
EasyTier Core / build (linux-aarch64, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-arm, ubuntu-22.04, arm-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armhf, ubuntu-22.04, arm-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-armv7, ubuntu-22.04, armv7-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armv7hf, ubuntu-22.04, armv7-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-mips, ubuntu-22.04, mips-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-mipsel, ubuntu-22.04, mipsel-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (windows-arm64, windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / build (windows-i686, windows-latest, i686-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / core-result (push) Has been cancelled
EasyTier Core / magisk_build (push) Has been cancelled
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (windows-arm64, aarch64-pc-windows-msvc, windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / build-gui (windows-i686, i686-pc-windows-msvc, windows-latest, i686-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / gui-result (push) Has been cancelled
EasyTier Mobile / build-mobile (android, ubuntu-22.04, android) (push) Has been cancelled
EasyTier Mobile / mobile-result (push) Has been cancelled
EasyTier Test / test (push) Has been cancelled
Some checks failed
EasyTier Core / pre_job (push) Has been cancelled
EasyTier GUI / pre_job (push) Has been cancelled
EasyTier Mobile / pre_job (push) Has been cancelled
EasyTier Test / pre_job (push) Has been cancelled
EasyTier Core / build_web (push) Has been cancelled
EasyTier Core / build (freebsd-13.2-x86_64, 13.2, ubuntu-22.04, x86_64-unknown-freebsd) (push) Has been cancelled
EasyTier Core / build (linux-aarch64, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-arm, ubuntu-22.04, arm-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armhf, ubuntu-22.04, arm-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-armv7, ubuntu-22.04, armv7-unknown-linux-musleabi) (push) Has been cancelled
EasyTier Core / build (linux-armv7hf, ubuntu-22.04, armv7-unknown-linux-musleabihf) (push) Has been cancelled
EasyTier Core / build (linux-mips, ubuntu-22.04, mips-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-mipsel, ubuntu-22.04, mipsel-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier Core / build (macos-aarch64, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (macos-x86_64, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier Core / build (windows-arm64, windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / build (windows-i686, windows-latest, i686-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / build (windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier Core / core-result (push) Has been cancelled
EasyTier Core / magisk_build (push) Has been cancelled
EasyTier GUI / build-gui (linux-aarch64, aarch64-unknown-linux-gnu, ubuntu-22.04, aarch64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (linux-x86_64, x86_64-unknown-linux-gnu, ubuntu-22.04, x86_64-unknown-linux-musl) (push) Has been cancelled
EasyTier GUI / build-gui (macos-aarch64, aarch64-apple-darwin, macos-latest, aarch64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (macos-x86_64, x86_64-apple-darwin, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
EasyTier GUI / build-gui (windows-arm64, aarch64-pc-windows-msvc, windows-latest, aarch64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / build-gui (windows-i686, i686-pc-windows-msvc, windows-latest, i686-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / build-gui (windows-x86_64, x86_64-pc-windows-msvc, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
EasyTier GUI / gui-result (push) Has been cancelled
EasyTier Mobile / build-mobile (android, ubuntu-22.04, android) (push) Has been cancelled
EasyTier Mobile / mobile-result (push) Has been cancelled
EasyTier Test / test (push) Has been cancelled
* add method to create NetworkConfig from TomlConfigLoader * allow web export/import toml config file and gui edit toml config * Extract the configuration file dialog into a separate component and allow direct editing of the configuration file on the web
This commit is contained in:
@@ -50,7 +50,11 @@ dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名
|
|||||||
off_text: 点击关闭
|
off_text: 点击关闭
|
||||||
on_text: 点击开启
|
on_text: 点击开启
|
||||||
show_config: 显示配置
|
show_config: 显示配置
|
||||||
|
edit_config: 编辑配置文件
|
||||||
close: 关闭
|
close: 关闭
|
||||||
|
save: 保存
|
||||||
|
config_saved: 配置已保存
|
||||||
|
|
||||||
|
|
||||||
use_latency_first: 延迟优先模式
|
use_latency_first: 延迟优先模式
|
||||||
my_node_info: 当前节点信息
|
my_node_info: 当前节点信息
|
||||||
|
@@ -51,7 +51,10 @@ dev_name_placeholder: 'Note: When multiple networks use the same TUN interface n
|
|||||||
off_text: Press to disable
|
off_text: Press to disable
|
||||||
on_text: Press to enable
|
on_text: Press to enable
|
||||||
show_config: Show Config
|
show_config: Show Config
|
||||||
|
edit_config: Edit Config File
|
||||||
close: Close
|
close: Close
|
||||||
|
save: Save
|
||||||
|
config_saved: Configuration saved
|
||||||
my_node_info: My Node Info
|
my_node_info: My Node Info
|
||||||
peer_count: Connected
|
peer_count: Connected
|
||||||
upload: Upload
|
upload: Upload
|
||||||
|
@@ -4,11 +4,9 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::config::{
|
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
|
||||||
ConfigLoader, FileLoggerConfig, LoggingConfigBuilder,
|
|
||||||
},
|
|
||||||
launcher::{ConfigSource, NetworkConfig, NetworkInstanceRunningInfo},
|
|
||||||
instance_manager::NetworkInstanceManager,
|
instance_manager::NetworkInstanceManager,
|
||||||
|
launcher::{ConfigSource, NetworkConfig, NetworkInstanceRunningInfo},
|
||||||
utils::{self, NewFilterSender},
|
utils::{self, NewFilterSender},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,6 +42,13 @@ fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
|||||||
Ok(toml.dump())
|
Ok(toml.dump())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn generate_network_config(toml_config: String) -> Result<NetworkConfig, String> {
|
||||||
|
let config = TomlConfigLoader::new_from_str(&toml_config).map_err(|e| e.to_string())?;
|
||||||
|
let cfg = NetworkConfig::new_from_config(&config).map_err(|e| e.to_string())?;
|
||||||
|
Ok(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
|
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
|
||||||
let instance_id = cfg.instance_id().to_string();
|
let instance_id = cfg.instance_id().to_string();
|
||||||
@@ -226,6 +231,7 @@ pub fn run() {
|
|||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
parse_network_config,
|
parse_network_config,
|
||||||
|
generate_network_config,
|
||||||
run_network_instance,
|
run_network_instance,
|
||||||
retain_network_instance,
|
retain_network_instance,
|
||||||
collect_network_infos,
|
collect_network_infos,
|
||||||
|
@@ -8,5 +8,6 @@ onBeforeMount(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Toast position="bottom-right" />
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</template>
|
</template>
|
||||||
|
2
easytier-gui/src/auto-imports.d.ts
vendored
2
easytier-gui/src/auto-imports.d.ts
vendored
@@ -23,6 +23,7 @@ declare global {
|
|||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
const event2human: typeof import('./composables/utils')['event2human']
|
const event2human: typeof import('./composables/utils')['event2human']
|
||||||
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
|
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
|
||||||
|
const generateNetworkConfig: typeof import('./composables/network')['generateNetworkConfig']
|
||||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
@@ -134,6 +135,7 @@ declare module 'vue' {
|
|||||||
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
|
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
|
||||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||||
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
|
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
|
||||||
|
readonly generateNetworkConfig: UnwrapRef<typeof import('./composables/network')['generateNetworkConfig']>
|
||||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||||
|
@@ -8,6 +8,10 @@ export async function parseNetworkConfig(cfg: NetworkConfig) {
|
|||||||
return invoke<string>('parse_network_config', { cfg })
|
return invoke<string>('parse_network_config', { cfg })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function generateNetworkConfig(tomlConfig: string) {
|
||||||
|
return invoke<NetworkConfig>('generate_network_config', { tomlConfig })
|
||||||
|
}
|
||||||
|
|
||||||
export async function runNetworkInstance(cfg: NetworkConfig) {
|
export async function runNetworkInstance(cfg: NetworkConfig) {
|
||||||
return invoke('run_network_instance', { cfg })
|
return invoke('run_network_instance', { cfg })
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ import { exit } from '@tauri-apps/plugin-process'
|
|||||||
import { open } from '@tauri-apps/plugin-shell'
|
import { open } from '@tauri-apps/plugin-shell'
|
||||||
import TieredMenu from 'primevue/tieredmenu'
|
import TieredMenu from 'primevue/tieredmenu'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
import { NetworkTypes, Config, Status, Utils, I18nUtils } from 'easytier-frontend-lib'
|
import { NetworkTypes, Config, Status, Utils, I18nUtils, ConfigEditDialog } from 'easytier-frontend-lib'
|
||||||
|
|
||||||
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
import { isAutostart, setLoggingLevel } from '~/composables/network'
|
||||||
import { useTray } from '~/composables/tray'
|
import { useTray } from '~/composables/tray'
|
||||||
@@ -23,7 +23,7 @@ useTray(true)
|
|||||||
|
|
||||||
const items = ref([
|
const items = ref([
|
||||||
{
|
{
|
||||||
label: () => t('show_config'),
|
label: () => activeStep.value == "2" ? t('show_config') : t('edit_config'),
|
||||||
icon: 'pi pi-file-edit',
|
icon: 'pi pi-file-edit',
|
||||||
command: async () => {
|
command: async () => {
|
||||||
try {
|
try {
|
||||||
@@ -262,6 +262,13 @@ onMounted(async () => {
|
|||||||
function isRunning(id: string) {
|
function isRunning(id: string) {
|
||||||
return networkStore.networkInstanceIds.includes(id)
|
return networkStore.networkInstanceIds.includes(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveTomlConfig(tomlConfig: string) {
|
||||||
|
const config = await generateNetworkConfig(tomlConfig)
|
||||||
|
networkStore.replaceCurNetwork(config);
|
||||||
|
toast.add({ severity: 'success', detail: t('config_saved'), life: 3000 })
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -269,17 +276,8 @@ function isRunning(id: string) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="root" class="flex flex-col">
|
<div id="root" class="flex flex-col">
|
||||||
<Dialog v-model:visible="visible" modal header="Config File" :style="{ width: '70%' }">
|
<ConfigEditDialog v-model:visible="visible" :cur-network="curNetworkConfig" :readonly="activeStep !== '1'"
|
||||||
<Panel>
|
:save-config="saveTomlConfig" :generate-config="parseNetworkConfig" />
|
||||||
<ScrollPanel style="width: 100%; height: 300px">
|
|
||||||
<pre>{{ tomlConfig }}</pre>
|
|
||||||
</ScrollPanel>
|
|
||||||
</Panel>
|
|
||||||
<Divider />
|
|
||||||
<div class="flex gap-2 justify-end">
|
|
||||||
<Button type="button" :label="t('close')" @click="visible = false" />
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
|
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
|
||||||
<About />
|
<About />
|
||||||
|
@@ -48,6 +48,12 @@ export const useNetworkStore = defineStore('networkStore', {
|
|||||||
this.curNetwork = this.networkList[nextCurNetworkIdx]
|
this.curNetwork = this.networkList[nextCurNetworkIdx]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
replaceCurNetwork(cfg: NetworkTypes.NetworkConfig) {
|
||||||
|
const curNetworkIdx = this.networkList.indexOf(this.curNetwork)
|
||||||
|
this.networkList[curNetworkIdx] = cfg
|
||||||
|
this.curNetwork = cfg
|
||||||
|
},
|
||||||
|
|
||||||
removeNetworkInstance(instanceId: string) {
|
removeNetworkInstance(instanceId: string) {
|
||||||
delete this.instances[instanceId]
|
delete this.instances[instanceId]
|
||||||
},
|
},
|
||||||
|
103
easytier-web/frontend-lib/src/components/ConfigEditDialog.vue
Normal file
103
easytier-web/frontend-lib/src/components/ConfigEditDialog.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import { NetworkConfig } from '../types/network';
|
||||||
|
import { Divider, Button, Dialog, Textarea } from 'primevue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
generateConfig: {
|
||||||
|
type: Object as () => (config: NetworkConfig) => Promise<string>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
saveConfig: {
|
||||||
|
type: Object as () => (config: string) => Promise<void>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const curNetwork = defineModel('curNetwork', {
|
||||||
|
type: Object as () => NetworkConfig | undefined,
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const visible = defineModel('visible', {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
watch([visible, curNetwork], async ([newVisible, newCurNetwork]) => {
|
||||||
|
if (!newVisible) {
|
||||||
|
tomlConfig.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!newCurNetwork) {
|
||||||
|
tomlConfig.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const config = newCurNetwork;
|
||||||
|
try {
|
||||||
|
errorMessage.value = '';
|
||||||
|
tomlConfig.value = await props.generateConfig(config);
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage.value = 'Failed to generate config: ' + (e instanceof Error ? e.message : String(e));
|
||||||
|
tomlConfig.value = '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!visible.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!curNetwork.value) {
|
||||||
|
tomlConfig.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const config = curNetwork.value;
|
||||||
|
try {
|
||||||
|
tomlConfig.value = await props.generateConfig(config);
|
||||||
|
errorMessage.value = '';
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage.value = 'Failed to generate config: ' + (e instanceof Error ? e.message : String(e));
|
||||||
|
tomlConfig.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleConfigSave = async () => {
|
||||||
|
if (props.readonly) return;
|
||||||
|
try {
|
||||||
|
await props.saveConfig(tomlConfig.value);
|
||||||
|
visible.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage.value = 'Failed to save config: ' + (e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tomlConfig = ref<string>('')
|
||||||
|
const tomlConfigRows = ref<number>(1);
|
||||||
|
const errorMessage = ref<string>('');
|
||||||
|
|
||||||
|
watch(tomlConfig, (newValue) => {
|
||||||
|
tomlConfigRows.value = newValue.split('\n').length;
|
||||||
|
errorMessage.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:visible="visible" modal :header="t('config_file')" :style="{ width: '70%' }">
|
||||||
|
<pre v-if="errorMessage"
|
||||||
|
class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
|
||||||
|
<div class="flex w-full" style="max-height: 60vh; overflow-y: auto;">
|
||||||
|
<Textarea v-model="tomlConfig" class="w-full h-full font-mono flex flex-col resize-none" :rows="tomlConfigRows"
|
||||||
|
spellcheck="false" :readonly="props.readonly"></Textarea>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<Button v-if="!props.readonly" type="button" :label="t('save')" @click="handleConfigSave" />
|
||||||
|
<Button type="button" :label="t('close')" @click="visible = false" />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
@@ -1,2 +1,3 @@
|
|||||||
export { default as Config } from './Config.vue';
|
export { default as Config } from './Config.vue';
|
||||||
export { default as Status } from './Status.vue';
|
export { default as Status } from './Status.vue';
|
||||||
|
export { default as ConfigEditDialog } from './ConfigEditDialog.vue';
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
import type { App } from 'vue';
|
import type { App } from 'vue';
|
||||||
import { Config, Status } from "./components";
|
import { Config, Status, ConfigEditDialog } from "./components";
|
||||||
import Aura from '@primevue/themes/aura'
|
import Aura from '@primevue/themes/aura'
|
||||||
import PrimeVue from 'primevue/config'
|
import PrimeVue from 'primevue/config'
|
||||||
|
|
||||||
@@ -41,10 +41,11 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.component('Config', Config);
|
app.component('Config', Config);
|
||||||
|
app.component('ConfigEditDialog', ConfigEditDialog);
|
||||||
app.component('Status', Status);
|
app.component('Status', Status);
|
||||||
app.component('HumanEvent', HumanEvent);
|
app.component('HumanEvent', HumanEvent);
|
||||||
app.directive('tooltip', vTooltip as any);
|
app.directive('tooltip', vTooltip as any);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Config, Status, I18nUtils, NetworkTypes, Api, Utils };
|
export { Config, ConfigEditDialog, Status, I18nUtils, NetworkTypes, Api, Utils };
|
||||||
|
@@ -51,7 +51,11 @@ dev_name_placeholder: 注意:当多个网络同时使用相同的TUN接口名
|
|||||||
off_text: 点击关闭
|
off_text: 点击关闭
|
||||||
on_text: 点击开启
|
on_text: 点击开启
|
||||||
show_config: 显示配置
|
show_config: 显示配置
|
||||||
|
edit_config: 编辑配置文件
|
||||||
|
config_file: 配置文件
|
||||||
close: 关闭
|
close: 关闭
|
||||||
|
save: 保存
|
||||||
|
config_saved: 配置已保存
|
||||||
|
|
||||||
use_latency_first: 延迟优先模式
|
use_latency_first: 延迟优先模式
|
||||||
my_node_info: 当前节点信息
|
my_node_info: 当前节点信息
|
||||||
|
@@ -52,7 +52,11 @@ dev_name_placeholder: 'Note: When multiple networks use the same TUN interface n
|
|||||||
off_text: Press to disable
|
off_text: Press to disable
|
||||||
on_text: Press to enable
|
on_text: Press to enable
|
||||||
show_config: Show Config
|
show_config: Show Config
|
||||||
|
edit_config: Edit Config File
|
||||||
|
config_file: Config File
|
||||||
close: Close
|
close: Close
|
||||||
|
save: Save
|
||||||
|
config_saved: Configuration saved
|
||||||
my_node_info: My Node Info
|
my_node_info: My Node Info
|
||||||
peer_count: Connected
|
peer_count: Connected
|
||||||
upload: Upload
|
upload: Upload
|
||||||
|
@@ -47,6 +47,15 @@ export interface GenerateConfigResponse {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ParseConfigRequest {
|
||||||
|
toml_config: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParseConfigResponse {
|
||||||
|
config?: NetworkConfig;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class ApiClient {
|
export class ApiClient {
|
||||||
private client: AxiosInstance;
|
private client: AxiosInstance;
|
||||||
private authFailedCb: Function | undefined;
|
private authFailedCb: Function | undefined;
|
||||||
@@ -215,6 +224,18 @@ export class ApiClient {
|
|||||||
return { error: 'Unknown error: ' + error };
|
return { error: 'Unknown error: ' + error };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async parse_config(config: ParseConfigRequest): Promise<ParseConfigResponse> {
|
||||||
|
try {
|
||||||
|
const response = await this.client.post<any, ParseConfigResponse>('/parse-config', config);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
return { error: error.response?.data };
|
||||||
|
}
|
||||||
|
return { error: 'Unknown error: ' + error };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApiClient;
|
export default ApiClient;
|
@@ -2,12 +2,11 @@
|
|||||||
import { NetworkTypes } from 'easytier-frontend-lib';
|
import { NetworkTypes } from 'easytier-frontend-lib';
|
||||||
import {computed, ref} from 'vue';
|
import {computed, ref} from 'vue';
|
||||||
import { Api } from 'easytier-frontend-lib'
|
import { Api } from 'easytier-frontend-lib'
|
||||||
import {AutoComplete, Divider} from "primevue";
|
import {AutoComplete, Divider, Button, Textarea} from "primevue";
|
||||||
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
|
import {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} from "../modules/api-host"
|
||||||
|
|
||||||
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
const api = computed<Api.ApiClient>(() => new Api.ApiClient(apiHost.value));
|
||||||
|
|
||||||
|
|
||||||
const apiHost = ref<string>(getInitialApiHost())
|
const apiHost = ref<string>(getInitialApiHost())
|
||||||
const apiHostSuggestions = ref<Array<string>>([])
|
const apiHostSuggestions = ref<Array<string>>([])
|
||||||
const apiHostSearch = async (event: { query: string }) => {
|
const apiHostSearch = async (event: { query: string }) => {
|
||||||
@@ -22,23 +21,46 @@ const apiHostSearch = async (event: { query: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||||
const toml_config = ref<string>("Press 'Run Network' to generate TOML configuration");
|
const toml_config = ref<string>("");
|
||||||
|
const errorMessage = ref<string>("");
|
||||||
|
|
||||||
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
||||||
saveApiHost(apiHost.value)
|
saveApiHost(apiHost.value)
|
||||||
|
errorMessage.value = "";
|
||||||
api.value?.generate_config({
|
api.value?.generate_config({
|
||||||
config: config
|
config: config
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
toml_config.value = res.error;
|
errorMessage.value = "Generation failed: " + res.error;
|
||||||
} else if (res.toml_config) {
|
} else if (res.toml_config) {
|
||||||
toml_config.value = res.toml_config;
|
toml_config.value = res.toml_config;
|
||||||
} else {
|
} else {
|
||||||
toml_config.value = "Api server returned an unexpected response";
|
errorMessage.value = "Api server returned an unexpected response";
|
||||||
}
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
errorMessage.value = "Generate request failed: " + (err instanceof Error ? err.message : String(err));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseConfig = async () => {
|
||||||
|
try {
|
||||||
|
errorMessage.value = "";
|
||||||
|
const res = await api.value?.parse_config({
|
||||||
|
toml_config: toml_config.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
errorMessage.value = "Parse failed: " + res.error;
|
||||||
|
} else if (res.config) {
|
||||||
|
newNetworkConfig.value = res.config;
|
||||||
|
} else {
|
||||||
|
errorMessage.value = "API returned an unexpected response";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage.value = "Parse request failed: " + (e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -55,8 +77,17 @@ const generateConfig = (config: NetworkTypes.NetworkConfig) => {
|
|||||||
</div>
|
</div>
|
||||||
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
|
<Config :cur-network="newNetworkConfig" @run-network="generateConfig" />
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:w-full md:w-1/2 p-4 bg-gray-100">
|
<div class="sm:w-full md:w-1/2 p-4 flex flex-col h-[calc(100vh-80px)]">
|
||||||
<pre class="whitespace-pre-wrap">{{ toml_config }}</pre>
|
<pre v-if="errorMessage" class="mb-2 p-2 rounded text-sm overflow-auto bg-red-100 text-red-700 max-h-40">{{ errorMessage }}</pre>
|
||||||
|
<Textarea
|
||||||
|
v-model="toml_config"
|
||||||
|
spellcheck="false"
|
||||||
|
class="w-full flex-grow p-2 bg-gray-100 whitespace-pre-wrap font-mono border-none focus:outline-none resize-none"
|
||||||
|
placeholder="Press 'Run Network' to generate TOML configuration, or paste your TOML configuration here to parse it"
|
||||||
|
></Textarea>
|
||||||
|
<div class="mt-3 flex justify-center">
|
||||||
|
<Button label="Parse Config" icon="pi pi-arrow-left" icon-pos="left" @click="parseConfig" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast, Divider } from 'primevue';
|
import { Toolbar, IftaLabel, Select, Button, ConfirmPopup, Dialog, useConfirm, useToast, Divider } from 'primevue';
|
||||||
import { NetworkTypes, Status, Utils, Api, } from 'easytier-frontend-lib';
|
import { NetworkTypes, Status, Utils, Api, ConfigEditDialog } from 'easytier-frontend-lib';
|
||||||
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
|
import { watch, computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ const curNetworkInfo = ref<NetworkTypes.NetworkInstance | null>(null);
|
|||||||
|
|
||||||
const isEditing = ref(false);
|
const isEditing = ref(false);
|
||||||
const showCreateNetworkDialog = ref(false);
|
const showCreateNetworkDialog = ref(false);
|
||||||
|
const showConfigEditDialog = ref(false);
|
||||||
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
const newNetworkConfig = ref<NetworkTypes.NetworkConfig>(NetworkTypes.DEFAULT_NETWORK_CONFIG());
|
||||||
|
|
||||||
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
|
const listInstanceIdResponse = ref<Api.ListNetworkInstanceIdResponse | undefined>(undefined);
|
||||||
@@ -103,7 +104,12 @@ const updateNetworkState = async (disabled: boolean) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (disabled || !disabledNetworkConfig.value) {
|
||||||
await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled);
|
await props.api?.update_device_instance_state(deviceId.value, selectedInstanceId.value.uuid, disabled);
|
||||||
|
} else if (disabledNetworkConfig.value) {
|
||||||
|
await props.api?.delete_network(deviceId.value, disabledNetworkConfig.value.instance_id);
|
||||||
|
await props.api?.run_network(deviceId.value, disabledNetworkConfig.value);
|
||||||
|
}
|
||||||
await loadNetworkInstanceIds();
|
await loadNetworkInstanceIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,9 +223,15 @@ const exportConfig = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let ret = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
let networkConfig = await props.api?.get_network_config(deviceId.value, instanceId.value);
|
||||||
delete ret.instance_id;
|
delete networkConfig.instance_id;
|
||||||
exportJsonFile(JSON.stringify(ret, null, 2),instanceId.value +'.json');
|
let { toml_config: tomlConfig, error } = await props.api?.generate_config({
|
||||||
|
config: networkConfig
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
throw { response: { data: error } };
|
||||||
|
}
|
||||||
|
exportTomlFile(tomlConfig ?? '', instanceId.value + '.toml');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to export network config, error: ' + JSON.stringify(e.response.data), life: 2000 });
|
||||||
@@ -234,30 +246,34 @@ const importConfig = () => {
|
|||||||
const handleFileUpload = (event: Event) => {
|
const handleFileUpload = (event: Event) => {
|
||||||
const files = (event.target as HTMLInputElement).files;
|
const files = (event.target as HTMLInputElement).files;
|
||||||
const file = files ? files[0] : null;
|
const file = files ? files[0] : null;
|
||||||
if (file) {
|
if (!file) return;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = async (e) => {
|
||||||
try {
|
try {
|
||||||
let str = e.target?.result?.toString();
|
let tomlConfig = e.target?.result?.toString();
|
||||||
if(str){
|
if (!tomlConfig) return;
|
||||||
const config = JSON.parse(str);
|
const resp = await props.api?.parse_config({ toml_config: tomlConfig });
|
||||||
if(config === null || typeof config !== "object"){
|
if (resp.error) {
|
||||||
throw new Error();
|
throw resp.error;
|
||||||
}
|
}
|
||||||
Object.assign(newNetworkConfig.value, config);
|
|
||||||
|
const config = resp.config;
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
config.instance_id = newNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||||
|
|
||||||
|
Object.assign(newNetworkConfig.value, resp.config);
|
||||||
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
|
toast.add({ severity: 'success', summary: 'Import Success', detail: "Config file import success", life: 2000 });
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error.', life: 2000 });
|
toast.add({ severity: 'error', summary: 'Error', detail: 'Config file parse error: ' + error, life: 2000 });
|
||||||
}
|
}
|
||||||
configFile.value.value = null;
|
configFile.value.value = null;
|
||||||
}
|
}
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const exportJsonFile = (context: string, name: string) => {
|
const exportTomlFile = (context: string, name: string) => {
|
||||||
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/json' }));
|
let url = window.URL.createObjectURL(new Blob([context], { type: 'application/toml' }));
|
||||||
let link = document.createElement('a');
|
let link = document.createElement('a');
|
||||||
link.style.display = 'none';
|
link.style.display = 'none';
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@@ -269,6 +285,31 @@ const exportJsonFile = (context: string, name: string) => {
|
|||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generateConfig = async (config: NetworkTypes.NetworkConfig): Promise<string> => {
|
||||||
|
let { toml_config: tomlConfig, error } = await props.api?.generate_config({ config });
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return tomlConfig ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveConfig = async (tomlConfig: string): Promise<void> => {
|
||||||
|
let resp = await props.api?.parse_config({ toml_config: tomlConfig });
|
||||||
|
if (resp.error) {
|
||||||
|
throw resp.error;
|
||||||
|
};
|
||||||
|
const config = resp.config;
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("Parsed config is empty");
|
||||||
|
}
|
||||||
|
config.instance_id = disabledNetworkConfig.value?.instance_id ?? config?.instance_id;
|
||||||
|
if (networkIsDisabled.value) {
|
||||||
|
disabledNetworkConfig.value = config;
|
||||||
|
} else {
|
||||||
|
newNetworkConfig.value = config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
let periodFunc = new Utils.PeriodicTask(async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
|
await Promise.all([loadNetworkInstanceIds(), loadDeviceInfo()]);
|
||||||
@@ -288,18 +329,23 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<input type="file" @change="handleFileUpload" class="hidden" accept="application/json" ref="configFile"/>
|
<input type="file" @change="handleFileUpload" class="hidden" accept="application/toml" ref="configFile" />
|
||||||
<ConfirmPopup></ConfirmPopup>
|
<ConfirmPopup></ConfirmPopup>
|
||||||
<Dialog v-model:visible="showCreateNetworkDialog" modal :header="!isEditing ? 'Create New Network' : 'Edit Network'"
|
<Dialog v-if="!networkIsDisabled" v-model:visible="showCreateNetworkDialog" modal
|
||||||
:style="{ width: '55rem' }">
|
:header="!isEditing ? 'Create New Network' : 'Edit Network'" :style="{ width: '55rem' }">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="w-11/12 self-center ">
|
<div class="w-11/12 self-center space-x-2">
|
||||||
|
<Button @click="showConfigEditDialog = true" icon="pi pi-pen-to-square" label="Edit File" iconPos="right" />
|
||||||
<Button @click="importConfig" icon="pi pi-file-import" label="Import" iconPos="right" />
|
<Button @click="importConfig" icon="pi pi-file-import" label="Import" iconPos="right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
<Config :cur-network="newNetworkConfig" @run-network="createNewNetwork"></Config>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<ConfigEditDialog v-if="networkIsDisabled" v-model:visible="showCreateNetworkDialog"
|
||||||
|
:cur-network="disabledNetworkConfig" :generate-config="generateConfig" :save-config="saveConfig" />
|
||||||
|
<ConfigEditDialog v-else v-model:visible="showConfigEditDialog" :cur-network="newNetworkConfig"
|
||||||
|
:generate-config="generateConfig" :save-config="saveConfig" />
|
||||||
|
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<template #start>
|
<template #start>
|
||||||
|
@@ -11,7 +11,7 @@ use axum::{extract::State, routing::get, Json, Router};
|
|||||||
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
use axum_login::tower_sessions::{ExpiredDeletion, SessionManagerLayer};
|
||||||
use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
|
use axum_login::{login_required, AuthManagerLayerBuilder, AuthUser, AuthzBackend};
|
||||||
use axum_messages::MessagesManagerLayer;
|
use axum_messages::MessagesManagerLayer;
|
||||||
use easytier::common::config::ConfigLoader;
|
use easytier::common::config::{ConfigLoader, TomlConfigLoader};
|
||||||
use easytier::common::scoped_task::ScopedTask;
|
use easytier::common::scoped_task::ScopedTask;
|
||||||
use easytier::launcher::NetworkConfig;
|
use easytier::launcher::NetworkConfig;
|
||||||
use easytier::proto::rpc_types;
|
use easytier::proto::rpc_types;
|
||||||
@@ -68,6 +68,17 @@ struct GenerateConfigResponse {
|
|||||||
toml_config: Option<String>,
|
toml_config: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
struct ParseConfigRequest {
|
||||||
|
toml_config: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
struct ParseConfigResponse {
|
||||||
|
error: Option<String>,
|
||||||
|
config: Option<NetworkConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Error {
|
pub struct Error {
|
||||||
message: String,
|
message: String,
|
||||||
@@ -158,6 +169,25 @@ impl RestfulServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_parse_config(
|
||||||
|
Json(req): Json<ParseConfigRequest>,
|
||||||
|
) -> Result<Json<ParseConfigResponse>, HttpHandleError> {
|
||||||
|
let config = TomlConfigLoader::new_from_str(&req.toml_config)
|
||||||
|
.and_then(|config| NetworkConfig::new_from_config(&config));
|
||||||
|
match config {
|
||||||
|
Ok(c) => Ok(ParseConfigResponse {
|
||||||
|
error: None,
|
||||||
|
config: Some(c),
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
Err(e) => Ok(ParseConfigResponse {
|
||||||
|
error: Some(format!("{:?}", e)),
|
||||||
|
config: None,
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
mut self,
|
mut self,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
@@ -216,6 +246,7 @@ impl RestfulServer {
|
|||||||
"/api/v1/generate-config",
|
"/api/v1/generate-config",
|
||||||
post(Self::handle_generate_config),
|
post(Self::handle_generate_config),
|
||||||
)
|
)
|
||||||
|
.route("/api/v1/parse-config", post(Self::handle_parse_config))
|
||||||
.layer(MessagesManagerLayer)
|
.layer(MessagesManagerLayer)
|
||||||
.layer(auth_layer)
|
.layer(auth_layer)
|
||||||
.layer(tower_http::cors::CorsLayer::very_permissive())
|
.layer(tower_http::cors::CorsLayer::very_permissive())
|
||||||
|
@@ -752,4 +752,396 @@ impl NetworkConfig {
|
|||||||
cfg.set_flags(flags);
|
cfg.set_flags(flags);
|
||||||
Ok(cfg)
|
Ok(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_from_config(config: &TomlConfigLoader) -> Result<Self, anyhow::Error> {
|
||||||
|
let default_config = TomlConfigLoader::default();
|
||||||
|
|
||||||
|
let mut result = Self::default();
|
||||||
|
|
||||||
|
result.instance_id = Some(config.get_id().to_string());
|
||||||
|
if config.get_hostname() != default_config.get_hostname() {
|
||||||
|
result.hostname = Some(config.get_hostname());
|
||||||
|
}
|
||||||
|
|
||||||
|
result.dhcp = Some(config.get_dhcp());
|
||||||
|
|
||||||
|
let network_identity = config.get_network_identity();
|
||||||
|
result.network_name = Some(network_identity.network_name.clone());
|
||||||
|
result.network_secret = network_identity.network_secret.clone();
|
||||||
|
|
||||||
|
if let Some(ipv4) = config.get_ipv4() {
|
||||||
|
result.virtual_ipv4 = Some(ipv4.address().to_string());
|
||||||
|
result.network_length = Some(ipv4.network_length() as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
let peers = config.get_peers();
|
||||||
|
match peers.len() {
|
||||||
|
1 => {
|
||||||
|
result.networking_method = Some(NetworkingMethod::PublicServer as i32);
|
||||||
|
result.public_server_url = Some(peers[0].uri.to_string());
|
||||||
|
}
|
||||||
|
0 => {
|
||||||
|
result.networking_method = Some(NetworkingMethod::Standalone as i32);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
result.networking_method = Some(NetworkingMethod::Manual as i32);
|
||||||
|
result.peer_urls = peers.iter().map(|p| p.uri.to_string()).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.listener_urls = config
|
||||||
|
.get_listeners()
|
||||||
|
.unwrap_or_else(|| vec![])
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
result.proxy_cidrs = config
|
||||||
|
.get_proxy_cidrs()
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
if let Some(mapped) = c.mapped_cidr {
|
||||||
|
format!("{}->{}", c.cidr, mapped)
|
||||||
|
} else {
|
||||||
|
c.cidr.to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(rpc_portal) = config.get_rpc_portal() {
|
||||||
|
result.rpc_port = Some(rpc_portal.port() as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(whitelist) = config.get_rpc_portal_whitelist() {
|
||||||
|
result.rpc_portal_whitelists = whitelist.iter().map(|w| w.to_string()).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(vpn_config) = config.get_vpn_portal_config() {
|
||||||
|
result.enable_vpn_portal = Some(true);
|
||||||
|
|
||||||
|
let cidr = vpn_config.client_cidr;
|
||||||
|
result.vpn_portal_client_network_addr = Some(cidr.first_address().to_string());
|
||||||
|
result.vpn_portal_client_network_len = Some(cidr.network_length() as i32);
|
||||||
|
|
||||||
|
result.vpn_portal_listen_port = Some(vpn_config.wireguard_listen.port() as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(routes) = config.get_routes() {
|
||||||
|
if !routes.is_empty() {
|
||||||
|
result.enable_manual_routes = Some(true);
|
||||||
|
result.routes = routes.iter().map(|r| r.to_string()).collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let exit_nodes = config.get_exit_nodes();
|
||||||
|
if !exit_nodes.is_empty() {
|
||||||
|
result.exit_nodes = exit_nodes.iter().map(|n| n.to_string()).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(socks5_portal) = config.get_socks5_portal() {
|
||||||
|
result.enable_socks5 = Some(true);
|
||||||
|
result.socks5_port = socks5_portal.port().map(|p| p as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mapped_listeners = config.get_mapped_listeners();
|
||||||
|
if !mapped_listeners.is_empty() {
|
||||||
|
result.mapped_listeners = mapped_listeners.iter().map(|l| l.to_string()).collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let flags = config.get_flags();
|
||||||
|
result.latency_first = Some(flags.latency_first);
|
||||||
|
result.dev_name = Some(flags.dev_name.clone());
|
||||||
|
result.use_smoltcp = Some(flags.use_smoltcp);
|
||||||
|
result.enable_kcp_proxy = Some(flags.enable_kcp_proxy);
|
||||||
|
result.disable_kcp_input = Some(flags.disable_kcp_input);
|
||||||
|
result.enable_quic_proxy = Some(flags.enable_quic_proxy);
|
||||||
|
result.disable_quic_input = Some(flags.disable_quic_input);
|
||||||
|
result.disable_p2p = Some(flags.disable_p2p);
|
||||||
|
result.bind_device = Some(flags.bind_device);
|
||||||
|
result.no_tun = Some(flags.no_tun);
|
||||||
|
result.enable_exit_node = Some(flags.enable_exit_node);
|
||||||
|
result.relay_all_peer_rpc = Some(flags.relay_all_peer_rpc);
|
||||||
|
result.multi_thread = Some(flags.multi_thread);
|
||||||
|
result.proxy_forward_by_system = Some(flags.proxy_forward_by_system);
|
||||||
|
result.disable_encryption = Some(!flags.enable_encryption);
|
||||||
|
result.disable_udp_hole_punching = Some(flags.disable_udp_hole_punching);
|
||||||
|
result.enable_magic_dns = Some(flags.accept_dns);
|
||||||
|
result.mtu = Some(flags.mtu as i32);
|
||||||
|
result.enable_private_mode = Some(flags.private_mode);
|
||||||
|
|
||||||
|
if !flags.relay_network_whitelist.is_empty() && flags.relay_network_whitelist != "*" {
|
||||||
|
result.enable_relay_network_whitelist = Some(true);
|
||||||
|
result.relay_network_whitelist = flags
|
||||||
|
.relay_network_whitelist
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::common::config::ConfigLoader;
|
||||||
|
use rand::Rng;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
|
fn gen_default_config() -> crate::common::config::TomlConfigLoader {
|
||||||
|
let config = crate::common::config::TomlConfigLoader::default();
|
||||||
|
config.set_id(uuid::Uuid::new_v4());
|
||||||
|
config.set_dhcp(false);
|
||||||
|
config.set_inst_name("default".to_string());
|
||||||
|
config.set_listeners(vec![]);
|
||||||
|
config.set_rpc_portal(std::net::SocketAddr::from(([0, 0, 0, 0], 0)));
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_config_conversion_basic() -> Result<(), anyhow::Error> {
|
||||||
|
let config = gen_default_config();
|
||||||
|
|
||||||
|
let network_config = super::NetworkConfig::new_from_config(&config)?;
|
||||||
|
|
||||||
|
let generated_config = network_config.gen_config()?;
|
||||||
|
|
||||||
|
let config_str = config.dump();
|
||||||
|
let generated_config_str = generated_config.dump();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config_str, generated_config_str,
|
||||||
|
"Generated config does not match original config:\nOriginal:\n{}\n\nGenerated:\n{}\nNetwork Config: {}\n",
|
||||||
|
config_str, generated_config_str, serde_json::to_string(&network_config).unwrap()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_config_conversion_random() -> Result<(), anyhow::Error> {
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
for _ in 0..100 {
|
||||||
|
let config = gen_default_config();
|
||||||
|
|
||||||
|
config.set_id(uuid::Uuid::new_v4());
|
||||||
|
|
||||||
|
config.set_dhcp(rng.gen_bool(0.5));
|
||||||
|
|
||||||
|
if rng.gen_bool(0.7) {
|
||||||
|
let hostname = format!("host-{}", rng.gen::<u16>());
|
||||||
|
config.set_hostname(Some(hostname));
|
||||||
|
}
|
||||||
|
|
||||||
|
config.set_network_identity(crate::common::config::NetworkIdentity::new(
|
||||||
|
format!("network-{}", rng.gen::<u16>()),
|
||||||
|
format!("secret-{}", rng.gen::<u64>()),
|
||||||
|
));
|
||||||
|
config.set_inst_name(config.get_network_identity().network_name.clone());
|
||||||
|
|
||||||
|
if !config.get_dhcp() {
|
||||||
|
let addr = Ipv4Addr::new(
|
||||||
|
rng.gen_range(1..254),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(1..254),
|
||||||
|
);
|
||||||
|
let prefix_len = rng.gen_range(1..31);
|
||||||
|
let ipv4 = format!("{}/{}", addr, prefix_len).parse().unwrap();
|
||||||
|
config.set_ipv4(Some(ipv4));
|
||||||
|
}
|
||||||
|
|
||||||
|
let peer_count = rng.gen_range(0..3);
|
||||||
|
let mut peers = Vec::new();
|
||||||
|
for _ in 0..peer_count {
|
||||||
|
let port = rng.gen_range(10000..60000);
|
||||||
|
let protocol = if rng.gen_bool(0.5) { "tcp" } else { "udp" };
|
||||||
|
let uri = format!("{}://127.0.0.1:{}", protocol, port)
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
peers.push(crate::common::config::PeerConfig { uri });
|
||||||
|
}
|
||||||
|
config.set_peers(peers);
|
||||||
|
|
||||||
|
if rng.gen_bool(0.7) {
|
||||||
|
let listener_count = rng.gen_range(0..3);
|
||||||
|
let mut listeners = Vec::new();
|
||||||
|
for _ in 0..listener_count {
|
||||||
|
let port = rng.gen_range(10000..60000);
|
||||||
|
let protocol = if rng.gen_bool(0.5) { "tcp" } else { "udp" };
|
||||||
|
listeners.push(format!("{}://0.0.0.0:{}", protocol, port).parse().unwrap());
|
||||||
|
}
|
||||||
|
config.set_listeners(listeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rng.gen_bool(0.6) {
|
||||||
|
let proxy_count = rng.gen_range(0..3);
|
||||||
|
for _ in 0..proxy_count {
|
||||||
|
let network = format!(
|
||||||
|
"{}.{}.{}.0/{}",
|
||||||
|
rng.gen_range(1..254),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(24..30)
|
||||||
|
)
|
||||||
|
.parse::<cidr::Ipv4Cidr>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mapped_network = if rng.gen_bool(0.5) {
|
||||||
|
Some(
|
||||||
|
format!(
|
||||||
|
"{}.{}.{}.0/{}",
|
||||||
|
rng.gen_range(1..254),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
network.network_length()
|
||||||
|
)
|
||||||
|
.parse::<cidr::Ipv4Cidr>()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
config.add_proxy_cidr(network, mapped_network);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rng.gen_bool(0.8) {
|
||||||
|
let port = rng.gen_range(0..65535);
|
||||||
|
config.set_rpc_portal(std::net::SocketAddr::from(([0, 0, 0, 0], port)));
|
||||||
|
|
||||||
|
if rng.gen_bool(0.6) {
|
||||||
|
let whitelist_count = rng.gen_range(1..3);
|
||||||
|
let mut whitelist = Vec::new();
|
||||||
|
for _ in 0..whitelist_count {
|
||||||
|
let ip = Ipv4Addr::new(
|
||||||
|
rng.gen_range(1..254),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
);
|
||||||
|
let cidr = format!("{}/32", ip);
|
||||||
|
whitelist.push(cidr.parse().unwrap());
|
||||||
|
}
|
||||||
|
config.set_rpc_portal_whitelist(Some(whitelist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rng.gen_bool(0.5) {
|
||||||
|
let vpn_network = format!(
|
||||||
|
"{}.{}.{}.0/{}",
|
||||||
|
rng.gen_range(10..173),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(24..30)
|
||||||
|
);
|
||||||
|
let vpn_port = rng.gen_range(10000..60000);
|
||||||
|
config.set_vpn_portal_config(crate::common::config::VpnPortalConfig {
|
||||||
|
client_cidr: vpn_network.parse().unwrap(),
|
||||||
|
wireguard_listen: format!("0.0.0.0:{}", vpn_port).parse().unwrap(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if rng.gen_bool(0.6) {
|
||||||
|
let route_count = rng.gen_range(1..3);
|
||||||
|
let mut routes = Vec::new();
|
||||||
|
for _ in 0..route_count {
|
||||||
|
let route = format!(
|
||||||
|
"{}.{}.{}.0/{}",
|
||||||
|
rng.gen_range(1..254),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(24..30)
|
||||||
|
);
|
||||||
|
routes.push(route.parse().unwrap());
|
||||||
|
}
|
||||||
|
config.set_routes(Some(routes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if rng.gen_bool(0.4) {
|
||||||
|
let node_count = rng.gen_range(1..3);
|
||||||
|
let mut nodes = Vec::new();
|
||||||
|
for _ in 0..node_count {
|
||||||
|
let ip = Ipv4Addr::new(
|
||||||
|
rng.gen_range(1..254),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(0..255),
|
||||||
|
rng.gen_range(1..254),
|
||||||
|
);
|
||||||
|
nodes.push(ip);
|
||||||
|
}
|
||||||
|
config.set_exit_nodes(nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if rng.gen_bool(0.5) {
|
||||||
|
let socks5_port = rng.gen_range(10000..60000);
|
||||||
|
config.set_socks5_portal(Some(
|
||||||
|
format!("socks5://0.0.0.0:{}", socks5_port).parse().unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if rng.gen_bool(0.4) {
|
||||||
|
let count = rng.gen_range(1..3);
|
||||||
|
let mut mapped_listeners = Vec::new();
|
||||||
|
for _ in 0..count {
|
||||||
|
let port = rng.gen_range(10000..60000);
|
||||||
|
mapped_listeners.push(format!("tcp://0.0.0.0:{}", port).parse().unwrap());
|
||||||
|
}
|
||||||
|
config.set_mapped_listeners(Some(mapped_listeners));
|
||||||
|
}
|
||||||
|
|
||||||
|
if rng.gen_bool(0.9) {
|
||||||
|
let mut flags = crate::common::config::gen_default_flags();
|
||||||
|
flags.latency_first = rng.gen_bool(0.5);
|
||||||
|
flags.dev_name = format!("etun{}", rng.gen_range(0..10));
|
||||||
|
flags.use_smoltcp = rng.gen_bool(0.3);
|
||||||
|
flags.enable_kcp_proxy = rng.gen_bool(0.5);
|
||||||
|
flags.disable_kcp_input = rng.gen_bool(0.3);
|
||||||
|
flags.enable_quic_proxy = rng.gen_bool(0.5);
|
||||||
|
flags.disable_quic_input = rng.gen_bool(0.3);
|
||||||
|
flags.disable_p2p = rng.gen_bool(0.2);
|
||||||
|
flags.bind_device = rng.gen_bool(0.3);
|
||||||
|
flags.no_tun = rng.gen_bool(0.1);
|
||||||
|
flags.enable_exit_node = rng.gen_bool(0.4);
|
||||||
|
flags.relay_all_peer_rpc = rng.gen_bool(0.5);
|
||||||
|
flags.multi_thread = rng.gen_bool(0.7);
|
||||||
|
flags.proxy_forward_by_system = rng.gen_bool(0.3);
|
||||||
|
flags.enable_encryption = rng.gen_bool(0.8);
|
||||||
|
flags.disable_udp_hole_punching = rng.gen_bool(0.2);
|
||||||
|
flags.accept_dns = rng.gen_bool(0.6);
|
||||||
|
flags.mtu = rng.gen_range(1200..1500);
|
||||||
|
flags.private_mode = rng.gen_bool(0.3);
|
||||||
|
|
||||||
|
if rng.gen_bool(0.4) {
|
||||||
|
flags.relay_network_whitelist = (0..rng.gen_range(1..3))
|
||||||
|
.map(|_| {
|
||||||
|
format!(
|
||||||
|
"{}.{}.0.0/16",
|
||||||
|
rng.gen_range(10..192),
|
||||||
|
rng.gen_range(0..255)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
config.set_flags(flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
let network_config = super::NetworkConfig::new_from_config(&config)?;
|
||||||
|
let generated_config = network_config.gen_config()?;
|
||||||
|
generated_config.set_peers(generated_config.get_peers()); // Ensure peers field is not None
|
||||||
|
|
||||||
|
let config_str = config.dump();
|
||||||
|
let generated_config_str = generated_config.dump();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config_str, generated_config_str,
|
||||||
|
"Generated config does not match original config:\nOriginal:\n{}\n\nGenerated:\n{}\nNetwork Config: {}\n",
|
||||||
|
config_str, generated_config_str, serde_json::to_string(&network_config).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user