feat: The website supports TCP/UDP proxying. (#11302)

Refs https://github.com/1Panel-dev/1Panel/issues/5135
Refs https://github.com/1Panel-dev/1Panel/issues/6976
This commit is contained in:
CityFun
2025-12-11 10:33:03 +00:00
committed by GitHub
parent 2d1792c6de
commit ab13ddec48
43 changed files with 2443 additions and 1771 deletions
+20
View File
@@ -1223,3 +1223,23 @@ func (b *BaseApi) UpdateCORSConfig(c *gin.Context) {
}
helper.Success(c)
}
// @Tags Website
// @Summary Update Stream Config
// @Accept json
// @Param request body request.StreamUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/stream/update [post]
func (b *BaseApi) UpdateStreamConfig(c *gin.Context) {
var req request.StreamUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteService.UpdateStream(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
+15
View File
@@ -38,6 +38,21 @@ type WebsiteCreate struct {
FtpConfig
DataBaseConfig
SSLConfig
StreamConfig
}
type StreamConfig struct {
StreamPorts string `json:"streamPorts" validate:"required"`
Name string `json:"name"`
Algorithm string `json:"algorithm"`
Servers []dto.NginxUpstreamServer `json:"servers"`
}
type StreamUpdate struct {
WebsiteID uint `json:"websiteID" validate:"required"`
StreamConfig
}
type WebsiteOptionReq struct {
+4
View File
@@ -3,6 +3,7 @@ package response
import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
@@ -16,6 +17,9 @@ type WebsiteDTO struct {
RuntimeType string `json:"runtimeType"`
SiteDir string `json:"siteDir"`
OpenBaseDir bool `json:"openBaseDir"`
Algorithm string `json:"algorithm"`
Servers []dto.NginxUpstreamServer `json:"servers"`
}
type WebsiteRes struct {
+2
View File
@@ -37,6 +37,8 @@ type Website struct {
Favorite bool `json:"favorite"`
StreamPorts string `json:"streamPorts"`
Domains []WebsiteDomain `json:"domains" gorm:"-:migration"`
WebsiteSSL WebsiteSSL `json:"webSiteSSL" gorm:"-:migration"`
}
+1 -1
View File
@@ -51,7 +51,7 @@ func getNginxFull(website *model.Website) (dto.NginxFull, error) {
if website != nil {
nginxFull.Website = *website
var siteNginxConfig dto.NginxConfig
siteConfigPath := GetSitePath(*website, SiteConf)
siteConfigPath := GetWebsiteConfigPath(*website)
siteNginxConfig.FilePath = siteConfigPath
siteNginxContent, err := os.ReadFile(siteConfigPath)
if err != nil {
+168 -1237
View File
File diff suppressed because it is too large Load Diff
+312
View File
@@ -0,0 +1,312 @@
package service
import (
"bufio"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
"golang.org/x/crypto/bcrypt"
"os"
"path"
"strings"
)
func (w WebsiteService) GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) {
var (
website model.Website
authContent []byte
nginxParams []response.NginxParam
)
website, err = websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return
}
absoluteAuthPath := GetSitePath(website, SiteRootAuthBasicPath)
fileOp := files.NewFileOp()
if !fileOp.Stat(absoluteAuthPath) {
return
}
nginxParams, err = getNginxParamsByKeys(constant.NginxScopeServer, []string{"auth_basic"}, &website)
if err != nil {
return
}
res.Enable = len(nginxParams[0].Params) > 0
authContent, err = fileOp.GetContent(absoluteAuthPath)
authArray := strings.Split(string(authContent), "\n")
for _, line := range authArray {
if line == "" {
continue
}
params := strings.Split(line, ":")
auth := dto.NginxAuth{
Username: params[0],
}
if len(params) == 3 {
auth.Remark = params[2]
}
res.Items = append(res.Items, auth)
}
return
}
func (w WebsiteService) UpdateAuthBasic(req request.NginxAuthUpdate) (err error) {
var (
website model.Website
params []dto.NginxParam
authContent []byte
authArray []string
)
website, err = websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return err
}
authPath := fmt.Sprintf("/www/sites/%s/auth_basic/auth.pass", website.Alias)
absoluteAuthPath := GetSitePath(website, SiteRootAuthBasicPath)
fileOp := files.NewFileOp()
if !fileOp.Stat(path.Dir(absoluteAuthPath)) {
_ = fileOp.CreateDir(path.Dir(absoluteAuthPath), constant.DirPerm)
}
if !fileOp.Stat(absoluteAuthPath) {
_ = fileOp.CreateFile(absoluteAuthPath)
}
params = append(params, dto.NginxParam{Name: "auth_basic", Params: []string{`"Authentication"`}})
params = append(params, dto.NginxParam{Name: "auth_basic_user_file", Params: []string{authPath}})
authContent, err = fileOp.GetContent(absoluteAuthPath)
if err != nil {
return
}
if len(authContent) > 0 {
authArray = strings.Split(string(authContent), "\n")
}
switch req.Operate {
case "disable":
return deleteNginxConfig(constant.NginxScopeServer, params, &website)
case "enable":
return updateNginxConfig(constant.NginxScopeServer, params, &website)
case "create":
for _, line := range authArray {
authParams := strings.Split(line, ":")
username := authParams[0]
if username == req.Username {
err = buserr.New("ErrUsernameIsExist")
return
}
}
var passwdHash []byte
passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return
}
line := fmt.Sprintf("%s:%s\n", req.Username, passwdHash)
if req.Remark != "" {
line = fmt.Sprintf("%s:%s:%s\n", req.Username, passwdHash, req.Remark)
}
authArray = append(authArray, line)
case "edit":
userExist := false
for index, line := range authArray {
authParams := strings.Split(line, ":")
username := authParams[0]
if username == req.Username {
userExist = true
var passwdHash []byte
passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return
}
userPasswd := fmt.Sprintf("%s:%s\n", req.Username, passwdHash)
if req.Remark != "" {
userPasswd = fmt.Sprintf("%s:%s:%s\n", req.Username, passwdHash, req.Remark)
}
authArray[index] = userPasswd
}
}
if !userExist {
err = buserr.New("ErrUsernameIsNotExist")
return
}
case "delete":
deleteIndex := -1
for index, line := range authArray {
authParams := strings.Split(line, ":")
username := authParams[0]
if username == req.Username {
deleteIndex = index
}
}
if deleteIndex < 0 {
return
}
authArray = append(authArray[:deleteIndex], authArray[deleteIndex+1:]...)
}
var passFile *os.File
passFile, err = os.Create(absoluteAuthPath)
if err != nil {
return
}
defer passFile.Close()
writer := bufio.NewWriter(passFile)
for _, line := range authArray {
if line == "" {
continue
}
_, err = writer.WriteString(line + "\n")
if err != nil {
return
}
}
err = writer.Flush()
if err != nil {
return
}
authContent, err = fileOp.GetContent(absoluteAuthPath)
if err != nil {
return
}
if len(authContent) == 0 {
if err = deleteNginxConfig(constant.NginxScopeServer, params, &website); err != nil {
return
}
}
return
}
func (w WebsiteService) GetPathAuthBasics(req request.NginxAuthReq) (res []response.NginxPathAuthRes, err error) {
var (
website model.Website
authContent []byte
)
website, err = websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return
}
fileOp := files.NewFileOp()
absoluteAuthDir := GetSitePath(website, SitePathAuthBasicDir)
passDir := path.Join(absoluteAuthDir, "pass")
if !fileOp.Stat(absoluteAuthDir) || !fileOp.Stat(passDir) {
return
}
entries, err := os.ReadDir(absoluteAuthDir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if !entry.IsDir() {
name := strings.TrimSuffix(entry.Name(), ".conf")
pathAuth := dto.NginxPathAuth{
Name: name,
}
configPath := path.Join(absoluteAuthDir, entry.Name())
content, err := fileOp.GetContent(configPath)
if err != nil {
return nil, err
}
config, err := parser.NewStringParser(string(content)).Parse()
if err != nil {
return nil, err
}
directives := config.Directives
location, _ := directives[0].(*components.Location)
pathAuth.Path = strings.TrimPrefix(location.Match, "^")
passPath := path.Join(passDir, fmt.Sprintf("%s.pass", name))
authContent, err = fileOp.GetContent(passPath)
if err != nil {
return nil, err
}
authArray := strings.Split(string(authContent), "\n")
for _, line := range authArray {
if line == "" {
continue
}
params := strings.Split(line, ":")
pathAuth.Username = params[0]
if len(params) == 3 {
pathAuth.Remark = params[2]
}
}
res = append(res, response.NginxPathAuthRes{
NginxPathAuth: pathAuth,
})
}
}
return
}
func (w WebsiteService) UpdatePathAuthBasic(req request.NginxPathAuthUpdate) error {
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return err
}
fileOp := files.NewFileOp()
authDir := GetSitePath(website, SitePathAuthBasicDir)
if !fileOp.Stat(authDir) {
_ = fileOp.CreateDir(authDir, constant.DirPerm)
}
passDir := path.Join(authDir, "pass")
if !fileOp.Stat(passDir) {
_ = fileOp.CreateDir(passDir, constant.DirPerm)
}
confPath := path.Join(authDir, fmt.Sprintf("%s.conf", req.Name))
passPath := path.Join(passDir, fmt.Sprintf("%s.pass", req.Name))
var config *components.Config
switch req.Operate {
case "delete":
_ = fileOp.DeleteFile(confPath)
_ = fileOp.DeleteFile(passPath)
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
case "create":
config, err = parser.NewStringParser(string(nginx_conf.PathAuth)).Parse()
if err != nil {
return err
}
if fileOp.Stat(confPath) || fileOp.Stat(passPath) {
return buserr.New("ErrNameIsExist")
}
case "edit":
par, err := parser.NewParser(confPath)
if err != nil {
return err
}
config, err = par.Parse()
if err != nil {
return err
}
}
config.FilePath = confPath
directives := config.Directives
location, _ := directives[0].(*components.Location)
location.UpdateDirective("auth_basic_user_file", []string{fmt.Sprintf("/www/sites/%s/path_auth/pass/%s", website.Alias, fmt.Sprintf("%s.pass", req.Name))})
location.ChangePath("~*", fmt.Sprintf("^%s", req.Path))
var passwdHash []byte
passwdHash, err = bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
line := fmt.Sprintf("%s:%s\n", req.Username, passwdHash)
if req.Remark != "" {
line = fmt.Sprintf("%s:%s:%s\n", req.Username, passwdHash, req.Remark)
}
_ = fileOp.SaveFile(passPath, line, constant.DirPerm)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr("ErrUpdateBuWebsite", err)
}
nginxInclude := fmt.Sprintf("/www/sites/%s/path_auth/*.conf", website.Alias)
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
return nil
}
return nil
}
+185
View File
@@ -0,0 +1,185 @@
package service
import (
"context"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"path"
"strconv"
)
func (w WebsiteService) CreateWebsiteDomain(create request.WebsiteDomainCreate) ([]model.WebsiteDomain, error) {
var (
domainModels []model.WebsiteDomain
addPorts []int
)
httpPort, httpsPort, err := getAppInstallPort(constant.AppOpenresty)
if err != nil {
return nil, err
}
website, err := websiteRepo.GetFirst(repo.WithByID(create.WebsiteID))
if err != nil {
return nil, err
}
domainModels, addPorts, _, err = getWebsiteDomains(create.Domains, httpPort, httpsPort, create.WebsiteID)
if err != nil {
return nil, err
}
go func() {
_ = OperateFirewallPort(nil, addPorts)
}()
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return nil, err
}
wafDataPath := path.Join(nginxInstall.GetPath(), "1pwaf", "data")
fileOp := files.NewFileOp()
if fileOp.Stat(wafDataPath) {
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
content, err := fileOp.GetContent(websitesConfigPath)
if err != nil {
return nil, err
}
var websitesArray []request.WafWebsite
if content != nil {
if err := json.Unmarshal(content, &websitesArray); err != nil {
return nil, err
}
}
for index, wafWebsite := range websitesArray {
if wafWebsite.Key == website.Alias {
wafSite := request.WafWebsite{
Key: website.Alias,
Domains: wafWebsite.Domains,
Host: wafWebsite.Host,
}
for _, domain := range domainModels {
wafSite.Domains = append(wafSite.Domains, domain.Domain)
wafSite.Host = append(wafSite.Host, domain.Domain+":"+strconv.Itoa(domain.Port))
}
if len(wafSite.Host) == 0 {
wafSite.Host = []string{}
}
websitesArray[index] = wafSite
break
}
}
websitesContent, err := json.Marshal(websitesArray)
if err != nil {
return nil, err
}
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return nil, err
}
}
if err = addListenAndServerName(website, domainModels); err != nil {
return nil, err
}
return domainModels, websiteDomainRepo.BatchCreate(context.TODO(), domainModels)
}
func (w WebsiteService) GetWebsiteDomain(websiteId uint) ([]model.WebsiteDomain, error) {
return websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteId))
}
func (w WebsiteService) DeleteWebsiteDomain(domainId uint) error {
webSiteDomain, err := websiteDomainRepo.GetFirst(repo.WithByID(domainId))
if err != nil {
return err
}
if websiteDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(webSiteDomain.WebsiteID)); len(websiteDomains) == 1 {
return fmt.Errorf("can not delete last domain")
}
website, err := websiteRepo.GetFirst(repo.WithByID(webSiteDomain.WebsiteID))
if err != nil {
return err
}
var ports []int
if oldDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(webSiteDomain.WebsiteID), websiteDomainRepo.WithPort(webSiteDomain.Port)); len(oldDomains) == 1 {
ports = append(ports, webSiteDomain.Port)
}
var domains []string
if oldDomains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(webSiteDomain.WebsiteID), websiteDomainRepo.WithDomain(webSiteDomain.Domain)); len(oldDomains) == 1 {
domains = append(domains, webSiteDomain.Domain)
}
if len(ports) > 0 || len(domains) > 0 {
stringBinds := make([]string, len(ports))
for i := 0; i < len(ports); i++ {
stringBinds[i] = strconv.Itoa(ports[i])
}
if err := deleteListenAndServerName(website, stringBinds, domains); err != nil {
return err
}
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
wafDataPath := path.Join(nginxInstall.GetPath(), "1pwaf", "data")
fileOp := files.NewFileOp()
if fileOp.Stat(wafDataPath) {
websitesConfigPath := path.Join(wafDataPath, "conf", "sites.json")
content, err := fileOp.GetContent(websitesConfigPath)
if err != nil {
return err
}
var websitesArray []request.WafWebsite
var newWebsitesArray []request.WafWebsite
if content != nil {
if err := json.Unmarshal(content, &websitesArray); err != nil {
return err
}
}
for _, wafWebsite := range websitesArray {
if wafWebsite.Key == website.Alias {
wafSite := wafWebsite
oldDomains := wafSite.Domains
var newDomains []string
for _, domain := range oldDomains {
if domain == webSiteDomain.Domain {
continue
}
newDomains = append(newDomains, domain)
}
wafSite.Domains = newDomains
oldHostArray := wafSite.Host
var newHostArray []string
for _, host := range oldHostArray {
if host == webSiteDomain.Domain+":"+strconv.Itoa(webSiteDomain.Port) {
continue
}
newHostArray = append(newHostArray, host)
}
wafSite.Host = newHostArray
if len(wafSite.Host) == 0 {
wafSite.Host = []string{}
}
newWebsitesArray = append(newWebsitesArray, wafSite)
} else {
newWebsitesArray = append(newWebsitesArray, wafWebsite)
}
}
websitesContent, err := json.Marshal(newWebsitesArray)
if err != nil {
return err
}
if err = fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return err
}
}
return websiteDomainRepo.DeleteBy(context.TODO(), repo.WithByID(domainId))
}
+229
View File
@@ -0,0 +1,229 @@
package service
import (
"bytes"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
"os"
"path"
"strings"
)
func (w WebsiteService) GetLoadBalances(id uint) ([]dto.NginxUpstream, error) {
website, err := websiteRepo.GetFirst(repo.WithByID(id))
if err != nil {
return nil, err
}
includeDir := GetSitePath(website, SiteUpstreamDir)
fileOp := files.NewFileOp()
if !fileOp.Stat(includeDir) {
return nil, nil
}
entries, err := os.ReadDir(includeDir)
if err != nil {
return nil, err
}
var res []dto.NginxUpstream
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !strings.HasSuffix(name, ".conf") {
continue
}
upstreamName := strings.TrimSuffix(name, ".conf")
upstream := dto.NginxUpstream{
Name: upstreamName,
}
upstreamPath := path.Join(includeDir, name)
content, err := fileOp.GetContent(upstreamPath)
if err != nil {
return nil, err
}
upstream.Content = string(content)
nginxParser, err := parser.NewParser(upstreamPath)
if err != nil {
return nil, err
}
config, err := nginxParser.Parse()
if err != nil {
return nil, err
}
upstreams := config.FindUpstreams()
for _, up := range upstreams {
if up.UpstreamName == upstreamName {
directives := up.GetDirectives()
for _, d := range directives {
dName := d.GetName()
if _, ok := dto.LBAlgorithms[dName]; ok {
upstream.Algorithm = dName
}
}
upstream.Servers = getNginxUpstreamServers(up.UpstreamServers)
}
}
res = append(res, upstream)
}
return res, nil
}
func (w WebsiteService) CreateLoadBalance(req request.WebsiteLBCreate) error {
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return err
}
includeDir := GetSitePath(website, SiteUpstreamDir)
fileOp := files.NewFileOp()
if !fileOp.Stat(includeDir) {
_ = fileOp.CreateDir(includeDir, constant.DirPerm)
}
filePath := path.Join(includeDir, fmt.Sprintf("%s.conf", req.Name))
if fileOp.Stat(filePath) {
return buserr.New("ErrNameIsExist")
}
config, err := parser.NewStringParser(string(nginx_conf.Upstream)).Parse()
if err != nil {
return err
}
config.Block = &components.Block{}
config.FilePath = filePath
upstream := components.Upstream{
UpstreamName: req.Name,
}
if req.Algorithm != "default" {
upstream.UpdateDirective(req.Algorithm, []string{})
}
upstream.UpstreamServers = parseUpstreamServers(req.Servers)
config.Block.Directives = append(config.Block.Directives, &upstream)
defer func() {
if err != nil {
_ = fileOp.DeleteFile(filePath)
}
}()
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr("ErrUpdateBuWebsite", err)
}
nginxInclude := fmt.Sprintf("/www/sites/%s/upstream/*.conf", website.Alias)
if err = updateNginxConfig("", []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
return err
}
return nil
}
func (w WebsiteService) UpdateLoadBalance(req request.WebsiteLBUpdate) error {
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return err
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
includeDir := GetSitePath(website, SiteUpstreamDir)
fileOp := files.NewFileOp()
filePath := path.Join(includeDir, fmt.Sprintf("%s.conf", req.Name))
if !fileOp.Stat(filePath) {
return nil
}
oldContent, err := fileOp.GetContent(filePath)
if err != nil {
return err
}
parser, err := parser.NewParser(filePath)
if err != nil {
return err
}
config, err := parser.Parse()
if err != nil {
return err
}
upstreams := config.FindUpstreams()
for _, up := range upstreams {
if up.UpstreamName == req.Name {
directives := up.GetDirectives()
for _, d := range directives {
dName := d.GetName()
if _, ok := dto.LBAlgorithms[dName]; ok {
up.RemoveDirective(dName, nil)
}
}
if req.Algorithm != "default" {
up.UpdateDirective(req.Algorithm, []string{})
}
up.UpstreamServers = parseUpstreamServers(req.Servers)
}
}
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr("ErrUpdateBuWebsite", err)
}
return nginxCheckAndReload(string(oldContent), filePath, nginxInstall.ContainerName)
}
func (w WebsiteService) DeleteLoadBalance(req request.WebsiteLBDelete) error {
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return err
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
proxies, _ := w.GetProxies(website.ID)
if len(proxies) > 0 {
for _, proxy := range proxies {
if strings.HasSuffix(proxy.ProxyPass, fmt.Sprintf("://%s", req.Name)) {
return buserr.New("ErrProxyIsUsed")
}
}
}
includeDir := GetSitePath(website, SiteUpstreamDir)
fileOp := files.NewFileOp()
filePath := path.Join(includeDir, fmt.Sprintf("%s.conf", req.Name))
if !fileOp.Stat(filePath) {
return nil
}
if err = fileOp.DeleteFile(filePath); err != nil {
return err
}
return opNginx(nginxInstall.ContainerName, constant.NginxReload)
}
func (w WebsiteService) UpdateLoadBalanceFile(req request.WebsiteLBUpdateFile) error {
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return err
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
includeDir := GetSitePath(website, SiteUpstreamDir)
filePath := path.Join(includeDir, fmt.Sprintf("%s.conf", req.Name))
fileOp := files.NewFileOp()
oldContent, err := fileOp.GetContent(filePath)
if err != nil {
return err
}
if err = fileOp.WriteFile(filePath, strings.NewReader(req.Content), constant.DirPerm); err != nil {
return err
}
defer func() {
if err != nil {
_ = fileOp.WriteFile(filePath, bytes.NewReader(oldContent), constant.DirPerm)
}
}()
return opNginx(nginxInstall.ContainerName, constant.NginxReload)
}
+408
View File
@@ -0,0 +1,408 @@
package service
import (
"bytes"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"path"
"strconv"
"strings"
)
func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error) {
var (
website model.Website
par *parser.Parser
oldContent []byte
)
website, err = websiteRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return
}
fileOp := files.NewFileOp()
includeDir := GetSitePath(website, SiteProxyDir)
if !fileOp.Stat(includeDir) {
_ = fileOp.CreateDir(includeDir, constant.DirPerm)
}
fileName := fmt.Sprintf("%s.conf", req.Name)
includePath := path.Join(includeDir, fileName)
backName := fmt.Sprintf("%s.bak", req.Name)
backPath := path.Join(includeDir, backName)
if req.Operate == "create" && (fileOp.Stat(includePath) || fileOp.Stat(backPath)) {
err = buserr.New("ErrNameIsExist")
return
}
defer func() {
if err != nil {
switch req.Operate {
case "create":
_ = fileOp.DeleteFile(includePath)
case "edit":
_ = fileOp.WriteFile(includePath, bytes.NewReader(oldContent), constant.DirPerm)
}
}
}()
var config *components.Config
switch req.Operate {
case "create":
config, err = parser.NewStringParser(string(nginx_conf.GetWebsiteFile("proxy.conf"))).Parse()
if err != nil {
return
}
case "edit":
par, err = parser.NewParser(includePath)
if err != nil {
return
}
config, err = par.Parse()
if err != nil {
return
}
oldContent, err = fileOp.GetContent(includePath)
if err != nil {
return
}
case "delete":
_ = fileOp.DeleteFile(includePath)
_ = fileOp.DeleteFile(backPath)
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
case "disable":
_ = fileOp.Rename(includePath, backPath)
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
case "enable":
_ = fileOp.Rename(backPath, includePath)
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
}
config.FilePath = includePath
directives := config.Directives
var location *components.Location
for _, directive := range directives {
if loc, ok := directive.(*components.Location); ok {
location = loc
break
}
}
if location == nil {
err = errors.New("invalid proxy config, no location found")
return
}
location.UpdateDirective("proxy_pass", []string{req.ProxyPass})
location.UpdateDirective("proxy_set_header", []string{"Host", req.ProxyHost})
location.ChangePath(req.Modifier, req.Match)
// Server Cache Settings
if req.Cache {
if err = openProxyCache(website); err != nil {
return
}
location.AddServerCache(fmt.Sprintf("proxy_cache_zone_of_%s", website.Alias), req.ServerCacheTime, req.ServerCacheUnit)
} else {
location.RemoveServerCache(fmt.Sprintf("proxy_cache_zone_of_%s", website.Alias))
}
// Browser Cache Settings
if req.CacheTime != 0 {
location.AddBrowserCache(req.CacheTime, req.CacheUnit)
} else {
location.RemoveBrowserCache()
}
// Content Replace Settings
if len(req.Replaces) > 0 {
location.AddSubFilter(req.Replaces)
} else {
location.RemoveSubFilter()
}
// SSL Settings
if req.SNI {
location.UpdateDirective("proxy_ssl_server_name", []string{"on"})
if req.ProxySSLName != "" {
location.UpdateDirective("proxy_ssl_name", []string{req.ProxySSLName})
}
} else {
location.UpdateDirective("proxy_ssl_server_name", []string{"off"})
}
// CORS Settings
if req.Cors {
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Origin", req.AllowOrigins, "always"})
if req.AllowMethods != "" {
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Methods", req.AllowMethods, "always"})
} else {
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Methods"})
}
if req.AllowHeaders != "" {
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Headers", req.AllowHeaders, "always"})
} else {
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Headers"})
}
if req.AllowCredentials {
location.UpdateDirective("add_header", []string{"Access-Control-Allow-Credentials", "true", "always"})
} else {
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Credentials"})
}
if req.Preflight {
location.AddCorsOption()
} else {
location.RemoveCorsOption()
}
} else {
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Origin"})
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Methods"})
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Headers"})
location.RemoveDirective("add_header", []string{"Access-Control-Allow-Credentials"})
location.RemoveDirectiveByFullParams("if", []string{"(", "$request_method", "=", "'OPTIONS'", ")"})
}
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr("ErrUpdateBuWebsite", err)
}
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
return updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website)
}
func (w WebsiteService) UpdateProxyCache(req request.NginxProxyCacheUpdate) (err error) {
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return
}
cacheDir := GetSitePath(website, SiteCacheDir)
fileOp := files.NewFileOp()
if !fileOp.Stat(cacheDir) {
_ = fileOp.CreateDir(cacheDir, constant.DirPerm)
}
if req.Open {
proxyCachePath := fmt.Sprintf("/www/sites/%s/cache levels=1:2 keys_zone=proxy_cache_zone_of_%s:%d%s max_size=%d%s inactive=%d%s", website.Alias, website.Alias, req.ShareCache, req.ShareCacheUnit, req.CacheLimit, req.CacheLimitUnit, req.CacheExpire, req.CacheExpireUnit)
return updateNginxConfig("", []dto.NginxParam{{Name: "proxy_cache_path", Params: []string{proxyCachePath}}}, &website)
}
return deleteNginxConfig("", []dto.NginxParam{{Name: "proxy_cache_path"}}, &website)
}
func (w WebsiteService) GetProxyCache(id uint) (res response.NginxProxyCache, err error) {
var (
website model.Website
)
website, err = websiteRepo.GetFirst(repo.WithByID(id))
if err != nil {
return
}
parser, err := parser.NewParser(GetSitePath(website, SiteConf))
if err != nil {
return
}
config, err := parser.Parse()
if err != nil {
return
}
var params []string
for _, d := range config.GetDirectives() {
if d.GetName() == "proxy_cache_path" {
params = d.GetParameters()
}
}
if len(params) == 0 {
return
}
for _, param := range params {
if re.GetRegex(re.ProxyCacheZonePattern).MatchString(param) {
matches := re.GetRegex(re.ProxyCacheZonePattern).FindStringSubmatch(param)
if len(matches) > 0 {
res.ShareCache, _ = strconv.Atoi(matches[1])
res.ShareCacheUnit = matches[2]
}
}
if re.GetRegex(re.ProxyCacheMaxSizeValidationPattern).MatchString(param) {
matches := re.GetRegex(re.ProxyCacheMaxSizePattern).FindStringSubmatch(param)
if len(matches) > 0 {
res.CacheLimit, _ = strconv.ParseFloat(matches[1], 64)
res.CacheLimitUnit = matches[2]
}
}
if re.GetRegex(re.ProxyCacheInactivePattern).MatchString(param) {
matches := re.GetRegex(re.ProxyCacheInactivePattern).FindStringSubmatch(param)
if len(matches) > 0 {
res.CacheExpire, _ = strconv.Atoi(matches[1])
res.CacheExpireUnit = matches[2]
}
}
}
res.Open = true
return
}
func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, err error) {
var (
website model.Website
fileList response.FileInfo
)
website, err = websiteRepo.GetFirst(repo.WithByID(id))
if err != nil {
return
}
includeDir := GetSitePath(website, SiteProxyDir)
fileOp := files.NewFileOp()
if !fileOp.Stat(includeDir) {
return
}
fileList, err = NewIFileService().GetFileList(request.FileOption{FileOption: files.FileOption{Path: includeDir, Expand: true, Page: 1, PageSize: 100}})
if len(fileList.Items) == 0 {
return
}
var (
content []byte
config *components.Config
)
for _, configFile := range fileList.Items {
proxyConfig := request.WebsiteProxyConfig{
ID: website.ID,
}
parts := strings.Split(configFile.Name, ".")
proxyConfig.Name = parts[0]
if parts[1] == "conf" {
proxyConfig.Enable = true
} else {
proxyConfig.Enable = false
}
proxyConfig.FilePath = configFile.Path
content, err = fileOp.GetContent(configFile.Path)
if err != nil {
return
}
proxyConfig.Content = string(content)
config, err = parser.NewStringParser(string(content)).Parse()
if err != nil {
return nil, err
}
directives := config.GetDirectives()
var location *components.Location
for _, directive := range directives {
if loc, ok := directive.(*components.Location); ok {
location = loc
break
}
}
if location == nil {
err = errors.New("invalid proxy config, no location found")
return
}
proxyConfig.ProxyPass = location.ProxyPass
proxyConfig.Cache = location.Cache
if location.CacheTime > 0 {
proxyConfig.CacheTime = location.CacheTime
proxyConfig.CacheUnit = location.CacheUint
}
if location.ServerCacheTime > 0 {
proxyConfig.ServerCacheTime = location.ServerCacheTime
proxyConfig.ServerCacheUnit = location.ServerCacheUint
}
proxyConfig.Match = location.Match
proxyConfig.Modifier = location.Modifier
proxyConfig.ProxyHost = location.Host
proxyConfig.Replaces = location.Replaces
for _, directive := range location.Directives {
if directive.GetName() == "proxy_ssl_server_name" {
proxyConfig.SNI = directive.GetParameters()[0] == "on"
}
if directive.GetName() == "proxy_ssl_name" && len(directive.GetParameters()) > 0 {
proxyConfig.ProxySSLName = directive.GetParameters()[0]
}
}
proxyConfig.Cors = location.Cors
proxyConfig.AllowCredentials = location.AllowCredentials
proxyConfig.AllowHeaders = location.AllowHeaders
proxyConfig.AllowOrigins = location.AllowOrigins
proxyConfig.AllowMethods = location.AllowMethods
proxyConfig.Preflight = location.Preflight
res = append(res, proxyConfig)
}
return
}
func (w WebsiteService) UpdateProxyFile(req request.NginxProxyUpdate) (err error) {
var (
website model.Website
oldRewriteContent []byte
)
website, err = websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return err
}
absolutePath := fmt.Sprintf("%s/%s.conf", GetSitePath(website, SiteProxyDir), req.Name)
fileOp := files.NewFileOp()
oldRewriteContent, err = fileOp.GetContent(absolutePath)
if err != nil {
return err
}
if err = fileOp.WriteFile(absolutePath, strings.NewReader(req.Content), constant.DirPerm); err != nil {
return err
}
defer func() {
if err != nil {
_ = fileOp.WriteFile(absolutePath, bytes.NewReader(oldRewriteContent), constant.DirPerm)
}
}()
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
}
func (w WebsiteService) ClearProxyCache(req request.NginxCommonReq) error {
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return err
}
cacheDir := GetSitePath(website, SiteCacheDir)
fileOp := files.NewFileOp()
if fileOp.Stat(cacheDir) {
if err = fileOp.CleanDir(cacheDir); err != nil {
return err
}
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
if err = opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
return err
}
return nil
}
func (w WebsiteService) DeleteProxy(req request.WebsiteProxyDel) (err error) {
fileOp := files.NewFileOp()
website, err := websiteRepo.GetFirst(repo.WithByID(req.ID))
if err != nil {
return
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return
}
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy")
if !fileOp.Stat(includeDir) {
_ = fileOp.CreateDir(includeDir, 0755)
}
fileName := fmt.Sprintf("%s.conf", req.Name)
includePath := path.Join(includeDir, fileName)
backName := fmt.Sprintf("%s.bak", req.Name)
backPath := path.Join(includeDir, backName)
_ = fileOp.DeleteFile(includePath)
_ = fileOp.DeleteFile(backPath)
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
}
+124
View File
@@ -0,0 +1,124 @@
package service
import (
"bytes"
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"os"
"path"
"strings"
)
func (w WebsiteService) UpdateRewriteConfig(req request.NginxRewriteUpdate) error {
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return err
}
includePath := fmt.Sprintf("/www/sites/%s/rewrite/%s.conf", website.Alias, website.Alias)
absolutePath := GetSitePath(website, SiteReWritePath)
fileOp := files.NewFileOp()
var oldRewriteContent []byte
if !fileOp.Stat(path.Dir(absolutePath)) {
if err := fileOp.CreateDir(path.Dir(absolutePath), constant.DirPerm); err != nil {
return err
}
}
if !fileOp.Stat(absolutePath) {
if err := fileOp.CreateFile(absolutePath); err != nil {
return err
}
} else {
oldRewriteContent, err = fileOp.GetContent(absolutePath)
if err != nil {
return err
}
}
if err := fileOp.WriteFile(absolutePath, strings.NewReader(req.Content), constant.DirPerm); err != nil {
return err
}
if err := updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{includePath}}}, &website); err != nil {
_ = fileOp.WriteFile(absolutePath, bytes.NewReader(oldRewriteContent), constant.DirPerm)
return err
}
website.Rewrite = req.Name
return websiteRepo.Save(context.Background(), &website)
}
func (w WebsiteService) GetRewriteConfig(req request.NginxRewriteReq) (*response.NginxRewriteRes, error) {
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
if err != nil {
return nil, err
}
var contentByte []byte
if req.Name == "current" {
rewriteConfPath := GetSitePath(website, SiteReWritePath)
fileOp := files.NewFileOp()
if fileOp.Stat(rewriteConfPath) {
contentByte, err = fileOp.GetContent(rewriteConfPath)
if err != nil {
return nil, err
}
}
} else {
rewriteFile := fmt.Sprintf("rewrite/%s.conf", strings.ToLower(req.Name))
contentByte, _ = nginx_conf.Rewrites.ReadFile(rewriteFile)
if contentByte == nil {
customRewriteDir := GetOpenrestyDir(DefaultRewriteDir)
customRewriteFile := path.Join(customRewriteDir, fmt.Sprintf("%s.conf", strings.ToLower(req.Name)))
contentByte, err = files.NewFileOp().GetContent(customRewriteFile)
}
}
return &response.NginxRewriteRes{
Content: string(contentByte),
}, err
}
func (w WebsiteService) OperateCustomRewrite(req request.CustomRewriteOperate) error {
rewriteDir := GetOpenrestyDir(DefaultRewriteDir)
fileOp := files.NewFileOp()
if !fileOp.Stat(rewriteDir) {
if err := fileOp.CreateDir(rewriteDir, constant.DirPerm); err != nil {
return err
}
}
rewriteFile := path.Join(rewriteDir, fmt.Sprintf("%s.conf", req.Name))
switch req.Operate {
case "create":
if fileOp.Stat(rewriteFile) {
return buserr.New("ErrNameIsExist")
}
return fileOp.WriteFile(rewriteFile, strings.NewReader(req.Content), constant.DirPerm)
case "delete":
return fileOp.DeleteFile(rewriteFile)
}
return nil
}
func (w WebsiteService) ListCustomRewrite() ([]string, error) {
rewriteDir := GetOpenrestyDir(DefaultRewriteDir)
fileOp := files.NewFileOp()
if !fileOp.Stat(rewriteDir) {
return nil, nil
}
entries, err := os.ReadDir(rewriteDir)
if err != nil {
return nil, err
}
var res []string
for _, entry := range entries {
if entry.IsDir() {
continue
}
res = append(res, strings.TrimSuffix(entry.Name(), ".conf"))
}
return res, nil
}
+185 -103
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"log"
"net"
"os"
@@ -16,8 +17,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
@@ -144,11 +143,13 @@ func createWebsiteFolder(website *model.Website, runtime *model.Runtime) error {
if err := fileOp.CreateFile(path.Join(siteFolder, "log", "error.log")); err != nil {
return err
}
if err := fileOp.CreateDir(path.Join(siteFolder, "index"), constant.DirPerm); err != nil {
return err
}
if err := fileOp.CreateDir(path.Join(siteFolder, "ssl"), constant.DirPerm); err != nil {
return err
if website.Type != constant.Stream {
if err := fileOp.CreateDir(path.Join(siteFolder, "index"), constant.DirPerm); err != nil {
return err
}
if err := fileOp.CreateDir(path.Join(siteFolder, "ssl"), constant.DirPerm); err != nil {
return err
}
}
if website.Type == constant.Runtime {
if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceLocal {
@@ -175,7 +176,7 @@ func createWebsiteFolder(website *model.Website, runtime *model.Runtime) error {
return nil
}
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime) error {
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
@@ -183,72 +184,83 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
if err = createWebsiteFolder(website, runtime); err != nil {
return err
}
configPath := GetSitePath(*website, SiteConf)
nginxContent := nginx_conf.GetWebsiteFile("website_default.conf")
config, err := parser.NewStringParser(string(nginxContent)).Parse()
if err != nil {
return err
}
servers := config.FindServers()
if len(servers) == 0 {
return errors.New("nginx config is not valid")
}
server := servers[0]
server.DeleteListen("80")
var serverNames []string
for _, domain := range domains {
serverNames = append(serverNames, domain.Domain)
setListen(server, strconv.Itoa(domain.Port), website.IPV6, false, website.DefaultServer, false)
}
server.UpdateServerName(serverNames)
siteFolder := path.Join("/www", "sites", website.Alias)
server.UpdateDirective("access_log", []string{path.Join(siteFolder, "log", "access.log"), "main"})
server.UpdateDirective("error_log", []string{path.Join(siteFolder, "log", "error.log")})
rootIndex := path.Join("/www/sites", website.Alias, "index")
switch website.Type {
case constant.Deployment:
proxy := fmt.Sprintf("http://127.0.0.1:%d", appInstall.HttpPort)
server.UpdateRootProxy([]string{proxy})
case constant.Static:
server.UpdateRoot(rootIndex)
server.UpdateDirective("error_page", []string{"404", "/404.html"})
case constant.Proxy:
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
server.UpdateDirective("include", []string{nginxInclude})
server.UpdateRoot(rootIndex)
case constant.Runtime:
switch runtime.Type {
case constant.RuntimePHP:
server.UpdateDirective("error_page", []string{"404", "/404.html"})
if runtime.Resource == constant.ResourceLocal {
server.UpdateRoot(rootIndex)
localPath := path.Join(rootIndex, "index.php")
server.UpdatePHPProxy([]string{website.Proxy}, localPath)
} else {
server.UpdateRoot(rootIndex)
server.UpdatePHPProxy([]string{website.Proxy}, "")
}
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
server.UpdateRootProxy([]string{fmt.Sprintf("http://%s", website.Proxy)})
}
case constant.Subsite:
parentWebsite, err := websiteRepo.GetFirst(repo.WithByID(website.ParentWebsiteID))
var (
configPath string
config *components.Config
)
if website.Type == constant.Stream {
nginxContent := nginx_conf.GetWebsiteFile("stream_default.conf")
config, err = parser.NewStringParser(string(nginxContent)).Parse()
if err != nil {
return err
}
website.Proxy = parentWebsite.Proxy
rootIndex = path.Join("/www/sites", parentWebsite.Alias, "index", website.SiteDir)
server.UpdateDirective("error_page", []string{"404", "/404.html"})
if parentWebsite.Type == constant.Runtime {
parentRuntime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(parentWebsite.RuntimeID))
if err != nil {
return err
servers := config.FindServers()
if len(servers) == 0 {
return errors.New("nginx config is not valid")
}
server := servers[0]
ports := strings.Split(streamConfig.StreamPorts, ",")
for _, port := range ports {
server.UpdateListen(port, false)
if website.IPV6 {
server.UpdateListen("[::]:"+port, false)
}
website.RuntimeID = parentRuntime.ID
if parentRuntime.Type == constant.RuntimePHP {
if parentRuntime.Resource == constant.ResourceLocal {
}
siteFolder := path.Join("/www", "sites", website.Alias)
server.UpdateDirective("access_log", []string{path.Join(siteFolder, "log", "access.log"), "streamlog"})
server.UpdateDirective("error_log", []string{path.Join(siteFolder, "log", "error.log")})
server.UpdateDirective("proxy_pass", []string{website.Alias})
upstream := components.Upstream{
UpstreamName: website.Alias,
}
if streamConfig.Algorithm != "default" {
upstream.UpdateDirective(streamConfig.Algorithm, []string{})
}
upstream.UpstreamServers = parseUpstreamServers(streamConfig.Servers)
config.Block.Directives = append(config.Block.Directives, &upstream)
configPath = GetSitePath(*website, StreamConf)
} else {
configPath = GetSitePath(*website, SiteConf)
nginxContent := nginx_conf.GetWebsiteFile("website_default.conf")
config, err = parser.NewStringParser(string(nginxContent)).Parse()
if err != nil {
return err
}
servers := config.FindServers()
if len(servers) == 0 {
return errors.New("nginx config is not valid")
}
server := servers[0]
server.DeleteListen("80")
var serverNames []string
for _, domain := range domains {
serverNames = append(serverNames, domain.Domain)
setListen(server, strconv.Itoa(domain.Port), website.IPV6, false, website.DefaultServer, false)
}
server.UpdateServerName(serverNames)
siteFolder := path.Join("/www", "sites", website.Alias)
server.UpdateDirective("access_log", []string{path.Join(siteFolder, "log", "access.log"), "main"})
server.UpdateDirective("error_log", []string{path.Join(siteFolder, "log", "error.log")})
rootIndex := path.Join("/www/sites", website.Alias, "index")
switch website.Type {
case constant.Deployment:
proxy := fmt.Sprintf("http://127.0.0.1:%d", appInstall.HttpPort)
server.UpdateRootProxy([]string{proxy})
case constant.Static:
server.UpdateRoot(rootIndex)
server.UpdateDirective("error_page", []string{"404", "/404.html"})
case constant.Proxy:
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
server.UpdateDirective("include", []string{nginxInclude})
server.UpdateRoot(rootIndex)
case constant.Runtime:
switch runtime.Type {
case constant.RuntimePHP:
server.UpdateDirective("error_page", []string{"404", "/404.html"})
if runtime.Resource == constant.ResourceLocal {
server.UpdateRoot(rootIndex)
localPath := path.Join(rootIndex, "index.php")
server.UpdatePHPProxy([]string{website.Proxy}, localPath)
@@ -256,13 +268,39 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
server.UpdateRoot(rootIndex)
server.UpdatePHPProxy([]string{website.Proxy}, "")
}
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo, constant.RuntimePython, constant.RuntimeDotNet:
server.UpdateRootProxy([]string{fmt.Sprintf("http://%s", website.Proxy)})
}
case constant.Subsite:
parentWebsite, err := websiteRepo.GetFirst(repo.WithByID(website.ParentWebsiteID))
if err != nil {
return err
}
website.Proxy = parentWebsite.Proxy
rootIndex = path.Join("/www/sites", parentWebsite.Alias, "index", website.SiteDir)
server.UpdateDirective("error_page", []string{"404", "/404.html"})
if parentWebsite.Type == constant.Runtime {
parentRuntime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(parentWebsite.RuntimeID))
if err != nil {
return err
}
website.RuntimeID = parentRuntime.ID
if parentRuntime.Type == constant.RuntimePHP {
if parentRuntime.Resource == constant.ResourceLocal {
server.UpdateRoot(rootIndex)
localPath := path.Join(rootIndex, "index.php")
server.UpdatePHPProxy([]string{website.Proxy}, localPath)
} else {
server.UpdateRoot(rootIndex)
server.UpdatePHPProxy([]string{website.Proxy}, "")
}
}
}
if parentWebsite.Type == constant.Static {
server.UpdateRoot(rootIndex)
}
}
if parentWebsite.Type == constant.Static {
server.UpdateRoot(rootIndex)
}
}
config.FilePath = configPath
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return err
@@ -432,14 +470,20 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
}
func delNginxConfig(website model.Website, force bool) error {
configPath := GetSitePath(website, SiteConf)
fileOp := files.NewFileOp()
var (
configPath string
)
if website.Type == constant.Stream {
configPath = GetSitePath(website, StreamConf)
if !fileOp.Stat(configPath) {
return nil
} else {
configPath = GetSitePath(website, SiteConf)
}
if err := fileOp.DeleteFile(configPath); err != nil {
return err
if fileOp.Stat(configPath) {
if err := fileOp.DeleteFile(configPath); err != nil {
return err
}
}
sitePath := GteSiteDir(website.Alias)
if fileOp.Stat(sitePath) {
@@ -880,9 +924,16 @@ func deleteWebsiteFolder(website *model.Website) error {
if fileOp.Stat(siteFolder) {
_ = fileOp.DeleteDir(siteFolder)
}
nginxFilePath := GetSitePath(*website, SiteConf)
if fileOp.Stat(nginxFilePath) {
_ = fileOp.DeleteFile(nginxFilePath)
if website.Type == constant.Stream {
steamFilePath := GetSitePath(*website, StreamConf)
if fileOp.Stat(steamFilePath) {
_ = fileOp.DeleteFile(steamFilePath)
}
} else {
nginxFilePath := GetSitePath(*website, SiteConf)
if fileOp.Stat(nginxFilePath) {
_ = fileOp.DeleteFile(nginxFilePath)
}
}
return nil
}
@@ -1089,27 +1140,8 @@ func getWebsiteDomains(domains []request.WebsiteDomain, defaultHTTPPort, default
addPorts = append(addPorts, port)
continue
}
if existPorts, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithPort(port)); len(existPorts) == 0 {
errMap := make(map[string]interface{})
errMap["port"] = port
appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port))
if appInstall.ID > 0 {
errMap["type"] = i18n.GetMsgByKey("TYPE_APP")
errMap["name"] = appInstall.Name
err = buserr.WithMap("ErrPortExist", errMap, nil)
return
}
runtime, _ := runtimeRepo.GetFirst(context.Background(), runtimeRepo.WithPort(port))
if runtime != nil {
errMap["type"] = i18n.GetMsgByKey("TYPE_RUNTIME")
errMap["name"] = runtime.Name
err = buserr.WithMap("ErrPortExist", errMap, nil)
return
}
if port != defaultHTTPsPort && common.ScanPort(port) {
err = buserr.WithDetail("ErrPortInUsed", port, nil)
return
}
if err = checkWebsitePort(defaultHTTPsPort, port, ""); err != nil {
return
}
if existPorts, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteID), websiteDomainRepo.WithPort(port)); len(existPorts) == 0 {
addPorts = append(addPorts, port)
@@ -1119,6 +1151,43 @@ func getWebsiteDomains(domains []request.WebsiteDomain, defaultHTTPPort, default
return
}
func checkWebsitePort(defaultHTTPsPort, port int, websiteType string) error {
if existPorts, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithPort(port)); len(existPorts) > 0 {
return nil
}
if websiteType == constant.Stream {
websites, _ := websiteRepo.List(websiteRepo.WithType(constant.Stream))
for _, website := range websites {
ports := strings.Split(website.StreamPorts, ",")
for _, p := range ports {
pInt, _ := strconv.Atoi(p)
if pInt == port {
return nil
}
}
}
}
errMap := make(map[string]interface{})
errMap["port"] = port
appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port))
if appInstall.ID > 0 {
errMap["type"] = i18n.GetMsgByKey("TYPE_APP")
errMap["name"] = appInstall.Name
return buserr.WithMap("ErrPortExist", errMap, nil)
}
runtime, _ := runtimeRepo.GetFirst(context.Background(), runtimeRepo.WithPort(port))
if runtime != nil {
errMap["type"] = i18n.GetMsgByKey("TYPE_RUNTIME")
errMap["name"] = runtime.Name
return buserr.WithMap("ErrPortExist", errMap, nil)
}
if port != defaultHTTPsPort && common.ScanPort(port) {
return buserr.WithDetail("ErrPortInUsed", port, nil)
}
return nil
}
func saveCertificateFile(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
if websiteSSL.PushDir {
fileOp := files.NewFileOp()
@@ -1304,12 +1373,23 @@ const (
SitePathAuthBasicDir = "SitePathAuthBasicDir"
SiteUpstreamDir = "SiteUpstreamDir"
SiteCorsPath = "SiteCorsPath"
StreamDir = "StreamDir"
StreamConf = "StreamConf"
)
func GetWebsiteConfigPath(website model.Website) string {
if website.Type != constant.Stream {
return GetSitePath(website, SiteConf)
}
return GetSitePath(website, StreamConf)
}
func GetSitePath(website model.Website, confType string) string {
switch confType {
case SiteConf:
return path.Join(GetWebSiteRootDir(), "conf.d", website.Alias+".conf")
case StreamConf:
return path.Join(GetWebSiteRootDir(), "stream.d", website.Alias+".conf")
case SiteAccessLog:
return path.Join(GteSiteDir(website.Alias), "log", "access.log")
case SiteErrorLog:
@@ -1346,6 +1426,8 @@ func GetOpenrestyDir(confType string) string {
return GetWebSiteRootDir()
case SiteConfDir:
return path.Join(GetWebSiteRootDir(), "conf.d")
case StreamDir:
return path.Join(GetWebSiteRootDir(), "steam.d")
case SitesRootDir:
return path.Join(GetWebSiteRootDir(), "sites")
case DefaultDir:
@@ -0,0 +1,5 @@
server {
proxy_pass mysql_cluster;
access_log /www/sites/domain/log/access.log streamlog;
error_log /www/sites/domain/log/error.log;
}
+4 -2
View File
@@ -4,8 +4,9 @@ const (
WebRunning = "Running"
WebStopped = "Stopped"
ProtocolHTTP = "HTTP"
ProtocolHTTPS = "HTTPS"
ProtocolHTTP = "HTTP"
ProtocolHTTPS = "HTTPS"
ProtocolStream = "TCP/UDP"
NewApp = "new"
InstalledApp = "installed"
@@ -15,6 +16,7 @@ const (
Proxy = "proxy"
Runtime = "runtime"
Subsite = "subsite"
Stream = "stream"
SSLExisted = "existed"
SSLAuto = "auto"
+5 -5
View File
@@ -42,7 +42,7 @@ require (
github.com/pkg/sftp v1.13.6
github.com/qiniu/go-sdk/v7 v7.21.1
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.7
github.com/shirou/gopsutil/v4 v4.25.11
github.com/sirupsen/logrus v1.9.3
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.10.1
@@ -121,7 +121,7 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
@@ -206,7 +206,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rs/xid v1.5.0 // indirect
@@ -221,8 +221,8 @@ require (
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.3 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect
github.com/tonistiigi/fsutil v0.0.0-20250605211040-586307ad452f // indirect
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
+10
View File
@@ -310,6 +310,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -831,6 +833,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
@@ -896,6 +900,8 @@ github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -962,8 +968,12 @@ github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19 h1:ZCmSnT6CLGhfoQ2lPEhL4nsJstKDCw1F1RfN8/smTCU=
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19/go.mod h1:SXTY+QvI+KTTKXQdg0zZ7nx0u94QWh8ZAwBQYsW9cqk=
github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4=
+1
View File
@@ -57,6 +57,7 @@ func InitAgentDB() {
migrations.AddGPUMonitor,
migrations.UpdateDatabaseMysql,
migrations.InitIptablesStatus,
migrations.UpdateWebsite,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
+7
View File
@@ -775,3 +775,10 @@ var InitIptablesStatus = &gormigrate.Migration{
return nil
},
}
var UpdateWebsite = &gormigrate.Migration{
ID: "20251203-update-website",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.Website{})
},
}
+1
View File
@@ -93,5 +93,6 @@ func (a *WebsiteRouter) InitRouter(Router *gin.RouterGroup) {
websiteRouter.POST("/crosssite", baseApi.OperateCrossSiteAccess)
websiteRouter.POST("/exec/composer", baseApi.ExecComposer)
websiteRouter.POST("/stream/update", baseApi.UpdateStreamConfig)
}
}
+15
View File
@@ -27,6 +27,7 @@ export namespace Website {
dbID: number;
dbType: string;
favorite: boolean;
streamPorts: string;
}
export interface WebsiteDTO extends Website {
@@ -37,7 +38,17 @@ export namespace Website {
runtimeName: string;
runtimeType: string;
openBaseDir: boolean;
algorithm: string;
servers: NginxUpstreamServer[];
}
export interface WebsiteStreamUpdate {
websiteID: number;
algorithm: string;
streamPorts?: string;
servers: NginxUpstreamServer[];
}
export interface WebsiteRes extends CommonModel {
protocol: string;
primaryDomain: string;
@@ -93,6 +104,10 @@ export namespace Website {
dbUser?: string;
dbHost?: string;
domains: SubDomain[];
streamPorts?: string;
name: string;
algorithm: string;
servers: NginxUpstreamServer[];
}
export interface WebSiteUpdateReq {
+4
View File
@@ -371,3 +371,7 @@ export const updateCorsConfig = (req: Website.CorsConfigReq) => {
export const batchSetGroup = (req: Website.BatchSetGroup) => {
return http.post(`/websites/batch/group`, req);
};
export const updateWebsiteStream = (req: Website.WebsiteStreamUpdate) => {
return http.post(`/websites/stream/update`, req);
};
+29 -17
View File
@@ -412,23 +412,31 @@ export const Actions = [
},
];
export const getAlgorithms = () => [
{
label: i18n.global.t('commons.table.default'),
value: 'default',
placeHolder: i18n.global.t('website.defaultHelper'),
},
{
label: i18n.global.t('website.ipHash'),
value: 'ip_hash',
placeHolder: i18n.global.t('website.ipHashHelper'),
},
{
label: i18n.global.t('website.leastConn'),
value: 'least_conn',
placeHolder: i18n.global.t('website.leastConnHelper'),
},
];
export const getAlgorithms = (type: string) => {
const baseAlgorithms = [
{
label: i18n.global.t('commons.table.default'),
value: 'default',
placeHolder: i18n.global.t('website.defaultHelper'),
},
{
label: i18n.global.t('website.ipHash'),
value: 'ip_hash',
placeHolder: i18n.global.t('website.ipHashHelper'),
},
{
label: i18n.global.t('website.leastConn'),
value: 'least_conn',
placeHolder: i18n.global.t('website.leastConnHelper'),
},
];
if (type === 'stream') {
return baseAlgorithms.filter((algo) => algo.value !== 'ip_hash');
}
return baseAlgorithms;
};
export const getStatusStrategy = () => [
{
@@ -462,4 +470,8 @@ export const getWebsiteTypes = () => [
label: i18n.global.t('website.subsite'),
value: 'subsite',
},
{
label: i18n.global.t('website.stream'),
value: 'stream',
},
];
+3
View File
@@ -2699,6 +2699,9 @@ const message = {
batchOpreate: 'Batch Operation',
batchOpreateHelper: 'Batch {0} websites, continue operation?',
stream: 'TCP/UDP Proxy',
streamPorts: 'Listening Ports',
streamPortsHelper: 'Separate with commas, e.g., 3306, 3307',
},
php: {
short_open_tag: 'Short tag support',
+3
View File
@@ -2683,6 +2683,9 @@ const message = {
batchOpreate: 'Operación en Lote',
batchOpreateHelper: 'Lote {0} sitios web, ¿continuar operación?',
stream: 'Proxy TCP/UDP',
streamPorts: 'Puertos de escucha',
streamPortsHelper: 'Separe con comas, por ejemplo: 3306, 3307',
},
php: {
short_open_tag: 'Soporte de etiquetas cortas',
+3
View File
@@ -2622,6 +2622,9 @@ const message = {
batchOpreate: 'バッチ操作',
batchOpreateHelper: 'ウェブサイトをバッチ{0}しますか',
stream: 'TCP/UDP プロキシ',
streamPorts: '待ち受けポート',
streamPortsHelper: 'カンマで区切ってください3306, 3307',
},
php: {
short_open_tag: '短いタグサポート',
+3
View File
@@ -2576,6 +2576,9 @@ const message = {
batchOpreate: '일괄 작업',
batchOpreateHelper: '웹사이트를 일괄 {0}, 계속 작업하시겠습니까?',
stream: 'TCP/UDP 프록시',
streamPorts: '수신 포트',
streamPortsHelper: '쉼표로 구분하세요. : 3306, 3307',
},
php: {
short_open_tag: '짧은 태그 지원',
+3
View File
@@ -2677,6 +2677,9 @@ const message = {
batchOpreate: 'Operasi Pukal',
batchOpreateHelper: 'Pukal {0} laman web, teruskan operasi?',
stream: 'Proksi TCP/UDP',
streamPorts: 'Port Mendengar',
streamPortsHelper: 'Sila asingkan dengan koma, contoh: 3306, 3307',
},
php: {
short_open_tag: 'Sokongan tag pendek',
+3
View File
@@ -2682,6 +2682,9 @@ const message = {
batchOpreate: 'Operação em Lote',
batchOpreateHelper: 'Lote {0} sites, continuar operação?',
stream: 'Proxy TCP/UDP',
streamPorts: 'Portas de escuta',
streamPortsHelper: 'Separe por vírgulas, por exemplo: 3306, 3307',
},
php: {
short_open_tag: 'Suporte para short tags',
+3
View File
@@ -2678,6 +2678,9 @@ const message = {
batchOpreate: 'Пакетная операция',
batchOpreateHelper: 'Пакетное {0} веб-сайтов, продолжить операцию?',
stream: 'Прокси TCP/UDP',
streamPorts: 'Порты прослушивания',
streamPortsHelper: 'Разделяйте запятыми, например: 3306, 3307',
},
php: {
short_open_tag: 'Поддержка коротких тегов',
+3
View File
@@ -2735,6 +2735,9 @@ const message = {
batchOpreate: 'Toplu İşlem',
batchOpreateHelper: 'Toplu {0} web siteleri, işlemi devam ettir?',
stream: 'TCP/UDP Proxy',
streamPorts: 'Dinleme Portları',
streamPortsHelper: 'Virgülle ayırın, örneğin: 3306, 3307',
},
php: {
short_open_tag: 'Kısa etiket desteği',
+3
View File
@@ -2510,6 +2510,9 @@ const message = {
batchOpreate: '批次操作',
batchOpreateHelper: '批次{0}網站是否繼續操作',
stream: 'TCP/UDP 代理',
streamPorts: '監聽端口',
streamPortsHelper: '請用逗號分隔例如33063307',
},
php: {
short_open_tag: '短標籤支援',
+3
View File
@@ -2506,6 +2506,9 @@ const message = {
batchOpreate: '批量操作',
batchOpreateHelper: '批量{0}网站是否继续操作',
stream: 'TCP/UDP 代理',
streamPorts: '监听端口',
streamPortsHelper: '请按逗号分割例如33063307',
},
php: {
short_open_tag: '短标签支持',
@@ -0,0 +1,38 @@
<template>
<el-descriptions :column="7" border direction="vertical">
<el-descriptions-item :label="$t('website.primaryDomain')">
{{ websiteSSL.primaryDomain }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.otherDomains')">
{{ websiteSSL.domains }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.brand')">
{{ websiteSSL.organization }}
</el-descriptions-item>
<el-descriptions-item :label="$t('ssl.provider')">
{{ getProvider(websiteSSL.provider) }}
</el-descriptions-item>
<el-descriptions-item
:label="$t('ssl.acmeAccount')"
v-if="websiteSSL.acmeAccount && websiteSSL.provider !== 'manual'"
>
{{ websiteSSL.acmeAccount.email }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.expireDate')">
{{ dateFormatSimple(websiteSSL.expireDate) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.remark')">
{{ websiteSSL.description }}
</el-descriptions-item>
</el-descriptions>
</template>
<script lang="ts" setup>
import { dateFormatSimple, getProvider } from '@/utils/util';
defineProps({
websiteSSL: {
type: Object,
required: true,
},
});
</script>
@@ -133,32 +133,7 @@
</div>
</div>
<el-form-item :label="' '" v-if="websiteSSL && websiteSSL.id > 0">
<el-descriptions :column="7" border direction="vertical">
<el-descriptions-item :label="$t('website.primaryDomain')">
{{ websiteSSL.primaryDomain }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.otherDomains')">
{{ websiteSSL.domains }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.brand')">
{{ websiteSSL.organization }}
</el-descriptions-item>
<el-descriptions-item :label="$t('ssl.provider')">
{{ getProvider(websiteSSL.provider) }}
</el-descriptions-item>
<el-descriptions-item
:label="$t('ssl.acmeAccount')"
v-if="websiteSSL.acmeAccount && websiteSSL.provider !== 'manual'"
>
{{ websiteSSL.acmeAccount.email }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.expireDate')">
{{ dateFormatSimple(websiteSSL.expireDate) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.remark')">
{{ websiteSSL.description }}
</el-descriptions-item>
</el-descriptions>
<WebsiteSSL :websiteSSL="websiteSSL" />
</el-form-item>
<el-divider content-position="left">{{ $t('website.SSLProConfig') }}</el-divider>
<el-form-item :label="$t('website.supportProtocol')" prop="SSLProtocol">
@@ -191,15 +166,17 @@
<FileList ref="certFileRef" @choose="getCertificatePath" />
</template>
<script lang="ts" setup>
import FileList from '@/components/file-list/index.vue';
import WebsiteSSL from '@/views/website/website/components/website-ssl/index.vue';
import { Website } from '@/api/interface/website';
import { getHTTPSConfig, listSSL, searchAcmeAccount, updateHTTPSConfig } from '@/api/modules/website';
import { ElMessageBox, FormInstance } from 'element-plus';
import { computed, onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { Rules } from '@/global/form-rules';
import { dateFormatSimple, getProvider, getAccountName } from '@/utils/util';
import { getAccountName } from '@/utils/util';
import { MsgSuccess } from '@/utils/message';
import FileList from '@/components/file-list/index.vue';
const keyFileRef = ref();
const certFileRef = ref();
@@ -1,6 +1,12 @@
<template>
<div :style="{ '--main-height': mainHeight + 'px' }">
<el-tabs tab-position="left" v-model="tabIndex" v-if="id > 0" class="custom-tabs" ref="tabsRef">
<el-tabs
tab-position="left"
v-model="tabIndex"
v-if="id > 0 && website.type != 'stream'"
class="custom-tabs"
ref="tabsRef"
>
<el-tab-pane :label="$t('website.domainConfig')" name="0">
<Domain :key="id" :id="id" v-if="tabIndex == '0'"></Domain>
</el-tab-pane>
@@ -59,6 +65,20 @@
<Other :id="id" v-if="tabIndex == '12'"></Other>
</el-tab-pane>
</el-tabs>
<el-tabs
tab-position="left"
v-model="tabIndex"
v-if="id > 0 && website.type == 'stream'"
class="custom-tabs"
ref="tabsRef"
>
<el-tab-pane :label="$t('website.other')" name="12">
<Other :id="id" v-if="tabIndex == '12'"></Other>
</el-tab-pane>
<el-tab-pane :label="$t('website.stream')" name="13">
<Stream :id="id" v-if="tabIndex == '13'"></Stream>
</el-tab-pane>
</el-tabs>
</div>
</template>
@@ -81,6 +101,7 @@ import PHP from './php/index.vue';
import RealIP from './real-ip/index.vue';
import Resource from './resource/index.vue';
import Cors from './cors/index.vue';
import Stream from './stream/index.vue';
const props = defineProps({
website: {
@@ -98,9 +119,10 @@ const id = computed(() => {
return props.website.id;
});
const tabIndex = ref('0');
const menuKey = ref('site-tabIndex-');
watch(tabIndex, (newVal) => {
localStorage.setItem('site-tabIndex', newVal);
localStorage.setItem(menuKey.value, newVal);
});
const handleResize = () => {
@@ -123,9 +145,15 @@ const handleScroll = (event: WheelEvent) => {
};
onMounted(() => {
const storedTabIndex = localStorage.getItem('site-tabIndex');
menuKey.value = 'site-tabIndex-' + props.website.id;
const storedTabIndex = localStorage.getItem(menuKey.value);
if (storedTabIndex !== null) {
tabIndex.value = storedTabIndex;
} else {
tabIndex.value = '0';
if (props.website.type == 'stream') {
tabIndex.value = '12';
}
}
window.addEventListener('resize', handleResize);
document.addEventListener('wheel', handleScroll, { passive: false });
@@ -0,0 +1,319 @@
<template>
<el-form ref="lbFormRef" label-position="top" :model="formData" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="formData.name" :disabled="disabled"></el-input>
</el-form-item>
<el-form-item :label="$t('website.algorithm')" prop="algorithm">
<el-select v-model="formData.algorithm" @change="handleAlgorithmChange">
<el-option
v-for="(algorithm, index) in Algorithms"
:label="algorithm.label"
:key="index"
:value="algorithm.value"
></el-option>
</el-select>
<span class="input-help">{{ getHelper(formData.algorithm) }}</span>
</el-form-item>
<div>
<el-card v-for="(server, index) of formData.servers" :key="index" class="server-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="server-title">
<el-icon><Monitor /></el-icon>
<span class="ml-2">{{ $t('website.server') }} - {{ server.server || index + 1 }}</span>
</span>
<el-button
v-if="formData.servers.length > 1"
text
type="danger"
icon="Delete"
size="small"
@click="removeServer(index)"
></el-button>
</div>
</template>
<div>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item
:label="$t('setting.address')"
:prop="`servers.${index}.server`"
:rules="rules.server"
>
<el-input
v-model="formData.servers[index].server"
:placeholder="'example.com:8080'"
@input="handleChange"
></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.weight')"
:prop="`servers.${index}.weight`"
:rules="rules.weight"
>
<el-input
type="number"
v-model.number="formData.servers[index].weight"
@input="handleChange"
></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.strategy')"
:prop="`servers.${index}.flag`"
:rules="rules.flag"
>
<el-select v-model="formData.servers[index].flag" clearable @change="handleChange">
<el-option
v-for="flag in getStatusStrategy()"
:label="flag.label"
:key="flag.value"
:value="flag.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.maxFails')"
:prop="`servers.${index}.maxFails`"
:rules="rules.maxFails"
>
<el-input
type="number"
v-model.number="formData.servers[index].maxFails"
@input="handleChange"
>
<template #append>{{ $t('commons.units.time') }}</template>
</el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item :prop="`servers.${index}.failTimeout`" :rules="rules.failTimeout">
<template #label>
<span class="inline-flex items-center">
{{ $t('website.failTimeout') }}
<el-tooltip :content="$t('website.failTimeoutHelper')" placement="top">
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</span>
</template>
<el-input
type="number"
v-model.number="formData.servers[index].failTimeout"
@input="handleChange"
>
<template #append>
<el-select
v-model.number="formData.servers[index].failTimeoutUnit"
class="!w-24"
@change="handleChange"
>
<el-option
v-for="(unit, indexKey) in Units"
:key="indexKey"
:label="unit.label"
:value="unit.value"
/>
</el-select>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.maxConns')"
:prop="`servers.${index}.maxConns`"
:rules="rules.maxConns"
>
<el-input
type="number"
v-model.number="formData.servers[index].maxConns"
@input="handleChange"
></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
<el-button class="add-server-btn" type="primary" plain @click="addServer" icon="Plus">
{{ $t('commons.button.add') + $t('website.server') }}
</el-button>
</div>
</el-form>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { FormInstance } from 'element-plus';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { getAlgorithms, getStatusStrategy, Units } from '@/global/mimetype';
import { Monitor, QuestionFilled } from '@element-plus/icons-vue';
interface Server {
server: string;
weight?: number;
maxFails?: number;
maxConns?: number;
failTimeout?: number;
failTimeoutUnit: string;
flag: string;
}
interface LoadBalanceFormData {
name: string;
algorithm: string;
servers: Server[];
}
interface Props {
modelValue: LoadBalanceFormData;
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
});
const emit = defineEmits<{
(e: 'update:modelValue', value: LoadBalanceFormData): void;
}>();
const lbFormRef = ref<FormInstance>();
const formData = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
const rules = ref<any>({
name: [Rules.appName],
algorithm: [Rules.requiredSelect],
server: [Rules.requiredInput],
weight: [checkNumberRange(0, 100)],
servers: {
type: Array,
},
maxFails: [checkNumberRange(0, 1000)],
maxConns: [checkNumberRange(0, 10000)],
failTimeout: [checkNumberRange(0, 300)],
});
const Algorithms = getAlgorithms('stream');
const helper = ref();
const getHelper = (key: string) => {
Algorithms.forEach((algorithm) => {
if (algorithm.value === key) {
helper.value = algorithm.placeHolder;
}
});
return helper.value;
};
const initServer = (): Server => ({
server: '',
weight: undefined,
maxFails: undefined,
maxConns: undefined,
failTimeout: undefined,
failTimeoutUnit: 's',
flag: '',
});
const addServer = () => {
const newServers = [...formData.value.servers, initServer()];
emit('update:modelValue', { ...formData.value, servers: newServers });
};
const removeServer = (index: number) => {
const newServers = [...formData.value.servers];
newServers.splice(index, 1);
emit('update:modelValue', { ...formData.value, servers: newServers });
};
const handleChange = () => {
emit('update:modelValue', { ...formData.value });
};
const handleAlgorithmChange = () => {
handleChange();
};
const validate = async () => {
if (!lbFormRef.value) return false;
return await lbFormRef.value.validate();
};
const resetFields = () => {
lbFormRef.value?.resetFields();
};
const clearValidate = () => {
lbFormRef.value?.clearValidate();
};
defineExpose({
validate,
resetFields,
clearValidate,
formRef: lbFormRef,
});
</script>
<style scoped lang="scss">
.server-card {
margin-bottom: 16px;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
border-color: var(--el-color-primary);
}
:deep(.el-card__header) {
padding: 12px 20px;
background-color: var(--el-fill-color-light);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 20px;
.server-title {
display: flex;
align-items: center;
font-weight: 500;
font-size: 14px;
color: var(--el-text-color-primary);
}
}
.add-server-btn {
width: 100%;
height: 48px;
border-style: dashed;
font-size: 14px;
margin-top: 8px;
&:hover {
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
}
</style>
@@ -90,7 +90,7 @@ const loading = ref(false);
const operateRef = ref();
const delRef = ref();
const fileRef = ref();
const Algorithms = getAlgorithms();
const Algorithms = getAlgorithms('');
const buttons = [
{
@@ -3,149 +3,17 @@
v-model="open"
@close="handleClose"
size="large"
:header="$t('commons.button.' + item.operate) + $t('website.loadBalance')"
:resource="item.operate == 'create' ? '' : item.name"
:header="$t('commons.button.' + operate) + $t('website.loadBalance')"
:resource="operate == 'create' ? '' : formData.name"
>
<el-form ref="lbForm" label-position="top" :model="item" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model.trim="item.name" :disabled="item.operate === 'edit'"></el-input>
</el-form-item>
<el-form-item :label="$t('website.algorithm')" prop="algorithm">
<el-select v-model="item.algorithm">
<el-option
v-for="(algorithm, index) in Algorithms"
:label="algorithm.label"
:key="index"
:value="algorithm.value"
></el-option>
</el-select>
<span class="input-help">{{ getHelper(item.algorithm) }}</span>
</el-form-item>
<LoadBalanceForm ref="lbFormRef" v-model="formData" :disabled="operate === 'edit' && disableName" />
<div>
<el-card v-for="(server, index) of item.servers" :key="index" class="server-card" shadow="hover">
<template #header>
<div class="card-header">
<span class="server-title">
<el-icon><Monitor /></el-icon>
<span class="ml-2">{{ $t('website.server') }} - {{ server.server }}</span>
</span>
<el-button
v-if="item.servers.length > 1"
text
type="danger"
icon="Delete"
size="small"
@click="removeServer(index)"
></el-button>
</div>
</template>
<div>
<el-row :gutter="16">
<el-col :span="24">
<el-form-item
:label="$t('setting.address')"
:prop="`servers.${index}.server`"
:rules="rules.server"
>
<el-input
v-model="item.servers[index].server"
:placeholder="'example.com:8080'"
></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.weight')"
:prop="`servers.${index}.weight`"
:rules="rules.weight"
>
<el-input type="number" v-model.number="item.servers[index].weight"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.strategy')"
:prop="`servers.${index}.flag`"
:rules="rules.flag"
>
<el-select v-model="item.servers[index].flag" clearable>
<el-option
v-for="flag in getStatusStrategy()"
:label="flag.label"
:key="flag.value"
:value="flag.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.maxFails')"
:prop="`servers.${index}.maxFails`"
:rules="rules.maxFails"
>
<el-input type="number" v-model.number="item.servers[index].maxFails">
<template #append>{{ $t('commons.units.time') }}</template>
</el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item :prop="`servers.${index}.failTimeout`" :rules="rules.failTimeout">
<template #label>
<span class="inline-flex items-center">
{{ $t('website.failTimeout') }}
<el-tooltip :content="$t('website.failTimeoutHelper')" placement="top">
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</span>
</template>
<el-input type="number" v-model.number="item.servers[index].failTimeout">
<template #append>
<el-select
v-model.number="item.servers[index].failTimeoutUnit"
class="!w-24"
>
<el-option
v-for="(unit, indexKey) in Units"
:key="indexKey"
:label="unit.label"
:value="unit.value"
/>
</el-select>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-form-item
:label="$t('website.maxConns')"
:prop="`servers.${index}.maxConns`"
:rules="rules.maxConns"
>
<el-input type="number" v-model.number="item.servers[index].maxConns"></el-input>
</el-form-item>
</el-col>
</el-row>
</div>
</el-card>
<el-button class="add-server-btn" type="primary" plain @click="addServer" icon="Plus">
{{ $t('commons.button.add') + $t('website.server') }}
</el-button>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(lbForm)" :disabled="loading">
<el-button @click="handleClose" :disabled="loading">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button type="primary" @click="submit" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@@ -154,29 +22,12 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { createLoadBalance, updateLoadBalance } from '@/api/modules/website';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { MsgError, MsgSuccess } from '@/utils/message';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { getAlgorithms, getStatusStrategy } from '@/global/mimetype';
import { Website } from '@/api/interface/website';
import { Monitor, QuestionFilled } from '@element-plus/icons-vue';
import { Units } from '@/global/mimetype';
const rules = ref<any>({
name: [Rules.appName],
algorithm: [Rules.requiredSelect],
server: [Rules.requiredInput],
weight: [checkNumberRange(0, 100)],
servers: {
type: Array,
},
maxFails: [checkNumberRange(1, 1000)],
maxConns: [checkNumberRange(0, 10000)],
failTimeout: [checkNumberRange(1, 300)],
});
import LoadBalanceForm from '@/views/website/website/config/basic/load-balance/form/index.vue';
interface LoadBalanceOperate {
websiteID: number;
@@ -184,62 +35,46 @@ interface LoadBalanceOperate {
upstream?: Website.NginxUpstream;
}
const lbForm = ref<FormInstance>();
const initServer = () => ({
server: '',
weight: undefined,
maxFails: undefined,
maxConns: undefined,
failTimeout: undefined,
failTimeoutUnit: 's',
flag: '',
});
const lbFormRef = ref();
const open = ref(false);
const loading = ref(false);
const item = ref({
websiteID: 0,
const operate = ref('create');
const websiteID = ref(0);
const disableName = ref(false);
const formData = ref({
name: '',
operate: 'create',
servers: [],
algorithm: 'default',
flag: '',
servers: [
{
server: '',
weight: undefined,
maxFails: undefined,
maxConns: undefined,
failTimeout: undefined,
failTimeoutUnit: 's',
flag: '',
},
],
});
const em = defineEmits(['close']);
const handleClose = () => {
lbForm.value?.resetFields();
lbFormRef.value?.resetFields();
open.value = false;
em('close', false);
};
const helper = ref();
const Algorithms = getAlgorithms();
const getHelper = (key: string) => {
Algorithms.forEach((algorithm) => {
if (algorithm.value === key) {
helper.value = algorithm.placeHolder;
}
});
return helper.value;
};
const addServer = () => {
item.value.servers.push(initServer());
};
const removeServer = (index: number) => {
item.value.servers.splice(index, 1);
};
const acceptParams = async (req: LoadBalanceOperate) => {
item.value.websiteID = req.websiteID;
item.value.operate = req.operate;
websiteID.value = req.websiteID;
operate.value = req.operate;
if (req.operate == 'edit') {
item.value.operate = 'edit';
item.value.name = req.upstream?.name || '';
item.value.algorithm = req.upstream?.algorithm || 'default';
disableName.value = true;
formData.value.name = req.upstream?.name || '';
formData.value.algorithm = req.upstream?.algorithm || 'default';
let servers = [];
req.upstream?.servers?.forEach((server) => {
const weight = server.weight == 0 ? undefined : server.weight;
@@ -257,16 +92,28 @@ const acceptParams = async (req: LoadBalanceOperate) => {
flag: server.flag,
});
});
item.value.servers = servers;
formData.value.servers = servers;
} else {
item.value.name = '';
item.value.servers = [initServer()];
disableName.value = false;
formData.value.name = '';
formData.value.algorithm = 'default';
formData.value.servers = [
{
server: '',
weight: undefined,
maxFails: undefined,
maxConns: undefined,
failTimeout: undefined,
failTimeoutUnit: 's',
flag: '',
},
];
}
open.value = true;
};
const handleServers = () => {
for (const server of item.value.servers) {
for (const server of formData.value.servers) {
if (!server.weight || server.weight == '') {
server.weight = 0;
}
@@ -283,7 +130,7 @@ const handleServers = () => {
};
const rollBackServers = () => {
for (const server of item.value.servers) {
for (const server of formData.value.servers) {
if (server.weight == 0) {
server.weight = undefined;
}
@@ -299,87 +146,44 @@ const rollBackServers = () => {
}
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate(async (valid) => {
if (!valid) {
return;
}
let checkBackup = false;
if (item.value.algorithm == 'ip_hash') {
checkBackup = true;
}
for (const server of item.value.servers) {
if (checkBackup && server.flag == 'backup') {
MsgError(i18n.global.t('website.ipHashBackupErr'));
return;
const submit = async () => {
try {
const valid = await lbFormRef.value?.validate();
if (!valid) return;
if (formData.value.algorithm == 'ip_hash') {
for (const server of formData.value.servers) {
if (server.flag == 'backup') {
MsgError(i18n.global.t('website.ipHashBackupErr'));
return;
}
}
}
handleServers();
loading.value = true;
try {
if (item.value.operate === 'edit') {
await updateLoadBalance(item.value);
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
} else {
await createLoadBalance(item.value);
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
}
handleClose();
} catch {
rollBackServers();
} finally {
loading.value = false;
const submitData = {
websiteID: websiteID.value,
...formData.value,
};
if (operate.value === 'edit') {
await updateLoadBalance(submitData);
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
} else {
await createLoadBalance(submitData);
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
}
});
handleClose();
} catch (error) {
rollBackServers();
} finally {
loading.value = false;
}
};
defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.server-card {
margin-bottom: 16px;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
border-color: var(--el-color-primary);
}
:deep(.el-card__header) {
padding: 12px 20px;
background-color: var(--el-fill-color-light);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 20px;
.server-title {
display: flex;
align-items: center;
font-weight: 500;
font-size: 14px;
color: var(--el-text-color-primary);
}
}
.add-server-btn {
width: 100%;
height: 48px;
border-style: dashed;
font-size: 14px;
margin-top: 8px;
&:hover {
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
}
</style>
@@ -0,0 +1,73 @@
<template>
<div>
<el-form ref="streamFormRef" :model="form" label-width="120px" label-position="top">
<el-row v-loading="loading">
<el-col :span="12" :offset="1">
<el-form-item :label="$t('website.streamPorts')" :rules="Rules.requiredInput">
<el-input v-model="form.streamPorts" />
</el-form-item>
<LoadBalanceForm ref="lbFormRef" v-model="form" :disabled="true" />
<el-form-item>
<el-button type="primary" @click="submit()" class="mt-2">
{{ $t('commons.button.save') }}
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script lang="ts" setup>
import LoadBalanceForm from '@/views/website/website/config/basic/load-balance/form/index.vue';
import { getWebsite, updateWebsiteStream } from '@/api/modules/website';
import { ref, onMounted } from 'vue';
import { Rules } from '@/global/form-rules';
import { MsgError, MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
const props = defineProps({
id: {
type: Number,
default: 0,
},
});
const form = ref({
streamPorts: '',
servers: [],
name: '',
algorithm: '',
websiteID: props.id,
});
const streamFormRef = ref();
const lbFormRef = ref();
const loading = ref(false);
const submit = async () => {
try {
loading.value = true;
const formValid = await streamFormRef.value?.validate();
if (!formValid) return;
const lbValid = await lbFormRef.value?.validate();
if (!lbValid) return;
await updateWebsiteStream(form.value);
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
} catch (error) {
if (error.message != '') {
MsgError(error.message);
}
} finally {
loading.value = false;
}
};
onMounted(async () => {
const res = await getWebsite(props.id);
form.value.streamPorts = res.data.streamPorts;
form.value.servers = res.data.servers;
form.value.name = res.data.primaryDomain;
form.value.algorithm = res.data.algorithm == '' ? 'default' : res.data.algorithm;
form.value.streamPorts = res.data.streamPorts;
});
</script>
@@ -30,7 +30,7 @@
</template>
<template #main>
<MainDiv :heightDiff="260">
<Basic :website="website" v-if="index === 'basic'" :heightDiff="320"></Basic>
<Basic :website="website" v-if="index === 'basic' && website.id" :heightDiff="320"></Basic>
<Log :id="id" v-if="index === 'log'"></Log>
<Resource :id="id" v-if="index === 'resource'"></Resource>
</MainDiv>
@@ -77,22 +77,18 @@ const changeTab = (index: string) => {
routerToNameWithParams('WebsiteConfig', { id: id.value, tab: index });
};
onMounted(() => {
onMounted(async () => {
index.value = props.tab;
id.value = Number(props.id);
loading.value = true;
getWebsite(id.value)
.then(async (res) => {
website.value = res.data;
if (res.data.type === 'runtime') {
const runRes = await GetRuntime(res.data.runtimeID);
if (runRes.data.type == 'php' && runRes.data.resource === 'appstore') {
configPHP.value = true;
}
}
})
.finally(() => {
loading.value = false;
});
const res = await getWebsite(id.value);
website.value = res.data;
if (res.data.type === 'runtime') {
const runRes = await GetRuntime(res.data.runtimeID);
if (runRes.data.type == 'php' && runRes.data.resource === 'appstore') {
configPHP.value = true;
}
}
loading.value = false;
});
</script>
@@ -185,7 +185,17 @@
{{ $t('website.runtimePortWarn') }}
</el-text>
</div>
<DomainCreate v-model:form="website" @gengerate="websiteForm.clearValidate()"></DomainCreate>
<div v-if="website.type === 'stream'">
<el-form-item :label="$t('website.streamPorts')" prop="streamPorts">
<el-input
v-model="website.streamPorts"
:placeholder="$t('website.streamPortsHelper')"
></el-input>
</el-form-item>
</div>
<div v-else>
<DomainCreate v-model:form="website" @gengerate="websiteForm.clearValidate()"></DomainCreate>
</div>
<el-form-item prop="IPV6">
<el-checkbox v-model="website.IPV6" :label="$t('website.ipv6')" size="large" />
</el-form-item>
@@ -323,76 +333,54 @@
</el-form-item>
</el-col>
</el-row>
<el-form-item prop="enableSSL">
<el-checkbox v-model="website.enableSSL" :label="$t('website.enableHTTPS')" size="large" />
<span class="input-help">{{ $t('website.enableSSLHelper') }}</span>
</el-form-item>
<div v-if="website.enableSSL">
<el-form-item :label="$t('website.acmeAccountManage')" prop="acmeAccountID">
<el-select
v-model="website.acmeAccountID"
:placeholder="$t('website.selectAcme')"
@change="listSSLs"
>
<el-option :key="0" :label="$t('website.imported')" :value="0"></el-option>
<el-option
v-for="(acme, index) in acmeAccounts"
:key="index"
:label="acme.email"
:value="acme.id"
<div v-if="website.type == 'stream'">
<LoadBalanceForm ref="lbFormRef" v-model="steamConfig" :disabled="true" />
</div>
<div v-else>
<el-form-item prop="enableSSL">
<el-checkbox v-model="website.enableSSL" :label="$t('website.enableHTTPS')" size="large" />
<span class="input-help">{{ $t('website.enableSSLHelper') }}</span>
</el-form-item>
<div v-if="website.enableSSL">
<el-form-item :label="$t('website.acmeAccountManage')" prop="acmeAccountID">
<el-select
v-model="website.acmeAccountID"
:placeholder="$t('website.selectAcme')"
@change="listSSLs"
>
<span>
{{ acme.email }}
<el-tag class="ml-5">{{ getAccountName(acme.type) }}</el-tag>
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('website.ssl')" prop="websiteSSLID" :hide-required-asterisk="true">
<el-select
v-model="website.websiteSSLID"
:placeholder="$t('website.selectSSL')"
@change="handleSSLSelectChange"
>
<el-option
v-for="(ssl, index) in ssls"
:key="index"
:label="ssl.primaryDomain"
:value="ssl.id"
:disabled="ssl.pem == ''"
></el-option>
</el-select>
</el-form-item>
<el-form-item :label="' '" v-if="websiteSSL && websiteSSL.id > 0">
<el-descriptions :column="7" border direction="vertical">
<el-descriptions-item :label="$t('website.primaryDomain')">
{{ websiteSSL.primaryDomain }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.otherDomains')">
{{ websiteSSL.domains }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.brand')">
{{ websiteSSL.organization }}
</el-descriptions-item>
<el-descriptions-item :label="$t('ssl.provider')">
{{ getProvider(websiteSSL.provider) }}
</el-descriptions-item>
<el-descriptions-item
:label="$t('ssl.acmeAccount')"
v-if="websiteSSL.acmeAccount && websiteSSL.provider !== 'manual'"
<el-option :key="0" :label="$t('website.imported')" :value="0"></el-option>
<el-option
v-for="(acme, index) in acmeAccounts"
:key="index"
:label="acme.email"
:value="acme.id"
>
<span>
{{ acme.email }}
<el-tag class="ml-5">{{ getAccountName(acme.type) }}</el-tag>
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('website.ssl')" prop="websiteSSLID" :hide-required-asterisk="true">
<el-select
v-model="website.websiteSSLID"
:placeholder="$t('website.selectSSL')"
@change="handleSSLSelectChange"
>
{{ websiteSSL.acmeAccount.email }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.expireDate')">
{{ dateFormatSimple(websiteSSL.expireDate) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('website.remark')">
{{ websiteSSL.description }}
</el-descriptions-item>
</el-descriptions>
</el-form-item>
<el-option
v-for="(ssl, index) in ssls"
:key="index"
:label="ssl.primaryDomain"
:value="ssl.id"
:disabled="ssl.pem == ''"
></el-option>
</el-select>
</el-form-item>
<el-form-item :label="' '" v-if="websiteSSL && websiteSSL.id > 0">
<WebsiteSSL :websiteSSL="websiteSSL" />
</el-form-item>
</div>
</div>
<el-form-item :label="$t('website.remark')" prop="remark">
<el-input type="textarea" :rows="3" clearable v-model="website.remark" />
@@ -422,6 +410,8 @@ import DomainCreate from '@/views/website/website/domain-create/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import Check from '../check/index.vue';
import GroupSelect from '@/views/website/website/components/group/index.vue';
import WebsiteSSL from '@/views/website/website/components/website-ssl/index.vue';
import LoadBalanceForm from '@/views/website/website/config/basic/load-balance/form/index.vue';
import { App } from '@/api/interface/app';
import { searchApp, getAppInstalled } from '@/api/modules/app';
@@ -443,7 +433,7 @@ import { Runtime } from '@/api/interface/runtime';
import { getRandomStr, getRuntimeLabel } from '@/utils/util';
import { getAppService } from '@/api/modules/app';
import { v4 as uuidv4 } from 'uuid';
import { dateFormatSimple, getProvider, getAccountName } from '@/utils/util';
import { getAccountName } from '@/utils/util';
import { Website } from '@/api/interface/website';
import { getPathByType } from '@/api/modules/files';
import { getWebsiteTypes } from '@/global/mimetype';
@@ -509,6 +499,11 @@ const initData = () => ({
domains: [],
parentWebsiteID: undefined,
siteDir: '',
streamPorts: '',
name: '',
algorithm: '',
servers: [],
});
const website = ref(initData());
const rules = ref<any>({
@@ -540,6 +535,7 @@ const rules = ref<any>({
websiteSSLID: [Rules.requiredSelect],
parentWebsiteID: [Rules.requiredSelect],
siteDir: [Rules.requiredSelect],
streamPorts: [Rules.requiredInput],
});
const open = ref(false);
@@ -575,6 +571,22 @@ const dirs = ref([]);
const runtimePorts = ref([]);
const WebsiteTypes = getWebsiteTypes();
const installFormRef = ref();
const lbFormRef = ref();
const steamConfig = ref({
name: '',
algorithm: 'default',
servers: [
{
server: '',
weight: undefined,
maxFails: undefined,
maxConns: undefined,
failTimeout: undefined,
failTimeoutUnit: 's',
flag: '',
},
],
});
const handleClose = () => {
open.value = false;
@@ -935,6 +947,10 @@ const submit = async (formEl: FormInstance | undefined) => {
MsgError(i18n.global.t('website.runtimePortWarn'));
return;
}
if (website.value.type == 'stream') {
const isValid = await lbFormRef.value?.validate();
if (!isValid) return;
}
loading.value = true;
try {
const res = await preCheck({});
@@ -948,6 +964,11 @@ const submit = async (formEl: FormInstance | undefined) => {
website.value.ftpUser = '';
website.value.ftpPassword = '';
}
if (website.value.type == 'stream') {
website.value.name = steamConfig.value.name;
website.value.algorithm = steamConfig.value.algorithm;
website.value.servers = steamConfig.value.servers;
}
const taskID = uuidv4();
website.value.taskID = taskID;
await createWebsite(website.value);
@@ -993,6 +1014,16 @@ watch(
},
);
watch(
() => website.value.alias,
(value) => {
if (website.value.type === 'stream') {
steamConfig.value.name = value;
}
},
{ deep: true },
);
const changeAlias = (value: string) => {
const domain = value.split(':')[0];
website.value.alias = domain;
@@ -13,7 +13,13 @@
<el-text v-else type="primary" class="cursor-pointer" @click="openConfig(row.id)">
{{ row.primaryDomain }}
</el-text>
<el-popover placement="right" trigger="hover" :width="300" @before-enter="searchDomains(row.id)">
<el-popover
placement="right"
trigger="hover"
:width="300"
@before-enter="searchDomains(row.id)"
v-if="row.type != 'stream'"
>
<template #reference>
<el-button link icon="Promotion" class="ml-2.5"></el-button>
</template>
@@ -32,7 +38,7 @@
</tbody>
</table>
</el-popover>
<el-button link icon="edit" @click="startEdit" v-if="!isEditing"></el-button>
<el-button link icon="edit" class="ml-2.5" @click="startEdit" v-if="!isEditing"></el-button>
</div>
<div>
<el-tooltip effect="dark" :content="$t('website.cancelFavorite')" placement="top-start" v-if="row.favorite">
+10 -9
View File
@@ -128,14 +128,9 @@
v-if="row.status === 'Running'"
:operate="true"
:status="row.status"
@click="operateWebsite('stop', row.id)"
/>
<Status
v-else
:status="row.status"
:operate="true"
@click="operateWebsite('start', row.id)"
@click="operateWebsite('stop', row)"
/>
<Status v-else :status="row.status" :operate="true" @click="operateWebsite('start', row)" />
</template>
</el-table-column>
@@ -466,6 +461,9 @@ const refreshData = () => {
};
const openDatePicker = (row: any) => {
if (row.type === 'stream') {
return;
}
refreshData();
row.showdate = true;
};
@@ -590,12 +588,15 @@ const checkDate = (date: Date) => {
return date.getTime() < now.getTime();
};
const operateWebsite = (op: string, id: number) => {
const operateWebsite = (op: string, row: Website.Website) => {
if (row.type === 'stream') {
return;
}
ElMessageBox.confirm(i18n.global.t('website.' + op + 'Helper'), i18n.global.t('cronjob.changeStatus'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
await opWebsite({ id: id, operate: op });
await opWebsite({ id: row.id, operate: op });
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
});