mirror of
https://github.com/MirageNetwork/MirageServer.git
synced 2025-10-17 05:41:13 +08:00
初步实现超管驾驶舱司南页面及司南登记自动部署步骤【不完整】
Signed-off-by: Chenyang Gao <gps949@outlook.com>
This commit is contained in:
@@ -34,6 +34,7 @@ const currentRoute = computed(() => {
|
|||||||
if (curPath.substring(0, 6) == "/users") return "users";
|
if (curPath.substring(0, 6) == "/users") return "users";
|
||||||
if (curPath.substring(0, 8) == "/tenants") return "tenants";
|
if (curPath.substring(0, 8) == "/tenants") return "tenants";
|
||||||
if (curPath.substring(0, 8) == "/setting") return "setting";
|
if (curPath.substring(0, 8) == "/setting") return "setting";
|
||||||
|
if (curPath.substring(0, 5) == "/navi") return "navi";
|
||||||
});
|
});
|
||||||
|
|
||||||
const serviceSwitch = ref(null);
|
const serviceSwitch = ref(null);
|
||||||
@@ -344,7 +345,40 @@ function doLogout() {
|
|||||||
<div :class="{ 'font-medium': currentRoute == 'users' }">用户</div>
|
<div :class="{ 'font-medium': currentRoute == 'users' }">用户</div>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link class="whitespace-nowrap py-2 group relative" to="/navi">
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
'text-blue-600 after:visible': currentRoute == 'navi',
|
||||||
|
'text-gray-600 group-hover:text-gray-800 after:invisible':
|
||||||
|
currentRoute != 'navi',
|
||||||
|
}"
|
||||||
|
class="px-3 py-2 flex items-center rounded-md group-hover:bg-gray-200 after:absolute after:bottom-0 after:right-3 after:left-3 after:h-0.5 after:bg-blue-600"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 1024 1024"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1.125em"
|
||||||
|
height="1.125em"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="currentColor"
|
||||||
|
:stroke-width="currentRoute == 'navi' ? '2.5' : '2'"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="mr-2 inline-block"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M710.4 295.9c-8-3.1-16.7-2.9-24.5 0.5L414.9 415 296.4 686c-3.6 8.2-3.6 17.5 0 25.7 3.4 7.8 9.7 13.9 17.7 17 3.8 1.5 7.7 2.2 11.7 2.2 4.4 0 8.7-0.9 12.8-2.7l271-118.6 118.5-271c3.6-8.2 3.6-17.5 0-25.7-3.5-7.9-9.8-13.9-17.7-17zM576.8 534.4l26.2 26.2-42.4 42.4-26.2-26.2L380 644.4 447.5 490 422 464.4l42.4-42.4 25.5 25.5L644.4 380l-67.6 154.4z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M464.4 422L422 464.4l25.5 25.6 86.9 86.8 26.2 26.2 42.4-42.4-26.2-26.2-86.8-86.9z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<div :class="{ 'font-medium': currentRoute == 'navi' }">司南</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
<router-link class="whitespace-nowrap py-2 group relative" to="/setting">
|
<router-link class="whitespace-nowrap py-2 group relative" to="/setting">
|
||||||
<div
|
<div
|
||||||
:class="{
|
:class="{
|
||||||
|
450
cockpit_web/src/DERPs.vue
Normal file
450
cockpit_web/src/DERPs.vue
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, nextTick, onMounted, onUnmounted, watch, watchEffect } from "vue";
|
||||||
|
import { onBeforeRouteLeave, onBeforeRouteUpdate } from "vue-router";
|
||||||
|
import NaviNodeMenu from "./components/NaviNodeMenu.vue";
|
||||||
|
import RemoveTenant from "./umenu/RemoveTenant.vue";
|
||||||
|
import EditTenant from "./umenu/EditTenant.vue";
|
||||||
|
|
||||||
|
import Deploy from "./derp/Deploy.vue";
|
||||||
|
import Toast from "./components/Toast.vue";
|
||||||
|
|
||||||
|
//与框架交互部分
|
||||||
|
|
||||||
|
//界面控制部分
|
||||||
|
const activeBtn = ref(null);
|
||||||
|
const btnLeft = ref(0);
|
||||||
|
const btnTop = ref(0);
|
||||||
|
function refreshNaviNodeMenuPos() {
|
||||||
|
if (activeBtn.value != null) {
|
||||||
|
btnLeft.value = activeBtn.value?.getBoundingClientRect().left + 14;
|
||||||
|
btnTop.value = activeBtn.value?.getBoundingClientRect().top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function openNaviNodeMenu(nr, nn, event) {
|
||||||
|
activeBtn.value = event.target;
|
||||||
|
while (activeBtn.value?.tagName != "DIV" && activeBtn.value?.tagName != "div") {
|
||||||
|
activeBtn.value = activeBtn.value.parentNode;
|
||||||
|
}
|
||||||
|
selectNaviNode.value = nn;
|
||||||
|
btnLeft.value = activeBtn.value?.getBoundingClientRect().left + 14;
|
||||||
|
btnTop.value = activeBtn.value?.getBoundingClientRect().top;
|
||||||
|
NaviNodeMenuShow.value = true;
|
||||||
|
}
|
||||||
|
function closeNaviNodeMenu() {
|
||||||
|
activeBtn.value = null;
|
||||||
|
NaviNodeMenuShow.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastShow = ref(false);
|
||||||
|
const toastMsg = ref("");
|
||||||
|
watch(toastShow, () => {
|
||||||
|
if (toastShow.value) {
|
||||||
|
setTimeout(function () {
|
||||||
|
toastShow.value = false;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectNaviNode = ref({});
|
||||||
|
function mouseOnNaviNode(u) {
|
||||||
|
selectNaviNode.value = u;
|
||||||
|
NaviNodeBtnShow.value = true;
|
||||||
|
}
|
||||||
|
function mouseLeaveNaviNode() {
|
||||||
|
NaviNodeBtnShow.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NaviNodeMenuShow = ref(false);
|
||||||
|
const NaviNodeBtnShow = ref(false);
|
||||||
|
|
||||||
|
const removeNaviNodeShow = ref(false);
|
||||||
|
function showRemoveNaviNode() {
|
||||||
|
NaviNodeBtnShow.value = false;
|
||||||
|
closeNaviNodeMenu();
|
||||||
|
removeNaviNodeShow.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editNaviNodeShow = ref(false);
|
||||||
|
function showEditNaviNode() {
|
||||||
|
NaviNodeBtnShow.value = false;
|
||||||
|
closeNaviNodeMenu();
|
||||||
|
editNaviNodeShow.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deployDERPShow = ref(false);
|
||||||
|
function showDeployDERP() {
|
||||||
|
deployDERPShow.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNaviDone(newlist) {
|
||||||
|
toastShow.value = true;
|
||||||
|
toastMsg.value = "添加成功";
|
||||||
|
NaviRegionList.value = newlist;
|
||||||
|
deployDERPShow.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doRemoveTenant() {
|
||||||
|
axios
|
||||||
|
.post("/cockpit/api/tenants", {
|
||||||
|
tenantID: selectTenant.value["id"],
|
||||||
|
action: "delete_tenant",
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.data["status"] != "success") {
|
||||||
|
toastMsg.value = response.data["status"].substring(6);
|
||||||
|
toastShow.value = true;
|
||||||
|
} else {
|
||||||
|
removeTenantShow.value = false;
|
||||||
|
toastMsg.value = "已删除 " + selectTenant.value["name"];
|
||||||
|
toastShow.value = true;
|
||||||
|
getTenants().then().catch();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
toastMsg.value = error;
|
||||||
|
toastShow.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function doUpdateTenant(newV) {
|
||||||
|
axios
|
||||||
|
.post("/cockpit/api/tenants", {
|
||||||
|
tenantID: selectTenant.value["id"],
|
||||||
|
action: "update_tenant",
|
||||||
|
newValue: newV,
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.data["status"] != "success") {
|
||||||
|
toastMsg.value = response.data["status"].substring(6);
|
||||||
|
toastShow.value = true;
|
||||||
|
} else {
|
||||||
|
editTenantShow.value = false;
|
||||||
|
toastMsg.value = "已更新 " + selectTenant.value["name"] + " 租户配置";
|
||||||
|
toastShow.value = true;
|
||||||
|
getTenants().then().catch();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
toastMsg.value = error;
|
||||||
|
toastShow.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//数据填充控制部分
|
||||||
|
const NaviRegionList = ref([]);
|
||||||
|
const NaviRegionNum = computed(() => {
|
||||||
|
if (NaviRegionList.value == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return NaviRegionList.value.length;
|
||||||
|
});
|
||||||
|
let getNaviRegionsIntID;
|
||||||
|
function getNaviRegions() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios
|
||||||
|
.get("/cockpit/api/derp/query")
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.data["status"] != "success") {
|
||||||
|
toastMsg.value = "获租户信息出错:" + response.data["status"].substring(6);
|
||||||
|
toastShow.value = true;
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理成功情况
|
||||||
|
NaviRegionList.value = response.data["data"];
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
// 处理错误情况
|
||||||
|
toastMsg.value = "获取用户信息出错:" + error;
|
||||||
|
toastShow.value = true;
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
refreshNaviNodeMenuPos();
|
||||||
|
window.addEventListener("resize", refreshNaviNodeMenuPos);
|
||||||
|
window.addEventListener("scroll", refreshNaviNodeMenuPos);
|
||||||
|
|
||||||
|
getNaviRegions().then().catch();
|
||||||
|
getNaviRegionsIntID = setInterval(() => {
|
||||||
|
getNaviRegions().then().catch();
|
||||||
|
}, 20000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("resize", refreshNaviNodeMenuPos);
|
||||||
|
window.removeEventListener("scroll", refreshNaviNodeMenuPos);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeRouteLeave(() => {
|
||||||
|
clearInterval(getNaviRegionsIntID);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="container mx-auto pb-20 md:pb-24">
|
||||||
|
<section class="mb-24">
|
||||||
|
<header class="mb-4 flex items-center">
|
||||||
|
<div class="flex justify-between items-center min-w-fit">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h1 class="text-3xl font-semibold tracking-tight leading-tight mb-2">司南</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center align-middle justify-center font-medium border border-gray-200 bg-gray-200 text-gray-600 rounded-full px-2 py-1 leading-none text-sm ml-4 min-w-fit h-7"
|
||||||
|
>
|
||||||
|
{{ NaviRegionNum }} 个区域
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full justify-end">
|
||||||
|
<input
|
||||||
|
type="button"
|
||||||
|
class="btn border-0 bg-blue-500 hover:bg-blue-900 disabled:bg-blue-500/60 text-white disabled:text-white/60 h-9 min-h-fit"
|
||||||
|
value="添加新司南"
|
||||||
|
@click="showDeployDERP"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<template v-for="nr in NaviRegionList">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center align-middle justify-center font-medium border border-gray-200 bg-gray-200 text-gray-600 rounded-full px-2 py-1 leading-none text-sm ml-4 min-w-fit h-7"
|
||||||
|
>
|
||||||
|
{{ nr.Region.RegionID }} 号区-{{ nr.Region.RegionCode }}-{{
|
||||||
|
nr.Region.RegionName
|
||||||
|
}}
|
||||||
|
共 {{ nr.Nodes ? nr.Nodes.length : 0 }} 只司南
|
||||||
|
</div>
|
||||||
|
<table class="table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="flex-auto table-cell items-center">名称</th>
|
||||||
|
<th class="table-cell items-center md:w-1/4 lg:w-1/5">指定IP</th>
|
||||||
|
<th class="hidden lg:table-cell items-center lg:w-1/5">端口</th>
|
||||||
|
<th class="hidden lg:table-cell items-center lg:w-1/5">架构</th>
|
||||||
|
<th
|
||||||
|
class="table-cell justify-end ml-auto md:ml-0 relative items-center w-8"
|
||||||
|
>
|
||||||
|
<span class="sr-only">租户操作菜单</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="nn in nr.Nodes">
|
||||||
|
<tr
|
||||||
|
:v-if="nn != nil"
|
||||||
|
@mouseenter="mouseOnNaviNode(nn)"
|
||||||
|
@mouseleave="mouseLeaveNaviNode()"
|
||||||
|
class="w-full px-0.5 hover"
|
||||||
|
>
|
||||||
|
<td class="flex-auto flex items-center">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="items-center text-gray-900">
|
||||||
|
<p class="font-semibold hover:text-blue-500">
|
||||||
|
<a class="stretched-link">{{ nn.HostName }} </a>
|
||||||
|
</p>
|
||||||
|
<span v-if="nn.status == 'suspend'">
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center align-middle justify-center font-medium border border-red-50 bg-red-50 text-red-600 rounded-sm px-1 text-xs mr-1"
|
||||||
|
>
|
||||||
|
外部司南
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center text-gray-600 text-xs">
|
||||||
|
<span>{{ nn.Name }} </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell items-center md:w-1/4 lg:w-1/5">
|
||||||
|
<div class="flex relative min-w-0">
|
||||||
|
<div class="flex flex-col items-start text-gray-600 text-sm">
|
||||||
|
<span>{{ nn.IPv4 }} </span>
|
||||||
|
<span>{{ nn.IPv6 }} </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="hidden lg:table-cell items-center lg:w-1/5">
|
||||||
|
<div class="flex relative min-w-0">
|
||||||
|
<div class="flex flex-col items-start text-sm">
|
||||||
|
<span>{{ "中继端口:" + nn.DERPPort }}</span>
|
||||||
|
<span>{{ "导航端口:" + nn.STUNPort }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="hidden lg:table-cell items-center lg:w-1/5">
|
||||||
|
<span>
|
||||||
|
<div class="inline-flex items-center cursor-default">
|
||||||
|
<span
|
||||||
|
class="inline-block w-2 h-2 rounded-full mr-2"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500': !nn.STUNOnly,
|
||||||
|
'bg-gray-300': nn.STUNOnly,
|
||||||
|
}"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-if="nn.STUNOnly"
|
||||||
|
class="text-sm text-gray-600 tooltip tooltip-top"
|
||||||
|
data-tip=" "
|
||||||
|
>{{ nn.Arch }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="text-sm text-gray-600 tooltip tooltip-top"
|
||||||
|
data-tip=" "
|
||||||
|
>{{ nn.Arch }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="table-cell justify-end ml-auto md:ml-0 relative items-center w-8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
(!NaviNodeBtnShow && !NaviNodeMenuShow) ||
|
||||||
|
selectNaviNode.Name != nn.Name
|
||||||
|
"
|
||||||
|
@click="openNaviNodeMenu(nr, nn, $event)"
|
||||||
|
class="flex-none w-12 -mt-0.5 relative"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="py-0.5 px-2 shadow-none rounded-md border border-gray-300/0 hover:border-gray-300/100 hover:bg-gray-100 hover:shadow-md hover:cursor-pointer active:border-gray-300/100 active:shadow focus:outline-none focus:ring transition-shadow duration-100 ease-in-out z-20"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="text-gray-500"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="1"></circle>
|
||||||
|
<circle cx="19" cy="12" r="1"></circle>
|
||||||
|
<circle cx="5" cy="12" r="1"></circle>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
(NaviNodeBtnShow || NaviNodeMenuShow) &&
|
||||||
|
selectNaviNode.Name == nn.Name
|
||||||
|
"
|
||||||
|
@click="openNaviNodeMenu(nr, nn, $event)"
|
||||||
|
class="flex-none w-12 border button-outline bg-white shadow-md cursor-pointer focus:outline-none focus:ring -mt-0.5 relative py-0.5 px-2 rounded-md border-gray-300/100 hover:border-gray-300/100 hover:bg-gray-100 hover:shadow-md hover:cursor-pointer active:border-gray-300/100 transition-shadow duration-100 ease-in-out z-20"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="text-gray-500"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="1"></circle>
|
||||||
|
<circle cx="19" cy="12" r="1"></circle>
|
||||||
|
<circle cx="5" cy="12" r="1"></circle>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 提示框显示 -->
|
||||||
|
<Teleport to=".toast-container">
|
||||||
|
<Toast :show="toastShow" :msg="toastMsg" @close="toastShow = false"></Toast>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<!--设备配置菜单显示-->
|
||||||
|
<Teleport to="body">
|
||||||
|
<NaviNodeMenu
|
||||||
|
v-if="NaviNodeMenuShow"
|
||||||
|
:toleft="btnLeft"
|
||||||
|
:totop="btnTop"
|
||||||
|
:select-tenant="selectNaviNode"
|
||||||
|
@close="closeNaviNodeMenu"
|
||||||
|
@showdialog-removetenant="showRemoveNaviNode"
|
||||||
|
@showdialog-edittenant="showEditNaviNode"
|
||||||
|
></NaviNodeMenu>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
|
<!-- 菜单弹出提示框显示 -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<!--部署新司南提示框显示-->
|
||||||
|
<Deploy
|
||||||
|
v-if="deployDERPShow"
|
||||||
|
:navi-region-list="NaviRegionList"
|
||||||
|
@close="deployDERPShow = false"
|
||||||
|
@add-done="addNaviDone"
|
||||||
|
></Deploy>
|
||||||
|
|
||||||
|
<!-- 移除租户提示框显示 -->
|
||||||
|
<RemoveTenant
|
||||||
|
v-if="removeTenantShow"
|
||||||
|
:select-tenant="selectTenant"
|
||||||
|
@close="removeTenantShow = false"
|
||||||
|
@confirm-remove="doRemoveTenant"
|
||||||
|
>
|
||||||
|
</RemoveTenant>
|
||||||
|
|
||||||
|
<!-- 编辑租户提示框显示 -->
|
||||||
|
<EditTenant
|
||||||
|
v-if="editTenantShow"
|
||||||
|
:select-tenant="selectTenant"
|
||||||
|
@close="editTenantShow = false"
|
||||||
|
@update-tenant="doUpdateTenant"
|
||||||
|
>
|
||||||
|
</EditTenant>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table tr.hover:hover th,
|
||||||
|
.table tr.hover:hover td,
|
||||||
|
.table tr.hover:nth-child(even):hover th,
|
||||||
|
.table tr.hover:nth-child(even):hover td {
|
||||||
|
background-color: #faf9f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table :where(thead, tfoot) :where(th, td) {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #71706f;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
--tooltip-color: #faf9f8;
|
||||||
|
--tooltip-text-color: #3a3939;
|
||||||
|
text-align: start;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:before {
|
||||||
|
max-width: 16rem;
|
||||||
|
font-size: small;
|
||||||
|
font-weight: 300;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #e1dfde;
|
||||||
|
}
|
||||||
|
</style>
|
78
cockpit_web/src/components/NaviNodeMenu.vue
Normal file
78
cockpit_web/src/components/NaviNodeMenu.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<script setup>
|
||||||
|
import { watch, ref, onMounted, computed } from "vue";
|
||||||
|
import { useDisScroll } from "../utils.js";
|
||||||
|
|
||||||
|
useDisScroll();
|
||||||
|
|
||||||
|
const tenantMenu = ref(null);
|
||||||
|
const props = defineProps({
|
||||||
|
toleft: Number,
|
||||||
|
totop: Number,
|
||||||
|
selectTenant: Object,
|
||||||
|
});
|
||||||
|
const menuLeft = computed(() => {
|
||||||
|
return String(String(props.toleft + 32 - tenantMenu.value?.clientWidth));
|
||||||
|
});
|
||||||
|
const menuTop = computed(() => {
|
||||||
|
if (props.totop <= window.innerHeight / 2) {
|
||||||
|
return String(props.totop + 36);
|
||||||
|
} else {
|
||||||
|
return String(props.totop - 10 - tenantMenu.value?.clientHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["close"]);
|
||||||
|
const closeMe = (event) => {
|
||||||
|
emit("close");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="tenantMenu"
|
||||||
|
v-click-away="closeMe"
|
||||||
|
class="shadow-xl border border-base-300 rounded-md z-20"
|
||||||
|
:style="
|
||||||
|
'position: fixed; left: ' +
|
||||||
|
menuLeft +
|
||||||
|
'px; top: ' +
|
||||||
|
menuTop +
|
||||||
|
'px; min-width: max-content; --radix-popper-transform-origin: 0% 0px;'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dropdown bg-white rounded-md py-1 z-20"
|
||||||
|
style="outline: none; pointer-events: auto"
|
||||||
|
>
|
||||||
|
<div class="block px-4 py-2 cursor-pointer hover:bg-gray-100">
|
||||||
|
共 {{ selectTenant.userCount }} 用户
|
||||||
|
</div>
|
||||||
|
<div class="block px-4 py-2 cursor-pointer hover:bg-gray-100">
|
||||||
|
共 {{ selectTenant.adminCount }} 管理员
|
||||||
|
</div>
|
||||||
|
<div class="my-1 border-b border-base-300"></div>
|
||||||
|
<div class="block px-4 py-2 cursor-pointer hover:bg-gray-100">
|
||||||
|
共 {{ selectTenant.deviceCount }} 设备
|
||||||
|
</div>
|
||||||
|
<div class="block px-4 py-2 cursor-pointer hover:bg-gray-100">
|
||||||
|
共 {{ selectTenant.subnetCount }} 子网路由
|
||||||
|
</div>
|
||||||
|
<div class="my-1 border-b border-base-300"></div>
|
||||||
|
<div
|
||||||
|
@click="$emit('showdialog-edittenant')"
|
||||||
|
class="block px-4 py-2 cursor-pointer hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
编辑租户…
|
||||||
|
</div>
|
||||||
|
<div class="my-1 border-b border-base-300"></div>
|
||||||
|
<div
|
||||||
|
@click="$emit('showdialog-removetenant')"
|
||||||
|
class="block px-4 py-2 cursor-pointer hover:bg-gray-100 text-red-400"
|
||||||
|
>
|
||||||
|
移除租户…
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
565
cockpit_web/src/derp/Deploy.vue
Normal file
565
cockpit_web/src/derp/Deploy.vue
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch, computed, onMounted, nextTick } from "vue";
|
||||||
|
import { useDisScroll } from "/src/utils.js";
|
||||||
|
import Toast from "../components/Toast.vue";
|
||||||
|
|
||||||
|
const toastShow = ref(false);
|
||||||
|
const toastMsg = ref("");
|
||||||
|
watch(toastShow, () => {
|
||||||
|
if (toastShow.value) {
|
||||||
|
setTimeout(function () {
|
||||||
|
toastShow.value = false;
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["add-done"]);
|
||||||
|
|
||||||
|
useDisScroll();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
naviRegionList: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canAdd = computed(() => {
|
||||||
|
console.log("willcreateregion ", willCreateRegion.value);
|
||||||
|
console.log("selectRegio n", selectRegion.value);
|
||||||
|
return (
|
||||||
|
(willCreateRegion.value
|
||||||
|
? newRegionCode.value != "" && newRegionName.value != ""
|
||||||
|
: !selectRegion.value) &&
|
||||||
|
NewNaviNode.value["HostName"] &&
|
||||||
|
NewNaviNode.value["HostName"] != "" &&
|
||||||
|
(NewNaviNode.value["NoSTUN"] || NewNaviNode.value["STUNPort"] != "") &&
|
||||||
|
(NewNaviNode.value["NoDERP"] || NewNaviNode.value["DERPPort"] != "") &&
|
||||||
|
(extNavi.value ||
|
||||||
|
(NewNaviNode.value["SSHAddr"] &&
|
||||||
|
NewNaviNode.value["SSHAddr"] != "" &&
|
||||||
|
(sshUsePassword.value
|
||||||
|
? NewNaviNode.value["SSHPwd"] && NewNaviNode.value["SSHPwd"] != ""
|
||||||
|
: true)))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
axios
|
||||||
|
.get("/cockpit/api/setting/general")
|
||||||
|
.then(function (response) {
|
||||||
|
// 处理成功情况
|
||||||
|
if (response.data["status"] == "success") {
|
||||||
|
remotePubKey.value = response.data["data"]["navi_deploy_pub"];
|
||||||
|
} else {
|
||||||
|
toastMsg.value = response.data["status"].substring(6);
|
||||||
|
toastShow.value = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
// 处理错误情况
|
||||||
|
toastMsg.value = error;
|
||||||
|
toastShow.value = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 新司南信息部分
|
||||||
|
const selectRegion = ref({});
|
||||||
|
|
||||||
|
const newRegionCode = ref("");
|
||||||
|
const newRegionName = ref("");
|
||||||
|
const willCreateRegion = ref(false);
|
||||||
|
|
||||||
|
const NewNaviNode = ref({});
|
||||||
|
|
||||||
|
// 远程主机参数部分
|
||||||
|
const extNavi = ref(false);
|
||||||
|
|
||||||
|
const selectDNSProvider = ref({});
|
||||||
|
const DNSChallenge = ref(true);
|
||||||
|
const DNSProviders = ref([
|
||||||
|
{ label: "Cloudflare", value: "cloudflare" },
|
||||||
|
{ label: "阿里云", value: "aliyun" },
|
||||||
|
{ label: "腾讯云", value: "qcloud" },
|
||||||
|
{ label: "NameSilo", value: "namesilo" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sshUsePassword = ref(false);
|
||||||
|
const remotePubKey = ref("");
|
||||||
|
|
||||||
|
const copyPubkeyBtnText = ref("复制");
|
||||||
|
function copyRemotePubkey(event) {
|
||||||
|
navigator.clipboard.writeText(remotePubKey.value).then(function () {
|
||||||
|
copyPubkeyBtnText.value = "已复制!";
|
||||||
|
setTimeout(() => {
|
||||||
|
copyPubkeyBtnText.value = "复制";
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const inDeploying = ref(false);
|
||||||
|
|
||||||
|
function doAddDerp() {
|
||||||
|
inDeploying.value = true;
|
||||||
|
console.log("Whether to create new region: ", willCreateRegion.value);
|
||||||
|
console.log("New region select: ", selectRegion.value);
|
||||||
|
|
||||||
|
NewNaviNode.value["RegionID"] = -1;
|
||||||
|
var nodeRegionCode = newRegionCode.value;
|
||||||
|
var nodeRegionName = newRegionName.value;
|
||||||
|
if (willCreateRegion.value == false) {
|
||||||
|
console.log("New region select ID is: ", selectRegion.value["RegionID"]);
|
||||||
|
NewNaviNode.value["RegionID"] = selectRegion.value["RegionID"];
|
||||||
|
nodeRegionCode = selectRegion.value["RegionCode"];
|
||||||
|
nodeRegionName = selectRegion.value["RegionName"];
|
||||||
|
}
|
||||||
|
if (sshUsePassword.value == false) {
|
||||||
|
NewNaviNode.value["SSHPwd"] = ""; //TEMP
|
||||||
|
}
|
||||||
|
|
||||||
|
NewNaviNode.value["DNSProvider"] = selectDNSProvider.value["value"];
|
||||||
|
if (DNSChallenge.value == false) {
|
||||||
|
NewNaviNode.value["DNSProvider"] = "";
|
||||||
|
NewNaviNode.value["DNSID"] = "";
|
||||||
|
NewNaviNode.value["DNSKey"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post("/cockpit/api/derp/add", {
|
||||||
|
RegionCode: nodeRegionCode,
|
||||||
|
RegionName: nodeRegionName,
|
||||||
|
NaviNode: NewNaviNode.value,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
if (res.data["status"] == "success") {
|
||||||
|
inDeploying.value = false;
|
||||||
|
emit("add-done", res.data["data"]);
|
||||||
|
} else {
|
||||||
|
inDeploying.value = false;
|
||||||
|
toastShow.value = true;
|
||||||
|
toastMsg.value = "添加失败:" + res.data["status"].substring(6);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
toastShow.value = true;
|
||||||
|
toastMsg.value = "部署失败:" + err;
|
||||||
|
inDeploying.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
@click.self="$emit('close')"
|
||||||
|
class="fixed overflow-y-auto inset-0 py-8 z-30 bg-gray-900 bg-opacity-[0.07]"
|
||||||
|
style="pointer-events: auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-white rounded-lg relative p-4 md:p-6 text-gray-700 max-w-lg min-w-[19rem] my-8 mx-auto w-[97%] shadow-2xl"
|
||||||
|
tabindex="-1"
|
||||||
|
style="pointer-events: auto"
|
||||||
|
>
|
||||||
|
<header class="flex items-center justify-between space-x-4 mb-5 mr-8">
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<div class="font-semibold text-lg truncate">添加新司南</div>
|
||||||
|
<el-switch
|
||||||
|
v-model="extNavi"
|
||||||
|
size="large"
|
||||||
|
class="ml-2 onetwoSwitch"
|
||||||
|
inline-prompt
|
||||||
|
active-text="登记"
|
||||||
|
inactive-text="部署"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<form @submit.prevent="">
|
||||||
|
<div class="flex flex-row w-full justify-between mb-2 space-x-2 items-center">
|
||||||
|
<el-switch
|
||||||
|
v-model="willCreateRegion"
|
||||||
|
size="large"
|
||||||
|
class="onetwoSwitch"
|
||||||
|
inline-prompt
|
||||||
|
active-text="新建区域"
|
||||||
|
inactive-text="选择区域"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
:class="{
|
||||||
|
'swap-active': willCreateRegion,
|
||||||
|
}"
|
||||||
|
class="swap flex-inline w-2/3 text-md"
|
||||||
|
>
|
||||||
|
<el-select
|
||||||
|
class="swap-off flex relative w-full z-20 justify-end"
|
||||||
|
:teleported="false"
|
||||||
|
:disabled="!naviRegionList || naviRegionList.length == 0"
|
||||||
|
v-model="selectRegion"
|
||||||
|
value-key="RegionID"
|
||||||
|
filterable
|
||||||
|
:placeholder="
|
||||||
|
!naviRegionList || naviRegionList.length == 0
|
||||||
|
? '不存在区域'
|
||||||
|
: '选择一个区域'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="nr in naviRegionList"
|
||||||
|
:key="nr.Region.RegionID"
|
||||||
|
:label="nr.Region.RegionName"
|
||||||
|
:value="nr.Region"
|
||||||
|
>
|
||||||
|
<span style="float: left">{{ nr.Region.RegionName }}</span>
|
||||||
|
<span
|
||||||
|
style="
|
||||||
|
float: right;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
"
|
||||||
|
>{{ nr.Region.RegionCode }}
|
||||||
|
</span>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<div class="swap-on flex flex-row relative w-full">
|
||||||
|
<div class="flex w-2/5 justify-end mr-2 items-center">
|
||||||
|
<p class="text-sm min-w-fit mr-1">代码</p>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="relative w-full z-20">
|
||||||
|
<input
|
||||||
|
class="input w-full border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 text-sm min-h-fit"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="newRegionCode"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-3/5 justify-end items-center">
|
||||||
|
<p class="text-sm min-w-fit mr-1">名称</p>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="relative w-full z-20">
|
||||||
|
<input
|
||||||
|
class="input w-full border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 text-sm min-h-fit"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="newRegionName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row w-full mb-2 items-center">
|
||||||
|
<p class="w-1/3 text-sm">域名</p>
|
||||||
|
<div class="flex w-2/3">
|
||||||
|
<div class="relative w-full z-10">
|
||||||
|
<input
|
||||||
|
class="input w-full z-30 border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 min-h-fit"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="NewNaviNode.HostName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row w-full mb-2 items-center">
|
||||||
|
<p class="w-1/3 text-sm">IPv4 [可选]</p>
|
||||||
|
<div class="flex w-2/3">
|
||||||
|
<div class="relative w-full z-10">
|
||||||
|
<input
|
||||||
|
class="input w-full z-30 border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 min-h-fit"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="NewNaviNode.IPv4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row w-full mb-2 items-center">
|
||||||
|
<p class="w-1/3 text-sm min-w-fit">IPv6 [可选]</p>
|
||||||
|
<div class="flex w-2/3">
|
||||||
|
<div class="relative w-full z-10">
|
||||||
|
<input
|
||||||
|
class="input w-full z-30 border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 min-h-fit"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="NewNaviNode.IPv6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row w-full mb-2 items-center">
|
||||||
|
<el-switch
|
||||||
|
v-model="NewNaviNode.NoSTUN"
|
||||||
|
size="large"
|
||||||
|
class="w-1/3 onoffSwitch"
|
||||||
|
inline-prompt
|
||||||
|
active-text="禁用探路"
|
||||||
|
inactive-text="探路端口"
|
||||||
|
/>
|
||||||
|
<div class="flex w-2/3">
|
||||||
|
<div class="relative w-full z-10">
|
||||||
|
<input
|
||||||
|
:disabled="NewNaviNode.NoSTUN"
|
||||||
|
class="input w-full z-30 border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 min-h-fit"
|
||||||
|
type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="NewNaviNode.STUNPort"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row w-full mb-2 items-center">
|
||||||
|
<el-switch
|
||||||
|
v-model="NewNaviNode.NoDERP"
|
||||||
|
size="large"
|
||||||
|
class="w-1/3 onoffSwitch"
|
||||||
|
inline-prompt
|
||||||
|
active-text="禁用中继"
|
||||||
|
inactive-text="中继端口"
|
||||||
|
/>
|
||||||
|
<div class="flex w-2/3">
|
||||||
|
<div class="relative w-full z-10">
|
||||||
|
<input
|
||||||
|
:disabled="NewNaviNode.NoDERP"
|
||||||
|
class="input w-full z-30 border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 min-h-fit"
|
||||||
|
type="number"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="NewNaviNode.DERPPort"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!extNavi">
|
||||||
|
<div class="flex flex-row w-full items-center">
|
||||||
|
<el-switch
|
||||||
|
v-model="DNSChallenge"
|
||||||
|
size="large"
|
||||||
|
class="onetwoSwitch mr-2"
|
||||||
|
inline-prompt
|
||||||
|
active-text="新配DNS"
|
||||||
|
inactive-text="已有DNS"
|
||||||
|
/>
|
||||||
|
<el-divider content-position="left" class="flex items-center"
|
||||||
|
>部署环境信息
|
||||||
|
</el-divider>
|
||||||
|
</div>
|
||||||
|
<div v-if="DNSChallenge" class="flex flex-col w-full">
|
||||||
|
<div class="flex flex-row w-full mb-2 items-center">
|
||||||
|
<p class="w-1/3 text-sm">DNS 供应商</p>
|
||||||
|
<el-select
|
||||||
|
class="flex w-2/3 justify-end"
|
||||||
|
:teleported="false"
|
||||||
|
v-model="selectDNSProvider"
|
||||||
|
value-key="value"
|
||||||
|
filterable
|
||||||
|
placeholder="选择一个供应商"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dp in DNSProviders"
|
||||||
|
:key="dp.value"
|
||||||
|
:label="dp.label"
|
||||||
|
:value="dp"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row w-full mb-2 items-center">
|
||||||
|
<p class="w-1/3 text-sm">DNS API ID</p>
|
||||||
|
<div class="flex w-2/3">
|
||||||
|
<div class="relative w-full z-10">
|
||||||
|
<input
|
||||||
|
:disabled="
|
||||||
|
selectDNSProvider.value == 'cloudflare' ||
|
||||||
|
selectDNSProvider.value == 'namesilo'
|
||||||
|
"
|
||||||
|
class="input w-full z-30 border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 min-h-fit"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="NewNaviNode.DNSID"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row w-full mb-2 items-center">
|
||||||
|
<p class="w-1/3 text-sm">DNS API Key</p>
|
||||||
|
<div class="flex w-2/3">
|
||||||
|
<div class="relative w-full z-10">
|
||||||
|
<input
|
||||||
|
class="input w-full z-30 border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 min-h-fit"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="NewNaviNode.DNSKey"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row w-full mb-2 items-center">
|
||||||
|
<p class="w-1/3 text-sm">SSH地址(带端口号)</p>
|
||||||
|
<div class="flex w-2/3">
|
||||||
|
<div class="relative w-full z-10">
|
||||||
|
<input
|
||||||
|
class="input w-full z-30 border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 min-h-fit"
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="NewNaviNode.SSHAddr"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row w-full h-7 justify-start py-1 space-x-2 items-center">
|
||||||
|
<el-switch
|
||||||
|
v-model="sshUsePassword"
|
||||||
|
size="large"
|
||||||
|
class="onetwoSwitch z-30"
|
||||||
|
inline-prompt
|
||||||
|
active-text="使用口令"
|
||||||
|
inactive-text="使用密钥"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
:class="{
|
||||||
|
'swap-active': sshUsePassword,
|
||||||
|
}"
|
||||||
|
class="swap block w-full text-md -mt-7"
|
||||||
|
>
|
||||||
|
<div class="swap-on flex relative w-full z-10 justify-end">
|
||||||
|
<input
|
||||||
|
class="input w-2/3 z-30 border focus:outline-blue-500/60 hover:border disabled:hover:border-stone-200 disabled:border-stone-200 border-stone-200 hover:border-stone-400 rounded-md h-7 min-h-fit"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
v-model="NewNaviNode.SSHPwd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="swap-off flex relative w-full -mt-7 justify-end">
|
||||||
|
<div
|
||||||
|
class="rounded-md border w-2/3 border-stone-200 gap-2 max-w-sm bg-stone-50 p-2"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col justify-start">
|
||||||
|
<div class="w-full text-left font-bold max-w-xl text-gray-500">
|
||||||
|
添加到目标机器.ssh/authorized_keys
|
||||||
|
</div>
|
||||||
|
<div class="w-full text-left max-w-xl text-gray-500 break-all">
|
||||||
|
{{ remotePubKey }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="copyRemotePubkey($event)"
|
||||||
|
class="btn border border-stone-300 hover:border-stone-300 bg-stone-200 hover:bg-stone-300 text-black h-7 min-h-fit"
|
||||||
|
>
|
||||||
|
{{ copyPubkeyBtnText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="flex mt-10 justify-end space-x-4">
|
||||||
|
<button
|
||||||
|
@click="$emit('close')"
|
||||||
|
class="btn border border-stone-300 hover:border-stone-300 bg-stone-200 hover:bg-stone-300 text-black h-9 min-h-fit"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:disabled="!canAdd || inDeploying"
|
||||||
|
@click="doAddDerp"
|
||||||
|
:class="{
|
||||||
|
loading: inDeploying,
|
||||||
|
}"
|
||||||
|
class="btn border-0 bg-blue-500 hover:bg-blue-900 disabled:bg-blue-500/60 text-white disabled:text-white/60 h-9 min-h-fit"
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
<button
|
||||||
|
@click="$emit('close')"
|
||||||
|
class="btn btn-sm btn-ghost absolute top-5 right-5 px-2 py-2 border-0 bg-base-0 focus:bg-base-200 hover:bg-base-200"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1.25em"
|
||||||
|
height="1.25em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Teleport to=".toast-container">
|
||||||
|
<Toast :show="toastShow" :msg="toastMsg" @close="toastShow = false"></Toast>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
input[type="number"] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
.toggle {
|
||||||
|
border: 0;
|
||||||
|
--tglbg: #d6d3d1;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle:checked {
|
||||||
|
border: 0;
|
||||||
|
--tglbg: #1e40af;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle:disabled {
|
||||||
|
--togglehandleborder: 0 0 0 3px white inset,
|
||||||
|
var(--handleoffsetcalculator) 0 0 3px white inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio {
|
||||||
|
--chkbg: white;
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: #d6d3d1;
|
||||||
|
}
|
||||||
|
.radio:checked {
|
||||||
|
--chkbg: white;
|
||||||
|
border-width: 5px;
|
||||||
|
border-color: #3e5db3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onoffSwitch {
|
||||||
|
--el-switch-on-color: #ff4949;
|
||||||
|
--el-switch-off-color: #13ce66;
|
||||||
|
}
|
||||||
|
.offonSwitch {
|
||||||
|
--el-switch-on-color: #13ce66;
|
||||||
|
--el-switch-off-color: #ff4949;
|
||||||
|
}
|
||||||
|
.onetwoSwitch {
|
||||||
|
--el-switch-on-color: #0082f6;
|
||||||
|
--el-switch-off-color: #0082f6;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -6,6 +6,7 @@ import Setting from './Settings.vue'
|
|||||||
import Tenants from './Tenants.vue'
|
import Tenants from './Tenants.vue'
|
||||||
import RegAdmin from './RegAdmin.vue'
|
import RegAdmin from './RegAdmin.vue'
|
||||||
import Login from './Login.vue'
|
import Login from './Login.vue'
|
||||||
|
import DERPs from './DERPs.vue'
|
||||||
import VueClickAway from "vue3-click-away"
|
import VueClickAway from "vue3-click-away"
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +16,8 @@ const routes = [
|
|||||||
{ path: '/login', component: Login },
|
{ path: '/login', component: Login },
|
||||||
{ path: '/setting', redirect: '/setting/general' },
|
{ path: '/setting', redirect: '/setting/general' },
|
||||||
{ path: '/setting/:setpart', component: Setting },
|
{ path: '/setting/:setpart', component: Setting },
|
||||||
{path:'/tenants',component:Tenants},
|
{ path:'/tenants',component:Tenants },
|
||||||
|
{ path:'/navi',component:DERPs },
|
||||||
]
|
]
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
|
@@ -364,6 +364,16 @@ function publishWin() {
|
|||||||
: "未设置"
|
: "未设置"
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
<progress
|
||||||
|
v-if="naviProc != ''"
|
||||||
|
:class="{
|
||||||
|
'progress-success': naviProc == 'uploading',
|
||||||
|
'progress-error': naviProc == 'fail',
|
||||||
|
}"
|
||||||
|
class="progress w-full"
|
||||||
|
:value="naviProcPercent"
|
||||||
|
max="100"
|
||||||
|
></progress>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full max-w-sm space-x-2 justify-around items-center mb-3">
|
<div class="flex w-full max-w-sm space-x-2 justify-around items-center mb-3">
|
||||||
|
@@ -368,7 +368,7 @@ func (h *Mirage) Serve(ctrlChn chan CtrlMsg) error {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Fetch an initial DERP Map before we start serving
|
// Fetch an initial DERP Map before we start serving
|
||||||
h.DERPMap, err = LoadDERPMapFromURL(h.cfg.DERPURL)
|
h.DERPMap, err = h.LoadDERPMapFromURL(h.cfg.DERPURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -25,9 +25,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Cockpit struct {
|
type Cockpit struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
Addr string
|
Addr string
|
||||||
ServerURL string
|
|
||||||
|
|
||||||
serviceState bool
|
serviceState bool
|
||||||
CtrlChn chan CtrlMsg
|
CtrlChn chan CtrlMsg
|
||||||
@@ -103,6 +102,18 @@ func (c *Cockpit) GetSysCfg() *SysConfig {
|
|||||||
if err != nil || cfg == nil || len(cfg) == 0 {
|
if err != nil || cfg == nil || len(cfg) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if cfg[0].NaviDeployKey == "" {
|
||||||
|
pri, pub, err := genSSHKeypair()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Msg(err.Error())
|
||||||
|
}
|
||||||
|
cfg[0].NaviDeployPub = pub
|
||||||
|
cfg[0].NaviDeployKey = pri
|
||||||
|
err = c.db.Save(&cfg[0]).Error
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Msg(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
return &cfg[0]
|
return &cfg[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,11 +140,13 @@ func (c *Cockpit) createRouter() *mux.Router {
|
|||||||
cockpit_router.HandleFunc("/api/service/stop", c.DoServiceStop).Methods(http.MethodPost)
|
cockpit_router.HandleFunc("/api/service/stop", c.DoServiceStop).Methods(http.MethodPost)
|
||||||
cockpit_router.HandleFunc("/api/tenants", c.CAPIPostTenants).Methods(http.MethodPost)
|
cockpit_router.HandleFunc("/api/tenants", c.CAPIPostTenants).Methods(http.MethodPost)
|
||||||
cockpit_router.HandleFunc("/api/publish/{os}", c.CAPIPublishClient).Methods(http.MethodPost)
|
cockpit_router.HandleFunc("/api/publish/{os}", c.CAPIPublishClient).Methods(http.MethodPost)
|
||||||
|
cockpit_router.HandleFunc("/api/derp/add", c.CAPIAddDERP).Methods(http.MethodPost)
|
||||||
|
|
||||||
cockpit_router.HandleFunc("/api/logout", c.Logout).Methods(http.MethodGet)
|
cockpit_router.HandleFunc("/api/logout", c.Logout).Methods(http.MethodGet)
|
||||||
cockpit_router.HandleFunc("/api/service/state", c.GetServiceState).Methods(http.MethodGet)
|
cockpit_router.HandleFunc("/api/service/state", c.GetServiceState).Methods(http.MethodGet)
|
||||||
cockpit_router.HandleFunc("/api/setting/general", c.GetSettingGeneral).Methods(http.MethodGet)
|
cockpit_router.HandleFunc("/api/setting/general", c.GetSettingGeneral).Methods(http.MethodGet)
|
||||||
cockpit_router.HandleFunc("/api/tenants", c.CAPIGetTenant).Methods(http.MethodGet)
|
cockpit_router.HandleFunc("/api/tenants", c.CAPIGetTenant).Methods(http.MethodGet)
|
||||||
|
cockpit_router.HandleFunc("/api/derp/query", c.CAPIQueryDERP).Methods(http.MethodGet)
|
||||||
|
|
||||||
cockpit_router.PathPrefix("").Handler(http.StripPrefix("/cockpit", http.FileServer(http.FS(cockpitDir))))
|
cockpit_router.PathPrefix("").Handler(http.StripPrefix("/cockpit", http.FileServer(http.FS(cockpitDir))))
|
||||||
|
|
||||||
|
313
controller/cockpit_api_derp.go
Normal file
313
controller/cockpit_api_derp.go
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/pkg/sftp"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NaviDeployREQ struct {
|
||||||
|
RegionCode string `json:"RegionCode"`
|
||||||
|
RegionName string `json:"RegionName"`
|
||||||
|
|
||||||
|
NaviNode NaviNode `json:"NaviNode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接受/cockpit/api/derp/query的Get请求,用于进行DERP查询
|
||||||
|
func (c *Cockpit) CAPIQueryDERP(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
) {
|
||||||
|
resData := []struct {
|
||||||
|
Region NaviRegion `json:"Region"`
|
||||||
|
Nodes []NaviNode `json:"Nodes"`
|
||||||
|
}{}
|
||||||
|
naviRegions := c.ListNaviRegions()
|
||||||
|
for _, naviRegion := range naviRegions {
|
||||||
|
naviNodes := c.ListNaviNodes(naviRegion.ID)
|
||||||
|
for index := range naviNodes { // 清除掉敏感信息
|
||||||
|
naviNodes[index].NaviKey = ""
|
||||||
|
naviNodes[index].SSHPwd = ""
|
||||||
|
naviNodes[index].DNSKey = ""
|
||||||
|
}
|
||||||
|
resData = append(resData, struct {
|
||||||
|
Region NaviRegion `json:"Region"`
|
||||||
|
Nodes []NaviNode `json:"Nodes"`
|
||||||
|
}{
|
||||||
|
Region: naviRegion,
|
||||||
|
Nodes: naviNodes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.doAPIResponse(w, "", resData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接受/cockpit/api/derp/add的Post请求,用于进行DERP登记以及部署
|
||||||
|
func (c *Cockpit) CAPIAddDERP(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
) {
|
||||||
|
reqData := NaviDeployREQ{}
|
||||||
|
json.NewDecoder(r.Body).Decode(&reqData)
|
||||||
|
|
||||||
|
remoteAuth := []ssh.AuthMethod{}
|
||||||
|
if reqData.NaviNode.SSHPwd != "" {
|
||||||
|
remoteAuth = append(remoteAuth, ssh.Password(reqData.NaviNode.SSHPwd))
|
||||||
|
} else {
|
||||||
|
var keyData []byte
|
||||||
|
|
||||||
|
sysCfg := c.GetSysCfg()
|
||||||
|
if sysCfg != nil && sysCfg.NaviDeployKey != "" {
|
||||||
|
keyData = []byte(sysCfg.NaviDeployKey)
|
||||||
|
} else {
|
||||||
|
c.doAPIResponse(w, "不存在远程主机认证信息", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBlock, _ := pem.Decode(keyData)
|
||||||
|
if pemBlock == nil {
|
||||||
|
c.doAPIResponse(w, "解析私钥失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ecdsaKey, err := x509.ParseECPrivateKey(pemBlock.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "解析私钥失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pk, err := ssh.NewSignerFromKey(ecdsaKey)
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "解析私钥失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remoteAuth = append(remoteAuth, ssh.PublicKeys(pk))
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := ssh.Dial("tcp", reqData.NaviNode.SSHAddr, &ssh.ClientConfig{
|
||||||
|
User: "root",
|
||||||
|
Auth: remoteAuth,
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "连接远程主机失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
archCheckSession, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "创建会话失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer archCheckSession.Close()
|
||||||
|
|
||||||
|
// 检查目标机处理器架构以便传送对应版本
|
||||||
|
arch, err := archCheckSession.Output("arch")
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "执行命令失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
archStr := strings.TrimSuffix(string(arch), "\n")
|
||||||
|
if archStr != "x86_64" && archStr != "aarch64" {
|
||||||
|
c.doAPIResponse(w, "不支持的处理器架构:"+archStr, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始处理服务端数据信息
|
||||||
|
if reqData.NaviNode.NaviRegionID == -1 {
|
||||||
|
naviRegion := &NaviRegion{
|
||||||
|
RegionCode: reqData.RegionCode,
|
||||||
|
RegionName: reqData.RegionName,
|
||||||
|
}
|
||||||
|
naviRegion = c.CreateNaviRegion(naviRegion)
|
||||||
|
if naviRegion == nil {
|
||||||
|
c.doAPIResponse(w, "创建区域失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reqData.NaviNode.NaviRegionID = naviRegion.ID
|
||||||
|
} else {
|
||||||
|
naviRegion := c.GetNaviRegion(reqData.NaviNode.NaviRegionID)
|
||||||
|
if naviRegion == nil {
|
||||||
|
c.doAPIResponse(w, "区域不存在", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 是否需要检查目标机曾部署过司南
|
||||||
|
derpid := uuid.New().String()
|
||||||
|
reqData.NaviNode.ID = derpid
|
||||||
|
reqData.NaviNode.Arch = archStr
|
||||||
|
naviNode := c.CreateNaviNode(&reqData.NaviNode)
|
||||||
|
if naviNode == nil {
|
||||||
|
c.doAPIResponse(w, "新建司南档案失败", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: 司南建档成功后在目标机执行部署启动
|
||||||
|
// 停止服务
|
||||||
|
systemdStopSession, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "创建会话失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer systemdStopSession.Close()
|
||||||
|
|
||||||
|
_, err = systemdStopSession.Output("systemctl stop MirageNavi")
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "执行命令失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sshSendFile(client, "download/"+archStr+"/MirageNavi", "/usr/local/bin/MirageNavi")
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "传送司南客户端到目标服务器失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 进行赋权
|
||||||
|
chmodSession, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "创建会话失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer chmodSession.Close()
|
||||||
|
|
||||||
|
_, err = chmodSession.Output("chmod +x /usr/local/bin/MirageNavi")
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "执行命令失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceScript :=
|
||||||
|
`[Unit]
|
||||||
|
Description=Mirage Navigation Node Service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/MirageNavi -ctrl-url ${MIRAGE_CTRL_URL} -id ${MIRAGE_NAVI_ID} >> ${LOG_DIR}/MirageNavi.log 2>&1
|
||||||
|
Restart=always
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||||
|
Environment=LOG_DIR=/var/log
|
||||||
|
Environment=MIRAGE_CTRL_URL=https://` + c.GetSysCfg().ServerURL + `
|
||||||
|
Environment=MIRAGE_NAVI_ID=` + derpid + `
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target`
|
||||||
|
|
||||||
|
// 将文本写入文件
|
||||||
|
err = ioutil.WriteFile("download/"+derpid+".service.tmp", []byte(serviceScript), 0644)
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "创建临时服务文件失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = sshSendFile(client, "download/"+derpid+".service.tmp", "/etc/systemd/system/MirageNavi.service")
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "传送服务文件到目标服务器失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = os.Remove("download/" + derpid + ".service.tmp")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Caller().Err(err).Msg("删除服务临时文件失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置服务配置
|
||||||
|
systemdReloadSession, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "创建会话失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer systemdReloadSession.Close()
|
||||||
|
|
||||||
|
_, err = systemdReloadSession.Output("systemctl daemon-reload")
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "执行命令失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务
|
||||||
|
systemdEnableSession, err := client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "创建会话失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer systemdEnableSession.Close()
|
||||||
|
|
||||||
|
_, err = systemdEnableSession.Output("systemctl enable --now MirageNavi")
|
||||||
|
if err != nil {
|
||||||
|
c.doAPIResponse(w, "执行命令失败:"+err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.CAPIQueryDERP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func genSSHKeypair() (priKey, pubKey string, err error) {
|
||||||
|
var privateKey *ecdsa.PrivateKey
|
||||||
|
var publicKey ssh.PublicKey
|
||||||
|
|
||||||
|
if privateKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if publicKey, err = ssh.NewPublicKey(privateKey.Public()); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pubKey = string(ssh.MarshalAuthorizedKey(publicKey))
|
||||||
|
var priKeyData []byte
|
||||||
|
if priKeyData, err = x509.MarshalECPrivateKey(privateKey); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
priKey = string(pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "EC PRIVATE KEY",
|
||||||
|
Bytes: priKeyData,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshSendFile(sshClient *ssh.Client, localFilePath string, remoteFilePath string) error {
|
||||||
|
sftpClient, err := sftp.NewClient(sshClient)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sftpClient.Close()
|
||||||
|
|
||||||
|
remoteFile, err := sftpClient.Create(remoteFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer remoteFile.Close()
|
||||||
|
|
||||||
|
localFile, err := os.Open(localFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer localFile.Close()
|
||||||
|
|
||||||
|
n, err := io.Copy(remoteFile, localFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
localFileInfo, err := os.Stat(localFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n != localFileInfo.Size() {
|
||||||
|
return errors.New("文件大小不一致,传输失败")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@@ -40,6 +40,8 @@ type SysConfig struct {
|
|||||||
GoogleCfg GoogleCfg
|
GoogleCfg GoogleCfg
|
||||||
AppleCfg AppleCfg
|
AppleCfg AppleCfg
|
||||||
|
|
||||||
|
NaviDeployPub string
|
||||||
|
NaviDeployKey string
|
||||||
ClientVersion ClientVersionInfo
|
ClientVersion ClientVersionInfo
|
||||||
|
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
@@ -68,6 +70,7 @@ type GeneralCfg struct {
|
|||||||
GoogleCfg GoogleCfg `json:"google"`
|
GoogleCfg GoogleCfg `json:"google"`
|
||||||
AppleCfg AppleCfg `json:"apple"`
|
AppleCfg AppleCfg `json:"apple"`
|
||||||
|
|
||||||
|
NaviDeployPub string `json:"navi_deploy_pub"`
|
||||||
ClientVersion ClientVersionInfo `json:"client_version"`
|
ClientVersion ClientVersionInfo `json:"client_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +96,7 @@ func (s *SysConfig) toGeneralCfg() GeneralCfg {
|
|||||||
GoogleCfg: s.GoogleCfg,
|
GoogleCfg: s.GoogleCfg,
|
||||||
AppleCfg: s.AppleCfg,
|
AppleCfg: s.AppleCfg,
|
||||||
|
|
||||||
|
NaviDeployPub: s.NaviDeployPub,
|
||||||
ClientVersion: s.ClientVersion,
|
ClientVersion: s.ClientVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -34,6 +34,16 @@ func (dp *DataPool) InitCockpitDB() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = dp.db.AutoMigrate(&NaviRegion{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = dp.db.AutoMigrate(&NaviNode{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,7 +10,147 @@ import (
|
|||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadDERPMapFromURL(addr string) (*tailcfg.DERPMap, error) {
|
type NaviRegion struct {
|
||||||
|
ID int `gorm:"primary_key;unique;not null" json:"RegionID"`
|
||||||
|
OrgID int64 `gorm:";not null" json:"OrgID"` // 0代表全局向导
|
||||||
|
RegionCode string `gorm:"not null" json:"RegionCode"`
|
||||||
|
RegionName string `gorm:"not null" json:"RegionName"`
|
||||||
|
//这个不知道有何用 Avoid bool `json:",omitempty"`
|
||||||
|
}
|
||||||
|
type NaviNode struct {
|
||||||
|
ID string `gorm:"primary_key;unique;not null" json:"Name"` //映射到DERPNode的Name
|
||||||
|
NaviKey string `json:"NaviKey"` //记录DERPNode的MachineKey公钥
|
||||||
|
|
||||||
|
NaviRegionID int `gorm:"not null" json:"RegionID"` //映射到DERPNode的RegionID
|
||||||
|
NaviRegion *NaviRegion `gorm:"foreignKey:NaviRegionID;references:ID" json:"-"` //映射到DERPNode的RegionID
|
||||||
|
|
||||||
|
HostName string `json:"HostName"` //这个不需要独有,但是否必须域名呢?
|
||||||
|
//这个不用? CertName string `json:",omitempty"`
|
||||||
|
|
||||||
|
IPv4 string `json:"IPv4"` // 不是ipv4地址则失效,为none则禁用ipv4
|
||||||
|
IPv6 string `json:"IPv6"` // 不是ipv6地址则失效,为none则禁用ipv6
|
||||||
|
|
||||||
|
NoSTUN bool `json:"NoSTUN"` //禁用STUN
|
||||||
|
STUNPort int `json:"STUNPort"` //0代表3478,-1代表禁用
|
||||||
|
|
||||||
|
NoDERP bool `json:"NoDERP"` //禁用DERP
|
||||||
|
DERPPort int `json:"DERPPort"` //0代表443
|
||||||
|
|
||||||
|
SSHAddr string `json:"SSHAddr"` //SSH地址
|
||||||
|
SSHPwd string `json:"SSHPwd"` //SSH口令
|
||||||
|
DNSProvider string `json:"DNSProvider"` //DNS服务商
|
||||||
|
DNSID string `json:"DNSID"` //DNS服务商的ID
|
||||||
|
DNSKey string `json:"DNSKey"` //DNS服务商的Key
|
||||||
|
|
||||||
|
Arch string `json:"Arch"` //所在环境架构,x86_64或aarch64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cockpit) toDERPRegion(nr NaviRegion) (tailcfg.DERPRegion, error) {
|
||||||
|
nodes := c.ListNaviNodes(nr.ID)
|
||||||
|
derpNodes, err := c.toDERPNodes(nodes)
|
||||||
|
if err != nil {
|
||||||
|
return tailcfg.DERPRegion{}, err
|
||||||
|
}
|
||||||
|
return tailcfg.DERPRegion{
|
||||||
|
RegionID: nr.ID,
|
||||||
|
RegionCode: nr.RegionCode,
|
||||||
|
RegionName: nr.RegionName,
|
||||||
|
Nodes: derpNodes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Cockpit) toDERPNodes(nodes []NaviNode) ([]*tailcfg.DERPNode, error) {
|
||||||
|
derpNodes := make([]*tailcfg.DERPNode, len(nodes))
|
||||||
|
for index, node := range nodes {
|
||||||
|
derpNode, err := m.toDERPNode(node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
derpNodes[index] = derpNode
|
||||||
|
}
|
||||||
|
return derpNodes, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cockpit) toDERPNode(node NaviNode) (*tailcfg.DERPNode, error) {
|
||||||
|
derp := &tailcfg.DERPNode{
|
||||||
|
Name: node.ID,
|
||||||
|
RegionID: node.NaviRegionID,
|
||||||
|
HostName: node.HostName,
|
||||||
|
IPv4: node.IPv4,
|
||||||
|
IPv6: node.IPv6,
|
||||||
|
STUNPort: node.STUNPort,
|
||||||
|
STUNOnly: node.NoDERP,
|
||||||
|
DERPPort: node.DERPPort,
|
||||||
|
}
|
||||||
|
if node.NoSTUN {
|
||||||
|
derp.STUNPort = -1
|
||||||
|
}
|
||||||
|
return derp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cockpit) ListNaviRegions() []NaviRegion {
|
||||||
|
naviRegions := []NaviRegion{}
|
||||||
|
if err := c.db.Find(&naviRegions).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviRegions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cockpit) GetNaviRegion(id int) *NaviRegion {
|
||||||
|
naviRegion := NaviRegion{}
|
||||||
|
if err := c.db.First(&naviRegion, id).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &naviRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cockpit) CreateNaviRegion(naviRegion *NaviRegion) *NaviRegion {
|
||||||
|
if err := c.db.Create(naviRegion).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cockpit) UpdateNaviRegion(naviRegion *NaviRegion) *NaviRegion {
|
||||||
|
if err := c.db.Save(naviRegion).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cockpit) ListNaviNodes(regionID int) []NaviNode {
|
||||||
|
naviNodes := []NaviNode{}
|
||||||
|
if err := c.db.Preload("NaviRegion").Where("navi_region_id = ?", regionID).Find(&naviNodes).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cockpit) GetNaviNode(id string) *NaviNode {
|
||||||
|
naviNode := NaviNode{}
|
||||||
|
if err := c.db.Preload("NaviRegion").First(&naviNode, "id = ?", id).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &naviNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cockpit) CreateNaviNode(naviNode *NaviNode) *NaviNode {
|
||||||
|
if err := c.db.Create(naviNode).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cockpit) UpdateNaviNode(naviNode *NaviNode) *NaviNode {
|
||||||
|
if err := c.db.Save(naviNode).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// cgao6: 以下为Mirage的实现
|
||||||
|
func (m *Mirage) LoadDERPMapFromURL(addr string) (*tailcfg.DERPMap, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), HTTPReadTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), HTTPReadTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -42,5 +182,112 @@ func LoadDERPMapFromURL(addr string) (*tailcfg.DERPMap, error) {
|
|||||||
Msg("DERP map is empty, not a single DERP map datasource was loaded correctly or contained a region")
|
Msg("DERP map is empty, not a single DERP map datasource was loaded correctly or contained a region")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从数据库读取DERP信息
|
||||||
|
naviRegions := m.ListNaviRegions()
|
||||||
|
if len(naviRegions) != 0 {
|
||||||
|
for _, nr := range naviRegions {
|
||||||
|
derpRegion, err := m.toDERPRegion(nr)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Cannot convert NaviRegion to DERPRegion")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
derpMap.Regions[derpRegion.RegionID] = &derpRegion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &derpMap, err
|
return &derpMap, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Mirage) toDERPRegion(nr NaviRegion) (tailcfg.DERPRegion, error) {
|
||||||
|
nodes := m.ListNaviNodes(nr.ID)
|
||||||
|
derpNodes, err := m.toDERPNodes(nodes)
|
||||||
|
if err != nil {
|
||||||
|
return tailcfg.DERPRegion{}, err
|
||||||
|
}
|
||||||
|
return tailcfg.DERPRegion{
|
||||||
|
RegionID: nr.ID,
|
||||||
|
RegionCode: nr.RegionCode,
|
||||||
|
RegionName: nr.RegionName,
|
||||||
|
Nodes: derpNodes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
func (m *Mirage) toDERPNodes(nodes []NaviNode) ([]*tailcfg.DERPNode, error) {
|
||||||
|
derpNodes := make([]*tailcfg.DERPNode, len(nodes))
|
||||||
|
for index, node := range nodes {
|
||||||
|
derpNode, err := m.toDERPNode(node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
derpNodes[index] = derpNode
|
||||||
|
}
|
||||||
|
return derpNodes, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mirage) toDERPNode(node NaviNode) (*tailcfg.DERPNode, error) {
|
||||||
|
derp := &tailcfg.DERPNode{
|
||||||
|
Name: node.ID,
|
||||||
|
RegionID: node.NaviRegionID,
|
||||||
|
HostName: node.HostName,
|
||||||
|
IPv4: node.IPv4,
|
||||||
|
IPv6: node.IPv6,
|
||||||
|
STUNPort: node.STUNPort,
|
||||||
|
STUNOnly: node.NoDERP,
|
||||||
|
DERPPort: node.DERPPort,
|
||||||
|
}
|
||||||
|
if node.NoSTUN {
|
||||||
|
derp.STUNPort = -1
|
||||||
|
}
|
||||||
|
return derp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mirage) ListNaviRegions() []NaviRegion {
|
||||||
|
naviRegions := []NaviRegion{}
|
||||||
|
if err := m.db.Find(&naviRegions).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviRegions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mirage) GetNaviRegion(id int64) *NaviRegion {
|
||||||
|
naviRegion := NaviRegion{}
|
||||||
|
if err := m.db.First(&naviRegion, id).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &naviRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mirage) CreateNaviRegion(naviRegion *NaviRegion) *NaviRegion {
|
||||||
|
if err := m.db.Create(naviRegion).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviRegion
|
||||||
|
}
|
||||||
|
func (m *Mirage) UpdateNaviRegion(naviRegion *NaviRegion) *NaviRegion {
|
||||||
|
if err := m.db.Save(naviRegion).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviRegion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mirage) ListNaviNodes(regionID int) []NaviNode {
|
||||||
|
naviNodes := []NaviNode{}
|
||||||
|
if err := m.db.Preload("NaviRegion").Where("navi_region_id = ?", regionID).Find(&naviNodes).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviNodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mirage) GetNaviNode(id string) *NaviNode {
|
||||||
|
naviNode := NaviNode{}
|
||||||
|
if err := m.db.Preload("NaviRegion").First(&naviNode, "id = ?", id).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &naviNode
|
||||||
|
}
|
||||||
|
func (m *Mirage) UpdateNaviNode(naviNode *NaviNode) *NaviNode {
|
||||||
|
if err := m.db.Save(naviNode).Error; err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return naviNode
|
||||||
|
}
|
||||||
|
@@ -67,6 +67,9 @@ func (h *Mirage) NoiseUpgradeHandler(
|
|||||||
Methods(http.MethodPost)
|
Methods(http.MethodPost)
|
||||||
router.HandleFunc("/machine/map", ts2021App.NoisePollNetMapHandler)
|
router.HandleFunc("/machine/map", ts2021App.NoisePollNetMapHandler)
|
||||||
|
|
||||||
|
router.HandleFunc("/navi/register", ts2021App.NoiseNaviRegisterHandler).
|
||||||
|
Methods(http.MethodPost)
|
||||||
|
|
||||||
server := http.Server{
|
server := http.Server{
|
||||||
ReadTimeout: HTTPReadTimeout,
|
ReadTimeout: HTTPReadTimeout,
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
@@ -34,3 +35,91 @@ func (t *ts2021App) NoiseRegistrationHandler(
|
|||||||
|
|
||||||
t.mirage.handleRegisterCommon(writer, req, registerRequest, t.conn.Peer())
|
t.mirage.handleRegisterCommon(writer, req, registerRequest, t.conn.Peer())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NaviRegisterRequest struct {
|
||||||
|
ID string
|
||||||
|
Timestamp *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type NaviRegisterResponse struct {
|
||||||
|
NodeInfo NaviNode
|
||||||
|
Timestamp *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// 司南注册noise协议接口
|
||||||
|
func (t *ts2021App) NoiseNaviRegisterHandler(
|
||||||
|
writer http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
) {
|
||||||
|
log.Trace().Caller().Msgf("Noise registration handler for Navi %s", req.RemoteAddr)
|
||||||
|
if req.Method != http.MethodPost {
|
||||||
|
http.Error(writer, "Wrong method", http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(req.Body)
|
||||||
|
registerRequest := NaviRegisterRequest{}
|
||||||
|
if err := json.Unmarshal(body, ®isterRequest); err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot parse RegisterRequest")
|
||||||
|
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
node := t.mirage.GetNaviNode(registerRequest.ID)
|
||||||
|
if node == nil {
|
||||||
|
log.Warn().Caller().Msgf("Navi node %s not found", registerRequest.ID)
|
||||||
|
http.Error(writer, "Navi node not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if node.NaviKey == "" || node.NaviKey == MachinePublicKeyStripPrefix(t.conn.Peer()) {
|
||||||
|
node.NaviKey = MachinePublicKeyStripPrefix(t.conn.Peer())
|
||||||
|
node := t.mirage.UpdateNaviNode(node)
|
||||||
|
if node == nil {
|
||||||
|
log.Warn().Caller().Msgf("Navi node %s update failed", registerRequest.ID)
|
||||||
|
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Trace().Caller().Msgf("Navi node %s registered", node.ID)
|
||||||
|
now := time.Now().Round(time.Second)
|
||||||
|
resp := NaviRegisterResponse{
|
||||||
|
NodeInfo: *node,
|
||||||
|
Timestamp: &now,
|
||||||
|
}
|
||||||
|
respBody, err := t.mirage.marshalResponse(resp, t.conn.Peer())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Str("func", "handleNaviRegister").
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot encode message")
|
||||||
|
http.Error(writer, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
_, err = writer.Write(respBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to write response")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("func", "handleNaviRegister").
|
||||||
|
Str("derpID", registerRequest.ID).
|
||||||
|
Msg("Successfully register Navi node")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Msg("Navi node not created yet or key mismatch")
|
||||||
|
http.Error(writer, "Internal error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
3
go.mod
3
go.mod
@@ -82,6 +82,7 @@ require (
|
|||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/lib/pq v1.10.7 // indirect
|
github.com/lib/pq v1.10.7 // indirect
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||||
@@ -93,6 +94,7 @@ require (
|
|||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pkg/sftp v1.13.5 // indirect
|
||||||
github.com/pquerna/cachecontrol v0.1.0 // indirect
|
github.com/pquerna/cachecontrol v0.1.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
@@ -143,6 +145,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
github.com/mdlayher/netlink v1.7.1 // indirect
|
github.com/mdlayher/netlink v1.7.1 // indirect
|
||||||
github.com/mdlayher/socket v0.4.0 // indirect
|
github.com/mdlayher/socket v0.4.0 // indirect
|
||||||
|
github.com/melbahja/goph v1.3.1
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/sethvargo/go-diceware v0.3.0
|
github.com/sethvargo/go-diceware v0.3.0
|
||||||
|
11
go.sum
11
go.sum
@@ -387,6 +387,7 @@ github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0P
|
|||||||
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -426,6 +427,8 @@ github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTm
|
|||||||
github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ=
|
github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ=
|
||||||
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
|
github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw=
|
||||||
github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
|
github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc=
|
||||||
|
github.com/melbahja/goph v1.3.1 h1:FxFevAwCCpLkM4WBmnVVxcJBcBz6lKQpsN5biV2hA6w=
|
||||||
|
github.com/melbahja/goph v1.3.1/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68=
|
||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
@@ -460,6 +463,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
|||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||||
|
github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go=
|
||||||
|
github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
|
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
|
||||||
@@ -641,9 +646,11 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
|||||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
|
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
@@ -732,6 +739,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
@@ -826,12 +834,14 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -843,6 +853,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
Reference in New Issue
Block a user