diff --git a/agent/app/dto/request/website.go b/agent/app/dto/request/website.go index 209e71a6b..a0b6721db 100644 --- a/agent/app/dto/request/website.go +++ b/agent/app/dto/request/website.go @@ -162,6 +162,7 @@ type WebsiteHTTPSOp struct { Algorithm string `json:"algorithm"` Hsts bool `json:"hsts"` HttpsPorts []int `json:"httpsPorts"` + Http3 bool `json:"http3"` } type WebsiteNginxUpdate struct { diff --git a/agent/app/dto/response/website.go b/agent/app/dto/response/website.go index 67da78b50..bb0d347c9 100644 --- a/agent/app/dto/response/website.go +++ b/agent/app/dto/response/website.go @@ -64,6 +64,7 @@ type WebsiteHTTPS struct { Hsts bool `json:"hsts"` HttpsPorts []int `json:"httpsPorts"` HttpsPort string `json:"httpsPort"` + Http3 bool `json:"http3"` } type WebsiteLog struct { diff --git a/agent/app/service/website.go b/agent/app/service/website.go index 39fc4e9e7..08418bb31 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -620,28 +620,24 @@ func (w WebsiteService) UpdateWebsiteDomain(req request.WebsiteDomainUpdate) err if err != nil { return err } - if website.Protocol == constant.ProtocolHTTPS { - nginxFull, err := getNginxFull(&website) - if err != nil { - return nil - } - nginxConfig := nginxFull.SiteConfig - config := nginxFull.SiteConfig.Config - server := config.FindServers()[0] - var params []string - if domain.SSL { - params = append(params, "ssl", "http2") - } - server.UpdateListen(strconv.Itoa(domain.Port), false, params...) - if website.IPV6 { - server.UpdateListen("[::]:"+strconv.Itoa(domain.Port), false, params...) - } - if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { - return err - } - if err = nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxFull.Install.ContainerName); err != nil { - return err - } + nginxFull, err := getNginxFull(&website) + if err != nil { + return nil + } + nginxConfig := nginxFull.SiteConfig + config := nginxFull.SiteConfig.Config + server := config.FindServers()[0] + server.DeleteListen(strconv.Itoa(domain.Port)) + if website.IPV6 { + server.DeleteListen("[::]:" + strconv.Itoa(domain.Port)) + } + http3 := isHttp3(server) + setListen(server, strconv.Itoa(domain.Port), website.IPV6, http3, website.DefaultServer, domain.SSL && website.Protocol == constant.ProtocolHTTPS) + if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return err + } + if err = nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxFull.Install.ContainerName); err != nil { + return err } return websiteDomainRepo.Save(context.TODO(), &domain) } @@ -921,7 +917,7 @@ func (w WebsiteService) GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS, } else { res.HttpConfig = constant.HTTPToHTTPS } - params, err := getNginxParamsByKeys(constant.NginxScopeServer, []string{"ssl_protocols", "ssl_ciphers", "add_header"}, &website) + params, err := getNginxParamsByKeys(constant.NginxScopeServer, []string{"ssl_protocols", "ssl_ciphers", "add_header", "listen"}, &website) if err != nil { return res, err } @@ -932,8 +928,13 @@ func (w WebsiteService) GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS, if p.Name == "ssl_ciphers" { res.Algorithm = p.Params[0] } - if p.Name == "add_header" && len(p.Params) > 0 && p.Params[0] == "Strict-Transport-Security" { - res.Hsts = true + if p.Name == "add_header" && len(p.Params) > 0 { + if p.Params[0] == "Strict-Transport-Security" { + res.Hsts = true + } + if p.Params[0] == "Alt-Svc" { + res.Http3 = true + } } } return res, nil @@ -998,6 +999,9 @@ func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteH dto.NginxParam{ Name: "ssl_ciphers", }, + dto.NginxParam{ + Name: "http2", + }, ) if err = deleteNginxConfig(constant.NginxScopeServer, nginxParams, &website); err != nil { return nil, err diff --git a/agent/app/service/website_utils.go b/agent/app/service/website_utils.go index 063db15dd..189edcfec 100644 --- a/agent/app/service/website_utils.go +++ b/agent/app/service/website_utils.go @@ -198,10 +198,7 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a var serverNames []string for _, domain := range domains { serverNames = append(serverNames, domain.Domain) - server.UpdateListen(strconv.Itoa(domain.Port), false) - if website.IPV6 { - server.UpdateListen("[::]:"+strconv.Itoa(domain.Port), false) - } + setListen(server, strconv.Itoa(domain.Port), website.IPV6, false, website.DefaultServer, false) } server.UpdateServerName(serverNames) @@ -463,6 +460,17 @@ func delWafConfig(website model.Website, force bool) error { return nil } +func isHttp3(server *components.Server) bool { + for _, listen := range server.Listens { + for _, param := range listen.Parameters { + if param == "quic" { + return true + } + } + } + return false +} + func addListenAndServerName(website model.Website, domains []model.WebsiteDomain) error { nginxFull, err := getNginxFull(&website) if err != nil { @@ -471,16 +479,10 @@ func addListenAndServerName(website model.Website, domains []model.WebsiteDomain nginxConfig := nginxFull.SiteConfig config := nginxFull.SiteConfig.Config server := config.FindServers()[0] + http3 := isHttp3(server) for _, domain := range domains { - var params []string - if website.Protocol == constant.ProtocolHTTPS && domain.SSL { - params = append(params, "ssl", "http2") - } - server.UpdateListen(strconv.Itoa(domain.Port), false, params...) - if website.IPV6 { - server.UpdateListen("[::]:"+strconv.Itoa(domain.Port), false, params...) - } + setListen(server, strconv.Itoa(domain.Port), website.IPV6, http3, website.DefaultServer, website.Protocol == constant.ProtocolHTTPS && domain.SSL) server.UpdateServerName([]string{domain.Domain}) } @@ -512,6 +514,24 @@ func deleteListenAndServerName(website model.Website, binds []string, domains [] return nginxCheckAndReload(nginxConfig.OldContent, nginxConfig.FilePath, nginxFull.Install.ContainerName) } +func setListen(server *components.Server, port string, ipv6, http3, defaultServer, ssl bool) { + var params []string + if ssl { + params = []string{"ssl"} + } + server.UpdateListen(port, defaultServer, params...) + if ssl && http3 { + server.UpdateListen(port, defaultServer, "quic") + } + if !ipv6 { + return + } + server.UpdateListen("[::]:"+port, defaultServer, params...) + if ssl && http3 { + server.UpdateListen("[::]:"+port, defaultServer, "quic") + } +} + func removeSSLListen(website model.Website, binds []string) error { nginxFull, err := getNginxFull(&website) if err != nil { @@ -520,11 +540,13 @@ func removeSSLListen(website model.Website, binds []string) error { nginxConfig := nginxFull.SiteConfig config := nginxFull.SiteConfig.Config server := config.FindServers()[0] + http3 := isHttp3(server) for _, bind := range binds { - server.UpdateListen(bind, false) + server.DeleteListen(bind) if website.IPV6 { - server.UpdateListen("[::]:"+bind, false) + server.DeleteListen("[::]:" + bind) } + setListen(server, bind, website.IPV6, http3, website.DefaultServer, website.Protocol == constant.ProtocolHTTPS) } if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { return err @@ -609,13 +631,11 @@ func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.W httpPortIPV6 := "[::]:" + httpPort for _, port := range httpsPort { - httpsPortIPV6 := "[::]:" + strconv.Itoa(port) - server.UpdateListen(strconv.Itoa(port), website.DefaultServer, "ssl", "http2") - if website.IPV6 { - server.UpdateListen(httpsPortIPV6, website.DefaultServer, "ssl", "http2") - } + setListen(server, strconv.Itoa(port), website.IPV6, req.Http3, website.DefaultServer, true) } + server.UpdateDirective("http2", []string{"on"}) + switch req.HttpConfig { case constant.HTTPSOnly: server.RemoveListenByBind(httpPort) @@ -642,11 +662,21 @@ func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.W if !req.Hsts { server.RemoveDirective("add_header", []string{"Strict-Transport-Security", "\"max-age=31536000\""}) } + if !req.Http3 { + for _, port := range httpsPort { + server.RemoveListen(strconv.Itoa(port), "quic") + if website.IPV6 { + httpsPortIPV6 := "[::]:" + strconv.Itoa(port) + server.RemoveListen(httpsPortIPV6, "quic") + } + } + server.RemoveDirective("add_header", []string{"Alt-Svc"}) + } - if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { return err } - if err := createPemFile(*website, websiteSSL); err != nil { + if err = createPemFile(*website, websiteSSL); err != nil { return err } nginxParams := getNginxParamsFromStaticFile(dto.SSL, []dto.NginxParam{}) @@ -670,6 +700,12 @@ func applySSL(website *model.Website, websiteSSL model.WebsiteSSL, req request.W Params: []string{"Strict-Transport-Security", "\"max-age=31536000\""}, }) } + if req.Http3 { + nginxParams = append(nginxParams, dto.NginxParam{ + Name: "add_header", + Params: []string{"Alt-Svc", "'h3=\":443\"; ma=2592000'"}, + }) + } if err := updateNginxConfig(constant.NginxScopeServer, nginxParams, website); err != nil { return err diff --git a/agent/utils/nginx/components/server.go b/agent/utils/nginx/components/server.go index c7138936f..a5b0446e8 100644 --- a/agent/utils/nginx/components/server.go +++ b/agent/utils/nginx/components/server.go @@ -174,6 +174,19 @@ func (s *Server) AddListen(bind string, defaultServer bool, params ...string) { s.Listens = append(s.Listens, listen) } +func isSameArray(arr1, arr2 []string) bool { + set1 := make(map[string]struct{}) + for _, v := range arr1 { + set1[v] = struct{}{} + } + for _, v := range arr2 { + if _, exists := set1[v]; !exists { + return false + } + } + return true +} + func (s *Server) UpdateListen(bind string, defaultServer bool, params ...string) { listen := &ServerListen{ Bind: bind, @@ -185,7 +198,7 @@ func (s *Server) UpdateListen(bind string, defaultServer bool, params ...string) var newListens []*ServerListen exist := false for _, li := range s.Listens { - if li.Bind == bind { + if li.Bind == bind && isSameArray(li.Parameters, params) { exist = true newListens = append(newListens, listen) } else { @@ -209,6 +222,17 @@ func (s *Server) DeleteListen(bind string) { s.Listens = newListens } +func (s *Server) RemoveListen(bind string, params ...string) { + var newListens []*ServerListen + for _, li := range s.Listens { + if li.Bind == bind && isSameArray(li.Parameters, params) { + continue + } + newListens = append(newListens, li) + } + s.Listens = newListens +} + func (s *Server) DeleteServerName(name string) { var names []string dirs := s.FindDirectives("server_name") diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index def0fb33e..8fbeab6e9 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -296,6 +296,7 @@ export namespace Website { httpConfig: string; SSLProtocol: string[]; algorithm: string; + http3: boolean; } export interface HTTPSConfig { @@ -306,6 +307,7 @@ export namespace Website { algorithm: string; hsts: boolean; httpsPort?: string; + http3: boolean; } export interface CheckReq { diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 2fa36659c..9e82b12a3 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -2202,6 +2202,8 @@ const message = { ipFromExample2: "If the frontend is a CDN, you can enter the CDN's IP address range", ipFromExample3: 'If unsure, you can enter 0.0.0.0/0 (ipv4) ::/0 (ipv6) [Note: Allowing any source IP is not secure]', + http3Helper: + 'HTTP/3 is an upgrade to HTTP/2, offering faster connection speeds and better performance, but not all browsers support HTTP/3. Enabling it may cause some browsers to be unable to access the site.', }, php: { short_open_tag: 'Short tag support', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index 675edf099..62d65bf34 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -2049,6 +2049,8 @@ const message = { ipFromExample1: '如果前端是 Frp 等工具,可以填寫 Frp 的 IP 地址,類似 127.0.0.1', ipFromExample2: '如果前端是 CDN,可以填寫 CDN 的 IP 地址段', ipFromExample3: '如果不確定,可以填 0.0.0.0/0(ipv4) ::/0(ipv6) [注意:允許任意來源 IP 不安全]', + http3Helper: + 'HTTP/3 是 HTTP/2 的升級版本,提供更快的連線速度和更好的性能,但並非所有瀏覽器都支援 HTTP/3,啟用後可能會導致部分瀏覽器無法訪問', }, php: { short_open_tag: '短標簽支持', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 4e6ad4028..f19f53538 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -2050,6 +2050,8 @@ const message = { ipFromExample1: '如果前端是 Frp 等工具,可以填写 Frp 的 IP 地址,类似 127.0.0.1', ipFromExample2: '如果前端是 CDN,可以填写 CDN 的 IP 地址段', ipFromExample3: '如果不确定,可以填 0.0.0.0/0(ipv4) ::/0(ipv6) [注意:允许任意来源 IP 不安全]', + http3Helper: + 'HTTP/3 是 HTTP/2 的升级版本,提供更快的连接速度和更好的性能,但是不是所有浏览器都支持 HTTP/3,开启后可能会导致部分浏览器无法访问', }, php: { short_open_tag: '短标签支持', diff --git a/frontend/src/views/website/website/config/basic/https/index.vue b/frontend/src/views/website/website/config/basic/https/index.vue index c0c547d8e..81d8de2dc 100644 --- a/frontend/src/views/website/website/config/basic/https/index.vue +++ b/frontend/src/views/website/website/config/basic/https/index.vue @@ -1,6 +1,6 @@