diff --git a/agent/app/service/website.go b/agent/app/service/website.go index 5b88b2fd63fe..767fc2f5a50a 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -265,6 +265,19 @@ func (w WebsiteService) GetWebsites() ([]response.WebsiteDTO, error) { return websiteDTOs, nil } +func newWebsiteCreateHTTPSOp(sslID uint) request.WebsiteHTTPSOp { + return request.WebsiteHTTPSOp{ + Enable: true, + WebsiteSSLID: sslID, + Type: constant.SSLExisted, + HttpConfig: constant.HTTPToHTTPS, + SSLProtocol: []string{"TLSv1.3", "TLSv1.2"}, + Algorithm: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED", + Hsts: true, + HstsIncludeSubDomains: true, + } +} + func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) { alias := create.Alias if alias == "default" { @@ -313,6 +326,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) appInstall *model.AppInstall runtime *model.Runtime primaryDomain string + initialSSL *websiteInitialSSL ) if website.Type == constant.Stream { if create.StreamConfig.StreamPorts == "" { @@ -339,6 +353,29 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) } website.PrimaryDomain = primaryDomain website.Protocol = constant.ProtocolHTTP + for _, domain := range domains { + if domain.SSL { + create.EnableSSL = true + break + } + } + if create.EnableSSL { + if create.WebsiteSSLID == 0 { + return buserr.New("ErrSSLValid") + } + websiteSSL, sslErr := websiteSSLRepo.GetFirst(repo.WithByID(create.WebsiteSSLID)) + if sslErr != nil { + return sslErr + } + if websiteSSL.Pem == "" || websiteSSL.PrivateKey == "" { + return buserr.New("ErrSSLValid") + } + sslReq := newWebsiteCreateHTTPSOp(websiteSSL.ID) + website.Protocol = constant.ProtocolHTTPS + website.WebsiteSSLID = websiteSSL.ID + website.HttpConfig = sslReq.HttpConfig + initialSSL = &websiteInitialSSL{certificate: *websiteSSL, request: sslReq} + } } createTask, err := task.NewTaskWithOps(website.PrimaryDomain, task.TaskCreate, task.TaskScopeWebsite, create.TaskID, 0) @@ -490,7 +527,7 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) } configNginx := func(t *task.Task) error { - if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig); err != nil { + if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig, initialSSL); err != nil { return err } if create.Type == constant.Static && create.TemplateOutputID > 0 { @@ -548,36 +585,6 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error) createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite) - if create.EnableSSL { - enableSSL := func(t *task.Task) error { - websiteModel, err := websiteSSLRepo.GetFirst(repo.WithByID(create.WebsiteSSLID)) - if err != nil { - return err - } - website.Protocol = constant.ProtocolHTTPS - website.WebsiteSSLID = create.WebsiteSSLID - appSSLReq := request.WebsiteHTTPSOp{ - WebsiteID: website.ID, - Enable: true, - WebsiteSSLID: websiteModel.ID, - Type: "existed", - HttpConfig: "HTTPToHTTPS", - SSLProtocol: []string{"TLSv1.3", "TLSv1.2"}, - Algorithm: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED", - Hsts: true, - HstsIncludeSubDomains: true, - } - if err = applySSL(website, *websiteModel, appSSLReq); err != nil { - return err - } - if err = websiteRepo.Save(context.Background(), website); err != nil { - return err - } - return nil - } - createTask.AddSubTaskWithIgnoreErr(i18n.GetMsgByKey("EnableSSL"), enableSSL) - } - if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 { createFtpUser := func(t *task.Task) error { indexDir := GetSitePath(*website, SiteIndexDir) diff --git a/agent/app/service/website_utils.go b/agent/app/service/website_utils.go index 9d86b4f143ff..bfa88d7f6068 100644 --- a/agent/app/service/website_utils.go +++ b/agent/app/service/website_utils.go @@ -11,6 +11,7 @@ import ( "os" "path" "path/filepath" + "sort" "strconv" "strings" "syscall" @@ -255,7 +256,12 @@ 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, streamConfig request.StreamConfig) error { +type websiteInitialSSL struct { + certificate model.WebsiteSSL + request request.WebsiteHTTPSOp +} + +func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig, initialSSL *websiteInitialSSL) error { nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) if err != nil { return err @@ -325,6 +331,13 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a setListen(server, strconv.Itoa(domain.Port), website.IPV6, false, website.DefaultServer, false) } server.UpdateServerName(serverNames) + if initialSSL != nil { + plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort) + applyWebsiteSSLConfig(server, *website, plan, initialSSL.request) + if err = createPemFile(*website, initialSSL.certificate); err != nil { + return err + } + } siteFolder := path.Join("/www", "sites", website.Alias) server.UpdateDirective("access_log", []string{path.Join(siteFolder, "log", "access.log"), "main"}) @@ -789,102 +802,140 @@ func createPemFile(website model.Website, websiteSSL model.WebsiteSSL) error { return nil } -func getHttpsPort(websiteID uint) map[int]struct{} { - domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteID)) - if err != nil { - return nil - } +type websiteTLSPlan struct { + httpPorts []int + httpsPorts []int + redirectPort int + defaultHTTPPort int + hasDefaultHTTP bool +} + +func buildWebsiteTLSPlan(domains []model.WebsiteDomain, defaultHTTPPort, defaultHTTPSPort int) websiteTLSPlan { + httpPorts := make(map[int]struct{}) httpsPorts := make(map[int]struct{}) - nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty) - hasDefaultPort := false + plan := websiteTLSPlan{defaultHTTPPort: defaultHTTPPort} + for _, domain := range domains { - if domain.Port == nginxInstall.HttpPort { - hasDefaultPort = true + if domain.Port == defaultHTTPPort { + plan.hasDefaultHTTP = true } if domain.SSL { httpsPorts[domain.Port] = struct{}{} + } else { + httpPorts[domain.Port] = struct{}{} } } - if hasDefaultPort { - httpsPorts[nginxInstall.HttpsPort] = struct{}{} + if plan.hasDefaultHTTP { + httpsPorts[defaultHTTPSPort] = struct{}{} } if len(httpsPorts) == 0 { - for _, domain := range domains { - if !domain.SSL { - httpsPorts[domain.Port] = struct{}{} - } + for port := range httpPorts { + httpsPorts[port] = struct{}{} } } - return httpsPorts + for port := range httpsPorts { + delete(httpPorts, port) + } + for port := range httpPorts { + plan.httpPorts = append(plan.httpPorts, port) + } + for port := range httpsPorts { + plan.httpsPorts = append(plan.httpsPorts, port) + } + sort.Ints(plan.httpPorts) + sort.Ints(plan.httpsPorts) + if plan.hasDefaultHTTP { + plan.redirectPort = defaultHTTPSPort + } else if len(plan.httpsPorts) > 0 { + plan.redirectPort = plan.httpsPorts[0] + } + return plan } -func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.WebsiteHTTPSOp) error { - nginxFull, err := getNginxFull(website) +func getHttpsPort(websiteID uint) map[int]struct{} { + domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(websiteID)) if err != nil { return nil } - domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID)) - if err != nil { - return nil + nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty) + plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort) + httpsPorts := make(map[int]struct{}, len(plan.httpsPorts)) + for _, port := range plan.httpsPorts { + httpsPorts[port] = struct{}{} } - httpPorts := make(map[int]struct{}) - httpsPorts := make(map[int]struct{}) - sslPort := 0 + return httpsPorts +} - hasDefaultPort := false - for _, domain := range domains { - if domain.Port == nginxFull.Install.HttpPort { - hasDefaultPort = true - } - if domain.SSL { - httpsPorts[domain.Port] = struct{}{} - } else { - httpPorts[domain.Port] = struct{}{} +func buildWebsiteSSLParams(alias string, req request.WebsiteHTTPSOp, redirectPort int) []dto.NginxParam { + nginxParams := getNginxParamsFromStaticFile(dto.SSL, []dto.NginxParam{}) + for i := range nginxParams { + switch nginxParams[i].Name { + case "ssl_certificate": + nginxParams[i].Params = []string{path.Join("/www", "sites", alias, "ssl", "fullchain.pem")} + case "ssl_certificate_key": + nginxParams[i].Params = []string{path.Join("/www", "sites", alias, "ssl", "privkey.pem")} + case "ssl_protocols": + nginxParams[i].Params = req.SSLProtocol + if len(req.SSLProtocol) == 0 { + nginxParams[i].Params = []string{"TLSv1.3", "TLSv1.2"} + } + case "ssl_ciphers": + nginxParams[i].Params = []string{req.Algorithm} + if len(req.Algorithm) == 0 { + nginxParams[i].Params = []string{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED"} + } + case "error_page": + if len(nginxParams[i].Params) >= 2 && nginxParams[i].Params[0] == "497" && redirectPort != 443 && nginxParams[i].Params[1] == "https://$host$request_uri" { + nginxParams[i].Params[1] = fmt.Sprintf("https://$host:%d$request_uri", redirectPort) + } } } - if hasDefaultPort { - httpsPorts[nginxFull.Install.HttpsPort] = struct{}{} - } - if len(httpsPorts) == 0 { - for port := range httpPorts { - httpsPorts[port] = struct{}{} + if req.Hsts { + hstsValue := "\"max-age=31536000\"" + if req.HstsIncludeSubDomains { + hstsValue = "\"max-age=31536000; includeSubDomains\"" } + nginxParams = append(nginxParams, dto.NginxParam{ + Name: "add_header", + Params: []string{"Strict-Transport-Security", hstsValue}, + }) } - config := nginxFull.SiteConfig.Config - server := config.FindServers()[0] - - defaultHttpPort := strconv.Itoa(nginxFull.Install.HttpPort) - defaultHttpPortIPV6 := "[::]:" + defaultHttpPort + if req.Http3 { + nginxParams = append(nginxParams, dto.NginxParam{ + Name: "add_header", + Params: []string{"Alt-Svc", "'h3=\":443\"; ma=2592000'"}, + }) + } + return nginxParams +} - for port := range httpsPorts { - sslPort = port +func applyWebsiteSSLConfig(server *components.Server, website model.Website, plan websiteTLSPlan, req request.WebsiteHTTPSOp) { + for _, port := range plan.httpsPorts { portStr := strconv.Itoa(port) server.RemoveListenByBind(portStr) server.RemoveListenByBind("[::]:" + portStr) setListen(server, portStr, website.IPV6, req.Http3, website.DefaultServer, true) } - server.UpdateDirective("http2", []string{"on"}) + defaultHTTPPort := strconv.Itoa(plan.defaultHTTPPort) switch req.HttpConfig { case constant.HTTPSOnly: - server.RemoveListenByBind(defaultHttpPort) - server.RemoveListenByBind(defaultHttpPortIPV6) + if plan.hasDefaultHTTP { + server.RemoveListenByBind(defaultHTTPPort) + server.RemoveListenByBind("[::]:" + defaultHTTPPort) + } server.RemoveDirective("if", []string{"($scheme"}) case constant.HTTPToHTTPS: - if hasDefaultPort { - server.UpdateListen(defaultHttpPort, website.DefaultServer) - if website.IPV6 { - server.UpdateListen(defaultHttpPortIPV6, website.DefaultServer) - } + if plan.hasDefaultHTTP { + setListen(server, defaultHTTPPort, website.IPV6, false, website.DefaultServer, false) + } + if plan.redirectPort > 0 { + server.AddHTTP2HTTPS(plan.redirectPort) } - server.AddHTTP2HTTPS(sslPort) case constant.HTTPAlso: - if hasDefaultPort { - server.UpdateListen(defaultHttpPort, website.DefaultServer) - if website.IPV6 { - server.UpdateListen(defaultHttpPortIPV6, website.DefaultServer) - } + if plan.hasDefaultHTTP { + setListen(server, defaultHTTPPort, website.IPV6, false, website.DefaultServer, false) } server.RemoveDirective("if", []string{"($scheme"}) } @@ -894,77 +945,41 @@ func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.W server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000; includeSubDomains\""}) } if !req.Http3 { - for port := range httpsPorts { + for _, port := range plan.httpsPorts { server.RemoveListen(strconv.Itoa(port), "quic") if website.IPV6 { - httpsPortIPV6 := "[::]:" + strconv.Itoa(port) - server.RemoveListen(httpsPortIPV6, "quic") + server.RemoveListen("[::]:"+strconv.Itoa(port), "quic") } } server.RemoveDirective("add_header", []string{"Alt-Svc"}) } - if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return err - } - if err = createPemFile(*website, websiteSSL); err != nil { - return err - } - nginxParams := getNginxParamsFromStaticFile(dto.SSL, []dto.NginxParam{}) - for i, param := range nginxParams { - if param.Name == "ssl_certificate" { - nginxParams[i].Params = []string{path.Join("/www", "sites", website.Alias, "ssl", "fullchain.pem")} - } - if param.Name == "ssl_certificate_key" { - nginxParams[i].Params = []string{path.Join("/www", "sites", website.Alias, "ssl", "privkey.pem")} - } - if param.Name == "ssl_protocols" { - nginxParams[i].Params = req.SSLProtocol - if len(req.SSLProtocol) == 0 { - nginxParams[i].Params = []string{"TLSv1.3", "TLSv1.2"} - } - } - if param.Name == "ssl_ciphers" { - nginxParams[i].Params = []string{req.Algorithm} - if len(req.Algorithm) == 0 { - nginxParams[i].Params = []string{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED"} - } - } - if param.Name == "error_page" { - if len(param.Params) < 2 { - continue - } - code := param.Params[0] - if code == "497" { - if sslPort != 443 && param.Params[1] == "https://$host$request_uri" { - param.Params[1] = fmt.Sprintf("https://$host:%d$request_uri", sslPort) - } - } - } + for _, param := range buildWebsiteSSLParams(website.Alias, req, plan.redirectPort) { + server.UpdateDirective(param.Name, param.Params) } - if req.Hsts { - var hstsValue string - if req.HstsIncludeSubDomains { - hstsValue = "\"max-age=31536000; includeSubDomains\"" - } else { - hstsValue = "\"max-age=31536000\"" - } - nginxParams = append(nginxParams, dto.NginxParam{ - Name: "add_header", - Params: []string{"Strict-Transport-Security", hstsValue}, - }) +} + +func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.WebsiteHTTPSOp) error { + nginxFull, err := getNginxFull(website) + if err != nil { + return nil } - if req.Http3 { - nginxParams = append(nginxParams, dto.NginxParam{ - Name: "add_header", - Params: []string{"Alt-Svc", "'h3=\":443\"; ma=2592000'"}, - }) + domains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID)) + if err != nil { + return nil } + config := nginxFull.SiteConfig.Config + server := config.FindServers()[0] + plan := buildWebsiteTLSPlan(domains, nginxFull.Install.HttpPort, nginxFull.Install.HttpsPort) + applyWebsiteSSLConfig(server, *website, plan, req) - if err := updateNginxConfig(constant.NginxScopeServer, nginxParams, website); err != nil { + if err = createPemFile(*website, websiteSSL); err != nil { return err } - return nil + if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return err + } + return nginxCheckAndReload(nginxFull.SiteConfig.OldContent, nginxFull.SiteConfig.FilePath, nginxFull.Install.ContainerName) } func getParamArray(key string, param interface{}) []string { diff --git a/frontend/src/views/website/website/create/index.vue b/frontend/src/views/website/website/create/index.vue index 58fac95aa3c6..ba6e9fcac59d 100644 --- a/frontend/src/views/website/website/create/index.vue +++ b/frontend/src/views/website/website/create/index.vue @@ -1073,6 +1073,9 @@ const submit = async (formEl: FormInstance | undefined) => { watch( () => website.value.domains, () => { + if (website.value.domains.some((domain: any) => domain.ssl) && !website.value.enableSSL) { + website.value.enableSSL = true; + } tryAutoSelectSSL(); }, { deep: true }, @@ -1089,6 +1092,9 @@ watch( () => website.value.enableSSL, (enabled) => { if (!enabled) { + website.value.domains.forEach((domain: any) => { + domain.ssl = false; + }); applySSLSelection(undefined); userSelectedSSL.value = false; return;