fix28: полный production-фикс графиков, machine-auth, i18n и peer import/export
This commit is contained in:
@@ -228,6 +228,9 @@ func ExportHysteria2Config(c *gin.Context) {
|
|||||||
http.Insecure = &authHttpInsecure
|
http.Insecure = &authHttpInsecure
|
||||||
auth.HTTP = &http
|
auth.HTTP = &http
|
||||||
hysteria2ServerConfig.Auth = &auth
|
hysteria2ServerConfig.Auth = &auth
|
||||||
|
if hysteria2ServerConfig.TrafficStats == nil {
|
||||||
|
hysteria2ServerConfig.TrafficStats = &bo.ServerConfigTrafficStats{}
|
||||||
|
}
|
||||||
hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret
|
hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret
|
||||||
|
|
||||||
fileName := fmt.Sprintf("Hysteria2Config-%s.yaml", time.Now().Format("20060102150405"))
|
fileName := fmt.Sprintf("Hysteria2Config-%s.yaml", time.Now().Format("20060102150405"))
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ export default {
|
|||||||
more: "More",
|
more: "More",
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
|
orchestratorManaged: "Managed by hy2xs-orchestrator reconfigure",
|
||||||
huiWebPort: "HY2XS admin Web Port",
|
huiWebPort: "HY2XS admin Web Port",
|
||||||
huiWebContext: "HY2XS admin Web Context",
|
huiWebContext: "HY2XS admin Web Context",
|
||||||
hysteria2TrafficTime: "Hysteria2 Traffic Time",
|
hysteria2TrafficTime: "Hysteria2 Traffic Time",
|
||||||
@@ -185,6 +186,10 @@ export default {
|
|||||||
"Scheduled task expression, reference: https://pkg.go.dev/github.com/robfig/cron/v3",
|
"Scheduled task expression, reference: https://pkg.go.dev/github.com/robfig/cron/v3",
|
||||||
resetTrafficMonth: "Run once a month, midnight, first of month",
|
resetTrafficMonth: "Run once a month, midnight, first of month",
|
||||||
resetTrafficWeek: "Run once a week, midnight between Sat/Sun",
|
resetTrafficWeek: "Run once a week, midnight between Sat/Sun",
|
||||||
|
mustBeInteger: "Field must be an integer",
|
||||||
|
invalidWebContext:
|
||||||
|
"Field must start with / and contain only lowercase letters (a-z) and numbers (0-9)",
|
||||||
|
invalidTrafficTime: "Field must be a number with up to one decimal place",
|
||||||
},
|
},
|
||||||
monitor: {
|
monitor: {
|
||||||
huiVersion: "HY2XS admin Version",
|
huiVersion: "HY2XS admin Version",
|
||||||
@@ -200,6 +205,21 @@ export default {
|
|||||||
},
|
},
|
||||||
log: {
|
log: {
|
||||||
numLine: "Number of lines",
|
numLine: "Number of lines",
|
||||||
|
level: "Level",
|
||||||
|
message: "Message",
|
||||||
|
time: "Time",
|
||||||
|
},
|
||||||
|
errorPage: {
|
||||||
|
back: "Back",
|
||||||
|
oops: "Oops!",
|
||||||
|
forbiddenTitle: "You do not have permission to access this page",
|
||||||
|
forbiddenGo: "Or you can go:",
|
||||||
|
backHome: "Back to Home",
|
||||||
|
forbiddenImageAlt: "Girl has dropped her ice cream.",
|
||||||
|
notFoundHeadline: "The webmaster said that you can not enter this page...",
|
||||||
|
notFoundInfo:
|
||||||
|
"Please check that the URL you entered is correct, or click the button below to return to the homepage.",
|
||||||
|
notFoundBackHome: "Back to home",
|
||||||
},
|
},
|
||||||
hysteria: {
|
hysteria: {
|
||||||
enable: "Enable",
|
enable: "Enable",
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export default {
|
|||||||
releaseKickTip: "Снять офлайн-статус",
|
releaseKickTip: "Снять офлайн-статус",
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
|
orchestratorManaged: "Управляется hy2xs-orchestrator reconfigure",
|
||||||
huiWebPort: "Порт HY2XS admin",
|
huiWebPort: "Порт HY2XS admin",
|
||||||
huiWebContext: "Web-контекст HY2XS admin",
|
huiWebContext: "Web-контекст HY2XS admin",
|
||||||
hysteria2TrafficTime: "Период учёта трафика Hysteria2",
|
hysteria2TrafficTime: "Период учёта трафика Hysteria2",
|
||||||
@@ -179,6 +180,10 @@ export default {
|
|||||||
resetTrafficCronTip: "Cron-выражение для планового сброса трафика",
|
resetTrafficCronTip: "Cron-выражение для планового сброса трафика",
|
||||||
resetTrafficMonth: "Раз в месяц, в полночь первого дня",
|
resetTrafficMonth: "Раз в месяц, в полночь первого дня",
|
||||||
resetTrafficWeek: "Раз в неделю, в полночь между субботой и воскресеньем",
|
resetTrafficWeek: "Раз в неделю, в полночь между субботой и воскресеньем",
|
||||||
|
mustBeInteger: "Поле должно быть целым числом",
|
||||||
|
invalidWebContext:
|
||||||
|
"Поле должно начинаться с / и содержать только строчные буквы (a-z) и цифры (0-9)",
|
||||||
|
invalidTrafficTime: "Поле должно быть числом максимум с одним знаком после запятой",
|
||||||
},
|
},
|
||||||
monitor: {
|
monitor: {
|
||||||
huiVersion: "Версия HY2XS admin",
|
huiVersion: "Версия HY2XS admin",
|
||||||
@@ -194,6 +199,21 @@ export default {
|
|||||||
},
|
},
|
||||||
log: {
|
log: {
|
||||||
numLine: "Количество строк",
|
numLine: "Количество строк",
|
||||||
|
level: "Уровень",
|
||||||
|
message: "Сообщение",
|
||||||
|
time: "Время",
|
||||||
|
},
|
||||||
|
errorPage: {
|
||||||
|
back: "Назад",
|
||||||
|
oops: "Упс!",
|
||||||
|
forbiddenTitle: "У вас нет прав для доступа к этой странице",
|
||||||
|
forbiddenGo: "Или вы можете перейти:",
|
||||||
|
backHome: "На главную",
|
||||||
|
forbiddenImageAlt: "Девочка уронила своё мороженое.",
|
||||||
|
notFoundHeadline: "Вебмастер сообщил, что вам нельзя на эту страницу...",
|
||||||
|
notFoundInfo:
|
||||||
|
"Проверьте правильность URL или нажмите кнопку ниже, чтобы вернуться на главную страницу.",
|
||||||
|
notFoundBackHome: "На главную",
|
||||||
},
|
},
|
||||||
hysteria: {
|
hysteria: {
|
||||||
enable: "Включить",
|
enable: "Включить",
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-alert
|
<el-alert
|
||||||
title="Managed by hy2xs-orchestrator reconfigure"
|
:title="$t('config.orchestratorManaged')"
|
||||||
type="info"
|
type="info"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
style="margin-bottom: 12px"
|
style="margin-bottom: 12px"
|
||||||
@@ -205,7 +205,7 @@ const dataFormRules = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: /^\d+$/,
|
pattern: /^\d+$/,
|
||||||
message: "field must be a integer",
|
message: t("config.mustBeInteger"),
|
||||||
trigger: ["change", "blur"],
|
trigger: ["change", "blur"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -217,7 +217,7 @@ const dataFormRules = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: /^\/([a-z0-9]+(\/[a-z0-9]+)*)?$/,
|
pattern: /^\/([a-z0-9]+(\/[a-z0-9]+)*)?$/,
|
||||||
message: "field must start with / and contain only lowercase letters (a-z) and numbers (0-9)",
|
message: t("config.invalidWebContext"),
|
||||||
trigger: ["change", "blur"],
|
trigger: ["change", "blur"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -229,7 +229,7 @@ const dataFormRules = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
pattern: /^\d+(\.\d)?$/,
|
pattern: /^\d+(\.\d)?$/,
|
||||||
message: "field must be a number with up to one decimal place",
|
message: t("config.invalidTrafficTime"),
|
||||||
trigger: ["change", "blur"],
|
trigger: ["change", "blur"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -160,11 +160,16 @@ const loadDashboard = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const xAxisLabels = computed(() => {
|
const trafficXAxisLabels = computed(() => {
|
||||||
const allTs = new Set<number>();
|
const allTs = new Set<number>();
|
||||||
for (const item of timeseries.value.traffic || []) {
|
for (const item of timeseries.value.traffic || []) {
|
||||||
allTs.add(item.ts);
|
allTs.add(item.ts);
|
||||||
}
|
}
|
||||||
|
return Array.from(allTs).sort((a, b) => a - b);
|
||||||
|
});
|
||||||
|
|
||||||
|
const systemXAxisLabels = computed(() => {
|
||||||
|
const allTs = new Set<number>();
|
||||||
for (const item of timeseries.value.system || []) {
|
for (const item of timeseries.value.system || []) {
|
||||||
allTs.add(item.ts);
|
allTs.add(item.ts);
|
||||||
}
|
}
|
||||||
@@ -199,7 +204,7 @@ const trafficChartOption = computed(() => ({
|
|||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
data: xAxisLabels.value.map((ts) => timestampToDateTime(ts)),
|
data: trafficXAxisLabels.value.map((ts) => timestampToDateTime(ts)),
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
@@ -213,14 +218,14 @@ const trafficChartOption = computed(() => ({
|
|||||||
type: "line",
|
type: "line",
|
||||||
smooth: true,
|
smooth: true,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
data: xAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.download || 0),
|
data: trafficXAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.download || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("dashboard.upload"),
|
name: t("dashboard.upload"),
|
||||||
type: "line",
|
type: "line",
|
||||||
smooth: true,
|
smooth: true,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
data: xAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.upload || 0),
|
data: trafficXAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.upload || 0),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
@@ -237,7 +242,7 @@ const systemChartOption = computed(() => ({
|
|||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
data: xAxisLabels.value.map((ts) => timestampToDateTime(ts)),
|
data: systemXAxisLabels.value.map((ts) => timestampToDateTime(ts)),
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: "value",
|
type: "value",
|
||||||
@@ -251,14 +256,14 @@ const systemChartOption = computed(() => ({
|
|||||||
type: "line",
|
type: "line",
|
||||||
smooth: true,
|
smooth: true,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
data: xAxisLabels.value.map((ts) => systemMap.value.get(ts)?.cpu || 0),
|
data: systemXAxisLabels.value.map((ts) => systemMap.value.get(ts)?.cpu || 0),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t("dashboard.ram"),
|
name: t("dashboard.ram"),
|
||||||
type: "line",
|
type: "line",
|
||||||
smooth: true,
|
smooth: true,
|
||||||
showSymbol: false,
|
showSymbol: false,
|
||||||
data: xAxisLabels.value.map((ts) => systemMap.value.get(ts)?.mem || 0),
|
data: systemXAxisLabels.value.map((ts) => systemMap.value.get(ts)?.mem || 0),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ export default {
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, toRefs } from "vue";
|
import { reactive, toRefs } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
errGif: new URL(`../../assets/401_images/401.gif`, import.meta.url).href,
|
errGif: new URL(`../../assets/401_images/401.gif`, import.meta.url).href,
|
||||||
@@ -24,15 +27,15 @@ function back() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="errPage-container">
|
<div class="errPage-container">
|
||||||
<el-button class="pan-back-btn" @click="back">Back</el-button>
|
<el-button class="pan-back-btn" @click="back">{{ $t("errorPage.back") }}</el-button>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<h1 class="text-jumbo text-ginormous">Oops!</h1>
|
<h1 class="text-jumbo text-ginormous">{{ $t("errorPage.oops") }}</h1>
|
||||||
<h2>You do not have permission to access this page</h2>
|
<h2>{{ $t("errorPage.forbiddenTitle") }}</h2>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li>Or you can go:</li>
|
<li>{{ $t("errorPage.forbiddenGo") }}</li>
|
||||||
<li class="link-type">
|
<li class="link-type">
|
||||||
<router-link to="/">Back to Home</router-link>
|
<router-link to="/">{{ $t("errorPage.backHome") }}</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -41,7 +44,7 @@ function back() {
|
|||||||
:src="errGif"
|
:src="errGif"
|
||||||
width="313"
|
width="313"
|
||||||
height="428"
|
height="428"
|
||||||
alt="Girl has dropped her ice cream."
|
:alt="t('errorPage.forbiddenImageAlt')"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
function message() {
|
function message() {
|
||||||
return "The webmaster said that you can not enter this page...";
|
return t("errorPage.notFoundHeadline");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -37,13 +41,12 @@ function message() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="bullshit">
|
<div class="bullshit">
|
||||||
<div class="bullshit__oops">OOPS!</div>
|
<div class="bullshit__oops">{{ $t("errorPage.oops") }}</div>
|
||||||
<div class="bullshit__headline">{{ message() }}</div>
|
<div class="bullshit__headline">{{ message() }}</div>
|
||||||
<div class="bullshit__info">
|
<div class="bullshit__info">
|
||||||
Please check that the URL you entered is correct, or click the button
|
{{ $t("errorPage.notFoundInfo") }}
|
||||||
below to return to the homepage.
|
|
||||||
</div>
|
</div>
|
||||||
<a href="" class="bullshit__return-home">Back to home</a>
|
<a href="/#/" class="bullshit__return-home">{{ $t("errorPage.notFoundBackHome") }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,18 @@
|
|||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<el-button type="primary" @click="handleAdd">{{ $t("common.add") }}</el-button>
|
<el-button type="primary" @click="handleAdd">{{ $t("common.add") }}</el-button>
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="importFileList"
|
||||||
|
:http-request="handleImport"
|
||||||
|
:show-file-list="false"
|
||||||
|
accept=".json"
|
||||||
|
:limit="1"
|
||||||
|
:before-upload="beforeImport"
|
||||||
|
class="inline-block ml-2"
|
||||||
|
>
|
||||||
|
<el-button>{{ $t("common.import") }}</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-button class="ml-2" @click="handleExport">{{ $t("common.export") }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-loading="loading" :data="records">
|
<el-table v-loading="loading" :data="records">
|
||||||
<el-table-column :label="$t('peer.name')" min-width="220">
|
<el-table-column :label="$t('peer.name')" min-width="220">
|
||||||
@@ -88,7 +100,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="qrDialog" title="QR" width="420px">
|
<el-dialog v-model="qrDialog" :title="$t('peer.clientQr')" width="420px">
|
||||||
<el-image style="width:300px;height:300px" :src="qrSrc" />
|
<el-image style="width:300px;height:300px" :src="qrSrc" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -127,21 +139,24 @@
|
|||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { formatBytes } from "@/utils/byte";
|
import { formatBytes } from "@/utils/byte";
|
||||||
import { getMonthLater, timestampToDateTime } from "@/utils/time";
|
import { getMonthLater, timestampToDateTime } from "@/utils/time";
|
||||||
import {
|
import {
|
||||||
deletePeerApi,
|
deletePeerApi,
|
||||||
getPeerApi,
|
getPeerApi,
|
||||||
getPeerClientConfigApi,
|
getPeerClientConfigApi,
|
||||||
|
importPeerApi,
|
||||||
kickPeerApi,
|
kickPeerApi,
|
||||||
pagePeerApi,
|
pagePeerApi,
|
||||||
releaseKickPeerApi,
|
releaseKickPeerApi,
|
||||||
resetPeerTrafficApi,
|
resetPeerTrafficApi,
|
||||||
savePeerApi,
|
savePeerApi,
|
||||||
updatePeerApi,
|
updatePeerApi,
|
||||||
|
exportPeerApi,
|
||||||
} from "@/api/peer";
|
} from "@/api/peer";
|
||||||
import { PeerPageDto, PeerSaveDto, PeerUpdateDto, PeerVo } from "@/api/peer/types";
|
import { PeerPageDto, PeerSaveDto, PeerUpdateDto, PeerVo } from "@/api/peer/types";
|
||||||
|
import { UploadFile, UploadRawFile, UploadRequestOptions } from "element-plus/lib/components";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -149,6 +164,7 @@ const total = ref(0);
|
|||||||
const records = ref<PeerVo[]>([]);
|
const records = ref<PeerVo[]>([]);
|
||||||
const qrDialog = ref(false);
|
const qrDialog = ref(false);
|
||||||
const qrSrc = ref("");
|
const qrSrc = ref("");
|
||||||
|
const importFileList = ref<UploadFile[]>([]);
|
||||||
const overviewQrSrc = ref("");
|
const overviewQrSrc = ref("");
|
||||||
const overviewClientUrl = ref("");
|
const overviewClientUrl = ref("");
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
@@ -281,6 +297,47 @@ async function showQr(row: PeerVo) {
|
|||||||
qrDialog.value = true;
|
qrDialog.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleImport(params: UploadRequestOptions) {
|
||||||
|
if (importFileList.value.length <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", params.file);
|
||||||
|
await importPeerApi(formData);
|
||||||
|
importFileList.value = [];
|
||||||
|
await handleQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeImport(file: UploadRawFile) {
|
||||||
|
if (!file.name.endsWith(".json")) {
|
||||||
|
ElMessage.error(t("common.fileFormatUnsupported"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.size / 1024 / 1024 > 2) {
|
||||||
|
ElMessage.error(t("common.fileTooLarge"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
try {
|
||||||
|
const response = await exportPeerApi();
|
||||||
|
const blob = new Blob([response.data], { type: "application/octet-stream" });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.href = url;
|
||||||
|
const dis = response.headers["content-disposition"] || "";
|
||||||
|
a.download = dis.split("attachment; filename=")[1] || "peers-export.json";
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
ElMessage.success(t("common.downloadSuccess"));
|
||||||
|
} catch {
|
||||||
|
ElMessage.error(t("common.invalid"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(handleQuery);
|
onMounted(handleQuery);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"hy2xs-admin/dao"
|
||||||
|
"hy2xs-admin/model/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MachineAuthHandler() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
secretCfg, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret)
|
||||||
|
if err != nil || secretCfg.Value == nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||||
|
"code": http.StatusForbidden,
|
||||||
|
"type": "no",
|
||||||
|
"message": "machine auth failed",
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := strings.TrimSpace(*secretCfg.Value)
|
||||||
|
if expected == "" {
|
||||||
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||||
|
"code": http.StatusForbidden,
|
||||||
|
"type": "no",
|
||||||
|
"message": "machine auth not configured",
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
provided := strings.TrimSpace(c.Query("access_token"))
|
||||||
|
if provided == "" {
|
||||||
|
provided = strings.TrimSpace(c.GetHeader("X-HY2XS-Machine-Token"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if provided == "" || provided != expected {
|
||||||
|
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||||
|
"code": http.StatusForbidden,
|
||||||
|
"type": "no",
|
||||||
|
"message": "machine auth failed",
|
||||||
|
"data": nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ func Router(router *gin.Engine, huiWebContext *string) {
|
|||||||
globalGroup := router.Group(relativePath)
|
globalGroup := router.Group(relativePath)
|
||||||
|
|
||||||
machineApi := globalGroup.Group("/hui")
|
machineApi := globalGroup.Group("/hui")
|
||||||
machineApi.Use(middleware.LocalOnlyHandler(), middleware.LogHandler())
|
machineApi.Use(middleware.LocalOnlyHandler(), middleware.MachineAuthHandler(), middleware.LogHandler())
|
||||||
initHysteria2MachineAuthRouter(machineApi)
|
initHysteria2MachineAuthRouter(machineApi)
|
||||||
|
|
||||||
authApi := globalGroup.Group("/hui")
|
authApi := globalGroup.Group("/hui")
|
||||||
|
|||||||
+41
-3
@@ -9,6 +9,8 @@ import (
|
|||||||
"hy2xs-admin/model/bo"
|
"hy2xs-admin/model/bo"
|
||||||
"hy2xs-admin/model/constant"
|
"hy2xs-admin/model/constant"
|
||||||
"hy2xs-admin/model/entity"
|
"hy2xs-admin/model/entity"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -88,6 +90,9 @@ func UpdateHysteria2Config(hysteria2ServerConfig bo.Hysteria2ServerConfig) error
|
|||||||
http.Insecure = &authHttpInsecure
|
http.Insecure = &authHttpInsecure
|
||||||
auth.HTTP = &http
|
auth.HTTP = &http
|
||||||
hysteria2ServerConfig.Auth = &auth
|
hysteria2ServerConfig.Auth = &auth
|
||||||
|
if hysteria2ServerConfig.TrafficStats == nil {
|
||||||
|
hysteria2ServerConfig.TrafficStats = &bo.ServerConfigTrafficStats{}
|
||||||
|
}
|
||||||
hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret
|
hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret
|
||||||
|
|
||||||
yamlConfig, err := yaml.Marshal(&hysteria2ServerConfig)
|
yamlConfig, err := yaml.Marshal(&hysteria2ServerConfig)
|
||||||
@@ -119,15 +124,40 @@ func GetHysteria2ApiPort() (int64, error) {
|
|||||||
logrus.Errorf(errMsg)
|
logrus.Errorf(errMsg)
|
||||||
return 0, errors.New(errMsg)
|
return 0, errors.New(errMsg)
|
||||||
}
|
}
|
||||||
apiPort, err := strconv.ParseInt(strings.Split(*hysteria2Config.TrafficStats.Listen, ":")[1], 10, 64)
|
apiPort, err := parseTrafficStatsPort(*hysteria2Config.TrafficStats.Listen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := fmt.Sprintf("apiPort: %s is invalid", *hysteria2Config.TrafficStats.Listen)
|
errMsg := fmt.Sprintf("apiPort: %s is invalid: %v", *hysteria2Config.TrafficStats.Listen, err)
|
||||||
logrus.Errorf(errMsg)
|
logrus.Errorf(errMsg)
|
||||||
return 0, errors.New(errMsg)
|
return 0, errors.New(errMsg)
|
||||||
}
|
}
|
||||||
return apiPort, nil
|
return apiPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseTrafficStatsPort(listen string) (int64, error) {
|
||||||
|
trimmed := strings.TrimSpace(listen)
|
||||||
|
if trimmed == "" {
|
||||||
|
return 0, errors.New("empty listen")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPort := trimmed
|
||||||
|
if strings.HasPrefix(trimmed, ":") {
|
||||||
|
hostPort = "127.0.0.1" + trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
_, portStr, err := net.SplitHostPort(hostPort)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
port, err := strconv.ParseInt(portStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if port <= 0 || port > 65535 {
|
||||||
|
return 0, errors.New("port out of range")
|
||||||
|
}
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetPortAndCert() (int64, string, string, error) {
|
func GetPortAndCert() (int64, string, string, error) {
|
||||||
configs, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.HUICrtPath, constant.HUIKeyPath})
|
configs, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.HUICrtPath, constant.HUIKeyPath})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -173,5 +203,13 @@ func GetAuthHttpUrl() (string, error) {
|
|||||||
if config.Value != nil && *config.Value != "/" && strings.HasPrefix(*config.Value, "/") {
|
if config.Value != nil && *config.Value != "/" && strings.HasPrefix(*config.Value, "/") {
|
||||||
webContext = *config.Value
|
webContext = *config.Value
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s://127.0.0.1:%d%s/hui/hysteria2/auth", protocol, port, webContext), nil
|
trafficSecretConfig, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
authURL := fmt.Sprintf("%s://127.0.0.1:%d%s/hui/hysteria2/auth", protocol, port, webContext)
|
||||||
|
if trafficSecretConfig.Value != nil && strings.TrimSpace(*trafficSecretConfig.Value) != "" {
|
||||||
|
authURL = fmt.Sprintf("%s?access_token=%s", authURL, url.QueryEscape(strings.TrimSpace(*trafficSecretConfig.Value)))
|
||||||
|
}
|
||||||
|
return authURL, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user