初步实现超管驾驶舱司南页面及司南登记自动部署步骤【不完整】

Signed-off-by: Chenyang Gao <gps949@outlook.com>
This commit is contained in:
Chenyang Gao
2023-04-03 21:41:12 +08:00
parent 2d2e1a22b6
commit 00b2a120a7
16 changed files with 1839 additions and 7 deletions

View File

@@ -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
View 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>

View 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>

View 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>

View File

@@ -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(),

View File

@@ -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">

View File

@@ -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
} }

View File

@@ -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))))

View 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
}

View File

@@ -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,
} }
} }

View File

@@ -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
} }

View File

@@ -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
}

View File

@@ -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,
} }

View File

@@ -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, &registerRequest); 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
View File

@@ -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
View File

@@ -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=