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. 744
      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']
ComputerNavBar: typeof import('./src/components/computer/ComputerNavBar.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']
CustomIcon: typeof import('./src/components/taskbar/CustomIcon.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']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElFor: typeof import('element-plus/es')['ElFor']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElIn: typeof import('element-plus/es')['ElIn']
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']
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
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']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSpace: typeof import('element-plus/es')['ElSpace']
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']
ElText: typeof import('element-plus/es')['ElText']
ElTo: typeof import('element-plus/es')['ElTo']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
Error: typeof import('./src/components/taskbar/Error.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']
NasClient: typeof import('./src/components/setting/NasClient.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']
NetworkPop: typeof import('./src/components/taskbar/NetworkPop.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">
<FileIcon :file="item" />
</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>

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

@ -1,204 +1,546 @@
<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--;
}
};
import { useProxyStore } from "@/stores/proxy";
import { notifyError, notifySuccess } from "@/util/msg";
import { Plus } from "@element-plus/icons-vue";
import { ref } from "vue";
const proxyStore = useProxyStore();
const proxyDialogShow = ref(false);
const isEditing = ref(false);
const pwdRef = ref<any>(null);
const internalPortDialogVisible = ref(false);
//
const proxyRules = {
name: [{ required: true, message: "请输入代理名称", trigger: "blur" }],
port: [
{ required: true, message: "请输入端口", trigger: "blur" },
{ type: "number", message: "端口必须是数字", trigger: "blur" },
],
domain: [{ required: true, message: "请输入域名", trigger: "blur" }],
// ...
};
const addProxy = () => {
if (pwdRef.value.validate()) {
// const isNameDuplicate = proxyStore.proxies.some(
// (p) => p.name === proxyStore.proxyData.name
// );
// if (isNameDuplicate) {
// notifyError("");
// return;
// }
// proxyStore.addProxy({ ...proxyStore.proxyData });
// proxyDialogShow.value = false;
// proxyStore.resetProxyData();
// store createFrpcConfig
proxyStore
.createFrpcConfig()
.then(() => {
proxyDialogShow.value = false;
proxyStore.resetProxyData();
notifySuccess("代理配置已成功创建");
})
.catch((error) => {
notifyError(`创建代理配置失败: ${error.message}`);
});
}
};
const updateProxy = () => {
if (pwdRef.value.validate()) {
const index = proxyStore.proxies.findIndex(
(p) => p.id === proxyStore.proxyData.id
);
if (index !== -1) {
const isNameDuplicate = proxyStore.proxies.some(
(p, i) =>
p.name === proxyStore.proxyData.name && i !== index
);
if (isNameDuplicate) {
notifyError("代理名称已存在");
return;
}
proxyStore.updateProxy({ ...proxyStore.proxyData });
proxyDialogShow.value = false;
proxyStore.resetProxyData();
isEditing.value = false;
}
}
};
const saveProxy = () => {
pwdRef.value.validate((valid: boolean) => {
if (valid) {
if (isEditing.value) {
updateProxy();
} else {
addProxy();
}
} else {
console.log("表单验证失败");
}
});
};
const openInternalPortDialog = () => {
internalPortDialogVisible.value = true;
};
const selectPort = (port: number) => {
console.log(`Selected port: ${port}`);
internalPortDialogVisible.value = false;
};
const proxyTypes = ref([
"http",
"https",
"tcp",
"udp",
"stcp",
"xtcp",
"sudp",
]);
const stcpModels = ref([
{ label: "访问者", value: "visitors" },
{ label: "被访问者", value: "visited" },
]);
const handleSelectFile = (type: number, ext: string[]) => {
ipcRenderer.invoke("file.selectFile", ext).then((r) => {
switch (type) {
case 1:
proxyStore.proxyData.https2httpCaFile = r[0];
break;
case 2:
proxyStore.proxyData.https2httpKeyFile = r[0];
break;
}
});
};
</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>
<div>
<el-row justify="end">
<el-button
type="primary"
:icon="Plus"
circle
@click="proxyDialogShow = true"
/>
</el-row>
<el-table
:data="proxyStore.proxies"
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="proxyStore.totalPages > 1"
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>
<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/list", proxy.GetLocalProxiesHandler).Methods(http.MethodGet)
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")

1
godo/go.mod

@ -46,4 +46,5 @@ require (
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // 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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=

435
godo/proxy/frpc.go

@ -1,51 +1,74 @@
package proxy
import (
"encoding/json"
"fmt"
"godo/libs"
"net/http"
"os"
"path/filepath"
"strings"
"gopkg.in/ini.v1"
)
// FrpConfig 结构体用于存储 FRP 配置
type FrpConfig struct {
ServerAddr string
ServerPort int
AuthMethod string
AuthToken string
User string
MetaToken string
TransportHeartbeatInterval int
TransportHeartbeatTimeout int
LogLevel string
LogMaxDays int
WebPort int
TlsConfigEnable bool
TlsConfigCertFile string
TlsConfigKeyFile string
TlsConfigTrustedCaFile string
TlsConfigServerName string
ProxyConfigEnable bool
ProxyConfigProxyUrl string
ServerAddr string // 服务器地址
ServerPort int // 服务器端口
AuthMethod string // 认证方法,例如 "token" 或 "multiuser"
AuthToken string // 认证令牌,当 AuthMethod 为 "token" 时使用
User string // 用户名,当 AuthMethod 为 "multiuser" 时使用
MetaToken string // 元数据令牌,当 AuthMethod 为 "multiuser" 时使用
TransportHeartbeatInterval int // 传输心跳间隔(秒)
TransportHeartbeatTimeout int // 传输心跳超时(秒)
LogLevel string // 日志级别,例如 "info"、"warn"、"error"
LogMaxDays int // 日志保留天数
WebPort int // Web 管理界面的端口
TlsConfigEnable bool // 是否启用 TLS 配置
TlsConfigCertFile string // TLS 证书文件路径
TlsConfigKeyFile string // TLS 密钥文件路径
TlsConfigTrustedCaFile string // TLS 信任的 CA 文件路径
TlsConfigServerName string // TLS 服务器名称
ProxyConfigEnable bool // 是否启用代理配置
ProxyConfigProxyUrl string // 代理 URL
}
// Proxy 结构体用于存储代理配置
type Proxy struct {
Name string
Type string
LocalIp string
LocalPort int
RemotePort int
CustomDomains []string
Subdomain string
BasicAuth bool
HttpUser string
HttpPassword string
StcpModel string
ServerName string
BindAddr string
BindPort int
FallbackTo string
FallbackTimeoutMs int
SecretKey string
Name string `json:"name"` // 代理名称
Type string `json:"type"` // 代理类型,例如 "tcp"、"http"、"stcp"
LocalIp string `json:"localIp"` // 本地 IP 地址
LocalPort int `json:"localPort"` // 本地端口
RemotePort int `json:"remotePort"` // 远程端口
CustomDomains []string `json:"customDomains"` // 自定义域名列表
Subdomain string `json:"subdomain"` // 子域名
BasicAuth bool `json:"basicAuth"` // 是否启用基本认证
HttpUser string `json:"httpUser"` // HTTP 基本认证用户名
HttpPassword string `json:"httpPassword"` // HTTP 基本认证密码
StcpModel string `json:"stcpModel"` // STCP 模式,例如 "visitors" 或 "visited"
ServerName string `json:"serverName"` // 服务器名称
BindAddr string `json:"bindAddr"` // 绑定地址
BindPort int `json:"bindPort"` // 绑定端口
FallbackTo string `json:"fallbackTo"` // 回退到的目标
FallbackTimeoutMs int `json:"fallbackTimeoutMs"` // 回退超时时间(毫秒)
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 检查端口是否为范围端口
@ -283,3 +306,345 @@ httpProxy = "%s"
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