Browse Source

add:新增远程代理frpc接口,前端ui

master
秋田弘 5 months ago
parent
commit
26c93e5ea3
  1. 15
      frontend/components.d.ts
  2. 1
      frontend/src/components/builtin/FileList.vue
  3. 204
      frontend/src/components/setting/NetProxy copy.vue
  4. 742
      frontend/src/components/setting/NetProxy.vue
  5. 182
      frontend/src/stores/proxy.ts
  6. 7
      godo/cmd/main.go
  7. 1
      godo/go.mod
  8. 2
      godo/go.sum
  9. 435
      godo/proxy/frpc.go

15
frontend/components.d.ts

@ -49,6 +49,7 @@ declare module 'vue' {
Computer: typeof import('./src/components/computer/Computer.vue')['default'] Computer: typeof import('./src/components/computer/Computer.vue')['default']
ComputerNavBar: typeof import('./src/components/computer/ComputerNavBar.vue')['default'] ComputerNavBar: typeof import('./src/components/computer/ComputerNavBar.vue')['default']
ContextMenu: typeof import('./src/components/builtin/ContextMenu.vue')['default'] ContextMenu: typeof import('./src/components/builtin/ContextMenu.vue')['default']
copy: typeof import('./src/components/setting/NetProxy copy.vue')['default']
CreateUrl: typeof import('./src/components/builtin/CreateUrl.vue')['default'] CreateUrl: typeof import('./src/components/builtin/CreateUrl.vue')['default']
CustomIcon: typeof import('./src/components/taskbar/CustomIcon.vue')['default'] CustomIcon: typeof import('./src/components/taskbar/CustomIcon.vue')['default']
DateNote: typeof import('./src/components/builtin/DateNote.vue')['default'] DateNote: typeof import('./src/components/builtin/DateNote.vue')['default']
@ -72,28 +73,41 @@ declare module 'vue' {
ElCarousel: typeof import('element-plus/es')['ElCarousel'] ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem'] ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElFor: typeof import('element-plus/es')['ElFor']
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader'] ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage'] ElImage: typeof import('element-plus/es')['ElImage']
ElIn: typeof import('element-plus/es')['ElIn']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElItem: typeof import('element-plus/es')['ElItem']
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
ElPageHeader: typeof import('element-plus/es')['ElPageHeader'] ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover'] ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress'] ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSpace: typeof import('element-plus/es')['ElSpace'] ElSpace: typeof import('element-plus/es')['ElSpace']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import('element-plus/es')['ElTag']
ElText: typeof import('element-plus/es')['ElText'] ElText: typeof import('element-plus/es')['ElText']
ElTo: typeof import('element-plus/es')['ElTo']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
Error: typeof import('./src/components/taskbar/Error.vue')['default'] Error: typeof import('./src/components/taskbar/Error.vue')['default']
FileIcon: typeof import('./src/components/builtin/FileIcon.vue')['default'] FileIcon: typeof import('./src/components/builtin/FileIcon.vue')['default']
@ -127,6 +141,7 @@ declare module 'vue' {
MusicViewer: typeof import('./src/components/builtin/MusicViewer.vue')['default'] MusicViewer: typeof import('./src/components/builtin/MusicViewer.vue')['default']
NasClient: typeof import('./src/components/setting/NasClient.vue')['default'] NasClient: typeof import('./src/components/setting/NasClient.vue')['default']
NetProxy: typeof import('./src/components/setting/NetProxy.vue')['default'] NetProxy: typeof import('./src/components/setting/NetProxy.vue')['default']
'NetProxy copy': typeof import('./src/components/setting/NetProxy copy.vue')['default']
NetWork: typeof import('./src/components/taskbar/NetWork.vue')['default'] NetWork: typeof import('./src/components/taskbar/NetWork.vue')['default']
NetworkPop: typeof import('./src/components/taskbar/NetworkPop.vue')['default'] NetworkPop: typeof import('./src/components/taskbar/NetworkPop.vue')['default']
NotFound: typeof import('./src/components/window/NotFound.vue')['default'] NotFound: typeof import('./src/components/window/NotFound.vue')['default']

1
frontend/src/components/builtin/FileList.vue

@ -52,6 +52,7 @@
} }
" "
> >
<!-- {{item.name}} -->
<div class="file-item_img"> <div class="file-item_img">
<FileIcon :file="item" /> <FileIcon :file="item" />
</div> </div>

204
frontend/src/components/setting/NetProxy copy.vue

@ -0,0 +1,204 @@
<script setup lang="ts">
import { notifyError } from '@/util/msg';
import { Plus } from '@element-plus/icons-vue'
import { ref, computed } from "vue";
interface ProxyItem {
id: number;
port: string;
domain: string;
type: string;
name: string;
serverAddr: string;
serverPort: string;
}
const localKey = "godoos_net_proxy"
const getProxies = (): ProxyItem[] => {
const proxies = localStorage.getItem(localKey);
return proxies ? JSON.parse(proxies) : [];
};
const saveProxies = (proxies: ProxyItem[]) => {
localStorage.setItem(localKey, JSON.stringify(proxies));
};
const proxies = ref<ProxyItem[]>(getProxies());
const proxyData = ref<ProxyItem>({
id: Date.now(),
type: "http",
name: "",
port: "",
domain: "",
serverAddr: "",
serverPort: "",
});
const proxyDialogShow = ref(false);
const isEditing = ref(false);
const pwdRef = ref<any>(null);
const types = ref([
{ label: 'HTTP', value: 'http' },
{ label: '静态文件访问', value: 'file' },
{ label: '点对点穿透', value: 'p2p' },
{ label: 'SSH', value: 'ssh' },
{ label: 'SOCKS5', value: 'socks5' },
])
const addProxy = () => {
if (pwdRef.value.validate()) {
const isNameDuplicate = proxies.value.some(p => p.name === proxyData.value.name);
if (isNameDuplicate) {
notifyError("代理名称已存在");
return;
}
proxies.value.push({ ...proxyData.value });
saveProxies(proxies.value);
proxyDialogShow.value = false;
proxyData.value = { id: Date.now(), port: "", domain: "", type: "http", name: "", serverAddr: "", serverPort: "" };
}
};
const editProxy = (proxy: ProxyItem) => {
proxyData.value = { ...proxy };
isEditing.value = true;
proxyDialogShow.value = true;
};
const updateProxy = () => {
if (pwdRef.value.validate()) {
const index = proxies.value.findIndex(p => p.id === proxyData.value.id);
if (index !== -1) {
const isNameDuplicate = proxies.value.some((p, i) => p.name === proxyData.value.name && i !== index);
if (isNameDuplicate) {
notifyError("代理名称已存在");
return;
}
proxies.value[index] = { ...proxyData.value };
saveProxies(proxies.value);
proxyDialogShow.value = false;
proxyData.value = { id: Date.now(), port: "", domain: "", type: "http", name: "", serverAddr: "", serverPort: "" };
isEditing.value = false;
}
}
};
const deleteProxy = (id: number) => {
proxies.value = proxies.value.filter(p => p.id !== id);
saveProxies(proxies.value);
};
const saveProxy = () => {
pwdRef.value.validate((valid: boolean) => {
if (valid) {
if (isEditing.value) {
updateProxy();
} else {
addProxy();
}
} else {
console.log('表单验证失败');
}
});
};
const proxyRules = {
name: [
{ required: true, message: '代理名称不能为空', trigger: 'blur' },
{ min: 1, max: 20, message: '代理名称长度在 1 到 20 个字符', trigger: 'blur' },
{ pattern: /^[a-zA-Z]+$/, message: '代理名称只能包含英文字符', trigger: 'blur' }
],
port: [
{ required: true, message: '端口不能为空', trigger: 'blur' },
{ pattern: /^(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])$/, message: '请输入有效的端口号(1-65535)', trigger: 'blur' }
],
domain: [
{ required: true, message: '代理域名不能为空', trigger: 'blur' },
{ pattern: /^(https?:\/\/)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d{1,5})?(\/[^\s]*)?$/, message: '请输入有效的域名格式', trigger: 'blur' }
],
serverAddr: [
{ required: true, message: '服务器地址不能为空', trigger: 'blur' },
{ pattern: /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, message: '请输入有效的IP地址格式', trigger: 'blur' }
],
serverPort: [
{ required: true, message: '服务器端口不能为空', trigger: 'blur' },
{ pattern: /^(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])$/, message: '请输入有效的服务器端口号(1-65535)', trigger: 'blur' }
]
};
const pageSize = 10;
const currentPage = ref(1);
const paginatedProxies = computed(() => {
const start = (currentPage.value - 1) * pageSize;
const end = start + pageSize;
return proxies.value.slice(start, end);
});
const totalPages = computed(() => Math.ceil(proxies.value.length / pageSize));
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
</script>
<template>
<div>
<el-row justify="end">
<el-button type="primary" :icon="Plus" circle @click="proxyDialogShow = true" />
</el-row>
<el-table :data="paginatedProxies" style="width: 98%;border:none">
<el-table-column prop="name" label="名称" width="100" />
<el-table-column prop="type" label="类型" width="80" />
<el-table-column prop="port" label="本地端口" width="80" />
<el-table-column prop="domain" label="代理域名" width="180" />
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="editProxy(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteProxy(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-if="totalPages > 1" layout="prev, pager, next" :total="getProxies().length"
:page-size="pageSize" v-model:current-page="currentPage" @next-click="nextPage" @prev-click="prevPage" />
<el-dialog v-model="proxyDialogShow" :title="isEditing ? '编辑代理' : '添加代理'" width="400px">
<span>
<el-form :model="proxyData" :rules="proxyRules" ref="pwdRef">
<el-form-item label="代理类型" prop="type">
<el-select v-model="proxyData.type" placeholder="代理类型">
<el-option v-for="type in types" :key="type.value" :label="type.label"
:value="type.value" />
</el-select>
</el-form-item>
<el-form-item label="代理名称" prop="name">
<el-input v-model="proxyData.name" />
</el-form-item>
<el-form-item label="本地端口" prop="port">
<el-input v-model="proxyData.port" />
</el-form-item>
<el-form-item label="代理域名" prop="domain">
<el-input v-model="proxyData.domain" />
</el-form-item>
<el-form-item label="服务器地址" prop="serverAddr">
<el-input v-model="proxyData.serverAddr" />
</el-form-item>
<el-form-item label="服务器端口" prop="serverPort">
<el-input v-model="proxyData.serverPort" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveProxy" style="margin: 0 auto;">
确认
</el-button>
</el-form-item>
</el-form>
</span>
</el-dialog>
</div>
</template>

742
frontend/src/components/setting/NetProxy.vue

@ -1,204 +1,546 @@
<script setup lang="ts"> <script setup lang="ts">
import { notifyError } from '@/util/msg'; import { useProxyStore } from "@/stores/proxy";
import { Plus } from '@element-plus/icons-vue' import { notifyError, notifySuccess } from "@/util/msg";
import { ref, computed } from "vue"; import { Plus } from "@element-plus/icons-vue";
import { ref } from "vue";
interface ProxyItem {
id: number; const proxyStore = useProxyStore();
port: string;
domain: string; const proxyDialogShow = ref(false);
type: string; const isEditing = ref(false);
name: string; const pwdRef = ref<any>(null);
serverAddr: string; const internalPortDialogVisible = ref(false);
serverPort: string;
} //
const localKey = "godoos_net_proxy" const proxyRules = {
const getProxies = (): ProxyItem[] => { name: [{ required: true, message: "请输入代理名称", trigger: "blur" }],
const proxies = localStorage.getItem(localKey); port: [
return proxies ? JSON.parse(proxies) : []; { required: true, message: "请输入端口", trigger: "blur" },
}; { type: "number", message: "端口必须是数字", trigger: "blur" },
],
const saveProxies = (proxies: ProxyItem[]) => { domain: [{ required: true, message: "请输入域名", trigger: "blur" }],
localStorage.setItem(localKey, JSON.stringify(proxies)); // ...
}; };
const proxies = ref<ProxyItem[]>(getProxies()); const addProxy = () => {
const proxyData = ref<ProxyItem>({ if (pwdRef.value.validate()) {
id: Date.now(), // const isNameDuplicate = proxyStore.proxies.some(
type: "http", // (p) => p.name === proxyStore.proxyData.name
name: "", // );
port: "", // if (isNameDuplicate) {
domain: "", // notifyError("");
serverAddr: "", // return;
serverPort: "", // }
}); // proxyStore.addProxy({ ...proxyStore.proxyData });
const proxyDialogShow = ref(false); // proxyDialogShow.value = false;
const isEditing = ref(false); // proxyStore.resetProxyData();
const pwdRef = ref<any>(null); // store createFrpcConfig
const types = ref([ proxyStore
{ label: 'HTTP', value: 'http' }, .createFrpcConfig()
{ label: '静态文件访问', value: 'file' }, .then(() => {
{ label: '点对点穿透', value: 'p2p' }, proxyDialogShow.value = false;
{ label: 'SSH', value: 'ssh' }, proxyStore.resetProxyData();
{ label: 'SOCKS5', value: 'socks5' }, notifySuccess("代理配置已成功创建");
})
]) .catch((error) => {
notifyError(`创建代理配置失败: ${error.message}`);
const addProxy = () => { });
if (pwdRef.value.validate()) { }
const isNameDuplicate = proxies.value.some(p => p.name === proxyData.value.name); };
if (isNameDuplicate) {
notifyError("代理名称已存在"); const updateProxy = () => {
return; if (pwdRef.value.validate()) {
} const index = proxyStore.proxies.findIndex(
proxies.value.push({ ...proxyData.value }); (p) => p.id === proxyStore.proxyData.id
saveProxies(proxies.value); );
proxyDialogShow.value = false; if (index !== -1) {
proxyData.value = { id: Date.now(), port: "", domain: "", type: "http", name: "", serverAddr: "", serverPort: "" }; const isNameDuplicate = proxyStore.proxies.some(
} (p, i) =>
}; p.name === proxyStore.proxyData.name && i !== index
);
const editProxy = (proxy: ProxyItem) => { if (isNameDuplicate) {
proxyData.value = { ...proxy }; notifyError("代理名称已存在");
isEditing.value = true; return;
proxyDialogShow.value = true; }
}; proxyStore.updateProxy({ ...proxyStore.proxyData });
proxyDialogShow.value = false;
const updateProxy = () => { proxyStore.resetProxyData();
if (pwdRef.value.validate()) { isEditing.value = false;
const index = proxies.value.findIndex(p => p.id === proxyData.value.id); }
if (index !== -1) { }
const isNameDuplicate = proxies.value.some((p, i) => p.name === proxyData.value.name && i !== index); };
if (isNameDuplicate) {
notifyError("代理名称已存在"); const saveProxy = () => {
return; pwdRef.value.validate((valid: boolean) => {
} if (valid) {
proxies.value[index] = { ...proxyData.value }; if (isEditing.value) {
saveProxies(proxies.value); updateProxy();
proxyDialogShow.value = false; } else {
proxyData.value = { id: Date.now(), port: "", domain: "", type: "http", name: "", serverAddr: "", serverPort: "" }; addProxy();
isEditing.value = false; }
} } else {
} console.log("表单验证失败");
}; }
});
const deleteProxy = (id: number) => { };
proxies.value = proxies.value.filter(p => p.id !== id);
saveProxies(proxies.value); const openInternalPortDialog = () => {
}; internalPortDialogVisible.value = true;
};
const saveProxy = () => {
pwdRef.value.validate((valid: boolean) => { const selectPort = (port: number) => {
if (valid) { console.log(`Selected port: ${port}`);
if (isEditing.value) { internalPortDialogVisible.value = false;
updateProxy(); };
} else {
addProxy(); const proxyTypes = ref([
} "http",
} else { "https",
console.log('表单验证失败'); "tcp",
} "udp",
}); "stcp",
}; "xtcp",
"sudp",
const proxyRules = { ]);
name: [
{ required: true, message: '代理名称不能为空', trigger: 'blur' }, const stcpModels = ref([
{ min: 1, max: 20, message: '代理名称长度在 1 到 20 个字符', trigger: 'blur' }, { label: "访问者", value: "visitors" },
{ pattern: /^[a-zA-Z]+$/, message: '代理名称只能包含英文字符', trigger: 'blur' } { label: "被访问者", value: "visited" },
], ]);
port: [
{ required: true, message: '端口不能为空', trigger: 'blur' }, const handleSelectFile = (type: number, ext: string[]) => {
{ pattern: /^(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])$/, message: '请输入有效的端口号(1-65535)', trigger: 'blur' } ipcRenderer.invoke("file.selectFile", ext).then((r) => {
], switch (type) {
domain: [ case 1:
{ required: true, message: '代理域名不能为空', trigger: 'blur' }, proxyStore.proxyData.https2httpCaFile = r[0];
{ pattern: /^(https?:\/\/)?(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d{1,5})?(\/[^\s]*)?$/, message: '请输入有效的域名格式', trigger: 'blur' } break;
], case 2:
serverAddr: [ proxyStore.proxyData.https2httpKeyFile = r[0];
{ required: true, message: '服务器地址不能为空', trigger: 'blur' }, break;
{ pattern: /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, message: '请输入有效的IP地址格式', trigger: 'blur' } }
], });
serverPort: [ };
{ required: true, message: '服务器端口不能为空', trigger: 'blur' },
{ pattern: /^(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])$/, message: '请输入有效的服务器端口号(1-65535)', trigger: 'blur' }
]
};
const pageSize = 10;
const currentPage = ref(1);
const paginatedProxies = computed(() => {
const start = (currentPage.value - 1) * pageSize;
const end = start + pageSize;
return proxies.value.slice(start, end);
});
const totalPages = computed(() => Math.ceil(proxies.value.length / pageSize));
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
}
};
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
}
};
</script> </script>
<template> <template>
<div> <div>
<el-row justify="end"> <el-row justify="end">
<el-button type="primary" :icon="Plus" circle @click="proxyDialogShow = true" /> <el-button
</el-row> type="primary"
<el-table :data="paginatedProxies" style="width: 98%;border:none"> :icon="Plus"
<el-table-column prop="name" label="名称" width="100" /> circle
<el-table-column prop="type" label="类型" width="80" /> @click="proxyDialogShow = true"
<el-table-column prop="port" label="本地端口" width="80" /> />
<el-table-column prop="domain" label="代理域名" width="180" /> </el-row>
<el-table-column label="操作"> <el-table
<template #default="scope"> :data="proxyStore.proxies"
<el-button size="small" @click="editProxy(scope.row)">编辑</el-button> style="width: 98%; border: none"
<el-button size="small" type="danger" @click="deleteProxy(scope.row.id)">删除</el-button> >
</template> <el-table-column
</el-table-column> prop="name"
</el-table> label="名称"
<el-pagination v-if="totalPages > 1" layout="prev, pager, next" :total="getProxies().length" width="100"
:page-size="pageSize" v-model:current-page="currentPage" @next-click="nextPage" @prev-click="prevPage" /> />
<el-dialog v-model="proxyDialogShow" :title="isEditing ? '编辑代理' : '添加代理'" width="400px"> <el-table-column
<span> prop="type"
<el-form :model="proxyData" :rules="proxyRules" ref="pwdRef"> label="类型"
<el-form-item label="代理类型" prop="type"> width="80"
<el-select v-model="proxyData.type" placeholder="代理类型"> />
<el-option v-for="type in types" :key="type.value" :label="type.label" <el-table-column
:value="type.value" /> prop="port"
</el-select> label="本地端口"
</el-form-item> width="80"
<el-form-item label="代理名称" prop="name"> />
<el-input v-model="proxyData.name" /> <el-table-column
</el-form-item> prop="domain"
label="代理域名"
<el-form-item label="本地端口" prop="port"> width="180"
<el-input v-model="proxyData.port" /> />
</el-form-item> <el-table-column label="操作">
<el-form-item label="代理域名" prop="domain"> <template #default="scope">
<el-input v-model="proxyData.domain" /> <el-button
</el-form-item> size="small"
<el-form-item label="服务器地址" prop="serverAddr"> @click="editProxy(scope.row)"
<el-input v-model="proxyData.serverAddr" /> >编辑</el-button
</el-form-item> >
<el-form-item label="服务器端口" prop="serverPort"> <el-button
<el-input v-model="proxyData.serverPort" /> size="small"
</el-form-item> type="danger"
<el-form-item> @click="deleteProxy(scope.row.id)"
<el-button type="primary" @click="saveProxy" style="margin: 0 auto;"> >删除</el-button
确认 >
</el-button> </template>
</el-form-item> </el-table-column>
</el-form> </el-table>
</span> <el-pagination
</el-dialog> v-if="proxyStore.totalPages > 1"
</div> layout="prev, pager, next"
:total="proxyStore.proxies.length"
:page-size="proxyStore.pageSize"
v-model:current-page="proxyStore.currentPage"
@next-click="proxyStore.nextPage"
@prev-click="proxyStore.prevPage"
/>
<!-- 代理配置抽屉 -->
<el-drawer
v-model="proxyDialogShow"
:title="isEditing ? '编辑代理' : '添加代理'"
direction="rtl"
size="60%"
@close="proxyStore.resetProxyData"
>
<el-form
:model="proxyStore.proxyData"
:rules="proxyRules"
ref="pwdRef"
label-position="top"
>
<!-- 代理类型选择 -->
<el-form-item
label="代理类型:"
prop="type"
>
<el-radio-group v-model="proxyStore.proxyData.type">
<el-radio-button
v-for="type in proxyTypes"
:key="type"
:value="type"
>{{ type }}</el-radio-button
>
</el-radio-group>
</el-form-item>
<!-- HTTP/HTTPS模式 -->
<template
v-if="
proxyStore.proxyData.type === 'http' ||
proxyStore.proxyData.type === 'https' ||
proxyStore.proxyData.type === 'tcp' ||
proxyStore.proxyData.type === 'udp' ||
proxyStore.proxyData.type === 'stcp' ||
proxyStore.proxyData.type === 'xtcp' ||
proxyStore.proxyData.type === 'sudp'
"
>
<el-form-item
label="代理名称:"
prop="name"
>
<el-input
v-model="proxyStore.proxyData.name"
placeholder="代理名称"
/>
</el-form-item>
<el-row
v-if="
proxyStore.proxyData.type === 'http' ||
proxyStore.proxyData.type === 'https' ||
proxyStore.proxyData.type === 'tcp' ||
proxyStore.proxyData.type === 'udp'
"
:gutter="20"
>
<el-col :span="12">
<el-form-item
label="内网地址:"
prop="serverAddr"
>
<el-input
v-model="proxyStore.proxyData.serverAddr"
placeholder="内网地址"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
label="端口地址:"
prop="port"
>
<el-input-number
v-model="proxyStore.proxyData.port"
:min="1"
:max="65535"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item
v-if="
proxyStore.proxyData.type === 'http' ||
proxyStore.proxyData.type === 'https'
"
label="子域名:"
prop="domain"
>
<el-input
v-model="proxyStore.proxyData.domain"
placeholder="subdomain"
/>
</el-form-item>
<el-form-item
v-if="
proxyStore.proxyData.type === 'http' ||
proxyStore.proxyData.type === 'https'
"
label="自定义域名:"
prop="customDomain"
>
<el-row
v-for="(domain, index) in proxyStore.customDomains"
:key="index"
:gutter="24"
>
<el-col :span="12">
<el-input
v-model="proxyStore.customDomains[index]"
placeholder="example.com"
/>
</el-col>
<el-col :span="5">
<el-button
type="primary"
icon="Plus"
style="width: 80px"
@click="proxyStore.addCustomDomain"
>添加</el-button
>
</el-col>
<el-col :span="5">
<el-button
type="primary"
icon="Plus"
style="width: 80px"
@click="
proxyStore.removeCustomDomain(index)
"
>删除</el-button
>
</el-col>
</el-row>
</el-form-item>
<el-form-item
v-if="proxyStore.proxyData.type === 'http'"
label="HTTP基本认证:"
prop="httpAuth"
>
<el-switch v-model="proxyStore.proxyData.httpAuth" />
</el-form-item>
<el-form-item
v-if="proxyStore.proxyData.httpAuth"
label="认证用户名:"
prop="authUsername"
>
<el-input
v-model="proxyStore.proxyData.authUsername"
placeholder="username"
/>
</el-form-item>
<el-form-item
v-if="proxyStore.proxyData.httpAuth"
label="认证密码:"
prop="authPassword"
>
<el-input
v-model="proxyStore.proxyData.authPassword"
type="password"
placeholder="password"
/>
</el-form-item>
<el-form-item
v-if="proxyStore.proxyData.type === 'https'"
label="证书文件:"
prop="https2httpCaFile"
>
<el-input
v-model="proxyStore.proxyData.https2httpCaFile"
placeholder="点击选择证书文件"
readonly
@click="handleSelectFile(1, ['crt', 'pem'])"
/>
</el-form-item>
<el-form-item
v-if="proxyStore.proxyData.type === 'https'"
label="密钥文件:"
prop="https2httpKeyFile"
>
<el-input
v-model="proxyStore.proxyData.https2httpKeyFile"
placeholder="点击选择密钥文件"
readonly
@click="handleSelectFile(2, ['key'])"
/>
</el-form-item>
</template>
<el-form-item
v-if="
proxyStore.proxyData.type === 'tcp' ||
proxyStore.proxyData.type === 'udp'
"
label="外网端口:"
prop="remotePort"
>
<el-input-number
v-model="proxyStore.proxyData.remotePort"
:min="1"
:max="65535"
/>
</el-form-item>
<!-- STCP/XTCP/SUDP模式 -->
<template
v-if="
proxyStore.proxyData.type === 'stcp' ||
proxyStore.proxyData.type === 'xtcp' ||
proxyStore.proxyData.type === 'sudp'
"
>
<el-row :gutter="22">
<el-col :span="14">
<el-form-item
label="STCP模式:"
prop="stcpModel"
>
<el-radio-group
v-model="proxyStore.proxyData.stcpModel"
>
<el-radio
v-for="model in stcpModels"
:key="model.value"
:value="model.value"
>{{ model.label }}</el-radio
>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item
label="共享密钥:"
prop="secretKey"
>
<el-input
v-model="proxyStore.proxyData.secretKey"
type="password"
placeholder="密钥"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 被访问者代理名称 -->
<el-form-item
v-if="
proxyStore.proxyData.type === 'stcp' ||
proxyStore.proxyData.type === 'xtcp' ||
proxyStore.proxyData.type === 'sudp'
"
label="被访问者代理名称:"
prop="visitedName"
>
<el-input
v-model="proxyStore.proxyData.visitedName"
placeholder="被访问者代理名称"
/>
</el-form-item>
<template
v-if="
proxyStore.proxyData.type === 'stcp' ||
proxyStore.proxyData.type === 'xtcp' ||
proxyStore.proxyData.type === 'sudp'
"
>
<el-row :gutter="20">
<el-col :span="10">
<el-form-item
label="绑定地址:"
prop="bindAddr"
>
<el-input
v-model="proxyStore.proxyData.bindAddr"
placeholder="127.0.0.1"
/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item
label="绑定端口:"
prop="bindPort"
>
<el-input-number
v-model="proxyStore.proxyData.bindPort"
:min="1"
:max="65535"
/>
</el-form-item>
</el-col>
</el-row>
</template>
<template v-if="proxyStore.proxyData.type === 'xtcp'">
<el-row :gutter="20">
<el-col :span="10">
<el-form-item
label="回退代理名称:"
prop="fallbackTo"
>
<el-input
v-model="
proxyStore.proxyData.fallbackTo
"
placeholder="回退代理名称"
/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item
label="回退超时毫秒:"
prop="fallbackTimeoutMs"
>
<el-input-number
v-model="
proxyStore.proxyData
.fallbackTimeoutMs
"
:min="0"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 保持隧道开启 -->
<el-form-item
label="保持隧道开启:"
prop="keepAlive"
>
<el-switch
v-model="proxyStore.proxyData.keepAlive"
/>
</el-form-item>
</template>
</template>
<!-- 保存和取消按钮 -->
<el-row justify="start">
<el-button
type="primary"
@click="saveProxy"
style="width: 100px"
>保存</el-button
>
<el-button
type="primary"
style="width: 100px"
@click="proxyDialogShow = false"
>取消</el-button
>
</el-row>
</el-form>
</el-drawer>
</div>
</template> </template>
<style scoped>
:deep(.el-drawer) {
width: 60% !important;
}
</style>

182
frontend/src/stores/proxy.ts

@ -0,0 +1,182 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export interface ProxyItem {
port: string;
domain: string;
type: string;
name: string;
serverAddr: string;
serverPort: string;
httpAuth: boolean;
authUsername: string;
authPassword: string;
https2http: boolean;
https2httpCaFile: string;
https2httpKeyFile: string;
remotePort?: string;
stcpModel?: string;
secretKey?: string;
visitedName?: string;
bindAddr?: string;
bindPort?: string;
fallbackTo?: string;
fallbackTimeoutMs?: number;
keepAlive?: boolean;
}
export const useProxyStore = defineStore('proxyStore', () => {
const localKey = "godoos_net_proxy";
const proxies = ref<ProxyItem[]>(getProxies());
const customDomains = ref<string[]>([""]);
const proxyData = ref<ProxyItem>(createNewProxyData());
function createNewProxyData(): ProxyItem {
return {
type: "http",
name: "",
port: "",
domain: "",
serverAddr: "",
serverPort: "",
httpAuth: false,
authUsername: "",
authPassword: "",
https2http: false,
https2httpCaFile: "",
https2httpKeyFile: "",
remotePort: "",
stcpModel: "",
secretKey: "",
visitedName: "",
bindAddr: "",
bindPort: "",
fallbackTo: "",
fallbackTimeoutMs: 0,
keepAlive: false,
};
}
function getProxies(): ProxyItem[] {
const proxies = localStorage.getItem(localKey);
return proxies ? JSON.parse(proxies) : [];
}
function saveProxies(proxies: ProxyItem[]) {
localStorage.setItem(localKey, JSON.stringify(proxies));
}
const addProxy = (proxy: ProxyItem) => {
proxies.value.push(proxy);
saveProxies(proxies.value);
};
const updateProxy = (updatedProxy: ProxyItem) => {
const index = proxies.value.findIndex(p => p.id === updatedProxy.id);
if (index !== -1) {
proxies.value[index] = updatedProxy;
saveProxies(proxies.value);
}
};
const deleteProxy = (id: number) => {
proxies.value = proxies.value.filter(p => p.id !== id);
saveProxies(proxies.value);
};
const addCustomDomain = () => {
customDomains.value.push("");
};
const removeCustomDomain = (index: number) => {
if (customDomains.value.length > 1) {
customDomains.value.splice(index, 1);
}
};
const resetProxyData = () => {
proxyData.value = createNewProxyData();
};
const createFrpcConfig = async () => {
const url = "http://localhost:56780/frpc/create";
const requestData = {
config: {
ServerAddr: proxyData.value.serverAddr,
ServerPort: Number(proxyData.value.serverPort),
AuthMethod: "token",
AuthToken: "your-auth-token",
User: "your-username",
MetaToken: "your-meta-token",
TransportHeartbeatInterval: 30,
TransportHeartbeatTimeout: 90,
LogLevel: "info",
LogMaxDays: 3,
WebPort: 7500,
TlsConfigEnable: true,
TlsConfigCertFile: "/path/to/cert/file",
TlsConfigKeyFile: "/path/to/key/file",
TlsConfigTrustedCaFile: "/path/to/ca/file",
TlsConfigServerName: "server-name",
ProxyConfigEnable: true,
ProxyConfigProxyUrl: "http://proxy.example.com"
},
proxies: [
{
Name: proxyData.value.name,
Type: proxyData.value.type,
LocalIp: proxyData.value.serverAddr,
LocalPort: Number(proxyData.value.port),
RemotePort: Number(proxyData.value.remotePort),
CustomDomains: customDomains.value,
Subdomain: proxyData.value.domain,
BasicAuth: proxyData.value.httpAuth,
HttpUser: proxyData.value.authUsername,
HttpPassword: proxyData.value.authPassword,
StcpModel: proxyData.value.stcpModel,
ServerName: "server1",
BindAddr: proxyData.value.bindAddr,
BindPort: Number(proxyData.value.bindPort),
FallbackTo: proxyData.value.fallbackTo,
FallbackTimeoutMs: Number(proxyData.value.fallbackTimeoutMs),
SecretKey: proxyData.value.secretKey
}
]
};
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(requestData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log("Response data:", data);
// 可以在这里添加成功通知
} catch (error) {
console.error("Error:", error);
// 可以在这里添加错误通知
}
};
return {
proxies,
customDomains,
proxyData,
addProxy,
updateProxy,
deleteProxy,
addCustomDomain,
removeCustomDomain,
resetProxyData,
createFrpcConfig,
};
});

7
godo/cmd/main.go

@ -168,6 +168,13 @@ func OsStart() {
proxyRouter.HandleFunc("/local/get", proxy.GetLocalProxyHandler).Methods(http.MethodGet) proxyRouter.HandleFunc("/local/get", proxy.GetLocalProxyHandler).Methods(http.MethodGet)
proxyRouter.HandleFunc("/local/list", proxy.GetLocalProxiesHandler).Methods(http.MethodGet) proxyRouter.HandleFunc("/local/list", proxy.GetLocalProxiesHandler).Methods(http.MethodGet)
proxyRouter.HandleFunc("/local/update", proxy.UpdateLocalProxyHandler).Methods(http.MethodPost) proxyRouter.HandleFunc("/local/update", proxy.UpdateLocalProxyHandler).Methods(http.MethodPost)
// 注册frpc路由
frpcRouter := router.PathPrefix("/frpc").Subrouter()
frpcRouter.HandleFunc("/create", proxy.CreateFrpcHandler).Methods(http.MethodPost)
frpcRouter.HandleFunc("/delete", proxy.DeleteFrpcConfigHandler).Methods(http.MethodGet) // 删除frpc配置
frpcRouter.HandleFunc("/get", proxy.GetFrpcConfigHandler).Methods(http.MethodGet) // 获取frpc配置
frpcRouter.HandleFunc("/list", proxy.GetFrpcProxiesHandler).Methods(http.MethodGet) // 获取frpc代理列表
frpcRouter.HandleFunc("/update", proxy.UpdateFrpcConfigHandler).Methods(http.MethodPost) // 更新frpc配置
// 注册根路径的处理函数 // 注册根路径的处理函数
distFS, _ := fs.Sub(deps.Frontendassets, "dist") distFS, _ := fs.Sub(deps.Frontendassets, "dist")

1
godo/go.mod

@ -46,4 +46,5 @@ require (
github.com/tklauser/numcpus v0.9.0 // indirect github.com/tklauser/numcpus v0.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.31.0 // indirect
gopkg.in/ini.v1 v1.67.0
) )

2
godo/go.sum

@ -150,6 +150,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=

435
godo/proxy/frpc.go

@ -1,51 +1,74 @@
package proxy package proxy
import ( import (
"encoding/json"
"fmt" "fmt"
"godo/libs"
"net/http"
"os"
"path/filepath"
"strings" "strings"
"gopkg.in/ini.v1"
) )
// FrpConfig 结构体用于存储 FRP 配置 // FrpConfig 结构体用于存储 FRP 配置
type FrpConfig struct { type FrpConfig struct {
ServerAddr string ServerAddr string // 服务器地址
ServerPort int ServerPort int // 服务器端口
AuthMethod string AuthMethod string // 认证方法,例如 "token" 或 "multiuser"
AuthToken string AuthToken string // 认证令牌,当 AuthMethod 为 "token" 时使用
User string User string // 用户名,当 AuthMethod 为 "multiuser" 时使用
MetaToken string MetaToken string // 元数据令牌,当 AuthMethod 为 "multiuser" 时使用
TransportHeartbeatInterval int TransportHeartbeatInterval int // 传输心跳间隔(秒)
TransportHeartbeatTimeout int TransportHeartbeatTimeout int // 传输心跳超时(秒)
LogLevel string LogLevel string // 日志级别,例如 "info"、"warn"、"error"
LogMaxDays int LogMaxDays int // 日志保留天数
WebPort int WebPort int // Web 管理界面的端口
TlsConfigEnable bool TlsConfigEnable bool // 是否启用 TLS 配置
TlsConfigCertFile string TlsConfigCertFile string // TLS 证书文件路径
TlsConfigKeyFile string TlsConfigKeyFile string // TLS 密钥文件路径
TlsConfigTrustedCaFile string TlsConfigTrustedCaFile string // TLS 信任的 CA 文件路径
TlsConfigServerName string TlsConfigServerName string // TLS 服务器名称
ProxyConfigEnable bool ProxyConfigEnable bool // 是否启用代理配置
ProxyConfigProxyUrl string ProxyConfigProxyUrl string // 代理 URL
} }
// Proxy 结构体用于存储代理配置 // Proxy 结构体用于存储代理配置
type Proxy struct { type Proxy struct {
Name string Name string `json:"name"` // 代理名称
Type string Type string `json:"type"` // 代理类型,例如 "tcp"、"http"、"stcp"
LocalIp string LocalIp string `json:"localIp"` // 本地 IP 地址
LocalPort int LocalPort int `json:"localPort"` // 本地端口
RemotePort int RemotePort int `json:"remotePort"` // 远程端口
CustomDomains []string CustomDomains []string `json:"customDomains"` // 自定义域名列表
Subdomain string Subdomain string `json:"subdomain"` // 子域名
BasicAuth bool BasicAuth bool `json:"basicAuth"` // 是否启用基本认证
HttpUser string HttpUser string `json:"httpUser"` // HTTP 基本认证用户名
HttpPassword string HttpPassword string `json:"httpPassword"` // HTTP 基本认证密码
StcpModel string StcpModel string `json:"stcpModel"` // STCP 模式,例如 "visitors" 或 "visited"
ServerName string ServerName string `json:"serverName"` // 服务器名称
BindAddr string BindAddr string `json:"bindAddr"` // 绑定地址
BindPort int BindPort int `json:"bindPort"` // 绑定端口
FallbackTo string FallbackTo string `json:"fallbackTo"` // 回退到的目标
FallbackTimeoutMs int FallbackTimeoutMs int `json:"fallbackTimeoutMs"` // 回退超时时间(毫秒)
SecretKey string SecretKey string `json:"secretKey"` // 密钥,用于加密通信
}
type Response struct {
Code string `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
Success bool `json:"success"`
}
func NewResponse(code string, message string, data interface{}) Response {
return Response{
Code: code,
Message: message,
Data: data,
Success: true,
}
} }
// isRangePort 检查端口是否为范围端口 // isRangePort 检查端口是否为范围端口
@ -283,3 +306,345 @@ httpProxy = "%s"
return ini return ini
} }
// CreateFrpcHandler 创建frpc配置
func CreateFrpcHandler(w http.ResponseWriter, r *http.Request) {
var requestData struct {
Config FrpConfig `json:"config"`
Proxies []Proxy `json:"proxies"`
}
err := json.NewDecoder(r.Body).Decode(&requestData)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 读取现有配置
path := libs.GetAppExecDir()
filePath := filepath.Join(path, "frpc", "frpc.ini")
var existingProxies []Proxy
if iniContent, err := os.ReadFile(filePath); err == nil {
cfg, err := ini.Load(iniContent)
if err == nil {
for _, section := range cfg.Sections() {
if section.Name() == "common" {
continue
}
proxy := Proxy{
Name: section.Name(),
Type: section.Key("type").String(),
LocalIp: section.Key("localIP").String(),
LocalPort: section.Key("localPort").MustInt(),
RemotePort: section.Key("remotePort").MustInt(),
CustomDomains: strings.Split(section.Key("custom_domains").String(), ","),
Subdomain: section.Key("subdomain").String(),
BasicAuth: section.Key("httpUser").String() != "",
HttpUser: section.Key("httpUser").String(),
HttpPassword: section.Key("httpPassword").String(),
StcpModel: section.Key("role").String(),
ServerName: section.Key("serverName").String(),
BindAddr: section.Key("bindAddr").String(),
BindPort: section.Key("bindPort").MustInt(),
FallbackTo: section.Key("fallbackTo").String(),
FallbackTimeoutMs: section.Key("fallbackTimeoutMs").MustInt(),
SecretKey: section.Key("sk").String(),
}
existingProxies = append(existingProxies, proxy)
}
}
}
// 合并现有代理和新代理
proxyMap := make(map[string]Proxy)
for _, proxy := range existingProxies {
proxyMap[proxy.Name] = proxy
}
for _, proxy := range requestData.Proxies {
proxyMap[proxy.Name] = proxy // 新代理会覆盖同 Name 的旧代理
}
var allProxies []Proxy
for _, proxy := range proxyMap {
allProxies = append(allProxies, proxy)
}
// 生成新的配置内容
iniContent := GenFrpcIniConfig(requestData.Config, allProxies)
if _, err := os.Stat(filepath.Join(path, "frpc")); os.IsNotExist(err) {
os.MkdirAll(filepath.Join(path, "frpc"), 0755)
}
err = os.WriteFile(filePath, []byte(iniContent), 0644)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(NewResponse("0", "Configuration created successfully", nil))
}
// GetFrpcConfigHandler 处理获取单个 FRPC 配置的 HTTP 请求
func GetFrpcConfigHandler(w http.ResponseWriter, r *http.Request) {
// 获取请求参数中的 name
name := r.URL.Query().Get("name")
if name == "" {
http.Error(w, "Name parameter is required", http.StatusBadRequest)
return
}
// 使用 os.ReadFile 读取配置文件内容
path := libs.GetAppExecDir()
filePath := filepath.Join(path, "frpc", "frpc.ini")
iniContent, err := os.ReadFile(filePath)
if err != nil {
http.Error(w, "Failed to read configuration file", http.StatusInternalServerError)
return
}
// 解析 ini 文件
cfg, err := ini.Load(iniContent)
if err != nil {
http.Error(w, "Failed to parse configuration file", http.StatusInternalServerError)
return
}
// 提取 [common] 部分
commonSection := cfg.Section("common")
frpConfig := FrpConfig{
ServerAddr: commonSection.Key("serverAddr").String(),
ServerPort: commonSection.Key("serverPort").MustInt(),
AuthMethod: commonSection.Key("authenticationMethod").String(),
AuthToken: commonSection.Key("token").String(),
User: commonSection.Key("user").String(),
MetaToken: commonSection.Key("metaToken").String(),
TransportHeartbeatInterval: commonSection.Key("heartbeatInterval").MustInt(),
TransportHeartbeatTimeout: commonSection.Key("heartbeatTimeout").MustInt(),
LogLevel: commonSection.Key("logLevel").String(),
LogMaxDays: commonSection.Key("logMaxDays").MustInt(),
WebPort: commonSection.Key("adminPort").MustInt(),
TlsConfigEnable: commonSection.Key("tlsEnable").MustBool(),
TlsConfigCertFile: commonSection.Key("tlsCertFile").String(),
TlsConfigKeyFile: commonSection.Key("tlsKeyFile").String(),
TlsConfigTrustedCaFile: commonSection.Key("tlsTrustedCaFile").String(),
TlsConfigServerName: commonSection.Key("tlsServerName").String(),
ProxyConfigEnable: commonSection.Key("httpProxy").String() != "",
ProxyConfigProxyUrl: commonSection.Key("httpProxy").String(),
}
// 查找指定 name 的代理
var proxy *Proxy
for _, section := range cfg.Sections() {
if section.Name() == "common" {
continue
}
if section.Name() == name {
proxy = &Proxy{
Name: section.Name(),
Type: section.Key("type").String(),
LocalIp: section.Key("localIP").String(),
LocalPort: section.Key("localPort").MustInt(),
RemotePort: section.Key("remotePort").MustInt(),
// 继续提取其他字段...
}
break
}
}
if proxy == nil {
http.Error(w, "Proxy not found", http.StatusNotFound)
return
}
// 创建响应
responseData := struct {
Config FrpConfig `json:"config"`
Proxy Proxy `json:"proxy"`
}{
Config: frpConfig,
Proxy: *proxy,
}
response := NewResponse("0", "Configuration retrieved successfully", responseData)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// GetFrpcProxiesHandler 获取 FRPC 代理列表
func GetFrpcProxiesHandler(w http.ResponseWriter, r *http.Request) {
// 使用 os.ReadFile 读取配置文件内容
path := libs.GetAppExecDir()
filePath := filepath.Join(path, "frpc", "frpc.ini")
iniContent, err := os.ReadFile(filePath)
if err != nil {
http.Error(w, "Failed to read configuration file", http.StatusInternalServerError)
return
}
// 解析 ini 文件
cfg, err := ini.Load(iniContent)
if err != nil {
http.Error(w, "Failed to parse configuration file", http.StatusInternalServerError)
return
}
// 提取代理部分
var proxies []Proxy
for _, section := range cfg.Sections() {
if section.Name() == "common" {
continue
}
proxy := Proxy{
Name: section.Name(),
Type: section.Key("type").String(),
LocalIp: section.Key("localIP").String(),
LocalPort: section.Key("localPort").MustInt(),
RemotePort: section.Key("remotePort").MustInt(),
CustomDomains: strings.Split(section.Key("custom_domains").String(), ","),
Subdomain: section.Key("subdomain").String(),
BasicAuth: section.Key("httpUser").String() != "",
HttpUser: section.Key("httpUser").String(),
HttpPassword: section.Key("httpPassword").String(),
StcpModel: section.Key("role").String(),
ServerName: section.Key("serverName").String(),
BindAddr: section.Key("bindAddr").String(),
BindPort: section.Key("bindPort").MustInt(),
FallbackTo: section.Key("fallbackTo").String(),
FallbackTimeoutMs: section.Key("fallbackTimeoutMs").MustInt(),
SecretKey: section.Key("sk").String(),
}
proxies = append(proxies, proxy)
}
// 创建响应
response := NewResponse("0", "Proxies retrieved successfully", proxies)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func DeleteFrpcConfigHandler(w http.ResponseWriter, r *http.Request) {
// 获取请求参数中的 name
name := r.URL.Query().Get("name")
if name == "" {
http.Error(w, "Name parameter is required", http.StatusBadRequest)
return
}
// 使用 os.ReadFile 读取配置文件内容
path := libs.GetAppExecDir()
filePath := filepath.Join(path, "frpc", "frpc.ini")
iniContent, err := os.ReadFile(filePath)
if err != nil {
http.Error(w, "Failed to read configuration file", http.StatusInternalServerError)
return
}
// 解析 ini 文件
cfg, err := ini.Load(iniContent)
if err != nil {
http.Error(w, "Failed to parse configuration file", http.StatusInternalServerError)
return
}
// 检查是否存在指定 name 的代理
if cfg.Section(name) == nil {
http.Error(w, "Proxy not found", http.StatusNotFound)
return
}
// 删除指定 name 的代理
cfg.DeleteSection(name)
// 将更新后的配置写回文件
err = cfg.SaveTo(filePath)
if err != nil {
http.Error(w, "Failed to save configuration file", http.StatusInternalServerError)
return
}
// 创建响应
response := NewResponse("0", "Configuration deleted successfully", nil)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func UpdateFrpcConfigHandler(w http.ResponseWriter, r *http.Request) {
// 获取请求参数中的 name
name := r.URL.Query().Get("name")
if name == "" {
http.Error(w, "Name parameter is required", http.StatusBadRequest)
return
}
// 解析请求体中的更新数据
var updateData Proxy
err := json.NewDecoder(r.Body).Decode(&updateData)
if err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// 使用 os.ReadFile 读取配置文件内容
path := libs.GetAppExecDir()
filePath := filepath.Join(path, "frpc", "frpc.ini")
iniContent, err := os.ReadFile(filePath)
if err != nil {
http.Error(w, "Failed to read configuration file", http.StatusInternalServerError)
return
}
// 解析 ini 文件
cfg, err := ini.Load(iniContent)
if err != nil {
http.Error(w, "Failed to parse configuration file", http.StatusInternalServerError)
return
}
// 查找指定 name 的代理
section := cfg.Section(name)
if section == nil {
http.Error(w, "Proxy not found", http.StatusNotFound)
return
}
// 更新代理配置
section.Key("type").SetValue(updateData.Type)
section.Key("localIP").SetValue(updateData.LocalIp)
section.Key("localPort").SetValue(fmt.Sprintf("%d", updateData.LocalPort))
section.Key("remotePort").SetValue(fmt.Sprintf("%d", updateData.RemotePort))
section.Key("custom_domains").SetValue(strings.Join(updateData.CustomDomains, ","))
section.Key("subdomain").SetValue(updateData.Subdomain)
section.Key("httpUser").SetValue(updateData.HttpUser)
section.Key("httpPassword").SetValue(updateData.HttpPassword)
section.Key("role").SetValue(updateData.StcpModel)
section.Key("serverName").SetValue(updateData.ServerName)
section.Key("bindAddr").SetValue(updateData.BindAddr)
section.Key("bindPort").SetValue(fmt.Sprintf("%d", updateData.BindPort))
section.Key("fallbackTo").SetValue(updateData.FallbackTo)
section.Key("fallbackTimeoutMs").SetValue(fmt.Sprintf("%d", updateData.FallbackTimeoutMs))
section.Key("sk").SetValue(updateData.SecretKey)
// 将更新后的配置写回文件
err = cfg.SaveTo(filePath)
if err != nil {
http.Error(w, "Failed to save configuration file", http.StatusInternalServerError)
return
}
// 创建响应
response := NewResponse("0", "Configuration updated successfully", nil)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}

Loading…
Cancel
Save