Explorar o código

更新依赖并添加 ACME 相关代码

更新了项目的依赖项,增加了对 `github.com/go-acme/lego/v4` 和其他相关库的支持。同时,新增了 ACME 模块,用于处理证书和私钥的获取与管理。
SongZihuan hai 3 meses
pai
achega
c30a444c46
Modificáronse 12 ficheiros con 651 adicións e 18 borrados
  1. 12 3
      go.mod
  2. 26 11
      go.sum
  3. 157 0
      src/acme/ctrl.go
  4. 8 0
      src/acme/filename.go
  5. 153 0
      src/acme/newcert.go
  6. 83 0
      src/acme/read.go
  7. 35 0
      src/acme/user.go
  8. 4 0
      src/flagparser/data.go
  9. 5 0
      src/flagparser/flag.go
  10. 126 0
      src/httpsslserver/server.go
  11. 27 4
      src/mainfunc/v1.go
  12. 15 0
      utils/string.go

+ 12 - 3
go.mod

@@ -2,23 +2,29 @@ module github.com/SongZihuan/Http-Demo
 
 go 1.23.2
 
-require github.com/gin-gonic/gin v1.10.0
+require (
+	github.com/gin-gonic/gin v1.10.0
+	github.com/go-acme/lego/v4 v4.21.0
+)
 
 require (
 	github.com/bytedance/sonic v1.11.6 // indirect
 	github.com/bytedance/sonic/loader v0.1.1 // indirect
+	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cloudwego/base64x v0.1.4 // indirect
 	github.com/cloudwego/iasm v0.2.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-jose/go-jose/v4 v4.0.4 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/validator/v10 v10.20.0 // indirect
-	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/goccy/go-json v0.10.4 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.7 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/miekg/dns v1.1.62 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
@@ -26,9 +32,12 @@ require (
 	github.com/ugorji/go/codec v1.2.12 // indirect
 	golang.org/x/arch v0.8.0 // indirect
 	golang.org/x/crypto v0.31.0 // indirect
+	golang.org/x/mod v0.22.0 // indirect
 	golang.org/x/net v0.33.0 // indirect
+	golang.org/x/sync v0.10.0 // indirect
 	golang.org/x/sys v0.28.0 // indirect
 	golang.org/x/text v0.21.0 // indirect
-	google.golang.org/protobuf v1.34.1 // indirect
+	golang.org/x/tools v0.28.0 // indirect
+	google.golang.org/protobuf v1.35.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 26 - 11
go.sum

@@ -2,19 +2,26 @@ github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc
 github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
 github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
 github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
 github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
 github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
 github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
 github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o=
+github.com/go-acme/lego/v4 v4.21.0/go.mod h1:HrSWzm3Ckj45Ie3i+p1zKVobbQoMOaGu9m4up0dUeDI=
+github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
+github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
 github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -23,10 +30,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
 github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
-github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
-github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
+github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -38,6 +45,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
+github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -45,8 +54,9 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -57,8 +67,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
@@ -68,18 +79,22 @@ golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
 golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
 golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
 golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
 golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
 golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
-google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
+golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
+google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
+google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 157 - 0
src/acme/ctrl.go

@@ -0,0 +1,157 @@
+package acme
+
+import (
+	"crypto"
+	"crypto/x509"
+	"fmt"
+	"github.com/SongZihuan/Http-Demo/utils"
+	"time"
+)
+
+func GetCertificateAndPrivateKey(dir string, email string, httpsAddress string, domain string) (crypto.PrivateKey, *x509.Certificate, error) {
+	if email == "" {
+		email = "no-reply@example.com"
+	}
+
+	if !utils.IsValidEmail(email) {
+		return nil, nil, fmt.Errorf("not a valid email")
+	}
+
+	if !utils.IsValidDomain(domain) {
+		return nil, nil, fmt.Errorf("not a valid domain")
+	}
+
+	privateKey, cert, err := ReadLocalCertificateAndPrivateKey(dir)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if checkCertWithDomain(cert, domain) && checkCertWithTime(cert, 5*24*time.Hour) {
+		return privateKey, cert, nil
+	}
+
+	privateKey, resource, err := newCert(email, httpsAddress, domain)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	err = writerWithDate(dir, resource)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	err = writer(dir, resource)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cert, err = getCert(resource)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return privateKey, cert, nil
+}
+
+type NewCert struct {
+	PrivateKey  crypto.PrivateKey
+	Certificate *x509.Certificate
+	Error       error
+}
+
+func WatchCertificateAndPrivateKey(dir string, email string, httpsAddress string, domain string, oldPrivateKey crypto.PrivateKey, oldCert *x509.Certificate, stopchan chan bool, newchan chan NewCert) error {
+	for {
+		select {
+		case <-stopchan:
+			newchan <- NewCert{
+				PrivateKey:  nil,
+				Certificate: nil,
+				Error:       nil,
+			}
+			close(stopchan)
+			return nil
+		default:
+			privateKey, cert, err := watchCertificateAndPrivateKey(dir, email, httpsAddress, domain, oldPrivateKey, oldCert)
+			if err != nil {
+				newchan <- NewCert{
+					Error: err,
+				}
+			} else if privateKey != nil || cert != nil {
+				newchan <- NewCert{
+					PrivateKey:  privateKey,
+					Certificate: cert,
+				}
+			}
+		}
+	}
+}
+
+func watchCertificateAndPrivateKey(dir string, email string, httpsAddress string, domain string, oldPrivateKey crypto.PrivateKey, oldCert *x509.Certificate) (crypto.PrivateKey, *x509.Certificate, error) {
+	if email == "" {
+		email = "no-reply@example.com"
+	}
+
+	if !utils.IsValidEmail(email) {
+		return nil, nil, fmt.Errorf("not a valid email")
+	}
+
+	if !utils.IsValidDomain(domain) {
+		return nil, nil, fmt.Errorf("not a valid domain")
+	}
+
+	if checkCertWithDomain(oldCert, domain) && checkCertWithTime(oldCert, 5*24*time.Hour) {
+		return nil, nil, nil
+	}
+
+	privateKey, resource, err := newCert(email, httpsAddress, domain)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	err = writerWithDate(dir, resource)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	err = writer(dir, resource)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	cert, err := getCert(resource)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return privateKey, cert, nil
+}
+
+func checkCertWithDomain(cert *x509.Certificate, domain string) bool {
+	// 遍历主题备用名称查找匹配的域名
+	for _, name := range cert.DNSNames {
+		if name == domain {
+			return true // 找到了匹配的域名
+		}
+	}
+
+	// 检查通用名作为回退,虽然现代实践倾向于使用SAN
+	if cert.Subject.CommonName != "" && cert.Subject.CommonName == domain {
+		return true // 通用名匹配
+	}
+
+	// 如果没有找到匹配,则返回错误
+	return false
+}
+
+func checkCertWithTime(cert *x509.Certificate, gracePeriod time.Duration) bool {
+	now := time.Now()
+	nowWithGracePeriod := now.Add(gracePeriod)
+
+	if now.Before(cert.NotBefore) {
+		return false
+	} else if nowWithGracePeriod.After(cert.NotBefore) {
+		return false
+	}
+
+	return false
+}

+ 8 - 0
src/acme/filename.go

@@ -0,0 +1,8 @@
+package acme
+
+const (
+	FilePrivateKey        = "private.key"
+	FileCertificate       = "cert.pem"
+	FileIssuerCertificate = "ca-cert.pem"
+	FileCSR               = "csr.pem"
+)

+ 153 - 0
src/acme/newcert.go

@@ -0,0 +1,153 @@
+package acme
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"github.com/go-acme/lego/v4/certcrypto"
+	"github.com/go-acme/lego/v4/certificate"
+	"github.com/go-acme/lego/v4/challenge/http01"
+	"github.com/go-acme/lego/v4/lego"
+	"github.com/go-acme/lego/v4/registration"
+	"net"
+	"os"
+	"path"
+	"time"
+)
+
+func newCert(email string, httpsAddress string, domain string) (crypto.PrivateKey, *certificate.Resource, error) {
+	privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	user := NewUser(email, privateKey)
+
+	config := lego.NewConfig(user)
+	config.Certificate.KeyType = certcrypto.RSA4096
+	config.Certificate.Timeout = 30 * 24 * time.Hour
+	client, err := lego.NewClient(config)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	iface, port, err := net.SplitHostPort(httpsAddress)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer(iface, port))
+	if err != nil {
+		return nil, nil, err
+	}
+
+	regOption := registration.RegisterOptions{
+		TermsOfServiceAgreed: true,
+	}
+
+	reg, err := client.Registration.Register(regOption)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	user.setRegistration(reg)
+
+	if domain == "" {
+		domain = iface
+	}
+
+	request := certificate.ObtainRequest{
+		Domains: []string{domain},
+		Bundle:  true,
+	}
+
+	certificates, err := client.Certificate.Obtain(request)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return privateKey, certificates, nil
+}
+
+func getCert(resource *certificate.Resource) (*x509.Certificate, error) {
+	block, _ := pem.Decode(resource.Certificate)
+	if block == nil || block.Type != "CERTIFICATE" {
+		return nil, fmt.Errorf("failed to decode PEM block containing certificate")
+	}
+
+	cert, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse certificate: %v", err)
+	}
+
+	return cert, nil
+}
+
+func writerWithDate(baseDir string, resource *certificate.Resource) error {
+	cert, err := getCert(resource)
+	if err != nil {
+		return err
+	}
+
+	domain := cert.Subject.CommonName
+	if domain == "" && len(cert.DNSNames) == 0 {
+		return fmt.Errorf("no domains in certificate")
+	}
+	domain = cert.DNSNames[0]
+
+	year := fmt.Sprintf("%d", cert.NotBefore.Year())
+	month := fmt.Sprintf("%d", cert.NotBefore.Month())
+	day := fmt.Sprintf("%d", cert.NotBefore.Day())
+
+	dir := path.Join(baseDir, domain, year, month, day)
+
+	err = os.WriteFile(path.Join(dir, FilePrivateKey), resource.PrivateKey, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(dir, FileCertificate), resource.Certificate, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(dir, FileIssuerCertificate), resource.IssuerCertificate, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(dir, FileCSR), resource.CSR, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func writer(dir string, resource *certificate.Resource) error {
+	err := os.WriteFile(path.Join(dir, FilePrivateKey), resource.PrivateKey, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(dir, FileCertificate), resource.Certificate, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(dir, FileIssuerCertificate), resource.IssuerCertificate, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	err = os.WriteFile(path.Join(dir, FileCSR), resource.CSR, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 83 - 0
src/acme/read.go

@@ -0,0 +1,83 @@
+package acme
+
+import (
+	"crypto"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"os"
+	"path"
+)
+
+func ReadLocalCertificateAndPrivateKey(dir string) (crypto.PrivateKey, *x509.Certificate, error) {
+	cert, err := ReadCertificate(dir)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	privateKey, err := ReadPrivateKey(dir)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return privateKey, cert, nil
+}
+
+func ReadCertificate(dir string) (*x509.Certificate, error) {
+	// 请替换为你的证书文件路径
+	certPath := path.Join(dir, FileCertificate)
+
+	// 读取PEM编码的证书文件
+	pemData, err := os.ReadFile(certPath)
+	if err != nil {
+		return nil, err
+	}
+
+	// 解析PEM编码的数据
+	block, _ := pem.Decode(pemData)
+	if block == nil || block.Type != "CERTIFICATE" {
+		return nil, fmt.Errorf("failed to decode PEM block containing certificate")
+	}
+
+	// 解析证书
+	cert, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse certificate: %v", err)
+	}
+
+	return cert, nil
+}
+
+func ReadPrivateKey(dir string) (crypto.PrivateKey, error) {
+	// 请替换为你的RSA私钥文件路径
+	keyPath := path.Join(dir, FilePrivateKey)
+
+	// 读取PEM编码的私钥文件
+	pemData, err := os.ReadFile(keyPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read key file: %v", err)
+	}
+
+	// 解析PEM块
+	block, _ := pem.Decode(pemData)
+	if block == nil {
+		return nil, fmt.Errorf("failed to decode PEM block containing private key")
+	}
+
+	// 根据PEM块类型解析私钥
+	var privateKey crypto.PrivateKey
+	if block.Type == "RSA PRIVATE KEY" {
+		privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
+	} else if block.Type == "EC PRIVATE KEY" {
+		privateKey, err = x509.ParseECPrivateKey(block.Bytes)
+	} else if block.Type == "PRIVATE KEY" {
+		privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
+	} else {
+		return nil, fmt.Errorf("unknown private key type: %s", block.Type)
+	}
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse private key: %v", err)
+	}
+
+	return privateKey, nil
+}

+ 35 - 0
src/acme/user.go

@@ -0,0 +1,35 @@
+package acme
+
+import (
+	"crypto"
+	"github.com/go-acme/lego/v4/registration"
+)
+
+type CertUser struct {
+	email        string
+	registration *registration.Resource
+	key          crypto.PrivateKey
+}
+
+func NewUser(email string, key crypto.PrivateKey) *CertUser {
+	return &CertUser{
+		email: email,
+		key:   key,
+	}
+}
+
+func (u *CertUser) GetEmail() string {
+	return u.email
+}
+
+func (u *CertUser) GetRegistration() *registration.Resource {
+	return u.registration
+}
+
+func (u *CertUser) setRegistration(res *registration.Resource) {
+	u.registration = res
+}
+
+func (u *CertUser) GetPrivateKey() crypto.PrivateKey {
+	return u.key
+}

+ 4 - 0
src/flagparser/data.go

@@ -1,3 +1,7 @@
 package flagparser
 
 var HttpAddress string = ":3366"
+var HttpsAddress string = ""
+var HttpsDomain = ""
+var HttpsEmail = ""
+var HttpsCertDir = "./ssl-certs"

+ 5 - 0
src/flagparser/flag.go

@@ -20,6 +20,11 @@ func InitFlag() (err error) {
 	flag.StringVar(&HttpAddress, "http-address", HttpAddress, "http server address")
 	flag.StringVar(&HttpAddress, "h", HttpAddress, "http server address")
 
+	flag.StringVar(&HttpsAddress, "https-address", HttpsAddress, "https server address")
+	flag.StringVar(&HttpsDomain, "https-domain", HttpsDomain, "https server domain")
+	flag.StringVar(&HttpsEmail, "https-email", HttpsEmail, "https cert email")
+	flag.StringVar(&HttpsCertDir, "https-cert-dir", HttpsCertDir, "https cert save dir")
+
 	flag.Parse()
 
 	return nil

+ 126 - 0
src/httpsslserver/server.go

@@ -0,0 +1,126 @@
+package httpsslserver
+
+import (
+	"context"
+	"crypto"
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+	"fmt"
+	"github.com/SongZihuan/Http-Demo/src/acme"
+	"github.com/SongZihuan/Http-Demo/src/engine"
+	"github.com/SongZihuan/Http-Demo/src/flagparser"
+	"net/http"
+	"sync"
+	"time"
+)
+
+var HttpSSLServer *http.Server = nil
+var HttpSSLAddress string
+var HttpSSLDomain string
+var HttpSSLEmail string
+var HttpSSLCertDir string
+
+var PrivateKey crypto.PrivateKey
+var Certificate *x509.Certificate
+
+var ErrStop = fmt.Errorf("http server error")
+var ReloadMutex sync.Mutex
+
+func InitHttpSSLServer() (err error) {
+	HttpSSLAddress = flagparser.HttpsAddress
+	HttpSSLDomain = flagparser.HttpsDomain
+	HttpSSLEmail = flagparser.HttpsEmail
+	HttpSSLCertDir = flagparser.HttpsCertDir
+
+	PrivateKey, Certificate, err = acme.GetCertificateAndPrivateKey(HttpSSLCertDir, HttpSSLEmail, HttpSSLAddress, HttpSSLDomain)
+	if err != nil {
+		return err
+	}
+
+	return initHttpSSLServer()
+}
+
+func initHttpSSLServer() (err error) {
+	tlsConfig := &tls.Config{
+		Certificates: []tls.Certificate{{
+			Certificate: [][]byte{Certificate.Raw}, // Raw包含 DER 编码的证书
+			PrivateKey:  PrivateKey,
+			Leaf:        Certificate,
+		}},
+	}
+
+	HttpSSLServer = &http.Server{
+		Addr:      HttpSSLAddress,
+		Handler:   engine.Engine,
+		TLSConfig: tlsConfig,
+	}
+
+	return nil
+}
+
+func RunServer() error {
+	stopchan := make(chan bool)
+	WatchCert(stopchan)
+	err := runServer()
+	stopchan <- true
+	return err
+}
+
+func runServer() error {
+	fmt.Printf("https server start at %s\n", HttpSSLAddress)
+ListenCycle:
+	for {
+		err := HttpSSLServer.ListenAndServeTLS("", "")
+		if err != nil && errors.Is(err, http.ErrServerClosed) {
+			if ReloadMutex.TryLock() {
+				ReloadMutex.Unlock()
+				return ErrStop
+			}
+			ReloadMutex.Lock()
+			ReloadMutex.Unlock() // 等待证书更换完毕
+			continue ListenCycle
+		} else if err != nil {
+			return err
+		}
+	}
+}
+
+func WatchCert(stopchan chan bool) {
+	newchan := make(chan acme.NewCert)
+
+	go func() {
+		err := acme.WatchCertificateAndPrivateKey(HttpSSLCertDir, HttpSSLEmail, HttpSSLAddress, HttpSSLDomain, PrivateKey, Certificate, stopchan, newchan)
+		if err != nil {
+			fmt.Printf("watch cert error: %s", err.Error())
+		}
+	}()
+
+	go func() {
+		select {
+		case res := <-newchan:
+			if res.Certificate == nil && res.PrivateKey == nil && res.Error == nil {
+				close(newchan)
+				return
+			} else if res.Error != nil {
+				fmt.Printf("watch cert error: %s", res.Error.Error())
+			} else if res.PrivateKey == nil && res.Certificate == nil {
+				func() {
+					ReloadMutex.Lock()
+					defer ReloadMutex.Unlock()
+
+					ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+					defer cancel()
+
+					err := HttpSSLServer.Shutdown(ctx)
+					if err != nil {
+						fmt.Printf("reload error: %s", err.Error())
+					}
+
+					PrivateKey = res.PrivateKey
+					Certificate = res.Certificate
+				}()
+			}
+		}
+	}()
+}

+ 27 - 4
src/mainfunc/v1.go

@@ -6,6 +6,7 @@ import (
 	"github.com/SongZihuan/Http-Demo/src/engine"
 	"github.com/SongZihuan/Http-Demo/src/flagparser"
 	"github.com/SongZihuan/Http-Demo/src/httpserver"
+	"github.com/SongZihuan/Http-Demo/src/httpsslserver"
 	"github.com/SongZihuan/Http-Demo/src/signalchan"
 )
 
@@ -22,32 +23,54 @@ func MainV1() (exitcode int) {
 		return 1
 	}
 
-	err = httpserver.InitHttpServer()
+	err = signalchan.InitSignal()
 	if err != nil {
+		fmt.Printf("init signal fail: %s\n", err.Error())
 		return 1
 	}
 
-	err = signalchan.InitSignal()
+	err = httpserver.InitHttpServer()
 	if err != nil {
 		fmt.Printf("init http server fail: %s\n", err.Error())
 		return 1
 	}
 
 	var httpchan = make(chan error)
+	var httpsslchan = make(chan error)
+
 	go func() {
 		httpchan <- httpserver.RunServer()
 	}()
 
+	if flagparser.HttpsAddress != "" {
+		err = httpsslserver.InitHttpSSLServer()
+		if err != nil {
+			fmt.Printf("init https server fail: %s\n", err.Error())
+			return 1
+		}
+
+		go func() {
+			httpsslchan <- httpsslserver.RunServer()
+		}()
+	}
+
 	select {
 	case <-signalchan.SignalChan:
 		fmt.Printf("Server closed: safe\n")
 		return 0
 	case err := <-httpchan:
 		if errors.Is(err, httpserver.ErrStop) {
-			fmt.Printf("Server closed: safe\n")
+			fmt.Printf("Http Server closed: safe\n")
+			return 0
+		}
+		fmt.Printf("Http Server error closed: %s\n", err.Error())
+		return 1
+	case err := <-httpsslchan:
+		if errors.Is(err, httpserver.ErrStop) {
+			fmt.Printf("Https Server closed: safe\n")
 			return 0
 		}
-		fmt.Printf("Server error closed: %s\n", err.Error())
+		fmt.Printf("Https Server error closed: %s\n", err.Error())
 		return 1
 	}
 }

+ 15 - 0
utils/string.go

@@ -0,0 +1,15 @@
+package utils
+
+import "regexp"
+
+func IsValidEmail(email string) bool {
+	pattern := `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`
+	matched, _ := regexp.MatchString(pattern, email)
+	return matched
+}
+
+func IsValidDomain(domain string) bool {
+	pattern := `^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$`
+	matched, _ := regexp.MatchString(pattern, domain)
+	return matched
+}