fix(fix3): runtime env hardening and public endpoint source-of-truth
This commit is contained in:
+4
-148
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"hy2xs-admin/dao"
|
||||
"hy2xs-admin/model/bo"
|
||||
"hy2xs-admin/model/constant"
|
||||
@@ -15,8 +14,6 @@ import (
|
||||
"hy2xs-admin/service"
|
||||
"hy2xs-admin/util"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -167,24 +164,7 @@ func GetHysteria2Config(c *gin.Context) {
|
||||
}
|
||||
|
||||
func UpdateHysteria2Config(c *gin.Context) {
|
||||
hysteria2ServerConfig, err := validateField(c, bo.Hysteria2ServerConfig{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = service.UpdateHysteria2Config(hysteria2ServerConfig); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
running := service.Hysteria2IsRunning()
|
||||
if running {
|
||||
if err = service.RestartHysteria2(); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
}
|
||||
vo.Success(nil, c)
|
||||
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
||||
}
|
||||
|
||||
func ExportHysteria2Config(c *gin.Context) {
|
||||
@@ -253,84 +233,7 @@ func ExportHysteria2Config(c *gin.Context) {
|
||||
}
|
||||
|
||||
func ImportHysteria2Config(c *gin.Context) {
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
vo.Fail(constant.SysError, c)
|
||||
return
|
||||
}
|
||||
if header.Size > 1024*1024*2 {
|
||||
vo.Fail("the file is too big", c)
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(header.Filename, ".yaml") {
|
||||
vo.Fail("file format not supported", c)
|
||||
return
|
||||
}
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
vo.Fail("yaml file read err", c)
|
||||
return
|
||||
}
|
||||
var hysteria2ServerConfig bo.Hysteria2ServerConfig
|
||||
if err = yaml.Unmarshal(content, &hysteria2ServerConfig); err != nil {
|
||||
vo.Fail("content Unmarshal err", c)
|
||||
return
|
||||
}
|
||||
|
||||
// Значения по умолчанию
|
||||
config, err := dao.ListConfig("key in ?", []string{constant.HUIWebPort, constant.Hysteria2TrafficStatsSecret})
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
var hUIWebPort string
|
||||
var trafficStatsSecret string
|
||||
for _, item := range config {
|
||||
if *item.Key == constant.HUIWebPort {
|
||||
hUIWebPort = *item.Value
|
||||
} else if *item.Key == constant.Hysteria2TrafficStatsSecret {
|
||||
trafficStatsSecret = *item.Value
|
||||
}
|
||||
}
|
||||
|
||||
if hUIWebPort == "" || trafficStatsSecret == "" {
|
||||
logrus.Errorf("hUIWebPort or trafficStatsSecret is nil")
|
||||
vo.Fail(constant.SysError, c)
|
||||
return
|
||||
}
|
||||
|
||||
authHttpUrl, err := service.GetAuthHttpUrl()
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
authType := "http"
|
||||
authHttpInsecure := true
|
||||
var auth bo.ServerConfigAuth
|
||||
auth.Type = &authType
|
||||
var http bo.ServerConfigAuthHTTP
|
||||
http.URL = &authHttpUrl
|
||||
http.Insecure = &authHttpInsecure
|
||||
auth.HTTP = &http
|
||||
hysteria2ServerConfig.Auth = &auth
|
||||
hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret
|
||||
|
||||
if err = service.SetHysteria2Config(hysteria2ServerConfig); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
|
||||
running := service.Hysteria2IsRunning()
|
||||
if running {
|
||||
if err = service.RestartHysteria2(); err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
vo.Success(nil, c)
|
||||
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
||||
}
|
||||
|
||||
func ExportConfig(c *gin.Context) {
|
||||
@@ -401,56 +304,9 @@ func Hysteria2AcmePath(c *gin.Context) {
|
||||
}
|
||||
|
||||
func RestartServer(c *gin.Context) {
|
||||
go func() {
|
||||
_ = service.StopServer()
|
||||
}()
|
||||
vo.Success(nil, c)
|
||||
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
||||
}
|
||||
|
||||
func UploadCertFile(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
vo.Fail(constant.SysError, c)
|
||||
return
|
||||
}
|
||||
ext := filepath.Ext(file.Filename)
|
||||
if ext != ".crt" && ext != ".key" {
|
||||
vo.Fail("file format not supported", c)
|
||||
return
|
||||
}
|
||||
if file.Size > 1024*1024 {
|
||||
vo.Fail("the file is too big", c)
|
||||
return
|
||||
}
|
||||
err = filepath.WalkDir(constant.BinDir, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileExt := filepath.Ext(path)
|
||||
if !d.IsDir() && fileExt == ext {
|
||||
if err := os.Remove(path); err != nil {
|
||||
return fmt.Errorf("failed to delete file: %s, error: %v", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logrus.Errorf("error during file deletion: %v", err)
|
||||
vo.Fail("delete file failed", c)
|
||||
return
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
vo.Fail(constant.SysError, c)
|
||||
return
|
||||
}
|
||||
safeFilename := filepath.Base(file.Filename)
|
||||
certPath := filepath.Join(wd, constant.BinDir, safeFilename)
|
||||
|
||||
if err := c.SaveUploadedFile(file, certPath); err != nil {
|
||||
vo.Fail("file upload failed", c)
|
||||
return
|
||||
}
|
||||
vo.Success(certPath, c)
|
||||
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func Hysteria2Url(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
url, err := service.Hysteria2Url(*hysteria2UrlDto.AccountId, *hysteria2UrlDto.Hostname)
|
||||
url, err := service.Hysteria2Url(*hysteria2UrlDto.AccountId)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
@@ -88,8 +88,7 @@ func Hysteria2SubscribeUrl(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
subscribeUrl, err := service.Hysteria2SubscribeUrl(*hysteria2SubscribeUrlDto.AccountId,
|
||||
*hysteria2SubscribeUrlDto.Protocol,
|
||||
*hysteria2SubscribeUrlDto.Host)
|
||||
*hysteria2SubscribeUrlDto.Protocol)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
@@ -114,12 +113,6 @@ func Hysteria2Subscribe(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
userAgent := strings.ToLower(c.Request.Header.Get("User-Agent"))
|
||||
host := c.Request.Host
|
||||
|
||||
if host == "" {
|
||||
vo.Fail("Host is empty", c)
|
||||
return
|
||||
}
|
||||
|
||||
var clientType string
|
||||
if strings.Contains(userAgent, constant.Shadowrocket) {
|
||||
@@ -134,7 +127,7 @@ func Hysteria2Subscribe(c *gin.Context) {
|
||||
clientType = constant.Clash
|
||||
}
|
||||
|
||||
userInfo, configStr, err := service.Hysteria2Subscribe(conPass, clientType, host)
|
||||
userInfo, configStr, err := service.Hysteria2Subscribe(conPass, clientType)
|
||||
if err != nil {
|
||||
vo.Fail(err.Error(), c)
|
||||
return
|
||||
|
||||
@@ -6,12 +6,10 @@ export interface Hysteria2KickDto {
|
||||
export interface Hysteria2SubscribeUrlDto {
|
||||
accountId: number;
|
||||
protocol: string;
|
||||
host: string;
|
||||
}
|
||||
|
||||
export interface Hysteria2UrlDto {
|
||||
accountId: number;
|
||||
hostname: string;
|
||||
}
|
||||
|
||||
export interface Hysteria2SubscribeVo {
|
||||
|
||||
Vendored
-2
@@ -9,8 +9,6 @@ declare module "*.vue" {
|
||||
|
||||
// TypeScript-подсказки для переменных окружения
|
||||
interface ImportMetaEnv {
|
||||
VITE_APP_PORT: string;
|
||||
VITE_APP_BASE_API: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -2,9 +2,10 @@ import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { useAccountStoreHook } from "@/store/modules/account";
|
||||
|
||||
const dynamicBase = (window as any).__dynamic_base__ || "";
|
||||
const API_BASE = "/hui";
|
||||
// Создание axios instance
|
||||
const service = axios.create({
|
||||
baseURL: `${dynamicBase}${import.meta.env.VITE_APP_BASE_API}`,
|
||||
baseURL: `${dynamicBase}${API_BASE}`,
|
||||
timeout: 50000,
|
||||
headers: { "Content-Type": "application/json;charset=utf-8" },
|
||||
});
|
||||
|
||||
@@ -906,7 +906,6 @@ const handleSubscribe = async (row: { [key: string]: any }) => {
|
||||
const dto: Hysteria2SubscribeUrlDto = {
|
||||
accountId: row.id,
|
||||
protocol: window.location.protocol,
|
||||
host: window.location.host,
|
||||
};
|
||||
const { data } = await hysteria2SubscribeUrlApi(dto);
|
||||
copy(data.url);
|
||||
@@ -920,7 +919,6 @@ const handleNodeUrl = async (row: { [key: string]: any }) => {
|
||||
try {
|
||||
const dto: Hysteria2UrlDto = {
|
||||
accountId: row.id,
|
||||
hostname: window.location.hostname,
|
||||
};
|
||||
const { data } = await hysteria2UrlApi(dto);
|
||||
copy(data.url);
|
||||
@@ -934,7 +932,6 @@ const handleQrCode = async (row: { [key: string]: any }) => {
|
||||
try {
|
||||
const dto: Hysteria2UrlDto = {
|
||||
accountId: row.id,
|
||||
hostname: window.location.hostname,
|
||||
};
|
||||
const { data } = await hysteria2UrlApi(dto);
|
||||
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
||||
|
||||
@@ -204,7 +204,6 @@ const handleSubscribe = async () => {
|
||||
const dto: Hysteria2SubscribeUrlDto = {
|
||||
accountId: accountStore.id,
|
||||
protocol: window.location.protocol,
|
||||
host: window.location.host,
|
||||
};
|
||||
const { data } = await hysteria2SubscribeUrlApi(dto);
|
||||
copy(data.url);
|
||||
@@ -219,7 +218,6 @@ const handleSubscribeQrCode = async () => {
|
||||
const dto: Hysteria2SubscribeUrlDto = {
|
||||
accountId: accountStore.id,
|
||||
protocol: window.location.protocol,
|
||||
host: window.location.host,
|
||||
};
|
||||
const { data } = await hysteria2SubscribeUrlApi(dto);
|
||||
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
||||
@@ -233,7 +231,6 @@ const handleNodeUrl = async () => {
|
||||
try {
|
||||
const dto: Hysteria2UrlDto = {
|
||||
accountId: accountStore.id,
|
||||
hostname: window.location.hostname,
|
||||
};
|
||||
const { data } = await hysteria2UrlApi(dto);
|
||||
copy(data.url);
|
||||
@@ -247,7 +244,6 @@ const handleUrlQrCode = async () => {
|
||||
try {
|
||||
const dto: Hysteria2UrlDto = {
|
||||
accountId: accountStore.id,
|
||||
hostname: window.location.hostname,
|
||||
};
|
||||
const { data } = await hysteria2UrlApi(dto);
|
||||
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
|
||||
import { ConfigEnv, defineConfig, loadEnv, UserConfig } from "vite";
|
||||
import { defineConfig, UserConfig } from "vite";
|
||||
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
@@ -20,9 +20,10 @@ const pathSrc = path.resolve(__dirname, "src");
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g;
|
||||
const DRIVE_LETTER_REGEX = /^[a-z]:/i;
|
||||
const DEV_SERVER_PORT = 8080;
|
||||
const API_BASE = "/hui";
|
||||
|
||||
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
const env = loadEnv(mode, process.cwd());
|
||||
export default defineConfig((): UserConfig => {
|
||||
return {
|
||||
base: "/",
|
||||
resolve: {
|
||||
@@ -44,11 +45,11 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
||||
},
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: Number(env.VITE_APP_PORT),
|
||||
port: DEV_SERVER_PORT,
|
||||
open: true, // Automatically open browser on start
|
||||
proxy: {
|
||||
// Reverse proxy for CORS in development
|
||||
[env.VITE_APP_BASE_API]: {
|
||||
[API_BASE]: {
|
||||
target: "http://127.0.0.1:8081",
|
||||
changeOrigin: true,
|
||||
},
|
||||
|
||||
@@ -18,10 +18,8 @@ type Hysteria2VersionDto struct {
|
||||
type Hysteria2SubscribeUrlDto struct {
|
||||
AccountId *int64 `json:"accountId" form:"accountId" validate:"required,gt=0"`
|
||||
Protocol *string `json:"protocol" form:"protocol" validate:"required,min=1,max=8"`
|
||||
Host *string `json:"host" form:"host" validate:"required,min=1,max=301"`
|
||||
}
|
||||
|
||||
type Hysteria2UrlDto struct {
|
||||
AccountId *int64 `json:"accountId" form:"accountId" validate:"required,gt=0"`
|
||||
Hostname *string `json:"hostname" form:"hostname" validate:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
@@ -8,26 +8,27 @@ import (
|
||||
"hy2xs-admin/model/bo"
|
||||
"hy2xs-admin/model/constant"
|
||||
"hy2xs-admin/proxy"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func parseListenPort(listen string) (int, error) {
|
||||
host, port, err := net.SplitHostPort(listen)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
func resolvePublicEndpoint() (string, int, error) {
|
||||
host := strings.TrimSpace(os.Getenv("HY2XS_PUBLIC_HOST"))
|
||||
if host == "" || host == "0.0.0.0" {
|
||||
return "", 0, errors.New("HY2XS_PUBLIC_HOST must be set to public domain or IPv4")
|
||||
}
|
||||
if host == "" || port == "" {
|
||||
return 0, errors.New("invalid listen address")
|
||||
portRaw := strings.TrimSpace(os.Getenv("HY2XS_PUBLIC_PORT"))
|
||||
if portRaw == "" {
|
||||
return "", 0, errors.New("HY2XS_PUBLIC_PORT is required")
|
||||
}
|
||||
value, convErr := strconv.Atoi(port)
|
||||
if convErr != nil || value < 1 || value > 65535 {
|
||||
return 0, errors.New("invalid listen port")
|
||||
port, err := strconv.Atoi(portRaw)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return "", 0, errors.New("HY2XS_PUBLIC_PORT must be a valid TCP port")
|
||||
}
|
||||
return value, nil
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
func Hysteria2Auth(conPass string) (int64, string, error) {
|
||||
@@ -103,11 +104,15 @@ func Hysteria2Kick(ids []int64, kickUtilTime int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Hysteria2SubscribeUrl(accountId int64, protocol string, host string) (string, error) {
|
||||
func Hysteria2SubscribeUrl(accountId int64, protocol string) (string, error) {
|
||||
account, err := dao.GetAccount("id = ?", accountId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
publicHost, publicPort, err := resolvePublicEndpoint()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
config, err := dao.GetConfig("key = ?", constant.HUIWebContext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -116,10 +121,10 @@ func Hysteria2SubscribeUrl(accountId int64, protocol string, host string) (strin
|
||||
if config.Value != nil && *config.Value != "/" && strings.HasPrefix(*config.Value, "/") {
|
||||
webContext = *config.Value
|
||||
}
|
||||
return fmt.Sprintf("%s//%s%s/hui/%s", protocol, host, webContext, url.QueryEscape(*account.ConPass)), nil
|
||||
return fmt.Sprintf("%s//%s:%d%s/hui/%s", protocol, publicHost, publicPort, webContext, url.QueryEscape(*account.ConPass)), nil
|
||||
}
|
||||
|
||||
func Hysteria2Subscribe(conPass string, clientType string, host string) (string, string, error) {
|
||||
func Hysteria2Subscribe(conPass string, clientType string) (string, string, error) {
|
||||
hysteria2Config, err := GetHysteria2Config()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -132,6 +137,10 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string,
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
publicHost, publicPort, err := resolvePublicEndpoint()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
hysteria2Name := "hysteria2"
|
||||
hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark)
|
||||
@@ -144,11 +153,6 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string,
|
||||
|
||||
userInfo := ""
|
||||
configStr := ""
|
||||
listenPort, err := parseListenPort(*hysteria2Config.Listen)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
publicHost := strings.Split(host, ":")[0]
|
||||
if clientType == constant.Shadowrocket || clientType == constant.Clash {
|
||||
userInfo = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d",
|
||||
*account.Upload,
|
||||
@@ -160,7 +164,7 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string,
|
||||
Name: hysteria2Name,
|
||||
Type: "hysteria2",
|
||||
Server: publicHost,
|
||||
Port: strconv.Itoa(listenPort),
|
||||
Port: strconv.Itoa(publicPort),
|
||||
Password: conPass,
|
||||
}
|
||||
|
||||
@@ -224,7 +228,7 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string,
|
||||
}
|
||||
}
|
||||
} else if clientType == constant.V2rayN {
|
||||
hysteria2Url, err := Hysteria2Url(*account.Id, strings.Split(host, ":")[0])
|
||||
hysteria2Url, err := Hysteria2Url(*account.Id)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -234,7 +238,7 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string,
|
||||
return userInfo, configStr, nil
|
||||
}
|
||||
|
||||
func Hysteria2Url(accountId int64, hostname string) (string, error) {
|
||||
func Hysteria2Url(accountId int64) (string, error) {
|
||||
hysteria2Config, err := GetHysteria2Config()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -242,13 +246,10 @@ func Hysteria2Url(accountId int64, hostname string) (string, error) {
|
||||
if hysteria2Config.Listen == nil || *hysteria2Config.Listen == "" {
|
||||
return "", errors.New("hysteria2 config is empty")
|
||||
}
|
||||
port, err := parseListenPort(*hysteria2Config.Listen)
|
||||
hostname, port, err := resolvePublicEndpoint()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if hostname == "" || hostname == "0.0.0.0" {
|
||||
return "", errors.New("invalid public host")
|
||||
}
|
||||
|
||||
account, err := dao.GetAccount("id = ?", accountId)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user