fix(fix3): runtime env hardening and public endpoint source-of-truth
This commit is contained in:
+4
-148
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"hy2xs-admin/dao"
|
"hy2xs-admin/dao"
|
||||||
"hy2xs-admin/model/bo"
|
"hy2xs-admin/model/bo"
|
||||||
"hy2xs-admin/model/constant"
|
"hy2xs-admin/model/constant"
|
||||||
@@ -15,8 +14,6 @@ import (
|
|||||||
"hy2xs-admin/service"
|
"hy2xs-admin/service"
|
||||||
"hy2xs-admin/util"
|
"hy2xs-admin/util"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -167,24 +164,7 @@ func GetHysteria2Config(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateHysteria2Config(c *gin.Context) {
|
func UpdateHysteria2Config(c *gin.Context) {
|
||||||
hysteria2ServerConfig, err := validateField(c, bo.Hysteria2ServerConfig{})
|
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExportHysteria2Config(c *gin.Context) {
|
func ExportHysteria2Config(c *gin.Context) {
|
||||||
@@ -253,84 +233,7 @@ func ExportHysteria2Config(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ImportHysteria2Config(c *gin.Context) {
|
func ImportHysteria2Config(c *gin.Context) {
|
||||||
file, header, err := c.Request.FormFile("file")
|
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExportConfig(c *gin.Context) {
|
func ExportConfig(c *gin.Context) {
|
||||||
@@ -401,56 +304,9 @@ func Hysteria2AcmePath(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RestartServer(c *gin.Context) {
|
func RestartServer(c *gin.Context) {
|
||||||
go func() {
|
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
||||||
_ = service.StopServer()
|
|
||||||
}()
|
|
||||||
vo.Success(nil, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadCertFile(c *gin.Context) {
|
func UploadCertFile(c *gin.Context) {
|
||||||
file, err := c.FormFile("file")
|
vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func Hysteria2Url(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
url, err := service.Hysteria2Url(*hysteria2UrlDto.AccountId, *hysteria2UrlDto.Hostname)
|
url, err := service.Hysteria2Url(*hysteria2UrlDto.AccountId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
@@ -88,8 +88,7 @@ func Hysteria2SubscribeUrl(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
subscribeUrl, err := service.Hysteria2SubscribeUrl(*hysteria2SubscribeUrlDto.AccountId,
|
subscribeUrl, err := service.Hysteria2SubscribeUrl(*hysteria2SubscribeUrlDto.AccountId,
|
||||||
*hysteria2SubscribeUrlDto.Protocol,
|
*hysteria2SubscribeUrlDto.Protocol)
|
||||||
*hysteria2SubscribeUrlDto.Host)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
@@ -114,12 +113,6 @@ func Hysteria2Subscribe(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
userAgent := strings.ToLower(c.Request.Header.Get("User-Agent"))
|
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
|
var clientType string
|
||||||
if strings.Contains(userAgent, constant.Shadowrocket) {
|
if strings.Contains(userAgent, constant.Shadowrocket) {
|
||||||
@@ -134,7 +127,7 @@ func Hysteria2Subscribe(c *gin.Context) {
|
|||||||
clientType = constant.Clash
|
clientType = constant.Clash
|
||||||
}
|
}
|
||||||
|
|
||||||
userInfo, configStr, err := service.Hysteria2Subscribe(conPass, clientType, host)
|
userInfo, configStr, err := service.Hysteria2Subscribe(conPass, clientType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vo.Fail(err.Error(), c)
|
vo.Fail(err.Error(), c)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ export interface Hysteria2KickDto {
|
|||||||
export interface Hysteria2SubscribeUrlDto {
|
export interface Hysteria2SubscribeUrlDto {
|
||||||
accountId: number;
|
accountId: number;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
host: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Hysteria2UrlDto {
|
export interface Hysteria2UrlDto {
|
||||||
accountId: number;
|
accountId: number;
|
||||||
hostname: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Hysteria2SubscribeVo {
|
export interface Hysteria2SubscribeVo {
|
||||||
|
|||||||
Vendored
-2
@@ -9,8 +9,6 @@ declare module "*.vue" {
|
|||||||
|
|
||||||
// TypeScript-подсказки для переменных окружения
|
// TypeScript-подсказки для переменных окружения
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
VITE_APP_PORT: string;
|
|
||||||
VITE_APP_BASE_API: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
|
|||||||
import { useAccountStoreHook } from "@/store/modules/account";
|
import { useAccountStoreHook } from "@/store/modules/account";
|
||||||
|
|
||||||
const dynamicBase = (window as any).__dynamic_base__ || "";
|
const dynamicBase = (window as any).__dynamic_base__ || "";
|
||||||
|
const API_BASE = "/hui";
|
||||||
// Создание axios instance
|
// Создание axios instance
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: `${dynamicBase}${import.meta.env.VITE_APP_BASE_API}`,
|
baseURL: `${dynamicBase}${API_BASE}`,
|
||||||
timeout: 50000,
|
timeout: 50000,
|
||||||
headers: { "Content-Type": "application/json;charset=utf-8" },
|
headers: { "Content-Type": "application/json;charset=utf-8" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -906,7 +906,6 @@ const handleSubscribe = async (row: { [key: string]: any }) => {
|
|||||||
const dto: Hysteria2SubscribeUrlDto = {
|
const dto: Hysteria2SubscribeUrlDto = {
|
||||||
accountId: row.id,
|
accountId: row.id,
|
||||||
protocol: window.location.protocol,
|
protocol: window.location.protocol,
|
||||||
host: window.location.host,
|
|
||||||
};
|
};
|
||||||
const { data } = await hysteria2SubscribeUrlApi(dto);
|
const { data } = await hysteria2SubscribeUrlApi(dto);
|
||||||
copy(data.url);
|
copy(data.url);
|
||||||
@@ -920,7 +919,6 @@ const handleNodeUrl = async (row: { [key: string]: any }) => {
|
|||||||
try {
|
try {
|
||||||
const dto: Hysteria2UrlDto = {
|
const dto: Hysteria2UrlDto = {
|
||||||
accountId: row.id,
|
accountId: row.id,
|
||||||
hostname: window.location.hostname,
|
|
||||||
};
|
};
|
||||||
const { data } = await hysteria2UrlApi(dto);
|
const { data } = await hysteria2UrlApi(dto);
|
||||||
copy(data.url);
|
copy(data.url);
|
||||||
@@ -934,7 +932,6 @@ const handleQrCode = async (row: { [key: string]: any }) => {
|
|||||||
try {
|
try {
|
||||||
const dto: Hysteria2UrlDto = {
|
const dto: Hysteria2UrlDto = {
|
||||||
accountId: row.id,
|
accountId: row.id,
|
||||||
hostname: window.location.hostname,
|
|
||||||
};
|
};
|
||||||
const { data } = await hysteria2UrlApi(dto);
|
const { data } = await hysteria2UrlApi(dto);
|
||||||
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
||||||
|
|||||||
@@ -204,7 +204,6 @@ const handleSubscribe = async () => {
|
|||||||
const dto: Hysteria2SubscribeUrlDto = {
|
const dto: Hysteria2SubscribeUrlDto = {
|
||||||
accountId: accountStore.id,
|
accountId: accountStore.id,
|
||||||
protocol: window.location.protocol,
|
protocol: window.location.protocol,
|
||||||
host: window.location.host,
|
|
||||||
};
|
};
|
||||||
const { data } = await hysteria2SubscribeUrlApi(dto);
|
const { data } = await hysteria2SubscribeUrlApi(dto);
|
||||||
copy(data.url);
|
copy(data.url);
|
||||||
@@ -219,7 +218,6 @@ const handleSubscribeQrCode = async () => {
|
|||||||
const dto: Hysteria2SubscribeUrlDto = {
|
const dto: Hysteria2SubscribeUrlDto = {
|
||||||
accountId: accountStore.id,
|
accountId: accountStore.id,
|
||||||
protocol: window.location.protocol,
|
protocol: window.location.protocol,
|
||||||
host: window.location.host,
|
|
||||||
};
|
};
|
||||||
const { data } = await hysteria2SubscribeUrlApi(dto);
|
const { data } = await hysteria2SubscribeUrlApi(dto);
|
||||||
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
||||||
@@ -233,7 +231,6 @@ const handleNodeUrl = async () => {
|
|||||||
try {
|
try {
|
||||||
const dto: Hysteria2UrlDto = {
|
const dto: Hysteria2UrlDto = {
|
||||||
accountId: accountStore.id,
|
accountId: accountStore.id,
|
||||||
hostname: window.location.hostname,
|
|
||||||
};
|
};
|
||||||
const { data } = await hysteria2UrlApi(dto);
|
const { data } = await hysteria2UrlApi(dto);
|
||||||
copy(data.url);
|
copy(data.url);
|
||||||
@@ -247,7 +244,6 @@ const handleUrlQrCode = async () => {
|
|||||||
try {
|
try {
|
||||||
const dto: Hysteria2UrlDto = {
|
const dto: Hysteria2UrlDto = {
|
||||||
accountId: accountStore.id,
|
accountId: accountStore.id,
|
||||||
hostname: window.location.hostname,
|
|
||||||
};
|
};
|
||||||
const { data } = await hysteria2UrlApi(dto);
|
const { data } = await hysteria2UrlApi(dto);
|
||||||
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
state.qrCodeSrc = "data:image/png;base64," + data.qrCode;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import vue from "@vitejs/plugin-vue";
|
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 AutoImport from "unplugin-auto-import/vite";
|
||||||
import Components from "unplugin-vue-components/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
|
// eslint-disable-next-line no-control-regex
|
||||||
const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g;
|
const INVALID_CHAR_REGEX = /[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g;
|
||||||
const DRIVE_LETTER_REGEX = /^[a-z]:/i;
|
const DRIVE_LETTER_REGEX = /^[a-z]:/i;
|
||||||
|
const DEV_SERVER_PORT = 8080;
|
||||||
|
const API_BASE = "/hui";
|
||||||
|
|
||||||
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
export default defineConfig((): UserConfig => {
|
||||||
const env = loadEnv(mode, process.cwd());
|
|
||||||
return {
|
return {
|
||||||
base: "/",
|
base: "/",
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -44,11 +45,11 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: Number(env.VITE_APP_PORT),
|
port: DEV_SERVER_PORT,
|
||||||
open: true, // Automatically open browser on start
|
open: true, // Automatically open browser on start
|
||||||
proxy: {
|
proxy: {
|
||||||
// Reverse proxy for CORS in development
|
// Reverse proxy for CORS in development
|
||||||
[env.VITE_APP_BASE_API]: {
|
[API_BASE]: {
|
||||||
target: "http://127.0.0.1:8081",
|
target: "http://127.0.0.1:8081",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,10 +18,8 @@ type Hysteria2VersionDto struct {
|
|||||||
type Hysteria2SubscribeUrlDto struct {
|
type Hysteria2SubscribeUrlDto struct {
|
||||||
AccountId *int64 `json:"accountId" form:"accountId" validate:"required,gt=0"`
|
AccountId *int64 `json:"accountId" form:"accountId" validate:"required,gt=0"`
|
||||||
Protocol *string `json:"protocol" form:"protocol" validate:"required,min=1,max=8"`
|
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 {
|
type Hysteria2UrlDto struct {
|
||||||
AccountId *int64 `json:"accountId" form:"accountId" validate:"required,gt=0"`
|
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/bo"
|
||||||
"hy2xs-admin/model/constant"
|
"hy2xs-admin/model/constant"
|
||||||
"hy2xs-admin/proxy"
|
"hy2xs-admin/proxy"
|
||||||
"net"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseListenPort(listen string) (int, error) {
|
func resolvePublicEndpoint() (string, int, error) {
|
||||||
host, port, err := net.SplitHostPort(listen)
|
host := strings.TrimSpace(os.Getenv("HY2XS_PUBLIC_HOST"))
|
||||||
if err != nil {
|
if host == "" || host == "0.0.0.0" {
|
||||||
return 0, err
|
return "", 0, errors.New("HY2XS_PUBLIC_HOST must be set to public domain or IPv4")
|
||||||
}
|
}
|
||||||
if host == "" || port == "" {
|
portRaw := strings.TrimSpace(os.Getenv("HY2XS_PUBLIC_PORT"))
|
||||||
return 0, errors.New("invalid listen address")
|
if portRaw == "" {
|
||||||
|
return "", 0, errors.New("HY2XS_PUBLIC_PORT is required")
|
||||||
}
|
}
|
||||||
value, convErr := strconv.Atoi(port)
|
port, err := strconv.Atoi(portRaw)
|
||||||
if convErr != nil || value < 1 || value > 65535 {
|
if err != nil || port < 1 || port > 65535 {
|
||||||
return 0, errors.New("invalid listen port")
|
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) {
|
func Hysteria2Auth(conPass string) (int64, string, error) {
|
||||||
@@ -103,11 +104,15 @@ func Hysteria2Kick(ids []int64, kickUtilTime int64) error {
|
|||||||
return nil
|
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)
|
account, err := dao.GetAccount("id = ?", accountId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
publicHost, publicPort, err := resolvePublicEndpoint()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
config, err := dao.GetConfig("key = ?", constant.HUIWebContext)
|
config, err := dao.GetConfig("key = ?", constant.HUIWebContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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, "/") {
|
if config.Value != nil && *config.Value != "/" && strings.HasPrefix(*config.Value, "/") {
|
||||||
webContext = *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()
|
hysteria2Config, err := GetHysteria2Config()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
@@ -132,6 +137,10 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
publicHost, publicPort, err := resolvePublicEndpoint()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
hysteria2Name := "hysteria2"
|
hysteria2Name := "hysteria2"
|
||||||
hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark)
|
hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark)
|
||||||
@@ -144,11 +153,6 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string,
|
|||||||
|
|
||||||
userInfo := ""
|
userInfo := ""
|
||||||
configStr := ""
|
configStr := ""
|
||||||
listenPort, err := parseListenPort(*hysteria2Config.Listen)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
publicHost := strings.Split(host, ":")[0]
|
|
||||||
if clientType == constant.Shadowrocket || clientType == constant.Clash {
|
if clientType == constant.Shadowrocket || clientType == constant.Clash {
|
||||||
userInfo = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d",
|
userInfo = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d",
|
||||||
*account.Upload,
|
*account.Upload,
|
||||||
@@ -160,7 +164,7 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string,
|
|||||||
Name: hysteria2Name,
|
Name: hysteria2Name,
|
||||||
Type: "hysteria2",
|
Type: "hysteria2",
|
||||||
Server: publicHost,
|
Server: publicHost,
|
||||||
Port: strconv.Itoa(listenPort),
|
Port: strconv.Itoa(publicPort),
|
||||||
Password: conPass,
|
Password: conPass,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +228,7 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if clientType == constant.V2rayN {
|
} else if clientType == constant.V2rayN {
|
||||||
hysteria2Url, err := Hysteria2Url(*account.Id, strings.Split(host, ":")[0])
|
hysteria2Url, err := Hysteria2Url(*account.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
@@ -234,7 +238,7 @@ func Hysteria2Subscribe(conPass string, clientType string, host string) (string,
|
|||||||
return userInfo, configStr, nil
|
return userInfo, configStr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Hysteria2Url(accountId int64, hostname string) (string, error) {
|
func Hysteria2Url(accountId int64) (string, error) {
|
||||||
hysteria2Config, err := GetHysteria2Config()
|
hysteria2Config, err := GetHysteria2Config()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -242,13 +246,10 @@ func Hysteria2Url(accountId int64, hostname string) (string, error) {
|
|||||||
if hysteria2Config.Listen == nil || *hysteria2Config.Listen == "" {
|
if hysteria2Config.Listen == nil || *hysteria2Config.Listen == "" {
|
||||||
return "", errors.New("hysteria2 config is empty")
|
return "", errors.New("hysteria2 config is empty")
|
||||||
}
|
}
|
||||||
port, err := parseListenPort(*hysteria2Config.Listen)
|
hostname, port, err := resolvePublicEndpoint()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if hostname == "" || hostname == "0.0.0.0" {
|
|
||||||
return "", errors.New("invalid public host")
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := dao.GetAccount("id = ?", accountId)
|
account, err := dao.GetAccount("id = ?", accountId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ Hysteria2 — основной транспортный компонент се
|
|||||||
|
|
||||||
Инварианты:
|
Инварианты:
|
||||||
- `acme` -> только `acme` block в конфиге;
|
- `acme` -> только `acme` block в конфиге;
|
||||||
|
- `acme` block обязан содержать `type: http|tls|dns` из runtime env (`HY2XS_ACME_TYPE`);
|
||||||
- `file` -> только `tls.cert`/`tls.key` block;
|
- `file` -> только `tls.cert`/`tls.key` block;
|
||||||
- `self_signed_dev` -> только dev сценарии.
|
- `self_signed_dev` -> только dev сценарии.
|
||||||
|
|
||||||
@@ -111,3 +112,4 @@ Hysteria2 — основной транспортный компонент се
|
|||||||
7. bundled UI работает поверх актуального состояния сервера
|
7. bundled UI работает поверх актуального состояния сервера
|
||||||
8. `trafficStats.secret` отдельный от `JWT_SECRET`
|
8. `trafficStats.secret` отдельный от `JWT_SECRET`
|
||||||
9. IPv6 listen не используется
|
9. IPv6 listen не используется
|
||||||
|
10. публичные клиентские endpoint/URL берутся из `HY2XS_PUBLIC_HOST` + `HY2XS_PUBLIC_PORT`, а не из `listen`/request-host
|
||||||
|
|||||||
@@ -35,6 +35,10 @@
|
|||||||
|
|
||||||
В клиентской части используется только `public_host/public_port`.
|
В клиентской части используется только `public_host/public_port`.
|
||||||
|
|
||||||
|
В runtime-обозначениях HY2XS это эквивалентно:
|
||||||
|
- `HY2XS_PUBLIC_HOST`
|
||||||
|
- `HY2XS_PUBLIC_PORT`
|
||||||
|
|
||||||
Инварианты:
|
Инварианты:
|
||||||
- `listen` и `public endpoint` разделены;
|
- `listen` и `public endpoint` разделены;
|
||||||
- в клиентских URL не используется `0.0.0.0`;
|
- в клиентских URL не используется `0.0.0.0`;
|
||||||
|
|||||||
@@ -47,6 +47,8 @@
|
|||||||
12. bootstrap admin secret существует и имеет `0600`
|
12. bootstrap admin secret существует и имеет `0600`
|
||||||
13. `trafficStats` API: корректный secret принимает запрос, неверный secret отклоняется
|
13. `trafficStats` API: корректный secret принимает запрос, неверный secret отклоняется
|
||||||
14. TLS mode в `config.yaml` соответствует runtime env (`acme|file|self_signed_dev`)
|
14. TLS mode в `config.yaml` соответствует runtime env (`acme|file|self_signed_dev`)
|
||||||
|
15. при `HY2XS_TLS_MODE=acme` в `config.yaml` выставлен `acme.type` из `HY2XS_ACME_TYPE`
|
||||||
|
16. subscribe/node URL в API/QR формируются по `HY2XS_PUBLIC_HOST` + `HY2XS_PUBLIC_PORT`
|
||||||
15. `nft -c -f /etc/nftables.conf` проходит после apply
|
15. `nft -c -f /etc/nftables.conf` проходит после apply
|
||||||
|
|
||||||
## D. Negative tests
|
## D. Negative tests
|
||||||
@@ -76,3 +78,4 @@
|
|||||||
8. Telegram/access layer не требуется для прохождения install acceptance
|
8. Telegram/access layer не требуется для прохождения install acceptance
|
||||||
9. отсутствует production path для port hopping
|
9. отсутствует production path для port hopping
|
||||||
10. UI не запускается от root
|
10. UI не запускается от root
|
||||||
|
11. клиентские endpoint не зависят от request `Host`/`hostname`
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ function takeValue(args: string[], index: number, flag: string): string {
|
|||||||
function parseInstallOptions(args: string[]): InstallOptions {
|
function parseInstallOptions(args: string[]): InstallOptions {
|
||||||
const options: InstallOptions = {
|
const options: InstallOptions = {
|
||||||
packageDir: "",
|
packageDir: "",
|
||||||
configPath: "/etc/hy2xs/hy2xs.env",
|
sourceConfigPath: "",
|
||||||
|
runtimeConfigPath: "/etc/hy2xs/hy2xs.env",
|
||||||
nonInteractive: false,
|
nonInteractive: false,
|
||||||
skipFirewall: false,
|
skipFirewall: false,
|
||||||
skipStart: false
|
skipStart: false
|
||||||
@@ -44,7 +45,7 @@ function parseInstallOptions(args: string[]): InstallOptions {
|
|||||||
options.skipStart = true;
|
options.skipStart = true;
|
||||||
break;
|
break;
|
||||||
case "--config":
|
case "--config":
|
||||||
options.configPath = takeValue(args, i, arg);
|
options.sourceConfigPath = takeValue(args, i, arg);
|
||||||
i += 1;
|
i += 1;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -64,7 +65,8 @@ function parseInstallOptions(args: string[]): InstallOptions {
|
|||||||
function parseReconfigureOptions(args: string[]): ReconfigureOptions {
|
function parseReconfigureOptions(args: string[]): ReconfigureOptions {
|
||||||
const options: ReconfigureOptions = {
|
const options: ReconfigureOptions = {
|
||||||
packageDir: "",
|
packageDir: "",
|
||||||
configPath: "/etc/hy2xs/hy2xs.env",
|
sourceConfigPath: "/etc/hy2xs/hy2xs.env",
|
||||||
|
runtimeConfigPath: "/etc/hy2xs/hy2xs.env",
|
||||||
nonInteractive: false,
|
nonInteractive: false,
|
||||||
dryRun: false,
|
dryRun: false,
|
||||||
apply: false,
|
apply: false,
|
||||||
@@ -80,7 +82,7 @@ function parseReconfigureOptions(args: string[]): ReconfigureOptions {
|
|||||||
i += 1;
|
i += 1;
|
||||||
break;
|
break;
|
||||||
case "--config":
|
case "--config":
|
||||||
options.configPath = takeValue(args, i, arg);
|
options.sourceConfigPath = takeValue(args, i, arg);
|
||||||
i += 1;
|
i += 1;
|
||||||
break;
|
break;
|
||||||
case "--dry-run":
|
case "--dry-run":
|
||||||
|
|||||||
@@ -28,8 +28,11 @@ function secret(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function install(options: InstallOptions): Promise<void> {
|
export async function install(options: InstallOptions): Promise<void> {
|
||||||
const hasConfig = await exists(options.configPath);
|
const hasSourceConfig = options.sourceConfigPath ? await exists(options.sourceConfigPath) : false;
|
||||||
const sourceConfigPath = hasConfig ? options.configPath : `${options.packageDir}/config/hy2xs.env`;
|
if (options.sourceConfigPath && !hasSourceConfig) {
|
||||||
|
throw new Error(`config source not found: ${options.sourceConfigPath}`);
|
||||||
|
}
|
||||||
|
const sourceConfigPath = hasSourceConfig ? options.sourceConfigPath : `${options.packageDir}/config/hy2xs.env`;
|
||||||
const sourceConfigRaw = await readText(sourceConfigPath);
|
const sourceConfigRaw = await readText(sourceConfigPath);
|
||||||
const config = parseRuntimeEnv(sourceConfigRaw);
|
const config = parseRuntimeEnv(sourceConfigRaw);
|
||||||
|
|
||||||
@@ -49,11 +52,9 @@ export async function install(options: InstallOptions): Promise<void> {
|
|||||||
await installDeps(context);
|
await installDeps(context);
|
||||||
step("filesystem");
|
step("filesystem");
|
||||||
await prepareFilesystem(context);
|
await prepareFilesystem(context);
|
||||||
if (!hasConfig) {
|
step("write runtime env");
|
||||||
step("write runtime env");
|
await runVisible`mkdir -p /etc/hy2xs`;
|
||||||
await runVisible`mkdir -p /etc/hy2xs`;
|
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
|
||||||
await writeText(options.configPath, renderRuntimeEnv(config), 0o600);
|
|
||||||
}
|
|
||||||
step("bundled UI");
|
step("bundled UI");
|
||||||
await deployUi(context);
|
await deployUi(context);
|
||||||
step("Hysteria2 upstream install");
|
step("Hysteria2 upstream install");
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ async function rollbackCurrentState(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
||||||
const configRaw = await readText(options.configPath);
|
const configRaw = await readText(options.sourceConfigPath);
|
||||||
const config = parseRuntimeEnv(configRaw);
|
const config = parseRuntimeEnv(configRaw);
|
||||||
|
|
||||||
const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = {
|
const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = {
|
||||||
@@ -46,7 +46,8 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
|||||||
|
|
||||||
if (options.dryRun) {
|
if (options.dryRun) {
|
||||||
info("reconfigure dry-run: validated config and execution graph");
|
info("reconfigure dry-run: validated config and execution graph");
|
||||||
info(`config file: ${options.configPath}`);
|
info(`config source: ${options.sourceConfigPath}`);
|
||||||
|
info(`runtime file: ${options.runtimeConfigPath}`);
|
||||||
info(`ui bind: ${config.uiBindHost}:${config.uiPort}`);
|
info(`ui bind: ${config.uiBindHost}:${config.uiPort}`);
|
||||||
info(`hysteria bind: ${config.hysteriaBindHost}:${config.hysteriaPort}`);
|
info(`hysteria bind: ${config.hysteriaBindHost}:${config.hysteriaPort}`);
|
||||||
info(`public endpoint: ${config.publicHost}:${config.publicPort}`);
|
info(`public endpoint: ${config.publicHost}:${config.publicPort}`);
|
||||||
@@ -64,7 +65,7 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
|
|||||||
step("firewall");
|
step("firewall");
|
||||||
await applyFirewall(context);
|
await applyFirewall(context);
|
||||||
step("write env artifacts");
|
step("write env artifacts");
|
||||||
await writeText(options.configPath, renderRuntimeEnv(config), 0o600);
|
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
|
||||||
await writePostInstallEnv(context);
|
await writePostInstallEnv(context);
|
||||||
step("smoke checks");
|
step("smoke checks");
|
||||||
await smoke(context);
|
await smoke(context);
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ function randomSecret(): string {
|
|||||||
return randomBytes(24).toString("base64url");
|
return randomBytes(24).toString("base64url");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function valueOrGenerate(value: string | undefined): string {
|
||||||
|
if (!value || value === "__GENERATE__") {
|
||||||
|
return randomSecret();
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function parseEnv(content: string): EnvMap {
|
function parseEnv(content: string): EnvMap {
|
||||||
const result: EnvMap = {};
|
const result: EnvMap = {};
|
||||||
for (const rawLine of content.split(/\r?\n/)) {
|
for (const rawLine of content.split(/\r?\n/)) {
|
||||||
@@ -111,7 +118,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
|
|||||||
uiBindHost,
|
uiBindHost,
|
||||||
uiPort,
|
uiPort,
|
||||||
adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin"),
|
adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin"),
|
||||||
adminInitialPassword: env.HY2XS_ADMIN_INITIAL_PASSWORD || randomSecret(),
|
adminInitialPassword: valueOrGenerate(env.HY2XS_ADMIN_INITIAL_PASSWORD),
|
||||||
forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true),
|
forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true),
|
||||||
tlsMode,
|
tlsMode,
|
||||||
acmeType,
|
acmeType,
|
||||||
@@ -127,9 +134,9 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
|
|||||||
env.HY2XS_HYSTERIA_TRAFFIC_STATS_HOST || "127.0.0.1"
|
env.HY2XS_HYSTERIA_TRAFFIC_STATS_HOST || "127.0.0.1"
|
||||||
),
|
),
|
||||||
hysteriaTrafficStatsPort: trafficStatsPort,
|
hysteriaTrafficStatsPort: trafficStatsPort,
|
||||||
hysteriaTrafficStatsSecret: env.HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET || randomSecret(),
|
hysteriaTrafficStatsSecret: valueOrGenerate(env.HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET),
|
||||||
hysteriaObfsType: "salamander",
|
hysteriaObfsType: "salamander",
|
||||||
hysteriaObfsPassword: requireValue("HY2XS_HYSTERIA_OBFS_PASSWORD", env.HY2XS_HYSTERIA_OBFS_PASSWORD || randomSecret()),
|
hysteriaObfsPassword: requireValue("HY2XS_HYSTERIA_OBFS_PASSWORD", valueOrGenerate(env.HY2XS_HYSTERIA_OBFS_PASSWORD)),
|
||||||
hysteriaBandwidthUp: env.HY2XS_HYSTERIA_BANDWIDTH_UP || "50 mbps",
|
hysteriaBandwidthUp: env.HY2XS_HYSTERIA_BANDWIDTH_UP || "50 mbps",
|
||||||
hysteriaBandwidthDown: env.HY2XS_HYSTERIA_BANDWIDTH_DOWN || "50 mbps",
|
hysteriaBandwidthDown: env.HY2XS_HYSTERIA_BANDWIDTH_DOWN || "50 mbps",
|
||||||
hysteriaIgnoreClientBandwidth: parseBool(
|
hysteriaIgnoreClientBandwidth: parseBool(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { runVisible } from "../lib/process";
|
|||||||
|
|
||||||
export async function generateConfig(context: InstallContext): Promise<void> {
|
export async function generateConfig(context: InstallContext): Promise<void> {
|
||||||
const tlsAcmeBlock = context.config.tlsMode === "acme"
|
const tlsAcmeBlock = context.config.tlsMode === "acme"
|
||||||
? `acme:\n domains:\n - ${context.config.domain}\n email: ${context.config.acmeEmail}\n ca: letsencrypt\n dir: /var/lib/hysteria/acme\n listenHost: 0.0.0.0`
|
? `acme:\n domains:\n - ${context.config.domain}\n email: ${context.config.acmeEmail}\n ca: letsencrypt\n dir: /var/lib/hysteria/acme\n listenHost: 0.0.0.0\n type: ${context.config.acmeType}`
|
||||||
: "";
|
: "";
|
||||||
const tlsFileBlock = context.config.tlsMode === "file" || context.config.tlsMode === "self_signed_dev"
|
const tlsFileBlock = context.config.tlsMode === "file" || context.config.tlsMode === "self_signed_dev"
|
||||||
? `tls:\n cert: ${context.config.tlsCertPath}\n key: ${context.config.tlsKeyPath}`
|
? `tls:\n cert: ${context.config.tlsCertPath}\n key: ${context.config.tlsKeyPath}`
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ async function isPortBusy(port: number): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function isUnitActive(unit: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await run`systemctl is-active --quiet ${unit}`;
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function preflight(context: InstallContext): Promise<void> {
|
export async function preflight(context: InstallContext): Promise<void> {
|
||||||
const isReconfigure = context.packageVersion === "reconfigure";
|
const isReconfigure = context.packageVersion === "reconfigure";
|
||||||
|
|
||||||
@@ -76,11 +85,24 @@ export async function preflight(context: InstallContext): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await isPortBusy(context.config.hysteriaPort)) {
|
const hysteriaPortBusy = await isPortBusy(context.config.hysteriaPort);
|
||||||
fail(`Hysteria UDP/TCP port already appears to be in use: ${context.config.hysteriaPort}`);
|
const uiPortBusy = await isPortBusy(context.config.uiPort);
|
||||||
|
|
||||||
|
if (!isReconfigure) {
|
||||||
|
if (hysteriaPortBusy) {
|
||||||
|
fail(`Hysteria UDP/TCP port already appears to be in use: ${context.config.hysteriaPort}`);
|
||||||
|
}
|
||||||
|
if (uiPortBusy) {
|
||||||
|
fail(`HY2XS admin port already appears to be in use: ${context.config.uiPort}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await isPortBusy(context.config.uiPort)) {
|
if (hysteriaPortBusy && !(await isUnitActive("hysteria-server"))) {
|
||||||
fail(`HY2XS admin port already appears to be in use: ${context.config.uiPort}`);
|
fail(`Hysteria port ${context.config.hysteriaPort} is occupied by a non-HY2XS process`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiPortBusy && !(await isUnitActive("hy2xs-admin"))) {
|
||||||
|
fail(`HY2XS admin port ${context.config.uiPort} is occupied by a non-HY2XS process`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export async function smoke(context: InstallContext): Promise<void> {
|
|||||||
await runVisible`test -s /etc/hy2xs/hy2xs.env`;
|
await runVisible`test -s /etc/hy2xs/hy2xs.env`;
|
||||||
await runVisible`test -s /etc/hysteria/post-install.env`;
|
await runVisible`test -s /etc/hysteria/post-install.env`;
|
||||||
await runVisible`test -s ${context.config.bootstrapAdminSecretPath}`;
|
await runVisible`test -s ${context.config.bootstrapAdminSecretPath}`;
|
||||||
|
await runVisible`grep -q '^${context.config.adminUser}:' ${context.config.bootstrapAdminSecretPath}`;
|
||||||
await runVisible`test "$(stat -c '%a' /etc/hysteria/config.yaml)" = '600'`;
|
await runVisible`test "$(stat -c '%a' /etc/hysteria/config.yaml)" = '600'`;
|
||||||
await runVisible`test "$(stat -c '%a' /etc/hy2xs/hy2xs.env)" = '600'`;
|
await runVisible`test "$(stat -c '%a' /etc/hy2xs/hy2xs.env)" = '600'`;
|
||||||
await runVisible`test "$(stat -c '%a' /etc/hysteria/post-install.env)" = '600'`;
|
await runVisible`test "$(stat -c '%a' /etc/hysteria/post-install.env)" = '600'`;
|
||||||
@@ -26,10 +27,18 @@ export async function smoke(context: InstallContext): Promise<void> {
|
|||||||
}
|
}
|
||||||
await runVisible`ss -H -lun | grep -q '0.0.0.0:${context.config.hysteriaPort} '`;
|
await runVisible`ss -H -lun | grep -q '0.0.0.0:${context.config.hysteriaPort} '`;
|
||||||
await runVisible`! ss -H -ltnu | grep -q '\[::\]'`;
|
await runVisible`! ss -H -ltnu | grep -q '\[::\]'`;
|
||||||
await runHidden`curl -fsS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth >/dev/null`;
|
const invalidAuthResponse = await runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`;
|
||||||
|
if (!/"ok"\s*:\s*false/.test(invalidAuthResponse)) {
|
||||||
|
throw new Error(`unexpected auth response for invalid credentials: ${invalidAuthResponse}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validAuthResponse = await runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"${context.config.adminUser}.${context.config.adminInitialPassword}","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`;
|
||||||
|
if (!/"ok"\s*:\s*true/.test(validAuthResponse)) {
|
||||||
|
throw new Error(`unexpected auth response for valid credentials`);
|
||||||
|
}
|
||||||
|
|
||||||
await runHidden`curl -fsS --max-time 5 -H 'Authorization: ${context.config.hysteriaTrafficStatsSecret}' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online >/dev/null`;
|
await runHidden`curl -fsS --max-time 5 -H 'Authorization: ${context.config.hysteriaTrafficStatsSecret}' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online >/dev/null`;
|
||||||
const deniedCode = await runSecret`curl -fsS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: invalid-hy2xs-secret' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`;
|
const deniedCode = await runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: invalid-hy2xs-secret' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`;
|
||||||
if (!/(401|403)/.test(deniedCode)) {
|
if (!/(401|403)/.test(deniedCode)) {
|
||||||
throw new Error(`unexpected trafficStats status for invalid secret: ${deniedCode}`);
|
throw new Error(`unexpected trafficStats status for invalid secret: ${deniedCode}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export type InstallOptions = {
|
export type InstallOptions = {
|
||||||
packageDir: string;
|
packageDir: string;
|
||||||
configPath: string;
|
sourceConfigPath: string;
|
||||||
|
runtimeConfigPath: string;
|
||||||
nonInteractive: boolean;
|
nonInteractive: boolean;
|
||||||
skipFirewall: boolean;
|
skipFirewall: boolean;
|
||||||
skipStart: boolean;
|
skipStart: boolean;
|
||||||
@@ -8,7 +9,8 @@ export type InstallOptions = {
|
|||||||
|
|
||||||
export type ReconfigureOptions = {
|
export type ReconfigureOptions = {
|
||||||
packageDir: string;
|
packageDir: string;
|
||||||
configPath: string;
|
sourceConfigPath: string;
|
||||||
|
runtimeConfigPath: string;
|
||||||
nonInteractive: boolean;
|
nonInteractive: boolean;
|
||||||
dryRun: boolean;
|
dryRun: boolean;
|
||||||
apply: boolean;
|
apply: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# HY2XS canonical production runtime config (packaged baseline)
|
# HY2XS canonical production runtime config (packaged baseline)
|
||||||
HY2XS_IPV6_ENABLED=false
|
HY2XS_IPV6_ENABLED=false
|
||||||
HY2XS_DOMAIN=
|
HY2XS_DOMAIN=replace-with-your-domain.example
|
||||||
HY2XS_PUBLIC_HOST=127.0.0.1
|
HY2XS_PUBLIC_HOST=replace-with-your-domain.example
|
||||||
HY2XS_PUBLIC_PORT=443
|
HY2XS_PUBLIC_PORT=443
|
||||||
HY2XS_SSH_PORT=22
|
HY2XS_SSH_PORT=22
|
||||||
HY2XS_FIREWALL_ENABLED=true
|
HY2XS_FIREWALL_ENABLED=true
|
||||||
@@ -9,10 +9,11 @@ HY2XS_FIREWALL_STAGED_APPLY=true
|
|||||||
HY2XS_UI_BIND_HOST=127.0.0.1
|
HY2XS_UI_BIND_HOST=127.0.0.1
|
||||||
HY2XS_UI_PORT=8080
|
HY2XS_UI_PORT=8080
|
||||||
HY2XS_ADMIN_USER=admin
|
HY2XS_ADMIN_USER=admin
|
||||||
HY2XS_ADMIN_INITIAL_PASSWORD=
|
HY2XS_ADMIN_INITIAL_PASSWORD=__GENERATE__
|
||||||
HY2XS_FORCE_PASSWORD_CHANGE=true
|
HY2XS_FORCE_PASSWORD_CHANGE=true
|
||||||
HY2XS_TLS_MODE=self_signed_dev
|
HY2XS_TLS_MODE=acme
|
||||||
HY2XS_ACME_EMAIL=
|
HY2XS_ACME_TYPE=http
|
||||||
|
HY2XS_ACME_EMAIL=replace-with-your-email@example.com
|
||||||
HY2XS_TLS_CERT_PATH=/etc/hysteria/server.crt
|
HY2XS_TLS_CERT_PATH=/etc/hysteria/server.crt
|
||||||
HY2XS_TLS_KEY_PATH=/etc/hysteria/server.key
|
HY2XS_TLS_KEY_PATH=/etc/hysteria/server.key
|
||||||
HY2XS_HYSTERIA_BIND_HOST=0.0.0.0
|
HY2XS_HYSTERIA_BIND_HOST=0.0.0.0
|
||||||
@@ -21,9 +22,9 @@ HY2XS_HYSTERIA_AUTH_MODE=http
|
|||||||
HY2XS_HYSTERIA_AUTH_URL=http://127.0.0.1:8080/hui/hysteria2/auth
|
HY2XS_HYSTERIA_AUTH_URL=http://127.0.0.1:8080/hui/hysteria2/auth
|
||||||
HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=127.0.0.1
|
HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=127.0.0.1
|
||||||
HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=36712
|
HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=36712
|
||||||
HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=
|
HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=__GENERATE__
|
||||||
HY2XS_HYSTERIA_OBFS_TYPE=salamander
|
HY2XS_HYSTERIA_OBFS_TYPE=salamander
|
||||||
HY2XS_HYSTERIA_OBFS_PASSWORD=
|
HY2XS_HYSTERIA_OBFS_PASSWORD=__GENERATE__
|
||||||
HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps
|
HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps
|
||||||
HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps
|
HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps
|
||||||
HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false
|
HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ Type=simple
|
|||||||
User=hy2xs-admin
|
User=hy2xs-admin
|
||||||
Group=hy2xs-admin
|
Group=hy2xs-admin
|
||||||
WorkingDirectory={{INSTALL_DIR}}
|
WorkingDirectory={{INSTALL_DIR}}
|
||||||
|
EnvironmentFile=/etc/hy2xs/hy2xs.env
|
||||||
Environment=HUI_DATA={{DATA_DIR}}/
|
Environment=HUI_DATA={{DATA_DIR}}/
|
||||||
Environment=HY2XS_UI_BIND_HOST={{UI_BIND_HOST}}
|
Environment=HUI_LOG={{LOG_DIR}}
|
||||||
Environment=HY2XS_UI_PORT={{UI_PORT}}
|
|
||||||
ExecStart={{INSTALL_DIR}}/hy2xs-admin -p {{UI_PORT}}
|
ExecStart={{INSTALL_DIR}}/hy2xs-admin -p {{UI_PORT}}
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
|
|||||||
Reference in New Issue
Block a user