From e2403c9869ec2279338511747bacc126416cedc3 Mon Sep 17 00:00:00 2001
From: zhengkunwang223 <zhengkun@fit2cloud.com>
Date: Wed, 28 Dec 2022 16:07:43 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20HTTPS=E5=A2=9E=E5=8A=A0=E9=85=8D?=
 =?UTF-8?q?=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/nginx/versions/1.21.4/config.json        |  2 +-
 backend/app/api/v1/website.go                 |  5 +-
 backend/app/dto/request/website.go            |  1 +
 backend/app/dto/response/website.go           |  5 +-
 backend/app/model/website.go                  |  1 +
 backend/app/service/website.go                | 92 +++++++++----------
 backend/app/service/website_utils.go          | 17 +++-
 backend/constant/website.go                   |  4 +
 backend/utils/nginx/components/server.go      | 21 ++++-
 frontend/src/api/interface/website.ts         |  2 +
 frontend/src/lang/modules/zh.ts               | 10 ++
 .../website/config/basic/https/index.vue      | 47 +++++++---
 12 files changed, 132 insertions(+), 75 deletions(-)

diff --git a/apps/nginx/versions/1.21.4/config.json b/apps/nginx/versions/1.21.4/config.json
index 0b69ab743..739165b20 100644
--- a/apps/nginx/versions/1.21.4/config.json
+++ b/apps/nginx/versions/1.21.4/config.json
@@ -7,7 +7,7 @@
       "required": true,
       "default": 80,
       "envKey": "PANEL_APP_PORT_HTTP",
-      "disabled": true,
+      "disabled": true
     },
     {
       "type": "number",
diff --git a/backend/app/api/v1/website.go b/backend/app/api/v1/website.go
index b7ff96bfe..771d493b9 100644
--- a/backend/app/api/v1/website.go
+++ b/backend/app/api/v1/website.go
@@ -256,11 +256,14 @@ func (b *BaseApi) UpdateHTTPSConfig(c *gin.Context) {
 		helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
 		return
 	}
-	res, err := websiteService.OpWebsiteHTTPS(req)
+	tx, ctx := helper.GetTxAndContext()
+	res, err := websiteService.OpWebsiteHTTPS(ctx, req)
 	if err != nil {
+		tx.Rollback()
 		helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
 		return
 	}
+	tx.Commit()
 	helper.SuccessWithData(c, res)
 }
 
diff --git a/backend/app/dto/request/website.go b/backend/app/dto/request/website.go
index 08014a39b..37ba55532 100644
--- a/backend/app/dto/request/website.go
+++ b/backend/app/dto/request/website.go
@@ -102,6 +102,7 @@ type WebsiteHTTPSOp struct {
 	Type         string `json:"type"  validate:"oneof=existed auto manual"`
 	PrivateKey   string `json:"privateKey"`
 	Certificate  string `json:"certificate"`
+	HttpConfig   string `json:"HttpConfig"  validate:"oneof=HTTPSOnly HTTPAlso HTTPToHTTPS"`
 }
 
 type WebsiteNginxUpdate struct {
diff --git a/backend/app/dto/response/website.go b/backend/app/dto/response/website.go
index 4a5b95ca9..5580e7ea3 100644
--- a/backend/app/dto/response/website.go
+++ b/backend/app/dto/response/website.go
@@ -30,6 +30,7 @@ type WebsiteWafConfig struct {
 }
 
 type WebsiteHTTPS struct {
-	Enable bool             `json:"enable"`
-	SSL    model.WebsiteSSL `json:"SSL"`
+	Enable     bool             `json:"enable"`
+	HttpConfig string           `json:"httpConfig"`
+	SSL        model.WebsiteSSL `json:"SSL"`
 }
diff --git a/backend/app/model/website.go b/backend/app/model/website.go
index e8c8a4310..449278d69 100644
--- a/backend/app/model/website.go
+++ b/backend/app/model/website.go
@@ -10,6 +10,7 @@ type Website struct {
 	Alias          string          `gorm:"type:varchar(128);not null" json:"alias"`
 	Remark         string          `gorm:"type:longtext;" json:"remark"`
 	Status         string          `gorm:"type:varchar(64);not null" json:"status"`
+	HttpConfig     string          `gorm:"type:varchar(64);not null" json:"httpConfig"`
 	ExpireDate     time.Time       `json:"expireDate"`
 	AppInstallID   uint            `gorm:"type:integer" json:"appInstallId"`
 	WebsiteGroupID uint            `gorm:"type:integer" json:"webSiteGroupId"`
diff --git a/backend/app/service/website.go b/backend/app/service/website.go
index fcccf4ed5..5466c7154 100644
--- a/backend/app/service/website.go
+++ b/backend/app/service/website.go
@@ -44,7 +44,7 @@ type IWebsiteService interface {
 	UpdateNginxConfigByScope(req request.NginxConfigUpdate) error
 	GetWebsiteNginxConfig(websiteId uint) (response.FileInfo, error)
 	GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS, error)
-	OpWebsiteHTTPS(req request.WebsiteHTTPSOp) (response.WebsiteHTTPS, error)
+	OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (response.WebsiteHTTPS, error)
 	PreInstallCheck(req request.WebsiteInstallCheckReq) ([]response.WebsitePreInstallCheck, error)
 	GetWafConfig(req request.WebsiteWafReq) (response.WebsiteWafConfig, error)
 	UpdateWafConfig(req request.WebsiteWafUpdate) error
@@ -472,84 +472,78 @@ func (w WebsiteService) GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS,
 	}
 	res.SSL = websiteSSL
 	res.Enable = true
+	if website.HttpConfig != "" {
+		res.HttpConfig = website.HttpConfig
+	} else {
+		res.HttpConfig = constant.HTTPToHTTPS
+	}
 	return res, nil
 }
 
-func (w WebsiteService) OpWebsiteHTTPS(req request.WebsiteHTTPSOp) (response.WebsiteHTTPS, error) {
+func (w WebsiteService) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (response.WebsiteHTTPS, error) {
 	website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
 	if err != nil {
 		return response.WebsiteHTTPS{}, err
 	}
-
 	var (
 		res        response.WebsiteHTTPS
 		websiteSSL model.WebsiteSSL
 	)
 	res.Enable = req.Enable
-
-	if req.Type == constant.SSLExisted {
-		websiteSSL, err = websiteSSLRepo.GetFirst(commonRepo.WithByID(req.WebsiteSSLID))
-		if err != nil {
-			return response.WebsiteHTTPS{}, err
+	if req.Enable {
+		if req.Type == constant.SSLExisted {
+			websiteSSL, err = websiteSSLRepo.GetFirst(commonRepo.WithByID(req.WebsiteSSLID))
+			if err != nil {
+				return response.WebsiteHTTPS{}, err
+			}
+			website.WebsiteSSLID = websiteSSL.ID
+			res.SSL = websiteSSL
 		}
-		website.WebsiteSSLID = websiteSSL.ID
-		if err := websiteRepo.Save(context.TODO(), &website); err != nil {
-			return response.WebsiteHTTPS{}, err
-		}
-		res.SSL = websiteSSL
-	}
-
-	if req.Type == constant.SSLManual {
-		certBlock, _ := pem.Decode([]byte(req.Certificate))
-		cert, err := x509.ParseCertificate(certBlock.Bytes)
-		if err != nil {
-			return response.WebsiteHTTPS{}, err
-		}
-		websiteSSL.ExpireDate = cert.NotAfter
-		websiteSSL.StartDate = cert.NotBefore
-		websiteSSL.Type = cert.Issuer.CommonName
-		websiteSSL.Organization = cert.Issuer.Organization[0]
-		websiteSSL.PrimaryDomain = cert.Subject.CommonName
-		if len(cert.Subject.Names) > 0 {
-			var domains []string
-			for _, name := range cert.Subject.Names {
-				if v, ok := name.Value.(string); ok {
-					if v != cert.Subject.CommonName {
-						domains = append(domains, v)
+		if req.Type == constant.SSLManual {
+			certBlock, _ := pem.Decode([]byte(req.Certificate))
+			cert, err := x509.ParseCertificate(certBlock.Bytes)
+			if err != nil {
+				return response.WebsiteHTTPS{}, err
+			}
+			websiteSSL.ExpireDate = cert.NotAfter
+			websiteSSL.StartDate = cert.NotBefore
+			websiteSSL.Type = cert.Issuer.CommonName
+			websiteSSL.Organization = cert.Issuer.Organization[0]
+			websiteSSL.PrimaryDomain = cert.Subject.CommonName
+			if len(cert.Subject.Names) > 0 {
+				var domains []string
+				for _, name := range cert.Subject.Names {
+					if v, ok := name.Value.(string); ok {
+						if v != cert.Subject.CommonName {
+							domains = append(domains, v)
+						}
 					}
 				}
+				if len(domains) > 0 {
+					websiteSSL.Domains = strings.Join(domains, "")
+				}
 			}
-			if len(domains) > 0 {
-				websiteSSL.Domains = strings.Join(domains, "")
-			}
+			websiteSSL.Provider = constant.Manual
+			websiteSSL.PrivateKey = req.PrivateKey
+			websiteSSL.Pem = req.Certificate
+			res.SSL = websiteSSL
 		}
-
-		websiteSSL.Provider = constant.Manual
-		websiteSSL.PrivateKey = req.PrivateKey
-		websiteSSL.Pem = req.Certificate
-
-		res.SSL = websiteSSL
-	}
-
-	if req.Enable {
 		website.Protocol = constant.ProtocolHTTPS
-		if err := applySSL(website, websiteSSL); err != nil {
+		if err := applySSL(website, websiteSSL, req.HttpConfig); err != nil {
 			return response.WebsiteHTTPS{}, err
 		}
+		website.HttpConfig = req.HttpConfig
 	} else {
 		website.Protocol = constant.ProtocolHTTP
 		website.WebsiteSSLID = 0
-
 		if err := deleteListenAndServerName(website, []int{443}, []string{}); err != nil {
 			return response.WebsiteHTTPS{}, err
 		}
-
 		if err := deleteNginxConfig(constant.NginxScopeServer, getKeysFromStaticFile(dto.SSL), &website); err != nil {
 			return response.WebsiteHTTPS{}, err
 		}
 	}
 
-	tx, ctx := getTxAndContext()
 	if websiteSSL.ID == 0 {
 		if err := websiteSSLRepo.Create(ctx, &websiteSSL); err != nil {
 			return response.WebsiteHTTPS{}, err
@@ -559,8 +553,6 @@ func (w WebsiteService) OpWebsiteHTTPS(req request.WebsiteHTTPSOp) (response.Web
 	if err := websiteRepo.Save(ctx, &website); err != nil {
 		return response.WebsiteHTTPS{}, err
 	}
-
-	tx.Commit()
 	return res, nil
 }
 
diff --git a/backend/app/service/website_utils.go b/backend/app/service/website_utils.go
index d2914d4b0..a806bc890 100644
--- a/backend/app/service/website_utils.go
+++ b/backend/app/service/website_utils.go
@@ -298,7 +298,7 @@ func createPemFile(website model.Website, websiteSSL model.WebsiteSSL) error {
 	return nil
 }
 
-func applySSL(website model.Website, websiteSSL model.WebsiteSSL) error {
+func applySSL(website model.Website, websiteSSL model.WebsiteSSL, httpConfig string) error {
 	nginxFull, err := getNginxFull(&website)
 	if err != nil {
 		return nil
@@ -306,10 +306,22 @@ func applySSL(website model.Website, websiteSSL model.WebsiteSSL) error {
 	config := nginxFull.SiteConfig.Config
 	server := config.FindServers()[0]
 	server.UpdateListen("443", false, "ssl")
+
+	switch httpConfig {
+	case constant.HTTPSOnly:
+		server.RemoveListenByBind("80")
+		server.RemoveDirective("if", []string{"($scheme"})
+	case constant.HTTPToHTTPS:
+		server.UpdateListen("80", false)
+		server.AddHTTP2HTTPS()
+	case constant.HTTPAlso:
+		server.UpdateListen("80", false)
+		server.RemoveDirective("if", []string{"($scheme"})
+	}
+
 	if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
 		return err
 	}
-
 	if err := createPemFile(website, websiteSSL); err != nil {
 		return err
 	}
@@ -325,7 +337,6 @@ func applySSL(website model.Website, websiteSSL model.WebsiteSSL) error {
 	if err := updateNginxConfig(constant.NginxScopeServer, nginxParams, &website); err != nil {
 		return err
 	}
-
 	return nil
 }
 
diff --git a/backend/constant/website.go b/backend/constant/website.go
index 22e1de807..d8282f7b2 100644
--- a/backend/constant/website.go
+++ b/backend/constant/website.go
@@ -28,4 +28,8 @@ const (
 
 	StartWeb = "start"
 	StopWeb  = "stop"
+
+	HTTPSOnly   = "HTTPSOnly"
+	HTTPAlso    = "HTTPAlso"
+	HTTPToHTTPS = "HTTPToHTTPS"
 )
diff --git a/backend/utils/nginx/components/server.go b/backend/utils/nginx/components/server.go
index cdb3b89ec..1fd2b010f 100644
--- a/backend/utils/nginx/components/server.go
+++ b/backend/utils/nginx/components/server.go
@@ -242,13 +242,26 @@ func (s *Server) UpdateDirectiveBySecondKey(name string, key string, directive D
 }
 
 func (s *Server) RemoveListenByBind(bind string) {
-	index := 0
-	listens := s.Listens
+	var listens []*ServerListen
 	for _, listen := range s.Listens {
 		if listen.Bind != bind || len(listen.Parameters) > 0 {
-			listens[index] = listen
-			index++
+			listens = append(listens, listen)
 		}
 	}
 	s.Listens = listens
 }
+
+func (s *Server) AddHTTP2HTTPS() {
+	newDir := Directive{
+		Name:       "if",
+		Parameters: []string{"($scheme = http)"},
+		Block:      &Block{},
+	}
+	block := &Block{}
+	block.Directives = append(block.Directives, &Directive{
+		Name:       "return",
+		Parameters: []string{"301", "https://$host$request_uri"},
+	})
+	newDir.Block = block
+	s.UpdateDirectiveBySecondKey("if", "($scheme", newDir)
+}
diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts
index f55c1b0bd..59655e7d3 100644
--- a/frontend/src/api/interface/website.ts
+++ b/frontend/src/api/interface/website.ts
@@ -204,11 +204,13 @@ export namespace Website {
         type: string;
         certificate?: string;
         privateKey?: string;
+        httpConfig: string;
     }
 
     export interface HTTPSConfig {
         enable: boolean;
         SSL: SSL;
+        httpConfig: string;
     }
 
     export interface CheckReq {
diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts
index f6ae1149b..b67595ca6 100644
--- a/frontend/src/lang/modules/zh.ts
+++ b/frontend/src/lang/modules/zh.ts
@@ -921,6 +921,16 @@ export default {
         logFoler: '网站日志',
         sslFolder: '网站证书',
         enableOrNot: '是否启用',
+        oldSSL: '选择已有证书',
+        manualSSL: '手动导入证书',
+        select: '选择',
+        selectSSL: '选择证书',
+        privateKey: '密钥代码(pem格式)',
+        certificate: '密钥代码(pem格式)',
+        HTTPConfig: 'HTTP选项',
+        HTTPSOnly: '禁止HTTTP',
+        HTTPToHTTPS: '访问HTTP自动跳转到HTTPS',
+        HTTPAlso: 'HTTP可直接访问',
     },
     nginx: {
         serverNamesHashBucketSizeHelper: '服务器名字的hash表大小',
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 03d321a81..fcb996a63 100644
--- a/frontend/src/views/website/website/config/basic/https/index.vue
+++ b/frontend/src/views/website/website/config/basic/https/index.vue
@@ -9,22 +9,29 @@
                 :rules="rules"
                 :loading="loading"
             >
-                <el-form-item prop="websiteSSLId">
+                <el-form-item prop="enable">
                     <el-checkbox v-model="form.enable">
                         {{ $t('website.enableHTTPS') }}
                     </el-checkbox>
                 </el-form-item>
+                <el-form-item :label="$t('website.HTTPConfig')" prop="httpConfig">
+                    <el-select v-model="form.httpConfig" style="width: 240px">
+                        <el-option :label="$t('website.HTTPToHTTPS')" :value="'HTTPToHTTPS'"></el-option>
+                        <el-option :label="$t('website.HTTPAlso')" :value="'HTTPAlso'"></el-option>
+                        <el-option :label="$t('website.HTTPSOnly')" :value="'HTTPSOnly'"></el-option>
+                    </el-select>
+                </el-form-item>
                 <el-form-item :label="$t('website.ssl')" prop="type">
-                    <el-select v-model="form.type" @change="changeType()">
-                        <el-option :label="'选择已有证书'" :value="'existed'"></el-option>
-                        <el-option :label="'手动导入证书'" :value="'manual'"></el-option>
+                    <el-select v-model="form.type" @change="changeType(form.type)">
+                        <el-option :label="$t('website.oldSSL')" :value="'existed'"></el-option>
+                        <el-option :label="$t('website.manualSSL')" :value="'manual'"></el-option>
                         <!-- <el-option :label="'自动生成证书'" :value="'auto'"></el-option> -->
                     </el-select>
                 </el-form-item>
-                <el-form-item :label="' '" prop="websiteSSLId" v-if="form.type === 'existed'">
+                <el-form-item :label="$t('website.select')" prop="websiteSSLId" v-if="form.type === 'existed'">
                     <el-select
                         v-model="form.websiteSSLId"
-                        placeholder="选择证书"
+                        :placeholder="$t('website.selectSSL')"
                         @change="changeSSl(form.websiteSSLId)"
                     >
                         <el-option
@@ -36,18 +43,22 @@
                     </el-select>
                 </el-form-item>
                 <div v-if="form.type === 'manual'">
-                    <el-form-item :label="'密钥代码(pem格式)'" prop="privateKey">
+                    <el-form-item :label="$t('website.privateKey')" prop="privateKey">
                         <el-input v-model="form.privateKey" :rows="6" type="textarea" />
                     </el-form-item>
-                    <el-form-item :label="'证书代码(pem格式)'" prop="certificate">
+                    <el-form-item :label="$t('website.certificate')" prop="certificate">
                         <el-input v-model="form.certificate" :rows="6" type="textarea" />
                     </el-form-item>
                 </div>
                 <el-form-item :label="' '" v-if="websiteSSL && websiteSSL.id > 0">
                     <el-descriptions :column="3" border direction="vertical">
-                        <el-descriptions-item label="主域名">{{ websiteSSL.primaryDomain }}</el-descriptions-item>
-                        <el-descriptions-item label="备用域名">{{ websiteSSL.otherDomains }}</el-descriptions-item>
-                        <el-descriptions-item label="过期时间">
+                        <el-descriptions-item :label="$t('website.primaryDomain')">
+                            {{ websiteSSL.primaryDomain }}
+                        </el-descriptions-item>
+                        <el-descriptions-item :label="$t('website.otherDomains')">
+                            {{ websiteSSL.otherDomains }}
+                        </el-descriptions-item>
+                        <el-descriptions-item :label="$t('website.expireDate')">
                             {{ dateFromat(1, 1, websiteSSL.expireDate) }}
                         </el-descriptions-item>
                     </el-descriptions>
@@ -87,6 +98,7 @@ let form = reactive({
     type: 'existed',
     privateKey: '',
     certificate: '',
+    httpConfig: 'HTTPToHTTPS',
 });
 let loading = ref(false);
 const ssls = ref();
@@ -95,11 +107,14 @@ let rules = ref({
     type: [Rules.requiredSelect],
     privateKey: [Rules.requiredInput],
     certificate: [Rules.requiredInput],
+    websiteSSLId: [Rules.requiredSelect],
+    httpConfig: [Rules.requiredSelect],
 });
 
 const listSSL = () => {
     ListSSL({}).then((res) => {
         ssls.value = res.data;
+        changeSSl(form.websiteSSLId);
     });
 };
 
@@ -110,15 +125,19 @@ const changeSSl = (sslid: number) => {
     websiteSSL.value = res[0];
 };
 
-const changeType = () => {
-    websiteSSL.value = {};
-    form.websiteSSLId = undefined;
+const changeType = (type: string) => {
+    if (type != 'existed') {
+        websiteSSL.value = {};
+        form.websiteSSLId = undefined;
+    }
 };
 
 const get = () => {
     GetHTTPSConfig(id.value).then((res) => {
+        console.log(res);
         if (res.data) {
             form.enable = res.data.enable;
+            form.httpConfig = res.data.httpConfig;
         }
         if (res.data?.SSL && res.data?.SSL.id > 0) {
             form.websiteSSLId = res.data.SSL.id;