2 Commits

Author SHA1 Message Date
GL.iNet-Yongping.Xie 3e9e5bb93a feat: support multi-level subdomain certificates
1. Added support for multi-level subdomains and certificates, such as
   *.example.com, *.level1.example.com, and *.level2.level1.example.com.
2. Enabled remote device access through domains like
   devId.example.com, devId.level1.example.com, and
   devId.level2.level1.example.com.

Signed-off-by: GL.iNet-Yongping.Xie <yongping.xie@gl-inet.com>
2025-09-26 02:29:17 -07:00
pengyu.lu 3c6c214479 fix: Fixed the issue where the gl/main dependency could not be downloaded 2025-09-25 11:40:26 +08:00
32 changed files with 638 additions and 582 deletions
+40 -8
View File
@@ -35,6 +35,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
@@ -44,7 +45,6 @@ import (
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/valyala/bytebufferpool"
"golang.org/x/net/publicsuffix"
)
type HttpProxySession struct {
@@ -346,13 +346,8 @@ func httpProxyRedirect(srv *RttyServer, c *gin.Context, group string) {
location = fmt.Sprintf("https://%s%s?sid=%s", hostname, cfg.AddrHttpProxy, sid)
log.Info().Msgf("Using IP redirect: %s", location)
} else {
// 域名访问,拼接 devid 子域名
eTLDPlusOne, err := publicsuffix.EffectiveTLDPlusOne(hostname)
if err != nil {
log.Info().Msgf("Error parsing domain: %v", err)
eTLDPlusOne = hostname // fallback
}
location = fmt.Sprintf("https://%s.%s%s?sid=%s", devid, eTLDPlusOne, cfg.AddrHttpProxy, sid)
redirHost := buildRedirectHost(hostname, devid)
location = fmt.Sprintf("https://%s%s?sid=%s", redirHost, cfg.AddrHttpProxy, sid)
log.Info().Msgf("Using domain redirect: %s", location)
}
@@ -624,3 +619,40 @@ func Write302WithCookie(conn net.Conn, location, cookieName, cookieValue string)
)
_, _ = conn.Write([]byte(response))
}
// buildRedirectHost removes the first label of the hostname and prepends devid.
// Rules:
// - "www.example.com" -> "devid.example.com"
// - "www.l1.example.com" -> "devid.l1.example.com"
// - "www.l1.l2.example.com" -> "devid.l1.l2.example.com"
// - Two-level domain "example.com" -> "devid.example.com"
// - Single label / abnormal cases -> "devid." + hostname (fallback)
//
// The input hostname must be a pure hostname without port.
func buildRedirectHost(hostname, devid string) string {
// Allow FQDN with trailing dot like "example.com."
hostname = strings.TrimSuffix(hostname, ".")
// Split into labels
labels := strings.Split(hostname, ".")
// Remove empty labels (in case of consecutive dots)
compact := make([]string, 0, len(labels))
for _, l := range labels {
if l != "" {
compact = append(compact, l)
}
}
labels = compact
switch len(labels) {
case 0:
return devid // extreme case: just return devid
case 1:
// Single label (e.g., "localhost") — keep original as suffix
return devid + "." + labels[0]
default:
// >=2: drop the leftmost label
suffix := strings.Join(labels[1:], ".")
return devid + "." + suffix
}
}
Vendored
+1 -1
View File
@@ -8,7 +8,7 @@
*/
/// <reference types="vite/client" />
import type { BaseButton, BaseText } from '@gl/main/components'
import type { BaseButton, BaseText } from 'gl-web-main/components'
import type BaseSvg from './src/components/base/baseSvg.vue'
declare module 'vue' {
+1 -1
View File
@@ -18,7 +18,6 @@
"prepare": "husky"
},
"dependencies": {
"@gl/main": "0.0.50",
"@vue/eslint-config-typescript": "12.0.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/addon-web-links": "0.11.0",
@@ -26,6 +25,7 @@
"ant-design-vue": "^4.2.6",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
"gl-web-main": "^1.0.0",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"pinia": "^3.0.2",
+2 -2
View File
@@ -27,8 +27,8 @@ import { computed } from 'vue'
import { useAppStore } from './stores/modules/app'
import { RouterView } from 'vue-router'
import type { ThemeConfig } from 'ant-design-vue/es/config-provider/context'
import { ConfigProvider as GlConfigProvider } from '@gl/main/components'
import { Languages } from '@gl/main'
import { ConfigProvider as GlConfigProvider } from 'gl-web-main/components'
import { Languages } from 'gl-web-main'
const appStore = useAppStore()
+1 -1
View File
@@ -9,7 +9,7 @@
import { NotNeedHandledRequestErrorCodeList, RequestErrorCodeEnum } from '@/models/request'
import { useUserStore } from '@/stores/modules/user'
import { showErrorMessage } from './requestError'
import { BaseResponse, HttpService } from '@gl/main'
import { BaseResponse, HttpService } from 'gl-web-main'
import type { AxiosResponse } from 'axios'
export const httpService = new HttpService(
@@ -19,8 +19,8 @@
</template>
<script setup lang="ts" generic="T">
import type { SelectOptions } from '@gl/main'
import { BaseDivider, BaseDropdownSelect } from '@gl/main/components'
import type { SelectOptions } from 'gl-web-main'
import { BaseDivider, BaseDropdownSelect } from 'gl-web-main/components'
import type { Trigger } from 'ant-design-vue/es/dropdown/props'
import { computed } from 'vue'
+2 -2
View File
@@ -36,8 +36,8 @@
<script setup lang="ts" generic="T extends AnyObject">
import type { DropdownGroupItem } from '@/models/component'
import type { AnyObject } from '@gl/main'
import { BaseDivider } from '@gl/main/components'
import type { AnyObject } from 'gl-web-main'
import { BaseDivider } from 'gl-web-main/components'
import { Dropdown, Menu, Radio, RadioGroup } from 'ant-design-vue'
import type { Trigger } from 'ant-design-vue/es/dropdown/props'
import { computed, reactive, watch } from 'vue'
+1 -1
View File
@@ -46,7 +46,7 @@
</template>
<script setup lang="ts">
import { debounce } from '@gl/main'
import { debounce } from 'gl-web-main'
import { ref } from 'vue'
const props = withDefaults(defineProps<{
+1 -1
View File
@@ -24,7 +24,7 @@
<script setup lang="ts" generic="T extends BaseData" >
import type { BaseTableProps } from '@/models/component'
import { isEmpty, type BaseData } from '@gl/main'
import { isEmpty, type BaseData } from 'gl-web-main'
import { Table } from 'ant-design-vue'
import { useSlots } from 'vue'
+1 -1
View File
@@ -24,7 +24,7 @@
</template>
<script setup lang="ts">
import { ViewType } from '@gl/main'
import { ViewType } from 'gl-web-main'
import { Tooltip } from 'ant-design-vue'
const props = withDefaults(defineProps<{
+2 -2
View File
@@ -53,11 +53,11 @@
<script setup lang="ts">
import useLanguage from '@/hooks/useLanguage'
import { languageOptions, Languages } from '@gl/main'
import { languageOptions, Languages } from 'gl-web-main'
import BaseStep from './baseStep.vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores/modules/user'
import { BaseDropdownSelect } from '@gl/main/components'
import { BaseDropdownSelect } from 'gl-web-main/components'
import { isForeignEnv } from '@/utils'
const route = useRoute()
+1 -1
View File
@@ -6,7 +6,7 @@
* @FilePath: \kvm-cloud-frontend\src\hooks\useDeviceQueue.ts
* @Description: 自动发现设备队列
*/
import { AnyObject, deepClone, isEmpty } from '@gl/main'
import { AnyObject, deepClone, isEmpty } from 'gl-web-main'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
const DEFAULT_QUEUE_INTERVAL = 400
+1 -1
View File
@@ -11,7 +11,7 @@ import i18n from '@/lang'
import { languageLabelMap } from '@/models/setting'
import { computed } from 'vue'
import { LocalStorageKeys, useLocalStorage } from './useLocalStorage'
import { Languages } from '@gl/main'
import { Languages } from 'gl-web-main'
/** 语言hook */
export default function useLanguage () {
// @ts-ignore
+1 -1
View File
@@ -6,7 +6,7 @@
* @FilePath: \gl-cloud-frontend\src\hooks\usePageLink.js
* @Description: 用于处理分页相关的逻辑
*/
import type { AnyObject, TableDataResponse } from '@gl/main'
import type { AnyObject, TableDataResponse } from 'gl-web-main'
import { computed, ref } from 'vue'
export const GLOBAL_PAGE_SIZE = 'GLOBAL_PAGE_SIZE'
+1 -1
View File
@@ -8,7 +8,7 @@
*/
import { computed } from 'vue'
import useLanguage from './useLanguage'
import type { SelectOptions } from '@gl/main'
import type { SelectOptions } from 'gl-web-main'
export const useTranslatedOptions = <T>(options: SelectOptions<T>[]) => {
const { t } = useLanguage()
+1 -1
View File
@@ -10,7 +10,7 @@ import { createI18n } from 'vue-i18n'
import zh from './locales/zh.json'
import en from './locales/en.json'
import useLanguage from '@/hooks/useLanguage'
import { Languages } from '@gl/main'
import { Languages } from 'gl-web-main'
const i18n = createI18n({
legacy: false,
+1 -1
View File
@@ -8,7 +8,7 @@
*/
import { createApp } from 'vue'
import '@gl/main/style.css'
import 'gl-web-main/style.css'
import '@/styles/index.scss'
import App from './App.vue'
import projectInitialize from './projectInitialize'
+1 -1
View File
@@ -6,7 +6,7 @@
* @FilePath: \kvm-cloud-frontend\src\models\component.ts
* @Description: 组件有关的models
*/
import type { SelectOptions } from '@gl/main'
import type { SelectOptions } from 'gl-web-main'
import type { TableProps } from 'ant-design-vue'
export interface DropdownGroupItem<T = any> {
+1 -1
View File
@@ -7,7 +7,7 @@
* @Description: 设置相关类型声明
*/
import { Languages, SelectOptions } from '@gl/main'
import { Languages, SelectOptions } from 'gl-web-main'
/** 语言对应的label映射 */
export const languageLabelMap = new Map<Languages, string>([
+1 -1
View File
@@ -10,7 +10,7 @@ import type { App } from 'vue'
import '@/assets/iconfont/iconfont.js'
import BaseSvg from '@/components/base/baseSvg.vue'
import { BaseButton, BaseText } from '@gl/main/components'
import { BaseButton, BaseText } from 'gl-web-main/components'
/** 全局注册自定义组件 */
const installComponent = function (app: App) {
+1 -1
View File
@@ -7,7 +7,7 @@
* @Description: app相关状态存储
*/
import { LocalStorageKeys, useLocalStorage } from '@/hooks/useLocalStorage'
import { baseTheme, createStyleInsert, darkTheme, replaceAntTheme, ThemeMode } from '@gl/main'
import { baseTheme, createStyleInsert, darkTheme, replaceAntTheme, ThemeMode } from 'gl-web-main'
import type { ThemeConfig } from 'ant-design-vue/es/config-provider/context'
import { defineStore } from 'pinia'
import { computed, reactive } from 'vue'
+1 -1
View File
@@ -8,7 +8,7 @@
*/
import { getDeviceListApi } from '@/api/device'
import { type DeviceInfo, type DeviceQuery } from '@/models/device'
import { PageLink } from '@gl/main'
import { PageLink } from 'gl-web-main'
import { defineStore } from 'pinia'
import { computed, reactive, ref, watch } from 'vue'
+1 -1
View File
@@ -8,7 +8,7 @@
*/
import { t } from '@/hooks/useLanguage'
import { glConfirm, type BaseConfirmProps } from '@gl/main'
import { glConfirm, type BaseConfirmProps } from 'gl-web-main'
import { message } from 'ant-design-vue'
import { ValidateErrorEntity } from 'ant-design-vue/es/form/interface'
import { AxiosError } from 'axios'
@@ -31,9 +31,9 @@
<script setup lang="ts">
import { reactive, watch } from 'vue'
import { BaseModal } from '@gl/main/components'
import { BaseModal } from 'gl-web-main/components'
import { getAddDeviceScriptInfoApi } from '@/api/device'
import { copyText } from '@gl/main'
import { copyText } from 'gl-web-main'
import { message } from 'ant-design-vue'
import { t } from '@/hooks/useLanguage'
@@ -26,7 +26,7 @@
</template>
<script setup lang="ts">
import { BaseModal } from '@gl/main/components'
import { BaseModal } from 'gl-web-main/components'
const props = defineProps<{ open: boolean, type: string, res: any }>()
@@ -79,8 +79,8 @@ import { reqExecuteCommand } from '@/api/device'
import BaseTable from '@/components/base/baseTable.vue'
import { t } from '@/hooks/useLanguage'
import { DeviceInfo, ExecuteCommandFormData } from '@/models/device'
import { useFakeUpgradeProgress } from '@gl/main'
import { BaseModal } from '@gl/main/components'
import { useFakeUpgradeProgress } from 'gl-web-main'
import { BaseModal } from 'gl-web-main/components'
import { TableColumnType } from 'ant-design-vue'
import { computed, reactive, watch } from 'vue'
import CmdResDetailDialog from './cmdResDetailDialog.vue'
@@ -64,8 +64,8 @@
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import { BaseModal } from '@gl/main/components'
import { FormRules, OnBeforeOk, useValidateInfo } from '@gl/main'
import { BaseModal } from 'gl-web-main/components'
import { FormRules, OnBeforeOk, useValidateInfo } from 'gl-web-main'
import { t } from '@/hooks/useLanguage'
import { DeviceInfo, ExecuteCommandFormData } from '@/models/device'
import { FormInstance } from 'ant-design-vue'
+1 -1
View File
@@ -44,7 +44,7 @@ import '@xterm/xterm/css/xterm.css'
import OverlayAddon from './components/xterm-addon-overlay'
import ContextMenu from './components/contextMenu.vue'
import RttyKeyboard from './components/rttyKeyboard.vue'
import { BaseModal } from '@gl/main/components'
import { BaseModal } from 'gl-web-main/components'
import { message, Modal } from 'ant-design-vue'
import { t } from '@/hooks/useLanguage'
+1 -1
View File
@@ -47,7 +47,7 @@ import NoDevicePage from './components/noDevicePage.vue'
import { useDeviceStore } from '@/stores/modules/device'
import DeviceListView from './components/deviceListView.vue'
import { reactive, onBeforeUnmount, onMounted } from 'vue'
import { BaseLoading } from '@gl/main/components'
import { BaseLoading } from 'gl-web-main/components'
import AddDeviceDialog from './components/addDeviceDialog.vue'
const deviceStore = useDeviceStore()
+2 -2
View File
@@ -44,8 +44,8 @@ import LoginBox from './components/loginBox.vue'
import { computed, reactive, ref } from 'vue'
import { t } from '@/hooks/useLanguage'
import { useUserStore } from '@/stores/modules/user'
import { useValidateInfo, type FormRules } from '@gl/main'
import { GlPassword } from '@gl/main/components'
import { useValidateInfo, type FormRules } from 'gl-web-main'
import { GlPassword } from 'gl-web-main/components'
import { useRouter } from 'vue-router'
import { LoginParams } from '@/models/user'
import { message } from 'ant-design-vue'
+8 -8
View File
@@ -23,35 +23,35 @@ export default defineConfig(({ mode }) => {
port: 3011,
proxy: {
'/devs': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
secure: false,
},
'/signin': {
target: 'https://49.7.174.146:1443/',
target: 'https://106.55.158.199',
secure: false,
},
'/signout': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
secure: false,
},
'/alive': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
secure: false,
},
'/get': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
secure: false,
},
'^/cmd/.*': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
secure: false,
},
'^/connect/.*': {
ws: true,
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
},
'^/web/*': {
target: 'https://49.7.174.146:1443',
target: 'https://106.55.158.199',
},
},
},
+553 -529
View File
File diff suppressed because it is too large Load Diff